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