From ab563c1d93cc25c3b07fb8d8c4b5bfab059f565a Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 21 Jul 2023 09:00:34 -0500 Subject: [PATCH 1/8] Improve line height in filter column drop down --- src/qqq/styles/qqq-override-styles.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/qqq/styles/qqq-override-styles.css b/src/qqq/styles/qqq-override-styles.css index 73d6aa5..a48a7c1 100644 --- a/src/qqq/styles/qqq-override-styles.css +++ b/src/qqq/styles/qqq-override-styles.css @@ -433,6 +433,13 @@ input[type="search"]::-webkit-search-results-decoration { display: none; } line-height: 1.75; } +.filterCriteriaRowColumnPopper .MuiAutocomplete-groupLabel +{ + line-height: 1.75; + padding-top: 8px; + padding-bottom: 8px; +} + /* taller list box */ .filterCriteriaRowColumnPopper .MuiAutocomplete-listbox { From 7637d8cd62b608842e39412e1a9b86c8f2df8dcd Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 21 Jul 2023 09:47:50 -0500 Subject: [PATCH 2/8] Auto-focus the text input --- src/qqq/components/query/CustomColumnsPanel.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/qqq/components/query/CustomColumnsPanel.tsx b/src/qqq/components/query/CustomColumnsPanel.tsx index 44874e0..cb20ffe 100644 --- a/src/qqq/components/query/CustomColumnsPanel.tsx +++ b/src/qqq/components/query/CustomColumnsPanel.tsx @@ -31,7 +31,7 @@ import TextField from "@mui/material/TextField"; import {GridColDef, GridSlotsComponentsProps, useGridApiContext, useGridSelector} from "@mui/x-data-grid-pro"; import {GridColumnsPanelProps} from "@mui/x-data-grid/components/panel/GridColumnsPanel"; import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector"; -import React, {createRef, forwardRef, useEffect, useReducer, useState} from "react"; +import React, {createRef, forwardRef, useEffect, useReducer, useRef, useState} from "react"; declare module "@mui/x-data-grid" { @@ -55,6 +55,9 @@ export const CustomColumnsPanel = forwardRef( const [, forceUpdate] = useReducer((x) => x + 1, 0); const someRef = createRef(); + const textRef = useRef(null); + const [didInitialFocus, setDidInitialFocus] = useState(false) + const [openGroups, setOpenGroups] = useState(props.initialOpenedGroups || {}); const openGroupsBecauseOfFilter = {} as { [name: string]: boolean }; const [lastScrollTop, setLastScrollTop] = useState(0); @@ -68,6 +71,15 @@ export const CustomColumnsPanel = forwardRef( console.log(`Open groups: ${JSON.stringify(openGroups)}`); + if(!didInitialFocus) + { + if(textRef.current) + { + textRef.current.select(); + setDidInitialFocus(true); + } + } + if (props.tableMetaData.exposedJoins) { for (let i = 0; i < props.tableMetaData.exposedJoins.length; i++) @@ -360,7 +372,7 @@ export const CustomColumnsPanel = forwardRef( return ( - filterTextChanged(event)} > From 70581f6641613c5d6c9287fd0d29430b15f864f0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jul 2023 08:34:36 -0500 Subject: [PATCH 3/8] fix runtime error checking filter validity, in case criteria.values is null --- src/qqq/components/query/FilterCriteriaRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index 8114c63..cf0ebdb 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -473,7 +473,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, } else { - if(isNotSet(criteria.values[0])) + if(!criteria.values || isNotSet(criteria.values[0])) { criteriaIsValid = false; criteriaStatusTooltip = "You must enter a value to complete the definition of this condition."; From cd7e6b8db171bd20274c3913478b535ceb97034e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jul 2023 08:35:18 -0500 Subject: [PATCH 4/8] Move getOperatorOptions into exported function --- .../components/query/FilterCriteriaRow.tsx | 208 +++++++++--------- 1 file changed, 105 insertions(+), 103 deletions(-) diff --git a/src/qqq/components/query/FilterCriteriaRow.tsx b/src/qqq/components/query/FilterCriteriaRow.tsx index cf0ebdb..364d52f 100644 --- a/src/qqq/components/query/FilterCriteriaRow.tsx +++ b/src/qqq/components/query/FilterCriteriaRow.tsx @@ -56,12 +56,114 @@ export interface OperatorOption { label: string; value: QCriteriaOperator; - implicitValues?: [any]; + implicitValues?: any[]; valueMode: ValueMode; } export const getDefaultCriteriaValue = () => [""]; +export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: string): OperatorOption[] => +{ + const [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName); + let operatorOptions = []; + if (field && fieldTable) + { + ////////////////////////////////////////////////////// + // setup array of options for operator Autocomplete // + ////////////////////////////////////////////////////// + if (field.possibleValueSourceName) + { + 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}); + 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: "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}); + 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: "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 on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE}); + operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, 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, valueMode: ValueMode.DOUBLE_DATE}); + operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE}); + //? 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: "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 at 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 at 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, valueMode: ValueMode.DOUBLE_DATE_TIME}); + operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME}); + //? 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: "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}); + /* + ? 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}); + } + } + } + + return (operatorOptions); +} + + interface FilterCriteriaRowProps { id: number; @@ -120,105 +222,6 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, //////////////////////////////////////////////////////////// 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: "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}); - 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: "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}); - 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: "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 on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE}); - operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE}); - operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, 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, valueMode: ValueMode.DOUBLE_DATE}); - operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE}); - //? 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: "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 at 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 at 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, valueMode: ValueMode.DOUBLE_DATE_TIME}); - operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME}); - //? 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: "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}); - /* - ? 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 // //////////////////////////////////////////////////////////////// @@ -240,8 +243,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, defaultFieldValue = {field: field, table: fieldTable, fieldName: criteria.fieldName}; } - setOperatorOptions(criteria.fieldName); - + operatorOptions = getOperatorOptions(tableMetaData, criteria.fieldName); let newOperatorSelectedValue = operatorOptions.filter(option => { @@ -294,7 +296,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria, //////////////////////////////////////////////////////////////////// // update the operator options, and the operator on this criteria // //////////////////////////////////////////////////////////////////// - setOperatorOptions(criteria.fieldName); + operatorOptions = getOperatorOptions(tableMetaData, criteria.fieldName); if (operatorOptions.length) { if (isFieldTypeDifferent(oldFieldName, criteria.fieldName)) From 9c51b3949eaed5279887cd995cb05bf91d185d9e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jul 2023 14:10:43 -0500 Subject: [PATCH 5/8] CE-551 Add keyboard shortcuts to Record Query screen; fix to not take keyboard commands while keyboard-help screen is up --- src/App.tsx | 3 ++ src/CommandMenu.tsx | 11 +++- src/QContext.tsx | 4 ++ src/qqq/pages/records/query/RecordQuery.tsx | 58 ++++++++++++++++++++- src/qqq/pages/records/view/RecordView.tsx | 10 ++-- 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 59ee713..c9995f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -574,6 +574,7 @@ export default function App() const [tableMetaData, setTableMetaData] = useState(null); const [tableProcesses, setTableProcesses] = useState(null); const [dotMenuOpen, setDotMenuOpen] = useState(false); + const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false); return ( appRoutes && ( @@ -583,11 +584,13 @@ export default function App() tableMetaData: tableMetaData, tableProcesses: tableProcesses, dotMenuOpen: dotMenuOpen, + keyboardHelpOpen: keyboardHelpOpen, setPageHeader: (header: string | JSX.Element) => setPageHeader(header), setAccentColor: (accentColor: string) => setAccentColor(accentColor), setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData), setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses), setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent), + setKeyboardHelpOpen: (keyboardHelpOpen: boolean) => setKeyboardHelpOpen(keyboardHelpOpen), pathToLabelMap: pathToLabelMap, branding: branding }}> diff --git a/src/CommandMenu.tsx b/src/CommandMenu.tsx index 6d672f2..f368813 100644 --- a/src/CommandMenu.tsx +++ b/src/CommandMenu.tsx @@ -67,9 +67,8 @@ const CommandMenu = ({metaData}: Props) => const navigate = useNavigate(); const pathParts = location.pathname.replace(/\/+$/, "").split("/"); - const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, setTableMetaData, tableProcesses} = useContext(QContext); + const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses} = useContext(QContext); - const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false) const classes = useStyles(); function evalueKeyPress(e: KeyboardEvent) @@ -351,6 +350,14 @@ const CommandMenu = ({metaData}: Props) => ?Open Keyboard Shortcuts Help + Table Query + + nCreate a New Record + rRefresh the Query + cOpen the Columns Panel + fOpen the Filter Panel + + Record View nCreate a New Record diff --git a/src/QContext.tsx b/src/QContext.tsx index 2cd7ca5..b13c8e5 100644 --- a/src/QContext.tsx +++ b/src/QContext.tsx @@ -37,6 +37,9 @@ interface QContext dotMenuOpen: boolean; setDotMenuOpen?: (dotMenuOpen: boolean) => void; + keyboardHelpOpen: boolean; + setKeyboardHelpOpen?: (keyboardHelpOpen: boolean) => void; + tableMetaData?: QTableMetaData; setTableMetaData?: (tableMetaData: QTableMetaData) => void; @@ -54,6 +57,7 @@ const defaultState = { pageHeader: "", accentColor: "#0062FF", dotMenuOpen: false, + keyboardHelpOpen: false, pathToLabelMap: {}, }; diff --git a/src/qqq/pages/records/query/RecordQuery.tsx b/src/qqq/pages/records/query/RecordQuery.tsx index 0a121f8..9ebef6d 100644 --- a/src/qqq/pages/records/query/RecordQuery.tsx +++ b/src/qqq/pages/records/query/RecordQuery.tsx @@ -51,7 +51,7 @@ import MenuItem from "@mui/material/MenuItem"; import Modal from "@mui/material/Modal"; import TextField from "@mui/material/TextField"; import Tooltip from "@mui/material/Tooltip"; -import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro"; +import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue} from "@mui/x-data-grid-pro"; import {GridRowModel} from "@mui/x-data-grid/models/gridRows"; import FormData from "form-data"; import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react"; @@ -252,12 +252,65 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element const [queryErrors, setQueryErrors] = useState({} as any); const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date()); - const {setPageHeader} = useContext(QContext); + const {setPageHeader, dotMenuOpen, keyboardHelpOpen} = useContext(QContext); const [, forceUpdate] = useReducer((x) => x + 1, 0); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const closeActionsMenu = () => setActionsMenu(null); + const gridApiRef = useGridApiRef(); + + /////////////////////// + // Keyboard handling // + /////////////////////// + useEffect(() => + { + if(tableMetaData == null) + { + (async() => + { + const tableMetaData = await qController.loadTableMetaData(tableName); + setTableMetaData(tableMetaData); + })(); + } + + const down = (e: KeyboardEvent) => + { + const type = (e.target as any).type; + const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search"); + + if(validType && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess) + { + if (! e.metaKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) + { + e.preventDefault() + navigate(`${metaData?.getTablePathByName(tableName)}/create`); + } + else if (! e.metaKey && e.key === "r") + { + e.preventDefault() + updateTable(); + } + else if (! e.metaKey && e.key === "c") + { + e.preventDefault() + gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns) + } + else if (! e.metaKey && e.key === "f") + { + e.preventDefault() + gridApiRef.current.showFilterPanel() + } + } + } + + document.addEventListener("keydown", down) + return () => + { + document.removeEventListener("keydown", down) + } + }, [dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess]) + ///////////////////////////////////////////////////////////////////////////////////////// // monitor location changes - if our url looks like a process, then open that process. // ///////////////////////////////////////////////////////////////////////////////////////// @@ -1934,6 +1987,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element setActionsMenu(event.currentTarget); const closeActionsMenu = () => setActionsMenu(null); - const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen} = useContext(QContext); + const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen} = useContext(QContext); if (localStorage.getItem(tableVariantLocalStorageKey)) { @@ -138,7 +138,9 @@ function RecordView({table, launchProcess}: Props): JSX.Element setShowAudit(false); }; - // Toggle the menu when ⌘K is pressed + /////////////////////// + // Keyboard handling // + /////////////////////// useEffect(() => { if(tableMetaData == null) @@ -155,7 +157,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element const type = (e.target as any).type; const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search"); - if(validType && !dotMenuOpen && !showAudit && !showEditChildForm) + if(validType && !dotMenuOpen && !keyboardHelpOpen && !showAudit && !showEditChildForm) { if (! e.metaKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) { @@ -190,7 +192,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element { document.removeEventListener("keydown", down) } - }, [dotMenuOpen, showEditChildForm, showAudit, metaData]) + }, [dotMenuOpen, keyboardHelpOpen, showEditChildForm, showAudit, metaData]) const gotoCreate = () => { From 4984ddbf7353d87dbb39b275ad613c622b561446 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jul 2023 14:14:27 -0500 Subject: [PATCH 6/8] CE-551 Implement default values from fieldMetaData for record create; scroll to error; add errors to possible-value (DynamicSelect); --- src/qqq/components/forms/DynamicFormField.tsx | 2 +- src/qqq/components/forms/DynamicFormUtils.ts | 13 +- src/qqq/components/forms/DynamicSelect.tsx | 144 +++++++++--------- src/qqq/components/forms/EntityForm.tsx | 79 +++++++--- 4 files changed, 148 insertions(+), 90 deletions(-) diff --git a/src/qqq/components/forms/DynamicFormField.tsx b/src/qqq/components/forms/DynamicFormField.tsx index e2fb6f8..4a80caa 100644 --- a/src/qqq/components/forms/DynamicFormField.tsx +++ b/src/qqq/components/forms/DynamicFormField.tsx @@ -135,7 +135,7 @@ function QDynamicFormField({ /> - {!isDisabled &&
} + {!isDisabled &&
{msg}} />
}
diff --git a/src/qqq/components/forms/DynamicFormUtils.ts b/src/qqq/components/forms/DynamicFormUtils.ts index 02f3d85..59c060e 100644 --- a/src/qqq/components/forms/DynamicFormUtils.ts +++ b/src/qqq/components/forms/DynamicFormUtils.ts @@ -104,7 +104,18 @@ class DynamicFormUtils { if (field.isRequired) { - return (Yup.string().required(`${field.label} is required.`)); + if(field.possibleValueSourceName) + { + //////////////////////////////////////////////////////////////////////////////////////////// + // the "nullable(true)" here doesn't mean that you're allowed to set the field to null... // + // rather, it's more like "null is how empty will be treated" or some-such... // + //////////////////////////////////////////////////////////////////////////////////////////// + return (Yup.string().required(`${field.label} is required.`).nullable(true)); + } + else + { + return (Yup.string().required(`${field.label} is required.`)); + } } return (null); } diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index ac558c8..a11a2b3 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -27,8 +27,9 @@ import Autocomplete from "@mui/material/Autocomplete"; import Box from "@mui/material/Box"; import Switch from "@mui/material/Switch"; import TextField from "@mui/material/TextField"; -import {useFormikContext} from "formik"; +import {ErrorMessage, useFormikContext} from "formik"; import React, {useEffect, useState} from "react"; +import MDTypography from "qqq/components/legacy/MDTypography"; import Client from "qqq/utils/qqq/Client"; interface Props @@ -246,78 +247,85 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe // console.log(`default value: ${JSON.stringify(defaultValue)}`); const autocomplete = ( - - { - setOpen(true); - // console.log("setting open..."); - if(options.length == 0) + + { - // console.log("no options yet, so setting search term to ''..."); - setSearchTerm(""); - } - }} - onClose={() => - { - setOpen(false); - }} - isOptionEqualToValue={(option, value) => option.id === value.id} - getOptionLabel={(option) => - { - // @ts-ignore - if(option && option.length) + setOpen(true); + // console.log("setting open..."); + if(options.length == 0) + { + // console.log("no options yet, so setting search term to ''..."); + setSearchTerm(""); + } + }} + onClose={() => + { + setOpen(false); + }} + isOptionEqualToValue={(option, value) => option.id === value.id} + getOptionLabel={(option) => { // @ts-ignore - option = option[0]; - } + if(option && option.length) + { + // @ts-ignore + option = option[0]; + } + // @ts-ignore + return option.label + }} + options={options} + loading={loading} + onInputChange={inputChanged} + onBlur={handleBlur} + defaultValue={defaultValue} // @ts-ignore - return option.label - }} - options={options} - loading={loading} - onInputChange={inputChanged} - onBlur={handleBlur} - defaultValue={defaultValue} - // @ts-ignore - onChange={handleChanged} - noOptionsText={"No matches found"} - onKeyPress={e => - { - if (e.key === "Enter") + onChange={handleChanged} + noOptionsText={"No matches found"} + onKeyPress={e => { - e.preventDefault(); - } - }} - renderOption={renderOption} - filterOptions={filterOptions} - disabled={isDisabled} - multiple={isMultiple} - disableCloseOnSelect={isMultiple} - limitTags={5} - slotProps={{popper: {className: "DynamicSelectPopper"}}} - renderInput={(params) => ( - - {loading ? : null} - {params.InputProps.endAdornment} - - ), - }} - /> - )} - /> + if (e.key === "Enter") + { + e.preventDefault(); + } + }} + renderOption={renderOption} + filterOptions={filterOptions} + disabled={isDisabled} + multiple={isMultiple} + disableCloseOnSelect={isMultiple} + limitTags={5} + slotProps={{popper: {className: "DynamicSelectPopper"}}} + renderInput={(params) => ( + + {loading ? : null} + {params.InputProps.endAdornment} + + ), + }} + /> + )} + /> + + + {!isDisabled &&
{msg}} />
} +
+
+
); diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 1d3c2ef..9d1761a 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -32,8 +32,8 @@ import Box from "@mui/material/Box"; import Card from "@mui/material/Card"; import Grid from "@mui/material/Grid"; import Icon from "@mui/material/Icon"; -import {Form, Formik} from "formik"; -import React, {useContext, useReducer, useState} from "react"; +import {Form, Formik, useFormikContext} from "formik"; +import React, {useContext, useEffect, useReducer, useState} from "react"; import {useLocation, useNavigate, useParams} from "react-router-dom"; import * as Yup from "yup"; import QContext from "QContext"; @@ -77,7 +77,7 @@ function EntityForm(props: Props): JSX.Element const [formTitle, setFormTitle] = useState(""); const [validations, setValidations] = useState({}); - const [initialValues, setInitialValues] = useState({} as { [key: string]: string }); + const [initialValues, setInitialValues] = useState({} as { [key: string]: any }); const [formFields, setFormFields] = useState(null as Map); const [t1sectionName, setT1SectionName] = useState(null as string); const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]); @@ -233,27 +233,26 @@ function EntityForm(props: Props): JSX.Element //////////////////////////////////////////////////////////////////////////////////////////////// // if default values were supplied for a new record, then populate initialValues, for formik. // //////////////////////////////////////////////////////////////////////////////////////////////// - if(defaultValues) + for (let i = 0; i < fieldArray.length; i++) { - for (let i = 0; i < fieldArray.length; i++) + const fieldMetaData = fieldArray[i]; + const fieldName = fieldMetaData.name; + const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue; + if (defaultValue) { - const fieldMetaData = fieldArray[i]; - const fieldName = fieldMetaData.name; - if (defaultValues[fieldName]) - { - initialValues[fieldName] = defaultValues[fieldName]; + initialValues[fieldName] = defaultValue; - /////////////////////////////////////////////////////////////////////////////////////////// - // we need to set the initialDisplayValue for possible value fields with a default value // - // so, look them up here now if needed // - /////////////////////////////////////////////////////////////////////////////////////////// - if (fieldMetaData.possibleValueSourceName) + /////////////////////////////////////////////////////////////////////////////////////////// + // we need to set the initialDisplayValue for possible value fields with a default value // + // so, look them up here now if needed // + /////////////////////////////////////////////////////////////////////////////////////////// + if (fieldMetaData.possibleValueSourceName) + { + const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); + if (results && results.length > 0) { - const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); - if (results && results.length > 0) - { - defaultDisplayValues.set(fieldName, results[0].label); - } + defaultDisplayValues.set(fieldName, results[0].label); + initialValues[fieldName] = {id: defaultValue, value: results[0].label} } } } @@ -598,6 +597,7 @@ function EntityForm(props: Props): JSX.Element isSubmitting, }) => (
+ @@ -672,4 +672,43 @@ function EntityForm(props: Props): JSX.Element } } +function ScrollToFirstError(): JSX.Element +{ + const {submitCount, isValid} = useFormikContext() + + useEffect(() => + { + ///////////////////////////////////////////////////////////////////////////// + // Wrap the code in setTimeout to make sure it runs after the DOM has been // + // updated and has the error message elements. // + ///////////////////////////////////////////////////////////////////////////// + setTimeout(() => + { + //////////////////////////////////////// + // Only run on submit or if not valid // + //////////////////////////////////////// + if (submitCount === 0 || isValid) + { + return; + } + + ////////////////////////////////// + // Find the first error message // + ////////////////////////////////// + const errorMessageSelector = "[data-field-error]"; + const firstErrorMessage = document.querySelector(errorMessageSelector); + if (!firstErrorMessage) + { + console.warn(`Form failed validation but no error field was found with selector: ${errorMessageSelector}`); + return; + } + firstErrorMessage.scrollIntoView({block: "center"}); + + }, 100) + }, [submitCount, isValid]) + + return null; +} + + export default EntityForm; From 555e50689a3df26a5988d80525eb7b5ea3152ccf Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jul 2023 15:41:26 -0500 Subject: [PATCH 7/8] CE-551 Fix initialValue for possible values to do validation right --- src/qqq/components/forms/EntityForm.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/qqq/components/forms/EntityForm.tsx b/src/qqq/components/forms/EntityForm.tsx index 9d1761a..ffc4853 100644 --- a/src/qqq/components/forms/EntityForm.tsx +++ b/src/qqq/components/forms/EntityForm.tsx @@ -252,7 +252,6 @@ function EntityForm(props: Props): JSX.Element if (results && results.length > 0) { defaultDisplayValues.set(fieldName, results[0].label); - initialValues[fieldName] = {id: defaultValue, value: results[0].label} } } } From ee7d224ba20ef61ae6677f2ebb5f5444514d4f94 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 25 Jul 2023 16:15:45 -0500 Subject: [PATCH 8/8] CE-551 Fix, only do formik ErrorMessage when inForm --- src/qqq/components/forms/DynamicSelect.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/qqq/components/forms/DynamicSelect.tsx b/src/qqq/components/forms/DynamicSelect.tsx index a11a2b3..2f375f3 100644 --- a/src/qqq/components/forms/DynamicSelect.tsx +++ b/src/qqq/components/forms/DynamicSelect.tsx @@ -320,11 +320,14 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe /> )} /> - - - {!isDisabled &&
{msg}} />
} -
-
+ { + inForm && + + + {!isDisabled &&
{msg}} />
} +
+
+ }
);