From 813067be257f3f2344cdde0bc3ea77d5ad8b1693 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 18 May 2023 15:48:39 -0500 Subject: [PATCH 1/5] Add env settings to branding, show banner in left bar --- package.json | 2 +- src/App.tsx | 1 + src/qqq/components/horseshoe/sidenav/SideNav.tsx | 10 +++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bda2769..bb1b9aa 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.63", + "@kingsrook/qqq-frontend-core": "1.0.64", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/App.tsx b/src/App.tsx index 98321cd..8b5c118 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -567,6 +567,7 @@ export default function App() icon={branding.icon} logo={branding.logo} appName={branding.appName} + branding={branding} routes={sideNavRoutes} onMouseEnter={handleOnMouseEnter} onMouseLeave={handleOnMouseLeave} diff --git a/src/qqq/components/horseshoe/sidenav/SideNav.tsx b/src/qqq/components/horseshoe/sidenav/SideNav.tsx index 30d2251..8ac14e9 100644 --- a/src/qqq/components/horseshoe/sidenav/SideNav.tsx +++ b/src/qqq/components/horseshoe/sidenav/SideNav.tsx @@ -19,6 +19,7 @@ * along with this program. If not, see . */ +import {QBrandingMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QBrandingMetaData"; import Box from "@mui/material/Box"; import Divider from "@mui/material/Divider"; import Icon from "@mui/material/Icon"; @@ -42,6 +43,7 @@ interface Props icon?: string; logo?: string; appName?: string; + branding?: QBrandingMetaData; routes: { [key: string]: | ReactNode @@ -64,7 +66,7 @@ interface Props [key: string]: any; } -function Sidenav({color, icon, logo, appName, routes, ...rest}: Props): JSX.Element +function Sidenav({color, icon, logo, appName, branding, routes, ...rest}: Props): JSX.Element { const [openCollapse, setOpenCollapse] = useState(false); const [openNestedCollapse, setOpenNestedCollapse] = useState(false); @@ -328,6 +330,12 @@ function Sidenav({color, icon, logo, appName, routes, ...rest}: Props): JSX.Elem } + { + branding && branding.environmentBannerText && + + {branding.environmentBannerText} + + } Date: Thu, 18 May 2023 15:52:40 -0500 Subject: [PATCH 2/5] Add export to table widgets; add reload to most widgets; refactor widget label components (render in class!) --- package.json | 1 + .../components/widgets/DashboardWidgets.tsx | 36 +-- src/qqq/components/widgets/Widget.tsx | 214 +++++++++++++----- .../widgets/misc/RecordGridWidget.tsx | 1 + .../components/widgets/tables/TableWidget.tsx | 157 +++++++++++++ 5 files changed, 330 insertions(+), 79 deletions(-) create mode 100644 src/qqq/components/widgets/tables/TableWidget.tsx diff --git a/package.json b/package.json index bb1b9aa..49f6f83 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/qqq/components/widgets/DashboardWidgets.tsx b/src/qqq/components/widgets/DashboardWidgets.tsx index 9929d40..005acf4 100644 --- a/src/qqq/components/widgets/DashboardWidgets.tsx +++ b/src/qqq/components/widgets/DashboardWidgets.tsx @@ -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" && ( - reloadWidget(i, data)} - footerHTML={widgetData[i]?.footerHTML} isChild={areChildren} - > - - + /> ) } { @@ -254,7 +246,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit reloadWidget(i, data)}> + reloadWidgetCallback={(data) => reloadWidget(i, data)} + showReloadControl={false} + >
@@ -265,7 +259,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit widgetMetaData.type === "stepper" && ( + widgetData={widgetData[i]} + reloadWidgetCallback={(data) => reloadWidget(i, data)} + > @@ -276,7 +272,11 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit } { widgetMetaData.type === "html" && ( - + reloadWidget(i, data)} + widgetData={widgetData[i]} + > { @@ -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)} > reloadWidget(i, data)} isChild={areChildren} >
@@ -379,7 +379,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit + reloadWidgetCallback={(data) => reloadWidget(i, data)} + isChild={areChildren} + > 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 (
Unsupported component type
) + } } @@ -86,6 +103,15 @@ export class HeaderLink extends LabelComponent this.label = label; this.to = to; } + + render = (args: LabelComponentRenderArgs): JSX.Element => + { + return ( + + {this.to ? {this.label} : null} + + ); + } } @@ -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 ( + + + + ); + } +} + + +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 ( + + + + ); + } } @@ -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 ( + + + + ); + } +} + + +export class ReloadControl extends LabelComponent +{ + callback: () => void; + + constructor(callback: () => void) + { + super(); + this.callback = callback; + } + + render = (args: LabelComponentRenderArgs): JSX.Element => + { + return ( + + + + ); + } } @@ -132,64 +247,11 @@ function Widget(props: React.PropsWithChildren): 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 ( - - {link.to ? {link.label} : null} - - ); - } - - if (component instanceof AddNewRecordButton) - { - const addNewRecordButton = component as AddNewRecordButton - return ( - - - - ); - } - - 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 ( - - - - ); - } - - return (
Unsupported component type.
) + return component.render({navigate: navigate, widgetProps: props, dropdownData: dropdownData, componentIndex: componentIndex, reloadFunction: doReload}) } @@ -209,6 +271,27 @@ function Widget(props: React.PropsWithChildren): 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): JSX.Element const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true; const widgetContent = - + { hasPermission ? @@ -367,7 +450,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element */} { hasPermission && ( - props.labelAdditionalComponentsLeft.map((component, i) => + effectiveLabelAdditionalComponentsLeft.map((component, i) => { return ({renderComponent(component, i)}); }) @@ -385,6 +468,9 @@ function Widget(props: React.PropsWithChildren): JSX.Element } + { + props.widgetMetaData?.isCard && (reloading ? : ) + } { hasPermission && props.widgetData?.dropdownNeedsSelectedText ? ( @@ -407,7 +493,11 @@ function Widget(props: React.PropsWithChildren): JSX.Element } ; - return props.widgetMetaData?.isCard ? {widgetContent} : widgetContent; + return props.widgetMetaData?.isCard + ? + {widgetContent} + + : widgetContent; } export default Widget; diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 419934b..2daa5fc 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -123,6 +123,7 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element return ( diff --git a/src/qqq/components/widgets/tables/TableWidget.tsx b/src/qqq/components/widgets/tables/TableWidget.tsx new file mode 100644 index 0000000..156f4ec --- /dev/null +++ b/src/qqq/components/widgets/tables/TableWidget.tsx @@ -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 . + */ + + +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 ( + props.reloadWidgetCallback(data)} + footerHTML={props.widgetData?.footerHTML} + isChild={props.isChild} + labelAdditionalComponentsLeft={componentLeft} + > + + + ); +} + +export default TableWidget; From 75268b6b3c4843aa368e258a982089786465a8a7 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 19 May 2023 09:14:42 -0500 Subject: [PATCH 3/5] Some final adjustments for widget reload & export --- package.json | 2 +- src/qqq/components/widgets/Widget.tsx | 41 +++++++++++++---- .../components/widgets/tables/TableWidget.tsx | 44 +++++++------------ src/qqq/utils/qqq/ValueUtils.tsx | 8 ++++ 4 files changed, 57 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 49f6f83..1797de4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.64", + "@kingsrook/qqq-frontend-core": "1.0.65", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index a44ed1a..cd23c2e 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -26,10 +26,12 @@ 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 Tooltip from "@mui/material/Tooltip/Tooltip"; import Typography from "@mui/material/Typography"; import parse from "html-react-parser"; import React, {useEffect, useState} from "react"; import {Link, useNavigate, NavigateFunction} from "react-router-dom"; +import {bool} from "yup"; import colors from "qqq/components/legacy/colors"; import DropdownMenu, {DropdownOption} from "qqq/components/widgets/components/DropdownMenu"; @@ -166,8 +168,8 @@ export class ExportDataButton extends LabelComponent render = (args: LabelComponentRenderArgs): JSX.Element => { return ( - - + + ); } @@ -231,8 +233,8 @@ export class ReloadControl extends LabelComponent render = (args: LabelComponentRenderArgs): JSX.Element => { return ( - - + + ); } @@ -283,16 +285,15 @@ function Widget(props: React.PropsWithChildren): JSX.Element }, [props.widgetData]); const effectiveLabelAdditionalComponentsLeft: LabelComponent[] = []; + if(props.reloadWidgetCallback && props.widgetData && props.showReloadControl && props.widgetMetaData.showReloadButton) + { + effectiveLabelAdditionalComponentsLeft.push(new ReloadControl(doReload)) + } 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) { if(dropdownData) @@ -380,8 +381,29 @@ function Widget(props: React.PropsWithChildren): JSX.Element } const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true; + + const isSet = (v: any): boolean => + { + return(v !== null && v !== undefined); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // to avoid taking up the space of the Box with the label and icon and label-components (since it has a height), only output that box if we need any of the components // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + let needLabelBox = false; + if(hasPermission) + { + needLabelBox ||= (effectiveLabelAdditionalComponentsLeft && effectiveLabelAdditionalComponentsLeft.length > 0); + needLabelBox ||= (effectiveLabelAdditionalComponentsRight && effectiveLabelAdditionalComponentsRight.length > 0); + needLabelBox ||= isSet(props.widgetMetaData?.icon); + needLabelBox ||= isSet(props.widgetData?.label); + needLabelBox ||= isSet(props.widgetMetaData?.label); + } + const widgetContent = + { + needLabelBox && { @@ -468,6 +490,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element } + } { props.widgetMetaData?.isCard && (reloading ? : ) } diff --git a/src/qqq/components/widgets/tables/TableWidget.tsx b/src/qqq/components/widgets/tables/TableWidget.tsx index 156f4ec..2aad023 100644 --- a/src/qqq/components/widgets/tables/TableWidget.tsx +++ b/src/qqq/components/widgets/tables/TableWidget.tsx @@ -56,9 +56,22 @@ function download(filename: string, text: string) function TableWidget(props: Props): JSX.Element { + const [isExportDisabled, setIsExportDisabled] = useState(true); + const rows = props.widgetData?.rows; const columns = props.widgetData?.columns; + useEffect(() => + { + let isExportDisabled = true; + if (props.widgetData && columns && rows && rows.length > 0) + { + isExportDisabled = false; + } + setIsExportDisabled(isExportDisabled); + + }, [props.widgetMetaData, props.widgetData]); + const exportCallback = () => { if (props.widgetData && rows && columns) @@ -101,40 +114,15 @@ function TableWidget(props: Props): JSX.Element console.log(csv); - const fileName = props.widgetData.label + "-" + ValueUtils.formatDateTimeISO8601(new Date()) + ".csv"; + const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; download(fileName, csv); } else { - alert("Error exporting widget data."); + alert("There is no data available to export."); } }; - - 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 ( props.reloadWidgetCallback(data)} footerHTML={props.widgetData?.footerHTML} isChild={props.isChild} - labelAdditionalComponentsLeft={componentLeft} + labelAdditionalComponentsLeft={props.widgetMetaData?.showExportButton ? [new ExportDataButton(() => exportCallback(), isExportDisabled)] : []} > (value < 10 ? `0${value}` : `${value}`); + const d = new Date(); + const dateString = `${d.getFullYear()}-${zp(d.getMonth() + 1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`; + return (date); + } + public static getFullWeekday(date: Date) { if (!(date instanceof Date)) From 1011271b5e74b51d9655ba38dc0a25bfd1c42b06 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 19 May 2023 11:17:16 -0500 Subject: [PATCH 4/5] Add export to ColumnStats; fix formatDateTimeForFileName --- src/qqq/pages/records/query/ColumnStats.tsx | 46 +++++++++++++++++++-- src/qqq/utils/qqq/ValueUtils.tsx | 2 +- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index e84dab0..6e48eaa 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -54,6 +54,21 @@ ColumnStats.defaultProps = { const qController = Client.getInstance(); +// todo - merge w/ same function in TableWidget +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 ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Props): JSX.Element { const [statusString, setStatusString] = useState("Calculating statistics..."); @@ -97,6 +112,8 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro } else { + // todo - job running! + const result = processResult as QJobComplete; const statFieldObjects = result.values.statsFields; @@ -174,6 +191,24 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro setStatusString("Refreshing...") } + const doExport = () => + { + let csv = `"${fieldMetaData.label}","Count"\n`; + for (let i = 0; i < valueCounts.length; i++) + { + let fieldValue = valueCounts[i].displayValues.get(fieldMetaData.name); + if(fieldValue === undefined) + { + fieldValue = ""; + } + + csv += `"${fieldValue}",${valueCounts[i].values.get("count")}\n`; + } + + const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; + download(fileName, csv); + } + function Loading() { return ( @@ -200,9 +235,14 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro {statusString ?? <> } - + + + + diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx index c6de9b1..ccb68d0 100644 --- a/src/qqq/utils/qqq/ValueUtils.tsx +++ b/src/qqq/utils/qqq/ValueUtils.tsx @@ -278,7 +278,7 @@ class ValueUtils const zp = (value: number): string => (value < 10 ? `0${value}` : `${value}`); const d = new Date(); const dateString = `${d.getFullYear()}-${zp(d.getMonth() + 1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`; - return (date); + return (dateString); } public static getFullWeekday(date: Date) From 3a7cadf5c20d1bf44e9357c4152d44b78b316341 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 19 May 2023 11:51:20 -0500 Subject: [PATCH 5/5] Clean csv values; Update qfc - for audit count fix --- package.json | 2 +- src/qqq/components/widgets/tables/TableWidget.tsx | 2 +- src/qqq/pages/records/query/ColumnStats.tsx | 12 ++++-------- src/qqq/utils/qqq/ValueUtils.tsx | 13 +++++++++++++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 1797de4..65b58a2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.65", + "@kingsrook/qqq-frontend-core": "1.0.66", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/widgets/tables/TableWidget.tsx b/src/qqq/components/widgets/tables/TableWidget.tsx index 2aad023..0f66067 100644 --- a/src/qqq/components/widgets/tables/TableWidget.tsx +++ b/src/qqq/components/widgets/tables/TableWidget.tsx @@ -107,7 +107,7 @@ function TableWidget(props: Props): JSX.Element {selector: ".button", format: "skip"} ] }); - csv += `"${text}"`; + csv += `"${ValueUtils.cleanForCsv(text)}"`; } csv += "\n"; } diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index 6e48eaa..65d6077 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -193,16 +193,12 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro const doExport = () => { - let csv = `"${fieldMetaData.label}","Count"\n`; + let csv = `"${ValueUtils.cleanForCsv(fieldMetaData.label)}","Count"\n`; for (let i = 0; i < valueCounts.length; i++) { - let fieldValue = valueCounts[i].displayValues.get(fieldMetaData.name); - if(fieldValue === undefined) - { - fieldValue = ""; - } - - csv += `"${fieldValue}",${valueCounts[i].values.get("count")}\n`; + const fieldValue = valueCounts[i].displayValues.get(fieldMetaData.name); + const count = valueCounts[i].values.get("count"); + csv += `"${ValueUtils.cleanForCsv(fieldValue)}",${count}\n`; } const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; diff --git a/src/qqq/utils/qqq/ValueUtils.tsx b/src/qqq/utils/qqq/ValueUtils.tsx index ccb68d0..d8c1332 100644 --- a/src/qqq/utils/qqq/ValueUtils.tsx +++ b/src/qqq/utils/qqq/ValueUtils.tsx @@ -418,6 +418,19 @@ class ValueUtils return toPush; } + + /******************************************************************************* + ** for building CSV in frontends, cleanse null & undefined, and escape "'s + *******************************************************************************/ + public static cleanForCsv(param: any): string + { + if(param === undefined || param === null) + { + return (""); + } + + return (String(param).replaceAll(/"/g, "\"\"")); + } } ////////////////////////////////////////////////////////////////////////////////////////////////