From 50979a1ecc85178107331b1279d0886d268507ab Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 19 Jun 2023 08:40:47 -0500 Subject: [PATCH] Checkpoint; nearing completion of custom filter panel --- src/qqq/components/forms/DynamicSelect.tsx | 30 ++-- .../components/query/CustomFilterPanel.tsx | 30 +++- .../components/query/FilterCriteriaPaster.tsx | 46 ++---- .../components/query/FilterCriteriaRow.tsx | 86 +++++++--- .../query/FilterCriteriaRowValues.tsx | 147 ++++++++++++++---- src/qqq/pages/records/query/RecordQuery.tsx | 43 +++-- src/qqq/styles/qqq-override-styles.css | 27 ++++ src/qqq/utils/DataGridUtils.tsx | 38 ++++- src/qqq/utils/qqq/FilterUtils.ts | 67 ++++++-- 9 files changed, 380 insertions(+), 134 deletions(-) diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index 46ee247..ba22d4b 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -38,6 +38,7 @@ interface Props tableName?: string; processName?: string; fieldName: string; + overrideId?: string; fieldLabel: string; inForm: boolean; initialValue?: any; @@ -70,29 +71,34 @@ DynamicSelect.defaultProps = { const qController = Client.getInstance(); -function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props) +function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues}: Props) { - const [ open, setOpen ] = useState(false); - const [ options, setOptions ] = useState([]); - const [ searchTerm, setSearchTerm ] = useState(null); - const [ firstRender, setFirstRender ] = useState(true); + const [open, setOpen] = useState(false); + const [options, setOptions] = useState([]); + const [searchTerm, setSearchTerm] = useState(null); + const [firstRender, setFirstRender] = useState(true); //////////////////////////////////////////////////////////////////////////////////////////////// // default value - needs to be an array (from initialValues (array) prop) for multiple mode - // // else non-multiple, assume we took in an initialValue (id) and initialDisplayValue (label), // // and build a little object that looks like a possibleValue out of those // //////////////////////////////////////////////////////////////////////////////////////////////// - const [defaultValue, _] = isMultiple ? useState(initialValues ?? undefined) + let [defaultValue, _] = isMultiple ? useState(initialValues ?? undefined) : useState(initialValue && initialDisplayValue ? [{id: initialValue, label: initialDisplayValue}] : null); + if (isMultiple && defaultValue === null) + { + defaultValue = []; + } + // const loading = open && options.length === 0; const [loading, setLoading] = useState(false); - const [ switchChecked, setSwitchChecked ] = useState(false); - const [ isDisabled, setIsDisabled ] = useState(!isEditable || bulkEditMode); + const [switchChecked, setSwitchChecked] = useState(false); + const [isDisabled, setIsDisabled] = useState(!isEditable || bulkEditMode); const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData); let setFieldValueRef: (field: string, value: any, shouldValidate?: boolean) => void = null; - if(inForm) + if (inForm) { const {setFieldValue} = useFormikContext(); setFieldValueRef = setFieldValue; @@ -239,9 +245,11 @@ function DynamicSelect({tableName, processName, fieldName, fieldLabel, inForm, i bulkEditSwitchChangeHandler(fieldName, newSwitchValue); }; + // console.log(`default value: ${JSON.stringify(defaultValue)}`); + const autocomplete = ( ( . */ +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; 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"; @@ -28,7 +29,7 @@ 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"; +import {FilterCriteriaRow, getDefaultCriteriaValue} from "qqq/components/query/FilterCriteriaRow"; declare module "@mui/x-data-grid" @@ -39,6 +40,7 @@ declare module "@mui/x-data-grid" interface FilterPanelPropsOverrides { tableMetaData: QTableMetaData; + metaData: QInstance; queryFilter: QQueryFilter; updateFilter: (newFilter: QQueryFilter) => void; } @@ -66,9 +68,9 @@ export const CustomFilterPanel = forwardRef( { setTimeout(() => { - console.log(`Try to focus ${criteriaId - 1}`); try { + // console.log(`Try to focus ${criteriaId - 1}`); document.getElementById(`field-${criteriaId - 1}`).focus(); } catch (e) @@ -80,7 +82,7 @@ export const CustomFilterPanel = forwardRef( const addCriteria = () => { - const qFilterCriteriaWithId = new QFilterCriteriaWithId(null, QCriteriaOperator.EQUALS, [""]); + const qFilterCriteriaWithId = new QFilterCriteriaWithId(null, QCriteriaOperator.EQUALS, getDefaultCriteriaValue()); qFilterCriteriaWithId.id = criteriaId++; console.log(`adding criteria id ${qFilterCriteriaWithId.id}`); queryFilter.criteria.push(qFilterCriteriaWithId); @@ -98,8 +100,29 @@ export const CustomFilterPanel = forwardRef( if (queryFilter.criteria.length == 0) { + ///////////////////////////////////////////// + // make sure there's at least one criteria // + ///////////////////////////////////////////// addCriteria(); } + else + { + //////////////////////////////////////////////////////////////////////////////////// + // make sure all criteria have an id on them (to be used as react component keys) // + //////////////////////////////////////////////////////////////////////////////////// + let updatedAny = false; + for (let i = 0; i < queryFilter.criteria.length; i++) + { + if (!queryFilter.criteria[i].id) + { + queryFilter.criteria[i].id = criteriaId++; + } + } + if (updatedAny) + { + props.updateFilter(queryFilter); + } + } if(queryFilter.criteria.length == 1 && !queryFilter.criteria[0].fieldName) { @@ -149,6 +172,7 @@ export const CustomFilterPanel = forwardRef( id={criteria.id} index={index} tableMetaData={props.tableMetaData} + metaData={props.metaData} criteria={criteria} booleanOperator={booleanOperator} updateCriteria={(newCriteria, needDebounce) => updateCriteria(newCriteria, index, needDebounce)} diff --git a/src/qqq/components/query/FilterCriteriaPaster.tsx b/src/qqq/components/query/FilterCriteriaPaster.tsx index b594d02..482332e 100644 --- a/src/qqq/components/query/FilterCriteriaPaster.tsx +++ b/src/qqq/components/query/FilterCriteriaPaster.tsx @@ -36,11 +36,12 @@ import ChipTextField from "qqq/components/forms/ChipTextField"; interface Props { type: string; + onSave: (newValues: any[]) => void; } FilterCriteriaPaster.defaultProps = {}; -function FilterCriteriaPaster({type}: Props): JSX.Element +function FilterCriteriaPaster({type, onSave}: Props): JSX.Element { enum Delimiter { @@ -86,14 +87,6 @@ function FilterCriteriaPaster({type}: Props): JSX.Element setPasteModalIsOpen(true); }; - const applyValue = (item: GridFilterItem) => - { - console.log(`updating grid values: ${JSON.stringify(item.value)}`); - // todo! - // setGridFilterItem(item); - // props.applyValue(item); - }; - const clearData = () => { setDelimiter(""); @@ -113,34 +106,19 @@ function FilterCriteriaPaster({type}: Props): JSX.Element const handleSaveClicked = () => { - //x if (gridFilterItem) - /* todo + //////////////////////////////////////// + // if numeric remove any non-numerics // + //////////////////////////////////////// + let saveData = []; + for (let i = 0; i < chipData.length; i++) { - //////////////////////////////////////// - // if numeric remove any non-numerics // - //////////////////////////////////////// - let saveData = []; - for (let i = 0; i < chipData.length; i++) + if (type !== "number" || !Number.isNaN(Number(chipData[i]))) { - if (type !== "number" || !Number.isNaN(Number(chipData[i]))) - { - saveData.push(chipData[i]); - } + saveData.push(chipData[i]); } - - if (gridFilterItem.value) - { - gridFilterItem.value = [...gridFilterItem.value, ...saveData]; - } - else - { - gridFilterItem.value = saveData; - } - - setGridFilterItem(gridFilterItem); - props.applyValue(gridFilterItem); } - */ + + onSave(saveData); clearData(); setPasteModalIsOpen(false); @@ -299,7 +277,7 @@ function FilterCriteriaPaster({type}: Props): JSX.Element return ( - paste_content + paste_content { pasteModalIsOpen && diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index 216b2c9..0e10549 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -20,6 +20,7 @@ */ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType"; +import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; 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"; @@ -57,12 +58,14 @@ export interface OperatorOption valueMode: ValueMode; } +export const getDefaultCriteriaValue = () => [""]; interface FilterCriteriaRowProps { id: number; index: number; tableMetaData: QTableMetaData; + metaData: QInstance; criteria: QFilterCriteria; booleanOperator: "AND" | "OR" | null; updateCriteria: (newCriteria: QFilterCriteria, needDebounce: boolean) => void; @@ -82,11 +85,11 @@ function makeFieldOptionsForTable(tableMetaData: QTableMetaData, fieldOptions: a } } -export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOperator, updateCriteria, removeCriteria, updateBooleanOperator}: FilterCriteriaRowProps): JSX.Element +export function FilterCriteriaRow({id, index, tableMetaData, metaData, 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("") + const [operatorInputValue, setOperatorInputValue] = useState(""); /////////////////////////////////////////////////////////////// // set up the array of options for the fields Autocomplete // @@ -98,12 +101,14 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp 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); + if (metaData.tables.has(exposedJoin.joinTable.name)) + { + fieldsGroupBy = (option: any) => `${option.table.label} fields`; + makeFieldOptionsForTable(exposedJoin.joinTable, fieldOptions, true); + } } } @@ -124,8 +129,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp ////////////////////////////////////////////////////// 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: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE}); + operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, 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}); @@ -138,7 +143,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp 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: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, 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}); @@ -151,8 +156,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp 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: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, 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}); @@ -163,8 +168,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp //? 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: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME}); + operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, 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}); @@ -175,8 +180,8 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp //? 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: "equals yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]}); + operatorOptions.push({label: "equals 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}); /* @@ -266,20 +271,39 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp ////////////////////////////////////////// const handleFieldChange = (event: any, newValue: any, reason: string) => { - criteria.fieldName = newValue ? newValue.fieldName : null; - updateCriteria(criteria, false); + const oldFieldName = criteria.fieldName; - setOperatorOptions(criteria.fieldName) - if(operatorOptions.length) + criteria.fieldName = newValue ? newValue.fieldName : null; + + ////////////////////////////////////////////////////// + // decide if we should clear out the values or not. // + ////////////////////////////////////////////////////// + if (criteria.fieldName == null || isFieldTypeDifferent(oldFieldName, criteria.fieldName)) { - setOperatorSelectedValue(operatorOptions[0]); - setOperatorInputValue(operatorOptions[0].label); + criteria.values = getDefaultCriteriaValue(); + } + + //////////////////////////////////////////////////////////////////// + // update the operator options, and the operator on this criteria // + //////////////////////////////////////////////////////////////////// + setOperatorOptions(criteria.fieldName); + if (operatorOptions.length) + { + if (isFieldTypeDifferent(oldFieldName, criteria.fieldName)) + { + criteria.operator = operatorOptions[0].value; + setOperatorSelectedValue(operatorOptions[0]); + setOperatorInputValue(operatorOptions[0].label); + } } else { + criteria.operator = null; setOperatorSelectedValue(null); setOperatorInputValue(""); } + + updateCriteria(criteria, false); }; ///////////////////////////////////////////// @@ -314,7 +338,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp const handleValueChange = (event: React.ChangeEvent | SyntheticEvent, valueIndex: number | "all" = 0, newValue?: any) => { // @ts-ignore - const value = newValue ? newValue : event ? event.target.value : null; + const value = newValue !== undefined ? newValue : event ? event.target.value : null; if(!criteria.values) { @@ -323,7 +347,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp if(valueIndex == "all") { - criteria.values= value; + criteria.values = value; } else { @@ -333,6 +357,22 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp updateCriteria(criteria, true); }; + const isFieldTypeDifferent = (fieldNameA: string, fieldNameB: string): boolean => + { + const [fieldA] = FilterUtils.getField(tableMetaData, fieldNameA); + const [fieldB] = FilterUtils.getField(tableMetaData, fieldNameB); + if (fieldA?.type !== fieldB.type) + { + return (true); + } + if (fieldA.possibleValueSourceName !== fieldB.possibleValueSourceName) + { + return (true); + } + + return (false); + }; + function isFieldOptionEqual(option: any, value: any) { return option.fieldName === value.fieldName; @@ -465,6 +505,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp renderOption={(props, option, state) => renderFieldOption(props, option, state)} autoSelect={true} autoHighlight={true} + slotProps={{popper: {style: {padding: 0, width: "250px"}}}} /> @@ -481,6 +522,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, criteria, booleanOp getOptionLabel={(option: any) => option.label} autoSelect={true} autoHighlight={true} + slotProps={{popper: {style: {padding: 0, maxHeight: "unset", width: "200px"}}}} /*disabled={criteria.fieldName == null}*/ /> diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx index 67c7642..c8324cb 100644 --- a/src/qqq/components/query/FilterCriteriaRowValues.tsx +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -25,11 +25,16 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import Autocomplete from "@mui/material/Autocomplete"; import Box from "@mui/material/Box"; +import Icon from "@mui/material/Icon"; +import IconButton from "@mui/material/IconButton"; +import InputAdornment from "@mui/material/InputAdornment/InputAdornment"; import TextField from "@mui/material/TextField"; -import React, {SyntheticEvent} from "react"; +import React, {SyntheticEvent, useReducer} from "react"; import DynamicSelect from "qqq/components/forms/DynamicSelect"; import {QFilterCriteriaWithId} from "qqq/components/query/CustomFilterPanel"; +import FilterCriteriaPaster from "qqq/components/query/FilterCriteriaPaster"; import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow"; +import ValueUtils from "qqq/utils/qqq/ValueUtils"; interface Props { @@ -45,31 +50,64 @@ FilterCriteriaRowValues.defaultProps = { function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler}: Props): JSX.Element { - if(!operatorOption) + const [, forceUpdate] = useReducer((x) => x + 1, 0); + + if (!operatorOption) { - return
+ return
; } - const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix="value-") => + const getTypeForTextField = (): string => { - let type = "search" - const inputLabelProps: any = {}; + let type = "search"; - if(field.type == QFieldType.INTEGER) + if (field.type == QFieldType.INTEGER) { type = "number"; } - else if(field.type == QFieldType.DATE) + else if (field.type == QFieldType.DATE) { type = "date"; - inputLabelProps.shrink = true; } - else if(field.type == QFieldType.DATE_TIME) + else if (field.type == QFieldType.DATE_TIME) { type = "datetime-local"; + } + + return (type); + }; + + const makeTextField = (valueIndex: number = 0, label = "Value", idPrefix = "value-") => + { + let type = getTypeForTextField(); + const inputLabelProps: any = {}; + + if (field.type == QFieldType.DATE || field.type == QFieldType.DATE_TIME) + { inputLabelProps.shrink = true; } + let value = criteria.values[valueIndex]; + if (field.type == QFieldType.DATE_TIME && value && String(value).indexOf("Z") > -1) + { + value = ValueUtils.formatDateTimeValueForForm(value); + } + + const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => + { + valueChangeHandler(event, index, ""); + document.getElementById(`${idPrefix}${criteria.id}`).focus(); + }; + + const inputProps: any = {}; + inputProps.endAdornment = ( + + clearValue(event, valueIndex)}> + close + + + ); + return valueChangeHandler(event, valueIndex)} - value={criteria.values[valueIndex]} + value={value} InputLabelProps={inputLabelProps} + InputProps={inputProps} fullWidth - // todo - x to clear value? - /> + />; + }; + + function saveNewPasterValues(newValues: any[]) + { + if (criteria.values) + { + criteria.values = [...criteria.values, ...newValues]; + } + else + { + criteria.values = newValues; + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // we are somehow getting some empty-strings as first-value leaking through. they aren't cool, so, remove them if we find them // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if (criteria.values.length > 0 && criteria.values[0] == "") + { + criteria.values = criteria.values.splice(1); + } + + valueChangeHandler(null, "all", criteria.values); + forceUpdate(); } switch (operatorOption.valueMode) { case ValueMode.NONE: - return
+ return
; case ValueMode.SINGLE: return makeTextField(); case ValueMode.SINGLE_DATE: @@ -100,30 +161,36 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC { makeTextField(0, "From", "from-") }
- { makeTextField(1, "To", "to-") } + {makeTextField(1, "To", "to-")} ; case ValueMode.MULTI: let values = criteria.values; - if(values && values.length == 1 && values[0] == "") + 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)} - /> - // todo - need the Paste button + return + ()} + options={[]} + multiple + freeSolo // todo - no debounce after enter? + selectOnFocus + clearOnBlur + fullWidth + limitTags={5} + value={values} + onChange={(event, value) => valueChangeHandler(event, "all", value)} + /> + + saveNewPasterValues(newValues)} /> + + ; case ValueMode.PVS_SINGLE: + console.log("Doing pvs single: " + criteria.values); let selectedPossibleValue = null; - if(criteria.values && criteria.values.length > 0) + if (criteria.values && criteria.values.length > 0) { selectedPossibleValue = criteria.values[0]; } @@ -131,22 +198,38 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC valueChangeHandler(null, 0, value)} /> - + ; case ValueMode.PVS_MULTI: - // todo - values not sticking when re-opening filter panel + console.log("Doing pvs multi: " + criteria.values); + let initialValues: any[] = []; + if (criteria.values && criteria.values.length > 0) + { + if (criteria.values.length == 1 && criteria.values[0] == "") + { + // we never want a tag that's just ""... + } + else + { + initialValues = criteria.values; + } + } return valueChangeHandler(null, "all", value)} /> diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 4da63d7..072f177 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -350,7 +350,8 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element ////////////////////////////////////////////////////////////////////////////////////////////////////// const buildQFilter = (tableMetaData: QTableMetaData, filterModel: GridFilterModel, limit?: number) => { - const filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, limit); + let filter = FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, limit); + filter = FilterUtils.convertFilterPossibleValuesToIds(filter); setHasValidFilters(filter.criteria && filter.criteria.length > 0); return (filter); }; @@ -879,11 +880,11 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element console.log(columnOrderChangeParams); }; - const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true) => + const handleFilterChange = (filterModel: GridFilterModel, doSetQueryFilter = true, isChangeFromDataGrid = false) => { setFilterModel(filterModel); - if(doSetQueryFilter) + if (doSetQueryFilter) { ////////////////////////////////////////////////////////////////////////////////// // someone might have already set the query filter, so, only set it if asked to // @@ -891,6 +892,18 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage)); } + if (isChangeFromDataGrid) + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this function is called by our code several times, but also from dataGridPro when its filter model changes. // + // in general, we don't want a "partial" criteria to be part of our query filter object (e.g., w/ no values) // + // BUT - for one use-case, when the user adds a "filter" (criteria) from column-header "..." menu, then dataGridPro // + // puts a partial item in its filter - so - in that case, we do like to get this partial criteria in our QFilter. // + // so far, not seeing any negatives to this being here, and it fixes that user experience, so keep this. // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + setQueryFilter(FilterUtils.buildQFilterFromGridFilter(tableMetaData, filterModel, columnSortModel, rowsPerPage, true)); + } + if (filterLocalStorageKey) { localStorage.setItem(filterLocalStorageKey, JSON.stringify(filterModel)); @@ -1700,7 +1713,6 @@ 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]); @@ -1712,7 +1724,6 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element { setTotalRecords(null); setDistinctRecords(null); - // console.log("calling update table for UE 2"); updateTable(); }, [columnsModel, tableState]); @@ -1722,19 +1733,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element 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]); @@ -1744,12 +1748,12 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element document.scrollingElement.scrollTop = 0; }, [pageNumber, rowsPerPage]); - const updateFilter = (newFilter: QQueryFilter): void => + const updateFilterFromFilterPanel = (newFilter: QQueryFilter): void => { setQueryFilter(newFilter); const gridFilterModel = FilterUtils.buildGridFilterFromQFilter(tableMetaData, queryFilter); handleFilterChange(gridFilterModel, false); - } + }; if (tableMetaData && !tableMetaData.readPermission) { @@ -1813,7 +1817,10 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element } - + { + metaData && metaData.processes.has("querySavedFilter") && + + } @@ -1840,6 +1847,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element columnsPanel: { tableMetaData: tableMetaData, + metaData: metaData, initialOpenedGroups: columnChooserOpenGroups, openGroupsChanger: setColumnChooserOpenGroups, initialFilterText: columnChooserFilterText, @@ -1848,8 +1856,9 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element filterPanel: { tableMetaData: tableMetaData, + metaData: metaData, queryFilter: queryFilter, - updateFilter: updateFilter + updateFilter: updateFilterFromFilterPanel } }} localeText={{ @@ -1880,7 +1889,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element density={density} loading={loading} filterModel={filterModel} - onFilterModelChange={(model) => handleFilterChange(model)} + onFilterModelChange={(model) => handleFilterChange(model, true, true)} 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 ce5a40d..cd3ed0b 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -421,42 +421,50 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } top: -60px !important; } +/* tighten the text in the field select dropdown in custom filters */ .customFilterPanel .MuiAutocomplete-paper { line-height: 1.375; } +/* tighten the text in the field select dropdown in custom filters */ .customFilterPanel .MuiAutocomplete-groupLabel { line-height: 1.75; } +/* taller list box */ .customFilterPanel .MuiAutocomplete-listbox { max-height: 60vh; } +/* shrink down-arrows in custom filters panel */ .customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard, .customFilterPanel .MuiSvgIcon-root { font-size: 14px !important; } +/* fix something in AND/OR dropdown in filters */ .customFilterPanel .booleanOperatorColumn .MuiSvgIcon-root { display: inline-block !important; } +/* adjust bottom of AND/OR dropdown in filters */ .customFilterPanel .booleanOperatorColumn .MuiInputBase-formControl { padding-bottom: calc(0.25rem + 1px); } +/* adjust down-arrow in AND/OR dropdown in filters */ .customFilterPanel .booleanOperatorColumn .MuiSelect-iconStandard { top: calc(50% - 0.75rem); } +/* change tags in any-of value fields to not be black bg with white text */ .customFilterPanel .filterValuesColumn .MuiChip-root { background: none; @@ -464,13 +472,32 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } border: 1px solid gray; } +/* change 'x' icon in tags in any-of value */ .customFilterPanel .filterValuesColumn .MuiChip-root .MuiChip-deleteIcon { color: gray; } +/* change tags in any-of value fields to not be black bg with white text */ .customFilterPanel .filterValuesColumn .MuiAutocomplete-tag { color: #191919; background: none; } + +/* default hover color for the 'x' to remove a tag from an 'any-of' value was white, which made it disappear */ +.customFilterPanel .filterValuesColumn .MuiAutocomplete-tag .MuiSvgIcon-root:hover +{ + color: lightgray; +} + +.DynamicSelectPopper ul +{ + padding: 0; +} + +.DynamicSelectPopper ul li.MuiAutocomplete-option +{ + padding-left: 0.25rem; + padding-right: 0.25rem; +} \ No newline at end of file diff --git a/src/qqq/utils/DataGridUtils.tsx b/src/qqq/utils/DataGridUtils.tsx index f43364f..489b793 100644 --- a/src/qqq/utils/DataGridUtils.tsx +++ b/src/qqq/utils/DataGridUtils.tsx @@ -25,13 +25,42 @@ import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QField import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance"; import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData"; import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord"; -import {getGridDateOperators, GridColDef, GridRowsProp} from "@mui/x-data-grid-pro"; +import {GridColDef, GridFilterItem, GridRowsProp} from "@mui/x-data-grid-pro"; import {GridFilterOperator} from "@mui/x-data-grid/models/gridFilterOperator"; import React from "react"; import {Link} from "react-router-dom"; import {buildQGridPvsOperators, QGridBooleanOperators, QGridNumericOperators, QGridStringOperators} from "qqq/pages/records/query/GridFilterOperators"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; + +const emptyApplyFilterFn = (filterItem: GridFilterItem, column: GridColDef): null => null; + +function NullInputComponent() +{ + return (); +} + +const makeGridFilterOperator = (value: string, label: string, takesValues: boolean = false): GridFilterOperator => +{ + const rs: GridFilterOperator = {value: value, label: label, getApplyFilterFn: emptyApplyFilterFn}; + if (takesValues) + { + rs.InputComponent = NullInputComponent; + } + return (rs); +}; + +const QGridDateOperators = [ + makeGridFilterOperator("equals", "equals", true), + makeGridFilterOperator("isNot", "not equals", true), + makeGridFilterOperator("after", "is after", true), + makeGridFilterOperator("onOrAfter", "is on or after", true), + makeGridFilterOperator("before", "is before", true), + makeGridFilterOperator("onOrBefore", "is on or before", true), + makeGridFilterOperator("isEmpty", "is empty"), + makeGridFilterOperator("isNotEmpty", "is not empty"), +]; + export default class DataGridUtils { @@ -40,7 +69,7 @@ export default class DataGridUtils *******************************************************************************/ public static makeRows = (results: QRecord[], tableMetaData: QTableMetaData): GridRowsProp[] => { - const fields = [ ...tableMetaData.fields.values() ]; + const fields = [...tableMetaData.fields.values()]; const rows = [] as any[]; let rowIndex = 0; results.forEach((record: QRecord) => @@ -188,6 +217,7 @@ export default class DataGridUtils }); } + /******************************************************************************* ** *******************************************************************************/ @@ -220,12 +250,12 @@ export default class DataGridUtils case QFieldType.DATE: columnType = "date"; columnWidth = 100; - filterOperators = getGridDateOperators(); + filterOperators = QGridDateOperators; break; case QFieldType.DATE_TIME: columnType = "dateTime"; columnWidth = 200; - filterOperators = getGridDateOperators(true); + filterOperators = QGridDateOperators; break; case QFieldType.BOOLEAN: columnType = "string"; // using boolean gives an odd 'no' for nulls. diff --git a/src/qqq/utils/qqq/FilterUtils.ts b/src/qqq/utils/qqq/FilterUtils.ts index 7830a8e..4102651 100644 --- a/src/qqq/utils/qqq/FilterUtils.ts +++ b/src/qqq/utils/qqq/FilterUtils.ts @@ -264,10 +264,10 @@ class FilterUtils ///////////////////////////////////////////////////////////////////////////////////////////////// return ([null, null]); } - return (FilterUtils.prepFilterValuesForBackend(value, fieldMetaData)); + return (FilterUtils.cleanseCriteriaValueForQQQ(value, fieldMetaData)); } - return (FilterUtils.prepFilterValuesForBackend([value], fieldMetaData)); + return (FilterUtils.cleanseCriteriaValueForQQQ([value], fieldMetaData)); }; @@ -278,7 +278,7 @@ class FilterUtils ** ** Or, if the values are date-times, convert them to UTC. *******************************************************************************/ - private static prepFilterValuesForBackend = (param: any[], fieldMetaData: QFieldMetaData): number[] | string[] => + private static cleanseCriteriaValueForQQQ = (param: any[], fieldMetaData: QFieldMetaData): number[] | string[] => { if (param === null || param === undefined) { @@ -291,10 +291,15 @@ class FilterUtils console.log(param[i]); if (param[i] && param[i].id && param[i].label) { - ///////////////////////////////////////////////////////////// - // if the param looks like a possible value, return its id // - ///////////////////////////////////////////////////////////// - rs.push(param[i].id); + ////////////////////////////////////////////////////////////////////////////////////////// + // if the param looks like a possible value, return its id // + // during build of new custom filter panel, this ended up causing us // + // problems (because we wanted the full PV object in the filter model for the frontend) // + // so, we can keep the PV as-is here, and see calls to convertFilterPossibleValuesToIds // + // to do what this used to do. // + ////////////////////////////////////////////////////////////////////////////////////////// + // rs.push(param[i].id); + rs.push(param[i]); } else { @@ -464,7 +469,16 @@ class FilterUtils amount = -amount; } + ///////////////////////////////////////////// + // shift the date/time by the input amount // + ///////////////////////////////////////////// value.setTime(value.getTime() + 1000 * amount); + + ///////////////////////////////////////////////// + // now also shift from local-timezone into UTC // + ///////////////////////////////////////////////// + value.setTime(value.getTime() + 1000 * 60 * value.getTimezoneOffset()); + values = [ValueUtils.formatDateTimeISO8601(value)]; } } @@ -598,7 +612,7 @@ class FilterUtils /******************************************************************************* ** build a qqq filter from a grid and column sort model *******************************************************************************/ - public static buildQFilterFromGridFilter(tableMetaData: QTableMetaData, filterModel: GridFilterModel, columnSortModel: GridSortItem[], limit?: number): QQueryFilter + public static buildQFilterFromGridFilter(tableMetaData: QTableMetaData, filterModel: GridFilterModel, columnSortModel: GridSortItem[], limit?: number, allowIncompleteCriteria = false): QQueryFilter { console.log("Building q filter with model:"); console.log(filterModel); @@ -638,13 +652,15 @@ class FilterUtils //////////////////////////////////////////////////////////////////////////////// // if no value set and not 'empty' or 'not empty' operators, skip this filter // //////////////////////////////////////////////////////////////////////////////// - if ((!item.value || item.value.length == 0) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty") + if ((!item.value || item.value.length == 0 || (item.value.length == 1 && item.value[0] == "")) && item.operatorValue !== "isEmpty" && item.operatorValue !== "isNotEmpty") { - return; + if (!allowIncompleteCriteria) + { + return; + } } - var fieldMetadata = tableMetaData?.fields.get(item.columnField); - + const fieldMetadata = tableMetaData?.fields.get(item.columnField); const operator = FilterUtils.gridCriteriaOperatorToQQQ(item.operatorValue); const values = FilterUtils.gridCriteriaValueToQQQ(operator, item.value, item.operatorValue, fieldMetadata); qFilter.addCriteria(new QFilterCriteria(item.columnField, operator, values)); @@ -664,6 +680,33 @@ class FilterUtils return qFilter; }; + + public static convertFilterPossibleValuesToIds(inputFilter: QQueryFilter): QQueryFilter + { + const filter = Object.assign({}, inputFilter); + + if (filter.criteria) + { + for (let i = 0; i < filter.criteria.length; i++) + { + const criteria = filter.criteria[i]; + if (criteria.values) + { + for (let j = 0; j < criteria.values.length; j++) + { + let value = criteria.values[j]; + if (value && value.id && value.label) + { + criteria.values[j] = value.id; + } + } + } + } + } + + return (filter); + } + } export default FilterUtils;