Add export to table widgets; add reload to most widgets; refactor widget label components (render in class!)

This commit is contained in:
2023-05-18 15:52:40 -05:00
parent 813067be25
commit 65652f04f0
5 changed files with 330 additions and 79 deletions

View File

@ -30,6 +30,7 @@
"form-data": "4.0.0",
"formik": "2.2.9",
"html-react-parser": "1.4.8",
"html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6",
"rapidoc": "9.3.4",
"react": "17.0.2",

View File

@ -44,10 +44,10 @@ import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
import ParentWidget from "qqq/components/widgets/ParentWidget";
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
import TableCard from "qqq/components/widgets/tables/TableCard";
import Widget, {WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT} from "qqq/components/widgets/Widget";
import ProcessRun from "qqq/pages/processes/ProcessRun";
import Client from "qqq/utils/qqq/Client";
import TableWidget from "./tables/TableWidget";
const qController = Client.getInstance();
@ -221,20 +221,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
}
{
widgetMetaData.type === "table" && (
<Widget
<TableWidget
widgetMetaData={widgetMetaData}
widgetData={widgetData[i]}
reloadWidgetCallback={(data) => reloadWidget(i, data)}
footerHTML={widgetData[i]?.footerHTML}
isChild={areChildren}
>
<TableCard
noRowsFoundHTML={widgetData[i]?.noRowsFoundHTML}
rowsPerPage={widgetData[i]?.rowsPerPage}
hidePaginationDropdown={widgetData[i]?.hidePaginationDropdown}
data={widgetData[i]}
/>
</Widget>
/>
)
}
{
@ -254,7 +246,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
<Widget
widgetMetaData={widgetMetaData}
widgetData={widgetData[i]}
reloadWidgetCallback={(data) => reloadWidget(i, data)}>
reloadWidgetCallback={(data) => reloadWidget(i, data)}
showReloadControl={false}
>
<div className="widgetProcessMidDiv" style={{height: "100%"}}>
<ProcessRun process={widgetData[i]?.processMetaData} defaultProcessValues={widgetData[i]?.defaultValues} isWidget={true} forceReInit={widgetCounter} />
</div>
@ -265,7 +259,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
widgetMetaData.type === "stepper" && (
<Widget
widgetMetaData={widgetMetaData}
widgetData={widgetData[i]}>
widgetData={widgetData[i]}
reloadWidgetCallback={(data) => reloadWidget(i, data)}
>
<Box sx={{alignItems: "stretch", flexGrow: 1, display: "flex", marginTop: "0px", paddingTop: "0px"}}>
<Box padding="1rem" sx={{width: "100%"}}>
<StepperCard data={widgetData[i]} />
@ -276,7 +272,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
}
{
widgetMetaData.type === "html" && (
<Widget widgetMetaData={widgetMetaData}>
<Widget
widgetMetaData={widgetMetaData}
reloadWidgetCallback={(data) => reloadWidget(i, data)}
widgetData={widgetData[i]}
>
<Box px={3} pt={0} pb={2}>
<MDTypography component="div" variant="button" color="text" fontWeight="light">
{
@ -306,8 +306,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
widgetMetaData={widgetMetaData}
widgetData={widgetData[i]}
isChild={areChildren}
// reloadWidgetCallback={(data) => reloadWidget(i, data)}
reloadWidgetCallback={(data) => reloadWidget(i, data)}
>
<StatisticsCard
data={widgetData[i]}
@ -346,6 +345,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
<Widget
widgetMetaData={widgetMetaData}
widgetData={widgetData[i]}
reloadWidgetCallback={(data) => reloadWidget(i, data)}
isChild={areChildren}
>
<div>
@ -379,7 +379,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit
<Widget
widgetMetaData={widgetMetaData}
widgetData={widgetData[i]}
isChild={areChildren}>
reloadWidgetCallback={(data) => reloadWidget(i, data)}
isChild={areChildren}
>
<DefaultLineChart sx={{alignItems: "center"}}
data={widgetData[i]?.chartData}
isYAxisCurrency={widgetData[i]?.isYAxisCurrency}

View File

@ -25,10 +25,11 @@ import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Icon from "@mui/material/Icon";
import LinearProgress from "@mui/material/LinearProgress";
import Typography from "@mui/material/Typography";
import parse from "html-react-parser";
import React, {useState} from "react";
import {Link, useNavigate} from "react-router-dom";
import React, {useEffect, useState} from "react";
import {Link, useNavigate, NavigateFunction} from "react-router-dom";
import colors from "qqq/components/legacy/colors";
import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu";
@ -43,6 +44,7 @@ export interface WidgetData
}[][];
dropdownNeedsSelectedText?: string;
hasPermission?: boolean;
[other: string]: any;
}
@ -54,6 +56,7 @@ interface Props
widgetData?: WidgetData;
children: JSX.Element;
reloadWidgetCallback?: (params: string) => void;
showReloadControl: boolean;
isChild?: boolean;
footerHTML?: string;
storeDropdownSelections?: boolean;
@ -61,6 +64,7 @@ interface Props
Widget.defaultProps = {
isChild: false,
showReloadControl: true,
widgetMetaData: {},
widgetData: {},
labelAdditionalComponentsLeft: [],
@ -68,9 +72,22 @@ Widget.defaultProps = {
};
interface LabelComponentRenderArgs
{
navigate: NavigateFunction;
widgetProps: Props;
dropdownData: any[];
componentIndex: number;
reloadFunction: () => void;
}
export class LabelComponent
{
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
return (<div>Unsupported component type</div>)
}
}
@ -86,6 +103,15 @@ export class HeaderLink extends LabelComponent
this.label = label;
this.to = to;
}
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
return (
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
{this.to ? <Link to={this.to}>{this.label}</Link> : null}
</Typography>
);
}
}
@ -97,6 +123,7 @@ export class AddNewRecordButton extends LabelComponent
defaultValues: any;
disabledFields: any;
constructor(table: QTableMetaData, defaultValues: any, label: string = "Add new", disabledFields: any = defaultValues)
{
super();
@ -105,6 +132,45 @@ export class AddNewRecordButton extends LabelComponent
this.defaultValues = defaultValues;
this.disabledFields = disabledFields;
}
openEditForm = (navigate: any, table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any) =>
{
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
}
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
return (
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="0.25rem">
<Button sx={{mt: 0.75}} onClick={() => this.openEditForm(args.navigate, this.table, null, this.defaultValues, this.disabledFields)}>{this.label}</Button>
</Typography>
);
}
}
export class ExportDataButton extends LabelComponent
{
callbackToExport: any;
label: string;
isDisabled: boolean;
constructor(callbackToExport: any, isDisabled = false, label: string = "Export")
{
super();
this.callbackToExport = callbackToExport;
this.isDisabled = isDisabled;
this.label = label;
}
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
return (
<Typography variant="body2" py={2} px={1} display="inline" position="relative" top="-0.25rem">
<Button sx={{px: 1}} onClick={() => this.callbackToExport()} disabled={this.isDisabled}><Icon>save_alt</Icon>&nbsp;{this.label}</Button>
</Typography>
);
}
}
@ -121,6 +187,55 @@ export class Dropdown extends LabelComponent
this.options = options;
this.onChangeCallback = onChangeCallback;
}
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
let defaultValue = null;
const dropdownName = args.widgetProps.widgetData.dropdownNameList[args.componentIndex];
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${args.widgetProps.widgetMetaData.name}.${dropdownName}`;
if(args.widgetProps.storeDropdownSelections)
{
///////////////////////////////////////////////////////////////////////////////////////
// see if an existing value is stored in local storage, and if so set it in dropdown //
///////////////////////////////////////////////////////////////////////////////////////
defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
args.dropdownData[args.componentIndex] = defaultValue?.id;
}
return (
<Box my={2} sx={{float: "right"}}>
<DropdownMenu
name={dropdownName}
defaultValue={defaultValue}
sx={{width: 200, marginLeft: "15px"}}
label={`Select ${this.label}`}
dropdownOptions={this.options}
onChangeCallback={this.onChangeCallback}
/>
</Box>
);
}
}
export class ReloadControl extends LabelComponent
{
callback: () => void;
constructor(callback: () => void)
{
super();
this.callback = callback;
}
render = (args: LabelComponentRenderArgs): JSX.Element =>
{
return (
<Typography variant="body2" py={2} px={1} display="inline" position="relative" top="-0.25rem">
<Button sx={{px: 1}} onClick={() => this.callback()}><Icon>refresh</Icon> Refresh</Button>
</Typography>
);
}
}
@ -132,64 +247,11 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
const navigate = useNavigate();
const [dropdownData, setDropdownData] = useState([]);
const [fullScreenWidgetClassName, setFullScreenWidgetClassName] = useState("");
const [reloading, setReloading] = useState(false);
function openEditForm(table: QTableMetaData, id: any = null, defaultValues: any, disabledFields: any)
function renderComponent(component: LabelComponent, componentIndex: number)
{
navigate(`#/createChild=${table.name}/defaultValues=${JSON.stringify(defaultValues)}/disabledFields=${JSON.stringify(disabledFields)}`)
}
function renderComponent(component: LabelComponent, index: number)
{
if(component instanceof HeaderLink)
{
const link = component as HeaderLink
return (
<Typography variant="body2" p={2} display="inline" fontSize=".875rem" pt="0" position="relative" top="-0.25rem">
{link.to ? <Link to={link.to}>{link.label}</Link> : null}
</Typography>
);
}
if (component instanceof AddNewRecordButton)
{
const addNewRecordButton = component as AddNewRecordButton
return (
<Typography variant="body2" p={2} pr={0} display="inline" position="relative" top="0.25rem">
<Button sx={{mt: 0.75}} onClick={() => openEditForm(addNewRecordButton.table, null, addNewRecordButton.defaultValues, addNewRecordButton.disabledFields)}>{addNewRecordButton.label}</Button>
</Typography>
);
}
if (component instanceof Dropdown)
{
let defaultValue = null;
const dropdownName = props.widgetData.dropdownNameList[index];
const localStorageKey = `${WIDGET_DROPDOWN_SELECTION_LOCAL_STORAGE_KEY_ROOT}.${props.widgetMetaData.name}.${dropdownName}`;
if(props.storeDropdownSelections)
{
///////////////////////////////////////////////////////////////////////////////////////
// see if an existing value is stored in local storage, and if so set it in dropdown //
///////////////////////////////////////////////////////////////////////////////////////
defaultValue = JSON.parse(localStorage.getItem(localStorageKey));
dropdownData[index] = defaultValue?.id;
}
const dropdown = component as Dropdown
return (
<Box my={2} sx={{float: "right"}}>
<DropdownMenu
name={dropdownName}
defaultValue={defaultValue}
sx={{width: 200, marginLeft: "15px"}}
label={`Select ${dropdown.label}`}
dropdownOptions={dropdown.options}
onChangeCallback={dropdown.onChangeCallback}
/>
</Box>
);
}
return (<div>Unsupported component type.</div>)
return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload})
}
@ -209,6 +271,27 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
});
}
const doReload = () =>
{
setReloading(true);
reloadWidget(dropdownData);
}
useEffect(() =>
{
setReloading(false);
}, [props.widgetData]);
const effectiveLabelAdditionalComponentsLeft: LabelComponent[] = [];
if(props.labelAdditionalComponentsLeft)
{
props.labelAdditionalComponentsLeft.map((component) => effectiveLabelAdditionalComponentsLeft.push(component));
}
if(props.reloadWidgetCallback && props.widgetData && props.showReloadControl)
{
effectiveLabelAdditionalComponentsLeft.push(new ReloadControl(doReload))
}
function handleDataChange(dropdownLabel: string, changedData: any)
{
@ -299,7 +382,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true;
const widgetContent =
<Box sx={{width: "100%", height: "100%", minHeight: props.widgetMetaData?.minHeight ? props.widgetMetaData?.minHeight : "initial"}}>
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}}>
<Box pr={2} display="flex" justifyContent="space-between" alignItems="flex-start" sx={{width: "100%"}} height={"3.5rem"}>
<Box pt={2} pb={1}>
{
hasPermission ?
@ -367,7 +450,7 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
*/}
{
hasPermission && (
props.labelAdditionalComponentsLeft.map((component, i) =>
effectiveLabelAdditionalComponentsLeft.map((component, i) =>
{
return (<span key={i}>{renderComponent(component, i)}</span>);
})
@ -385,6 +468,9 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
}
</Box>
</Box>
{
props.widgetMetaData?.isCard && (reloading ? <LinearProgress color="info" sx={{overflow: "hidden", borderRadius: "0"}} /> : <Box height="0.375rem"/>)
}
{
hasPermission && props.widgetData?.dropdownNeedsSelectedText ? (
<Box pb={3} pr={3} sx={{width: "100%", textAlign: "right"}}>
@ -407,7 +493,11 @@ function Widget(props: React.PropsWithChildren<Props>): JSX.Element
}
</Box>;
return props.widgetMetaData?.isCard ? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>{widgetContent}</Card> : widgetContent;
return props.widgetMetaData?.isCard
? <Card sx={{marginTop: props.widgetMetaData?.icon ? 2 : 0, width: "100%"}} className={fullScreenWidgetClassName}>
{widgetContent}
</Card>
: widgetContent;
}
export default Widget;

View File

@ -123,6 +123,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element
return (
<Widget
widgetMetaData={widgetMetaData}
widgetData={data}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
>

View File

@ -0,0 +1,157 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
// @ts-ignore
import {htmlToText} from "html-to-text";
import React, {useEffect, useState} from "react";
import TableCard from "qqq/components/widgets/tables/TableCard";
import Widget, {ExportDataButton, WidgetData} from "qqq/components/widgets/Widget";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
interface Props
{
widgetMetaData?: QWidgetMetaData;
widgetData?: WidgetData;
reloadWidgetCallback?: (params: string) => void;
isChild?: boolean;
}
TableWidget.defaultProps = {
foo: null,
};
function download(filename: string, text: string)
{
var element = document.createElement("a");
element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
element.setAttribute("download", filename);
element.style.display = "none";
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function TableWidget(props: Props): JSX.Element
{
const rows = props.widgetData?.rows;
const columns = props.widgetData?.columns;
const exportCallback = () =>
{
if (props.widgetData && rows && columns)
{
console.log(props.widgetData);
let csv = "";
for (let j = 0; j < columns.length; j++)
{
if (j > 0)
{
csv += ",";
}
csv += `"${columns[j].header}"`;
}
csv += "\n";
for (let i = 0; i < rows.length; i++)
{
for (let j = 0; j < columns.length; j++)
{
if (j > 0)
{
csv += ",";
}
const cell = rows[i][columns[j].accessor];
const text = htmlToText(cell,
{
selectors: [
{selector: "a", format: "inline"},
{selector: ".MuiIcon-root", format: "skip"},
{selector: ".button", format: "skip"}
]
});
csv += `"${text}"`;
}
csv += "\n";
}
console.log(csv);
const fileName = props.widgetData.label + "-" + ValueUtils.formatDateTimeISO8601(new Date()) + ".csv";
download(fileName, csv);
}
else
{
alert("Error exporting widget data.");
}
};
const [exportDataButton, setExportDataButton] = useState(new ExportDataButton(() => exportCallback(), true));
const [isExportDisabled, setIsExportDisabled] = useState(true);
const [componentLeft, setComponentLeft] = useState([exportDataButton])
useEffect(() =>
{
if (props.widgetData && columns && rows && rows.length > 0)
{
console.log("Setting export disabled false")
setIsExportDisabled(false);
}
else
{
console.log("Setting export disabled true")
setIsExportDisabled(true);
}
}, [props.widgetData])
useEffect(() =>
{
console.log("Setting new export button with disabled=" + isExportDisabled)
setComponentLeft([new ExportDataButton(() => exportCallback(), isExportDisabled)]);
}, [isExportDisabled])
return (
<Widget
widgetMetaData={props.widgetMetaData}
widgetData={props.widgetData}
reloadWidgetCallback={(data) => props.reloadWidgetCallback(data)}
footerHTML={props.widgetData?.footerHTML}
isChild={props.isChild}
labelAdditionalComponentsLeft={componentLeft}
>
<TableCard
noRowsFoundHTML={props.widgetData?.noRowsFoundHTML}
rowsPerPage={props.widgetData?.rowsPerPage}
hidePaginationDropdown={props.widgetData?.hidePaginationDropdown}
data={{columns: props.widgetData?.columns, rows: props.widgetData?.rows}}
/>
</Widget>
);
}
export default TableWidget;