diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts
deleted file mode 100644
index 2feb61d..0000000
--- a/src/qqq/utils/qqq/FilterUtils.ts
+++ /dev/null
@@ -1,849 +0,0 @@
-/*
- * QQQ - Low-code Application Framework for Engineers.
- * Copyright (C) 2021-2022. Kingsrook, LLC
- * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
- * contact@kingsrook.com
- * https://github.com/Kingsrook/
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see .
- */
-
-import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
-import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
-import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
-import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
-import {NowExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowExpression";
-import {NowWithOffsetExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowWithOffsetExpression";
-import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
-import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
-import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
-import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
-import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression";
-import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro";
-import TableUtils from "qqq/utils/qqq/TableUtils";
-import ValueUtils from "qqq/utils/qqq/ValueUtils";
-
-const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId";
-
-/*******************************************************************************
- ** Utility class for working with QQQ Filters
- **
- *******************************************************************************/
-class FilterUtils
-{
- /*******************************************************************************
- ** Convert a grid operator to a QQQ Criteria Operator.
- *******************************************************************************/
- public static gridCriteriaOperatorToQQQ = (operator: string): QCriteriaOperator =>
- {
- switch (operator)
- {
- case "contains":
- return QCriteriaOperator.CONTAINS;
- case "notContains":
- return QCriteriaOperator.NOT_CONTAINS;
- case "startsWith":
- return QCriteriaOperator.STARTS_WITH;
- case "notStartsWith":
- return QCriteriaOperator.NOT_STARTS_WITH;
- case "endsWith":
- return QCriteriaOperator.ENDS_WITH;
- case "notEndsWith":
- return QCriteriaOperator.NOT_ENDS_WITH;
- case "is":
- case "equals":
- case "=":
- case "isTrue":
- case "isFalse":
- return QCriteriaOperator.EQUALS;
- case "isNot":
- case "!=":
- return QCriteriaOperator.NOT_EQUALS_OR_IS_NULL;
- case "after":
- case ">":
- return QCriteriaOperator.GREATER_THAN;
- case "onOrAfter":
- case ">=":
- return QCriteriaOperator.GREATER_THAN_OR_EQUALS;
- case "before":
- case "<":
- return QCriteriaOperator.LESS_THAN;
- case "onOrBefore":
- case "<=":
- return QCriteriaOperator.LESS_THAN_OR_EQUALS;
- case "isEmpty":
- return QCriteriaOperator.IS_BLANK;
- case "isNotEmpty":
- return QCriteriaOperator.IS_NOT_BLANK;
- case "isAnyOf":
- return QCriteriaOperator.IN;
- case "isNone":
- return QCriteriaOperator.NOT_IN;
- case "between":
- return QCriteriaOperator.BETWEEN;
- case "notBetween":
- return QCriteriaOperator.NOT_BETWEEN;
- default:
- return QCriteriaOperator.EQUALS;
- }
- };
-
- /*******************************************************************************
- ** Convert a qqq criteria operator to one expected by the grid.
- *******************************************************************************/
- public static qqqCriteriaOperatorToGrid = (operator: QCriteriaOperator, field: QFieldMetaData, criteriaValues: any[]): string =>
- {
- const fieldType = field.type;
- switch (operator)
- {
- case QCriteriaOperator.EQUALS:
-
- if (field.possibleValueSourceName)
- {
- return ("is");
- }
-
- switch (fieldType)
- {
- case QFieldType.INTEGER:
- case QFieldType.DECIMAL:
- return ("=");
- case QFieldType.DATE:
- case QFieldType.TIME:
- case QFieldType.DATE_TIME:
- case QFieldType.STRING:
- case QFieldType.TEXT:
- case QFieldType.HTML:
- case QFieldType.PASSWORD:
- case QFieldType.BLOB:
- return ("equals");
- case QFieldType.BOOLEAN:
- if (criteriaValues && criteriaValues[0] === true)
- {
- return ("isTrue");
- }
- else if (criteriaValues && criteriaValues[0] === false)
- {
- return ("isFalse");
- }
- return ("is");
- default:
- return ("is");
- }
- case QCriteriaOperator.NOT_EQUALS:
- case QCriteriaOperator.NOT_EQUALS_OR_IS_NULL:
-
- if (field.possibleValueSourceName)
- {
- return ("isNot");
- }
-
- switch (fieldType)
- {
- case QFieldType.INTEGER:
- case QFieldType.DECIMAL:
- return ("!=");
- case QFieldType.DATE:
- case QFieldType.TIME:
- case QFieldType.DATE_TIME:
- case QFieldType.BOOLEAN:
- case QFieldType.STRING:
- case QFieldType.TEXT:
- case QFieldType.HTML:
- case QFieldType.PASSWORD:
- case QFieldType.BLOB:
- default:
- return ("isNot");
- }
- case QCriteriaOperator.IN:
- return ("isAnyOf");
- case QCriteriaOperator.NOT_IN:
- return ("isNone");
- case QCriteriaOperator.STARTS_WITH:
- return ("startsWith");
- case QCriteriaOperator.ENDS_WITH:
- return ("endsWith");
- case QCriteriaOperator.CONTAINS:
- return ("contains");
- case QCriteriaOperator.NOT_STARTS_WITH:
- return ("notStartsWith");
- case QCriteriaOperator.NOT_ENDS_WITH:
- return ("notEndsWith");
- case QCriteriaOperator.NOT_CONTAINS:
- return ("notContains");
- case QCriteriaOperator.LESS_THAN:
- switch (fieldType)
- {
- case QFieldType.DATE:
- case QFieldType.TIME:
- case QFieldType.DATE_TIME:
- return ("before");
- default:
- return ("<");
- }
- case QCriteriaOperator.LESS_THAN_OR_EQUALS:
- switch (fieldType)
- {
- case QFieldType.DATE:
- case QFieldType.TIME:
- case QFieldType.DATE_TIME:
- return ("onOrBefore");
- default:
- return ("<=");
- }
- case QCriteriaOperator.GREATER_THAN:
- switch (fieldType)
- {
- case QFieldType.DATE:
- case QFieldType.TIME:
- case QFieldType.DATE_TIME:
- return ("after");
- default:
- return (">");
- }
- case QCriteriaOperator.GREATER_THAN_OR_EQUALS:
- switch (fieldType)
- {
- case QFieldType.DATE:
- case QFieldType.TIME:
- case QFieldType.DATE_TIME:
- return ("onOrAfter");
- default:
- return (">=");
- }
- case QCriteriaOperator.IS_BLANK:
- return ("isEmpty");
- case QCriteriaOperator.IS_NOT_BLANK:
- return ("isNotEmpty");
- case QCriteriaOperator.BETWEEN:
- return ("between");
- case QCriteriaOperator.NOT_BETWEEN:
- return ("notBetween");
- default:
- console.warn(`Unhandled criteria operator: ${operator}`);
- return ("=");
- }
- };
-
- /*******************************************************************************
- ** the values object needs handled differently based on cardinality of the operator.
- ** that is - qqq always wants a list, but the grid provides it differently per-operator.
- ** for single-values (the default), we must wrap it in an array.
- ** for non-values (e.g., blank), set it to null.
- ** for list-values, it's already in an array, so don't wrap it.
- *******************************************************************************/
- public static gridCriteriaValueToQQQ = (operator: QCriteriaOperator, value: any, gridOperatorValue: string, fieldMetaData: QFieldMetaData): any[] =>
- {
- if (gridOperatorValue === "isTrue")
- {
- return [true];
- }
- else if (gridOperatorValue === "isFalse")
- {
- return [false];
- }
-
- if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK)
- {
- return (null);
- }
- else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN || operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN)
- {
- if ((value == null || value.length < 2) && (operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN))
- {
- /////////////////////////////////////////////////////////////////////////////////////////////////
- // if we send back null, we get a 500 - bad look every time you try to set up a BETWEEN filter //
- // but array of 2 nulls? comes up sunshine. //
- /////////////////////////////////////////////////////////////////////////////////////////////////
- return ([null, null]);
- }
- return (FilterUtils.cleanseCriteriaValueForQQQ(value, fieldMetaData));
- }
-
- return (FilterUtils.cleanseCriteriaValueForQQQ([value], fieldMetaData));
- };
-
-
- /*******************************************************************************
- ** Helper method - take a list of values, which may be possible values, and
- ** either return the original list, or a new list that is just the ids of the
- ** possible values (if it was a list of possible values).
- **
- ** Or, if the values are date-times, convert them to UTC.
- *******************************************************************************/
- private static cleanseCriteriaValueForQQQ = (param: any[], fieldMetaData: QFieldMetaData): number[] | string[] =>
- {
- if (param === null || param === undefined)
- {
- return (param);
- }
-
- if (FilterUtils.gridCriteriaValueToExpression(param))
- {
- return (param);
- }
-
- let rs = [];
- for (let i = 0; i < param.length; i++)
- {
- console.log(param[i]);
- if (param[i] && param[i].id && param[i].label)
- {
- //////////////////////////////////////////////////////////////////////////////////////////
- // if the param looks like a possible value, return its id //
- // during build of new custom filter panel, this ended up causing us //
- // problems (because we wanted the full PV object in the filter model for the frontend) //
- // so, we can keep the PV as-is here, and see calls to convertFilterPossibleValuesToIds //
- // to do what this used to do. //
- //////////////////////////////////////////////////////////////////////////////////////////
- // rs.push(param[i].id);
- rs.push(param[i]);
- }
- else
- {
- if (fieldMetaData?.type == QFieldType.DATE_TIME)
- {
- try
- {
- let toPush = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(param[i]);
- rs.push(toPush);
- }
- catch (e)
- {
- console.log("Error converting date-time to UTC: ", e);
- rs.push(param[i]);
- }
- }
- else
- {
- rs.push(param[i]);
- }
- }
- }
- return (rs);
- };
-
-
- /*******************************************************************************
- ** Convert a filter field's value from the style that qqq uses, to the style that
- ** the grid uses.
- *******************************************************************************/
- public static qqqCriteriaValuesToGrid = (operator: QCriteriaOperator, values: any[], field: QFieldMetaData): any | any[] =>
- {
- const fieldType = field.type;
- if (operator === QCriteriaOperator.IS_BLANK || operator === QCriteriaOperator.IS_NOT_BLANK)
- {
- return null;
- }
- else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN || operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN)
- {
- return (values);
- }
-
- if (values && values.length > 0)
- {
- ////////////////////////////////////////////////////////////////////////////////////////////////
- // make sure dates are formatted for the grid the way it expects - not the way we pass it in. //
- ////////////////////////////////////////////////////////////////////////////////////////////////
- if (fieldType === QFieldType.DATE_TIME)
- {
- for (let i = 0; i < values.length; i++)
- {
- if (!values[i].type)
- {
- values[i] = ValueUtils.formatDateTimeValueForForm(values[i]);
- }
- }
- }
- }
-
- return (values ? values[0] : "");
- };
-
-
- /*******************************************************************************
- ** Get the default filter to use on the page - either from given filter string, query string param, or
- ** local storage, or a default (empty).
- *******************************************************************************/
- public static async determineFilterAndSortModels(qController: QController, tableMetaData: QTableMetaData, filterString: string, searchParams: URLSearchParams, filterLocalStorageKey: string, sortLocalStorageKey: string): Promise<{ filter: GridFilterModel, sort: GridSortItem[], warning: string }>
- {
- let defaultFilter = {items: []} as GridFilterModel;
- let defaultSort = [] as GridSortItem[];
- let warningParts = [] as string[];
-
- if (tableMetaData && tableMetaData.fields !== undefined)
- {
- if (filterString != null || (searchParams && searchParams.has("filter")))
- {
- try
- {
- const filterJSON = (filterString !== null) ? JSON.parse(filterString) : JSON.parse(searchParams.get("filter"));
- const qQueryFilter = filterJSON as QQueryFilter;
-
- //////////////////////////////////////////////////////////////////
- // translate from a qqq-style filter to one that the grid wants //
- //////////////////////////////////////////////////////////////////
- let id = 1;
- for (let i = 0; i < qQueryFilter?.criteria?.length; i++)
- {
- const criteria = qQueryFilter.criteria[i];
- let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
- if (field == null)
- {
- console.log("Couldn't find field for filter: " + criteria.fieldName);
- warningParts.push("Your filter contained an unrecognized field name: " + criteria.fieldName);
- continue;
- }
-
- let values = criteria.values;
- if (field.possibleValueSourceName)
- {
- //////////////////////////////////////////////////////////////////////////////////
- // possible-values in query-string are expected to only be their id values. //
- // e.g., ...values=[1]... //
- // but we need them to be possibleValue objects (w/ id & label) so the label //
- // can be shown in the filter dropdown. So, make backend call to look them up. //
- //////////////////////////////////////////////////////////////////////////////////
- if (values && values.length > 0)
- {
- values = await qController.possibleValues(fieldTable.name, null, field.name, "", values);
- }
-
- ////////////////////////////////////////////
- // log message if no values were returned //
- ////////////////////////////////////////////
- if (!values || values.length === 0)
- {
- console.warn("WARNING: No possible values were returned for [" + field.possibleValueSourceName + "] for values [" + criteria.values + "].");
- }
- }
-
- //////////////////////////////////////////////////////////////////////////
- // replace objects that look like expressions with expression instances //
- //////////////////////////////////////////////////////////////////////////
- if (values && values.length)
- {
- for (let i = 0; i < values.length; i++)
- {
- const expression = this.gridCriteriaValueToExpression(values[i]);
- if (expression)
- {
- values[i] = expression;
- }
- }
- }
-
- defaultFilter.items.push({
- columnField: criteria.fieldName,
- operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, values),
- value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, values, field),
- id: id++
- });
- }
-
- defaultFilter.linkOperator = GridLinkOperator.And;
- if (qQueryFilter.booleanOperator === "OR")
- {
- defaultFilter.linkOperator = GridLinkOperator.Or;
- }
-
- /////////////////////////////////////////////////////////////////
- // translate from qqq-style orderBy to one that the grid wants //
- /////////////////////////////////////////////////////////////////
- if (qQueryFilter.orderBys && qQueryFilter.orderBys.length > 0)
- {
- for (let i = 0; i < qQueryFilter.orderBys.length; i++)
- {
- const orderBy = qQueryFilter.orderBys[i];
- defaultSort.push({
- field: orderBy.fieldName,
- sort: orderBy.isAscending ? "asc" : "desc"
- });
- }
- }
-
- if (searchParams && searchParams.has("filter"))
- {
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- // if we're setting the filter based on a filter query-string param, then make sure we don't have a currentSavedFilter in local storage. //
- ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- localStorage.removeItem(`${CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT}.${tableMetaData.name}`);
- localStorage.setItem(filterLocalStorageKey, JSON.stringify(defaultFilter));
- localStorage.setItem(sortLocalStorageKey, JSON.stringify(defaultSort));
- }
-
- return ({filter: defaultFilter, sort: defaultSort, warning: warningParts.length > 0 ? "Warning: " + warningParts.join("; ") : ""});
- }
- catch (e)
- {
- console.warn("Error parsing filter from query string", e);
- }
- }
-
- if (localStorage.getItem(filterLocalStorageKey))
- {
- defaultFilter = JSON.parse(localStorage.getItem(filterLocalStorageKey));
- console.log(`Got default from LS: ${JSON.stringify(defaultFilter)}`);
- }
-
- if (localStorage.getItem(sortLocalStorageKey))
- {
- defaultSort = JSON.parse(localStorage.getItem(sortLocalStorageKey));
- console.log(`Got default from LS: ${JSON.stringify(defaultSort)}`);
- }
- }
-
- /////////////////////////////////////////////////////////////////////////////////
- // if any values in the items are objects, but should be expression instances, //
- // then convert & replace them. //
- /////////////////////////////////////////////////////////////////////////////////
- if (defaultFilter && defaultFilter.items && defaultFilter.items.length)
- {
- defaultFilter.items.forEach((item) =>
- {
- if (item.value && item.value.length)
- {
- for (let i = 0; i < item.value.length; i++)
- {
- const expression = this.gridCriteriaValueToExpression(item.value[i]);
- if (expression)
- {
- item.value[i] = expression;
- }
- }
- }
- else
- {
- const expression = this.gridCriteriaValueToExpression(item.value);
- if (expression)
- {
- item.value = expression;
- }
- }
- });
- }
-
- return ({filter: defaultFilter, sort: defaultSort, warning: warningParts.length > 0 ? "Warning: " + warningParts.join("; ") : ""});
- }
-
-
- /*******************************************************************************
- ** build a grid filter from a qqq filter
- *******************************************************************************/
- public static buildGridFilterFromQFilter(tableMetaData: QTableMetaData, queryFilter: QQueryFilter): GridFilterModel
- {
- const gridItems: GridFilterItem[] = [];
-
- for (let i = 0; i < queryFilter.criteria.length; i++)
- {
- const criteria = queryFilter.criteria[i];
- const [field, fieldTable] = FilterUtils.getField(tableMetaData, criteria.fieldName);
- if (field)
- {
- gridItems.push({columnField: criteria.fieldName, id: i, operatorValue: FilterUtils.qqqCriteriaOperatorToGrid(criteria.operator, field, criteria.values), value: FilterUtils.qqqCriteriaValuesToGrid(criteria.operator, criteria.values, field)});
- }
- }
-
- const gridFilter: GridFilterModel = {items: gridItems, linkOperator: queryFilter.booleanOperator == "AND" ? GridLinkOperator.And : GridLinkOperator.Or};
- return (gridFilter);
- }
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- public static getField(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
- {
- if (fieldName == null)
- {
- return ([null, null]);
- }
-
- if (fieldName.indexOf(".") > -1)
- {
- let parts = fieldName.split(".", 2);
- if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length)
- {
- for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
- {
- const joinTable = tableMetaData.exposedJoins[i].joinTable;
- if (joinTable.name == parts[0])
- {
- return ([joinTable.fields.get(parts[1]), joinTable]);
- }
- }
- }
-
- console.log(`Failed to find join field: ${fieldName}`);
- return ([null, null]);
- }
- else
- {
- return ([tableMetaData.fields.get(fieldName), tableMetaData]);
- }
- }
-
-
- /*******************************************************************************
- ** build a qqq filter from a grid and column sort model
- *******************************************************************************/
- public static buildQFilterFromGridFilter(tableMetaData: QTableMetaData, filterModel: GridFilterModel, columnSortModel: GridSortItem[], limit?: number, allowIncompleteCriteria = false): QQueryFilter
- {
- console.log("Building q filter with model:");
- console.log(filterModel);
-
- const qFilter = new QQueryFilter();
- if (columnSortModel)
- {
- columnSortModel.forEach((gridSortItem) =>
- {
- qFilter.addOrderBy(new QFilterOrderBy(gridSortItem.field, gridSortItem.sort === "asc"));
- });
- }
-
- if (limit)
- {
- console.log("Setting limit to: " + limit);
- qFilter.limit = limit;
- }
-
- if (filterModel)
- {
- let foundFilter = false;
- filterModel.items.forEach((item) =>
- {
- /////////////////////////////////////////////////////////////////////////
- // set the values for these operators that otherwise don't have values //
- /////////////////////////////////////////////////////////////////////////
- if (item.operatorValue === "isTrue")
- {
- item.value = [true];
- }
- else if (item.operatorValue === "isFalse")
- {
- item.value = [false];
- }
-
- ////////////////////////////////////////////////////////////////////////////////
- // if no value set and not 'empty' or 'not empty' operators, skip this filter //
- ////////////////////////////////////////////////////////////////////////////////
- let incomplete = false;
- if (item.operatorValue === "between" || item.operatorValue === "notBetween")
- {
- if (!item.value || !item.value.length || item.value.length < 2 || this.isUnset(item.value[0]) || this.isUnset(item.value[1]))
- {
- incomplete = true;
- }
- }
- else if ((!item.value || item.value.length == 0 || (item.value.length == 1 && this.isUnset(item.value[0]))) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty")
- {
- incomplete = true;
- }
-
- if (incomplete && !allowIncompleteCriteria)
- {
- console.log(`Discarding incomplete filter criteria: ${JSON.stringify(item)}`);
- return;
- }
-
- const fieldMetadata = tableMetaData?.fields.get(item.columnField);
- const operator = FilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue);
- const values = FilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue, fieldMetadata);
- let criteria = new QFilterCriteria(item.columnField, operator, values);
- qFilter.addCriteria(criteria);
- foundFilter = true;
- });
-
- qFilter.booleanOperator = "AND";
- if (filterModel.linkOperator == "or")
- {
- ///////////////////////////////////////////////////////////////////////////////////////////
- // by default qFilter uses AND - so only if we see linkOperator=or do we need to set it //
- ///////////////////////////////////////////////////////////////////////////////////////////
- qFilter.booleanOperator = "OR";
- }
- }
-
- return qFilter;
- };
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static isUnset(value: any)
- {
- return value === "" || value === undefined;
- }
-
- /*******************************************************************************
- **
- *******************************************************************************/
- private static gridCriteriaValueToExpression(value: any)
- {
- if (value && value.length)
- {
- value = value[0];
- }
-
- if (value && value.type)
- {
- if (value.type == "NowWithOffset")
- {
- return (new NowWithOffsetExpression(value));
- }
- else if (value.type == "Now")
- {
- return (new NowExpression(value));
- }
- else if (value.type == "ThisOrLastPeriod")
- {
- return (new ThisOrLastPeriodExpression(value));
- }
- }
-
- return (null);
- }
-
-
- /*******************************************************************************
- ** edit the input filter object, replacing any values which have {id,label} attributes
- ** to instead just have the id part.
- *******************************************************************************/
- public static convertFilterPossibleValuesToIds(inputFilter: QQueryFilter): QQueryFilter
- {
- const filter = Object.assign({}, inputFilter);
-
- if (filter.criteria)
- {
- for (let i = 0; i < filter.criteria.length; i++)
- {
- const criteria = filter.criteria[i];
- if (criteria.values)
- {
- for (let j = 0; j < criteria.values.length; j++)
- {
- let value = criteria.values[j];
- if (value && value.id && value.label)
- {
- criteria.values[j] = value.id;
- }
- }
- }
- }
- }
-
- return (filter);
- }
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- public static canFilterWorkAsBasic(tableMetaData: QTableMetaData, filter: QQueryFilter): { canFilterWorkAsBasic: boolean; reasonsWhyItCannot?: string[] }
- {
- const reasonsWhyItCannot: string[] = [];
-
- if(filter == null)
- {
- return ({canFilterWorkAsBasic: true});
- }
-
- if(filter.booleanOperator == "OR")
- {
- reasonsWhyItCannot.push("Filter uses the 'OR' operator.")
- }
-
- if(filter.criteria)
- {
- const usedFields: {[name: string]: boolean} = {};
- const warnedFields: {[name: string]: boolean} = {};
- for (let i = 0; i < filter.criteria.length; i++)
- {
- const criteriaName = filter.criteria[i].fieldName;
- if(!criteriaName)
- {
- continue;
- }
-
- if(usedFields[criteriaName])
- {
- if(!warnedFields[criteriaName])
- {
- const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, criteriaName);
- let fieldLabel = field.label;
- if(tableForField.name != tableMetaData.name)
- {
- let fieldLabel = `${tableForField.label}: ${field.label}`;
- }
- reasonsWhyItCannot.push(`Filter contains more than 1 condition for the field: ${fieldLabel}`);
- warnedFields[criteriaName] = true;
- }
- }
- usedFields[criteriaName] = true;
- }
- }
-
- if(reasonsWhyItCannot.length == 0)
- {
- return ({canFilterWorkAsBasic: true});
- }
- else
- {
- return ({canFilterWorkAsBasic: false, reasonsWhyItCannot: reasonsWhyItCannot});
- }
- }
-
- /*******************************************************************************
- ** get the values associated with a criteria as a string, e.g., for showing
- ** in a tooltip.
- *******************************************************************************/
- public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3): string
- {
- let valuesString = "";
- if (criteria.values && criteria.values.length && fieldMetaData.type !== QFieldType.BOOLEAN)
- {
- let labels = [] as string[];
-
- let maxLoops = criteria.values.length;
- if (maxLoops > (maxValuesToShow + 2))
- {
- maxLoops = maxValuesToShow;
- }
-
- for (let i = 0; i < maxLoops; i++)
- {
- if (criteria.values[i] && criteria.values[i].label)
- {
- labels.push(criteria.values[i].label);
- }
- else
- {
- labels.push(criteria.values[i]);
- }
- }
-
- if (maxLoops < criteria.values.length)
- {
- labels.push(" and " + (criteria.values.length - maxLoops) + " other values.");
- }
-
- valuesString = (labels.join(", "));
- }
- return valuesString;
- }
-
-}
-
-export default FilterUtils;
diff --git a/src/qqq/utils/qqq/FilterUtils.tsx b/src/qqq/utils/qqq/FilterUtils.tsx
new file mode 100644
index 0000000..ae30d48
--- /dev/null
+++ b/src/qqq/utils/qqq/FilterUtils.tsx
@@ -0,0 +1,485 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import {QController} from "@kingsrook/qqq-frontend-core/lib/controllers/QController";
+import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
+import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
+import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
+import {NowExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowExpression";
+import {NowWithOffsetExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/NowWithOffsetExpression";
+import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
+import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
+import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
+import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
+import {ThisOrLastPeriodExpression} from "@kingsrook/qqq-frontend-core/lib/model/query/ThisOrLastPeriodExpression";
+import {GridSortModel} from "@mui/x-data-grid-pro";
+import TableUtils from "qqq/utils/qqq/TableUtils";
+import ValueUtils from "qqq/utils/qqq/ValueUtils";
+
+/*******************************************************************************
+ ** Utility class for working with QQQ Filters
+ **
+ *******************************************************************************/
+class FilterUtils
+{
+
+ /*******************************************************************************
+ ** Helper method - take a list of values, which may be possible values, and
+ ** either return the original list, or a new list that is just the ids of the
+ ** possible values (if it was a list of possible values).
+ **
+ ** Or, if the values are date-times, convert them to UTC.
+ *******************************************************************************/
+ public static cleanseCriteriaValueForQQQ = (param: any[], fieldMetaData: QFieldMetaData): number[] | string[] =>
+ {
+ if (param === null || param === undefined)
+ {
+ return (param);
+ }
+
+ if (FilterUtils.gridCriteriaValueToExpression(param))
+ {
+ return (param);
+ }
+
+ let rs = [];
+ for (let i = 0; i < param.length; i++)
+ {
+ console.log(param[i]);
+ if (param[i] && param[i].id && param[i].label)
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // if the param looks like a possible value, return its id //
+ // during build of new custom filter panel, this ended up causing us //
+ // problems (because we wanted the full PV object in the filter model for the frontend) //
+ // so, we can keep the PV as-is here, and see calls to convertFilterPossibleValuesToIds //
+ // to do what this used to do. //
+ //////////////////////////////////////////////////////////////////////////////////////////
+ // rs.push(param[i].id);
+ rs.push(param[i]);
+ }
+ else
+ {
+ if (fieldMetaData?.type == QFieldType.DATE_TIME)
+ {
+ try
+ {
+ let toPush = ValueUtils.frontendLocalZoneDateTimeStringToUTCStringForBackend(param[i]);
+ rs.push(toPush);
+ }
+ catch (e)
+ {
+ console.log("Error converting date-time to UTC: ", e);
+ rs.push(param[i]);
+ }
+ }
+ else
+ {
+ rs.push(param[i]);
+ }
+ }
+ }
+ return (rs);
+ };
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static async cleanupValuesInFilerFromQueryString(qController: QController, tableMetaData: QTableMetaData, queryFilter: QQueryFilter)
+ {
+ for (let i = 0; i < queryFilter?.criteria?.length; i++)
+ {
+ const criteria = queryFilter.criteria[i];
+ let [field, fieldTable] = TableUtils.getFieldAndTable(tableMetaData, criteria.fieldName);
+
+ let values = criteria.values;
+ if (field.possibleValueSourceName)
+ {
+ //////////////////////////////////////////////////////////////////////////////////
+ // possible-values in query-string are expected to only be their id values. //
+ // e.g., ...values=[1]... //
+ // but we need them to be possibleValue objects (w/ id & label) so the label //
+ // can be shown in the filter dropdown. So, make backend call to look them up. //
+ //////////////////////////////////////////////////////////////////////////////////
+ if (values && values.length > 0)
+ {
+ values = await qController.possibleValues(fieldTable.name, null, field.name, "", values);
+ }
+
+ ////////////////////////////////////////////
+ // log message if no values were returned //
+ ////////////////////////////////////////////
+ if (!values || values.length === 0)
+ {
+ console.warn("WARNING: No possible values were returned for [" + field.possibleValueSourceName + "] for values [" + criteria.values + "].");
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////
+ // replace objects that look like expressions with expression instances //
+ //////////////////////////////////////////////////////////////////////////
+ if (values && values.length)
+ {
+ for (let i = 0; i < values.length; i++)
+ {
+ const expression = this.gridCriteriaValueToExpression(values[i]);
+ if (expression)
+ {
+ values[i] = expression;
+ }
+ }
+ }
+
+ criteria.values = values;
+ }
+ }
+
+
+ /*******************************************************************************
+ ** given a table, and a field name (which may be prefixed with an exposed-join
+ ** table name (from the table) - return the corresponding field-meta-data, and
+ ** the table that the field is from (e.g., may be a join table!)
+ *******************************************************************************/
+ public static getField(tableMetaData: QTableMetaData, fieldName: string): [QFieldMetaData, QTableMetaData]
+ {
+ if (fieldName == null)
+ {
+ return ([null, null]);
+ }
+
+ if (fieldName.indexOf(".") > -1)
+ {
+ let parts = fieldName.split(".", 2);
+ if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length)
+ {
+ for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
+ {
+ const joinTable = tableMetaData.exposedJoins[i].joinTable;
+ if (joinTable.name == parts[0])
+ {
+ return ([joinTable.fields.get(parts[1]), joinTable]);
+ }
+ }
+ }
+
+ console.log(`Failed to find join field: ${fieldName}`);
+ return ([null, null]);
+ }
+ else
+ {
+ return ([tableMetaData.fields.get(fieldName), tableMetaData]);
+ }
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static gridCriteriaValueToExpression(value: any)
+ {
+ if (value && value.length)
+ {
+ value = value[0];
+ }
+
+ if (value && value.type)
+ {
+ if (value.type == "NowWithOffset")
+ {
+ return (new NowWithOffsetExpression(value));
+ }
+ else if (value.type == "Now")
+ {
+ return (new NowExpression(value));
+ }
+ else if (value.type == "ThisOrLastPeriod")
+ {
+ return (new ThisOrLastPeriodExpression(value));
+ }
+ }
+
+ return (null);
+ }
+
+
+ /*******************************************************************************
+ ** edit the input filter object, replacing any values which have {id,label} attributes
+ ** to instead just have the id part.
+ *******************************************************************************/
+ public static convertFilterPossibleValuesToIds(inputFilter: QQueryFilter): QQueryFilter
+ {
+ const filter = Object.assign({}, inputFilter);
+
+ if (filter.criteria)
+ {
+ for (let i = 0; i < filter.criteria.length; i++)
+ {
+ const criteria = filter.criteria[i];
+ if (criteria.values)
+ {
+ for (let j = 0; j < criteria.values.length; j++)
+ {
+ let value = criteria.values[j];
+ if (value && value.id && value.label)
+ {
+ criteria.values[j] = value.id;
+ }
+ }
+ }
+ }
+ }
+
+ return (filter);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static canFilterWorkAsBasic(tableMetaData: QTableMetaData, filter: QQueryFilter): { canFilterWorkAsBasic: boolean; reasonsWhyItCannot?: string[] }
+ {
+ const reasonsWhyItCannot: string[] = [];
+
+ if(filter == null)
+ {
+ return ({canFilterWorkAsBasic: true});
+ }
+
+ if(filter.booleanOperator == "OR")
+ {
+ reasonsWhyItCannot.push("Filter uses the 'OR' operator.")
+ }
+
+ if(filter.criteria)
+ {
+ const usedFields: {[name: string]: boolean} = {};
+ const warnedFields: {[name: string]: boolean} = {};
+ for (let i = 0; i < filter.criteria.length; i++)
+ {
+ const criteriaName = filter.criteria[i].fieldName;
+ if(!criteriaName)
+ {
+ continue;
+ }
+
+ if(usedFields[criteriaName])
+ {
+ if(!warnedFields[criteriaName])
+ {
+ const [field, tableForField] = TableUtils.getFieldAndTable(tableMetaData, criteriaName);
+ let fieldLabel = field.label;
+ if(tableForField.name != tableMetaData.name)
+ {
+ let fieldLabel = `${tableForField.label}: ${field.label}`;
+ }
+ reasonsWhyItCannot.push(`Filter contains more than 1 condition for the field: ${fieldLabel}`);
+ warnedFields[criteriaName] = true;
+ }
+ }
+ usedFields[criteriaName] = true;
+ }
+ }
+
+ if(reasonsWhyItCannot.length == 0)
+ {
+ return ({canFilterWorkAsBasic: true});
+ }
+ else
+ {
+ return ({canFilterWorkAsBasic: false, reasonsWhyItCannot: reasonsWhyItCannot});
+ }
+ }
+
+ /*******************************************************************************
+ ** get the values associated with a criteria as a string, e.g., for showing
+ ** in a tooltip.
+ *******************************************************************************/
+ public static getValuesString(fieldMetaData: QFieldMetaData, criteria: QFilterCriteria, maxValuesToShow: number = 3): string
+ {
+ let valuesString = "";
+ if (criteria.values && criteria.values.length && fieldMetaData.type !== QFieldType.BOOLEAN)
+ {
+ let labels = [] as string[];
+
+ let maxLoops = criteria.values.length;
+ if (maxLoops > (maxValuesToShow + 2))
+ {
+ maxLoops = maxValuesToShow;
+ }
+
+ for (let i = 0; i < maxLoops; i++)
+ {
+ if (criteria.values[i] && criteria.values[i].label)
+ {
+ labels.push(criteria.values[i].label);
+ }
+ else
+ {
+ labels.push(criteria.values[i]);
+ }
+ }
+
+ if (maxLoops < criteria.values.length)
+ {
+ labels.push(" and " + (criteria.values.length - maxLoops) + " other values.");
+ }
+
+ valuesString = (labels.join(", "));
+ }
+ return valuesString;
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static buildQFilterFromJSONObject(object: any): QQueryFilter
+ {
+ const queryFilter = new QQueryFilter();
+
+ queryFilter.criteria = [];
+ for (let i = 0; i < object.criteria?.length; i++)
+ {
+ const criteriaObject = object.criteria[i];
+ queryFilter.criteria.push(new QFilterCriteria(criteriaObject.fieldName, criteriaObject.operator, criteriaObject.values));
+ }
+
+ queryFilter.orderBys = [];
+ for (let i = 0; i < object.orderBys?.length; i++)
+ {
+ const orderByObject = object.orderBys[i];
+ queryFilter.orderBys.push(new QFilterOrderBy(orderByObject.fieldName, orderByObject.isAscending));
+ }
+
+ queryFilter.booleanOperator = object.booleanOperator;
+ queryFilter.skip = object.skip;
+ queryFilter.limit = object.limit;
+
+ return (queryFilter);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static getGridSortFromQueryFilter(queryFilter: QQueryFilter): GridSortModel
+ {
+ const gridSortModel: GridSortModel = [];
+ for (let i = 0; i < queryFilter?.orderBys?.length; i++)
+ {
+ const orderBy = queryFilter.orderBys[i];
+ gridSortModel.push({field: orderBy.fieldName, sort: orderBy.isAscending ? "asc" : "desc"})
+ }
+ return (gridSortModel);
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static operatorToHumanString(criteria: QFilterCriteria): string
+ {
+ if(criteria == null || criteria.operator == null)
+ {
+ return (null);
+ }
+
+ try
+ {
+ switch(criteria.operator)
+ {
+ case QCriteriaOperator.EQUALS:
+ return ("equals");
+ case QCriteriaOperator.NOT_EQUALS:
+ case QCriteriaOperator.NOT_EQUALS_OR_IS_NULL:
+ return ("does not equal");
+ case QCriteriaOperator.IN:
+ return ("is any of");
+ case QCriteriaOperator.NOT_IN:
+ return ("is none of");
+ case QCriteriaOperator.STARTS_WITH:
+ return ("starts with");
+ case QCriteriaOperator.ENDS_WITH:
+ return ("ends with");
+ case QCriteriaOperator.CONTAINS:
+ return ("contains");
+ case QCriteriaOperator.NOT_STARTS_WITH:
+ return ("does not start with");
+ case QCriteriaOperator.NOT_ENDS_WITH:
+ return ("does not end with");
+ case QCriteriaOperator.NOT_CONTAINS:
+ return ("does not contain");
+ case QCriteriaOperator.LESS_THAN:
+ return ("less than");
+ case QCriteriaOperator.LESS_THAN_OR_EQUALS:
+ return ("less than or equals");
+ case QCriteriaOperator.GREATER_THAN:
+ return ("greater than or equals");
+ case QCriteriaOperator.GREATER_THAN_OR_EQUALS:
+ return ("greater than or equals");
+ case QCriteriaOperator.IS_BLANK:
+ return ("is blank");
+ case QCriteriaOperator.IS_NOT_BLANK:
+ return ("is not blank");
+ case QCriteriaOperator.BETWEEN:
+ return ("is between");
+ case QCriteriaOperator.NOT_BETWEEN:
+ return ("is not between");
+ }
+ }
+ catch(e)
+ {
+ console.log(`Error getting operator human string for ${JSON.stringify(criteria)}: ${e}`);
+ return criteria?.operator
+ }
+ }
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static criteriaToHumanString(table: QTableMetaData, criteria: QFilterCriteria, styled = false): string | JSX.Element
+ {
+ if(criteria == null)
+ {
+ return (null);
+ }
+
+ const [field, fieldTable] = TableUtils.getFieldAndTable(table, criteria.fieldName);
+ const fieldLabel = TableUtils.getFieldFullLabel(table, criteria.fieldName);
+ const valuesString = FilterUtils.getValuesString(field, criteria);
+
+ if(styled)
+ {
+ return (<>
+ {fieldLabel} {FilterUtils.operatorToHumanString(criteria)} {valuesString}
+ >);
+ }
+ else
+ {
+ return (`${fieldLabel} ${FilterUtils.operatorToHumanString(criteria)} ${valuesString}`);
+ }
+ }
+
+}
+
+export default FilterUtils;