diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 75d8601..8860c62 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -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 = () => ); } - /******************************************************************************* ** 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 => + { + 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 => + { + 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}`) } } }