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