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: