/** ========================================================= * Material Dashboard 2 PRO React TS - v1.0.0 ========================================================= * Product Page: https://www.creative-tim.com/product/material-dashboard-2-pro-react-ts * Copyright 2022 Creative Tim (https://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. */ /* eslint-disable react/no-unstable-nested-components */ import React, {useEffect, useReducer, useState} from "react"; import {useParams, useSearchParams} from "react-router-dom"; // @mui material components import Card from "@mui/material/Card"; import Icon from "@mui/material/Icon"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import Link from "@mui/material/Link"; import {Alert, tableFooterClasses} from "@mui/material"; import { DataGridPro, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridFilterModel, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, GridExportMenuItemProps, } from "@mui/x-data-grid-pro"; // Material Dashboard 2 PRO React TS components import DashboardLayout from "examples/LayoutContainers/DashboardLayout"; import DashboardNavbar from "examples/Navbars/DashboardNavbar"; import MDBox from "components/MDBox"; import MDButton from "components/MDButton"; import MDAlert from "components/MDAlert"; // QQQ import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; 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 Navbar from "qqq/components/Navbar"; import Button from "@mui/material/Button"; import Footer from "../../components/Footer"; import QProcessUtils from "../../utils/QProcessUtils"; import "./styles.css"; const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT = "qqq.columnVisibility"; const COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort"; // Declaring props types for DefaultCell interface Props { table?: QTableMetaData; } function EntityList({table}: Props): JSX.Element { const tableNameParam = useParams().tableName; const tableName = table === null ? tableNameParam : table.name; const [searchParams] = useSearchParams(); //////////////////////////////////////////// // look for defaults in the local storage // //////////////////////////////////////////// const sortLocalStorageKey = `${COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; let defaultSort = [] as GridSortItem[]; let defaultVisibility = {}; if (localStorage.getItem(sortLocalStorageKey)) { defaultSort = JSON.parse(localStorage.getItem(sortLocalStorageKey)); } if (localStorage.getItem(columnVisibilityLocalStorageKey)) { defaultVisibility = JSON.parse(localStorage.getItem(columnVisibilityLocalStorageKey)); } const [buttonText, setButtonText] = useState(""); const [tableState, setTableState] = useState(""); const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); const [, setFiltersMenu] = useState(null); const [actionsMenu, setActionsMenu] = useState(null); const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]); const [pageNumber, setPageNumber] = useState(0); const [totalRecords, setTotalRecords] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); const [selectedIds, setSelectedIds] = useState([] as string[]); const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter"); const [columns, setColumns] = useState([] as GridColDef[]); const [rows, setRows] = useState([] as GridRowsProp[]); const [loading, setLoading] = useState(true); const [filterModel, setFilterModel] = useState(null as GridFilterModel); const [alertContent, setAlertContent] = useState(""); const [tableLabel, setTableLabel] = useState(""); const [columnSortModel, setColumnSortModel] = useState(defaultSort); const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility); const [, forceUpdate] = useReducer((x) => x + 1, 0); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const closeActionsMenu = () => setActionsMenu(null); const translateCriteriaOperator = (operator: string) => { switch (operator) { case "contains": return QCriteriaOperator.CONTAINS; case "startsWith": return QCriteriaOperator.STARTS_WITH; case "endsWith": return QCriteriaOperator.ENDS_WITH; case "is": case "equals": case "=": return QCriteriaOperator.EQUALS; case "isNot": case "!=": return QCriteriaOperator.NOT_EQUALS; case "after": case ">": return QCriteriaOperator.GREATER_THAN; case "onOrAfter": case ">=": return QCriteriaOperator.GREATER_THAN_OR_EQUALS; case "before": case "<": return QCriteriaOperator.LESS_THAN; case "onOrBefore": case "<=": return QCriteriaOperator.LESS_THAN_OR_EQUALS; case "isEmpty": return QCriteriaOperator.IS_BLANK; case "isNotEmpty": return QCriteriaOperator.IS_NOT_BLANK; // case "is any of": // TODO: handle this case default: return QCriteriaOperator.EQUALS; } }; const buildQFilter = () => { const qFilter = new QQueryFilter(); if (columnSortModel) { columnSortModel.forEach((gridSortItem) => { qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc")); }); } if (filterModel) { filterModel.items.forEach((item) => { const operator = translateCriteriaOperator(item.operatorValue); let criteria = new QFilterCriteria(item.columnField, operator, [item.value]); if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK) { criteria = new QFilterCriteria(item.columnField, translateCriteriaOperator(item.operatorValue), null); } qFilter.addCriteria(criteria); }); } return qFilter; }; const updateTable = () => { (async () => { const newTableMetaData = await QClient.loadTableMetaData(tableName); setTableMetaData(newTableMetaData); if (columnSortModel.length === 0) { columnSortModel.push({ field: newTableMetaData.primaryKeyField, sort: "desc", }); setColumnSortModel(columnSortModel); } const qFilter = buildQFilter(); const count = await QClient.count(tableName, qFilter); setTotalRecords(count); setButtonText(`new ${newTableMetaData.label}`); setTableLabel(newTableMetaData.label); const columns = [] as GridColDef[]; 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[]; results.forEach((record) => { rows.push(Object.fromEntries(record.values.entries())); }); const sortedKeys = [...newTableMetaData.fields.keys()].sort(); sortedKeys.forEach((key) => { const field = newTableMetaData.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 = { field: field.name, type: columnType, headerName: field.label, width: 200, }; if (key === newTableMetaData.primaryKeyField) { column.width = 75; columns.splice(0, 0, column); } else { columns.push(column); } }); setColumns(columns); setRows(rows); setLoading(false); forceUpdate(); })(); }; const handlePageChange = (page: number) => { setPageNumber(page); }; const handleRowsPerPageChange = (size: number) => { setRowsPerPage(size); }; const handleRowClick = (params: GridRowParams) => { document.location.href = `/${tableName}/${params.id}`; }; const handleFilterChange = (filterModel: GridFilterModel) => { setFilterModel(filterModel); }; const selectionChanged = (selectionModel: GridSelectionModel, details: GridCallbackDetails) => { const newSelectedIds: string[] = []; selectionModel.forEach((value: GridRowId) => { newSelectedIds.push(value as string); }); setSelectedIds(newSelectedIds); if (newSelectedIds.length === rowsPerPage) { setSelectFullFilterState("checked"); } else { setSelectFullFilterState("n/a"); } }; const handleColumnVisibilityChange = (columnVisibilityModel: GridColumnVisibilityModel) => { setColumnVisibilityModel(columnVisibilityModel); if (columnVisibilityLocalStorageKey) { localStorage.setItem( columnVisibilityLocalStorageKey, JSON.stringify(columnVisibilityModel), ); } }; const handleColumnOrderChange = (columnOrderChangeParams: GridColumnOrderChangeParams) => { // TODO: make local storaged console.log(JSON.stringify(columns)); console.log(columnOrderChangeParams); }; const handleSortChange = (gridSort: GridSortModel) => { if (gridSort && gridSort.length > 0) { setColumnSortModel(gridSort); localStorage.setItem(sortLocalStorageKey, JSON.stringify(gridSort)); } }; if (tableName !== tableState) { (async () => { setTableState(tableName); setFilterModel(null); setFiltersMenu(null); const metaData = await QClient.loadMetaData(); setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); // reset rows to trigger rerender setRows([]); })(); } interface QExportMenuItemProps extends GridExportMenuItemProps<{}> { format: string; } function ExportMenuItem(props: QExportMenuItemProps) { const {format, hideMenu} = props; return ( { /////////////////////////////////////////////////////////////////////////////// // 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); } }); /////////////////////// // zero-pad function // /////////////////////// 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=${encodeURIComponent(JSON.stringify(buildQFilter()))}&fields=${visibleFields.join(",")}`; ////////////////////////////////////////////////////////////////////////////////////// // open a window (tab) with a little page that says the file is being generated. // // then have that page load the url for the export. // // If there's an error, it'll appear in that window. else, the file will download. // ////////////////////////////////////////////////////////////////////////////////////// const exportWindow = window.open("", "_blank"); exportWindow.document.write(` ${filename} Generating file ${filename} with ${totalRecords.toLocaleString()} records... `); /////////////////////////////////////////// // Hide the export menu after the export // /////////////////////////////////////////// hideMenu?.(); }} > Export {` ${format.toUpperCase()}`} ); } function getNoOfSelectedRecords() { if (selectFullFilterState === "filter") { return (totalRecords); } return (selectedIds.length); } function getRecordsQueryString() { if (selectFullFilterState === "filter") { return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter())}`; } if (selectedIds.length > 0) { return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`; } return ""; } const bulkLoadClicked = () => { document.location.href = `/processes/${tableName}.bulkInsert`; }; const bulkEditClicked = () => { if (getNoOfSelectedRecords() === 0) { setAlertContent("No records were selected to Bulk Edit."); return; } document.location.href = `/processes/${tableName}.bulkEdit${getRecordsQueryString()}`; }; const bulkDeleteClicked = () => { if (getNoOfSelectedRecords() === 0) { setAlertContent("No records were selected to Bulk Delete."); return; } document.location.href = `/processes/${tableName}.bulkDelete${getRecordsQueryString()}`; }; function CustomToolbar() { const [bulkActionsMenuAnchor, setBulkActionsMenuAnchor] = useState(null as HTMLElement); const bulkActionsMenuOpen = Boolean(bulkActionsMenuAnchor); const openBulkActionsMenu = (event: React.MouseEvent) => { setBulkActionsMenuAnchor(event.currentTarget); }; const closeBulkActionsMenu = () => { setBulkActionsMenuAnchor(null); }; return (
Bulk Load Bulk Edit Bulk Delete
{ selectFullFilterState === "checked" && (
The {` ${selectedIds.length.toLocaleString()} `} records on this page are selected.
) } { selectFullFilterState === "filter" && (
All {` ${totalRecords.toLocaleString()} `} records matching this query are selected.
) }
); } const renderActionsMenu = ( {tableProcesses.map((process) => ( {process.label} ))} ); useEffect(() => { updateTable(); }, [pageNumber, rowsPerPage, tableState, columnSortModel, filterModel]); return ( {alertContent ? ( { setAlertContent(null); }} > {alertContent} ) : ( "" )} { (tableLabel && searchParams.get("deleteSuccess")) ? ( {`${tableLabel} successfully deleted`} ) : ("") } {buttonText ? ( { buttonText } ) : ( "" )} {tableProcesses.length > 0 && ( actions  keyboard_arrow_down )} {renderActionsMenu} (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")} />