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)

This commit is contained in:
2024-04-16 11:36:46 -05:00
parent 48e3eeabd4
commit 73c907a3e1
3 changed files with 133 additions and 65 deletions

View File

@ -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
<h5>Query Filter</h5>
{
mayShowQueryPreview() &&
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={queryFilter} isEditable={false} isQueryTooComplex={queryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 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}
/>
}
<Box>

View File

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

View File

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