ONE-39: added filters, order by, pagination

This commit is contained in:
Tim Chamberlain
2022-07-12 10:28:58 -05:00
parent 71dc8f6ef9
commit cc324fd76d
10 changed files with 321 additions and 121 deletions

View File

@ -1,7 +1,6 @@
{ {
"printWidth": 100, "printWidth": 100,
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 2,
"semi": true, "semi": true,
"singleQuote": false, "singleQuote": false,
"endOfLine": "auto" "endOfLine": "auto"

60
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.4.1", "@mui/material": "5.4.1",
"@mui/styled-engine": "5.4.1", "@mui/styled-engine": "5.4.1",
"@mui/x-data-grid": "5.13.0",
"@react-jvectormap/core": "1.0.1", "@react-jvectormap/core": "1.0.1",
"@react-jvectormap/world": "1.0.0", "@react-jvectormap/world": "1.0.0",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
@ -3291,6 +3292,31 @@
"react": "^17.0.0 || ^18.0.0" "react": "^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/@mui/x-data-grid": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-5.13.0.tgz",
"integrity": "sha512-x310qsOJFIT0JuqnDusM6DC4hlX6eeL9biEveb/y+hdeLa+VHSwrclS0M7e9UUwmh1MPza6VT4ceEWJjbySf3A==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"@mui/utils": "^5.4.1",
"clsx": "^1.2.1",
"prop-types": "^15.8.1",
"reselect": "^4.1.6"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
},
"peerDependencies": {
"@mui/material": "^5.4.1",
"@mui/system": "^5.4.1",
"react": "^17.0.2 || ^18.0.0",
"react-dom": "^17.0.2 || ^18.0.0"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -5934,9 +5960,9 @@
} }
}, },
"node_modules/clsx": { "node_modules/clsx": {
"version": "1.1.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -14466,6 +14492,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
}, },
"node_modules/reselect": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz",
"integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ=="
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.1", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@ -19213,6 +19244,18 @@
"react-is": "^17.0.2" "react-is": "^17.0.2"
} }
}, },
"@mui/x-data-grid": {
"version": "5.13.0",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-5.13.0.tgz",
"integrity": "sha512-x310qsOJFIT0JuqnDusM6DC4hlX6eeL9biEveb/y+hdeLa+VHSwrclS0M7e9UUwmh1MPza6VT4ceEWJjbySf3A==",
"requires": {
"@babel/runtime": "^7.17.2",
"@mui/utils": "^5.4.1",
"clsx": "^1.2.1",
"prop-types": "^15.8.1",
"reselect": "^4.1.6"
}
},
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -21178,9 +21221,9 @@
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==" "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="
}, },
"clsx": { "clsx": {
"version": "1.1.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==" "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
}, },
"co": { "co": {
"version": "4.6.0", "version": "4.6.0",
@ -27233,6 +27276,11 @@
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
}, },
"reselect": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.6.tgz",
"integrity": "sha512-ZovIuXqto7elwnxyXbBtCPo9YFEr3uJqj2rRbcOOog1bmu2Ag85M4hixSwFWyaBMKXNgvPaJ9OSu9SkBPIeJHQ=="
},
"resolve": { "resolve": {
"version": "1.22.1", "version": "1.22.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",

View File

@ -12,7 +12,7 @@
"@fullcalendar/interaction": "5.10.0", "@fullcalendar/interaction": "5.10.0",
"@fullcalendar/react": "5.10.0", "@fullcalendar/react": "5.10.0",
"@fullcalendar/timegrid": "5.10.0", "@fullcalendar/timegrid": "5.10.0",
"@kingsrook/qqq-frontend-core": "1.0.1", "@kingsrook/qqq-frontend-core": "file:.yalc/@kingsrook/qqq-frontend-core",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.4.1", "@mui/material": "5.4.1",
"@mui/styled-engine": "5.4.1", "@mui/styled-engine": "5.4.1",

View File

@ -13,7 +13,14 @@ Coded by www.creative-tim.com
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/ */
import React, { useState, useEffect, JSXElementConstructor, Key, ReactElement } from "react"; import React, {
useState,
useEffect,
JSXElementConstructor,
Key,
ReactElement,
useReducer,
} from "react";
// react-router components // react-router components
import { Routes, Route, Navigate, useLocation } from "react-router-dom"; import { Routes, Route, Navigate, useLocation } from "react-router-dom";

View File

@ -1,19 +1,19 @@
/** /**
========================================================= =========================================================
* Material Dashboard 2 PRO React TS - v1.0.0 * Material Dashboard 2 PRO React TS - v1.0.0
========================================================= =========================================================
* Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts * Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts
* Copyright 2022 Creative Tim (https://www.creative-tim.com) * Copyright 2022 Creative Tim (https://www.creative-tim.com)
Coded by www.creative-tim.com Coded by www.creative-tim.com
========================================================= =========================================================
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*/ */
import { useEffect, ReactNode } from "react"; import { useEffect, ReactNode, useReducer, useState } from "react";
// react-router-dom components // react-router-dom components
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";

View File

@ -42,9 +42,11 @@ import reportsLineChartData from "layouts/dashboards/analytics/data/reportsLineC
import booking1 from "assets/images/products/product-1-min.jpg"; import booking1 from "assets/images/products/product-1-min.jpg";
import booking2 from "assets/images/products/product-2-min.jpg"; import booking2 from "assets/images/products/product-2-min.jpg";
import booking3 from "assets/images/products/product-3-min.jpg"; import booking3 from "assets/images/products/product-3-min.jpg";
import { useReducer } from "react";
function Analytics(): JSX.Element { function Analytics(): JSX.Element {
const { sales, tasks } = reportsLineChartData; const { sales, tasks } = reportsLineChartData;
const [, forceUpdate] = useReducer((x) => x + 1, 0);
// Action buttons for the BookingCard // Action buttons for the BookingCard
const actionButtons = ( const actionButtons = (

View File

@ -15,8 +15,10 @@ import { QFieldMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/
// @material-ui core components // @material-ui core components
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import { Alert } from "@mui/material";
// Material Dashboard 2 PRO React TS components // Material Dashboard 2 PRO React TS components
import MDAlert from "components/MDAlert";
import MDBox from "components/MDBox"; import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography"; import MDTypography from "components/MDTypography";
import MDButton from "../../../components/MDButton"; import MDButton from "../../../components/MDButton";
@ -33,6 +35,7 @@ function EntityForm({ id }: Props): JSX.Element {
const [validations, setValidations] = useState({}); const [validations, setValidations] = useState({});
const [initialValues, setInitialValues] = useState({} as { [key: string]: string }); const [initialValues, setInitialValues] = useState({} as { [key: string]: string });
const [formFields, setFormFields] = useState({}); const [formFields, setFormFields] = useState({});
const [alertContent, setAlertContent] = useState("");
const [asyncLoadInited, setAsyncLoadInited] = useState(false); const [asyncLoadInited, setAsyncLoadInited] = useState(false);
const [formValues, setFormValues] = useState({} as { [key: string]: string }); const [formValues, setFormValues] = useState({} as { [key: string]: string });
@ -85,17 +88,27 @@ function EntityForm({ id }: Props): JSX.Element {
actions.setSubmitting(true); actions.setSubmitting(true);
await (async () => { await (async () => {
if (id !== null) { if (id !== null) {
await qController.update(tableName, id, values).then((record) => { await qController
window.location.href = `/${tableName}/${record.values.get( .update(tableName, id, values)
tableMetaData.primaryKeyField .then((record) => {
)}`; window.location.href = `/${tableName}/${record.values.get(
}); tableMetaData.primaryKeyField
)}`;
})
.catch((error) => {
setAlertContent(error.response.data.error);
});
} else { } else {
await qController.create(tableName, values).then((record) => { await qController
window.location.href = `/${tableName}/${record.values.get( .create(tableName, values)
tableMetaData.primaryKeyField .then((record) => {
)}`; window.location.href = `/${tableName}/${record.values.get(
}); tableMetaData.primaryKeyField
)}`;
})
.catch((error) => {
setAlertContent(error.response.data.error);
});
} }
})(); })();
}; };
@ -106,43 +119,56 @@ function EntityForm({ id }: Props): JSX.Element {
id != null ? `edit-${tableMetaData?.label}-form` : `create-${tableMetaData?.label}-form`; id != null ? `edit-${tableMetaData?.label}-form` : `create-${tableMetaData?.label}-form`;
return ( return (
<Card id="edit-form-container" sx={{ overflow: "visible" }}> <MDBox mb={3}>
<MDBox p={3}> <Grid container spacing={3}>
<MDTypography variant="h5">{formTitle}</MDTypography> <Grid item xs={12}>
</MDBox> {alertContent ? (
<MDBox pb={3} px={3}> <MDBox mb={3}>
<Grid key="fields-grid" container spacing={3}> <Alert severity="error">{alertContent}</Alert>
<Formik </MDBox>
initialValues={initialValues} ) : (
validationSchema={validations} ""
onSubmit={handleSubmit} )}
> <Card id="edit-form-container" sx={{ overflow: "visible" }}>
{({ values, errors, touched, isSubmitting }) => ( <MDBox p={3}>
<Form id={formId} autoComplete="off"> <MDTypography variant="h5">{formTitle}</MDTypography>
<MDBox p={3} width="100%"> </MDBox>
{/*************************************************************************** <MDBox pb={3} px={3}>
** step content - e.g., the appropriate form or other screen for the step ** <Grid key="fields-grid" container spacing={3}>
***************************************************************************/} <Formik
{getDynamicStepContent({ initialValues={initialValues}
values, validationSchema={validations}
touched, onSubmit={handleSubmit}
formFields, >
errors, {({ values, errors, touched, isSubmitting }) => (
})} <Form id={formId} autoComplete="off">
<Grid key="buttonGrid" container spacing={3}> <MDBox p={3} width="100%">
<MDBox mt={5} ml="auto"> {/***************************************************************************
<MDButton type="submit" variant="gradient" color="dark" size="small"> ** step content - e.g., the appropriate form or other screen for the step **
save {tableMetaData?.label} ***************************************************************************/}
</MDButton> {getDynamicStepContent({
</MDBox> values,
</Grid> touched,
</MDBox> formFields,
</Form> errors,
)} })}
</Formik> <Grid key="buttonGrid" container spacing={3}>
<MDBox mt={5} ml="auto">
<MDButton type="submit" variant="gradient" color="dark" size="small">
save {tableMetaData?.label}
</MDButton>
</MDBox>
</Grid>
</MDBox>
</Form>
)}
</Formik>
</Grid>
</MDBox>
</Card>
</Grid> </Grid>
</MDBox> </Grid>
</Card> </MDBox>
); );
} }

View File

@ -20,7 +20,7 @@ import Grid from "@mui/material/Grid";
import MDBox from "components/MDBox"; import MDBox from "components/MDBox";
// Settings page components // Settings page components
import CreateForm from "qqq/components/EntityForm"; import EntityForm from "qqq/components/EntityForm";
import BaseLayout from "qqq/components/BaseLayout"; import BaseLayout from "qqq/components/BaseLayout";
function EntityCreate(): JSX.Element { function EntityCreate(): JSX.Element {
@ -29,13 +29,7 @@ function EntityCreate(): JSX.Element {
<MDBox mt={4}> <MDBox mt={4}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} lg={12}> <Grid item xs={12} lg={12}>
<MDBox mb={3}> <EntityForm />
<Grid container spacing={3}>
<Grid item xs={12}>
<CreateForm />
</Grid>
</Grid>
</MDBox>
</Grid> </Grid>
</Grid> </Grid>
</MDBox> </MDBox>

View File

@ -15,6 +15,7 @@
/* eslint-disable react/no-unstable-nested-components */ /* eslint-disable react/no-unstable-nested-components */
import React, { useEffect, useReducer, useState } from "react"; import React, { useEffect, useReducer, useState } from "react";
import { useParams } from "react-router-dom";
// @mui material components // @mui material components
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -23,32 +24,43 @@ import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem"; import MenuItem from "@mui/material/MenuItem";
import Divider from "@mui/material/Divider"; import Divider from "@mui/material/Divider";
import Link from "@mui/material/Link"; import Link from "@mui/material/Link";
import { makeStyles } from "@mui/material"; import { makeStyles, Alert } from "@mui/material";
import { import {
DataGrid, DataGrid,
GridCallbackDetails, GridCallbackDetails,
GridColDef, GridColDef,
GridColumnVisibilityModel,
GridFilterModel,
GridRowId, GridRowId,
GridRowParams, GridRowParams,
GridRowsProp, GridRowsProp,
GridSelectionModel, GridSelectionModel,
GridSortItem,
GridSortModel,
GridToolbar,
} from "@mui/x-data-grid"; } from "@mui/x-data-grid";
// Material Dashboard 2 PRO React TS components // Material Dashboard 2 PRO React TS components
import DashboardLayout from "examples/LayoutContainers/DashboardLayout"; import DashboardLayout from "examples/LayoutContainers/DashboardLayout";
import DashboardNavbar from "examples/Navbars/DashboardNavbar"; import DashboardNavbar from "examples/Navbars/DashboardNavbar";
import MDBox from "components/MDBox"; import MDBox from "components/MDBox";
import MDTypography from "components/MDTypography";
import MDButton from "components/MDButton"; import MDButton from "components/MDButton";
// QQQ // QQQ
import { QProcessMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import { QProcessMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import { QTableMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import { QTableMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import { useParams } from "react-router-dom"; import { QQueryFilter } from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import { QFilterOrderBy } from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import { QFilterCriteria } from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import { QCriteriaOperator } from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
import { QFieldType } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import QClient from "qqq/utils/QClient"; import QClient from "qqq/utils/QClient";
import Footer from "../../components/Footer"; import Footer from "../../components/Footer";
import QProcessUtils from "../../utils/QProcessUtils"; import QProcessUtils from "../../utils/QProcessUtils";
const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY = "qqq.columnVisibility";
const COLUMN_SORT_LOCAL_STORAGE_KEY = "qqq.columnSort";
// Declaring props types for DefaultCell // Declaring props types for DefaultCell
interface Props { interface Props {
table?: QTableMetaData; table?: QTableMetaData;
@ -69,6 +81,10 @@ function EntityList({ table }: Props): JSX.Element {
const [columns, setColumns] = useState([] as GridColDef[]); const [columns, setColumns] = useState([] as GridColDef[]);
const [rows, setRows] = useState([] as GridRowsProp[]); const [rows, setRows] = useState([] as GridRowsProp[]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [columnVisibilityModel, setColumnVisibilityModel] = useState({});
const [sortModel, setSortModel] = useState([] as GridSortItem[]);
const [filterModel, setFilterModel] = useState(null as GridFilterModel);
const [alertContent, setAlertContent] = useState("");
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -77,14 +93,89 @@ function EntityList({ table }: Props): JSX.Element {
const openFiltersMenu = (event: any) => setFiltersMenu(event.currentTarget); const openFiltersMenu = (event: any) => setFiltersMenu(event.currentTarget);
const closeFiltersMenu = () => setFiltersMenu(null); const closeFiltersMenu = () => setFiltersMenu(null);
const translateCriteriaOperator = (operator: string) => {
switch (operator) {
case "contains":
return QCriteriaOperator.CONTAINS;
case "starts with":
return QCriteriaOperator.STARTS_WITH;
case "ends with":
return QCriteriaOperator.STARTS_WITH;
case "is":
case "equals":
case "=":
return QCriteriaOperator.EQUALS;
case "is not":
case "!=":
return QCriteriaOperator.NOT_EQUALS;
case "is after":
case ">":
return QCriteriaOperator.GREATER_THAN;
case "is on or after":
case ">=":
return QCriteriaOperator.GREATER_THAN_OR_EQUALS;
case "is before":
case "<":
return QCriteriaOperator.LESS_THAN;
case "is on or before":
case "<=":
return QCriteriaOperator.LESS_THAN_OR_EQUALS;
case "is empty":
return QCriteriaOperator.IS_BLANK;
case "is not empty":
return QCriteriaOperator.IS_NOT_BLANK;
// case "is any of":
// TODO: handle this case
default:
return QCriteriaOperator.EQUALS;
}
};
const buildQFilter = () => {
const qFilter = new QQueryFilter();
sortModel.forEach((gridSortItem) => {
qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc"));
});
if (filterModel) {
filterModel.items.forEach((item) => {
qFilter.addCriteria(
new QFilterCriteria(item.columnField, translateCriteriaOperator(item.operatorValue), [
item.value,
])
);
});
}
return qFilter;
};
const updateTable = () => { const updateTable = () => {
(async () => { (async () => {
const tableMetaData = await QClient.loadTableMetaData(tableName); const tableMetaData = await QClient.loadTableMetaData(tableName);
const count = await QClient.count(tableName); const count = await QClient.count(tableName);
setTotalRecords(count); setTotalRecords(count);
if (sortModel.length === 0) {
sortModel.push({ field: tableMetaData.primaryKeyField, sort: "desc" });
setSortModel(sortModel);
}
const qFilter = buildQFilter();
const columns = [] as GridColDef[]; const columns = [] as GridColDef[];
const results = await QClient.query(tableName, rowsPerPage, pageNumber * rowsPerPage);
const results = await QClient.query(
tableName,
qFilter,
rowsPerPage,
pageNumber * rowsPerPage
).catch((error) => {
if (error.message) {
setAlertContent(error.message);
} else {
setAlertContent(error.response.data.error);
}
throw error;
});
const rows = [] as any[]; const rows = [] as any[];
results.forEach((record) => { results.forEach((record) => {
@ -95,19 +186,46 @@ function EntityList({ table }: Props): JSX.Element {
sortedKeys.forEach((key) => { sortedKeys.forEach((key) => {
const field = tableMetaData.fields.get(key); const field = tableMetaData.fields.get(key);
let columnType = "string";
switch (field.type) {
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
columnType = "number";
break;
case QFieldType.DATE:
columnType = "date";
break;
case QFieldType.DATE_TIME:
columnType = "dateTime";
break;
case QFieldType.BOOLEAN:
columnType = "boolean";
break;
default:
// noop
}
const column = { const column = {
field: field.name, field: field.name,
type: columnType,
headerName: field.label, headerName: field.label,
width: 200, width: 200,
}; };
if (key === tableMetaData.primaryKeyField) { if (key === tableMetaData.primaryKeyField) {
column.width = 75;
columns.splice(0, 0, column); columns.splice(0, 0, column);
} else { } else {
columns.push(column); columns.push(column);
} }
}); });
const columnVisibilityModel = localStorage.getItem(COLUMN_VISIBILITY_LOCAL_STORAGE_KEY);
if (columnVisibilityModel) {
setColumnVisibilityModel(
JSON.parse(localStorage.getItem(COLUMN_VISIBILITY_LOCAL_STORAGE_KEY))
);
}
setColumns(columns); setColumns(columns);
setRows(rows); setRows(rows);
setLoading(false); setLoading(false);
@ -127,6 +245,10 @@ function EntityList({ table }: Props): JSX.Element {
document.location.href = `/${tableName}/${params.id}`; document.location.href = `/${tableName}/${params.id}`;
}; };
const handleFilterChange = (filterModel: GridFilterModel) => {
setFilterModel(filterModel);
};
const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) => { const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) => {
const newSelectedIds: string[] = []; const newSelectedIds: string[] = [];
selectionModel.forEach((value: GridRowId) => { selectionModel.forEach((value: GridRowId) => {
@ -135,6 +257,19 @@ function EntityList({ table }: Props): JSX.Element {
setSelectedIds(newSelectedIds); setSelectedIds(newSelectedIds);
}; };
const handleColumnVisibilityChange = (columnVisibilityModel: GridColumnVisibilityModel) => {
setColumnVisibilityModel(columnVisibilityModel);
localStorage.setItem(
COLUMN_VISIBILITY_LOCAL_STORAGE_KEY,
JSON.stringify(columnVisibilityModel)
);
};
const handleSortChange = (gridSort: GridSortModel) => {
setSortModel(gridSort);
localStorage.setItem(COLUMN_SORT_LOCAL_STORAGE_KEY, JSON.stringify(gridSort));
};
if (tableName !== tableState) { if (tableName !== tableState) {
(async () => { (async () => {
setTableState(tableName); setTableState(tableName);
@ -171,39 +306,28 @@ function EntityList({ table }: Props): JSX.Element {
</Menu> </Menu>
); );
const renderFiltersMenu = (
<Menu
anchorEl={filtersMenu}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
open={Boolean(filtersMenu)}
onClose={closeFiltersMenu}
keepMounted
>
<MenuItem onClick={closeFiltersMenu}>Status: Paid</MenuItem>
<MenuItem onClick={closeFiltersMenu}>Status: Refunded</MenuItem>
<MenuItem onClick={closeFiltersMenu}>Status: Canceled</MenuItem>
<Divider sx={{ margin: "0.5rem 0" }} />
<MenuItem onClick={closeFiltersMenu}>
<MDTypography variant="button" color="error" fontWeight="regular">
Remove Filter
</MDTypography>
</MenuItem>
</Menu>
);
useEffect(() => { useEffect(() => {
updateTable(); updateTable();
}, [pageNumber, rowsPerPage, tableState]); }, [pageNumber, rowsPerPage, tableState, sortModel, filterModel]);
return ( return (
<DashboardLayout> <DashboardLayout>
<DashboardNavbar /> <DashboardNavbar />
<MDBox my={3}> <MDBox my={3}>
{alertContent ? (
<MDBox mb={3}>
<Alert severity="error">{alertContent}</Alert>
</MDBox>
) : (
""
)}
<MDBox display="flex" justifyContent="space-between" alignItems="flex-start" mb={2}> <MDBox display="flex" justifyContent="space-between" alignItems="flex-start" mb={2}>
<MDButton variant="gradient" color="info"> <Link href={`/${tableName}/create`}>
<Link href={`/${tableName}/create`}>new {tableName}</Link> <MDButton variant="gradient" color="info">
</MDButton> new {tableName}
</MDButton>
</Link>
<MDBox display="flex"> <MDBox display="flex">
{tableProcesses.length > 0 && ( {tableProcesses.length > 0 && (
<MDButton <MDButton
@ -216,28 +340,15 @@ function EntityList({ table }: Props): JSX.Element {
</MDButton> </MDButton>
)} )}
{renderActionsMenu} {renderActionsMenu}
<MDBox ml={1}>
<MDButton
variant={filtersMenu ? "contained" : "outlined"}
color="dark"
onClick={openFiltersMenu}
>
filters&nbsp;
<Icon>keyboard_arrow_down</Icon>
</MDButton>
{renderFiltersMenu}
</MDBox>
<MDBox ml={1}>
<MDButton variant="outlined" color="dark">
<Icon>description</Icon>
&nbsp;export csv
</MDButton>
</MDBox>
</MDBox> </MDBox>
</MDBox> </MDBox>
<Card> <Card>
<MDBox height="100%"> <MDBox height="100%">
<DataGrid <DataGrid
components={{ Toolbar: GridToolbar }}
paginationMode="server"
sortingMode="server"
filterMode="server"
page={pageNumber} page={pageNumber}
checkboxSelection checkboxSelection
disableSelectionOnClick disableSelectionOnClick
@ -251,10 +362,18 @@ function EntityList({ table }: Props): JSX.Element {
onPageSizeChange={handleRowsPerPageChange} onPageSizeChange={handleRowsPerPageChange}
onPageChange={handlePageChange} onPageChange={handlePageChange}
onRowClick={handleRowClick} onRowClick={handleRowClick}
paginationMode="server"
density="compact" density="compact"
loading={loading} loading={loading}
onFilterModelChange={handleFilterChange}
columnVisibilityModel={columnVisibilityModel}
onColumnVisibilityModelChange={handleColumnVisibilityChange}
onSelectionModelChange={selectionChanged} onSelectionModelChange={selectionChanged}
onSortModelChange={handleSortChange}
sortingOrder={["asc", "desc"]}
sortModel={sortModel}
getRowClassName={(params) =>
params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd"
}
/> />
</MDBox> </MDBox>
</Card> </Card>

View File

@ -21,6 +21,7 @@
import { QFieldMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData"; import { QFieldMetaData } from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController"; import { QController } from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
import { QQueryFilter } from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
/******************************************************************************* /*******************************************************************************
** client wrapper of qqq backend ** client wrapper of qqq backend
@ -45,8 +46,12 @@ class QClient {
return this.getInstance().loadMetaData(); return this.getInstance().loadMetaData();
} }
public static query(tableName: string, limit: number, skip: number) { public static query(tableName: string, filter: QQueryFilter, limit: number, skip: number) {
return this.getInstance().query(tableName, limit, skip); return this.getInstance()
.query(tableName, filter, limit, skip)
.catch((error) => {
throw error;
});
} }
public static count(tableName: string) { public static count(tableName: string) {