From e96a189721b4e15ceb2e7bd2ca01b39ce69118b8 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 31 Jan 2024 09:46:59 -0600 Subject: [PATCH 1/3] CE-793 - More cleanup from initial pre-qa qa (human words, values, expressions) --- src/qqq/components/forms/DynamicSelect.tsx | 7 +- src/qqq/components/misc/SavedViews.tsx | 5 +- .../query/BasicAndAdvancedQueryControls.tsx | 3 +- .../components/query/FilterCriteriaRow.tsx | 39 +++++++++ src/qqq/models/query/RecordQueryView.ts | 18 ++++ src/qqq/pages/records/query/RecordQuery.tsx | 15 ++-- src/qqq/utils/qqq/FilterUtils.tsx | 82 ++++++++++++++++--- 7 files changed, 145 insertions(+), 24 deletions(-) diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index ccaeb92..005dfc7 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -321,9 +321,14 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe { setOpen(false); }} - isOptionEqualToValue={(option, value) => option.id === value.id} + isOptionEqualToValue={(option, value) => value !== null && value !== undefined && option.id === value.id} getOptionLabel={(option) => { + if(option === null || option === undefined) + { + return (""); + } + // @ts-ignore if(option && option.length) { diff --git a/src/qqq/components/misc/SavedViews.tsx b/src/qqq/components/misc/SavedViews.tsx index 0fd6886..f604181 100644 --- a/src/qqq/components/misc/SavedViews.tsx +++ b/src/qqq/components/misc/SavedViews.tsx @@ -47,7 +47,6 @@ import FormData from "form-data"; import React, {useEffect, useRef, useState} from "react"; import {useLocation, useNavigate} from "react-router-dom"; import {QCancelButton, QDeleteButton, QSaveButton, QSavedViewsMenuButton} from "qqq/components/buttons/DefaultButtons"; -import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip"; import QQueryColumns from "qqq/models/query/QQueryColumns"; import RecordQueryView from "qqq/models/query/RecordQueryView"; import FilterUtils from "qqq/utils/qqq/FilterUtils"; @@ -390,8 +389,8 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, vie } }; - diffVisibilityFunction(savedView.queryColumns, activeView.queryColumns, "Turned on visibility for "); - diffVisibilityFunction(activeView.queryColumns, savedView.queryColumns, "Turned off visibility for "); + diffVisibilityFunction(savedView.queryColumns, activeView.queryColumns, "Turned on "); + diffVisibilityFunction(activeView.queryColumns, savedView.queryColumns, "Turned off "); diffPinsFunction(savedView.queryColumns, activeView.queryColumns, "Changed pinned state for "); if(savedView.queryColumns.columns.map(c => c.name).join(",") != activeView.queryColumns.columns.map(c => c.name).join(",")) diff --git a/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx b/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx index 16d2697..18617dd 100644 --- a/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx +++ b/src/qqq/components/query/BasicAndAdvancedQueryControls.tsx @@ -347,7 +347,8 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo {queryFilter.criteria.map((criteria, i) => { - if(criteria && criteria.fieldName && criteria.operator) + const {criteriaIsValid} = validateCriteria(criteria, null); + if(criteriaIsValid) { const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName); counter++; diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index 745ca78..efd41bc 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -53,6 +53,27 @@ export enum ValueMode PVS_MULTI = "PVS_MULTI", } +const getValueModeRequiredCount = (valueMode: ValueMode): number => +{ + switch (valueMode) + { + case ValueMode.NONE: + return (0); + case ValueMode.SINGLE: + case ValueMode.SINGLE_DATE: + case ValueMode.SINGLE_DATE_TIME: + case ValueMode.PVS_SINGLE: + return (1); + case ValueMode.DOUBLE: + case ValueMode.DOUBLE_DATE: + case ValueMode.DOUBLE_DATE_TIME: + return (2); + case ValueMode.MULTI: + case ValueMode.PVS_MULTI: + return (null); + } +} + export interface OperatorOption { label: string; @@ -367,6 +388,24 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, { criteria.values = newValue.implicitValues; } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // we've seen cases where switching operators can sometimes put a null in as the first value... // + // that just causes a bad time (e.g., null pointers in Autocomplete), so, get rid of that. // + ////////////////////////////////////////////////////////////////////////////////////////////////// + if(criteria.values && criteria.values.length == 1 && criteria.values[0] == null) + { + criteria.values = []; + } + + if(newValue.valueMode) + { + const requiredValueCount = getValueModeRequiredCount(newValue.valueMode); + if(requiredValueCount != null && criteria.values.length > requiredValueCount) + { + criteria.values.splice(requiredValueCount); + } + } } else { diff --git a/src/qqq/models/query/RecordQueryView.ts b/src/qqq/models/query/RecordQueryView.ts index 9784b29..a705603 100644 --- a/src/qqq/models/query/RecordQueryView.ts +++ b/src/qqq/models/query/RecordQueryView.ts @@ -21,6 +21,7 @@ import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; import QQueryColumns, {PreLoadQueryColumns} from "qqq/models/query/QQueryColumns"; +import FilterUtils from "qqq/utils/qqq/FilterUtils"; /******************************************************************************* @@ -62,6 +63,23 @@ export default class RecordQueryView view.queryFilter = json.queryFilter as QQueryFilter; + ////////////////////////////////////////////////////////////////////////////////////////// + // it's important that some criteria values exist as expression objects - so - do that. // + ////////////////////////////////////////////////////////////////////////////////////////// + for (let i = 0; i < view.queryFilter?.criteria?.length; i++) + { + const criteria = view.queryFilter.criteria[i] + for (let j = 0; j < criteria?.values?.length; j++) + { + const value = criteria.values[j]; + const expression = FilterUtils.gridCriteriaValueToExpression(value); + if(expression) + { + criteria.values[j] = expression; + } + } + } + if(json.queryColumns) { view.queryColumns = QQueryColumns.buildFromJSON(json.queryColumns); diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 59fc00f..fbb5ca8 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -78,7 +78,6 @@ import TableUtils from "qqq/utils/qqq/TableUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; const CURRENT_SAVED_VIEW_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedViewId"; -const SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT = "qqq.seenJoinTables"; const DENSITY_LOCAL_STORAGE_KEY_ROOT = "qqq.density"; const VIEW_LOCAL_STORAGE_KEY_ROOT = "qqq.recordQueryView"; @@ -159,7 +158,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element // look for defaults in the local storage // //////////////////////////////////////////// const currentSavedViewLocalStorageKey = `${CURRENT_SAVED_VIEW_ID_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; - const seenJoinTablesLocalStorageKey = `${SEEN_JOIN_TABLES_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; const tableVariantLocalStorageKey = `${TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; const viewLocalStorageKey = `${VIEW_LOCAL_STORAGE_KEY_ROOT}.${tableName}`; @@ -167,10 +165,8 @@ 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 didDefaultVisibilityComeFromLocalStorage = false; let defaultRowsPerPage = 10; let defaultDensity = "standard" as GridDensity; - let seenJoinTables: {[tableName: string]: boolean} = {}; let defaultTableVariant: QTableVariant = null; let defaultMode = "basic"; let defaultQueryColumns: QQueryColumns = new PreLoadQueryColumns(); @@ -188,10 +184,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { defaultDensity = JSON.parse(localStorage.getItem(densityLocalStorageKey)); } - if (localStorage.getItem(seenJoinTablesLocalStorageKey)) - { - seenJoinTables = JSON.parse(localStorage.getItem(seenJoinTablesLocalStorageKey)); - } if (localStorage.getItem(tableVariantLocalStorageKey)) { defaultTableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey)); @@ -719,6 +711,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { if(pageState != "ready") { + console.log(`In updateTable, but pageSate[${pageState}] is not ready, so returning with noop`); + return; + } + + if(tableMetaData?.usesVariants && (!tableVariant || tableVariantPromptOpen)) + { + console.log("In updateTable, but a variant is needed, so returning with noop"); return; } diff --git a/src/qqq/utils/qqq/FilterUtils.tsx b/src/qqq/utils/qqq/FilterUtils.tsx index c99149b..9f55aab 100644 --- a/src/qqq/utils/qqq/FilterUtils.tsx +++ b/src/qqq/utils/qqq/FilterUtils.tsx @@ -189,7 +189,7 @@ class FilterUtils /******************************************************************************* ** *******************************************************************************/ - private static gridCriteriaValueToExpression(value: any) + public static gridCriteriaValueToExpression(value: any) { if (value && value.length) { @@ -311,6 +311,15 @@ class FilterUtils public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3): string { let valuesString = ""; + + if(criteria.operator == QCriteriaOperator.IS_BLANK || criteria.operator == QCriteriaOperator.IS_NOT_BLANK) + { + /////////////////////////////////////////////// + // we don't want values for these operators. // + /////////////////////////////////////////////// + return valuesString; + } + if (criteria.values && criteria.values.length) { let labels = [] as string[]; @@ -323,17 +332,41 @@ class FilterUtils for (let i = 0; i < maxLoops; i++) { - if(fieldMetaData.type == QFieldType.BOOLEAN) + const value = criteria.values[i]; + if (value.type == "NowWithOffset") { - labels.push(criteria.values[i] == true ? "yes" : "no") + const expression = new NowWithOffsetExpression(value); + labels.push(expression.toString()); } - else if (criteria.values[i] && criteria.values[i].label) + else if (value.type == "Now") { - labels.push(criteria.values[i].label); + const expression = new NowExpression(value); + labels.push(expression.toString()); + } + else if (value.type == "ThisOrLastPeriod") + { + const expression = new ThisOrLastPeriodExpression(value); + labels.push(expression.toString()); + } + else if(fieldMetaData.type == QFieldType.BOOLEAN) + { + labels.push(value == true ? "yes" : "no") + } + else if(fieldMetaData.type == QFieldType.DATE_TIME) + { + labels.push(ValueUtils.formatDateTime(value)); + } + else if(fieldMetaData.type == QFieldType.DATE) + { + labels.push(ValueUtils.formatDate(value)); + } + else if (value && value.label) + { + labels.push(value.label); } else { - labels.push(criteria.values[i]); + labels.push(value); } } @@ -395,13 +428,16 @@ class FilterUtils /******************************************************************************* ** *******************************************************************************/ - public static operatorToHumanString(criteria: QFilterCriteria): string + public static operatorToHumanString(criteria: QFilterCriteria, field: QFieldMetaData): string { if(criteria == null || criteria.operator == null) { return (null); } + const isDate = field.type == QFieldType.DATE; + const isDateTime = field.type == QFieldType.DATE_TIME; + try { switch(criteria.operator) @@ -428,17 +464,41 @@ class FilterUtils case QCriteriaOperator.NOT_CONTAINS: return ("does not contain"); case QCriteriaOperator.LESS_THAN: + if(isDate || isDateTime) + { + return ("is before") + } return ("less than"); case QCriteriaOperator.LESS_THAN_OR_EQUALS: + if(isDate) + { + return ("is on or before") + } + if(isDateTime) + { + return ("is at or before") + } return ("less than or equals"); case QCriteriaOperator.GREATER_THAN: + if(isDate || isDateTime) + { + return ("is after") + } return ("greater than or equals"); case QCriteriaOperator.GREATER_THAN_OR_EQUALS: + if(isDate) + { + return ("is on or after") + } + if(isDateTime) + { + return ("is at or after") + } return ("greater than or equals"); case QCriteriaOperator.IS_BLANK: - return ("is blank"); + return ("is empty"); case QCriteriaOperator.IS_NOT_BLANK: - return ("is not blank"); + return ("is not empty"); case QCriteriaOperator.BETWEEN: return ("is between"); case QCriteriaOperator.NOT_BETWEEN: @@ -470,12 +530,12 @@ class FilterUtils if(styled) { return (<> - {fieldLabel} {FilterUtils.operatorToHumanString(criteria)} {valuesString}  + {fieldLabel} {FilterUtils.operatorToHumanString(criteria, field)} {valuesString}  ); } else { - return (`${fieldLabel} ${FilterUtils.operatorToHumanString(criteria)} ${valuesString}`); + return (`${fieldLabel} ${FilterUtils.operatorToHumanString(criteria, field)} ${valuesString}`); } } From ac97ac016d67e2bd0383827a92ce5db02eb5aaee Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 1 Feb 2024 18:33:26 -0600 Subject: [PATCH 2/3] 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]); + } + } } } From 585294c06d37a34dbb33cb6f1b1555b2440bc465 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 1 Feb 2024 21:05:35 -0600 Subject: [PATCH 3/3] CE-798 fix launching processes, because somehow that broke in here... --- src/qqq/components/horseshoe/Breadcrumbs.tsx | 11 +++ src/qqq/components/misc/SavedViews.tsx | 2 +- src/qqq/pages/records/query/RecordQuery.tsx | 90 +++++++++++--------- 3 files changed, 63 insertions(+), 40 deletions(-) diff --git a/src/qqq/components/horseshoe/Breadcrumbs.tsx b/src/qqq/components/horseshoe/Breadcrumbs.tsx index c319b98..79e5720 100644 --- a/src/qqq/components/horseshoe/Breadcrumbs.tsx +++ b/src/qqq/components/horseshoe/Breadcrumbs.tsx @@ -92,11 +92,22 @@ function QBreadcrumbs({icon, title, route, light}: Props): JSX.Element let accumulatedPath = ""; for (let i = 0; i < routes.length; i++) { + //////////////////////////////////////////////////////// + // avoid showing "saved view" as a breadcrumb element // + //////////////////////////////////////////////////////// if(routes[i] === "savedView") { continue; } + /////////////////////////////////////////////////////////////////////// + // avoid showing the table name if it's the element before savedView // + /////////////////////////////////////////////////////////////////////// + if(i < routes.length - 1 && routes[i+1] == "savedView") + { + continue; + } + if(routes[i] === "") { continue; diff --git a/src/qqq/components/misc/SavedViews.tsx b/src/qqq/components/misc/SavedViews.tsx index 0248137..4ab6685 100644 --- a/src/qqq/components/misc/SavedViews.tsx +++ b/src/qqq/components/misc/SavedViews.tsx @@ -104,7 +104,7 @@ function SavedViews({qController, metaData, tableMetaData, currentSavedView, vie { setSavedViewsHaveLoaded(true); }); - }, [location, tableMetaData, currentSavedView, view]) // todo#elimGrid does this monitoring work?? + }, [location, tableMetaData]) /******************************************************************************* diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index a673ca0..f1f0c39 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -538,40 +538,22 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } }, [dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]) - ///////////////////////////////////////////////////////////////////////////////////////// - // monitor location changes - if our url looks like a process, then open that process. // - ///////////////////////////////////////////////////////////////////////////////////////// + + /******************************************************************************* + ** + *******************************************************************************/ + const urlLooksLikeProcess = (): boolean => + { + return (pathParts[pathParts.length - 2] === tableName); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + // monitor location changes - if our url looks like a savedView, then load that view, kinda // + ////////////////////////////////////////////////////////////////////////////////////////////// useEffect(() => { try { - ///////////////////////////////////////////////////////////////// - // the path for a process looks like: .../table/process // - // so if our tableName is in the -2 index, try to open process // - ///////////////////////////////////////////////////////////////// - if (pathParts[pathParts.length - 2] === tableName) - { - const processName = pathParts[pathParts.length - 1]; - const processList = allTableProcesses.filter(p => p.name == processName); - if (processList.length > 0) - { - setActiveModalProcess(processList[0]); - return; - } - else if (metaData?.processes.has(processName)) - { - /////////////////////////////////////////////////////////////////////////////////////// - // check for generic processes - should this be a specific attribute on the process? // - /////////////////////////////////////////////////////////////////////////////////////// - setActiveModalProcess(metaData?.processes.get(processName)); - return; - } - else - { - console.log(`Couldn't find process named ${processName}`); - } - } - ///////////////////////////////////////////////////////////////// // the path for a savedView looks like: .../table/savedView/32 // // so if path has '/savedView/' get last parsed string // @@ -609,12 +591,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { console.log(e); } - - //////////////////////////////////////////////////////////////////////////////////// - // if we didn't open a process... not sure what we do in the table/query use-case // - //////////////////////////////////////////////////////////////////////////////////// - setActiveModalProcess(null); - }, [location]); /******************************************************************************* @@ -678,6 +654,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setTableVariantPromptOpen(true); } + /******************************************************************************* ** *******************************************************************************/ @@ -2079,6 +2056,41 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element (async () => { + ////////////////////////////////////////////////////////////////////////////////////////////// + // once we've loaded meta data, let's check the location to see if we should open a process // + ////////////////////////////////////////////////////////////////////////////////////////////// + try + { + ///////////////////////////////////////////////////////////////// + // the path for a process looks like: .../table/process // + // so if our tableName is in the -2 index, try to open process // + ///////////////////////////////////////////////////////////////// + if (pathParts[pathParts.length - 2] === tableName) + { + const processName = pathParts[pathParts.length - 1]; + const processList = allTableProcesses.filter(p => p.name == processName); + if (processList.length > 0) + { + 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}`); + } + } + } + catch (e) + { + console.log(e); + } + if (searchParams && searchParams.has("filter")) { ////////////////////////////////////////////////////////////////////////////////////// @@ -2161,9 +2173,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { /////////////////////////////////////////////////////////////////////////////////////////////// // if the last time we were on this table, a currentSavedView was written to local storage - // - // then navigate back to that view's URL // + // then navigate back to that view's URL - unless - it looks like we're on a process! // /////////////////////////////////////////////////////////////////////////////////////////////// - if (localStorage.getItem(currentSavedViewLocalStorageKey)) + if (localStorage.getItem(currentSavedViewLocalStorageKey) && !urlLooksLikeProcess()) { const currentSavedViewId = Number.parseInt(localStorage.getItem(currentSavedViewLocalStorageKey)); console.log(`returning to previously active saved view ${currentSavedViewId}`); @@ -2531,7 +2543,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } { - tableMetaData && + tableMetaData && tableMetaData.usesVariants && { setTableVariantPromptOpen(false);