Merge branch 'feature/search-possible-values-by-label' into integration

This commit is contained in:
Tim Chamberlain
2025-05-27 15:51:48 -05:00
12 changed files with 4499 additions and 19724 deletions

23618
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.119", "@kingsrook/qqq-frontend-core": "1.0.122",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",
@ -35,6 +35,7 @@
"html-react-parser": "1.4.8", "html-react-parser": "1.4.8",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"http-proxy-middleware": "2.0.6", "http-proxy-middleware": "2.0.6",
"lodash": "4.17.21",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"oidc-client-ts": "2.4.1", "oidc-client-ts": "2.4.1",
"react-oidc-context": "2.3.1", "react-oidc-context": "2.3.1",

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<revision>0.25.0-SNAPSHOT</revision> <revision>0.26.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

View File

@ -23,8 +23,10 @@ import {Chip} from "@mui/material";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {makeStyles} from "@mui/styles"; import {makeStyles} from "@mui/styles";
import Downshift from "downshift"; import Downshift from "downshift";
import {debounce} from "lodash";
import {arrayOf, func, string} from "prop-types"; import {arrayOf, func, string} from "prop-types";
import React, {useEffect, useState} from "react"; import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useRef, useState} from "react";
const useStyles = makeStyles((theme: any) => ({ const useStyles = makeStyles((theme: any) => ({
chip: { chip: {
@ -34,21 +36,99 @@ const useStyles = makeStyles((theme: any) => ({
function ChipTextField({...props}) function ChipTextField({...props})
{ {
const qController = Client.getInstance();
const classes = useStyles(); const classes = useStyles();
const {handleChipChange, label, chipType, disabled, placeholder, chipData, multiline, rows, ...other} = props; const {table, field, handleChipChange, label, chipType, disabled, placeholder, chipData, multiline, rows, ...other} = props;
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [chips, setChips] = useState([]); const [chips, setChips] = useState([]);
const [chipColors, setChipColors] = useState([]);
const [chipValidity, setChipValidity] = useState([] as boolean[]);
const [chipPVSIds, setChipPVSIds] = useState([] as any[]);
const [isMakingRequest, setIsMakingRequest] = useState(false);
////////////////////////////////////////////////////////////////////
// these refs are used for the async api call for possible values //
////////////////////////////////////////////////////////////////////
const chipsRef = useRef<string[]>([]);
/////////////////////////////////////////////////////////////////////////////////////////////
// use debounce library to not flood server as user types, wait a second before requesting //
/////////////////////////////////////////////////////////////////////////////////////////////
async function fetchPVSLabelsAndColorChips()
{
//////////////////////////////////////////////////////////
// make a request for the possible value labels (chips) //
//////////////////////////////////////////////////////////
setIsMakingRequest(true);
const currentChips = chipsRef.current;
setChipColors([]);
///////////////////////////////////////////////////////////////////////////////
// Determine chip colors based on whether each chip value appears in results //
///////////////////////////////////////////////////////////////////////////////
const newChipColors = [] as string[];
const chipValidity = [] as boolean[];
const chipPVSIds = [] as any[];
////////////////////////////////////////////////////////////////////////////
// make the request for all 'chips' with pagination to handle large sizes //
////////////////////////////////////////////////////////////////////////////
const BATCH_SIZE = 250;
for (let i = 0; i < currentChips.length; i += BATCH_SIZE)
{
const batch = currentChips.slice(i, i + BATCH_SIZE);
const page = await qController.possibleValues(
table.name,
null,
field.name,
"",
null,
batch
);
for (let j = 0; j < batch.length; j++)
{
let found = false;
for (let k = 0; k < page.length; k++)
{
const result = page[k];
if (result.label === batch[j])
{
chipPVSIds.push(result.id);
newChipColors.push("info");
chipValidity.push(true);
found = true;
break;
}
}
if (!found)
{
chipPVSIds.push(null);
chipValidity.push(false);
newChipColors.push("error");
}
}
}
setChipPVSIds(chipPVSIds);
setChipColors(newChipColors);
setChipValidity(chipValidity);
setIsMakingRequest(false);
}
const debouncedApiCall = useRef(debounce(fetchPVSLabelsAndColorChips, 500)).current;
useEffect(() => useEffect(() =>
{ {
setChips(chipData); setChips(chipData);
}, [chipData]); chipsRef.current = chipData;
determineChipColors();
}, [JSON.stringify(chipData)]);
useEffect(() => useEffect(() =>
{ {
handleChipChange(chips); handleChipChange(isMakingRequest, chipValidity, chipPVSIds);
}, [chips, handleChipChange]); }, [chipValidity, chipPVSIds, isMakingRequest]);
function handleKeyDown(event: any) function handleKeyDown(event: any)
{ {
@ -64,13 +144,16 @@ function ChipTextField({...props})
setInputValue(""); setInputValue("");
return; return;
} }
if (!event.target.value.replace(/\s/g, "").length) return; if (!event.target.value.replace(/\s/g, "").length)
{
return;
}
setInputValue("");
newChipList.push(event.target.value.trim()); newChipList.push(event.target.value.trim());
setChips(newChipList); setChips(newChipList);
setInputValue("");
} }
else if (chips.length && !inputValue.length && event.key === "Backspace" ) else if (chips.length && !inputValue.length && event.key === "Backspace")
{ {
setChips(chips.slice(0, chips.length - 1)); setChips(chips.slice(0, chips.length - 1));
} }
@ -87,18 +170,26 @@ function ChipTextField({...props})
setChips(newChipList); setChips(newChipList);
} }
const handleDelete = (item: any) => () =>
{
const newChipList = [...chips];
newChipList.splice(newChipList.indexOf(item), 1);
setChips(newChipList);
};
function handleInputChange(event: { target: { value: React.SetStateAction<string>; }; }) function handleInputChange(event: { target: { value: React.SetStateAction<string>; }; })
{ {
setInputValue(event.target.value); setInputValue(event.target.value);
} }
function determineChipColors(): any
{
if (chipType === "pvs")
{
debouncedApiCall();
}
else
{
const newChipColors = chips.map((chip, i) =>
(chipType !== "number" || !Number.isNaN(Number(chips[i]))) ? "info" : "error"
);
setChipColors(newChipColors);
}
}
return ( return (
<React.Fragment> <React.Fragment>
@ -116,7 +207,7 @@ function ChipTextField({...props})
}); });
// @ts-ignore // @ts-ignore
return ( return (
<div id="chip-text-field-container" style={{flexWrap: "wrap", display:"flex"}}> <div id="chip-text-field-container" style={{flexWrap: "wrap", display: "flex"}}>
<TextField <TextField
sx={{width: "99%"}} sx={{width: "99%"}}
disabled={disabled} disabled={disabled}
@ -125,16 +216,16 @@ function ChipTextField({...props})
startAdornment: startAdornment:
<div style={{overflowY: "auto", overflowX: "hidden", margin: "-10px", width: "calc(100% + 20px)", padding: "10px", marginBottom: "-20px", height: "calc(100% + 10px)"}}> <div style={{overflowY: "auto", overflowX: "hidden", margin: "-10px", width: "calc(100% + 20px)", padding: "10px", marginBottom: "-20px", height: "calc(100% + 10px)"}}>
{ {
chips.map((item, i) => ( chips.map((item, index) => (
<Chip <Chip
color={(chipType !== "number" || ! Number.isNaN(Number(item))) ? "info" : "error"} onChange={determineChipColors}
key={`${item}-${i}`} color={chipColors[index]}
key={`${item}-${index}`}
variant="outlined" variant="outlined"
tabIndex={-1} tabIndex={-1}
label={item} label={item}
className={classes.chip} className={classes.chip}
/> />
)) ))
} }
</div>, </div>,
@ -158,6 +249,7 @@ function ChipTextField({...props})
</React.Fragment> </React.Fragment>
); );
} }
ChipTextField.defaultProps = { ChipTextField.defaultProps = {
chipData: [] chipData: []
}; };
@ -166,4 +258,4 @@ ChipTextField.propTypes = {
chipData: arrayOf(string) chipData: arrayOf(string)
}; };
export default ChipTextField export default ChipTextField;

View File

@ -20,17 +20,18 @@
*/ */
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue"; import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {Box, InputAdornment, InputLabel} from "@mui/material"; import {InputAdornment, InputLabel} from "@mui/material";
import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import {ErrorMessage, Field, useFormikContext} from "formik"; import {ErrorMessage, Field, useFormikContext} from "formik";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import React, {useMemo, useState} from "react";
import AceEditor from "react-ace";
import colors from "qqq/assets/theme/base/colors"; import colors from "qqq/assets/theme/base/colors";
import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch"; import BooleanFieldSwitch from "qqq/components/forms/BooleanFieldSwitch";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import MDInput from "qqq/components/legacy/MDInput"; import MDInput from "qqq/components/legacy/MDInput";
import MDTypography from "qqq/components/legacy/MDTypography"; import MDTypography from "qqq/components/legacy/MDTypography";
import React, {useMemo, useState} from "react";
import AceEditor from "react-ace";
import {flushSync} from "react-dom"; import {flushSync} from "react-dom";
// Declaring props types for FormField // Declaring props types for FormField
@ -83,10 +84,10 @@ function QDynamicFormField({
if (placeholder) if (placeholder)
{ {
inputProps.placeholder = placeholder inputProps.placeholder = placeholder;
} }
if(backgroundColor) if (backgroundColor)
{ {
inputProps.sx = { inputProps.sx = {
"&.MuiInputBase-root": { "&.MuiInputBase-root": {
@ -124,7 +125,7 @@ function QDynamicFormField({
{ {
onChange.onChange = (e: any) => onChange.onChange = (e: any) =>
{ {
if(isToUpperCase || isToLowerCase) if (isToUpperCase || isToLowerCase)
{ {
const beforeStart = e.target.selectionStart; const beforeStart = e.target.selectionStart;
const beforeEnd = e.target.selectionEnd; const beforeEnd = e.target.selectionEnd;
@ -141,7 +142,7 @@ function QDynamicFormField({
newValue = newValue.toLowerCase(); newValue = newValue.toLowerCase();
} }
setFieldValue(name, newValue); setFieldValue(name, newValue);
if(onChangeCallback) if (onChangeCallback)
{ {
onChangeCallback(newValue); onChangeCallback(newValue);
} }
@ -153,7 +154,7 @@ function QDynamicFormField({
input.setSelectionRange(beforeStart, beforeEnd); input.setSelectionRange(beforeStart, beforeEnd);
} }
} }
else if(onChangeCallback) else if (onChangeCallback)
{ {
onChangeCallback(e.currentTarget.value); onChangeCallback(e.currentTarget.value);
} }
@ -165,15 +166,15 @@ function QDynamicFormField({
***************************************************************************/ ***************************************************************************/
function dynamicSelectOnChange(newValue?: QPossibleValue) function dynamicSelectOnChange(newValue?: QPossibleValue)
{ {
if(onChangeCallback) if (onChangeCallback)
{ {
onChangeCallback(newValue == null ? null : newValue.id) onChangeCallback(newValue == null ? null : newValue.id);
} }
} }
let field; let field;
let getsBulkEditHtmlLabel = true; let getsBulkEditHtmlLabel = true;
if(formFieldObject.possibleValueProps) if (formFieldObject.possibleValueProps)
{ {
field = (<DynamicSelect field = (<DynamicSelect
name={name} name={name}
@ -186,7 +187,7 @@ function QDynamicFormField({
onChange={dynamicSelectOnChange} onChange={dynamicSelectOnChange}
// otherValues={otherValuesMap} // otherValues={otherValuesMap}
useCase="form" useCase="form"
/>) />);
} }
else if (type === "checkbox") else if (type === "checkbox")
{ {
@ -220,7 +221,7 @@ function QDynamicFormField({
onChange={(value: string, event: any) => onChange={(value: string, event: any) =>
{ {
setFieldValue(name, value, false); setFieldValue(name, value, false);
if(onChangeCallback) if (onChangeCallback)
{ {
onChangeCallback(value); onChangeCallback(value);
} }

View File

@ -174,7 +174,7 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] => const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
{ {
return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm)); return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm));
} };
/*************************************************************************** /***************************************************************************
@ -182,15 +182,15 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, name, fieldLabel, i
***************************************************************************/ ***************************************************************************/
const loadResults = async (): Promise<QPossibleValue[]> => const loadResults = async (): Promise<QPossibleValue[]> =>
{ {
if(possibleValues) if (possibleValues)
{ {
return filterInlinePossibleValues(searchTerm, possibleValues) return filterInlinePossibleValues(searchTerm, possibleValues);
} }
else else
{ {
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase); return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, null, otherValues, useCase);
} }
} };
/*************************************************************************** /***************************************************************************

View File

@ -184,9 +184,9 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////
// copy values from specified fields in the parent record down into the child record // // copy values from specified fields in the parent record down into the child record //
/////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////
if(widgetData.defaultValuesForNewChildRecordsFromParentFields) if (widgetData.defaultValuesForNewChildRecordsFromParentFields)
{ {
for(let childField in widgetData.defaultValuesForNewChildRecordsFromParentFields) for (let childField in widgetData.defaultValuesForNewChildRecordsFromParentFields)
{ {
const parentField = widgetData.defaultValuesForNewChildRecordsFromParentFields[childField]; const parentField = widgetData.defaultValuesForNewChildRecordsFromParentFields[childField];
defaultValues[childField] = formValues[parentField]; defaultValues[childField] = formValues[parentField];
@ -278,21 +278,21 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// build a map of display values for the new record, specifically, for any possible-values that need translated. // // build a map of display values for the new record, specifically, for any possible-values that need translated. //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const displayValues: {[fieldName: string]: string} = {}; const displayValues: { [fieldName: string]: string } = {};
if(childTableName && values) if (childTableName && values)
{ {
/////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////
// this function internally memoizes, so, we could potentially avoid an await here, but, seems ok... // // this function internally memoizes, so, we could potentially avoid an await here, but, seems ok... //
/////////////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////////////
const childTableMetaData = await qController.loadTableMetaData(childTableName) const childTableMetaData = await qController.loadTableMetaData(childTableName);
for (let key in values) for (let key in values)
{ {
const value = values[key]; const value = values[key];
const field = childTableMetaData.fields.get(key); const field = childTableMetaData.fields.get(key);
if(field.possibleValueSourceName) if (field.possibleValueSourceName)
{ {
const possibleValues = await qController.possibleValues(childTableName, null, field.name, null, [value], objectToMap(values), "form") const possibleValues = await qController.possibleValues(childTableName, null, field.name, null, [value], null, objectToMap(values), "form");
if(possibleValues && possibleValues.length > 0) if (possibleValues && possibleValues.length > 0)
{ {
displayValues[key] = possibleValues[0].label; displayValues[key] = possibleValues[0].label;
} }
@ -516,13 +516,12 @@ function EntityForm(props: Props): JSX.Element
} }
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/
function objectToMap(object: { [key: string]: any }): Map<string, any> function objectToMap(object: { [key: string]: any }): Map<string, any>
{ {
if(object == null) if (object == null)
{ {
return (null); return (null);
} }
@ -532,7 +531,7 @@ function EntityForm(props: Props): JSX.Element
{ {
rs.set(key, object[key]); rs.set(key, object[key]);
} }
return rs return rs;
} }
@ -667,7 +666,7 @@ function EntityForm(props: Props): JSX.Element
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue; const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
if (defaultValue && fieldMetaData.possibleValueSourceName) if (defaultValue && fieldMetaData.possibleValueSourceName)
{ {
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], objectToMap(initialValues), "form"); const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], null, objectToMap(initialValues), "form");
if (results && results.length > 0) if (results && results.length > 0)
{ {
defaultDisplayValues.set(fieldName, results[0].label); defaultDisplayValues.set(fieldName, results[0].label);

View File

@ -26,7 +26,6 @@ import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import RadioGroup from "@mui/material/RadioGroup"; import RadioGroup from "@mui/material/RadioGroup";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {useFormikContext} from "formik"; import {useFormikContext} from "formik";
@ -94,7 +93,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
// deal with dynamically loading the initial default value for a possible value... // // deal with dynamically loading the initial default value for a possible value... //
///////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////
let actuallyDoingInitialLoadOfPossibleValue = doingInitialLoadOfPossibleValue; let actuallyDoingInitialLoadOfPossibleValue = doingInitialLoadOfPossibleValue;
if(dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue) if (dynamicField.possibleValueProps && bulkLoadField.defaultValue && !doingInitialLoadOfPossibleValue && !everDidInitialLoadOfPossibleValue)
{ {
actuallyDoingInitialLoadOfPossibleValue = true; actuallyDoingInitialLoadOfPossibleValue = true;
setDoingInitialLoadOfPossibleValue(true); setDoingInitialLoadOfPossibleValue(true);
@ -104,7 +103,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
{ {
try try
{ {
const possibleValues = await qController.possibleValues(bulkLoadField.tableStructure.tableName, null, fieldMetaData.name, null, [bulkLoadField.defaultValue], undefined, "filter"); const possibleValues = await qController.possibleValues(bulkLoadField.tableStructure.tableName, null, fieldMetaData.name, null, [bulkLoadField.defaultValue], undefined, null, "filter");
if (possibleValues && possibleValues.length > 0) if (possibleValues && possibleValues.length > 0)
{ {
setPossibleValueInitialDisplayValue(possibleValues[0].label); setPossibleValueInitialDisplayValue(possibleValues[0].label);
@ -114,9 +113,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
setPossibleValueInitialDisplayValue(null); setPossibleValueInitialDisplayValue(null);
} }
} }
catch(e) catch (e)
{ {
console.log(`Error loading possible value: ${e}`) console.log(`Error loading possible value: ${e}`);
} }
actuallyDoingInitialLoadOfPossibleValue = false; actuallyDoingInitialLoadOfPossibleValue = false;
@ -124,7 +123,7 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
})(); })();
} }
if(dynamicField.possibleValueProps && possibleValueInitialDisplayValue) if (dynamicField.possibleValueProps && possibleValueInitialDisplayValue)
{ {
dynamicField.possibleValueProps.initialDisplayValue = possibleValueInitialDisplayValue; dynamicField.possibleValueProps.initialDisplayValue = possibleValueInitialDisplayValue;
} }
@ -134,11 +133,11 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
// don't allow duplicates // // don't allow duplicates //
////////////////////////////////////////////////////// //////////////////////////////////////////////////////
const columnOptions: { value: number, label: string }[] = []; const columnOptions: { value: number, label: string }[] = [];
const usedLabels: {[label: string]: boolean} = {}; const usedLabels: { [label: string]: boolean } = {};
for (let i = 0; i < columnNames.length; i++) for (let i = 0; i < columnNames.length; i++)
{ {
const label = columnNames[i]; const label = columnNames[i];
if(!usedLabels[label]) if (!usedLabels[label])
{ {
columnOptions.push({label: label, value: i}); columnOptions.push({label: label, value: i});
usedLabels[label] = true; usedLabels[label] = true;
@ -148,9 +147,9 @@ export default function BulkLoadFileMappingField({bulkLoadField, isRequired, rem
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// try to pick up changes in the hasHeaderRow toggle from way above // // try to pick up changes in the hasHeaderRow toggle from way above //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if(bulkLoadField.columnIndex != null && bulkLoadField.columnIndex != undefined && selectedColumn.label && columnNames[bulkLoadField.columnIndex] != selectedColumn.label) if (bulkLoadField.columnIndex != null && bulkLoadField.columnIndex != undefined && selectedColumn.label && columnNames[bulkLoadField.columnIndex] != selectedColumn.label)
{ {
setSelectedColumn({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex}) setSelectedColumn({label: columnNames[bulkLoadField.columnIndex], value: bulkLoadField.columnIndex});
setSelectedColumnInputValue(columnNames[bulkLoadField.columnIndex]); setSelectedColumnInputValue(columnNames[bulkLoadField.columnIndex]);
} }

View File

@ -19,6 +19,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {FormControl, InputLabel, Select, SelectChangeEvent} from "@mui/material"; import {FormControl, InputLabel, Select, SelectChangeEvent} from "@mui/material";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
@ -28,20 +31,23 @@ import Modal from "@mui/material/Modal";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import {GridFilterItem} from "@mui/x-data-grid-pro";
import React, {useEffect, useState} from "react";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons"; import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import ChipTextField from "qqq/components/forms/ChipTextField"; import ChipTextField from "qqq/components/forms/ChipTextField";
import {LoadingState} from "qqq/models/LoadingState";
import React, {useEffect, useReducer, useState} from "react";
interface Props interface Props
{ {
type: string; type: string;
onSave: (newValues: any[]) => void; onSave: (newValues: any[]) => void;
table?: QTableMetaData;
field?: QFieldMetaData;
} }
FilterCriteriaPaster.defaultProps = {}; FilterCriteriaPaster.defaultProps = {};
function FilterCriteriaPaster({type, onSave}: Props): JSX.Element function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
{ {
enum Delimiter enum Delimiter
{ {
@ -68,6 +74,12 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
mainCardStyles.width = "60%"; mainCardStyles.width = "60%";
mainCardStyles.minWidth = "500px"; mainCardStyles.minWidth = "500px";
///////////////////////////////////////////////////////////////////////////////////////////
// add a LoadingState object, in case the initial loads (of meta data and view) are slow //
///////////////////////////////////////////////////////////////////////////////////////////
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const [pageLoadingState, _] = useState(new LoadingState(forceUpdate));
//x const [gridFilterItem, setGridFilterItem] = useState(props.item); //x const [gridFilterItem, setGridFilterItem] = useState(props.item);
const [pasteModalIsOpen, setPasteModalIsOpen] = useState(false); const [pasteModalIsOpen, setPasteModalIsOpen] = useState(false);
const [inputText, setInputText] = useState(""); const [inputText, setInputText] = useState("");
@ -75,8 +87,11 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
const [delimiterCharacter, setDelimiterCharacter] = useState(""); const [delimiterCharacter, setDelimiterCharacter] = useState("");
const [customDelimiterValue, setCustomDelimiterValue] = useState(""); const [customDelimiterValue, setCustomDelimiterValue] = useState("");
const [chipData, setChipData] = useState(undefined); const [chipData, setChipData] = useState(undefined);
const [chipValidity, setChipValidity] = useState([] as boolean[]);
const [chipPVSIds, setChipPVSIds] = useState([] as any[]);
const [detectedText, setDetectedText] = useState(""); const [detectedText, setDetectedText] = useState("");
const [errorText, setErrorText] = useState(""); const [errorText, setErrorText] = useState("");
const [saveDisabled, setSaveDisabled] = useState(true);
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
// handler for when paste icon is clicked in 'any' operator // // handler for when paste icon is clicked in 'any' operator //
@ -92,6 +107,7 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
setDelimiter(""); setDelimiter("");
setDelimiterCharacter(""); setDelimiterCharacter("");
setChipData([]); setChipData([]);
setChipValidity([]);
setInputText(""); setInputText("");
setDetectedText(""); setDetectedText("");
setCustomDelimiterValue(""); setCustomDelimiterValue("");
@ -106,18 +122,43 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
const handleSaveClicked = () => const handleSaveClicked = () =>
{ {
//////////////////////////////////////// ///////////////////////////////////////////////////////////////
// if numeric remove any non-numerics // // if numeric remove any non-numerics, or invalid pvs values //
//////////////////////////////////////// ///////////////////////////////////////////////////////////////
let saveData = []; let saveData = [];
let usedLabels = new Map<any, boolean>();
for (let i = 0; i < chipData.length; i++) for (let i = 0; i < chipData.length; i++)
{ {
if (type !== "number" || !Number.isNaN(Number(chipData[i]))) if (chipValidity[i] === true)
{ {
saveData.push(chipData[i]); if (type === "pvs")
{
/////////////////////////////////////////////
// if already used this PVS label, skip it //
/////////////////////////////////////////////
if (usedLabels.get(chipData[i]) != null)
{
continue;
}
saveData.push(new QPossibleValue({id: chipPVSIds[i], label: chipData[i]}));
usedLabels.set(chipData[i], true);
}
else
{
saveData.push(chipData[i]);
}
} }
} }
//////////////////////////////////////////
// for pvs, sort by label before saving //
//////////////////////////////////////////
if (type === "pvs")
{
saveData.sort((a: QPossibleValue, b: QPossibleValue) => b.label.localeCompare(a.label));
}
onSave(saveData); onSave(saveData);
clearData(); clearData();
@ -259,12 +300,19 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
{ {
chipData.push(part); chipData.push(part);
/////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// if numeric, check that first before pushing as a chip // // if numeric or pvs, check validity first before pushing as a chip //
/////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
if (type === "number" && Number.isNaN(Number(part))) if (chipValidity[i] !== true)
{ {
setErrorText("Some values are not numbers"); if (type === "number" && Number.isNaN(Number(part)))
{
setErrorText("Some values are not numbers");
}
else if (type === "pvs")
{
setErrorText("Some values are not valid");
}
} }
} }
} }
@ -272,7 +320,7 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
setChipData(chipData); setChipData(chipData);
}, [inputText, delimiterCharacter, customDelimiterValue, detectedText]); }, [inputText, delimiterCharacter, customDelimiterValue, detectedText, chipValidity]);
return ( return (
<Box> <Box>
@ -283,128 +331,153 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
pasteModalIsOpen && pasteModalIsOpen &&
( (
<Modal open={pasteModalIsOpen}> <Modal open={pasteModalIsOpen}>
<Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}> <Box>
<Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}> <Box sx={{position: "absolute", overflowY: "auto", width: "100%"}}>
<Card sx={mainCardStyles}> <Box py={3} justifyContent="center" sx={{display: "flex", mt: 8}}>
<Box p={4} pb={2}> <Card sx={mainCardStyles}>
<Grid container> <Box p={4} pb={2}>
<Grid item pr={3} xs={12} lg={12}> <Grid container>
<Typography variant="h5">Bulk Add Filter Values</Typography> <Grid item pr={3} xs={12} lg={12}>
<Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button"> <Typography variant="h5">Bulk Add Filter Values</Typography>
Paste into the box on the left. <Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
Review the filter values in the box on the right. Paste into the box on the left.
If the filter values are not what are expected, try changing the separator using the dropdown below. Review the filter values in the box on the right.
</Typography> If the filter values are not what are expected, try changing the separator using the dropdown below.
</Typography>
</Grid>
</Grid>
</Box>
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
<Grid item pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
<FormControl sx={{m: 1, width: "100%"}}>
<TextField
id="outlined-multiline-static"
label="PASTE TEXT"
multiline
onChange={handleTextChange}
rows={16}
value={inputText}
/>
</FormControl>
</Grid>
<Grid item xs={6} lg={6} sx={{display: "flex", flexGrow: 1}}>
<FormControl sx={{m: 1, width: "100%"}}>
<ChipTextField
handleChipChange={(isMakingRequest: boolean, chipValidity: boolean[], chipPVSIds: any[]) =>
{
setErrorText("");
if (isMakingRequest)
{
pageLoadingState.setLoading();
}
else
{
pageLoadingState.setNotLoading();
}
setSaveDisabled(isMakingRequest);
setChipPVSIds(chipPVSIds);
setChipValidity(chipValidity);
}}
table={table}
field={field}
chipData={chipData}
chipValidity={chipValidity}
chipType={type}
multiline
fullWidth
variant="outlined"
id="tags"
rows={0}
name="tags"
label="FILTER VALUES REVIEW"
/>
</FormControl>
</Grid> </Grid>
</Grid> </Grid>
</Box> <Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}> <Grid item pl={1} pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
<Grid item pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}> <Box sx={{display: "inline-flex", alignItems: "baseline"}}>
<FormControl sx={{m: 1, width: "100%"}}> <FormControl sx={{mt: 2, width: "50%"}}>
<TextField <InputLabel htmlFor="select-native">
id="outlined-multiline-static" SEPARATOR
label="PASTE TEXT" </InputLabel>
multiline <Select
onChange={handleTextChange} multiline
rows={16} native
value={inputText} value={delimiter}
/> onChange={handleDelimiterChange}
</FormControl> label="SEPARATOR"
</Grid> size="medium"
<Grid item xs={6} lg={6} sx={{display: "flex", flexGrow: 1}}> inputProps={{
<FormControl sx={{m: 1, width: "100%"}}> id: "select-native",
<ChipTextField }}
handleChipChange={() => >
{ {delimiterDropdownOptions.map((delimiter) => (
}} <option key={delimiter} value={delimiter}>
chipData={chipData} {delimiter}
chipType={type} </option>
multiline ))}
fullWidth </Select>
variant="outlined"
id="tags"
rows={0}
name="tags"
label="FILTER VALUES REVIEW"
/>
</FormControl>
</Grid>
</Grid>
<Grid container pl={3} pr={3} justifyContent="center" alignItems="stretch" sx={{display: "flex", height: "100%"}}>
<Grid item pl={1} pr={3} xs={6} lg={6} sx={{width: "100%", display: "flex", flexDirection: "column", flexGrow: 1}}>
<Box sx={{display: "inline-flex", alignItems: "baseline"}}>
<FormControl sx={{mt: 2, width: "50%"}}>
<InputLabel htmlFor="select-native">
SEPARATOR
</InputLabel>
<Select
multiline
native
value={delimiter}
onChange={handleDelimiterChange}
label="SEPARATOR"
size="medium"
inputProps={{
id: "select-native",
}}
>
{delimiterDropdownOptions.map((delimiter) => (
<option key={delimiter} value={delimiter}>
{delimiter}
</option>
))}
</Select>
</FormControl>
{delimiter === Delimiter.CUSTOM.valueOf() && (
<FormControl sx={{pl: 2, top: 5, width: "50%"}}>
<TextField
name="custom-delimiter-value"
placeholder="Custom Separator"
label="Custom Separator"
variant="standard"
value={customDelimiterValue}
onChange={handleCustomDelimiterChange}
inputProps={{maxLength: 1}}
/>
</FormControl> </FormControl>
)} {delimiter === Delimiter.CUSTOM.valueOf() && (
{inputText && delimiter === Delimiter.DETECT_AUTOMATICALLY.valueOf() && (
<Typography pl={2} variant="button" sx={{top: "1px", textTransform: "revert"}}> <FormControl sx={{pl: 2, top: 5, width: "50%"}}>
<i>{detectedText}</i> <TextField
</Typography> name="custom-delimiter-value"
)} placeholder="Custom Separator"
</Box> label="Custom Separator"
variant="standard"
value={customDelimiterValue}
onChange={handleCustomDelimiterChange}
inputProps={{maxLength: 1}}
/>
</FormControl>
)}
{inputText && delimiter === Delimiter.DETECT_AUTOMATICALLY.valueOf() && (
<Typography pl={2} variant="button" sx={{top: "1px", textTransform: "revert"}}>
<i>{detectedText}</i>
</Typography>
)}
</Box>
</Grid>
<Grid sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={3} lg={3}>
{
errorText && chipData.length > 0 && (
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
<Icon color="error">error</Icon>
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">{errorText}</Typography>
</Box>
)
}
{
pageLoadingState.isLoadingSlow() && (
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}>
<Icon color="warning">warning</Icon>
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">Loading...</Typography>
</Box>
)
}
</Grid>
<Grid sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={3} lg={3}>
{
chipData && chipData.length > 0 && (
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"}</Typography>
)
}
</Grid>
</Grid> </Grid>
<Grid sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}} item pl={1} xs={3} lg={3}> <Box p={3} pt={0}>
{ <Grid container pl={1} pr={1} justifyContent="right" alignItems="stretch" sx={{display: "flex-inline "}}>
errorText && chipData.length > 0 && ( <QCancelButton
<Box sx={{display: "flex", justifyContent: "flex-start", alignItems: "flex-start"}}> onClickHandler={handleCancelClicked}
<Icon color="error">error</Icon> iconName="cancel"
<Typography sx={{paddingLeft: "4px", textTransform: "revert"}} variant="button">{errorText}</Typography> disabled={false} />
</Box> <QSaveButton onClickHandler={handleSaveClicked} label="Add Filters" disabled={saveDisabled} />
) </Grid>
} </Box>
</Grid> </Card>
<Grid sx={{display: "flex", justifyContent: "flex-end", alignItems: "flex-start"}} item pr={1} xs={3} lg={3}> </Box>
{
chipData && chipData.length > 0 && (
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"}</Typography>
)
}
</Grid>
</Grid>
<Box p={3} pt={0}>
<Grid container pl={1} pr={1} justifyContent="right" alignItems="stretch" sx={{display: "flex-inline "}}>
<QCancelButton
onClickHandler={handleCancelClicked}
iconName="cancel"
disabled={false} />
<QSaveButton onClickHandler={handleSaveClicked} label="Add Filters" disabled={false} />
</Grid>
</Box>
</Card>
</Box> </Box>
</Box> </Box>
</Modal> </Modal>

View File

@ -398,20 +398,25 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
initialValues = criteria.values; initialValues = criteria.values;
} }
} }
return <Box> return <Box display="flex" alignItems="flex-end" className="multiValue">
<DynamicSelect <Box width={"100%"}>
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}} <DynamicSelect
overrideId={field.name + "-multi-" + criteria.id} fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
key={field.name + "-multi-" + criteria.id} overrideId={field.name + "-multi-" + criteria.id}
isMultiple key={field.name + "-multi-" + criteria.id + "-" + criteria.values.length}
fieldLabel="Values" isMultiple
initialValues={initialValues} fieldLabel="Values"
initiallyOpen={false /*initiallyOpenMultiValuePvs*/} initialValues={initialValues}
inForm={false} initiallyOpen={false /*initiallyOpenMultiValuePvs*/}
onChange={(value: any) => valueChangeHandler(null, "all", value)} inForm={false}
variant="standard" onChange={(value: any) => valueChangeHandler(null, "all", value)}
useCase="filter" variant="standard"
/> useCase="filter"
/>
</Box>
<Box>
<FilterCriteriaPaster table={table} field={field} type="pvs" onSave={(newValues: any[]) => saveNewPasterValues(newValues)} />
</Box>
</Box>; </Box>;
} }

View File

@ -38,7 +38,7 @@ import XIcon from "qqq/components/query/XIcon";
import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery"; import {QueryScreenUsage} from "qqq/pages/records/query/RecordQuery";
import FilterUtils from "qqq/utils/qqq/FilterUtils"; import FilterUtils from "qqq/utils/qqq/FilterUtils";
import TableUtils from "qqq/utils/qqq/TableUtils"; import TableUtils from "qqq/utils/qqq/TableUtils";
import React, {SyntheticEvent, useContext, useReducer, useState} from "react"; import React, {SyntheticEvent, useContext, useEffect, useReducer, useState} from "react";
export type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex"; export type CriteriaParamType = QFilterCriteriaWithId | null | "tooComplex";
@ -135,7 +135,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
return (filteredOptions[0]); return (filteredOptions[0]);
} }
if(return0thOptionInsteadOfNull) if (return0thOptionInsteadOfNull)
{ {
console.log("Returning 0th operator instead of null - this isn't expected, but has been seen to happen - so here's some additional debugging:"); console.log("Returning 0th operator instead of null - this isn't expected, but has been seen to happen - so here's some additional debugging:");
try try
@ -144,7 +144,7 @@ const getOperatorSelectedValue = (operatorOptions: OperatorOption[], criteria: Q
console.log("Criteria: " + JSON.stringify(criteria)); console.log("Criteria: " + JSON.stringify(criteria));
console.log("Default Operator: " + JSON.stringify(defaultOperator)); console.log("Default Operator: " + JSON.stringify(defaultOperator));
} }
catch(e) catch (e)
{ {
console.log(`Error in debug output: ${e}`); console.log(`Error in debug output: ${e}`);
} }
@ -186,6 +186,13 @@ export default function QuickFilter({tableMetaData, fullFieldName, fieldMetaData
////////////////////// //////////////////////
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
useEffect(() =>
{
//////////////////////////////////////////////////////////////////////////////
// was not seeing criteria changes take place until watching it stringified //
//////////////////////////////////////////////////////////////////////////////
setCriteria(criteria);
}, [JSON.stringify(criteria)]);
/******************************************************************************* /*******************************************************************************
** **

View File

@ -133,7 +133,7 @@ class FilterUtils
} }
else else
{ {
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, "filter"); values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, undefined, "filter");
} }
} }