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;