From ac97ac016d67e2bd0383827a92ce5db02eb5aaee Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 1 Feb 2024 18:33:26 -0600 Subject: [PATCH] CE-798 bug fixes after qa --- src/qqq/components/misc/SavedViews.tsx | 10 +- src/qqq/pages/records/query/RecordQuery.tsx | 213 +++++++++++++------- src/qqq/utils/qqq/FilterUtils.tsx | 16 +- 3 files changed, 163 insertions(+), 76 deletions(-) diff --git a/src/qqq/components/misc/SavedViews.tsx b/src/qqq/components/misc/SavedViews.tsx index f604181..0248137 100644 --- a/src/qqq/components/misc/SavedViews.tsx +++ b/src/qqq/components/misc/SavedViews.tsx @@ -586,11 +586,11 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, vie { formData.append("tableName", tableMetaData.name); - //////////////////////////////////////////////////////////////////////////////////////////////////// - // 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 stringify that for the backend // - //////////////////////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////// + // clone view via json serialization/deserialization // + // then replace the viewJson in it with a copy that has had its possible values changed to ids // + // then stringify that for the backend // + ///////////////////////////////////////////////////////////////////////////////////////////////// const viewObject = JSON.parse(JSON.stringify(view)); viewObject.queryFilter = JSON.parse(JSON.stringify(FilterUtils.convertFilterPossibleValuesToIds(viewObject.queryFilter))); formData.append("viewJson", JSON.stringify(viewObject)); diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index fbb5ca8..a673ca0 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -132,6 +132,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const pathParts = location.pathname.replace(/\/+$/, "").split("/"); 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 // @@ -180,6 +181,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element // only load things out of local storage on the first render if(firstRender) { + console.log("This is firstRender, so reading defaults from local storage..."); if (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)); } + + setFirstRender(false); } if(defaultView == null) @@ -279,7 +283,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [tableVariantPromptOpen, setTableVariantPromptOpen] = useState(false); const [alertContent, setAlertContent] = useState(""); 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); ///////////////////////////////////////////////////// @@ -350,6 +354,14 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element /////////////////////////////////////////////////////////////////////////////////////////// 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, ** 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 // - // so if path has '/savedView/' get last parsed string // - ///////////////////////////////////////////////////////////////////// + // so if path has '/savedView/' get last parsed string // + ///////////////////////////////////////////////////////////////// let currentSavedViewId = null as number; if (location.pathname.indexOf("/savedView/") != -1) { const parts = location.pathname.split("/"); 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 // @@ -603,7 +615,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element //////////////////////////////////////////////////////////////////////////////////// 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 // /////////////////////////////////////////////////////// - const gridSort = FilterUtils.getGridSortFromQueryFilter(view.queryFilter); + const gridSort = FilterUtils.getGridSortFromQueryFilter(queryFilter); 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 ** that view. *******************************************************************************/ - const doSetCurrentSavedView = (savedView: QRecord) => + const doSetCurrentSavedView = (savedViewRecord: QRecord) => { - setCurrentSavedView(savedView); + setCurrentSavedView(savedViewRecord); - if(savedView) + if(savedViewRecord) { (async () => { - const viewJson = savedView.values.get("viewJson") + const viewJson = savedViewRecord.values.get("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); @@ -1431,7 +1437,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element //////////////////////////////////////////////////////////////// // 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 @@ -1488,11 +1494,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element /******************************************************************************* ** utility function to fetch a saved view from the backend. *******************************************************************************/ - const fetchSavedView = async (filterId: number): Promise => + const fetchSavedView = async (id: number): Promise => { let qRecord = null; const formData = new FormData(); - formData.append("id", filterId); + formData.append("id", id); formData.append(QController.STEP_TIMEOUT_MILLIS_PARAM_NAME, 60 * 1000); const processResult = await qController.processInit("querySavedView", formData, qController.defaultMultipartFormDataHeaders()); if (processResult instanceof QJobError) @@ -1504,12 +1510,73 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { const result = processResult as QJobComplete; 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); } + /******************************************************************************* + ** 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 *******************************************************************************/ @@ -1550,7 +1617,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element *******************************************************************************/ const openColumnStatistics = async (column: GridColDef) => { - setFilterForColumnStats(queryFilter); + setFilterForColumnStats(prepQueryFilterForBackend(queryFilter)); setColumnStatsFieldName(column.field); const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field); @@ -1598,26 +1665,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element + { - ///////////////////////////////////////////////////////////////////////////////////////////////////////////// - // in advanced mode, use the default GridFilterMenuItem, which punches into the advanced/filter-builder UI // - ///////////////////////////////////////////////////////////////////////////////////////////////////////////// - mode == "advanced" && - } - - { - /////////////////////////////////////////////////////////////////////////////////// - // for basic mode, use our own menu item to turn on this field as a quick-filter // - /////////////////////////////////////////////////////////////////////////////////// - mode == "basic" && + hideMenu(e); + if(mode == "advanced") + { + handleColumnMenuAdvancedFilterSelection(currentColumn.field); + } + else { - hideMenu(e); // @ts-ignore basicAndAdvancedQueryControlsRef.current.addField(currentColumn.field); - }}> - Filter - - } + } + }}> + Filter + @@ -1662,29 +1724,32 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const CustomColumnHeaderFilterIconButton = forwardRef( function ColumnHeaderFilterIconButton(props: ColumnHeaderFilterIconButtonProps, ref) { - if(mode == "basic") + let showFilter = false; + for (let i = 0; i < queryFilter?.criteria?.length; i++) { - let showFilter = false; - for (let i = 0; i < queryFilter?.criteria?.length; i++) + const criteria = queryFilter.criteria[i]; + if(criteria.fieldName == props.field && validateCriteria(criteria, null).criteriaIsValid) { - const criteria = queryFilter.criteria[i]; - if(criteria.fieldName == props.field && criteria.operator) - { - // todo - test values too right? - showFilter = true; - } + showFilter = true; } + } - if(showFilter) + if(showFilter) + { + return ( { - return ( + if(mode == "basic") { // @ts-ignore !? basicAndAdvancedQueryControlsRef.current.addField(props.field); + } + else + { + gridApiRef.current.showPreferences(GridPreferencePanelsValue.filters) + } - event.stopPropagation(); - }}>filter_alt); - } + event.stopPropagation(); + }}>filter_alt); } return (<>); @@ -1815,7 +1880,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element totalRecords: totalRecords, columnsModel: columnsModel, columnVisibilityModel: columnVisibilityModel, - queryFilter: queryFilter + queryFilter: prepQueryFilterForBackend(queryFilter) } return ( @@ -2063,9 +2128,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element 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, // @@ -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' // // 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); ///////////////////////////////////////////////////////////////////////////////////////////////////////// // 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). // ///////////////////////////////////////////////////////////////////////////////////////////////////////// - const savedViewRecord = await fetchSavedView(filterIdInLocation); + const savedViewRecord = await fetchSavedView(viewIdInLocation); setCurrentSavedView(savedViewRecord); } 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 // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - console.log(`Initializing view to a clean saved view (id=${filterIdInLocation})`); - await handleSavedViewChange(filterIdInLocation); + console.log(`Initializing view to a clean saved view (id=${viewIdInLocation})`); + await handleSavedViewChange(viewIdInLocation); } } 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 // ////////////////////////////////////////////////////////////////// @@ -2207,6 +2292,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setQueryFilter(new QQueryFilter()); setQueryColumns(new PreLoadQueryColumns()); setRows([]); + setIsFirstRenderAfterChangingTables(true); return (getLoadingScreen()); } @@ -2264,15 +2350,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element 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 // //////////////////////// @@ -2379,7 +2456,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element ColumnMenu: CustomColumnMenu, ColumnsPanel: CustomColumnsPanel, FilterPanel: CustomFilterPanel, - ... restOfDataGridProCustomComponents + ColumnHeaderFilterIconButton: CustomColumnHeaderFilterIconButton, }} componentsProps={{ columnsPanel: diff --git a/src/qqq/utils/qqq/FilterUtils.tsx b/src/qqq/utils/qqq/FilterUtils.tsx index 9f55aab..fe7c45e 100644 --- a/src/qqq/utils/qqq/FilterUtils.tsx +++ b/src/qqq/utils/qqq/FilterUtils.tsx @@ -129,18 +129,28 @@ class FilterUtils } } - ////////////////////////////////////////////////////////////////////////// - // replace objects that look like expressions with expression instances // - ////////////////////////////////////////////////////////////////////////// if (values && values.length) { for (let i = 0; i < values.length; i++) { + ////////////////////////////////////////////////////////////////////////// + // replace objects that look like expressions with expression instances // + ////////////////////////////////////////////////////////////////////////// const expression = this.gridCriteriaValueToExpression(values[i]); if (expression) { values[i] = expression; } + else + { + /////////////////////////////////////////// + // make date-times work for the frontend // + /////////////////////////////////////////// + if (field.type == QFieldType.DATE_TIME) + { + values[i] = ValueUtils.formatDateTimeValueForForm(values[i]); + } + } } }