mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 07:08:44 +00:00
Compare commits
6 Commits
wip/hotfix
...
wip/bugfix
Author | SHA1 | Date | |
---|---|---|---|
b1eba925fa | |||
e7b5821fbd | |||
aed1c9d4d0 | |||
88a4c17bbc | |||
2900cd8593 | |||
8ab0f5f549 |
@ -33,6 +33,7 @@
|
||||
"html-react-parser": "1.4.8",
|
||||
"html-to-text": "^9.0.5",
|
||||
"http-proxy-middleware": "2.0.6",
|
||||
"idb": "8.0.0",
|
||||
"jwt-decode": "3.1.2",
|
||||
"rapidoc": "9.3.4",
|
||||
"react": "18.0.0",
|
||||
|
@ -98,7 +98,7 @@ export default function ExportMenuItem(props: QExportMenuItemProps)
|
||||
</head>
|
||||
<body>
|
||||
Generating file <u>${filename}</u>${totalRecords ? " with " + totalRecords.toLocaleString() + " record" + (totalRecords == 1 ? "" : "s") : ""}...
|
||||
<form id="exportForm" method="post" action="${url}" enctype="multipart/form-data">
|
||||
<form id="exportForm" method="post" action="${url}" >
|
||||
<input type="hidden" name="fields" value="${visibleFields.join(",")}">
|
||||
<input type="hidden" name="filter" id="filter">
|
||||
</form>
|
||||
|
@ -94,6 +94,24 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
document.getElementById(`${idPrefix}${criteria.id}`).focus();
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Event handler for key-down events - specifically added here, to stop pressing
|
||||
** 'tab' in a date or date-time from closing the quick-filter...
|
||||
*******************************************************************************/
|
||||
const handleKeyDown = (e: any) =>
|
||||
{
|
||||
if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME)
|
||||
{
|
||||
if(e.code == "Tab")
|
||||
{
|
||||
console.log("Tab on date or date-time - don't close me, just move to the next sub-field!...");
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const inputProps: any = {};
|
||||
inputProps.endAdornment = (
|
||||
<InputAdornment position="end">
|
||||
@ -110,6 +128,7 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi
|
||||
autoComplete="off"
|
||||
type={type}
|
||||
onChange={(event) => valueChangeHandler(event, valueIndex)}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={value}
|
||||
InputLabelProps={inputLabelProps}
|
||||
InputProps={inputProps}
|
||||
|
@ -504,7 +504,7 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
|
||||
//////////////////////////////
|
||||
// return the button & menu //
|
||||
//////////////////////////////
|
||||
const widthAndMaxWidth = 250
|
||||
const widthAndMaxWidth = fieldMetaData?.type == QFieldType.DATE_TIME ? 275 : 250
|
||||
return (
|
||||
<>
|
||||
{button}
|
||||
|
@ -1089,19 +1089,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const formData = new FormData();
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
let queryStringPairsForInit = [];
|
||||
if (urlSearchParams.get("recordIds"))
|
||||
{
|
||||
const recordIdsFromQueryString = urlSearchParams.get("recordIds").split(",");
|
||||
const encodedRecordIds = recordIdsFromQueryString.map(r => encodeURIComponent(r)).join(",");
|
||||
queryStringPairsForInit.push("recordsParam=recordIds");
|
||||
queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
|
||||
formData.append("recordsParam", "recordIds")
|
||||
formData.append("recordIds", urlSearchParams.get("recordIds"))
|
||||
}
|
||||
else if (urlSearchParams.get("filterJSON"))
|
||||
{
|
||||
queryStringPairsForInit.push("recordsParam=filterJSON");
|
||||
queryStringPairsForInit.push(`filterJSON=${encodeURIComponent(urlSearchParams.get("filterJSON"))}`);
|
||||
formData.append("recordsParam", "filterJSON")
|
||||
formData.append("filterJSON", urlSearchParams.get("filterJSON"));
|
||||
}
|
||||
// todo once saved filters exist
|
||||
//else if(urlSearchParams.get("filterId")) {
|
||||
@ -1110,23 +1108,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
// }
|
||||
else if (recordIds)
|
||||
{
|
||||
if (recordIds instanceof QQueryFilter)
|
||||
// @ts-ignore - we're checking to see if recordIds is a QQueryFilter-looking object here.
|
||||
if (recordIds instanceof QQueryFilter || (typeof recordIds === "object" && recordIds.criteria))
|
||||
{
|
||||
queryStringPairsForInit.push("recordsParam=filterJSON");
|
||||
queryStringPairsForInit.push(`filterJSON=${encodeURIComponent(JSON.stringify(recordIds))}`);
|
||||
formData.append("recordsParam", "filterJSON")
|
||||
formData.append("filterJSON", JSON.stringify(recordIds));
|
||||
}
|
||||
else if (typeof recordIds === "object" && recordIds.length)
|
||||
{
|
||||
const encodedRecordIds = recordIds.map(r => encodeURIComponent(r)).join(",");
|
||||
queryStringPairsForInit.push("recordsParam=recordIds");
|
||||
queryStringPairsForInit.push(`recordIds=${encodedRecordIds}`);
|
||||
formData.append("recordsParam", "recordIds")
|
||||
formData.append("recordIds", recordIds.join(","))
|
||||
}
|
||||
}
|
||||
|
||||
if (tableVariantLocalStorageKey && localStorage.getItem(tableVariantLocalStorageKey))
|
||||
{
|
||||
let tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
|
||||
queryStringPairsForInit.push(`tableVariant=${encodeURIComponent(JSON.stringify(tableVariant))}`);
|
||||
formData.append("tableVariant", JSON.stringify(tableVariant));
|
||||
}
|
||||
|
||||
try
|
||||
@ -1170,18 +1168,18 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{
|
||||
for (let key in defaultProcessValues)
|
||||
{
|
||||
queryStringPairsForInit.push(`${key}=${encodeURIComponent(defaultProcessValues[key])}`);
|
||||
formData.append(key, defaultProcessValues[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (tableMetaData)
|
||||
{
|
||||
queryStringPairsForInit.push(`tableName=${encodeURIComponent(tableMetaData.name)}`);
|
||||
formData.append("tableName", tableMetaData.name);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const processResponse = await Client.getInstance().processInit(processName, queryStringPairsForInit.join("&"));
|
||||
const processResponse = await Client.getInstance().processInit(processName, formData);
|
||||
setProcessUUID(processResponse.processUUID);
|
||||
setLastProcessResponse(processResponse);
|
||||
}
|
||||
|
@ -44,9 +44,10 @@ import LinearProgress from "@mui/material/LinearProgress";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {ColumnHeaderFilterIconButtonProps, DataGridPro, GridCallbackDetails, GridColDef, GridColumnHeaderParams, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnResizeParams, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridFilterMenuItem, GridPinnedColumns, gridPreferencePanelStateSelector, GridPreferencePanelsValue, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridApiRef, useGridSelector} from "@mui/x-data-grid-pro";
|
||||
import {ColumnHeaderFilterIconButtonProps, DataGridPro, GridCallbackDetails, GridColDef, GridColumnHeaderParams, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnResizeParams, GridColumnVisibilityModel, GridDensity, GridEventListener, GridPinnedColumns, gridPreferencePanelStateSelector, GridPreferencePanelsValue, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarContainer, GridToolbarDensitySelector, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridApiRef, useGridSelector} from "@mui/x-data-grid-pro";
|
||||
import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
|
||||
import FormData from "form-data";
|
||||
import {IDBPDatabase, openDB} from "idb";
|
||||
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
|
||||
import {useLocation, useNavigate, useSearchParams} from "react-router-dom";
|
||||
import QContext from "QContext";
|
||||
@ -101,6 +102,13 @@ RecordQuery.defaultProps = {
|
||||
///////////////////////////////////////////////////////
|
||||
type PageState = "initial" | "loadingMetaData" | "loadedMetaData" | "loadingView" | "loadedView" | "preparingGrid" | "ready";
|
||||
|
||||
//////////////////////////////////////////
|
||||
// define IndexedDB store & field names //
|
||||
//////////////////////////////////////////
|
||||
const recordIdsForProcessesDBName = "qqq.recordIdsForProcesses";
|
||||
const recordIdsForProcessesStoreName = "recordIdsForProcesses";
|
||||
const timestampIndexAndFieldName = "timestamp";
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
/*******************************************************************************
|
||||
@ -114,7 +122,6 @@ const getLoadingScreen = () =>
|
||||
</BaseLayout>);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** QQQ Record Query Screen component.
|
||||
**
|
||||
@ -1407,17 +1414,114 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** launch/open a modal process. Ends up navigating to the process's path w/
|
||||
** records selected via query string.
|
||||
** For the various functions that work with the recordIdsForProcess indexedDB,
|
||||
** open that database (creating if needed).
|
||||
*******************************************************************************/
|
||||
const openModalProcess = (process: QProcessMetaData = null) =>
|
||||
const openRecordIdsForProcessIndexedDB = async () =>
|
||||
{
|
||||
return await openDB(recordIdsForProcessesDBName, 1, {
|
||||
upgrade(db)
|
||||
{
|
||||
const store = db.createObjectStore(recordIdsForProcessesStoreName, {
|
||||
keyPath: "uuid"
|
||||
});
|
||||
store.createIndex(timestampIndexAndFieldName, timestampIndexAndFieldName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** clean up old indexedDB records that were created to launch processes in the past.
|
||||
*******************************************************************************/
|
||||
const manageRecordIdsForProcessIndexedDB = async (db: IDBPDatabase) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const now = new Date().getTime();
|
||||
const limit = now - (1000 * 60 * 60 * 24 * 7); // now minus 1 week
|
||||
|
||||
const expiredKeys = await db.getAllKeysFromIndex(recordIdsForProcessesStoreName, timestampIndexAndFieldName, IDBKeyRange.upperBound(limit, true));
|
||||
for (let expiredKey of expiredKeys)
|
||||
{
|
||||
db.delete(recordIdsForProcessesStoreName, expiredKey);
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.log("Error managing recordIdsForProcess in indexeddb: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** we used to pass recordIds (that is, either an array of ids (from checkboxes)
|
||||
** or a json-query-filter) on the query string... but that gets too big, so,
|
||||
** instead, write it ... not to local storage (5MB limit, and we want to keep
|
||||
** them around for ... some period of time, for reloading processes), so,
|
||||
** write it to indexedDB instead.
|
||||
*******************************************************************************/
|
||||
const storeRecordIdsForProcessInIndexedDB = async (recordIds: string[] | QQueryFilter): Promise<string> =>
|
||||
{
|
||||
const uuid = crypto.randomUUID()
|
||||
|
||||
let db = await openRecordIdsForProcessIndexedDB();
|
||||
await db.add(recordIdsForProcessesStoreName, {
|
||||
uuid: uuid,
|
||||
json: JSON.stringify(recordIds),
|
||||
timestamp: new Date().getTime()
|
||||
});
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we shouldn't need to await this function - it's just good to run the cleanup "sometimes" //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||
manageRecordIdsForProcessIndexedDB(db);
|
||||
|
||||
return (uuid);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** when launching a process, if we're to use recordIds(/filter) from indexedDB,
|
||||
** then do that read (async), and open the modal process after it completes.
|
||||
*******************************************************************************/
|
||||
const launchModalProcessUsingRecordIdsFromIndexedDB = async (uuid: string, processMetaData: QProcessMetaData): Promise<void> =>
|
||||
{
|
||||
let db = await openRecordIdsForProcessIndexedDB();
|
||||
const recordIds = await db.get(recordIdsForProcessesStoreName, uuid)
|
||||
|
||||
if(recordIds)
|
||||
{
|
||||
const recordIdsObject = JSON.parse(recordIds.json);
|
||||
setRecordIdsForProcess(recordIdsObject);
|
||||
setActiveModalProcess(processMetaData);
|
||||
}
|
||||
else
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// closing the process will do a navigate - so we can't just set an alert, we need it to be passed in the navigation call as state - //
|
||||
// so pass it as a param to closeModalProcess, which will set it in the navigation state. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
closeModalProcess({}, "failed to start", "Could not find query filter to start this process. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** launch/open a modal process. Writes the records (ids or filter) to indexedDB,
|
||||
** identified by a UUID. Then navigates to the process's path w/
|
||||
** that UUID in the query string.
|
||||
*******************************************************************************/
|
||||
const openModalProcess = async (process: QProcessMetaData = null) =>
|
||||
{
|
||||
let uuid = "";
|
||||
if (selectFullFilterState === "filter")
|
||||
{
|
||||
const filterForBackend = prepQueryFilterForBackend(queryFilter);
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = null;
|
||||
setRecordIdsForProcess(filterForBackend);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB(filterForBackend);
|
||||
}
|
||||
else if (selectFullFilterState === "filterSubset")
|
||||
{
|
||||
@ -1425,24 +1529,27 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
filterForBackend.skip = 0;
|
||||
filterForBackend.limit = selectionSubsetSize;
|
||||
setRecordIdsForProcess(filterForBackend);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB(filterForBackend);
|
||||
}
|
||||
else if (selectedIds.length > 0)
|
||||
{
|
||||
setRecordIdsForProcess(selectedIds);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB(selectedIds);
|
||||
}
|
||||
else
|
||||
{
|
||||
setRecordIdsForProcess([]);
|
||||
uuid = await storeRecordIdsForProcessInIndexedDB([]);
|
||||
}
|
||||
|
||||
navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}${getRecordsQueryString()}`);
|
||||
navigate(`${metaData?.getTablePathByName(tableName)}/${process.name}?recordsParam=recordsKey&recordsKey=${uuid}`);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** close callback for modal processes
|
||||
*******************************************************************************/
|
||||
const closeModalProcess = (event: object, reason: string) =>
|
||||
const closeModalProcess = (event: object, reason: string, warning?: string) =>
|
||||
{
|
||||
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
||||
{
|
||||
@ -1454,7 +1561,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
/////////////////////////////////////////////////////////////////////////
|
||||
const newPath = location.pathname.split("/");
|
||||
newPath.pop();
|
||||
navigate(newPath.join("/"));
|
||||
navigate(newPath.join("/"), warning ? {state: {warning: warning}} : undefined);
|
||||
|
||||
updateTable("close modal process");
|
||||
};
|
||||
@ -2333,22 +2440,38 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
|
||||
/////////////////////////////////////////////////////////////////
|
||||
if (pathParts[pathParts.length - 2] === tableName)
|
||||
{
|
||||
////////////////////////////////////////
|
||||
// try to find a process by this name //
|
||||
////////////////////////////////////////
|
||||
const processName = pathParts[pathParts.length - 1];
|
||||
const processList = allTableProcesses.filter(p => p.name == processName);
|
||||
if (processList.length > 0)
|
||||
let process = processList.length > 0 ? processList[0] : null;
|
||||
if(!process)
|
||||
{
|
||||
setActiveModalProcess(processList[0]);
|
||||
process = metaData?.processes.get(processName)
|
||||
}
|
||||
else if (metaData?.processes.has(processName))
|
||||
|
||||
if(process)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
// check for generic processes - should this be a specific attribute on the process? //
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
setActiveModalProcess(metaData?.processes.get(processName));
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// check if a recordsKey UUID (e.g., from indexedDB) is in the query string //
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const uuid = urlSearchParams.get("recordsKey")
|
||||
|
||||
if(uuid)
|
||||
{
|
||||
await launchModalProcessUsingRecordIdsFromIndexedDB(uuid, processList[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
setActiveModalProcess(processList[0]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`Couldn't find process named ${processName}`);
|
||||
setWarningAlert(`Couldn't find process named ${processName}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -114,8 +114,11 @@ class FilterUtils
|
||||
// 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. //
|
||||
// also, there are cases where we can get a null or "" as the only value in the //
|
||||
// values array - avoid sending that to the backend, as it comes back w/ all //
|
||||
// possible values, and a general "bad time" //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if (values && values.length > 0)
|
||||
if (values && values.length > 0 && values[0] !== null && values[0] !== undefined && values[0] !== "")
|
||||
{
|
||||
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values);
|
||||
}
|
||||
@ -395,10 +398,10 @@ class FilterUtils
|
||||
switch (andMoreFormat)
|
||||
{
|
||||
case "andNOther":
|
||||
labels.push(` and ${n.toLocaleString()} other value${n == 1 ? "" : "s"}.`);
|
||||
labels.push(` and ${n} other value${n == 1 ? "" : "s"}.`);
|
||||
break;
|
||||
case "+N":
|
||||
labels[labels.length-1] += ` +${n.toLocaleString()}`;
|
||||
labels[labels.length-1] += ` +${n}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -93,11 +93,6 @@ class TableUtils
|
||||
*******************************************************************************/
|
||||
public static getFieldAndTable(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
|
||||
{
|
||||
if(fieldName == null || tableMetaData == null)
|
||||
{
|
||||
return ([null, null]);
|
||||
}
|
||||
|
||||
if (fieldName.indexOf(".") > -1)
|
||||
{
|
||||
const nameParts = fieldName.split(".", 2);
|
||||
@ -115,7 +110,7 @@ class TableUtils
|
||||
return ([tableMetaData.fields.get(fieldName), tableMetaData]);
|
||||
}
|
||||
|
||||
return ([null, null]);
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user