mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-17 21:00:45 +00:00
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:
@ -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;
|
label: string;
|
||||||
onClickCallback: () => void;
|
onClickCallback: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
disabledTooltip?: string;
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
export function HeaderToggleComponent({label, getValue, onClickCallback}: HeaderToggleComponentProps): JSX.Element
|
||||||
{
|
{
|
||||||
console.log(`@dk in HTComponent, getValue(): ${getValue()}`);
|
|
||||||
|
|
||||||
const onClick = () =>
|
const onClick = () =>
|
||||||
{
|
{
|
||||||
onClickCallback();
|
onClickCallback();
|
||||||
|
204
src/qqq/components/widgets/misc/PivotTableGroupByElement.tsx
Normal file
204
src/qqq/components/widgets/misc/PivotTableGroupByElement.tsx
Normal 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>);
|
||||||
|
};
|
@ -30,19 +30,88 @@ import Grid from "@mui/material/Grid";
|
|||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
|
import QContext from "QContext";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import FieldAutoComplete from "qqq/components/misc/FieldAutoComplete";
|
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 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 Client from "qqq/utils/qqq/Client";
|
||||||
import FilterUtils from "qqq/utils/qqq/FilterUtils";
|
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";
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
export const DragItemTypes =
|
||||||
// put a unique key value in all the pivot table group-by and value objects, //
|
{
|
||||||
// to help react rendering be sane. //
|
ROW: "row",
|
||||||
///////////////////////////////////////////////////////////////////////////////
|
COLUMN: "column",
|
||||||
let pivotObjectKey = new Date().getTime();
|
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
|
interface PivotTableSetupWidgetProps
|
||||||
{
|
{
|
||||||
isEditable: boolean;
|
isEditable: boolean;
|
||||||
@ -51,71 +120,14 @@ interface PivotTableSetupWidgetProps
|
|||||||
onSaveCallback?: (values: { [name: string]: any }) => void;
|
onSaveCallback?: (values: { [name: string]: any }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** default values for props
|
||||||
|
*******************************************************************************/
|
||||||
PivotTableSetupWidget.defaultProps = {
|
PivotTableSetupWidget.defaultProps = {
|
||||||
onSaveCallback: null
|
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();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
@ -134,54 +146,62 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
|
|||||||
const [pivotTableDefinition, setPivotTableDefinition] = useState(null as PivotTableDefinition);
|
const [pivotTableDefinition, setPivotTableDefinition] = useState(null as PivotTableDefinition);
|
||||||
|
|
||||||
const [usedGroupByFieldNames, setUsedGroupByFieldNames] = useState([] as string[]);
|
const [usedGroupByFieldNames, setUsedGroupByFieldNames] = useState([] as string[]);
|
||||||
|
const [availableFieldNames, setAvailableFieldNames] = useState([] as string[]);
|
||||||
|
|
||||||
|
const {helpHelpActive} = useContext(QContext);
|
||||||
|
|
||||||
//////////////////
|
//////////////////
|
||||||
// initial load //
|
// initial load //
|
||||||
//////////////////
|
//////////////////
|
||||||
useEffect(() =>
|
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;
|
setEnabled(true);
|
||||||
if (originalPivotTableDefinition)
|
}
|
||||||
{
|
else if (!originalPivotTableDefinition)
|
||||||
setEnabled(true);
|
{
|
||||||
}
|
originalPivotTableDefinition = new PivotTableDefinition();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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());
|
setMetaData(await qController.loadMetaData());
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
@ -202,6 +222,27 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
|
|||||||
}, [recordValues]);
|
}, [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)});
|
onSaveCallback({pivotTableJson: JSON.stringify(pivotTableDefinition)});
|
||||||
updateUsedGroupByFieldNames();
|
updateUsedGroupByFieldNames();
|
||||||
forceUpdate();
|
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(columns.columns[i].isVisible)
|
||||||
if (field && fieldTable)
|
|
||||||
{
|
{
|
||||||
return ({field: field, table: fieldTable, fieldName: fieldName});
|
fieldNames.push(columns.columns[i].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setAvailableFieldNames(fieldNames);
|
||||||
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>);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -409,12 +360,12 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function renderOneValue(value: PivotTableValue, index: number)
|
function renderOneValue(value: PivotTableValue, index: number)
|
||||||
{
|
{
|
||||||
if(!isEditable)
|
if (!isEditable)
|
||||||
{
|
{
|
||||||
const selectedField = getSelectedFieldForAutoComplete(value.fieldName);
|
const selectedField = getSelectedFieldForAutoComplete(tableMetaData, value.fieldName);
|
||||||
if(selectedField && value.function)
|
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>);
|
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 label = "" + pivotTableFunctionLabels[pivotTableFunctionKey];
|
||||||
const option = {id: pivotTableFunctionKey, label: label};
|
const option = {id: pivotTableFunctionKey, label: label};
|
||||||
functionOptions.push(option);
|
functionOptions.push(option);
|
||||||
|
|
||||||
if(option.id == value.function)
|
if (option.id == value.function)
|
||||||
{
|
{
|
||||||
defaultFunctionValue = option;
|
defaultFunctionValue = option;
|
||||||
}
|
}
|
||||||
@ -462,7 +413,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
|
|||||||
metaData={metaData}
|
metaData={metaData}
|
||||||
tableMetaData={tableMetaData}
|
tableMetaData={tableMetaData}
|
||||||
handleFieldChange={handleFieldChange}
|
handleFieldChange={handleFieldChange}
|
||||||
defaultValue={getSelectedFieldForAutoComplete(value.fieldName)}
|
defaultValue={getSelectedFieldForAutoComplete(tableMetaData, value.fieldName)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
<Box width="330px">
|
<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 //
|
// 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";
|
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}>
|
return (<Widget widgetMetaData={widgetMetaData} labelAdditionalElementsRight={labelAdditionalElementsRight}>
|
||||||
{enabled && pivotTableDefinition &&
|
{enabled && pivotTableDefinition &&
|
||||||
<React.Fragment>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<Grid container spacing="16" >
|
{
|
||||||
|
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}>
|
<Grid item lg={4} md={6} xs={12}>
|
||||||
<h5>Rows</h5>
|
<h5>Rows</h5>
|
||||||
<Box fontSize="1rem">
|
<Box fontSize="1rem">
|
||||||
{
|
{
|
||||||
tableMetaData && pivotTableDefinition.rows?.map((row: PivotTableGroupBy, index: number) =>
|
tableMetaData && (<div>{pivotTableDefinition?.rows?.map((row, i) => renderGroupBy(row, "rows", i))}</div>)
|
||||||
(
|
|
||||||
<React.Fragment key={row.key}>{renderOneGroupBy(row, index, "rows")}</React.Fragment>
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
@ -530,10 +569,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
|
|||||||
<h5>Columns</h5>
|
<h5>Columns</h5>
|
||||||
<Box fontSize="1rem">
|
<Box fontSize="1rem">
|
||||||
{
|
{
|
||||||
tableMetaData && pivotTableDefinition.columns?.map((column: PivotTableGroupBy, index: number) =>
|
tableMetaData && (<div>{pivotTableDefinition?.columns?.map((column, i) => renderGroupBy(column, "columns", i))}</div>)
|
||||||
(
|
|
||||||
<React.Fragment key={column.key}>{renderOneGroupBy(column, index, "columns")}</React.Fragment>
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
@ -550,10 +586,7 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
|
|||||||
<h5>Values</h5>
|
<h5>Values</h5>
|
||||||
<Box fontSize="1rem">
|
<Box fontSize="1rem">
|
||||||
{
|
{
|
||||||
tableMetaData && pivotTableDefinition.values?.map((value: PivotTableValue, index: number) =>
|
tableMetaData && (<div>{pivotTableDefinition?.values?.map((value, i) => renderValue(value, i))}</div>)
|
||||||
(
|
|
||||||
<React.Fragment key={value.key}>{renderOneValue(value, index)}</React.Fragment>
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
@ -567,7 +600,44 @@ export default function PivotTableSetupWidget({isEditable, widgetMetaData, recor
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</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>);
|
</Widget>);
|
||||||
}
|
}
|
||||||
|
279
src/qqq/components/widgets/misc/PivotTableValueElement.tsx
Normal file
279
src/qqq/components/widgets/misc/PivotTableValueElement.tsx
Normal 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>);
|
||||||
|
|
||||||
|
};
|
@ -28,12 +28,13 @@ import Box from "@mui/material/Box";
|
|||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Link from "@mui/material/Link";
|
import Link from "@mui/material/Link";
|
||||||
import Modal from "@mui/material/Modal";
|
import Modal from "@mui/material/Modal";
|
||||||
|
import Tooltip from "@mui/material/Tooltip/Tooltip";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||||
import AdvancedQueryPreview from "qqq/components/query/AdvancedQueryPreview";
|
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 QQueryColumns, {Column} from "qqq/models/query/QQueryColumns";
|
||||||
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
import RecordQuery from "qqq/pages/records/query/RecordQuery";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
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 [alertContent, setAlertContent] = useState(null as string);
|
||||||
|
|
||||||
|
const {helpHelpActive} = useContext(QContext);
|
||||||
|
|
||||||
const recordQueryRef = useRef();
|
const recordQueryRef = useRef();
|
||||||
|
|
||||||
|
|
||||||
|
/////////////////////////////
|
||||||
|
// load values from record //
|
||||||
|
/////////////////////////////
|
||||||
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
|
let queryFilter = recordValues["queryFilterJson"] && JSON.parse(recordValues["queryFilterJson"]) as QQueryFilter;
|
||||||
if(!queryFilter)
|
if(!queryFilter)
|
||||||
{
|
{
|
||||||
@ -79,6 +85,9 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
columns = new QQueryColumns();
|
columns = new QQueryColumns();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
|
// load tableMetaData initially, and if/when selected table changes //
|
||||||
|
//////////////////////////////////////////////////////////////////////
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
|
if (recordValues["tableName"] && (tableMetaData == null || tableMetaData.name != recordValues["tableName"]))
|
||||||
@ -101,10 +110,6 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
{
|
{
|
||||||
setModalOpen(true);
|
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);
|
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);
|
return (false);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////
|
const helpRoles = isEditable ? [recordValues["id"] ? "EDIT_SCREEN" : "INSERT_SCREEN", "WRITE_SCREENS", "ALL_SCREENS"] : ["VIEW_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
|
||||||
// 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."
|
|
||||||
|
|
||||||
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>
|
<React.Fragment>
|
||||||
|
{
|
||||||
|
showHelp("sectionSubhead") &&
|
||||||
|
<Box color={colors.gray.main} pb={"0.5rem"} fontSize={"0.875rem"}>
|
||||||
|
{getHelpContent("sectionSubhead")}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
<Collapse in={Boolean(alertContent)}>
|
<Collapse in={Boolean(alertContent)}>
|
||||||
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert>
|
<Alert severity="error" sx={{mt: 1.5, mb: 0.5}} onClose={() => setAlertContent(null)}>{alertContent}</Alert>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
@ -224,7 +250,10 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
!mayShowQueryPreview() &&
|
!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}`}>
|
<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>
|
!isEditable && <Box color={colors.gray.main}>Your report has no filters.</Box>
|
||||||
@ -243,7 +272,10 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
|
|||||||
!mayShowColumnsPreview() &&
|
!mayShowColumnsPreview() &&
|
||||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
<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>
|
!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"}}>
|
<Card sx={{m: "2rem", p: "2rem"}}>
|
||||||
<h3>Edit Filters and Columns</h3>
|
<h3>Edit Filters and Columns</h3>
|
||||||
{
|
{
|
||||||
showHelp &&
|
showHelp("modalSubheader") &&
|
||||||
<Box color={colors.gray.main} pb={"0.5rem"}>
|
<Box color={colors.gray.main} pb={"0.5rem"}>
|
||||||
{formattedHelpContent}
|
{getHelpContent("modalSubheader")}
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
115
src/qqq/models/misc/PivotTableDefinitionModels.ts
Normal file
115
src/qqq/models/misc/PivotTableDefinitionModels.ts
Normal 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"
|
||||||
|
};
|
@ -845,7 +845,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<Box>
|
<Box className="recordView">
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box mb={3}>
|
<Box mb={3}>
|
||||||
|
@ -658,3 +658,9 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
|
|||||||
{
|
{
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entityForm h5,
|
||||||
|
.recordView h5
|
||||||
|
{
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
Reference in New Issue
Block a user