From 000eb2426251b4446c465146ea736cbde9be619b Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 1 Jun 2023 10:59:39 -0500 Subject: [PATCH] Checkpoint - new custom filter panel --- src/App.tsx | 16 + .../components/query/CustomFilterPanel.tsx | 167 ++++++ .../components/query/FilterCriteriaRow.tsx | 507 ++++++++++++++++++ .../query/FilterCriteriaRowValues.tsx | 129 +++++ src/qqq/pages/records/FilterPoc.tsx | 75 +++ src/qqq/pages/records/IntersectionMatrix.tsx | 123 +++++ src/qqq/pages/records/query/RecordQuery.tsx | 158 ++++-- src/qqq/styles/qqq-override-styles.css | 70 ++- src/qqq/utils/qqq/FilterUtils.ts | 61 ++- 9 files changed, 1258 insertions(+), 48 deletions(-) create mode 100644 src/qqq/components/query/CustomFilterPanel.tsx create mode 100644 src/qqq/components/query/FilterCriteriaRow.tsx create mode 100644 src/qqq/components/query/FilterCriteriaRowValues.tsx create mode 100644 src/qqq/pages/records/FilterPoc.tsx create mode 100644 src/qqq/pages/records/IntersectionMatrix.tsx diff --git a/src/App.tsx b/src/App.tsx index 8b5c118..129fc8a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,8 @@ import ReportRun from "qqq/pages/processes/ReportRun"; import EntityCreate from "qqq/pages/records/create/RecordCreate"; import TableDeveloperView from "qqq/pages/records/developer/TableDeveloperView"; import EntityEdit from "qqq/pages/records/edit/RecordEdit"; +import FilterPoc from "qqq/pages/records/FilterPoc"; +import IntersectionMatrix from "qqq/pages/records/IntersectionMatrix"; import RecordQuery from "qqq/pages/records/query/RecordQuery"; import RecordDeveloperView from "qqq/pages/records/view/RecordDeveloperView"; import RecordView from "qqq/pages/records/view/RecordView"; @@ -456,6 +458,20 @@ export default function App() }); } + appRoutesList.push({ + name: "Intersection Matrix", + key: "intersection-matrix", + route: "/intersection-matrix", + component: , + }); + + appRoutesList.push({ + name: "Filer POC", + key: "filter-poc", + route: "/filter-poc", + component: , + }); + const newSideNavRoutes = []; // @ts-ignore newSideNavRoutes.unshift(profileRoutes); diff --git a/src/qqq/components/query/CustomFilterPanel.tsx b/src/qqq/components/query/CustomFilterPanel.tsx new file mode 100644 index 0000000..aa768e2 --- /dev/null +++ b/src/qqq/components/query/CustomFilterPanel.tsx @@ -0,0 +1,167 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; +import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; +import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button/Button"; +import Icon from "@mui/material/Icon/Icon"; +import {GridFilterPanelProps, GridSlotsComponentsProps} from "@mui/x-data-grid-pro"; +import React, {forwardRef, useReducer} from "react"; +import {FilterCriteriaRow} from "qqq/components/query/FilterCriteriaRow"; + + + + +declare module "@mui/x-data-grid" +{ + interface FilterPanelPropsOverrides + { + tableMetaData: QTableMetaData; + queryFilter: QQueryFilter; + updateFilter: (newFilter: QQueryFilter) => void; + } +} + + +export class QFilterCriteriaWithId extends QFilterCriteria +{ + id: number +} + + +let debounceTimeout: string | number | NodeJS.Timeout; +let criteriaId = (new Date().getTime()) + 1000; + +export const CustomFilterPanel = forwardRef( + function MyCustomFilterPanel(props: GridSlotsComponentsProps["filterPanel"], ref) + { + const [, forceUpdate] = useReducer((x) => x + 1, 0); + + const queryFilter = props.queryFilter; + // console.log(`CustomFilterPanel: filter: ${JSON.stringify(queryFilter)}`); + + function focusLastField() + { + setTimeout(() => + { + console.log(`Try to focus ${criteriaId - 1}`); + try + { + document.getElementById(`field-${criteriaId - 1}`).focus(); + } + catch (e) + { + console.log("Error trying to focus field ...", e); + } + }); + } + + const addCriteria = () => + { + const qFilterCriteriaWithId = new QFilterCriteriaWithId(null, QCriteriaOperator.EQUALS, [""]); + qFilterCriteriaWithId.id = criteriaId++; + console.log(`adding criteria id ${qFilterCriteriaWithId.id}`); + queryFilter.criteria.push(qFilterCriteriaWithId); + props.updateFilter(queryFilter); + forceUpdate(); + + focusLastField(); + }; + + if (!queryFilter.criteria) + { + queryFilter.criteria = []; + addCriteria(); + } + + if (queryFilter.criteria.length == 0) + { + addCriteria(); + } + + if(queryFilter.criteria.length == 1 && !queryFilter.criteria[0].fieldName) + { + focusLastField(); + } + + let booleanOperator: "AND" | "OR" | null = null; + if (queryFilter.criteria.length > 1) + { + booleanOperator = queryFilter.booleanOperator; + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // needDebounce param - things like typing in a text field DO need debounce, but changing an operator doesn't // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + const updateCriteria = (newCriteria: QFilterCriteria, index: number, needDebounce = false) => + { + queryFilter.criteria[index] = newCriteria; + + clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => props.updateFilter(queryFilter), needDebounce ? 500 : 1); + + forceUpdate(); + }; + + const updateBooleanOperator = (newValue: string) => + { + queryFilter.booleanOperator = newValue; + props.updateFilter(queryFilter); + forceUpdate(); + }; + + const removeCriteria = (index: number) => + { + queryFilter.criteria.splice(index, 1); + props.updateFilter(queryFilter); + forceUpdate(); + }; + + return ( + + { + queryFilter.criteria.map((criteria: QFilterCriteriaWithId, index: number) => + ( + + updateCriteria(newCriteria, index, needDebounce)} + removeCriteria={() => removeCriteria(index)} + updateBooleanOperator={(newValue) => updateBooleanOperator(newValue)} + /> + {/*JSON.stringify(criteria)*/} + + )) + } + + + + + ); + } +); diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx new file mode 100644 index 0000000..9aa95b2 --- /dev/null +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -0,0 +1,507 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator"; +import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria"; +import Autocomplete, {AutocompleteRenderOptionState} from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import FormControl from "@mui/material/FormControl/FormControl"; +import Icon from "@mui/material/Icon/Icon"; +import IconButton from "@mui/material/IconButton"; +import MenuItem from "@mui/material/MenuItem"; +import Select, {SelectChangeEvent} from "@mui/material/Select/Select"; +import TextField from "@mui/material/TextField"; +import Tooltip from "@mui/material/Tooltip"; +import React, {ReactNode, SyntheticEvent, useState} from "react"; +import FilterCriteriaRowValues from "qqq/components/query/FilterCriteriaRowValues"; +import FilterUtils from "qqq/utils/qqq/FilterUtils"; + + +export enum ValueMode +{ + NONE = "NONE", + SINGLE = "SINGLE", + DOUBLE = "DOUBLE", + MULTI = "MULTI", + SINGLE_DATE = "SINGLE_DATE", + SINGLE_DATE_TIME = "SINGLE_DATE_TIME", + PVS_SINGLE = "PVS_SINGLE", + PVS_MULTI = "PVS_MULTI", +} + +export interface OperatorOption +{ + label: string; + value: QCriteriaOperator; + implicitValues?: [any]; + valueMode: ValueMode; +} + + +interface FilterCriteriaRowProps +{ + id: number; + index: number; + tableMetaData: QTableMetaData; + criteria: QFilterCriteria; + booleanOperator: "AND" | "OR" | null; + updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void; + removeCriteria: () => void; + updateBooleanOperator: (newValue: string) => void; +} + +FilterCriteriaRow.defaultProps = {}; + +function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: any[], isJoinTable: boolean) +{ + const sortedFields = [...tableMetaData.fields.values()].sort((a, b) => a.label.localeCompare(b.label)); + for (let i = 0; i < sortedFields.length; i++) + { + const fieldName = isJoinTable ? `${tableMetaData.name}.${sortedFields[i].name}` : sortedFields[i].name; + fieldOptions.push({field: sortedFields[i], table: tableMetaData, fieldName: fieldName}); + } +} + +export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element +{ + // console.log(`FilterCriteriaRow: criteria: ${JSON.stringify(criteria)}`); + const [operatorSelectedValue, setOperatorSelectedValue] = useState(null as OperatorOption); + const [operatorInputValue, setOperatorInputValue] = useState("") + + /////////////////////////////////////////////////////////////// + // set up the array of options for the fields Autocomplete // + // also, a groupBy function, in case there are exposed joins // + /////////////////////////////////////////////////////////////// + const fieldOptions: any[] = []; + makeFieldOptionsForTable(tableMetaData, fieldOptions, false); + let fieldsGroupBy = null; + + if (tableMetaData.exposedJoins && tableMetaData.exposedJoins.length > 0) + { + fieldsGroupBy = (option: any) => `${option.table.label} Fields`; + + for (let i = 0; i < tableMetaData.exposedJoins.length; i++) + { + const exposedJoin = tableMetaData.exposedJoins[i]; + makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true); + } + } + + //////////////////////////////////////////////////////////// + // set up array of options for operator dropdown // + // only call the function to do it if we have a field set // + //////////////////////////////////////////////////////////// + let operatorOptions: OperatorOption[] = []; + + function setOperatorOptions(fieldName: string) + { + const [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName); + operatorOptions = []; + if (field && fieldTable) + { + ////////////////////////////////////////////////////// + // setup array of options for operator Autocomplete // + ////////////////////////////////////////////////////// + if (field.possibleValueSourceName) + { + operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE}); + operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.PVS_SINGLE}); + operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.PVS_MULTI}); + operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.PVS_MULTI}); + } + else + { + switch (field.type) + { + case QFieldType.DECIMAL: + case QFieldType.INTEGER: + operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "not equals", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "greater than or equals", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "less than", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "less than or equals", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE}); + operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE}); + operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI}); + operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI}); + break; + case QFieldType.DATE: + operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + //? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN}); + //? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN}); + //? operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN}); + //? operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN}); + break; + case QFieldType.DATE_TIME: + operatorOptions.push({label: "is", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is not", value: QCriteriaOperator.NOT_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + //? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN}); + //? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN}); + break; + case QFieldType.BOOLEAN: + operatorOptions.push({label: "is yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]}); + operatorOptions.push({label: "is no", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [false]}); + operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + /* + ? is yes or empty (is not no) + ? is no or empty (is not yes) + */ + break; + case QFieldType.BLOB: + operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + break; + default: + operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "contains ", value: QCriteriaOperator.CONTAINS, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "does not contain", value: QCriteriaOperator.NOT_CONTAINS, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "starts with", value: QCriteriaOperator.STARTS_WITH, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "does not start with", value: QCriteriaOperator.NOT_STARTS_WITH, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "ends with", value: QCriteriaOperator.ENDS_WITH, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "does not end with", value: QCriteriaOperator.NOT_ENDS_WITH, valueMode: ValueMode.SINGLE}); + operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE}); + operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI}); + operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI}); + } + } + } + } + + //////////////////////////////////////////////////////////////// + // make currently selected values appear in the Autocompletes // + //////////////////////////////////////////////////////////////// + let defaultFieldValue; + let field = null; + let fieldTable = null; + if(criteria && criteria.fieldName) + { + [field, fieldTable] = FilterUtils.getField(tableMetaData, criteria.fieldName); + if (field && fieldTable) + { + if (fieldTable.name == tableMetaData.name) + { + // @ts-ignore + defaultFieldValue = {field: field, table: tableMetaData, fieldName: criteria.fieldName}; + } + else + { + defaultFieldValue = {field: field, table: fieldTable, fieldName: criteria.fieldName}; + } + + setOperatorOptions(criteria.fieldName); + + + let newOperatorSelectedValue = operatorOptions.filter(option => + { + if(option.value == criteria.operator) + { + if(option.implicitValues) + { + return (JSON.stringify(option.implicitValues) == JSON.stringify(criteria.values)); + } + else + { + return (true); + } + } + return (false); + })[0]; + if(newOperatorSelectedValue?.label !== operatorSelectedValue?.label) + { + setOperatorSelectedValue(newOperatorSelectedValue); + setOperatorInputValue(newOperatorSelectedValue?.label); + } + } + } + + ////////////////////////////////////////////// + // event handler for booleanOperator Select // + ////////////////////////////////////////////// + const handleBooleanOperatorChange = (event: SelectChangeEvent<"AND" | "OR">, child: ReactNode) => + { + updateBooleanOperator(event.target.value); + }; + + ////////////////////////////////////////// + // event handler for field Autocomplete // + ////////////////////////////////////////// + const handleFieldChange = (event: any, newValue: any, reason: string) => + { + criteria.fieldName = newValue ? newValue.fieldName : null; + updateCriteria(criteria, false); + + setOperatorOptions(criteria.fieldName) + if(operatorOptions.length) + { + setOperatorSelectedValue(operatorOptions[0]); + setOperatorInputValue(operatorOptions[0].label); + } + else + { + setOperatorSelectedValue(null); + setOperatorInputValue(""); + } + }; + + ///////////////////////////////////////////// + // event handler for operator Autocomplete // + ///////////////////////////////////////////// + const handleOperatorChange = (event: any, newValue: any, reason: string) => + { + criteria.operator = newValue ? newValue.value : null; + + if(newValue) + { + setOperatorSelectedValue(newValue); + setOperatorInputValue(newValue.label); + + if(newValue.implicitValues) + { + criteria.values = newValue.implicitValues; + } + } + else + { + setOperatorSelectedValue(null); + setOperatorInputValue(""); + } + + updateCriteria(criteria, false); + }; + + //////////////////////////////////////// + // event handler for value text field // + //////////////////////////////////////// + const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) => + { + // @ts-ignore + const value = newValue ? newValue : event.target.value + + if(!criteria.values) + { + criteria.values = []; + } + + if(valueIndex == "all") + { + criteria.values= value; + } + else + { + criteria.values[valueIndex] = value; + } + + updateCriteria(criteria, true); + }; + + function isFieldOptionEqual(option: any, value: any) + { + return option.fieldName === value.fieldName; + } + + function getFieldOptionLabel(option: any) + { + ///////////////////////////////////////////////////////////////////////////////////////// + // note - we're using renderFieldOption below for the actual select-box options, which // + // are always jut field label (as they are under groupings that show their table name) // + ///////////////////////////////////////////////////////////////////////////////////////// + if(option && option.field && option.table) + { + if(option.table.name == tableMetaData.name) + { + return (option.field.label); + } + else + { + return (option.table.label + ": " + option.field.label); + } + } + + return (""); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + // for options, we only want the field label (contrast with what we show in the input box, // + // which comes out of getFieldOptionLabel, which is the table-label prefix for join fields) // + ////////////////////////////////////////////////////////////////////////////////////////////// + function renderFieldOption(props: React.HTMLAttributes, option: any, state: AutocompleteRenderOptionState): ReactNode + { + let label = "" + if(option && option.field) + { + label = (option.field.label); + } + + return (
  • {label}
  • ); + } + + function isOperatorOptionEqual(option: OperatorOption, value: OperatorOption) + { + return (option?.value == value?.value && JSON.stringify(option?.implicitValues) == JSON.stringify(value?.implicitValues)); + } + + let criteriaIsValid = true; + let criteriaStatusTooltip = "This condition is fully defined and is part of your filter."; + + function isNotSet(value: any) + { + return (value === null || value == undefined || String(value).trim() === ""); + } + + if(!criteria.fieldName) + { + criteriaIsValid = false; + criteriaStatusTooltip = "You must select a field to begin to define this condition."; + } + else if(!criteria.operator) + { + criteriaIsValid = false; + criteriaStatusTooltip = "You must select an operator to continue to define this condition."; + } + else + { + if(operatorSelectedValue) + { + if (operatorSelectedValue.valueMode == ValueMode.NONE || operatorSelectedValue.implicitValues) + { + ////////////////////////////////// + // don't need to look at values // + ////////////////////////////////// + } + else if(operatorSelectedValue.valueMode == ValueMode.DOUBLE) + { + if(criteria.values.length < 2) + { + criteriaIsValid = false; + criteriaStatusTooltip = "You must enter two values to complete the definition of this condition."; + } + } + else if(operatorSelectedValue.valueMode == ValueMode.MULTI || operatorSelectedValue.valueMode == ValueMode.PVS_MULTI) + { + if(criteria.values.length < 1 || isNotSet(criteria.values[0])) + { + criteriaIsValid = false; + criteriaStatusTooltip = "You must enter one or more values complete the definition of this condition."; + } + } + else + { + if(isNotSet(criteria.values[0])) + { + criteriaIsValid = false; + criteriaStatusTooltip = "You must enter a value to complete the definition of this condition."; + } + } + } + } + + return ( + + + + close + + + + {booleanOperator && index > 0 ? + + + + : } + + + ()} + // @ts-ignore + defaultValue={defaultFieldValue} + options={fieldOptions} + onChange={handleFieldChange} + isOptionEqualToValue={(option, value) => isFieldOptionEqual(option, value)} + groupBy={fieldsGroupBy} + getOptionLabel={(option) => getFieldOptionLabel(option)} + renderOption={(props, option, state) => renderFieldOption(props, option, state)} + autoSelect={true} + autoHighlight={true} + /> + + + + ()} + options={operatorOptions} + value={operatorSelectedValue as any} + inputValue={operatorInputValue} + onChange={handleOperatorChange} + onInputChange={(e, value) => setOperatorInputValue(value)} + isOptionEqualToValue={(option, value) => isOperatorOptionEqual(option, value)} + getOptionLabel={(option: any) => option.label} + autoSelect={true} + autoHighlight={true} + /*disabled={criteria.fieldName == null}*/ + /> + + + + handleValueChange(event, valueIndex, newValue)} + /> + + + + { + criteriaIsValid + ? check + : pending + } + + + + ); +} diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx new file mode 100644 index 0000000..e84c285 --- /dev/null +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -0,0 +1,129 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +import {Chip} from "@mui/material"; +import Autocomplete from "@mui/material/Autocomplete"; +import Box from "@mui/material/Box"; +import TextField from "@mui/material/TextField"; +import React, {SyntheticEvent} from "react"; +import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; +import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow"; + +interface Props +{ + operatorOption: OperatorOption; + criteria: QFilterCriteriaWithId; + fieldType?: QFieldType; + valueChangeHandler: (event: React.ChangeEvent | SyntheticEvent, valueIndex?: number | "all", newValue?: any) => void; +} + +FilterCriteriaRowValues.defaultProps = { +}; + +function FilterCriteriaRowValues({operatorOption, criteria, fieldType, valueChangeHandler}: Props): JSX.Element +{ + if(!operatorOption) + { + return
    + } + + const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix="value-") => + { + let type = "search" + const inputLabelProps: any = {}; + + if(fieldType == QFieldType.INTEGER) + { + type = "number"; + } + else if(fieldType == QFieldType.DATE) + { + type = "date"; + inputLabelProps.shrink = true; + } + else if(fieldType == QFieldType.DATE_TIME) + { + type = "datetime-local"; + inputLabelProps.shrink = true; + } + + return valueChangeHandler(event, valueIndex)} + value={criteria.values[valueIndex]} + InputLabelProps={inputLabelProps} + fullWidth + // todo - x to clear value? + /> + } + + switch (operatorOption.valueMode) + { + case ValueMode.NONE: + return
    + case ValueMode.SINGLE: + return makeTextField(); + case ValueMode.SINGLE_DATE: + return makeTextField(); + case ValueMode.SINGLE_DATE_TIME: + return makeTextField(); + case ValueMode.DOUBLE: + return + + { makeTextField(0, "From", "from-") } + + + { makeTextField(1, "To", "to-") } + + ; + case ValueMode.MULTI: + let values = criteria.values; + if(values && values.length == 1 && values[0] == "") + { + values = []; + } + return ()} + options={[]} + multiple + freeSolo // todo - no debounce after enter? + selectOnFocus + clearOnBlur + limitTags={5} + value={values} + onChange={(event, value) => valueChangeHandler(event, "all", value)} + /> + case ValueMode.PVS_SINGLE: + break; + case ValueMode.PVS_MULTI: + break; + } + + return (
    ); +} + +export default FilterCriteriaRowValues; \ No newline at end of file diff --git a/src/qqq/pages/records/FilterPoc.tsx b/src/qqq/pages/records/FilterPoc.tsx new file mode 100644 index 0000000..a2ea88a --- /dev/null +++ b/src/qqq/pages/records/FilterPoc.tsx @@ -0,0 +1,75 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; +import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter"; +import Box from "@mui/material/Box"; +import {useEffect, useState} from "react"; +import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel"; +import BaseLayout from "qqq/layouts/BaseLayout"; +import Client from "qqq/utils/qqq/Client"; + + +interface Props +{ +} + +FilterPoc.defaultProps = {}; + +function FilterPoc({}: Props): JSX.Element +{ + const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData) + const [queryFilter, setQueryFilter] = useState(new QQueryFilter()) + + const updateFilter = (newFilter: QQueryFilter) => + { + setQueryFilter(JSON.parse(JSON.stringify(newFilter))); + } + + useEffect(() => + { + (async () => + { + const table = await Client.getInstance().loadTableMetaData("order") + setTableMetaData(table); + })(); + }, []); + + return ( + + { + tableMetaData && + + + {/* @ts-ignore */} + + +
    +                  {JSON.stringify(queryFilter, null, 3)})
    +               
    +
    + } +
    + ); +} + +export default FilterPoc; diff --git a/src/qqq/pages/records/IntersectionMatrix.tsx b/src/qqq/pages/records/IntersectionMatrix.tsx new file mode 100644 index 0000000..54657c1 --- /dev/null +++ b/src/qqq/pages/records/IntersectionMatrix.tsx @@ -0,0 +1,123 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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 Box from "@mui/material/Box"; +import Checkbox from "@mui/material/Checkbox/Checkbox"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import {makeStyles} from "@mui/styles"; +import {useState} from "react"; +import BaseLayout from "qqq/layouts/BaseLayout"; + + +interface Props +{ + foo: string; +} + +IntersectionMatrix.defaultProps = { + foo: null, +}; + +const useStyles = makeStyles({ + sticky: { + position: "sticky", + left: 0, + top: 0, + background: "white", + boxShadow: "2px 2px 2px grey", + borderRight: "2px solid grey", + zIndex: 1 + } +}); + +function IntersectionMatrix({foo}: Props): JSX.Element +{ + const permissions = ["apiLog.delete", "apiLog.edit", "apiLog.insert", "apiLog.read", "apiLogUser.delete", "apiLogUser.edit", "apiLogUser.insert", "apiLogUser.read", "audit.delete", "audit.edit", "audit.insert", "audit.read", "auditDetail.delete", "auditDetail.edit", "auditDetail.insert", "auditDetail.read", "auditTable.delete", "auditTable.edit", "auditTable.insert", "auditTable.read", "auditUser.delete", "auditUser.edit", "auditUser.insert", "auditUser.read", "availableInventoryIndex.delete", "availableInventoryIndex.edit", "availableInventoryIndex.insert", "availableInventoryIndex.read", "availablePermission.delete", "availablePermission.edit", "availablePermission.insert", "availablePermission.read", "billing.hasAccess", "billingActivity.delete", "billingActivity.edit", "billingActivity.insert", "billingActivity.read", "billingDashboard.hasAccess", "billingWorksheet.delete", "billingWorksheet.edit", "billingWorksheet.insert", "billingWorksheet.read", "billingWorksheetLine.delete", "billingWorksheetLine.edit", "billingWorksheetLine.insert", "billingWorksheetLine.read", "billingWorksheetLineDetail.hasAccess", "billingWorksheetRevenueReport.hasAccess", "billingWorksheetSummary.hasAccess", "blackboxCartonization.delete", "blackboxCartonization.edit", "blackboxCartonization.insert", "blackboxCartonization.read", "blackboxStatus.delete", "blackboxStatus.edit", "blackboxStatus.insert", "blackboxStatus.read", "cancelBillingWorksheet.hasAccess", "carrier.delete", "carrier.edit", "carrier.insert", "carrier.read", "carrierAccount.delete", "carrierAccount.edit", "carrierAccount.insert", "carrierAccount.read", "carrierInvoicing.hasAccess", "carrierPerformance.hasAccess", "carrierPerformanceDashboard.hasAccess", "carrierRevenueReport.hasAccess", "carrierService.delete", "carrierService.edit", "carrierService.insert", "carrierService.read", "carrierServiceSlaExclusionDate.delete", "carrierServiceSlaExclusionDate.edit", "carrierServiceSlaExclusionDate.insert", "carrierServiceSlaExclusionDate.read", "cartonType.delete", "cartonType.edit", "cartonType.insert", "cartonType.read", "cartonization.hasAccess", "client.delete", "client.edit", "client.insert", "client.read", "clientAlias.delete", "clientAlias.edit", "clientAlias.insert", "clientAlias.read", "clientAuth0Application.delete", "clientAuth0Application.edit", "clientAuth0Application.insert", "clientAuth0Application.read", "clientAuth0ApplicationApiKey.delete", "clientAuth0ApplicationApiKey.edit", "clientAuth0ApplicationApiKey.insert", "clientAuth0ApplicationApiKey.read", "clientBillingKey.delete", "clientBillingKey.edit", "clientBillingKey.insert", "clientBillingKey.read", "clientFeeKey.delete", "clientFeeKey.edit", "clientFeeKey.insert", "clientFeeKey.read", "clientShipStationStore.delete", "clientShipStationStore.edit", "clientShipStationStore.insert", "clientShipStationStore.read", "closeBillingWorksheet.hasAccess", "connection.delete", "connection.edit", "connection.insert", "connection.read", "createBillingWorksheet.hasAccess", "createTestOrdersProcess.hasAccess", "dashboard.hasAccess", "dashboards.hasAccess", "dataBag.delete", "dataBag.edit", "dataBag.insert", "dataBag.read", "dataBagVersion.delete", "dataBagVersion.edit", "dataBagVersion.insert", "dataBagVersion.read", "dataHealthDashboard.hasAccess", "dataIndex.delete", "dataIndex.edit", "dataIndex.insert", "dataIndex.read", "deleteSavedFilter.hasAccess", "deposcoCreateTestOrdersJob.delete", "deposcoCreateTestOrdersJob.edit", "deposcoCreateTestOrdersJob.insert", "deposcoCreateTestOrdersJob.read", "deposcoCreateTestOrdersProcess.hasAccess", "deposcoCurrentExceptionsWidget.hasAccess", "deposcoCurrentStatusWidget.hasAccess", "deposcoCustomerOrder.delete", "deposcoCustomerOrder.edit", "deposcoCustomerOrder.insert", "deposcoCustomerOrder.read", "deposcoEnterpriseInventory.delete", "deposcoEnterpriseInventory.edit", "deposcoEnterpriseInventory.insert", "deposcoEnterpriseInventory.read", "deposcoItem.delete", "deposcoItem.edit", "deposcoItem.insert", "deposcoItem.read", "deposcoOrder.delete", "deposcoOrder.edit", "deposcoOrder.insert", "deposcoOrder.read", "deposcoOrderToOrder.hasAccess", "deposcoOrdersApp.hasAccess", "deposcoOrdersByClientPieChart.hasAccess", "deposcoPollForCustomerOrders.hasAccess", "deposcoPollForOrders.hasAccess", "deposcoRecentDataParentWidget.hasAccess", "deposcoReplaceLineItemProcess.hasAccess", "deposcoSalesOrder.delete", "deposcoSalesOrder.edit", "deposcoSalesOrder.insert", "deposcoSalesOrder.read", "deposcoSalesOrderLine.delete", "deposcoSalesOrderLine.edit", "deposcoSalesOrderLine.insert", "deposcoSalesOrderLine.read", "deposcoSalesOrdersBarChart.hasAccess", "deposcoSentOrder.delete", "deposcoSentOrder.edit", "deposcoSentOrder.insert", "deposcoSentOrder.read", "deposcoShipment.delete", "deposcoShipment.edit", "deposcoShipment.insert", "deposcoShipment.read", "deposcoShipmentToSystemGeneratedTrackingNo.hasAccess", "deposcoTradingPartner.delete", "deposcoTradingPartner.edit", "deposcoTradingPartner.insert", "deposcoTradingPartner.read", "developer.hasAccess", "easypostTracker.delete", "easypostTracker.edit", "easypostTracker.insert", "easypostTracker.read", "extensivOrder.delete", "extensivOrder.edit", "extensivOrder.insert", "extensivOrder.read", "extensivOrderToOrder.hasAccess", "fedexTntCache.delete", "fedexTntCache.edit", "fedexTntCache.insert", "fedexTntCache.read", "freightStudy.delete", "freightStudy.edit", "freightStudy.insert", "freightStudy.read", "freightStudyActualShipment.delete", "freightStudyActualShipment.edit", "freightStudyActualShipment.insert", "freightStudyActualShipment.read", "freightStudyAllShipmentsReport.hasAccess", "freightStudyAllShipmentsReportProcess.hasAccess", "freightStudyApp.hasAccess", "freightStudyEstimateShipments.hasAccess", "freightStudyEstimatedShipment.delete", "freightStudyEstimatedShipment.edit", "freightStudyEstimatedShipment.insert", "freightStudyEstimatedShipment.read", "freightStudyScenario.delete", "freightStudyScenario.edit", "freightStudyScenario.insert", "freightStudyScenario.read", "fuelSurcharge.delete", "fuelSurcharge.edit", "fuelSurcharge.insert", "fuelSurcharge.read", "fulfillment.hasAccess", "generateBillingActivityFromBillingWorksheet.hasAccess", "generateBillingWorksheetDocuments.hasAccess", "generateParcelInvoiceLineFromRawAxleHire.hasAccess", "generateParcelInvoiceLineFromRawCdl.hasAccess", "generateParcelInvoiceLineFromRawFedEx.hasAccess", "generateParcelInvoiceLineFromRawLso.hasAccess", "generateParcelInvoiceLineFromRawOntrac.hasAccess", "generateParcelInvoiceLineFromRawUps.hasAccess", "graceDiscountAuditReport.hasAccess", "infoplusLOB.delete", "infoplusLOB.edit", "infoplusLOB.insert", "infoplusLOB.read", "infoplusOrder.delete", "infoplusOrder.edit", "infoplusOrder.insert", "infoplusOrder.read", "infoplusOrderToOrder.hasAccess", "infoplusShipment.delete", "infoplusShipment.edit", "infoplusShipment.insert", "infoplusShipment.read", "infoplusShipmentToSystemGeneratedTrackingNumber.hasAccess", "infoplusWarehouse.delete", "infoplusWarehouse.edit", "infoplusWarehouse.insert", "infoplusWarehouse.read", "initParcelSlaStatus.hasAccess", "integrations.hasAccess", "lineItem.delete", "lineItem.edit", "lineItem.insert", "lineItem.read", "manualUpdateInvoiceLineFromRaw.hasAccess", "markBillingActivityAsException.hasAccess", "markParcelInvoiceLineAsOrphan.hasAccess", "mergeDuplicatedParcels.hasAccess", "omsOperationsDashboard.hasAccess", "optimization.hasAccess", "optimizationCarrierServiceRulesChecker.hasAccess", "optimizationCarrierServiceStateRule.delete", "optimizationCarrierServiceStateRule.edit", "optimizationCarrierServiceStateRule.insert", "optimizationCarrierServiceStateRule.read", "optimizationCarrierServiceTNTRule.delete", "optimizationCarrierServiceTNTRule.edit", "optimizationCarrierServiceTNTRule.insert", "optimizationCarrierServiceTNTRule.read", "optimizationCarrierServiceZipCodeRule.delete", "optimizationCarrierServiceZipCodeRule.edit", "optimizationCarrierServiceZipCodeRule.insert", "optimizationCarrierServiceZipCodeRule.read", "optimizationConfig.delete", "optimizationConfig.edit", "optimizationConfig.insert", "optimizationConfig.read", "optimizationConfigApp.hasAccess", "optimizationDashboard.hasAccess", "optimizationRateChecker.hasAccess", "optimizationRulesChecker.hasAccess", "optimizationStateRule.delete", "optimizationStateRule.edit", "optimizationStateRule.insert", "optimizationStateRule.read", "optimizationTNTRule.delete", "optimizationTNTRule.edit", "optimizationTNTRule.insert", "optimizationTNTRule.read", "optimizationWarehouseRoutingStateRule.delete", "optimizationWarehouseRoutingStateRule.edit", "optimizationWarehouseRoutingStateRule.insert", "optimizationWarehouseRoutingStateRule.read", "optimizationWarehouseRoutingZipCodeRule.delete", "optimizationWarehouseRoutingZipCodeRule.edit", "optimizationWarehouseRoutingZipCodeRule.insert", "optimizationWarehouseRoutingZipCodeRule.read", "optimizationZipCodeRule.delete", "optimizationZipCodeRule.edit", "optimizationZipCodeRule.insert", "optimizationZipCodeRule.read", "order.delete", "order.edit", "order.insert", "order.read", "orderAndShipmentPerformanceDashboard.hasAccess", "orderCarton.delete", "orderCarton.edit", "orderCarton.insert", "orderCarton.read", "orderCartonization.delete", "orderCartonization.edit", "orderCartonization.insert", "orderCartonization.read", "orderExtrinsic.delete", "orderExtrinsic.edit", "orderExtrinsic.insert", "orderExtrinsic.read", "orderOptimization.hasAccess", "orders.hasAccess", "ordersAndShipmentsReport.hasAccess", "outboundApiLog.delete", "outboundApiLog.edit", "outboundApiLog.insert", "outboundApiLog.read", "outboundScannedTrackingNumber.delete", "outboundScannedTrackingNumber.edit", "outboundScannedTrackingNumber.insert", "outboundScannedTrackingNumber.read", "outboundScannedTrackingNumberToParcel.hasAccess", "overview.hasAccess", "overviewDashboard.hasAccess", "parcel.delete", "parcel.edit", "parcel.insert", "parcel.read", "parcelHealthApp.hasAccess", "parcelInvoice.delete", "parcelInvoice.edit", "parcelInvoice.insert", "parcelInvoice.read", "parcelInvoiceLine.delete", "parcelInvoiceLine.edit", "parcelInvoiceLine.insert", "parcelInvoiceLine.read", "parcelInvoiceLineChargeMappingRule.delete", "parcelInvoiceLineChargeMappingRule.edit", "parcelInvoiceLineChargeMappingRule.insert", "parcelInvoiceLineChargeMappingRule.read", "parcelInvoiceLineChargeRollupRule.read", "parcelInvoiceLineToParcel.hasAccess", "parcelInvoiceRawETLAxleHire.hasAccess", "parcelInvoiceRawETLCdl.hasAccess", "parcelInvoiceRawETLFedEx.hasAccess", "parcelInvoiceRawETLLso.hasAccess", "parcelInvoiceRawETLOntrac.hasAccess", "parcelInvoiceRawETLUps.hasAccess", "parcelInvoiceShiplabsSyncAxleHire.hasAccess", "parcelInvoiceShiplabsSyncCdl.hasAccess", "parcelInvoiceShiplabsSyncFedEx.hasAccess", "parcelInvoiceShiplabsSyncLso.hasAccess", "parcelInvoiceShiplabsSyncOntrac.hasAccess", "parcelInvoiceShiplabsSyncUps.hasAccess", "parcelSlaStatus.delete", "parcelSlaStatus.edit", "parcelSlaStatus.insert", "parcelSlaStatus.read", "parcelTrackingDetail.delete", "parcelTrackingDetail.edit", "parcelTrackingDetail.insert", "parcelTrackingDetail.read", "parcels.hasAccess", "pollExtensiveForOrders.hasAccess", "pushDeposcoSalesOrders.hasAccess", "querySavedFilter.hasAccess", "rawParcelInvoiceLineAxleHire.delete", "rawParcelInvoiceLineAxleHire.edit", "rawParcelInvoiceLineAxleHire.insert", "rawParcelInvoiceLineAxleHire.read", "rawParcelInvoiceLineCdl.delete", "rawParcelInvoiceLineCdl.edit", "rawParcelInvoiceLineCdl.insert", "rawParcelInvoiceLineCdl.read", "rawParcelInvoiceLineFedEx.delete", "rawParcelInvoiceLineFedEx.edit", "rawParcelInvoiceLineFedEx.insert", "rawParcelInvoiceLineFedEx.read", "rawParcelInvoiceLineLso.delete", "rawParcelInvoiceLineLso.edit", "rawParcelInvoiceLineLso.insert", "rawParcelInvoiceLineLso.read", "rawParcelInvoiceLineOntrac.delete", "rawParcelInvoiceLineOntrac.edit", "rawParcelInvoiceLineOntrac.insert", "rawParcelInvoiceLineOntrac.read", "rawParcelInvoiceLineUps.delete", "rawParcelInvoiceLineUps.edit", "rawParcelInvoiceLineUps.insert", "rawParcelInvoiceLineUps.read", "receiveEasypostTrackerWebhook.hasAccess", "reconcileClientsOnParcelInvoiceLine.hasAccess", "reconcileClientsOnParcelInvoiceLineFromBillingWorksheet.hasAccess", "reevaluateParcelSlaStatus.hasAccess", "registerParcelAsEasypostTracker.hasAccess", "releaseOrderToWmsProcess.hasAccess", "releaseOrdersJob.delete", "releaseOrdersJob.edit", "releaseOrdersJob.insert", "releaseOrdersJob.read", "releaseOrdersJobOrder.delete", "releaseOrdersJobOrder.edit", "releaseOrdersJobOrder.insert", "releaseOrdersJobOrder.read", "releaseOrdersToWmsProcess.hasAccess", "replaceLineItem.hasAccess", "resyncOrderFromSource.hasAccess", "resyncParcelTrackingStatus.hasAccess", "resyncSystemGeneratedTrackingNumberFromSource.hasAccess", "retrySendingReleaseOrdersJob.hasAccess", "runBillingWorksheetRevenueReport.hasAccess", "runRecordScript.hasAccess", "salesOrderAutomation.hasAccess", "savedFilter.delete", "savedFilter.edit", "savedFilter.insert", "savedFilter.read", "script.delete", "script.edit", "script.insert", "script.read", "scriptLog.delete", "scriptLog.edit", "scriptLog.insert", "scriptLog.read", "scriptLogLine.delete", "scriptLogLine.edit", "scriptLogLine.insert", "scriptLogLine.read", "scriptRevision.delete", "scriptRevision.edit", "scriptRevision.insert", "scriptRevision.read", "scriptType.delete", "scriptType.edit", "scriptType.insert", "scriptType.read", "setup.hasAccess", "shipStationOrder0.delete", "shipStationOrder0.edit", "shipStationOrder0.insert", "shipStationOrder0.read", "shipStationOrderToOrder0.hasAccess", "shipStationShipment0.delete", "shipStationShipment0.edit", "shipStationShipment0.insert", "shipStationShipment0.read", "shipStationShipmentToSystemGeneratedTrackingNumber0.hasAccess", "shipStationShipmentToSystemGeneratedTrackingNumber1.hasAccess", "shipStationShipmentToSystemGeneratedTrackingNumber2.hasAccess", "shipStationShipmentToSystemGeneratedTrackingNumber3.hasAccess", "shipStationShipmentToSystemGeneratedTrackingNumber4.hasAccess", "shipStationStore0.delete", "shipStationStore0.edit", "shipStationStore0.insert", "shipStationStore0.read", "shipStationWarehouse0.delete", "shipStationWarehouse0.edit", "shipStationWarehouse0.insert", "shipStationWarehouse0.read", "shippedOrderToExtensivOrder.hasAccess", "shipping.hasAccess", "shippingDashboard.hasAccess", "storeDataBagVersion.hasAccess", "storeSavedFilter.hasAccess", "storeScriptRevision.hasAccess", "systemGeneratedTrackingNumber.delete", "systemGeneratedTrackingNumber.edit", "systemGeneratedTrackingNumber.insert", "systemGeneratedTrackingNumber.read", "systemGeneratedTrackingNumberToParcel.hasAccess", "tableTrigger.delete", "tableTrigger.edit", "tableTrigger.insert", "tableTrigger.read", "testScript.hasAccess", "totalDeposcoOrdersImported.hasAccess", "uploadFileArchive.delete", "uploadFileArchive.edit", "uploadFileArchive.insert", "uploadFileArchive.read", "warehouse.delete", "warehouse.edit", "warehouse.insert", "warehouse.read", "warehouseClientInt.delete", "warehouseClientInt.edit", "warehouseClientInt.insert", "warehouseClientInt.read", "warehouseShipStationWarehouse.delete", "warehouseShipStationWarehouse.edit", "warehouseShipStationWarehouse.insert", "warehouseShipStationWarehouse.read", "zipZone.delete", "zipZone.edit", "zipZone.insert", "zipZone.read", "zipZoneCdl.delete", "zipZoneCdl.edit", "zipZoneCdl.insert", "zipZoneCdl.read"]; + permissions.splice(50) + const roles = ["External - Customer - OMS API User", "External - Customer - Reports API", "External - Customer - Viewer", "External - Deposco - Cartonization API", "External - Optimization - Viewer", "Internal - Carrier Invoicing - Admin", "Internal - Carrier Invoicing - User", "Internal - Carrier Invoicing - Viewer", "Internal - Developer - Admin", "Internal - Engineering Team - Admin", "Internal - Executive Team", "Internal - Freight Study - Admin", "Internal - Freight Study - User", "Internal - Freight Study - Viewer", "Internal - Integrations - Viewer", "Internal - Optimization - Admin", "Internal - Optimization - User", "Internal - Optimization - Viewer", "Internal - Orders & Parcels - Admin", "Internal - Orders & Parcels - User"]; + + const classes = useStyles(); + + return ( + + + + {/* display: fixes apparent bug in mui? */} + + + { + roles.map((name) => ( + + {name} + + )) + } + + + + { + permissions.map((name) => ( + + + {name.split(/(?=[A-Z.])/).map((part, index) => ( + {part} + ))} + + { + roles.map((role) => ( + + + + )) + } + + )) + } + +
    +
    +
    + ); + + return ( + + + { + permissions.map((name) => + { + return ( + + {name} + + ) + }) + } + + + ); +} + +export default IntersectionMatrix; diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index cdc303a..4da63d7 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -59,6 +59,7 @@ import {QActionsMenuButton, QCancelButton, QCreateNewButton, QSaveButton} from " import MenuButton from "qqq/components/buttons/MenuButton"; import SavedFilters from "qqq/components/misc/SavedFilters"; import {CustomColumnsPanel} from "qqq/components/query/CustomColumnsPanel"; +import {CustomFilterPanel} from "qqq/components/query/CustomFilterPanel"; import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip"; import BaseLayout from "qqq/layouts/BaseLayout"; import ProcessRun from "qqq/pages/processes/ProcessRun"; @@ -154,7 +155,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } const [filterModel, setFilterModel] = useState({items: []} as GridFilterModel); + const [lastFetchedQFilterJSON, setLastFetchedQFilterJSON] = useState(""); const [columnSortModel, setColumnSortModel] = useState(defaultSort); + const [queryFilter, setQueryFilter] = useState(new QQueryFilter()); + const [columnVisibilityModel, setColumnVisibilityModel] = useState(defaultVisibility); const [shouldSetAllNewJoinFieldsToHidden, setShouldSetAllNewJoinFieldsToHidden] = useState(!didDefaultVisibilityComeFromLocalStorage) const [visibleJoinTables, setVisibleJoinTables] = useState(new Set()); @@ -217,7 +221,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [queryErrors, setQueryErrors] = useState({} as any); const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date()); - const {setPageHeader} = useContext(QContext); const [, forceUpdate] = useReducer((x) => x + 1, 0); @@ -335,6 +338,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element updateColumnVisibilityModel(); setColumnsModel([]); setFilterModel({items: []}); + setQueryFilter(new QQueryFilter()); setDefaultFilterLoaded(false); setRows([]); } @@ -512,6 +516,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element let models = await FilterUtils.determineFilterAndSortModels(qController, tableMetaData, null, searchParams, filterLocalStorageKey, sortLocalStorageKey); setFilterModel(models.filter); setColumnSortModel(models.sort); + setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, models.filter, models.sort, rowsPerPage)); return; } @@ -545,6 +550,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { columnSortModel.splice(i, 1); setColumnSortModel(columnSortModel); + // todo - need to setQueryFilter? resetColumnSortModel = true; i--; } @@ -561,6 +567,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element sort: "desc", }); setColumnSortModel(columnSortModel); + // todo - need to setQueryFilter? resetColumnSortModel = true; } @@ -617,6 +624,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element }); } + setLastFetchedQFilterJSON(JSON.stringify(qFilter)); qController.query(tableName, qFilter, queryJoins).then((results) => { console.log(`Received results for query ${thisQueryId}`); @@ -859,6 +867,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const newVisibleJoinTables = getVisibleJoinTables(); if (JSON.stringify([...newVisibleJoinTables.keys()]) != JSON.stringify([...visibleJoinTables.keys()])) { + console.log("calling update table for visible join table change"); updateTable(); setVisibleJoinTables(newVisibleJoinTables); } @@ -870,9 +879,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element console.log(columnOrderChangeParams); }; - const handleFilterChange = (filterModel: GridFilterModel) => + const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true) => { setFilterModel(filterModel); + + if(doSetQueryFilter) + { + ////////////////////////////////////////////////////////////////////////////////// + // someone might have already set the query filter, so, only set it if asked to // + ////////////////////////////////////////////////////////////////////////////////// + setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage)); + } + if (filterLocalStorageKey) { localStorage.setItem(filterLocalStorageKey, JSON.stringify(filterModel)); @@ -884,6 +902,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element if (gridSort && gridSort.length > 0) { setColumnSortModel(gridSort); + setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, gridSort, rowsPerPage)); localStorage.setItem(sortLocalStorageKey, JSON.stringify(gridSort)); } }; @@ -948,8 +967,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element ////////////////////////////////////// // construct the url for the export // ////////////////////////////////////// - const d = new Date(); - const dateString = `${d.getFullYear()}-${zp(d.getMonth() + 1)}-${zp(d.getDate())} ${zp(d.getHours())}${zp(d.getMinutes())}`; + const dateString = ValueUtils.formatDateTimeForFileName(new Date()); const filename = `${tableMetaData.label} Export ${dateString}.${format}`; const url = `/data/${tableMetaData.name}/export/${filename}?filter=${encodeURIComponent(JSON.stringify(buildQFilter(tableMetaData, filterModel)))}&fields=${visibleFields.join(",")}`; @@ -1087,6 +1105,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element newPath.pop(); navigate(newPath.join("/")); + console.log("calling update table for close modal"); updateTable(); }; @@ -1196,6 +1215,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element return ( + { + if(fieldName.indexOf(".") > -1) + { + const nameParts = fieldName.split(".", 2); + for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++) + { + const join = tableMetaData?.exposedJoins[i]; + if(join?.joinTable.name == nameParts[0]) + { + return ([join.joinTable.fields.get(nameParts[1]), join.joinTable]); + } + } + } + else + { + return ([tableMetaData.fields.get(fieldName), tableMetaData]); + } + + return (null); + } + const copyColumnValues = async (column: GridColDef) => { let data = ""; let counter = 0; if (latestQueryResults && latestQueryResults.length) { - let qFieldMetaData = tableMetaData.fields.get(column.field); + let [qFieldMetaData, fieldTable] = getFieldAndTable(column.field); for (let i = 0; i < latestQueryResults.length; i++) { let record = latestQueryResults[i] as QRecord; - const value = ValueUtils.getUnadornedValueForDisplay(qFieldMetaData, record.values.get(qFieldMetaData.name), record.displayValues.get(qFieldMetaData.name)); + const value = ValueUtils.getUnadornedValueForDisplay(qFieldMetaData, record.values.get(column.field), record.displayValues.get(column.field)); if (value !== null && value !== undefined && String(value) !== "") { data += value + "\n"; @@ -1293,24 +1335,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setFilterForColumnStats(buildQFilter(tableMetaData, filterModel)); setColumnStatsFieldName(column.field); - if(column.field.indexOf(".") > -1) - { - const nameParts = column.field.split(".", 2); - for (let i = 0; i < tableMetaData?.exposedJoins?.length; i++) - { - const join = tableMetaData?.exposedJoins[i]; - if(join?.joinTable.name == nameParts[0]) - { - setColumnStatsField(join.joinTable.fields.get(nameParts[1])); - setColumnStatsFieldTableName(nameParts[0]); - } - } - } - else - { - setColumnStatsField(tableMetaData.fields.get(column.field)); - setColumnStatsFieldTableName(tableMetaData.name); - } + const [field, fieldTable] = getFieldAndTable(column.field); + setColumnStatsField(field); + setColumnStatsFieldTableName(fieldTable.name); }; const CustomColumnMenu = forwardRef( @@ -1474,6 +1501,15 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } }; + const doClearFilter = (event: React.KeyboardEvent, isYesButton: boolean = false) => + { + if (isYesButton|| event.key == "Enter") + { + setShowClearFiltersWarning(false); + handleFilterChange({items: []} as GridFilterModel); + } + } + return (
    @@ -1488,30 +1524,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { hasValidFilters && ( - -
    - +
    + setShowClearFiltersWarning(true)}>clear - setShowClearFiltersWarning(false)} onKeyPress={(e) => - { - if (e.key == "Enter") - { - setShowClearFiltersWarning(false) - handleFilterChange({items: []} as GridFilterModel); - } - }}> + setShowClearFiltersWarning(false)} onKeyPress={(e) => doClearFilter(e)}> Confirm - Are you sure you want to clear all filters? + Are you sure you want to remove all conditions from the current filter? setShowClearFiltersWarning(false)} /> - - { - setShowClearFiltersWarning(false); - handleFilterChange({items: []} as GridFilterModel); - }}/> + doClearFilter(null, true)}/>
    @@ -1676,6 +1700,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element // to avoid both this useEffect and the one below from both doing an "initial query", // // only run this one if at least 1 query has already been ran // //////////////////////////////////////////////////////////////////////////////////////// + // console.log("calling update table for UE 1"); updateTable(); } }, [pageNumber, rowsPerPage, columnSortModel, currentSavedFilter]); @@ -1687,8 +1712,31 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { setTotalRecords(null); setDistinctRecords(null); + // console.log("calling update table for UE 2"); updateTable(); - }, [columnsModel, tableState, filterModel]); + }, [columnsModel, tableState]); + + useEffect(() => + { + const currentQFilter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage); + currentQFilter.skip = pageNumber * rowsPerPage; + const currentQFilterJSON = JSON.stringify(currentQFilter); + + // console.log(`current ${currentQFilterJSON}`); + // console.log(`last... ${lastFetchedQFilterJSON}`); + if(currentQFilterJSON !== lastFetchedQFilterJSON) + { + setTotalRecords(null); + setDistinctRecords(null); + // console.log("calling update table for UE 3"); + updateTable(); + } + else + { + // console.log("NOT calling update table for UE 3!!"); + } + + }, [filterModel]); useEffect(() => { @@ -1696,6 +1744,13 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element document.scrollingElement.scrollTop = 0; }, [pageNumber, rowsPerPage]); + const updateFilter = (newFilter: QQueryFilter): void => + { + setQueryFilter(newFilter); + const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter); + handleFilterChange(gridFilterModel, false); + } + if (tableMetaData && !tableMetaData.readPermission) { return ( @@ -1769,7 +1824,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission && } - @@ -1779,7 +1833,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element Pagination: CustomPagination, LoadingOverlay: Loading, ColumnMenu: CustomColumnMenu, - ColumnsPanel: CustomColumnsPanel + ColumnsPanel: CustomColumnsPanel, + FilterPanel: CustomFilterPanel }} componentsProps={{ columnsPanel: @@ -1789,8 +1844,21 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element openGroupsChanger: setColumnChooserOpenGroups, initialFilterText: columnChooserFilterText, filterTextChanger: setColumnChooserFilterText + }, + filterPanel: + { + tableMetaData: tableMetaData, + queryFilter: queryFilter, + updateFilter: updateFilter } }} + localeText={{ + toolbarFilters: "Filter", // label on the filters button. we prefer singular (1 filter has many "conditions" in it). + toolbarFiltersLabel: "", // setting these 3 to "" turns off the "Show Filters" and "Hide Filters" tooltip (which can get in the way of the actual filters panel) + toolbarFiltersTooltipShow: "", + toolbarFiltersTooltipHide: "", + toolbarFiltersTooltipActive: count => count !== 1 ? `${count} conditions` : `${count} condition` + }} pinnedColumns={pinnedColumns} onPinnedColumnsChange={handlePinnedColumnsChange} pagination @@ -1812,7 +1880,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element density={density} loading={loading} filterModel={filterModel} - onFilterModelChange={handleFilterChange} + onFilterModelChange={(model) => handleFilterChange(model)} columnVisibilityModel={columnVisibilityModel} onColumnVisibilityModelChange={handleColumnVisibilityChange} onColumnOrderChange={handleColumnOrderChange} diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index 24b2451..ce5a40d 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -405,4 +405,72 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } height: 15px !important; position: relative; top: 3px; -} \ No newline at end of file +} + +.blobIcon +{ + margin-left: 0.25rem; + margin-right: 0.25rem; + cursor: pointer; +} + +/* move the columns & filter panels on the query screen data grid up to not be below the column headers row */ +/* todo - add a class to the query screen and qualify this like that */ +.MuiDataGrid-panel +{ + top: -60px !important; +} + +.customFilterPanel .MuiAutocomplete-paper +{ + line-height: 1.375; +} + +.customFilterPanel .MuiAutocomplete-groupLabel +{ + line-height: 1.75; +} + +.customFilterPanel .MuiAutocomplete-listbox +{ + max-height: 60vh; +} + +.customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard, +.customFilterPanel .MuiSvgIcon-root +{ + font-size: 14px !important; +} + +.customFilterPanel .booleanOperatorColumn .MuiSvgIcon-root +{ + display: inline-block !important; +} + +.customFilterPanel .booleanOperatorColumn .MuiInputBase-formControl +{ + padding-bottom: calc(0.25rem + 1px); +} + +.customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard +{ + top: calc(50% - 0.75rem); +} + +.customFilterPanel .filterValuesColumn .MuiChip-root +{ + background: none; + color: black; + border: 1px solid gray; +} + +.customFilterPanel .filterValuesColumn .MuiChip-root .MuiChip-deleteIcon +{ + color: gray; +} + +.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag +{ + color: #191919; + background: none; +} diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts index 0aebaf7..7830a8e 100644 --- a/src/qqq/utils/qqq/FilterUtils.ts +++ b/src/qqq/utils/qqq/FilterUtils.ts @@ -27,7 +27,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC 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 {GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro"; +import {GridFilterItem, GridFilterModel, GridLinkOperator, GridSortItem} from "@mui/x-data-grid-pro"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; const CURRENT_SAVED_FILTER_ID_LOCAL_STORAGE_KEY_ROOT = "qqq.currentSavedFilterId"; @@ -256,7 +256,7 @@ class FilterUtils } else if (operator === QCriteriaOperator.IN || operator === QCriteriaOperator.NOT_IN || operator === QCriteriaOperator.BETWEEN || operator === QCriteriaOperator.NOT_BETWEEN) { - if (value == null && (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 // @@ -538,6 +538,63 @@ class FilterUtils } + /******************************************************************************* + ** 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 *******************************************************************************/