From 6df245ca9954ff82689145d0cfc158d26edd0da1 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 12 May 2023 15:10:16 -0500 Subject: [PATCH 1/9] Switch to use NOT_EQUALS_OR_IS_NULL instead of NOT_EQUALS --- package.json | 2 +- src/qqq/utils/qqq/FilterUtils.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a258814..bda2769 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.61", + "@kingsrook/qqq-frontend-core": "1.0.63", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts index 7c56bf6..0aebaf7 100644 --- a/src/qqq/utils/qqq/FilterUtils.ts +++ b/src/qqq/utils/qqq/FilterUtils.ts @@ -65,7 +65,7 @@ class FilterUtils return QCriteriaOperator.EQUALS; case "isNot": case "!=": - return QCriteriaOperator.NOT_EQUALS; + return QCriteriaOperator.NOT_EQUALS_OR_IS_NULL; case "after": case ">": return QCriteriaOperator.GREATER_THAN; @@ -138,6 +138,7 @@ class FilterUtils return ("is"); } case QCriteriaOperator.NOT_EQUALS: + case QCriteriaOperator.NOT_EQUALS_OR_IS_NULL: if (field.possibleValueSourceName) { From 813067be257f3f2344cdde0bc3ea77d5ad8b1693 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 18 May 2023 15:48:39 -0500 Subject: [PATCH 2/9] 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:51:46 -0500 Subject: [PATCH 3/9] updated to show error if widgets dont load correctly, tried to make 'big icons' more specific and an 'opt in' --- .../components/widgets/DashboardWidgets.tsx | 32 +++++++++++++++---- src/qqq/components/widgets/Widget.tsx | 32 +++++++++++-------- src/qqq/pages/apps/Home.tsx | 5 +-- src/qqq/styles/qqq-override-styles.css | 9 +++--- 4 files changed, 51 insertions(+), 27 deletions(-) diff --git a/src/qqq/components/widgets/DashboardWidgets.tsx b/src/qqq/components/widgets/DashboardWidgets.tsx index 9929d40..b870b85 100644 --- a/src/qqq/components/widgets/DashboardWidgets.tsx +++ b/src/qqq/components/widgets/DashboardWidgets.tsx @@ -97,9 +97,19 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit widgetData[i] = {}; (async () => { - widgetData[i] = await qController.widget(widgetMetaData.name, urlParams); - setWidgetData(widgetData); - setWidgetCounter(widgetCounter + 1); + try + { + widgetData[i] = await qController.widget(widgetMetaData.name, urlParams); + setWidgetData(widgetData); + setWidgetCounter(widgetCounter + 1); + widgetData[i]["errorLoading"] = false; + } + catch(e) + { + console.error(e); + widgetData[i]["errorLoading"] = true; + } + forceUpdate(); })(); } @@ -112,9 +122,19 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit const urlParams = getQueryParams(widgetMetaDataList[index], data); setCurrentUrlParams(urlParams); - widgetData[index] = await qController.widget(widgetMetaDataList[index].name, urlParams); - setWidgetCounter(widgetCounter + 1); - setWidgetData(widgetData); + try + { + widgetData[index] = await qController.widget(widgetMetaDataList[index].name, urlParams); + setWidgetCounter(widgetCounter + 1); + setWidgetData(widgetData); + widgetData[index]["errorLoading"] = false; + } + catch(e) + { + console.error(e); + widgetData[index]["errorLoading"] = true; + } + forceUpdate(); })(); }; diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index 21f0582..5e5a4d3 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -43,6 +43,7 @@ export interface WidgetData }[][]; dropdownNeedsSelectedText?: string; hasPermission?: boolean; + errorLoading?: boolean; } @@ -297,6 +298,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element } const hasPermission = props.widgetData?.hasPermission === undefined || props.widgetData?.hasPermission === true; + const errorLoading = props.widgetData?.errorLoading !== undefined && props.widgetData?.errorLoading === true; const widgetContent = @@ -360,11 +362,6 @@ function Widget(props: React.PropsWithChildren): JSX.Element ) ) } - {/* - - */} { hasPermission && ( props.labelAdditionalComponentsLeft.map((component, i) => @@ -386,22 +383,29 @@ function Widget(props: React.PropsWithChildren): JSX.Element { - hasPermission && props.widgetData?.dropdownNeedsSelectedText ? ( - - - {props.widgetData?.dropdownNeedsSelectedText} - + errorLoading ? ( + + error + An error occurred loading widget content. ) : ( - hasPermission ? ( - props.children + hasPermission && props.widgetData?.dropdownNeedsSelectedText ? ( + + + {props.widgetData?.dropdownNeedsSelectedText} + + ) : ( - You do not have permission to view this data. + hasPermission ? ( + props.children + ) : ( + You do not have permission to view this data. + ) ) ) } { - props?.footerHTML && ( + ! errorLoading && props?.footerHTML && ( {parse(props.footerHTML)} ) } diff --git a/src/qqq/pages/apps/Home.tsx b/src/qqq/pages/apps/Home.tsx index db4f326..58639d6 100644 --- a/src/qqq/pages/apps/Home.tsx +++ b/src/qqq/pages/apps/Home.tsx @@ -26,7 +26,8 @@ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/ import {QReportMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QReportMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; -import {Box, Icon, Typography} from "@mui/material"; +import {Icon, Typography} from "@mui/material"; +import Box from "@mui/material/Box"; import Card from "@mui/material/Card"; import Divider from "@mui/material/Divider"; import Grid from "@mui/material/Grid"; @@ -316,7 +317,7 @@ function AppHome({app}: Props): JSX.Element {hasTablePermission(tableName) ? - + .MuiBox-root > .material-icons-round, -.MuiBox-root > .MuiBox-root > .material-icons-round +.big-icon .material-icons-round { font-size: 2rem !important; } .dashboard-schedule-icon { - font-size: 1rem !important; + font-size: 1.1rem !important; position: relative; - top: -13px; - margin-right: 3px; + top: -5px; + margin-right: 8px; } From 65652f04f0aac2df6153e5cddd93d01edfde67ae Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 18 May 2023 15:52:40 -0500 Subject: [PATCH 4/9] 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 96bc57f5f9a18f749ae4a1c64897ed4bef88a023 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 18 May 2023 19:36:16 -0500 Subject: [PATCH 5/9] fixed layout, null checks --- .../components/widgets/DashboardWidgets.tsx | 24 +++++++++++++++---- src/qqq/components/widgets/Widget.tsx | 2 +- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/qqq/components/widgets/DashboardWidgets.tsx b/src/qqq/components/widgets/DashboardWidgets.tsx index b870b85..831e735 100644 --- a/src/qqq/components/widgets/DashboardWidgets.tsx +++ b/src/qqq/components/widgets/DashboardWidgets.tsx @@ -102,12 +102,18 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit widgetData[i] = await qController.widget(widgetMetaData.name, urlParams); setWidgetData(widgetData); setWidgetCounter(widgetCounter + 1); - widgetData[i]["errorLoading"] = false; + if(widgetData[i]) + { + widgetData[i]["errorLoading"] = false; + } } catch(e) { console.error(e); - widgetData[i]["errorLoading"] = true; + if(widgetData[i]) + { + widgetData[i]["errorLoading"] = true; + } } forceUpdate(); @@ -121,23 +127,31 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, omit { const urlParams = getQueryParams(widgetMetaDataList[index], data); setCurrentUrlParams(urlParams); + widgetData[index] = {}; try { widgetData[index] = await qController.widget(widgetMetaDataList[index].name, urlParams); setWidgetCounter(widgetCounter + 1); setWidgetData(widgetData); - widgetData[index]["errorLoading"] = false; + + if (widgetData[index]) + { + widgetData[index]["errorLoading"] = false; + } } catch(e) { console.error(e); - widgetData[index]["errorLoading"] = true; + if (widgetData[index]) + { + widgetData[index]["errorLoading"] = true; + } } forceUpdate(); })(); - }; + } function getQueryParams(widgetMetaData: QWidgetMetaData, extraParams: string): string { diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index 5e5a4d3..447cfc9 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -384,7 +384,7 @@ function Widget(props: React.PropsWithChildren): JSX.Element { errorLoading ? ( - + error An error occurred loading widget content. From 75268b6b3c4843aa368e258a982089786465a8a7 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 19 May 2023 09:14:42 -0500 Subject: [PATCH 6/9] 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 7/9] 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 8/9] 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, "\"\"")); + } } //////////////////////////////////////////////////////////////////////////////////////////////// From f7ff4cf2fc16e56b0013e34e3ac449c9aab57f4d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 19 May 2023 14:54:57 -0500 Subject: [PATCH 9/9] Add export to RecordGridWidget --- src/qqq/components/widgets/Widget.tsx | 8 +-- .../widgets/misc/RecordGridWidget.tsx | 62 ++++++++++++++++++- .../components/widgets/tables/TableWidget.tsx | 18 +----- src/qqq/pages/records/query/ColumnStats.tsx | 19 +----- src/qqq/utils/HtmlUtils.ts | 62 +++++++++++++++++++ 5 files changed, 131 insertions(+), 38 deletions(-) create mode 100644 src/qqq/utils/HtmlUtils.ts diff --git a/src/qqq/components/widgets/Widget.tsx b/src/qqq/components/widgets/Widget.tsx index 32c320e..6ad8a89 100644 --- a/src/qqq/components/widgets/Widget.tsx +++ b/src/qqq/components/widgets/Widget.tsx @@ -155,22 +155,22 @@ export class AddNewRecordButton extends LabelComponent export class ExportDataButton extends LabelComponent { callbackToExport: any; - label: string; + tooltipTitle: string; isDisabled: boolean; - constructor(callbackToExport: any, isDisabled = false, label: string = "Export") + constructor(callbackToExport: any, isDisabled = false, tooltipTitle: string = "Export") { super(); this.callbackToExport = callbackToExport; this.isDisabled = isDisabled; - this.label = label; + this.tooltipTitle = tooltipTitle; } render = (args: LabelComponentRenderArgs): JSX.Element => { return ( - + ); } diff --git a/src/qqq/components/widgets/misc/RecordGridWidget.tsx b/src/qqq/components/widgets/misc/RecordGridWidget.tsx index 2daa5fc..6f02ba8 100644 --- a/src/qqq/components/widgets/misc/RecordGridWidget.tsx +++ b/src/qqq/components/widgets/misc/RecordGridWidget.tsx @@ -25,9 +25,11 @@ import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {DataGridPro, GridCallbackDetails, GridRowParams, MuiEvent} from "@mui/x-data-grid-pro"; import React, {useEffect, useState} from "react"; import {useNavigate} from "react-router-dom"; -import Widget, {AddNewRecordButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget"; +import Widget, {AddNewRecordButton, ExportDataButton, HeaderLink, LabelComponent} from "qqq/components/widgets/Widget"; import DataGridUtils from "qqq/utils/DataGridUtils"; +import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; +import ValueUtils from "qqq/utils/qqq/ValueUtils"; interface Props { @@ -42,7 +44,9 @@ const qController = Client.getInstance(); function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element { const [rows, setRows] = useState([]); + const [records, setRecords] = useState([] as QRecord[]) const [columns, setColumns] = useState([]); + const [allColumns, setAllColumns] = useState([]) const navigate = useNavigate(); useEffect(() => @@ -68,6 +72,11 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element const childTablePath = data.tablePath ? data.tablePath + (data.tablePath.endsWith("/") ? "" : "/") : data.tablePath; const columns = DataGridUtils.setupGridColumns(tableMetaData, childTablePath, null, "bySection"); + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // capture all-columns to use for the export (before we might splice some away from the on-screen display) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + setAllColumns(JSON.parse(JSON.stringify(columns))); + //////////////////////////////////////////////////////////////// // do not not show the foreign-key column of the parent table // //////////////////////////////////////////////////////////////// @@ -84,16 +93,67 @@ function RecordGridWidget({widgetMetaData, data}: Props): JSX.Element } setRows(rows); + setRecords(records) setColumns(columns); } }, [data]); + const exportCallback = () => + { + let csv = ""; + for (let i = 0; i < allColumns.length; i++) + { + csv += `${i > 0 ? "," : ""}"${ValueUtils.cleanForCsv(allColumns[i].headerName)}"` + } + csv += "\n"; + + for (let i = 0; i < records.length; i++) + { + for (let j = 0; j < allColumns.length; j++) + { + const value = records[i].displayValues.get(allColumns[j].field) ?? records[i].values.get(allColumns[j].field) + csv += `${j > 0 ? "," : ""}"${ValueUtils.cleanForCsv(value)}"` + } + csv += "\n"; + } + + const fileName = (data?.label ?? widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; + HtmlUtils.download(fileName, csv); + } + + /////////////////// + // view all link // + /////////////////// const labelAdditionalComponentsLeft: LabelComponent[] = [] if(data && data.viewAllLink) { labelAdditionalComponentsLeft.push(new HeaderLink("View All", data.viewAllLink)); } + /////////////////// + // export button // + /////////////////// + let isExportDisabled = true; + let tooltipTitle = "Export"; + if (data && data.childTableMetaData && data.queryOutput && data.queryOutput.records && data.queryOutput.records.length > 0) + { + isExportDisabled = false; + + if(data.totalRows && data.queryOutput.records.length < data.totalRows) + { + tooltipTitle = "Export these " + data.queryOutput.records.length + " records." + if(data.viewAllLink) + { + tooltipTitle += "\nClick View All to export all records."; + } + } + } + + labelAdditionalComponentsLeft.push(new ExportDataButton(() => exportCallback(), isExportDisabled, tooltipTitle)) + + //////////////////// + // add new button // + //////////////////// const labelAdditionalComponentsRight: LabelComponent[] = [] if(data && data.canAddChildRecord) { diff --git a/src/qqq/components/widgets/tables/TableWidget.tsx b/src/qqq/components/widgets/tables/TableWidget.tsx index 0f66067..a9e98c4 100644 --- a/src/qqq/components/widgets/tables/TableWidget.tsx +++ b/src/qqq/components/widgets/tables/TableWidget.tsx @@ -26,6 +26,7 @@ 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 HtmlUtils from "qqq/utils/HtmlUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; interface Props @@ -37,23 +38,8 @@ interface Props } 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 [isExportDisabled, setIsExportDisabled] = useState(true); @@ -115,7 +101,7 @@ function TableWidget(props: Props): JSX.Element console.log(csv); const fileName = (props.widgetData.label ?? props.widgetMetaData.label) + " " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; - download(fileName, csv); + HtmlUtils.download(fileName, csv); } else { diff --git a/src/qqq/pages/records/query/ColumnStats.tsx b/src/qqq/pages/records/query/ColumnStats.tsx index 65d6077..63743a2 100644 --- a/src/qqq/pages/records/query/ColumnStats.tsx +++ b/src/qqq/pages/records/query/ColumnStats.tsx @@ -27,7 +27,6 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; -import {TablePagination} from "@mui/material"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Grid from "@mui/material/Grid"; @@ -38,6 +37,7 @@ import {DataGridPro, GridSortModel} from "@mui/x-data-grid-pro"; import FormData from "form-data"; import React, {useEffect, useState} from "react"; import DataGridUtils from "qqq/utils/DataGridUtils"; +import HtmlUtils from "qqq/utils/HtmlUtils"; import Client from "qqq/utils/qqq/Client"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; @@ -54,21 +54,6 @@ 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..."); @@ -202,7 +187,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro } const fileName = tableMetaData.label + " - " + fieldMetaData.label + " Column Stats " + ValueUtils.formatDateTimeForFileName(new Date()) + ".csv"; - download(fileName, csv); + HtmlUtils.download(fileName, csv); } function Loading() diff --git a/src/qqq/utils/HtmlUtils.ts b/src/qqq/utils/HtmlUtils.ts new file mode 100644 index 0000000..5197867 --- /dev/null +++ b/src/qqq/utils/HtmlUtils.ts @@ -0,0 +1,62 @@ +/* + * 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 . + */ + +/******************************************************************************* + ** Utility functions for basic html/webpage/browser things. + *******************************************************************************/ +export default class HtmlUtils +{ + + /******************************************************************************* + ** Since our pages are set (w/ style on the HTML element) to smooth scroll, + ** if you ever want to do an "auto" scroll (e.g., instant, not smooth), you can + ** call this method, which will remove that style, and then put it back. + *******************************************************************************/ + static autoScroll = (top: number, left: number = 0) => + { + let htmlElement = document.querySelector("html"); + const initialScrollBehavior = htmlElement.style.scrollBehavior; + htmlElement.style.scrollBehavior = "auto"; + setTimeout(() => + { + window.scrollTo({top: top, left: left, behavior: "auto"}); + htmlElement.style.scrollBehavior = initialScrollBehavior; + }); + }; + + /******************************************************************************* + ** Download a client-side generated file (e.g., csv). + *******************************************************************************/ + static 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); + }; + +} \ No newline at end of file