Compare commits

..

16 Commits

Author SHA1 Message Date
b1eba925fa Add idb - library for working with IndexedDB. 2024-02-27 18:19:21 -06:00
e7b5821fbd Switch to write recordIds/queryFilter to IndexedDB when launching process (instead of passing on query string). 2024-02-27 18:18:48 -06:00
aed1c9d4d0 Update to POST (as multipart form) instead of query-string - so yuge filters work. 2024-02-27 18:17:51 -06:00
88a4c17bbc CE-798 follow-up - in cleanupValuesInFilerFromQueryString, don't try to translate [null] to a list of possible values (which fetches all of them)... 2024-02-27 13:57:04 -06:00
2900cd8593 CE-798 follow-up - Prevent tab in date/date-time filter value input boxes from closing a quick-filter menu (via an onKeyDown handler) 2024-02-27 13:35:37 -06:00
8ab0f5f549 CE-798 follow-up - increase width of date-time boxes (was too small to see all the fields!) 2024-02-27 11:56:54 -06:00
8cffbbcac4 Update to cimg/openjdk:17.0.9 2024-02-22 20:30:51 -06:00
37eb280d79 Revert to previous check for disabling export-menu items - that is - totalRecords === 0, instead of !totalRecords. makes tables w/o count allowed to do exports again. 2024-02-22 12:16:27 -06:00
948aee70fd Disable 'c' hotkey for columns (broke with buiding custom columsn button instead of using data grid's) 2024-02-21 19:43:20 -06:00
f0c1af18d0 Fix last commit (shouldn't add layout to ChartSubheaderWithData) 2024-02-21 19:42:00 -06:00
fa65d6c0ad Add cursor:pointer to PieChart and StackedBarChart 2024-02-21 19:18:34 -06:00
d6c9bf79b1 Update circleci/browser-tools@1.4.7 and try to re-activate verify 2024-02-19 14:49:55 -06:00
677b93a09f Turn off broken selenium/int tests 2024-02-19 14:49:02 -06:00
314bf0fd67 Fix to clear out limit when using a select-all filter for launching processes 2024-02-19 13:47:04 -06:00
76642f13e9 HOTFIX - Change defaultRowsPerPage from 10 to 50 2024-02-16 11:23:31 -06:00
0eaf171523 Merge pull request #43 from Kingsrook/feature/CE-798-quick-filters-fixes
Feature/ce 798 quick filters fixes
2024-02-16 10:56:15 -06:00
12 changed files with 286 additions and 97 deletions

View File

@ -2,12 +2,12 @@ version: 2.1
orbs:
node: circleci/node@5.1.0
browser-tools: circleci/browser-tools@1.4.6
browser-tools: circleci/browser-tools@1.4.7
executors:
java17:
docker:
- image: 'cimg/openjdk:17.0'
- image: 'cimg/openjdk:17.0.9'
commands:
install_java17:

View File

@ -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",

View File

@ -354,8 +354,7 @@ const CommandMenu = ({metaData}: Props) =>
<Grid container columnSpacing={5} rowSpacing={1}>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>n</span>Create a New Record</Grid>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>r</span>Refresh the Query</Grid>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>c</span>Open the Columns Panel</Grid>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Panel</Grid>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Builder (Advanced mode only)</Grid>
</Grid>
<Typography variant="h6" pt={3}>Record View</Typography>

View File

@ -45,7 +45,7 @@ export default function ExportMenuItem(props: QExportMenuItemProps)
return (
<MenuItem
disabled={!totalRecords}
disabled={totalRecords === 0}
onClick={() =>
{
///////////////////////////////////////////////////////////////////////////////

View File

@ -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}

View File

@ -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}

View File

@ -39,65 +39,79 @@ ChartJS.register(
Legend
);
export const options = {
maintainAspectRatio: false,
responsive: true,
animation: {
duration: 0
},
elements: {
bar: {
borderRadius: 4
}
},
plugins: {
tooltip: {
// todo - some configs around this
callbacks: {
title: function(context: any)
{
return ("");
},
label: function(context: any)
{
if(context.dataset.label.startsWith(context.label))
{
return `${context.label}: ${context.formattedValue}`;
}
else
export const makeOptions = (data: DefaultChartData) =>
{
return({
maintainAspectRatio: false,
responsive: true,
animation: {
duration: 0
},
elements: {
bar: {
borderRadius: 4
}
},
onHover: function (event: any, elements: any[], chart: any)
{
if(event.type == "mousemove" && elements.length > 0 && data.urls && data.urls.length > elements[0].index && data.urls[elements[0].index])
{
chart.canvas.style.cursor = "pointer";
}
else
{
chart.canvas.style.cursor = "default";
}
},
plugins: {
tooltip: {
// todo - some configs around this
callbacks: {
title: function(context: any)
{
return ("");
},
label: function(context: any)
{
if(context.dataset.label.startsWith(context.label))
{
return `${context.label}: ${context.formattedValue}`;
}
else
{
return ("");
}
}
}
},
legend: {
position: "bottom",
labels: {
usePointStyle: true,
pointStyle: "circle",
boxHeight: 6,
boxWidth: 6,
padding: 12,
font: {
size: 14
}
}
}
},
legend: {
position: "bottom",
labels: {
usePointStyle: true,
pointStyle: "circle",
boxHeight: 6,
boxWidth: 6,
padding: 12,
font: {
size: 14
}
}
}
},
scales: {
x: {
stacked: true,
grid: {display: false},
ticks: {autoSkip: false, maxRotation: 90}
scales: {
x: {
stacked: true,
grid: {display: false},
ticks: {autoSkip: false, maxRotation: 90}
},
y: {
stacked: true,
position: "right",
ticks: {precision: 0}
},
},
y: {
stacked: true,
position: "right",
ticks: {precision: 0}
},
},
};
});
}
interface Props
{
@ -151,7 +165,7 @@ function StackedBarChart({data, chartSubheaderData}: Props): JSX.Element
<Box>
{chartSubheaderData && (<ChartSubheaderWithData chartSubheaderData={chartSubheaderData} />)}
<Box width="100%" height="300px">
<Bar data={data} options={options} getElementsAtEvent={handleClick} />
<Bar data={data} options={makeOptions(data)} getElementsAtEvent={handleClick} />
</Box>
</Box>
) : <Skeleton sx={{marginLeft: "20px", marginRight: "20px", height: "200px"}} />;

View File

@ -70,7 +70,7 @@ function PieChart({description, chartData, chartSubheaderData}: Props): JSX.Elem
chartData.dataset.backgroundColors = chartColors;
}
}
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {});
const {data, options} = configs(chartData?.labels || [], chartData?.dataset || {}, chartData?.dataset?.urls);
useEffect(() =>
{

View File

@ -23,7 +23,7 @@ import colors from "qqq/assets/theme/base/colors";
const {gradients, dark} = colors;
function configs(labels: any, datasets: any)
function configs(labels: any, datasets: any, urls: string[] | undefined)
{
const backgroundColors = [];
@ -66,6 +66,17 @@ function configs(labels: any, datasets: any)
options: {
maintainAspectRatio: false,
responsive: true,
onHover: function (event: any, elements: any[], chart: any)
{
if(event.type == "mousemove" && elements.length > 0 && urls && urls.length > elements[0].index && urls[elements[0].index])
{
chart.canvas.style.cursor = "pointer";
}
else
{
chart.canvas.style.cursor = "default";
}
},
plugins: {
tooltip: {
callbacks: {

View File

@ -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);
}

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.
**
@ -168,7 +175,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// define some default values (e.g., to be used if nothing in local storage or no active view) //
/////////////////////////////////////////////////////////////////////////////////////////////////
let defaultSort = [] as GridSortItem[];
let defaultRowsPerPage = 10;
let defaultRowsPerPage = 50;
let defaultDensity = "standard" as GridDensity;
let defaultTableVariant: QTableVariant = null;
let defaultMode = "basic";
@ -610,11 +617,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
e.preventDefault()
updateTable("'r' keyboard event");
}
/*
// disable until we add a ... ref down to let us programmatically open Columns button
else if (! e.metaKey && e.key === "c")
{
e.preventDefault()
gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns)
}
*/
else if (! e.metaKey && e.key === "f")
{
e.preventDefault()
@ -1380,7 +1390,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
if (selectFullFilterState === "filter")
{
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(prepQueryFilterForBackend(queryFilter)))}`;
const filterForBackend = prepQueryFilterForBackend(queryFilter);
filterForBackend.skip = 0;
filterForBackend.limit = null;
return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
}
if (selectFullFilterState === "filterSubset")
@ -1401,14 +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")
{
setRecordIdsForProcess(prepQueryFilterForBackend(queryFilter));
const filterForBackend = prepQueryFilterForBackend(queryFilter);
filterForBackend.skip = 0;
filterForBackend.limit = null;
setRecordIdsForProcess(filterForBackend);
uuid = await storeRecordIdsForProcessInIndexedDB(filterForBackend);
}
else if (selectFullFilterState === "filterSubset")
{
@ -1416,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")
{
@ -1445,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");
};
@ -2103,20 +2219,32 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{
if(selectedIndex == 0)
{
///////////////
// this page //
///////////////
programmaticallySelectSomeOrAllRows();
setSelectFullFilterState("checked")
}
else if(selectedIndex == 1)
{
///////////////////////
// full query result //
///////////////////////
programmaticallySelectSomeOrAllRows();
setSelectFullFilterState("filter")
}
else if(selectedIndex == 2)
{
////////////////////////////
// subset of query result //
////////////////////////////
setSelectionSubsetSizePromptOpen(true);
}
else if(selectedIndex == 3)
{
/////////////////////
// clear selection //
/////////////////////
setSelectFullFilterState("n/a")
setRowSelectionModel([]);
setSelectedIds([]);
@ -2312,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}`)
}
}
}

View File

@ -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);
}