From 73c907a3e1e90f18f13491d1015a24c434f20cea Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 16 Apr 2024 11:36:46 -0500 Subject: [PATCH] CE-1115 Fix so that record query popup actually shows the filter & columns that are on the report edit screen... fix filters saved w/ record to be prep'ed for backend; refactor that prepForBackend method out of RecordQuery, into FilterUtils; update recordQuery to be a better manager of counts (showing when counting after the initial load, plus not always re-counting (e.g., when paginating) --- .../widgets/misc/ReportSetupWidget.tsx | 36 ++++-- src/qqq/pages/records/query/RecordQuery.tsx | 109 +++++++++--------- src/qqq/utils/qqq/FilterUtils.tsx | 53 +++++++++ 3 files changed, 133 insertions(+), 65 deletions(-) diff --git a/src/qqq/components/widgets/misc/ReportSetupWidget.tsx b/src/qqq/components/widgets/misc/ReportSetupWidget.tsx index 9cdf995..4195139 100644 --- a/src/qqq/components/widgets/misc/ReportSetupWidget.tsx +++ b/src/qqq/components/widgets/misc/ReportSetupWidget.tsx @@ -86,11 +86,17 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal const [alertContent, setAlertContent] = useState(null as string); + ////////////////////////////////////////////////////////////////////////////////////////////////// + // we'll actually keep 2 copies of the query filter around here - // + // the one in the record (as json) is one that the backend likes (e.g., possible values as ids) // + // this "frontend" one is one that the frontend can use (possible values as objects w/ labels). // + ////////////////////////////////////////////////////////////////////////////////////////////////// + const [frontendQueryFilter, setFrontendQueryFilter] = useState(null as QQueryFilter); + const {helpHelpActive} = useContext(QContext); const recordQueryRef = useRef(); - ///////////////////////////// // load values from record // ///////////////////////////// @@ -100,10 +106,10 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal queryFilter = new QQueryFilter(); } - let columns = recordValues["columnsJson"] && JSON.parse(recordValues["columnsJson"]) as QQueryColumns; - if(!columns) + let columns: QQueryColumns = null; + if(recordValues["columnsJson"]) { - columns = new QQueryColumns(); + columns = QQueryColumns.buildFromJSON(recordValues["columnsJson"]); } ////////////////////////////////////////////////////////////////////// @@ -117,6 +123,10 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal { const tableMetaData = await qController.loadTableMetaData(recordValues["tableName"]) setTableMetaData(tableMetaData); + + const queryFilterForFrontend = Object.assign({}, queryFilter); + await FilterUtils.cleanupValuesInFilerFromQueryString(qController, tableMetaData, queryFilterForFrontend) + setFrontendQueryFilter(queryFilterForFrontend) })(); } }, [recordValues]); @@ -150,7 +160,14 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal view.queryColumns.sortColumnsFixingPinPositions(); - onSaveCallback({queryFilterJson: JSON.stringify(view.queryFilter), columnsJson: JSON.stringify(view.queryColumns)}); + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // keep the query filter that came from the recordQuery screen as the front-end version (w/ possible value objects) // + // but prep a copy of it for the backend, to stringify as json in the record being edited // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + setFrontendQueryFilter(view.queryFilter); + const filter = FilterUtils.prepQueryFilterForBackend(tableMetaData, view.queryFilter); + + onSaveCallback({queryFilterJson: JSON.stringify(filter), columnsJson: JSON.stringify(view.queryColumns)}); closeEditor(); } @@ -197,7 +214,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal { if(tableMetaData) { - if(queryFilter?.criteria?.length > 0 || queryFilter?.subFilters?.length > 0) + if(frontendQueryFilter?.criteria?.length > 0 || frontendQueryFilter?.subFilters?.length > 0) { return (true); } @@ -271,7 +288,7 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal
Query Filter
{ mayShowQueryPreview() && - 0} removeCriteriaByIndexCallback={null} /> + 0} removeCriteriaByIndexCallback={null} /> } { !mayShowQueryPreview() && @@ -329,7 +346,10 @@ export default function ReportSetupWidget({isEditable, widgetMetaData, recordVal ref={recordQueryRef} table={tableMetaData} usage="reportSetup" - isModal={true} /> + isModal={true} + initialQueryFilter={frontendQueryFilter} + initialColumns={columns} + /> } diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 1c5e016..3b6d111 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -92,6 +92,8 @@ interface Props launchProcess?: QProcessMetaData; usage?: QueryScreenUsage; isModal?: boolean; + initialQueryFilter?: QQueryFilter; + initialColumns?: QQueryColumns; } /////////////////////////////////////////////////////// @@ -123,7 +125,7 @@ const getLoadingScreen = (isModal: boolean) => ** ** Yuge component. The best. Lots of very smart people are saying so. *******************************************************************************/ -const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => +const RecordQuery = forwardRef(({table, usage, isModal, initialQueryFilter, initialColumns}: Props, ref) => { const tableName = table.name; const [searchParams] = useSearchParams(); @@ -193,7 +195,9 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => ///////////////////////////////////// const densityLocalStorageKey = `${DENSITY_LOCAL_STORAGE_KEY_ROOT}`; - // only load things out of local storage on the first render + /////////////////////////////////////////////////////////////// + // only load things out of local storage on the first render // + /////////////////////////////////////////////////////////////// if (firstRender) { console.log("This is firstRender, so reading defaults from local storage..."); @@ -224,6 +228,25 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => defaultView.mode = defaultMode; } + if(firstRender) + { + ///////////////////////////////////////////////////////////////////////// + // allow a caller to send in an initial filter & set of columns. // + // only to be used on "first render" // + // JSON.parse(JSON.stringify()) to do deep clone and keep object clean // + // unclear why not needed on initialColumns... // + ///////////////////////////////////////////////////////////////////////// + if (initialQueryFilter) + { + defaultView.queryFilter = JSON.parse(JSON.stringify(initialQueryFilter)); + } + + if (initialColumns) + { + defaultView.queryColumns = initialColumns; + } + } + ///////////////////////////////////////////////////////////////////////////////////////// // in case the view is missing any of these attributes, give them a reasonable default // ///////////////////////////////////////////////////////////////////////////////////////// @@ -431,51 +454,6 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => }; - /******************************************************************************* - ** - *******************************************************************************/ - const prepQueryFilterForBackend = (sourceFilter: QQueryFilter) => - { - const filterForBackend = new QQueryFilter([], sourceFilter.orderBys, sourceFilter.subFilters, sourceFilter.booleanOperator); - for (let i = 0; i < sourceFilter?.criteria?.length; i++) - { - const criteria = sourceFilter.criteria[i]; - const {criteriaIsValid} = validateCriteria(criteria, null); - if (criteriaIsValid) - { - if (criteria.operator == QCriteriaOperator.IS_BLANK || criteria.operator == QCriteriaOperator.IS_NOT_BLANK) - { - /////////////////////////////////////////////////////////////////////////////////////////// - // do this to avoid submitting an empty-string argument for blank/not-blank operators... // - /////////////////////////////////////////////////////////////////////////////////////////// - filterForBackend.criteria.push(new QFilterCriteria(criteria.fieldName, criteria.operator, [])); - } - else - { - /////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // else push a clone of the criteria - since it may get manipulated below (convertFilterPossibleValuesToIds) // - /////////////////////////////////////////////////////////////////////////////////////////////////////////////// - const [field] = FilterUtils.getField(tableMetaData, criteria.fieldName); - filterForBackend.criteria.push(new QFilterCriteria(criteria.fieldName, criteria.operator, FilterUtils.cleanseCriteriaValueForQQQ(criteria.values, field))); - } - } - } - - ///////////////////////////////////////// - // recursively prep subfilters as well // - ///////////////////////////////////////// - let subFilters = [] as QQueryFilter[]; - for (let j = 0; j < sourceFilter?.subFilters?.length; j++) - { - subFilters.push(prepQueryFilterForBackend(sourceFilter.subFilters[j])); - } - - filterForBackend.subFilters = subFilters; - filterForBackend.skip = pageNumber * rowsPerPage; - filterForBackend.limit = rowsPerPage; - return filterForBackend; - }; - /******************************************************************************* ** @@ -507,7 +485,7 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => totalRecords: totalRecords, columnsModel: columnsModel, columnVisibilityModel: columnVisibilityModel, - queryFilter: prepQueryFilterForBackend(queryFilter) + queryFilter: FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter) }; exportMenu = (<> @@ -877,7 +855,7 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => /******************************************************************************* ** This is the method that actually executes a query to update the data in the table. *******************************************************************************/ - const updateTable = (reason?: string) => + const updateTable = (reason?: string, clearOutCount = true) => { if (pageState != "ready") { @@ -901,7 +879,7 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => // copy the orderBys & operator into it - but we'll build its criteria one-by-one, // // as clones, as we'll need to tweak them a bit // ///////////////////////////////////////////////////////////////////////////////////// - const filterForBackend = prepQueryFilterForBackend(queryFilter); + const filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter, pageNumber, rowsPerPage); ////////////////////////////////////////// // figure out joins to use in the query // @@ -927,6 +905,12 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => console.log(`Issuing query: ${thisQueryId}`); if (tableMetaData.capabilities.has(Capability.TABLE_COUNT)) { + if(clearOutCount) + { + setTotalRecords(null); + setDistinctRecords(null); + } + let includeDistinct = isJoinMany(tableMetaData, getVisibleJoinTables()); qController.count(tableName, filterForBackend, queryJoins, includeDistinct, tableVariant).then(([count, distinctCount]) => { @@ -1428,7 +1412,7 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => { if (selectFullFilterState === "filter") { - const filterForBackend = prepQueryFilterForBackend(queryFilter); + const filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter); filterForBackend.skip = 0; filterForBackend.limit = null; return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`; @@ -1436,7 +1420,7 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => if (selectFullFilterState === "filterSubset") { - const filterForBackend = prepQueryFilterForBackend(queryFilter); + const filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter); filterForBackend.skip = 0; filterForBackend.limit = selectionSubsetSize; return `?recordsParam=filterJSON&filterJSON=${encodeURIComponent(JSON.stringify(filterForBackend))}`; @@ -1459,14 +1443,14 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => { if (selectFullFilterState === "filter") { - const filterForBackend = prepQueryFilterForBackend(queryFilter); + const filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter); filterForBackend.skip = 0; filterForBackend.limit = null; setRecordIdsForProcess(filterForBackend); } else if (selectFullFilterState === "filterSubset") { - const filterForBackend = prepQueryFilterForBackend(queryFilter); + const filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter); filterForBackend.skip = 0; filterForBackend.limit = selectionSubsetSize; setRecordIdsForProcess(filterForBackend); @@ -1924,7 +1908,7 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => *******************************************************************************/ const openColumnStatistics = async (column: GridColDef) => { - setFilterForColumnStats(prepQueryFilterForBackend(queryFilter)); + setFilterForColumnStats(FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter)); setColumnStatsFieldName(column.field); const [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, column.field); @@ -2285,7 +2269,7 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => // to avoid both this useEffect and the one below from both doing an "initial query", // // only run this one if at least 1 query has already been ran // //////////////////////////////////////////////////////////////////////////////////////// - updateTable("useEffect(pageNumber,rowsPerPage)"); + updateTable("useEffect(pageNumber,rowsPerPage)", false); } }, [pageNumber, rowsPerPage]); @@ -2320,7 +2304,16 @@ const RecordQuery = forwardRef(({table, usage, isModal}: Props, ref) => if (pageState == "ready") { - const filterForBackend = prepQueryFilterForBackend(queryFilter); + const filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, queryFilter); + + /////////////////////////////////////////////////////////////////////// + // remove the skip & limit (e.g., pagination) from this hash - // + // as we have a specific useEffect watching these, specifically // + // so we can pass the dont-clear-count flag into updateTable, // + // to try to keep the count from flashing back & forth to "Counting" // + /////////////////////////////////////////////////////////////////////// + filterForBackend.skip = null; + filterForBackend.limit = null; const newFilterHash = JSON.stringify(filterForBackend); if (filterHash != newFilterHash) @@ -2960,6 +2953,8 @@ RecordQuery.defaultProps = { usage: "queryScreen", launchProcess: null, isModal: false, + initialQueryFilter: null, + initialColumns: null, }; diff --git a/src/qqq/utils/qqq/FilterUtils.tsx b/src/qqq/utils/qqq/FilterUtils.tsx index 153fbcb..eaa8940 100644 --- a/src/qqq/utils/qqq/FilterUtils.tsx +++ b/src/qqq/utils/qqq/FilterUtils.tsx @@ -32,6 +32,7 @@ import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryF import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression"; import Box from "@mui/material/Box"; import {GridSortModel} from "@mui/x-data-grid-pro"; +import {validateCriteria} from "qqq/components/query/FilterCriteriaRow"; import TableUtils from "qqq/utils/qqq/TableUtils"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; @@ -612,6 +613,58 @@ class FilterUtils } } + + /******************************************************************************* + ** make a new query filter, based on the input one, but w/ values good for the + ** backend. such as, possible values as just ids, not objects w/ a label; + ** date-times formatted properly and in UTC + *******************************************************************************/ + public static prepQueryFilterForBackend(tableMetaData: QTableMetaData, sourceFilter: QQueryFilter, pageNumber?: number, rowsPerPage?: number): QQueryFilter + { + const filterForBackend = new QQueryFilter([], sourceFilter.orderBys, sourceFilter.subFilters, sourceFilter.booleanOperator); + for (let i = 0; i < sourceFilter?.criteria?.length; i++) + { + const criteria = sourceFilter.criteria[i]; + const {criteriaIsValid} = validateCriteria(criteria, null); + if (criteriaIsValid) + { + if (criteria.operator == QCriteriaOperator.IS_BLANK || criteria.operator == QCriteriaOperator.IS_NOT_BLANK) + { + /////////////////////////////////////////////////////////////////////////////////////////// + // do this to avoid submitting an empty-string argument for blank/not-blank operators... // + /////////////////////////////////////////////////////////////////////////////////////////// + filterForBackend.criteria.push(new QFilterCriteria(criteria.fieldName, criteria.operator, [])); + } + else + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else push a clone of the criteria - since it may get manipulated below (convertFilterPossibleValuesToIds) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const [field] = FilterUtils.getField(tableMetaData, criteria.fieldName); + filterForBackend.criteria.push(new QFilterCriteria(criteria.fieldName, criteria.operator, FilterUtils.cleanseCriteriaValueForQQQ(criteria.values, field))); + } + } + } + + ///////////////////////////////////////// + // recursively prep subfilters as well // + ///////////////////////////////////////// + let subFilters = [] as QQueryFilter[]; + for (let j = 0; j < sourceFilter?.subFilters?.length; j++) + { + subFilters.push(FilterUtils.prepQueryFilterForBackend(tableMetaData, sourceFilter.subFilters[j])); + } + + filterForBackend.subFilters = subFilters; + + if(pageNumber !== undefined && rowsPerPage !== undefined) + { + filterForBackend.skip = pageNumber * rowsPerPage; + filterForBackend.limit = rowsPerPage; + } + + return filterForBackend; + }; } export default FilterUtils;