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

@ -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>);
}