QQQ-26 add export to entity-list. required http-proxy-middleware

This commit is contained in:
2022-07-19 13:04:40 -05:00
parent be42a98d4d
commit 8a33207966
5 changed files with 123 additions and 15 deletions

View File

@ -34,6 +34,7 @@
"flatpickr": "4.6.9", "flatpickr": "4.6.9",
"formik": "2.2.9", "formik": "2.2.9",
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"http-proxy-middleware": "2.0.6",
"react": "17.0.2", "react": "17.0.2",
"react-chartjs-2": "3.0.4", "react-chartjs-2": "3.0.4",
"react-dom": "17.0.2", "react-dom": "17.0.2",

View File

@ -20,7 +20,7 @@ import Icon from "@mui/material/Icon";
import Menu from "@mui/material/Menu"; import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import {Alert} from "@mui/material"; import {Alert, tableFooterClasses} from "@mui/material";
import { import {
DataGridPro, DataGridPro,
GridCallbackDetails, GridCallbackDetails,
@ -38,7 +38,9 @@ import {
GridToolbarContainer, GridToolbarContainer,
GridToolbarDensitySelector, GridToolbarDensitySelector,
GridToolbarExport, GridToolbarExport,
GridToolbarExportContainer,
GridToolbarFilterButton, GridToolbarFilterButton,
GridExportMenuItemProps,
} from "@mui/x-data-grid-pro"; } from "@mui/x-data-grid-pro";
// Material Dashboard 2 PRO React TS components // Material Dashboard 2 PRO React TS components
@ -96,6 +98,7 @@ function EntityList({table}: Props): JSX.Element
const [buttonText, setButtonText] = useState(""); const [buttonText, setButtonText] = useState("");
const [tableState, setTableState] = useState(""); const [tableState, setTableState] = useState("");
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [, setFiltersMenu] = useState(null); const [, setFiltersMenu] = useState(null);
const [actionsMenu, setActionsMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
@ -189,11 +192,12 @@ function EntityList({table}: Props): JSX.Element
{ {
(async () => (async () =>
{ {
const tableMetaData = await QClient.loadTableMetaData(tableName); const newTableMetaData = await QClient.loadTableMetaData(tableName);
setTableMetaData(newTableMetaData);
if (columnSortModel.length === 0) if (columnSortModel.length === 0)
{ {
columnSortModel.push({ columnSortModel.push({
field: tableMetaData.primaryKeyField, field: newTableMetaData.primaryKeyField,
sort: "desc", sort: "desc",
}); });
setColumnSortModel(columnSortModel); setColumnSortModel(columnSortModel);
@ -203,8 +207,8 @@ function EntityList({table}: Props): JSX.Element
const count = await QClient.count(tableName, qFilter); const count = await QClient.count(tableName, qFilter);
setTotalRecords(count); setTotalRecords(count);
setButtonText(`new ${tableMetaData.label}`); setButtonText(`new ${newTableMetaData.label}`);
setTableLabel(tableMetaData.label); setTableLabel(newTableMetaData.label);
const columns = [] as GridColDef[]; const columns = [] as GridColDef[];
@ -233,10 +237,10 @@ function EntityList({table}: Props): JSX.Element
rows.push(Object.fromEntries(record.values.entries())); rows.push(Object.fromEntries(record.values.entries()));
}); });
const sortedKeys = [...tableMetaData.fields.keys()].sort(); const sortedKeys = [...newTableMetaData.fields.keys()].sort();
sortedKeys.forEach((key) => sortedKeys.forEach((key) =>
{ {
const field = tableMetaData.fields.get(key); const field = newTableMetaData.fields.get(key);
let columnType = "string"; let columnType = "string";
switch (field.type) switch (field.type)
@ -265,7 +269,7 @@ function EntityList({table}: Props): JSX.Element
width: 200, width: 200,
}; };
if (key === tableMetaData.primaryKeyField) if (key === newTableMetaData.primaryKeyField)
{ {
column.width = 75; column.width = 75;
columns.splice(0, 0, column); columns.splice(0, 0, column);
@ -366,6 +370,67 @@ function EntityList({table}: Props): JSX.Element
})(); })();
} }
interface QExportMenuItemProps extends GridExportMenuItemProps<{}>
{
format: string;
}
function ExportMenuItem(props: QExportMenuItemProps)
{
const {format, hideMenu} = props;
return (
<MenuItem
disabled={totalRecords === 0}
onClick={() =>
{
///////////////////////////////////////////////////////////////////////////////
// build the list of visible fields. note, not doing them in-order (in case //
// the user did drag & drop), because column order model isn't right yet //
// so just doing them to match columns (which were pKey, then sorted) //
///////////////////////////////////////////////////////////////////////////////
const visibleFields: string[] = [];
columns.forEach((gridColumn) =>
{
const fieldName = gridColumn.field;
// @ts-ignore
if (columnVisibilityModel[fieldName] !== false)
{
visibleFields.push(fieldName);
}
});
const zp = (value: number): string => (value < 10 ? `0${value}` : `${value}`);
//////////////////////////////////////
// construct the url for the export //
//////////////////////////////////////
const d = new Date();
const dateString = `${d.getFullYear()}-${zp(d.getMonth())}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`;
const filename = `${tableMetaData.label} Export ${dateString}.${format}`;
const url = `/data/${tableMetaData.name}/export/${filename}?filter=${JSON.stringify(buildQFilter())}&fields=${visibleFields.join(",")}`;
////////////////////////////////////
// create an 'a' tag and click it //
////////////////////////////////////
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.target = "_blank";
a.click();
///////////////////////////////////////////
// Hide the export menu after the export //
///////////////////////////////////////////
hideMenu?.();
}}
>
Export
{` ${format.toUpperCase()}`}
</MenuItem>
);
}
function CustomToolbar() function CustomToolbar()
{ {
return ( return (
@ -373,7 +438,10 @@ function EntityList({table}: Props): JSX.Element
<GridToolbarColumnsButton /> <GridToolbarColumnsButton />
<GridToolbarFilterButton /> <GridToolbarFilterButton />
<GridToolbarDensitySelector /> <GridToolbarDensitySelector />
<GridToolbarExport /> <GridToolbarExportContainer>
<ExportMenuItem format="csv" />
<ExportMenuItem format="xlsx" />
</GridToolbarExportContainer>
<div> <div>
{ {
selectFullFilterState === "checked" && ( selectFullFilterState === "checked" && (

View File

@ -46,8 +46,8 @@ import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJo
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError"; import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning"; import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
import { import {
DataGrid, GridColDef, GridRowParams, GridRowsProp, DataGridPro, GridColDef, GridRowParams, GridRowsProp,
} from "@mui/x-data-grid"; } from "@mui/x-data-grid-pro";
import QDynamicForm from "../../components/QDynamicForm"; import QDynamicForm from "../../components/QDynamicForm";
import MDTypography from "../../../components/MDTypography"; import MDTypography from "../../../components/MDTypography";
@ -108,7 +108,7 @@ function getDynamicStepContent(
{" "} {" "}
<br /> <br />
<MDBox height="100%"> <MDBox height="100%">
<DataGrid <DataGridPro
page={recordConfig.pageNo} page={recordConfig.pageNo}
disableSelectionOnClick disableSelectionOnClick
autoHeight autoHeight

39
src/setupProxy.js Normal file
View File

@ -0,0 +1,39 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/////////////////////////////////////////////////////////////////////////////////////
// this file "magically" works with http-proxy-middleware. //
// Most API calls to the qqq backend (e.g., through QController) do NOT go through //
// the React Router. However, exports do (presumably because they are full- //
// page style requests, not ajax/fetches), so they need specific proxy config. //
/////////////////////////////////////////////////////////////////////////////////////
const {createProxyMiddleware} = require("http-proxy-middleware");
module.exports = function (app)
{
app.use(
"/data/*/export/*",
createProxyMiddleware({
target: "http://localhost:8000",
changeOrigin: true,
}),
);
};