CE-1115 checkpoint on report & pivotTable setup widgets:

- refactor into sub-components
- working drag & drop
- more help content
- disable things rather than alert if no table
This commit is contained in:
2024-04-11 10:11:43 -05:00
parent cdec98afd8
commit cb7fa641eb
8 changed files with 988 additions and 279 deletions

View File

@ -165,33 +165,38 @@ export class HeaderIcon extends LabelComponent
/*******************************************************************************
**
** a link (actually a button) for in a widget's header
*******************************************************************************/
export class HeaderLinkButton extends LabelComponent
interface HeaderLinkButtonComponentProps
{
label: string;
onClickCallback: () => void;
constructor(label: string, onClickCallback: () => void)
{
super();
this.label = label;
this.onClickCallback = onClickCallback;
}
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
return (
<Button onClick={() => this.onClickCallback()} sx={{p: 0}} disableRipple>
<Typography display="inline" textTransform="none" fontSize={"1.125rem"}>
{this.label}
</Typography>
</Button>
);
};
disabled?: boolean;
disabledTooltip?: string;
}
HeaderLinkButtonComponent.defaultProps = {
disabled: false,
disabledTooltip: null
};
export function HeaderLinkButtonComponent({label, onClickCallback, disabled, disabledTooltip}: HeaderLinkButtonComponentProps): JSX.Element
{
return (
<Tooltip title={disabledTooltip}>
<span>
<Button disabled={disabled} onClick={() => onClickCallback()} sx={{p: 0}} disableRipple>
<Typography display="inline" textTransform="none" fontSize={"1.125rem"}>
{label}
</Typography>
</Button>
</span>
</Tooltip>
);
}
/*******************************************************************************
**
@ -205,8 +210,6 @@ interface HeaderToggleComponentProps
export function HeaderToggleComponent({label, getValue, onClickCallback}: HeaderToggleComponentProps): JSX.Element
{
console.log(`@dk in HTComponent, getValue(): ${getValue()}`);
const onClick = () =>
{
onClickCallback();

View File

@ -0,0 +1,204 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon";
import type {Identifier, XYCoord} from "dnd-core";
import colors from "qqq/assets/theme/base/colors";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import {DragItemTypes, fieldAutoCompleteTextFieldSX, getSelectedFieldForAutoComplete, xIconButtonSX} from "qqq/components/widgets/misc/PivotTableSetupWidget";
import {PivotTableDefinition, PivotTableGroupBy} from "qqq/models/misc/PivotTableDefinitionModels";
import React, {FC, useRef} from "react";
import {useDrag, useDrop} from "react-dnd";
/*******************************************************************************
** component props
*******************************************************************************/
export interface PivotTableGroupByElementProps
{
id: string;
index: number;
dragCallback: (rowsOrColumns: "rows" | "columns", dragIndex: number, hoverIndex: number) => void;
metaData: QInstance;
tableMetaData: QTableMetaData;
pivotTableDefinition: PivotTableDefinition;
usedGroupByFieldNames: string[];
availableFieldNames: string[];
isEditable: boolean;
groupBy: PivotTableGroupBy;
rowsOrColumns: "rows" | "columns";
callback: () => void;
}
/*******************************************************************************
** item to support react-dnd
*******************************************************************************/
interface DragItem
{
index: number;
id: string;
type: string;
}
/*******************************************************************************
**
*******************************************************************************/
export const PivotTableGroupByElement: FC<PivotTableGroupByElementProps> = ({id, index, dragCallback, rowsOrColumns, metaData, tableMetaData, pivotTableDefinition, groupBy, usedGroupByFieldNames, availableFieldNames, isEditable, callback}) =>
{
////////////////////////////////////////////////////////////////////////////
// credit: https://react-dnd.github.io/react-dnd/examples/sortable/simple //
////////////////////////////////////////////////////////////////////////////
const ref = useRef<HTMLDivElement>(null);
const [{handlerId}, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>(
{
accept: rowsOrColumns == "rows" ? DragItemTypes.ROW : DragItemTypes.COLUMN,
collect(monitor)
{
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item: DragItem, monitor)
{
if (!ref.current)
{
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex)
{
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY)
{
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY)
{
return;
}
// Time to actually perform the action
dragCallback(rowsOrColumns, dragIndex, hoverIndex);
// Note: we're mutating the monitor item here! Generally it's better to avoid mutations,
// but it's good here for the sake of performance to avoid expensive index searches.
item.index = hoverIndex;
},
});
const [{isDragging}, drag, preview] = useDrag({
type: rowsOrColumns == "rows" ? DragItemTypes.ROW : DragItemTypes.COLUMN,
item: () =>
{
return {id, index};
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
});
/*******************************************************************************
**
*******************************************************************************/
const handleFieldChange = (event: any, newValue: any, reason: string) =>
{
groupBy.fieldName = newValue ? newValue.fieldName : null;
callback();
};
/*******************************************************************************
**
*******************************************************************************/
function removeGroupBy(index: number, rowsOrColumns: "rows" | "columns")
{
pivotTableDefinition[rowsOrColumns].splice(index, 1);
callback();
}
if (!isEditable)
{
const selectedField = getSelectedFieldForAutoComplete(tableMetaData, groupBy.fieldName);
if (selectedField)
{
const label = selectedField.table.name == tableMetaData.name ? selectedField.field.label : selectedField.table.label + ": " + selectedField.field.label;
return (<Box mr="0.375rem" mb="0.5rem" border={`1px solid ${colors.grayLines.main}`} borderRadius="0.75rem" p="0.25rem 0.75rem">{label}</Box>);
}
return (<React.Fragment />);
}
preview(drop(ref));
return (<Box ref={ref} display="flex" p="0.5rem" pl="0" gap="0.5rem" alignItems="center" sx={{backgroundColor: "white", opacity: isDragging ? 0 : 1}} data-handler-id={handlerId}>
<Box>
<Icon ref={drag} sx={{cursor: "ns-resize"}}>drag_indicator</Icon>
</Box>
<Box width="100%">
<FieldAutoComplete
id={`${rowsOrColumns}-${index}`}
label={null}
variant="outlined"
textFieldSX={fieldAutoCompleteTextFieldSX}
metaData={metaData}
tableMetaData={tableMetaData}
handleFieldChange={handleFieldChange}
hiddenFieldNames={usedGroupByFieldNames}
availableFieldNames={availableFieldNames}
defaultValue={getSelectedFieldForAutoComplete(tableMetaData, groupBy.fieldName)}
/>
</Box>
<Box>
<Button sx={xIconButtonSX} onClick={() => removeGroupBy(index, rowsOrColumns)}><Icon>clear</Icon></Button>
</Box>
</Box>);
};

View File

@ -30,19 +30,88 @@ import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import {PivotTableGroupByElement} from "qqq/components/widgets/misc/PivotTableGroupByElement";
import {PivotTableValueElement} from "qqq/components/widgets/misc/PivotTableValueElement";
import Widget, {HeaderToggleComponent} from "qqq/components/widgets/Widget";
import {PivotObjectKey, PivotTableDefinition, PivotTableFunction, pivotTableFunctionLabels, PivotTableGroupBy, PivotTableValue} from "qqq/models/misc/PivotTableDefinitionModels";
import QQueryColumns from "qqq/models/query/QQueryColumns";
import Client from "qqq/utils/qqq/Client";
import FilterUtils from "qqq/utils/qqq/FilterUtils";
import React, {useEffect, useReducer, useState} from "react";
import React, {useCallback, useContext, useEffect, useReducer, useState} from "react";
import {DndProvider} from "react-dnd";
import {HTML5Backend} from "react-dnd-html5-backend";
///////////////////////////////////////////////////////////////////////////////
// put a unique key value in all the pivot table group-by and value objects, //
// to help react rendering be sane. //
///////////////////////////////////////////////////////////////////////////////
let pivotObjectKey = new Date().getTime();
export const DragItemTypes =
{
ROW: "row",
COLUMN: "column",
VALUE: "value"
};
export const buttonSX =
{
border: `1px solid ${colors.grayLines.main} !important`,
borderRadius: "0.75rem",
textTransform: "none",
fontSize: "1rem",
fontWeight: "400",
width: "160px",
paddingLeft: 0,
paddingRight: 0,
color: colors.dark.main,
"&:hover": {color: colors.dark.main},
"&:focus": {color: colors.dark.main},
"&:focus:not(:hover)": {color: colors.dark.main},
};
export const xIconButtonSX =
{
border: `1px solid ${colors.grayLines.main} !important`,
borderRadius: "0.75rem",
textTransform: "none",
fontSize: "1rem",
fontWeight: "400",
width: "40px",
minWidth: "40px",
paddingLeft: 0,
paddingRight: 0,
color: colors.error.main,
"&:hover": {color: colors.error.main},
"&:focus": {color: colors.error.main},
"&:focus:not(:hover)": {color: colors.error.main},
};
export const fieldAutoCompleteTextFieldSX =
{
"& .MuiInputBase-input": {fontSize: "1rem", padding: "0 !important"}
};
/*******************************************************************************
**
*******************************************************************************/
export function getSelectedFieldForAutoComplete(tableMetaData: QTableMetaData, fieldName: string)
{
if (fieldName)
{
let [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName);
if (field && fieldTable)
{
return ({field: field, table: fieldTable, fieldName: fieldName});
}
}
return (null);
}
/*******************************************************************************
** component props
*******************************************************************************/
interface PivotTableSetupWidgetProps
{
isEditable: boolean;
@ -51,71 +120,14 @@ interface PivotTableSetupWidgetProps
onSaveCallback?: (values: { [name: string]: any }) => void;
}
/*******************************************************************************
** default values for props
*******************************************************************************/
PivotTableSetupWidget.defaultProps = {
onSaveCallback: null
};
export class PivotTableDefinition
{
rows: PivotTableGroupBy[];
columns: PivotTableGroupBy[];
values: PivotTableValue[];
}
export class PivotTableGroupBy
{
fieldName: string;
key: number;
constructor()
{
this.key = pivotObjectKey++;
}
}
export class PivotTableValue
{
fieldName: string;
function: PivotTableFunction;
key: number;
constructor()
{
this.key = pivotObjectKey++;
}
}
enum PivotTableFunction
{
AVERAGE = "AVERAGE",
COUNT = "COUNT",
COUNT_NUMS = "COUNT_NUMS",
MAX = "MAX",
MIN = "MIN",
PRODUCT = "PRODUCT",
STD_DEV = "STD_DEV",
STD_DEVP = "STD_DEVP",
SUM = "SUM",
VAR = "VAR",
VARP = "VARP",
}
const pivotTableFunctionLabels =
{
"AVERAGE": "Average",
"COUNT": "Count Values (COUNTA)",
"COUNT_NUMS": "Count Numbers (COUNT)",
"MAX": "Max",
"MIN": "Min",
"PRODUCT": "Product",
"STD_DEV": "StdDev",
"STD_DEVP": "StdDevp",
"SUM": "Sum",
"VAR": "Var",
"VARP": "Varp"
};
const qController = Client.getInstance();
@ -134,54 +146,62 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
const [pivotTableDefinition, setPivotTableDefinition] = useState(null as PivotTableDefinition);
const [usedGroupByFieldNames, setUsedGroupByFieldNames] = useState([] as string[]);
const [availableFieldNames, setAvailableFieldNames] = useState([] as string[]);
const {helpHelpActive} = useContext(QContext);
//////////////////
// initial load //
//////////////////
useEffect(() =>
{
(async () =>
if (!pivotTableDefinition)
{
if (!pivotTableDefinition)
let originalPivotTableDefinition = recordValues["pivotTableJson"] && JSON.parse(recordValues["pivotTableJson"]) as PivotTableDefinition;
if (originalPivotTableDefinition)
{
let originalPivotTableDefinition = recordValues["pivotTableJson"] && JSON.parse(recordValues["pivotTableJson"]) as PivotTableDefinition;
if (originalPivotTableDefinition)
{
setEnabled(true);
}
else if (!originalPivotTableDefinition)
{
originalPivotTableDefinition = new PivotTableDefinition();
}
for (let i = 0; i < originalPivotTableDefinition?.rows?.length; i++)
{
if (!originalPivotTableDefinition?.rows[i].key)
{
originalPivotTableDefinition.rows[i].key = pivotObjectKey++;
}
}
for (let i = 0; i < originalPivotTableDefinition?.columns?.length; i++)
{
if (!originalPivotTableDefinition?.columns[i].key)
{
originalPivotTableDefinition.columns[i].key = pivotObjectKey++;
}
}
for (let i = 0; i < originalPivotTableDefinition?.values?.length; i++)
{
if (!originalPivotTableDefinition?.values[i].key)
{
originalPivotTableDefinition.values[i].key = pivotObjectKey++;
}
}
setPivotTableDefinition(originalPivotTableDefinition);
setEnabled(true);
}
else if (!originalPivotTableDefinition)
{
originalPivotTableDefinition = new PivotTableDefinition();
}
for (let i = 0; i < originalPivotTableDefinition?.rows?.length; i++)
{
if (!originalPivotTableDefinition?.rows[i].key)
{
originalPivotTableDefinition.rows[i].key = PivotObjectKey.next();
}
}
for (let i = 0; i < originalPivotTableDefinition?.columns?.length; i++)
{
if (!originalPivotTableDefinition?.columns[i].key)
{
originalPivotTableDefinition.columns[i].key = PivotObjectKey.next();
}
}
for (let i = 0; i < originalPivotTableDefinition?.values?.length; i++)
{
if (!originalPivotTableDefinition?.values[i].key)
{
originalPivotTableDefinition.values[i].key = PivotObjectKey.next();
}
}
setPivotTableDefinition(originalPivotTableDefinition);
updateUsedGroupByFieldNames(originalPivotTableDefinition);
}
if(recordValues["columnsJson"])
{
updateAvailableFieldNames(JSON.parse(recordValues["columnsJson"]) as QQueryColumns)
}
(async () =>
{
setMetaData(await qController.loadMetaData());
})();
});
@ -202,6 +222,27 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
}, [recordValues]);
const helpRoles = isEditable ? [recordValues["id"] ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"] : ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
/*******************************************************************************
**
*******************************************************************************/
function showHelp(slot: string)
{
return (helpHelpActive || hasHelpContent(widgetMetaData?.helpContent?.get(slot), helpRoles));
}
/*******************************************************************************
**
*******************************************************************************/
function getHelpContent(slot: string)
{
const key = `widget:${widgetMetaData.name};slot:${slot}`;
return <HelpContent helpContents={widgetMetaData?.helpContent?.get(slot)} roles={helpRoles} helpContentKey={key} />;
}
/*******************************************************************************
**
*******************************************************************************/
@ -241,9 +282,8 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
/*******************************************************************************
**
*******************************************************************************/
function removeGroupBy(index: number, rowsOrColumns: "rows" | "columns")
function groupByChangedCallback()
{
pivotTableDefinition[rowsOrColumns].splice(index, 1);
onSaveCallback({pivotTableJson: JSON.stringify(pivotTableDefinition)});
updateUsedGroupByFieldNames();
forceUpdate();
@ -277,130 +317,41 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
}
const buttonSX =
{
border: `1px solid ${colors.grayLines.main} !important`,
borderRadius: "0.75rem",
textTransform: "none",
fontSize: "1rem",
fontWeight: "400",
width: "160px",
paddingLeft: 0,
paddingRight: 0,
color: colors.dark.main,
"&:hover": {color: colors.dark.main},
"&:focus": {color: colors.dark.main},
"&:focus:not(:hover)": {color: colors.dark.main},
};
const xIconButtonSX =
{
border: `1px solid ${colors.grayLines.main} !important`,
borderRadius: "0.75rem",
textTransform: "none",
fontSize: "1rem",
fontWeight: "400",
width: "40px",
minWidth: "40px",
paddingLeft: 0,
paddingRight: 0,
color: colors.error.main,
"&:hover": {color: colors.error.main},
"&:focus": {color: colors.error.main},
"&:focus:not(:hover)": {color: colors.error.main},
};
const fieldAutoCompleteTextFieldSX =
{
"& .MuiInputBase-input": {fontSize: "1rem", padding: "0 !important"}
};
/*******************************************************************************
**
*******************************************************************************/
function updateUsedGroupByFieldNames()
function updateUsedGroupByFieldNames(ptd: PivotTableDefinition = pivotTableDefinition)
{
const hiddenFieldNames: string[] = [];
const usedFieldNames: string[] = [];
for (let i = 0; i < pivotTableDefinition?.rows?.length; i++)
for (let i = 0; i < ptd?.rows?.length; i++)
{
hiddenFieldNames.push(pivotTableDefinition?.rows[i].fieldName);
usedFieldNames.push(ptd?.rows[i].fieldName);
}
for (let i = 0; i < pivotTableDefinition?.columns?.length; i++)
for (let i = 0; i < ptd?.columns?.length; i++)
{
hiddenFieldNames.push(pivotTableDefinition?.columns[i].fieldName);
usedFieldNames.push(ptd?.columns[i].fieldName);
}
setUsedGroupByFieldNames(hiddenFieldNames);
setUsedGroupByFieldNames(usedFieldNames);
}
/*******************************************************************************
**
*******************************************************************************/
function getSelectedFieldForAutoComplete(fieldName: string)
function updateAvailableFieldNames(columns: QQueryColumns)
{
if (fieldName)
const fieldNames: string[] = [];
for (let i = 0; i < columns?.columns?.length; i++)
{
let [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName);
if (field && fieldTable)
if(columns.columns[i].isVisible)
{
return ({field: field, table: fieldTable, fieldName: fieldName});
fieldNames.push(columns.columns[i].name);
}
}
return (null);
}
/*******************************************************************************
**
*******************************************************************************/
function renderOneGroupBy(groupBy: PivotTableGroupBy, index: number, rowsOrColumns: "rows" | "columns")
{
if(!isEditable)
{
const selectedField = getSelectedFieldForAutoComplete(groupBy.fieldName);
if(selectedField)
{
const label = selectedField.table.name == tableMetaData.name ? selectedField.field.label : selectedField.table.label + ": " + selectedField.field.label
return (<Box mr="0.375rem" mb="0.5rem" border={`1px solid ${colors.grayLines.main}`} borderRadius="0.75rem" p="0.25rem 0.75rem">{label}</Box>);
}
return (<React.Fragment />);
}
const handleFieldChange = (event: any, newValue: any, reason: string) =>
{
groupBy.fieldName = newValue ? newValue.fieldName : null;
onSaveCallback({pivotTableJson: JSON.stringify(pivotTableDefinition)});
updateUsedGroupByFieldNames();
};
// maybe cursor:grab (and then change to "grabbing")
return (<Box display="flex" p="0.5rem" pl="0" gap="0.5rem" alignItems="center">
<Box>
<Icon sx={{cursor: "ns-resize"}}>drag_indicator</Icon>
</Box>
<Box width="100%">
<FieldAutoComplete
id={`${rowsOrColumns}-${index}`}
label={null}
variant="outlined"
textFieldSX={fieldAutoCompleteTextFieldSX}
metaData={metaData}
tableMetaData={tableMetaData}
handleFieldChange={handleFieldChange}
hiddenFieldNames={usedGroupByFieldNames}
defaultValue={getSelectedFieldForAutoComplete(groupBy.fieldName)}
/>
</Box>
<Box>
<Button sx={xIconButtonSX} onClick={() => removeGroupBy(index, rowsOrColumns)}><Icon>clear</Icon></Button>
</Box>
</Box>);
setAvailableFieldNames(fieldNames);
}
@ -409,12 +360,12 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
*******************************************************************************/
function renderOneValue(value: PivotTableValue, index: number)
{
if(!isEditable)
if (!isEditable)
{
const selectedField = getSelectedFieldForAutoComplete(value.fieldName);
if(selectedField && value.function)
const selectedField = getSelectedFieldForAutoComplete(tableMetaData, value.fieldName);
if (selectedField && value.function)
{
const label = selectedField.table.name == tableMetaData.name ? selectedField.field.label : selectedField.table.label + ": " + selectedField.field.label
const label = selectedField.table.name == tableMetaData.name ? selectedField.field.label : selectedField.table.label + ": " + selectedField.field.label;
return (<Box mr="0.375rem" mb="0.5rem" border={`1px solid ${colors.grayLines.main}`} borderRadius="0.75rem" p="0.25rem 0.75rem">{pivotTableFunctionLabels[value.function]} of {label}</Box>);
}
@ -441,8 +392,8 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
const label = "" + pivotTableFunctionLabels[pivotTableFunctionKey];
const option = {id: pivotTableFunctionKey, label: label};
functionOptions.push(option);
if(option.id == value.function)
if (option.id == value.function)
{
defaultFunctionValue = option;
}
@ -462,7 +413,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
metaData={metaData}
tableMetaData={tableMetaData}
handleFieldChange={handleFieldChange}
defaultValue={getSelectedFieldForAutoComplete(value.fieldName)}
defaultValue={getSelectedFieldForAutoComplete(tableMetaData, value.fieldName)}
/>
</Box>
<Box width="330px">
@ -490,6 +441,36 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
}
/*******************************************************************************
** drag & drop callback to move one of the pivot-table group-bys (rows/columns)
*******************************************************************************/
const moveGroupBy = useCallback((rowsOrColumns: "rows" | "columns", dragIndex: number, hoverIndex: number) =>
{
const array = pivotTableDefinition[rowsOrColumns];
const dragItem = array[dragIndex];
array.splice(dragIndex, 1);
array.splice(hoverIndex, 0, dragItem);
onSaveCallback({pivotTableJson: JSON.stringify(pivotTableDefinition)});
forceUpdate();
}, [pivotTableDefinition]);
/*******************************************************************************
** drag & drop callback to move one of the pivot-table values
*******************************************************************************/
const moveValue = useCallback((dragIndex: number, hoverIndex: number) =>
{
const array = pivotTableDefinition.values;
const dragItem = array[dragIndex];
array.splice(dragIndex, 1);
array.splice(hoverIndex, 0, dragItem);
onSaveCallback({pivotTableJson: JSON.stringify(pivotTableDefinition)});
forceUpdate();
}, [pivotTableDefinition]);
/////////////////////////////////////////////////////////////
// add toggle component to widget header for editable mode //
/////////////////////////////////////////////////////////////
@ -501,19 +482,77 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
const selectTableFirstTooltipTitle = tableMetaData ? null : "You must select a table before you can set up a pivot table";
/*******************************************************************************
** render a group-by (row or column)
*******************************************************************************/
const renderGroupBy = useCallback(
(groupBy: PivotTableGroupBy, rowsOrColumns: "rows" | "columns", index: number) =>
{
return (
<PivotTableGroupByElement
key={groupBy.fieldName}
index={index}
id={`${groupBy.key}`}
dragCallback={moveGroupBy}
metaData={metaData}
tableMetaData={tableMetaData}
pivotTableDefinition={pivotTableDefinition}
usedGroupByFieldNames={usedGroupByFieldNames}
availableFieldNames={availableFieldNames}
isEditable={isEditable}
groupBy={groupBy}
rowsOrColumns={rowsOrColumns}
callback={groupByChangedCallback}
/>
);
},
[tableMetaData, usedGroupByFieldNames, availableFieldNames],
);
/*******************************************************************************
** render a pivot-table value (row or column)
*******************************************************************************/
const renderValue = useCallback(
(value: PivotTableValue, index: number) =>
{
return (
<PivotTableValueElement
key={value.key}
index={index}
id={`${value.key}`}
dragCallback={moveValue}
metaData={metaData}
tableMetaData={tableMetaData}
pivotTableDefinition={pivotTableDefinition}
availableFieldNames={availableFieldNames}
isEditable={isEditable}
value={value}
callback={groupByChangedCallback}
/>
);
},
[tableMetaData, usedGroupByFieldNames, availableFieldNames],
);
return (<Widget widgetMetaData={widgetMetaData} labelAdditionalElementsRight={labelAdditionalElementsRight}>
{enabled && pivotTableDefinition &&
<React.Fragment>
<Grid container spacing="16" >
<DndProvider backend={HTML5Backend}>
{
showHelp("sectionSubhead") &&
<Box color={colors.gray.main} pb={"0.5rem"} fontSize={"0.875rem"}>
{getHelpContent("sectionSubhead")}
</Box>
}
<Grid container spacing="16">
<Grid item lg={4} md={6} xs={12}>
<h5>Rows</h5>
<Box fontSize="1rem">
{
tableMetaData && pivotTableDefinition.rows?.map((row: PivotTableGroupBy, index: number) =>
(
<React.Fragment key={row.key}>{renderOneGroupBy(row, index, "rows")}</React.Fragment>
))
tableMetaData && (<div>{pivotTableDefinition?.rows?.map((row, i) => renderGroupBy(row, "rows", i))}</div>)
}
</Box>
{
@ -530,10 +569,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
<h5>Columns</h5>
<Box fontSize="1rem">
{
tableMetaData && pivotTableDefinition.columns?.map((column: PivotTableGroupBy, index: number) =>
(
<React.Fragment key={column.key}>{renderOneGroupBy(column, index, "columns")}</React.Fragment>
))
tableMetaData && (<div>{pivotTableDefinition?.columns?.map((column, i) => renderGroupBy(column, "columns", i))}</div>)
}
</Box>
{
@ -550,10 +586,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
<h5>Values</h5>
<Box fontSize="1rem">
{
tableMetaData && pivotTableDefinition.values?.map((value: PivotTableValue, index: number) =>
(
<React.Fragment key={value.key}>{renderOneValue(value, index)}</React.Fragment>
))
tableMetaData && (<div>{pivotTableDefinition?.values?.map((value, i) => renderValue(value, i))}</div>)
}
</Box>
{
@ -567,7 +600,44 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
</Grid>
</Grid>
</React.Fragment>
{/*
<Box mt={"1rem"}>
<h5>Preview</h5>
<table>
<tr>
<th style={{textAlign: "left", fontSize: "0.875rem"}}></th>
<th style={{textAlign: "left", fontSize: "0.875rem"}}>Column Labels</th>
</tr>
{
pivotTableDefinition?.columns?.map((column, i) =>
(
<tr key={column.key}>
<th style={{textAlign: "left", fontSize: "0.875rem"}}></th>
<th style={{textAlign: "left", fontSize: "0.875rem"}}>{column.fieldName}</th>
</tr>
))
}
<tr>
<th style={{textAlign: "left", fontSize: "0.875rem"}}>Row Labels</th>
{
pivotTableDefinition?.values?.map((value, i) =>
(
<th key={value.key} style={{textAlign: "left", fontSize: "0.875rem"}}>{value.function} of {value.fieldName}</th>
))
}
</tr>
{
pivotTableDefinition?.rows?.map((row, i) =>
(
<tr key={row.key}>
<th style={{textAlign: "left", fontSize: "0.875rem", paddingLeft: (i * 1) + "rem"}}>{row.fieldName}</th>
</tr>
))
}
</table>
</Box>
*/}
</DndProvider>
}
</Widget>);
}

View File

@ -0,0 +1,279 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon";
import TextField from "@mui/material/TextField";
import type {Identifier, XYCoord} from "dnd-core";
import colors from "qqq/assets/theme/base/colors";
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
import {DragItemTypes, fieldAutoCompleteTextFieldSX, getSelectedFieldForAutoComplete, xIconButtonSX} from "qqq/components/widgets/misc/PivotTableSetupWidget";
import {PivotTableDefinition, PivotTableFunction, pivotTableFunctionLabels, PivotTableValue} from "qqq/models/misc/PivotTableDefinitionModels";
import React, {FC, useRef} from "react";
import {useDrag, useDrop} from "react-dnd";
/*******************************************************************************
** component props
*******************************************************************************/
export interface PivotTableValueElementProps
{
id: string;
index: number;
dragCallback: (dragIndex: number, hoverIndex: number) => void;
metaData: QInstance;
tableMetaData: QTableMetaData;
pivotTableDefinition: PivotTableDefinition;
availableFieldNames: string[];
isEditable: boolean;
value: PivotTableValue;
callback: () => void;
}
/*******************************************************************************
** item to support react-dnd
*******************************************************************************/
interface DragItem
{
index: number;
id: string;
type: string;
}
/*******************************************************************************
** Element to render 1 pivot-table value.
*******************************************************************************/
export const PivotTableValueElement: FC<PivotTableValueElementProps> = ({id, index, dragCallback, metaData, tableMetaData, pivotTableDefinition, availableFieldNames, value, isEditable, callback}) =>
{
////////////////////////////////////////////////////////////////////////////
// credit: https://react-dnd.github.io/react-dnd/examples/sortable/simple //
////////////////////////////////////////////////////////////////////////////
const ref = useRef<HTMLDivElement>(null);
const [{handlerId}, drop] = useDrop<DragItem, void, { handlerId: Identifier | null }>(
{
accept: DragItemTypes.VALUE,
collect(monitor)
{
return {
handlerId: monitor.getHandlerId(),
};
},
hover(item: DragItem, monitor)
{
if (!ref.current)
{
return;
}
const dragIndex = item.index;
const hoverIndex = index;
// Don't replace items with themselves
if (dragIndex === hoverIndex)
{
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ref.current?.getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY)
{
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY)
{
return;
}
// Time to actually perform the action
dragCallback(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here! Generally it's better to avoid mutations,
// but it's good here for the sake of performance to avoid expensive index searches.
item.index = hoverIndex;
},
});
const [{isDragging}, drag] = useDrag({
type: DragItemTypes.VALUE,
item: () =>
{
return {id, index};
},
collect: (monitor: any) => ({
isDragging: monitor.isDragging(),
}),
});
/*******************************************************************************
** event handler for user selecting a field
*******************************************************************************/
const handleFieldChange = (event: any, newValue: any, reason: string) =>
{
value.fieldName = newValue ? newValue.fieldName : null;
callback();
};
/*******************************************************************************
** event handler for user selecting a function
*******************************************************************************/
const handleFunctionChange = (event: any, newValue: any, reason: string) =>
{
value.function = newValue ? newValue.id : null;
callback();
};
/*******************************************************************************
** event handler for clicking remove button
*******************************************************************************/
function removeValue(index: number)
{
pivotTableDefinition.values.splice(index, 1);
callback();
}
/////////////////////////////////////////////////////////////////////
// if we're not on an edit screen, return a simpler read-only view //
/////////////////////////////////////////////////////////////////////
if (!isEditable)
{
const selectedField = getSelectedFieldForAutoComplete(tableMetaData, value.fieldName);
if (selectedField && value.function)
{
const label = selectedField.table.name == tableMetaData.name ? selectedField.field.label : selectedField.table.label + ": " + selectedField.field.label;
return (<Box mr="0.375rem" mb="0.5rem" border={`1px solid ${colors.grayLines.main}`} borderRadius="0.75rem" p="0.25rem 0.75rem">{pivotTableFunctionLabels[value.function]} of {label}</Box>);
}
return (<React.Fragment />);
}
///////////////////////////////////////////////////////////////////////////////
// figure out functions to display in drop down, plus selected/default value //
///////////////////////////////////////////////////////////////////////////////
const functionOptions: any[] = [];
let defaultFunctionValue = null;
for (let pivotTableFunctionKey in PivotTableFunction)
{
// @ts-ignore any?
const label = "" + pivotTableFunctionLabels[pivotTableFunctionKey];
const option = {id: pivotTableFunctionKey, label: label};
functionOptions.push(option);
if (option.id == value.function)
{
defaultFunctionValue = option;
}
}
drag(drop(ref));
/*
return (<Box ref={ref} display="flex" p="0.5rem" pl="0" gap="0.5rem" alignItems="center" sx={{backgroundColor: "white", opacity: isDragging ? 0 : 1}} data-handler-id={handlerId}>
<Box sx={{whiteSpace: "nowrap"}}>
<Icon sx={{cursor: "ns-resize"}}>drag_indicator</Icon>
</Box>
<Box width="100%">
<FieldAutoComplete
id={`${rowsOrColumns}-${index}`}
label={null}
variant="outlined"
textFieldSX={fieldAutoCompleteTextFieldSX}
metaData={metaData}
tableMetaData={tableMetaData}
handleFieldChange={handleFieldChange}
hiddenFieldNames={usedGroupByFieldNames}
defaultValue={getSelectedFieldForAutoComplete(tableMetaData, groupBy.fieldName)}
/>
</Box>
<Box>
<Button sx={xIconButtonSX} onClick={() => removeGroupBy(index, rowsOrColumns)}><Icon>clear</Icon></Button>
</Box>
</Box>);
*/
return (<Box ref={ref} display="flex" p="0.5rem" pl="0" gap="0.5rem" alignItems="center" sx={{backgroundColor: "white", opacity: isDragging ? 0 : 1}} data-handler-id={handlerId}>
<Box>
<Icon sx={{cursor: "ns-resize"}}>drag_indicator</Icon>
</Box>
<Box width="100%">
<FieldAutoComplete
id={`values-field-${index}`}
label={null}
variant="outlined"
textFieldSX={fieldAutoCompleteTextFieldSX}
metaData={metaData}
tableMetaData={tableMetaData}
handleFieldChange={handleFieldChange}
availableFieldNames={availableFieldNames}
defaultValue={getSelectedFieldForAutoComplete(tableMetaData, value.fieldName)}
/>
</Box>
<Box width="330px">
<Autocomplete
id={`values-field-${index}`}
renderInput={(params) => (<TextField {...params} label={null} variant="outlined" sx={fieldAutoCompleteTextFieldSX} autoComplete="off" type="search" InputProps={{...params.InputProps}} />)}
// @ts-ignore
defaultValue={defaultFunctionValue}
options={functionOptions}
onChange={handleFunctionChange}
isOptionEqualToValue={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.label}
// todo? renderOption={(props, option, state) => renderFieldOption(props, option, state)}
autoSelect={true}
autoHighlight={true}
disableClearable
// slotProps={{popper: {className: "filterCriteriaRowColumnPopper", style: {padding: 0, width: "250px"}}}}
// {...alsoOpen}
/>
</Box>
<Box>
<Button sx={xIconButtonSX} onClick={() => removeValue(index)}><Icon>clear</Icon></Button>
</Box>
</Box>);
};

View File

@ -28,12 +28,13 @@ import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import Link from "@mui/material/Link";
import Modal from "@mui/material/Modal";
import Tooltip from "@mui/material/Tooltip/Tooltip";
import QContext from "QContext";
import colors from "qqq/assets/theme/base/colors";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import AdvancedQueryPreview from "qqq/components/query/AdvancedQueryPreview";
import Widget, {HeaderLinkButton, LabelComponent} from "qqq/components/widgets/Widget";
import Widget, {HeaderLinkButtonComponent} from "qqq/components/widgets/Widget";
import QQueryColumns, {Column} from "qqq/models/query/QQueryColumns";
import RecordQuery from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client";
@ -64,9 +65,14 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
const [alertContent, setAlertContent] = useState(null as string);
const {helpHelpActive} = useContext(QContext);
const recordQueryRef = useRef();
/////////////////////////////
// load values from record //
/////////////////////////////
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
if(!queryFilter)
{
@ -79,6 +85,9 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
columns = new QQueryColumns();
}
//////////////////////////////////////////////////////////////////////
// load tableMetaData initially, and if/when selected table changes //
//////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if (recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
@ -101,10 +110,6 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
{
setModalOpen(true);
}
else
{
setAlertContent("You must select a table before you can edit filters and columns")
}
}
@ -140,12 +145,6 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
setModalOpen(false);
}
const labelAdditionalComponentsRight: LabelComponent[] = []
if(isEditable)
{
labelAdditionalComponentsRight.push(new HeaderLinkButton("Edit Filters and Columns", openEditor))
}
/*******************************************************************************
**
@ -199,18 +198,45 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
return (false);
}
////////////////////
// load help text //
////////////////////
const helpRoles = ["ALL_SCREENS"]
const key = "slot:reportSetupSubheader"; // todo - ??
const {helpHelpActive} = useContext(QContext);
const showHelp = helpHelpActive || hasHelpContent(widgetMetaData?.helpContent?.get(key), helpRoles);
const formattedHelpContent = <HelpContent helpContents={widgetMetaData?.helpContent?.get(key)} roles={helpRoles} helpContentKey={key} />;
// const formattedHelpContent = "Add and edit filter and columns for your report."
const helpRoles = isEditable ? [recordValues["id"] ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"] : ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
return (<Widget widgetMetaData={widgetMetaData} labelAdditionalComponentsRight={labelAdditionalComponentsRight}>
/*******************************************************************************
**
*******************************************************************************/
function showHelp(slot: string)
{
return (helpHelpActive || hasHelpContent(widgetMetaData?.helpContent?.get(slot), helpRoles));
}
/*******************************************************************************
**
*******************************************************************************/
function getHelpContent(slot: string)
{
const key = `widget:${widgetMetaData.name};slot:${slot}`;
return <HelpContent helpContents={widgetMetaData?.helpContent?.get(slot)} roles={helpRoles} helpContentKey={key} />;
}
/////////////////////////////////////////////////
// add link to widget header for opening modal //
/////////////////////////////////////////////////
const selectTableFirstTooltipTitle = tableMetaData ? null : "You must select a table before you can set up your report filters and columns";
const labelAdditionalElementsRight: JSX.Element[] = []
if(isEditable)
{
labelAdditionalElementsRight.push(<HeaderLinkButtonComponent label="Edit Filters and Columns" onClickCallback={openEditor} disabled={tableMetaData == null} disabledTooltip={selectTableFirstTooltipTitle} />)
}
return (<Widget widgetMetaData={widgetMetaData} labelAdditionalElementsRight={labelAdditionalElementsRight}>
<React.Fragment>
{
showHelp("sectionSubhead") &&
<Box color={colors.gray.main} pb={"0.5rem"} fontSize={"0.875rem"}>
{getHelpContent("sectionSubhead")}
</Box>
}
<Collapse in={Boolean(alertContent)}>
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert>
</Collapse>
@ -224,7 +250,10 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
!mayShowQueryPreview() &&
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}>
{
isEditable && <Link sx={{cursor: "pointer"}} onClick={openEditor} color={colors.gray.main}>+ Add Filters</Link>
isEditable &&
<Tooltip title={selectTableFirstTooltipTitle}>
<Link sx={{cursor: "pointer"}} onClick={openEditor} color={colors.gray.main}>+ Add Filters</Link>
</Tooltip>
}
{
!isEditable && <Box color={colors.gray.main}>Your report has no filters.</Box>
@ -243,7 +272,10 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
!mayShowColumnsPreview() &&
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
{
isEditable && <Link sx={{cursor: "pointer"}} onClick={openEditor} color={colors.gray.main}>+ Add Columns</Link>
isEditable &&
<Tooltip title={selectTableFirstTooltipTitle}>
<Link sx={{cursor: "pointer"}} onClick={openEditor} color={colors.gray.main}>+ Add Columns</Link>
</Tooltip>
}
{
!isEditable && <Box color={colors.gray.main}>Your report has no filters.</Box>
@ -260,9 +292,9 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
<Card sx={{m: "2rem", p: "2rem"}}>
<h3>Edit Filters and Columns</h3>
{
showHelp &&
showHelp("modalSubheader") &&
<Box color={colors.gray.main} pb={"0.5rem"}>
{formattedHelpContent}
{getHelpContent("modalSubheader")}
</Box>
}
{

View File

@ -0,0 +1,115 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*******************************************************************************
** put a unique key value in all the pivot table group-by and value objects,
** to help react rendering be sane.
*******************************************************************************/
export class PivotObjectKey
{
private static value = new Date().getTime();
static next(): number
{
return PivotObjectKey.value++
}
}
/*******************************************************************************
** Full definition of pivot table
*******************************************************************************/
export class PivotTableDefinition
{
rows: PivotTableGroupBy[];
columns: PivotTableGroupBy[];
values: PivotTableValue[];
}
/*******************************************************************************
** A field that the pivot table is grouped by, either as a row or column
*******************************************************************************/
export class PivotTableGroupBy
{
fieldName: string;
key: number;
constructor()
{
this.key = PivotObjectKey.next()
}
}
/*******************************************************************************
** A field & function that serves as the computed values in the pivot table
*******************************************************************************/
export class PivotTableValue
{
fieldName: string;
function: PivotTableFunction;
key: number;
constructor()
{
this.key = PivotObjectKey.next()
}
}
/*******************************************************************************
** Functions that can be appplied to pivot table values
*******************************************************************************/
export enum PivotTableFunction
{
AVERAGE = "AVERAGE",
COUNT = "COUNT",
COUNT_NUMS = "COUNT_NUMS",
MAX = "MAX",
MIN = "MIN",
PRODUCT = "PRODUCT",
STD_DEV = "STD_DEV",
STD_DEVP = "STD_DEVP",
SUM = "SUM",
VAR = "VAR",
VARP = "VARP",
}
//////////////////////////////////////
// labels for pivot table functions //
//////////////////////////////////////
export const pivotTableFunctionLabels =
{
"AVERAGE": "Average",
"COUNT": "Count Values (COUNTA)",
"COUNT_NUMS": "Count Numbers (COUNT)",
"MAX": "Max",
"MIN": "Min",
"PRODUCT": "Product",
"STD_DEV": "StdDev",
"STD_DEVP": "StdDevp",
"SUM": "Sum",
"VAR": "Var",
"VARP": "Varp"
};

View File

@ -845,7 +845,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
return (
<BaseLayout>
<Box>
<Box className="recordView">
<Grid container>
<Grid item xs={12}>
<Box mb={3}>

View File

@ -658,3 +658,9 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
{
border: none;
}
.entityForm h5,
.recordView h5
{
font-weight: 500;
}