/*
* 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 .
*/
import {Capability} from "@kingsrook/qqq-frontend-core/lib/model/metaData/Capability";
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, TablePagination} from "@mui/material";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogContentText from "@mui/material/DialogContentText";
import DialogTitle from "@mui/material/DialogTitle";
import Divider from "@mui/material/Divider";
import Icon from "@mui/material/Icon";
import LinearProgress from "@mui/material/LinearProgress";
import ListItemIcon from "@mui/material/ListItemIcon";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal";
import Tooltip from "@mui/material/Tooltip";
import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnOrderChangeParams, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterModel, GridLinkOperator, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, MuiEvent, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro";
import React, {useContext, useEffect, useReducer, useRef, useState} from "react";
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
import QContext from "QContext";
import DashboardLayout from "qqq/components/DashboardLayout";
import Footer from "qqq/components/Footer";
import Navbar from "qqq/components/Navbar";
import {QActionsMenuButton, QCreateNewButton} from "qqq/components/QButtons";
import MDAlert from "qqq/components/Temporary/MDAlert";
import MDBox from "qqq/components/Temporary/MDBox";
import ProcessRun from "qqq/pages/process-run";
import DataGridUtils from "qqq/utils/DataGridUtils";
import QClient from "qqq/utils/QClient";
import QFilterUtils from "qqq/utils/QFilterUtils";
import QProcessUtils from "qqq/utils/QProcessUtils";
import QValueUtils from "qqq/utils/QValueUtils";
const COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT = "qqq.columnVisibility";
const COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT = "qqq.columnSort";
const FILTER_LOCAL_STORAGE_KEY_ROOT = "qqq.filter";
const ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT = "qqq.rowsPerPage";
const PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT = "qqq.pinnedColumns";
const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density";
interface Props
{
table?: QTableMetaData;
launchProcess?: QProcessMetaData;
}
EntityList.defaultProps = {
table: null,
launchProcess: null
};
const qController = QClient.getInstance();
/*******************************************************************************
** Get the default filter to use on the page - either from query string, or
** local storage, or a default (empty).
*******************************************************************************/
async function getDefaultFilter(tableMetaData: QTableMetaData, searchParams: URLSearchParams, filterLocalStorageKey: string): Promise
{
if (tableMetaData.fields !== undefined)
{
if (searchParams.has("filter"))
{
try
{
const qQueryFilter = JSON.parse(searchParams.get("filter")) as QQueryFilter;
//////////////////////////////////////////////////////////////////
// translate from a qqq-style filter to one that the grid wants //
//////////////////////////////////////////////////////////////////
const defaultFilter = {items: []} as GridFilterModel;
let id = 1;
for (let i = 0; i < qQueryFilter.criteria.length; i++)
{
const criteria = qQueryFilter.criteria[i];
const field = tableMetaData.fields.get(criteria.fieldName);
let values = criteria.values;
if (field.possibleValueSourceName)
{
//////////////////////////////////////////////////////////////////////////////////
// possible-values in query-string are expected to only be their id values. //
// e.g., ...values=[1]... //
// but we need them to be possibleValue objects (w/ id & label) so the label //
// can be shown in the filter dropdown. So, make backend call to look them up. //
//////////////////////////////////////////////////////////////////////////////////
if (values && values.length > 0)
{
values = await qController.possibleValues(tableMetaData.name, field.name, "", values);
}
}
defaultFilter.items.push({
columnField: criteria.fieldName,
operatorValue: QFilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values),
value: QFilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, field),
id: id++, // not sure what this id is!!
});
}
defaultFilter.linkOperator = GridLinkOperator.And;
if (qQueryFilter.booleanOperator === "OR")
{
defaultFilter.linkOperator = GridLinkOperator.Or;
}
return (defaultFilter);
}
catch (e)
{
console.warn("Error parsing filter from query string", e);
}
}
if (localStorage.getItem(filterLocalStorageKey))
{
const defaultFilter = JSON.parse(localStorage.getItem(filterLocalStorageKey));
console.log(`Got default from LS: ${JSON.stringify(defaultFilter)}`);
return (defaultFilter);
}
}
return ({items: []});
}
function EntityList({table, launchProcess}: Props): JSX.Element
{
const tableName = table.name;
const [ searchParams ] = useSearchParams();
const location = useLocation();
const navigate = useNavigate();
const pathParts = location.pathname.split("/");
////////////////////////////////////////////
// look for defaults in the local storage //
////////////////////////////////////////////
const sortLocalStorageKey = `${COLUMN_SORT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const rowsPerPageLocalStorageKey = `${ROWS_PER_PAGE_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const pinnedColumnsLocalStorageKey = `${PINNED_COLUMNS_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const columnVisibilityLocalStorageKey = `${COLUMN_VISIBILITY_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
const filterLocalStorageKey = `${FILTER_LOCAL_STORAGE_KEY_ROOT}.${tableName}`;
let defaultSort = [] as GridSortItem[];
let defaultVisibility = {};
let defaultRowsPerPage = 10;
let defaultDensity = "standard" as GridDensity;
let defaultPinnedColumns = {left: ["__check__", "id"]} as GridPinnedColumns;
////////////////////////////////////////////////////////////////////////////////////
// set the to be not per table (do as above if we want per table) at a later port //
////////////////////////////////////////////////////////////////////////////////////
const densityLocalStorageKey = `${DENSITY_LOCAL_STORAGE_KEY_ROOT}`;
if (localStorage.getItem(sortLocalStorageKey))
{
defaultSort = JSON.parse(localStorage.getItem(sortLocalStorageKey));
}
if (localStorage.getItem(columnVisibilityLocalStorageKey))
{
defaultVisibility = JSON.parse(localStorage.getItem(columnVisibilityLocalStorageKey));
}
if (localStorage.getItem(pinnedColumnsLocalStorageKey))
{
defaultPinnedColumns = JSON.parse(localStorage.getItem(pinnedColumnsLocalStorageKey));
}
if (localStorage.getItem(rowsPerPageLocalStorageKey))
{
defaultRowsPerPage = JSON.parse(localStorage.getItem(rowsPerPageLocalStorageKey));
}
if (localStorage.getItem(densityLocalStorageKey))
{
defaultDensity = JSON.parse(localStorage.getItem(densityLocalStorageKey));
}
const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel);
const [columnSortModel, setColumnSortModel] = useState(defaultSort);
const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility);
const [rowsPerPage, setRowsPerPage] = useState(defaultRowsPerPage);
const [density, setDensity] = useState(defaultDensity);
const [pinnedColumns, setPinnedColumns] = useState(defaultPinnedColumns);
const [tableState, setTableState] = useState("");
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [defaultFilterLoaded, setDefaultFilterLoaded] = useState(false);
const [actionsMenu, setActionsMenu] = useState(null);
const [tableProcesses, setTableProcesses] = useState([] as QProcessMetaData[]);
const [allTableProcesses, setAllTableProcesses] = useState([] as QProcessMetaData[]);
const [pageNumber, setPageNumber] = useState(0);
const [totalRecords, setTotalRecords] = useState(null);
const [selectedIds, setSelectedIds] = useState([] as string[]);
const [selectFullFilterState, setSelectFullFilterState] = useState("n/a" as "n/a" | "checked" | "filter");
const [columnsModel, setColumnsModel] = useState([] as GridColDef[]);
const [rows, setRows] = useState([] as GridRowsProp[]);
const [loading, setLoading] = useState(true);
const [alertContent, setAlertContent] = useState("");
const [tableLabel, setTableLabel] = useState("");
const [gridMouseDownX, setGridMouseDownX] = useState(0);
const [gridMouseDownY, setGridMouseDownY] = useState(0);
const [gridPreferencesWindow, setGridPreferencesWindow] = useState(undefined);
const [showClearFiltersWarning, setShowClearFiltersWarning] = useState(false);
const [hasValidFilters, setHasValidFilters] = useState(false);
const [activeModalProcess, setActiveModalProcess] = useState(null as QProcessMetaData);
const [launchingProcess, setLaunchingProcess] = useState(launchProcess);
const [recordIdsForProcess, setRecordIdsForProcess] = useState(null as string | QQueryFilter);
const instance = useRef({timer: null});
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// use all these states to avoid showing results from an "old" query, that finishes loading after a newer one //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const [ latestQueryId, setLatestQueryId ] = useState(0);
const [ countResults, setCountResults ] = useState({} as any);
const [ receivedCountTimestamp, setReceivedCountTimestamp ] = useState(new Date());
const [ queryResults, setQueryResults ] = useState({} as any);
const [ receivedQueryTimestamp, setReceivedQueryTimestamp ] = useState(new Date());
const [ queryErrors, setQueryErrors ] = useState({} as any);
const [ receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp ] = useState(new Date());
const {setPageHeader} = useContext(QContext);
const [ , forceUpdate ] = useReducer((x) => x + 1, 0);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null);
/////////////////////////////////////////////////////////////////////////////////////////
// monitor location changes - if our url looks like a process, then open that process. //
/////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
try
{
/////////////////////////////////////////////////////////////////
// the path for a process looks like: .../table/process //
// so if our tableName is in the -2 index, try to open process //
/////////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 2] === tableName)
{
const processName = pathParts[pathParts.length - 1];
const processList = allTableProcesses.filter(p => p.name.endsWith(processName));
if (processList.length > 0)
{
setActiveModalProcess(processList[0]);
return;
}
else
{
console.log(`Couldn't find process named ${processName}`);
}
}
}
catch (e)
{
console.log(e);
}
////////////////////////////////////////////////////////////////////////////////////
// if we didn't open a process... not sure what we do in the table/query use-case //
////////////////////////////////////////////////////////////////////////////////////
setActiveModalProcess(null);
}, [ location ]);
const buildQFilter = (filterModel: GridFilterModel) =>
{
console.log("Building q filter with model:");
console.log(filterModel);
const qFilter = new QQueryFilter();
if (columnSortModel)
{
columnSortModel.forEach((gridSortItem) =>
{
qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc"));
});
}
if (filterModel)
{
let foundFilter = false;
filterModel.items.forEach((item) =>
{
/////////////////////////////////////////////////////////////////////////
// set the values for these operators that otherwise don't have values //
/////////////////////////////////////////////////////////////////////////
if(item.operatorValue === "isTrue")
{
item.value = [true];
}
else if(item.operatorValue === "isFalse")
{
item.value = [false];
}
////////////////////////////////////////////////////////////////////////////////
// if no value set and not 'empty' or 'not empty' operators, skip this filter //
////////////////////////////////////////////////////////////////////////////////
if((! item.value || item.value.length == 0) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
{
return;
}
const operator = QFilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
const values = QFilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue);
qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values));
foundFilter = true;
});
setHasValidFilters(foundFilter);
qFilter.booleanOperator = "AND";
if (filterModel.linkOperator == "or")
{
///////////////////////////////////////////////////////////////////////////////////////////
// by default qFilter uses AND - so only if we see linkOperator=or do we need to set it //
///////////////////////////////////////////////////////////////////////////////////////////
qFilter.booleanOperator = "OR";
}
}
return qFilter;
};
const getTableMetaData = async (): Promise =>
{
if(tableMetaData !== null)
{
return(new Promise((resolve) =>
{
resolve(tableMetaData)
}));
}
return (qController.loadTableMetaData(tableName));
}
const updateTable = () =>
{
setLoading(true);
setRows([]);
(async () =>
{
const tableMetaData = await getTableMetaData();
setPageHeader(tableMetaData.label);
////////////////////////////////////////////////////////////////////////////////////////////////
// we need the table meta data to look up the default filter (if it comes from query string), //
// because we need to know field types to translate qqq filter to material filter //
// return here ane wait for the next 'turn' to allow doing the actual query //
////////////////////////////////////////////////////////////////////////////////////////////////
let localFilterModel = filterModel;
if (!defaultFilterLoaded)
{
setDefaultFilterLoaded(true);
localFilterModel = await getDefaultFilter(tableMetaData, searchParams, filterLocalStorageKey);
setFilterModel(localFilterModel);
return;
}
setTableMetaData(tableMetaData);
setTableLabel(tableMetaData.label);
if (columnSortModel.length === 0)
{
columnSortModel.push({
field: tableMetaData.primaryKeyField,
sort: "desc",
});
setColumnSortModel(columnSortModel);
}
const qFilter = buildQFilter(localFilterModel);
//////////////////////////////////////////////////////////////////////////////////////////////////
// assign a new query id to the query being issued here. then run both the count & query async //
// and when they load, store their results associated with this id. //
//////////////////////////////////////////////////////////////////////////////////////////////////
const thisQueryId = latestQueryId + 1;
setLatestQueryId(thisQueryId);
console.log(`Issuing query: ${thisQueryId}`);
if (tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
qController.count(tableName, qFilter).then((count) =>
{
countResults[thisQueryId] = count;
setCountResults(countResults);
setReceivedCountTimestamp(new Date());
});
}
qController.query(tableName, qFilter, rowsPerPage, pageNumber * rowsPerPage).then((results) =>
{
console.log(`Received results for query ${thisQueryId}`);
queryResults[thisQueryId] = results;
setQueryResults(queryResults);
setReceivedQueryTimestamp(new Date());
})
.catch((error) =>
{
console.log(`Received error for query ${thisQueryId}`);
console.log(error);
var errorMessage;
if (error && error.message)
{
errorMessage = error.message;
}
else if (error && error.response && error.response.data && error.response.data.error)
{
errorMessage = error.response.data.error;
}
else
{
errorMessage = "Unexpected error running query";
}
queryErrors[thisQueryId] = errorMessage;
setQueryErrors(queryErrors);
setReceivedQueryErrorTimestamp(new Date());
throw error;
});
})();
};
///////////////////////////
// display count results //
///////////////////////////
useEffect(() =>
{
if (countResults[latestQueryId] === null)
{
///////////////////////////////////////////////
// see same idea in displaying query results //
///////////////////////////////////////////////
console.log(`No count results for id ${latestQueryId}...`);
return;
}
setTotalRecords(countResults[latestQueryId]);
delete countResults[latestQueryId];
}, [ receivedCountTimestamp ]);
///////////////////////////
// display query results //
///////////////////////////
useEffect(() =>
{
if (!queryResults[latestQueryId])
{
///////////////////////////////////////////////////////////////////////////////////////////
// to avoid showing results from an "older" query (e.g., one that was slow, and returned //
// AFTER a newer one) only ever show results here for the latestQueryId that was issued. //
///////////////////////////////////////////////////////////////////////////////////////////
console.log(`No query results for id ${latestQueryId}...`);
return;
}
console.log(`Outputting results for query ${latestQueryId}...`);
const results = queryResults[latestQueryId];
delete queryResults[latestQueryId];
const {rows, columnsToRender} = DataGridUtils.makeRows(results, tableMetaData);
if(columnsModel.length == 0)
{
const columns = DataGridUtils.setupGridColumns(tableMetaData, columnsToRender);
setColumnsModel(columns);
}
setRows(rows);
setLoading(false);
setAlertContent(null);
forceUpdate();
}, [ receivedQueryTimestamp ]);
/////////////////////////
// display query error //
/////////////////////////
useEffect(() =>
{
if (!queryErrors[latestQueryId])
{
///////////////////////////////
// same logic as for success //
///////////////////////////////
console.log(`No query error for id ${latestQueryId}...`);
return;
}
console.log(`Outputting error for query ${latestQueryId}...`);
const errorMessage = queryErrors[latestQueryId];
delete queryErrors[latestQueryId];
setLoading(false);
setAlertContent(errorMessage);
}, [ receivedQueryErrorTimestamp ]);
const handlePageChange = (page: number) =>
{
setPageNumber(page);
};
const handleRowsPerPageChange = (size: number) =>
{
setRowsPerPage(size);
localStorage.setItem(rowsPerPageLocalStorageKey, JSON.stringify(size));
};
const handlePinnedColumnsChange = (pinnedColumns: GridPinnedColumns) =>
{
setPinnedColumns(pinnedColumns);
localStorage.setItem(pinnedColumnsLocalStorageKey, JSON.stringify(pinnedColumns));
};
const handleStateChange = (state: GridState, event: MuiEvent, details: GridCallbackDetails) =>
{
if (state && state.density && state.density.value !== density)
{
setDensity(state.density.value);
localStorage.setItem(densityLocalStorageKey, JSON.stringify(state.density.value));
}
};
const handleRowClick = (params: GridRowParams, event: MuiEvent, details: GridCallbackDetails) =>
{
/////////////////////////////////////////////////////////////////
// if a grid preference window is open, ignore and reset timer //
/////////////////////////////////////////////////////////////////
console.log(gridPreferencesWindow);
if (gridPreferencesWindow !== undefined)
{
clearTimeout(instance.current.timer);
return;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// strategy for when to trigger or not trigger a row click: //
// To avoid a drag-event that highlighted text in a cell: //
// - we capture the x & y upon mouse-down - then compare them in this method (which fires when the mouse is up) //
// if they are more than 5 pixels away from the mouse-down, then assume it's a drag, not a click. //
// - avoid clicking the row upon double-click, by setting a 500ms timer here - and in the onDoubleClick handler, //
// cancelling the timer. //
// - also avoid a click, then click-again-and-start-dragging, by always cancelling the timer in mouse-down. //
// All in, these seem to have good results - the only downside being the half-second delay... //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const diff = Math.max(Math.abs(event.clientX - gridMouseDownX), Math.abs(event.clientY - gridMouseDownY));
if (diff < 5)
{
console.log("clearing timeout");
clearTimeout(instance.current.timer);
instance.current.timer = setTimeout(() =>
{
navigate(`${params.id}`);
}, 100);
}
else
{
console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`);
}
};
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(columnsModel));
console.log(columnOrderChangeParams);
};
const handleFilterChange = (filterModel: GridFilterModel) =>
{
setFilterModel(filterModel);
if (filterLocalStorageKey)
{
localStorage.setItem(
filterLocalStorageKey,
JSON.stringify(filterModel),
);
}
};
const handleSortChange = (gridSort: GridSortModel) =>
{
if (gridSort && gridSort.length > 0)
{
setColumnSortModel(gridSort);
localStorage.setItem(sortLocalStorageKey, JSON.stringify(gridSort));
}
};
if (tableName !== tableState)
{
(async () =>
{
setTableState(tableName);
const metaData = await qController.loadMetaData();
QValueUtils.qInstance = metaData;
setTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName)); // these are the ones to show in the dropdown
setAllTableProcesses(QProcessUtils.getProcessesForTable(metaData, tableName, true)); // these include hidden ones (e.g., to find the bulks)
if (launchingProcess)
{
setLaunchingProcess(null);
setActiveModalProcess(launchingProcess);
}
// reset rows to trigger rerender
setRows([]);
})();
}
interface QExportMenuItemProps extends GridExportMenuItemProps<{}>
{
format: string;
}
function ExportMenuItem(props: QExportMenuItemProps)
{
const {format, hideMenu} = props;
return (
);
}
function getNoOfSelectedRecords()
{
if (selectFullFilterState === "filter")
{
return (totalRecords);
}
return (selectedIds.length);
}
function getRecordsQueryString()
{
if (selectFullFilterState === "filter")
{
return `?recordsParam=filterJSON&filterJSON=${JSON.stringify(buildQFilter(filterModel))}`;
}
if (selectedIds.length > 0)
{
return `?recordsParam=recordIds&recordIds=${selectedIds.join(",")}`;
}
return "";
}
const openModalProcess = (process: QProcessMetaData = null) =>
{
if (selectFullFilterState === "filter")
{
setRecordIdsForProcess(buildQFilter(filterModel));
}
else if (selectedIds.length > 0)
{
setRecordIdsForProcess(selectedIds.join(","));
}
else
{
setRecordIdsForProcess("");
}
navigate(`${process.name}${getRecordsQueryString()}`);
closeActionsMenu();
};
const closeModalProcess = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
/////////////////////////////////////////////////////////////////////////
// when closing a modal process, navigate up to the table being viewed //
/////////////////////////////////////////////////////////////////////////
const newPath = location.pathname.split("/");
newPath.pop();
navigate(newPath.join("/"));
updateTable();
};
const openBulkProcess = (processNamePart: "Insert" | "Edit" | "Delete", processLabelPart: "Load" | "Edit" | "Delete") =>
{
const processList = allTableProcesses.filter(p => p.name.endsWith(`.bulk${processNamePart}`));
if (processList.length > 0)
{
openModalProcess(processList[0]);
}
else
{
setAlertContent(`Could not find Bulk ${processLabelPart} process for this table.`);
}
};
const bulkLoadClicked = () =>
{
closeActionsMenu();
openBulkProcess("Insert", "Load");
};
const bulkEditClicked = () =>
{
closeActionsMenu();
if (getNoOfSelectedRecords() === 0)
{
setAlertContent("No records were selected to Bulk Edit.");
return;
}
openBulkProcess("Edit", "Edit");
};
const bulkDeleteClicked = () =>
{
closeActionsMenu();
if (getNoOfSelectedRecords() === 0)
{
setAlertContent("No records were selected to Bulk Delete.");
return;
}
openBulkProcess("Delete", "Delete");
};
const processClicked = (process: QProcessMetaData) =>
{
// todo - let the process specify that it needs initial rows - err if none selected.
// alternatively, let a process itself have an initial screen to select rows...
openModalProcess(process);
};
// @ts-ignore
const defaultLabelDisplayedRows = ({from, to, count}) =>
{
if(tableMetaData && !tableMetaData.capabilities.has(Capability.TABLE_COUNT))
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// to avoid a non-countable table showing (this is what data-grid did) 91-100 even if there were only 95 records, //
// we'll do this... not quite good enough, but better than the original //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(rows.length > 0 && rows.length < to - from)
{
to = from + rows.length;
}
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()}`);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// treat -1 as the sentinel that it's set as below -- remember, we did that so that 'to' would have a value in here when there's no count. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (count !== null && count !== undefined && count !== -1)
{
if (count === 0)
{
return (loading ? "Counting records..." : "No rows");
}
return (`Showing ${from.toLocaleString()} to ${to.toLocaleString()} of ${count !== -1 ? `${count.toLocaleString()} records` : `more than ${to.toLocaleString()} records`}`);
}
else
{
return ("Counting records...");
}
};
function CustomPagination()
{
return (
handlePageChange(value)}
onRowsPerPageChange={(event) => handleRowsPerPageChange(Number(event.target.value))}
labelDisplayedRows={defaultLabelDisplayedRows}
/>
);
}
function Loading()
{
return (
);
}
function CustomToolbar()
{
const handleMouseDown: GridEventListener<"cellMouseDown"> = (
params, // GridRowParams
event, // MuiEvent>
details, // GridCallbackDetails
) =>
{
setGridMouseDownX(event.clientX);
setGridMouseDownY(event.clientY);
clearTimeout(instance.current.timer);
};
const handleDoubleClick: GridEventListener<"rowDoubleClick"> = (event: any) =>
{
clearTimeout(instance.current.timer);
};
const apiRef = useGridApiContext();
useGridApiEventHandler(apiRef, "cellMouseDown", handleMouseDown);
useGridApiEventHandler(apiRef, "rowDoubleClick", handleDoubleClick);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// keep track of any preference windows that are opened in the toolbar, to allow ignoring clicks away from the window //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
const preferencePanelState = useGridSelector(apiRef, gridPreferencePanelStateSelector);
setGridPreferencesWindow(preferencePanelState.openedPanelValue);
});
return (
{
hasValidFilters && (
setShowClearFiltersWarning(true)}>clear
)
}
{
selectFullFilterState === "checked" && (
The
{` ${selectedIds.length.toLocaleString()} `}
records on this page are selected.
)
}
{
selectFullFilterState === "filter" && (
All
{` ${totalRecords ? totalRecords.toLocaleString() : ""} `}
records matching this query are selected.
)
}
);
}
const renderActionsMenu = (
);
///////////////////////////////////////////////////////////////////////////////////////////
// for changes in table controls that don't change the count, call to update the table - //
// but without clearing out totalRecords (so pagination doesn't flash) //
///////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if(latestQueryId > 0)
{
////////////////////////////////////////////////////////////////////////////////////////
// to avoid both this useEffect and the one below from both doing an "initial query", //
// only run this one if at least 1 query has already been ran //
////////////////////////////////////////////////////////////////////////////////////////
updateTable();
}
}, [ pageNumber, rowsPerPage, columnSortModel ]);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for state changes that DO change the filter, call to update the table - and DO clear out the totalRecords //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
setTotalRecords(null);
updateTable();
}, [ tableState, filterModel]);
useEffect(() =>
{
document.documentElement.scrollTop = 0;
document.scrollingElement.scrollTop = 0;
}, [ pageNumber, rowsPerPage ]);
return (
{alertContent ? (
{
setAlertContent(null);
}}
>
{alertContent}
) : (
""
)}
{
(tableLabel && searchParams.get("deleteSuccess")) ? (
{`${tableLabel} successfully deleted`}
) : null
}
{renderActionsMenu}
{
table.capabilities.has(Capability.TABLE_INSERT) &&
}
"auto"} // maybe nice? wraps values in cells...
columns={columnsModel}
rowBuffer={10}
rowCount={totalRecords === null ? 0 : totalRecords}
onPageSizeChange={handleRowsPerPageChange}
onRowClick={handleRowClick}
onStateChange={handleStateChange}
density={density}
loading={loading}
filterModel={filterModel}
onFilterModelChange={handleFilterChange}
columnVisibilityModel={columnVisibilityModel}
onColumnVisibilityModelChange={handleColumnVisibilityChange}
onColumnOrderChange={handleColumnOrderChange}
onSelectionModelChange={selectionChanged}
onSortModelChange={handleSortChange}
sortingOrder={[ "asc", "desc" ]}
sortModel={columnSortModel}
getRowClassName={(params) => (params.indexRelativeToCurrentPage % 2 === 0 ? "even" : "odd")}
/>
{
activeModalProcess &&
closeModalProcess(event, reason)}>