Switch to write recordIds/queryFilter to IndexedDB when launching process (instead of passing on query string).

This commit is contained in:
2024-02-27 18:18:48 -06:00
parent aed1c9d4d0
commit e7b5821fbd

View File

@ -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)
{
process = metaData?.processes.get(processName)
}
if(process)
{
//////////////////////////////////////////////////////////////////////////////
// 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 if (metaData?.processes.has(processName))
{
///////////////////////////////////////////////////////////////////////////////////////
// check for generic processes - should this be a specific attribute on the process? //
///////////////////////////////////////////////////////////////////////////////////////
setActiveModalProcess(metaData?.processes.get(processName));
}
else
{
console.log(`Couldn't find process named ${processName}`);
setWarningAlert(`Couldn't find process named ${processName}`)
}
}
}