CE-793 - Rename as .tsx; remove functions that worked with GridFilter; add some functions for human-strings & JSON processing;

This commit is contained in:
2024-01-29 20:02:42 -06:00
parent 4d5beea607
commit b7f34dee21
2 changed files with 485 additions and 849 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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 (<>
<b>{fieldLabel}</b> {FilterUtils.operatorToHumanString(criteria)} <span style={{color: "#0062FF"}}>{valuesString}</span>&nbsp;
</>);
}
else
{
return (`${fieldLabel} ${FilterUtils.operatorToHumanString(criteria)} ${valuesString}`);
}
}
}
export default FilterUtils;