CE-798 bug fixes after qa

This commit is contained in:
2024-02-01 18:33:26 -06:00
parent e96a189721
commit ac97ac016d
3 changed files with 163 additions and 76 deletions

View File

@ -586,11 +586,11 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, vie
{ {
formData.append("tableName", tableMetaData.name); formData.append("tableName", tableMetaData.name);
//////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
// clone view via json serialization/deserialization // // clone view via json serialization/deserialization //
// then replace the queryFilter in it with a copy that has had its possible values changed to ids // // then replace the viewJson in it with a copy that has had its possible values changed to ids //
// then stringify that for the backend // // then stringify that for the backend //
//////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
const viewObject = JSON.parse(JSON.stringify(view)); const viewObject = JSON.parse(JSON.stringify(view));
viewObject.queryFilter = JSON.parse(JSON.stringify(FilterUtils.convertFilterPossibleValuesToIds(viewObject.queryFilter))); viewObject.queryFilter = JSON.parse(JSON.stringify(FilterUtils.convertFilterPossibleValuesToIds(viewObject.queryFilter)));
formData.append("viewJson", JSON.stringify(viewObject)); formData.append("viewJson", JSON.stringify(viewObject));

View File

@ -132,6 +132,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const pathParts = location.pathname.replace(/\/+$/, "").split("/"); const pathParts = location.pathname.replace(/\/+$/, "").split("/");
const [firstRender, setFirstRender] = useState(true); const [firstRender, setFirstRender] = useState(true);
const [isFirstRenderAfterChangingTables, setIsFirstRenderAfterChangingTables] = useState(false);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// manage "state" being passed from some screens (like delete) into query screen - by grabbing, and then deleting // // manage "state" being passed from some screens (like delete) into query screen - by grabbing, and then deleting //
@ -180,6 +181,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// only load things out of local storage on the first render // only load things out of local storage on the first render
if(firstRender) if(firstRender)
{ {
console.log("This is firstRender, so reading defaults from local storage...");
if (localStorage.getItem(densityLocalStorageKey)) if (localStorage.getItem(densityLocalStorageKey))
{ {
defaultDensity = JSON.parse(localStorage.getItem(densityLocalStorageKey)); defaultDensity = JSON.parse(localStorage.getItem(densityLocalStorageKey));
@ -192,6 +194,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
defaultView = RecordQueryView.buildFromJSON(localStorage.getItem(viewLocalStorageKey)); defaultView = RecordQueryView.buildFromJSON(localStorage.getItem(viewLocalStorageKey));
} }
setFirstRender(false);
} }
if(defaultView == null) if(defaultView == null)
@ -279,7 +283,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const [tableVariantPromptOpen, setTableVariantPromptOpen] = useState(false); const [tableVariantPromptOpen, setTableVariantPromptOpen] = useState(false);
const [alertContent, setAlertContent] = useState(""); const [alertContent, setAlertContent] = useState("");
const [currentSavedView, setCurrentSavedView] = useState(null as QRecord); const [currentSavedView, setCurrentSavedView] = useState(null as QRecord);
const [filterIdInLocation, setFilterIdInLocation] = useState(null as number); const [viewIdInLocation, setViewIdInLocation] = useState(null as number);
const [loadingSavedView, setLoadingSavedView] = useState(false); const [loadingSavedView, setLoadingSavedView] = useState(false);
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -350,6 +354,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
const [pageLoadingState, _] = useState(new LoadingState(forceUpdate)) const [pageLoadingState, _] = useState(new LoadingState(forceUpdate))
if(isFirstRenderAfterChangingTables)
{
setIsFirstRenderAfterChangingTables(false);
console.log("This is the first render after changing tables - so - setting state based on 'defaults' from localStorage");
setView(defaultView)
}
/******************************************************************************* /*******************************************************************************
** utility function to get the names of any join tables which are active, ** utility function to get the names of any join tables which are active,
** either as a visible column, or as a query criteria ** either as a visible column, or as a query criteria
@ -560,16 +572,16 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
} }
} }
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// the path for a savedView looks like: .../table/savedView/32 // // the path for a savedView looks like: .../table/savedView/32 //
// so if path has '/savedView/' get last parsed string // // so if path has '/savedView/' get last parsed string //
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
let currentSavedViewId = null as number; let currentSavedViewId = null as number;
if (location.pathname.indexOf("/savedView/") != -1) if (location.pathname.indexOf("/savedView/") != -1)
{ {
const parts = location.pathname.split("/"); const parts = location.pathname.split("/");
currentSavedViewId = Number.parseInt(parts[parts.length - 1]); currentSavedViewId = Number.parseInt(parts[parts.length - 1]);
setFilterIdInLocation(currentSavedViewId); setViewIdInLocation(currentSavedViewId);
///////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////
// in case page-state has already advanced to "ready" (e.g., and we're dealing with a user // // in case page-state has already advanced to "ready" (e.g., and we're dealing with a user //
@ -603,7 +615,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
//////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////
setActiveModalProcess(null); setActiveModalProcess(null);
}, [location, tableMetaData]); }, [location]);
/******************************************************************************* /*******************************************************************************
** **
@ -1137,7 +1149,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
// propagate filter's orderBy into grid's sort model // // propagate filter's orderBy into grid's sort model //
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
const gridSort = FilterUtils.getGridSortFromQueryFilter(view.queryFilter); const gridSort = FilterUtils.getGridSortFromQueryFilter(queryFilter);
setColumnSortModel(gridSort); setColumnSortModel(gridSort);
/////////////////////////////////////////////// ///////////////////////////////////////////////
@ -1404,22 +1416,16 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
** wrapper around setting current saved view (as a QRecord) - which also activates ** wrapper around setting current saved view (as a QRecord) - which also activates
** that view. ** that view.
*******************************************************************************/ *******************************************************************************/
const doSetCurrentSavedView = (savedView: QRecord) => const doSetCurrentSavedView = (savedViewRecord: QRecord) =>
{ {
setCurrentSavedView(savedView); setCurrentSavedView(savedViewRecord);
if(savedView) if(savedViewRecord)
{ {
(async () => (async () =>
{ {
const viewJson = savedView.values.get("viewJson") const viewJson = savedViewRecord.values.get("viewJson")
const newView = RecordQueryView.buildFromJSON(viewJson); const newView = RecordQueryView.buildFromJSON(viewJson);
newView.viewIdentity = "savedView:" + savedView.values.get("id");
///////////////////////////////////////////////////////////////////
// e.g., translate possible values from ids to objects w/ labels //
///////////////////////////////////////////////////////////////////
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, newView.queryFilter);
activateView(newView); activateView(newView);
@ -1431,7 +1437,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// todo can/should/does this move into the view's "identity"? // // todo can/should/does this move into the view's "identity"? //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
localStorage.setItem(currentSavedViewLocalStorageKey, `${savedView.values.get("id")}`); localStorage.setItem(currentSavedViewLocalStorageKey, `${savedViewRecord.values.get("id")}`);
})() })()
} }
else else
@ -1488,11 +1494,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
/******************************************************************************* /*******************************************************************************
** utility function to fetch a saved view from the backend. ** utility function to fetch a saved view from the backend.
*******************************************************************************/ *******************************************************************************/
const fetchSavedView = async (filterId: number): Promise<QRecord> => const fetchSavedView = async (id: number): Promise<QRecord> =>
{ {
let qRecord = null; let qRecord = null;
const formData = new FormData(); const formData = new FormData();
formData.append("id", filterId); formData.append("id", id);
formData.append(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000); formData.append(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000);
const processResult = await qController.processInit("querySavedView", formData, qController.defaultMultipartFormDataHeaders()); const processResult = await qController.processInit("querySavedView", formData, qController.defaultMultipartFormDataHeaders());
if (processResult instanceof QJobError) if (processResult instanceof QJobError)
@ -1504,12 +1510,73 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
{ {
const result = processResult as QJobComplete; const result = processResult as QJobComplete;
qRecord = new QRecord(result.values.savedViewList[0]); qRecord = new QRecord(result.values.savedViewList[0]);
//////////////////////////////////////////////////////////////////////////////
// make the view json a good and healthy object for the UI here. //
// such as, making values be what they'd be in the UI (not necessarily //
// what they're like in the backend); similarly, set anything that's unset. //
//////////////////////////////////////////////////////////////////////////////
const viewJson = qRecord.values.get("viewJson")
const view = RecordQueryView.buildFromJSON(viewJson);
view.viewIdentity = "savedView:" + id;
///////////////////////////////////////////////////////////////////
// e.g., translate possible values from ids to objects w/ labels //
///////////////////////////////////////////////////////////////////
await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, view.queryFilter);
///////////////////////////
// set columns if absent //
///////////////////////////
if(!view.queryColumns || !view.queryColumns.columns || view.queryColumns.columns?.length == 0)
{
view.queryColumns = QQueryColumns.buildDefaultForTable(tableMetaData);
}
qRecord.values.set("viewJson", JSON.stringify(view))
} }
return (qRecord); return (qRecord);
} }
/*******************************************************************************
** event handler for selecting 'filter' action from columns menu in advanced mode.
*******************************************************************************/
const handleColumnMenuAdvancedFilterSelection = (fieldName: string) =>
{
const newCriteria = new QFilterCriteria(fieldName, null, []);
if(!queryFilter.criteria)
{
queryFilter.criteria = [];
}
const length = queryFilter.criteria.length;
if (length > 0 && !queryFilter.criteria[length - 1].fieldName)
{
/////////////////////////////////////////////////////////////////////////////////
// if the last criteria in the filter has no field name (e.g., a default state //
// when there's 1 criteria that's all blank - may happen other times too?), //
// then replace that criteria with a new one for this field. //
/////////////////////////////////////////////////////////////////////////////////
queryFilter.criteria[length - 1] = newCriteria;
}
else
{
//////////////////////////////////////////////////////////////////////
// else, add a new criteria for this field onto the end of the list //
//////////////////////////////////////////////////////////////////////
queryFilter.criteria.push(newCriteria);
}
///////////////////////////
// open the filter panel //
///////////////////////////
gridApiRef.current.showPreferences(GridPreferencePanelsValue.filters)
}
/******************************************************************************* /*******************************************************************************
** event handler from columns menu - that copies values from that column ** event handler from columns menu - that copies values from that column
*******************************************************************************/ *******************************************************************************/
@ -1550,7 +1617,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
*******************************************************************************/ *******************************************************************************/
const openColumnStatistics = async (column: GridColDef) => const openColumnStatistics = async (column: GridColDef) =>
{ {
setFilterForColumnStats(queryFilter); setFilterForColumnStats(prepQueryFilterForBackend(queryFilter));
setColumnStatsFieldName(column.field); setColumnStatsFieldName(column.field);
const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field); const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field);
@ -1598,26 +1665,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<GridColumnMenuContainer ref={ref} {...props}> <GridColumnMenuContainer ref={ref} {...props}>
<SortGridMenuItems onClick={hideMenu} column={currentColumn!} /> <SortGridMenuItems onClick={hideMenu} column={currentColumn!} />
<MenuItem onClick={(e) =>
{ {
///////////////////////////////////////////////////////////////////////////////////////////////////////////// hideMenu(e);
// in advanced mode, use the default GridFilterMenuItem, which punches into the advanced/filter-builder UI // if(mode == "advanced")
///////////////////////////////////////////////////////////////////////////////////////////////////////////// {
mode == "advanced" && <GridFilterMenuItem onClick={hideMenu} column={currentColumn!} /> handleColumnMenuAdvancedFilterSelection(currentColumn.field);
} }
else
{
///////////////////////////////////////////////////////////////////////////////////
// for basic mode, use our own menu item to turn on this field as a quick-filter //
///////////////////////////////////////////////////////////////////////////////////
mode == "basic" && <MenuItem onClick={(e) =>
{ {
hideMenu(e);
// @ts-ignore // @ts-ignore
basicAndAdvancedQueryControlsRef.current.addField(currentColumn.field); basicAndAdvancedQueryControlsRef.current.addField(currentColumn.field);
}}> }
Filter }}>
</MenuItem> Filter
} </MenuItem>
<HideGridColMenuItem onClick={hideMenu} column={currentColumn!} /> <HideGridColMenuItem onClick={hideMenu} column={currentColumn!} />
<GridColumnsMenuItem onClick={hideMenu} column={currentColumn!} /> <GridColumnsMenuItem onClick={hideMenu} column={currentColumn!} />
@ -1662,29 +1724,32 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const CustomColumnHeaderFilterIconButton = forwardRef<any, ColumnHeaderFilterIconButtonProps>( const CustomColumnHeaderFilterIconButton = forwardRef<any, ColumnHeaderFilterIconButtonProps>(
function ColumnHeaderFilterIconButton(props: ColumnHeaderFilterIconButtonProps, ref) function ColumnHeaderFilterIconButton(props: ColumnHeaderFilterIconButtonProps, ref)
{ {
if(mode == "basic") let showFilter = false;
for (let i = 0; i < queryFilter?.criteria?.length; i++)
{ {
let showFilter = false; const criteria = queryFilter.criteria[i];
for (let i = 0; i < queryFilter?.criteria?.length; i++) if(criteria.fieldName == props.field && validateCriteria(criteria, null).criteriaIsValid)
{ {
const criteria = queryFilter.criteria[i]; showFilter = true;
if(criteria.fieldName == props.field && criteria.operator)
{
// todo - test values too right?
showFilter = true;
}
} }
}
if(showFilter) if(showFilter)
{
return (<IconButton size="small" sx={{p: "2px"}} onClick={(event) =>
{ {
return (<IconButton size="small" sx={{p: "2px"}} onClick={(event) => if(mode == "basic")
{ {
// @ts-ignore !? // @ts-ignore !?
basicAndAdvancedQueryControlsRef.current.addField(props.field); basicAndAdvancedQueryControlsRef.current.addField(props.field);
}
else
{
gridApiRef.current.showPreferences(GridPreferencePanelsValue.filters)
}
event.stopPropagation(); event.stopPropagation();
}}><Icon fontSize="small">filter_alt</Icon></IconButton>); }}><Icon fontSize="small">filter_alt</Icon></IconButton>);
}
} }
return (<></>); return (<></>);
@ -1815,7 +1880,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
totalRecords: totalRecords, totalRecords: totalRecords,
columnsModel: columnsModel, columnsModel: columnsModel,
columnVisibilityModel: columnVisibilityModel, columnVisibilityModel: columnVisibilityModel,
queryFilter: queryFilter queryFilter: prepQueryFilterForBackend(queryFilter)
} }
return ( return (
@ -2063,9 +2128,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setAlertContent("Error parsing filter from URL"); setAlertContent("Error parsing filter from URL");
} }
} }
else if (filterIdInLocation) else if (viewIdInLocation)
{ {
if(view.viewIdentity == `savedView:${filterIdInLocation}`) if(view.viewIdentity == `savedView:${viewIdInLocation}`)
{ {
///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
// if the view id in the location is the same as the view that was most-recently active here, // // if the view id in the location is the same as the view that was most-recently active here, //
@ -2073,14 +2138,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
// we want to keep their current settings as the active view - thus - use the current 'view' // // we want to keep their current settings as the active view - thus - use the current 'view' //
// state variable (e.g., from local storage) as the view to be activated. // // state variable (e.g., from local storage) as the view to be activated. //
///////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////
console.log(`Initializing view to a (potentially dirty) saved view (id=${filterIdInLocation})`); console.log(`Initializing view to a (potentially dirty) saved view (id=${viewIdInLocation})`);
activateView(view); activateView(view);
///////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////
// now fetch that savedView, and set it in state, but don't activate it - because that would overwrite // // now fetch that savedView, and set it in state, but don't activate it - because that would overwrite //
// anything the user may have changed (e.g., anything in the local-storage/state view). // // anything the user may have changed (e.g., anything in the local-storage/state view). //
///////////////////////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////////////////////
const savedViewRecord = await fetchSavedView(filterIdInLocation); const savedViewRecord = await fetchSavedView(viewIdInLocation);
setCurrentSavedView(savedViewRecord); setCurrentSavedView(savedViewRecord);
} }
else else
@ -2088,12 +2153,32 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a filterId in the location, but it isn't the last one the user had active, then set that as our active view // // if there's a filterId in the location, but it isn't the last one the user had active, then set that as our active view //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
console.log(`Initializing view to a clean saved view (id=${filterIdInLocation})`); console.log(`Initializing view to a clean saved view (id=${viewIdInLocation})`);
await handleSavedViewChange(filterIdInLocation); await handleSavedViewChange(viewIdInLocation);
} }
} }
else else
{ {
///////////////////////////////////////////////////////////////////////////////////////////////
// if the last time we were on this table, a currentSavedView was written to local storage - //
// then navigate back to that view's URL //
///////////////////////////////////////////////////////////////////////////////////////////////
if (localStorage.getItem(currentSavedViewLocalStorageKey))
{
const currentSavedViewId = Number.parseInt(localStorage.getItem(currentSavedViewLocalStorageKey));
console.log(`returning to previously active saved view ${currentSavedViewId}`);
navigate(`${metaData.getTablePathByName(tableName)}/savedView/${currentSavedViewId}`);
setViewIdInLocation(currentSavedViewId);
/////////////////////////////////////////////////////////////////////////////////////////////////////
// return - without activating any view, and actually, reset the pageState back to loadedMetaData, //
// so the useEffect that monitors location will see the change, and will set viewIdInLocation //
// so upon a re-render we'll hit this block again. //
/////////////////////////////////////////////////////////////////////////////////////////////////////
setPageState("loadedMetaData")
return;
}
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
// view is ad-hoc - just activate the view that was last active // // view is ad-hoc - just activate the view that was last active //
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
@ -2207,6 +2292,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
setQueryFilter(new QQueryFilter()); setQueryFilter(new QQueryFilter());
setQueryColumns(new PreLoadQueryColumns()); setQueryColumns(new PreLoadQueryColumns());
setRows([]); setRows([]);
setIsFirstRenderAfterChangingTables(true);
return (getLoadingScreen()); return (getLoadingScreen());
} }
@ -2264,15 +2350,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
return (getLoadingScreen()); return (getLoadingScreen());
} }
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for basic mode, set a custom ColumnHeaderFilterIconButton - w/ action to activate basic-mode quick-filter //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
let restOfDataGridProCustomComponents: any = {}
if(mode == "basic")
{
restOfDataGridProCustomComponents.ColumnHeaderFilterIconButton = CustomColumnHeaderFilterIconButton;
}
//////////////////////// ////////////////////////
// main screen render // // main screen render //
//////////////////////// ////////////////////////
@ -2379,7 +2456,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
ColumnMenu: CustomColumnMenu, ColumnMenu: CustomColumnMenu,
ColumnsPanel: CustomColumnsPanel, ColumnsPanel: CustomColumnsPanel,
FilterPanel: CustomFilterPanel, FilterPanel: CustomFilterPanel,
... restOfDataGridProCustomComponents ColumnHeaderFilterIconButton: CustomColumnHeaderFilterIconButton,
}} }}
componentsProps={{ componentsProps={{
columnsPanel: columnsPanel:

View File

@ -129,18 +129,28 @@ class FilterUtils
} }
} }
//////////////////////////////////////////////////////////////////////////
// replace objects that look like expressions with expression instances //
//////////////////////////////////////////////////////////////////////////
if (values && values.length) if (values && values.length)
{ {
for (let i = 0; i < values.length; i++) for (let i = 0; i < values.length; i++)
{ {
//////////////////////////////////////////////////////////////////////////
// replace objects that look like expressions with expression instances //
//////////////////////////////////////////////////////////////////////////
const expression = this.gridCriteriaValueToExpression(values[i]); const expression = this.gridCriteriaValueToExpression(values[i]);
if (expression) if (expression)
{ {
values[i] = expression; values[i] = expression;
} }
else
{
///////////////////////////////////////////
// make date-times work for the frontend //
///////////////////////////////////////////
if (field.type == QFieldType.DATE_TIME)
{
values[i] = ValueUtils.formatDateTimeValueForForm(values[i]);
}
}
} }
} }