diff --git a/package.json b/package.json index feb4fdd..d0385dd 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "@auth0/auth0-react": "1.10.2", "@emotion/react": "11.7.1", "@emotion/styled": "11.6.0", - "@kingsrook/qqq-frontend-core": "1.0.102", + "@kingsrook/qqq-frontend-core": "1.0.103", "@mui/icons-material": "5.4.1", "@mui/material": "5.11.1", "@mui/styles": "5.11.1", diff --git a/src/qqq/components/forms/DynamicFormField.tsx b/src/qqq/components/forms/DynamicFormField.tsx index 1869e84..6930b66 100644 --- a/src/qqq/components/forms/DynamicFormField.tsx +++ b/src/qqq/components/forms/DynamicFormField.tsx @@ -19,16 +19,17 @@ * along with this program. If not, see . */ -import {InputAdornment, InputLabel} from "@mui/material"; -import Box from "@mui/material/Box"; +import {Box, InputAdornment, InputLabel} from "@mui/material"; import Switch from "@mui/material/Switch"; import {ErrorMessage, Field, useFormikContext} from "formik"; -import React, {useState} from "react"; +import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; +import React, {useMemo, useState} from "react"; import AceEditor from "react-ace"; import colors from "qqq/assets/theme/base/colors"; import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch"; import MDInput from "qqq/components/legacy/MDInput"; import MDTypography from "qqq/components/legacy/MDTypography"; +import {flushSync} from "react-dom"; // Declaring props types for FormField interface Props @@ -85,6 +86,51 @@ function QDynamicFormField({ } }; + /////////////////////////////////////////////////////////////////////////////////////// + // check the field meta data for behavior that says to do toUpperCase or toLowerCase // + /////////////////////////////////////////////////////////////////////////////////////// + let isToUpperCase = useMemo(() => DynamicFormUtils.isToUpperCase(formFieldObject?.fieldMetaData), [formFieldObject]); + let isToLowerCase = useMemo(() => DynamicFormUtils.isToLowerCase(formFieldObject?.fieldMetaData), [formFieldObject]); + + //////////////////////////////////////////////////////////////////////// + // if the field has a toUpperCase or toLowerCase behavior on it, then // + // apply that rule. But also, to avoid the cursor always jumping to // + // the end of the input, do some manipulation of the selection. // + // See: https://giacomocerquone.com/blog/keep-input-cursor-still // + // Note, we only want an onChange handle if we're doing one of these // + // behaviors, (because teh flushSync is potentially slow). hence, we // + // put the onChange in an object and assign it with a spread // + //////////////////////////////////////////////////////////////////////// + let onChange: any = {}; + if (isToUpperCase || isToLowerCase) + { + onChange.onChange = (e: any) => + { + const beforeStart = e.target.selectionStart; + const beforeEnd = e.target.selectionEnd; + + flushSync(() => + { + let newValue = e.currentTarget.value; + if (isToUpperCase) + { + newValue = newValue.toUpperCase(); + } + if (isToLowerCase) + { + newValue = newValue.toLowerCase(); + } + setFieldValue(name, newValue); + }); + + const input = document.getElementById(name) as HTMLInputElement; + if (input) + { + input.setSelectionRange(beforeStart, beforeEnd); + } + }; + } + let field; let getsBulkEditHtmlLabel = true; if (type === "checkbox") @@ -102,7 +148,7 @@ function QDynamicFormField({ else if (type === "ace") { let mode = "text"; - if(formFieldObject && formFieldObject.languageMode) + if (formFieldObject && formFieldObject.languageMode) { mode = formFieldObject.languageMode; } @@ -133,7 +179,7 @@ function QDynamicFormField({ { field = ( <> - { if (e.key === "Enter") @@ -173,7 +219,8 @@ function QDynamicFormField({ id={`bulkEditSwitch-${name}`} checked={switchChecked} onClick={bulkEditSwitchChanged} - sx={{top: "-4px", + sx={{ + top: "-4px", "& .MuiSwitch-track": { height: 20, borderRadius: 10, diff --git a/src/qqq/components/forms/DynamicFormUtils.ts b/src/qqq/components/forms/DynamicFormUtils.ts index e3ce923..71c1f03 100644 --- a/src/qqq/components/forms/DynamicFormUtils.ts +++ b/src/qqq/components/forms/DynamicFormUtils.ts @@ -176,7 +176,7 @@ class DynamicFormUtils initialDisplayValue: initialDisplayValue, }; } - else if(processName) + else if (processName) { dynamicFormFields[field.name].possibleValueProps = { @@ -214,7 +214,7 @@ class DynamicFormUtils if (Array.isArray(disabledFields)) { - return (disabledFields.indexOf(fieldName) > -1) + return (disabledFields.indexOf(fieldName) > -1); } else { @@ -222,6 +222,44 @@ class DynamicFormUtils } } + + /*************************************************************************** + * check if a field has the TO_UPPER_CASE behavior on it. + ***************************************************************************/ + public static isToUpperCase(fieldMetaData: QFieldMetaData): boolean + { + return this.hasFieldBehavior(fieldMetaData, "TO_UPPER_CASE"); + } + + + /*************************************************************************** + * check if a field has the TO_LOWER_CASE behavior on it. + ***************************************************************************/ + public static isToLowerCase(fieldMetaData: QFieldMetaData): boolean + { + return this.hasFieldBehavior(fieldMetaData, "TO_LOWER_CASE"); + } + + + /*************************************************************************** + * check if a field has a specific behavior name on it. + ***************************************************************************/ + private static hasFieldBehavior(fieldMetaData: QFieldMetaData, behaviorName: string): boolean + { + if (fieldMetaData && fieldMetaData.fieldBehaviors) + { + for (let i = 0; i < fieldMetaData.fieldBehaviors.length; i++) + { + if (fieldMetaData.fieldBehaviors[i] == behaviorName) + { + return (true); + } + } + } + + return (false); + } + } export default DynamicFormUtils; diff --git a/src/qqq/components/query/FilterCriteriaRowValues.tsx b/src/qqq/components/query/FilterCriteriaRowValues.tsx index 4887048..15d8994 100644 --- a/src/qqq/components/query/FilterCriteriaRowValues.tsx +++ b/src/qqq/components/query/FilterCriteriaRowValues.tsx @@ -30,6 +30,7 @@ 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 DynamicFormUtils from "qqq/components/forms/DynamicFormUtils"; import DynamicSelect from "qqq/components/forms/DynamicSelect"; import AssignFilterVariable from "qqq/components/query/AssignFilterVariable"; import CriteriaDateField, {NoWrapTooltip} from "qqq/components/query/CriteriaDateField"; @@ -40,6 +41,7 @@ import {OperatorOption, ValueMode} from "qqq/components/query/FilterCriteriaRow" import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery"; import ValueUtils from "qqq/utils/qqq/ValueUtils"; import React, {SyntheticEvent, useReducer} from "react"; +import {flushSync} from "react-dom"; interface Props { @@ -58,6 +60,10 @@ FilterCriteriaRowValues.defaultProps = initiallyOpenMultiValuePvs: false }; + +/*************************************************************************** + * get the type to use for an from a QFieldMetaData + ***************************************************************************/ export const getTypeForTextField = (field: QFieldMetaData): string => { let type = "search"; @@ -78,10 +84,15 @@ export const getTypeForTextField = (field: QFieldMetaData): string => return (type); }; + +/*************************************************************************** + * Make an (actually, might be a different type, but that's + * the gist of it), for a field. + ***************************************************************************/ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWithId, valueChangeHandler?: (event: (React.ChangeEvent | React.SyntheticEvent), valueIndex?: (number | "all"), newValue?: any) => void, valueIndex: number = 0, label = "Value", idPrefix = "value-", allowVariables = false) => { const isExpression = criteria.values && criteria.values[valueIndex] && criteria.values[valueIndex].type; - + const inputId = `${idPrefix}${criteria.id}`; let type = getTypeForTextField(field); const inputLabelProps: any = {}; @@ -96,10 +107,13 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi value = ValueUtils.formatDateTimeValueForForm(value); } + /*************************************************************************** + * Event handler for the clear 'x'. + ***************************************************************************/ const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => { valueChangeHandler(event, index, ""); - document.getElementById(`${idPrefix}${criteria.id}`).focus(); + document.getElementById(inputId).focus(); }; @@ -120,6 +134,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi }; + /*************************************************************************** + * make a version of the text field for when the criteria's value is set to + * be a "variable" + ***************************************************************************/ const makeFilterVariableTextField = (expression: FilterVariableExpression, valueIndex: number = 0, label = "Value", idPrefix = "value-") => { const clearValue = (event: React.MouseEvent | React.MouseEvent, index: number) => @@ -149,6 +167,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi />; }; + + /////////////////////////////////////////////////////////////////////////// + // set up an 'x' icon as an end-adornment, to clear value from the field // + /////////////////////////////////////////////////////////////////////////// const inputProps: any = {}; inputProps.endAdornment = ( @@ -158,18 +180,61 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi ); + + /*************************************************************************** + * onChange event handler. deals with, if the field has a to upper/lower + * case rule on it, to apply that transform, and adjust the cursor. + * See: https://giacomocerquone.com/blog/keep-input-cursor-still + ***************************************************************************/ + function onChange(event: any) + { + const beforeStart = event.target.selectionStart; + const beforeEnd = event.target.selectionEnd; + + flushSync(() => + { + let newValue = event.currentTarget.value; + + let isToUpperCase = DynamicFormUtils.isToUpperCase(field); + let isToLowerCase = DynamicFormUtils.isToLowerCase(field); + + if (isToUpperCase) + { + newValue = newValue.toUpperCase(); + } + if (isToLowerCase) + { + newValue = newValue.toLowerCase(); + } + + event.currentTarget.value = newValue; + }); + + const input = document.getElementById(inputId); + if (input) + { + // @ts-ignore + input.setSelectionRange(beforeStart, beforeEnd); + } + + valueChangeHandler(event, valueIndex); + } + + //////////////////////// + // return the element // + //////////////////////// return { isExpression ? ( makeFilterVariableTextField(criteria.values[valueIndex], valueIndex, label, idPrefix) ) : ( valueChangeHandler(event, valueIndex)} + onChange={onChange} onKeyDown={handleKeyDown} value={value} InputLabelProps={inputLabelProps} @@ -188,6 +253,10 @@ export const makeTextField = (field: QFieldMetaData, criteria: QFilterCriteriaWi }; +/*************************************************************************** + * Component that is the "values" portion of a FilterCriteria Row in the + * advanced query filter editor. + ***************************************************************************/ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueChangeHandler, initiallyOpenMultiValuePvs, queryScreenUsage, allowVariables}: Props): JSX.Element { const [, forceUpdate] = useReducer((x) => x + 1, 0); @@ -197,6 +266,10 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC return null; } + + /*************************************************************************** + * Callback for the Save button from the paste-values modal + ***************************************************************************/ function saveNewPasterValues(newValues: any[]) { if (criteria.values) @@ -222,6 +295,9 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC const isExpression = criteria.values && criteria.values[0] && criteria.values[0].type; + ////////////////////////////////////////////////////////////////////////////// + // render different form element9s) based on operator option's "value mode" // + ////////////////////////////////////////////////////////////////////////////// switch (operatorOption.valueMode) { case ValueMode.NONE: