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 */}
+
+
+