Compare commits

..

12 Commits

15 changed files with 4631 additions and 19782 deletions

1
.gitignore vendored
View File

@ -31,3 +31,4 @@ yalc.lock
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
/src/main/resources/material-dashboard/

View File

@ -154,7 +154,7 @@ material-dashboard-2-pro-react-ts
│ │   ├── Cards │ │   ├── Cards
│ │   ├── Charts │ │   ├── Charts
│ │   ├── Configurator │ │   ├── Configurator
│ │   ├── Footer │ │   ├── FooterCard
│ │   ├── Items │ │   ├── Items
│ │   ├── LayoutContainers │ │   ├── LayoutContainers
│ │   ├── Lists │ │   ├── Lists

23732
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",
@ -23,6 +23,8 @@
"@types/react-dom": "18.0.0", "@types/react-dom": "18.0.0",
"@types/react-router-hash-link": "2.4.5", "@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3", "ace-builds": "1.12.3",
"ajv": "^8.11.0",
"ajv-keywords": "^5.1.0",
"chart.js": "3.4.1", "chart.js": "3.4.1",
"chroma-js": "2.4.2", "chroma-js": "2.4.2",
"cmdk": "0.2.0", "cmdk": "0.2.0",
@ -36,8 +38,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",
"jwt-decode": "3.1.2", "jwt-decode": "3.1.2",
"lodash": "4.17.21",
"oidc-client-ts": "2.4.1", "oidc-client-ts": "2.4.1",
"react-oidc-context": "2.3.1",
"rapidoc": "9.3.4", "rapidoc": "9.3.4",
"react": "18.0.0", "react": "18.0.0",
"react-ace": "10.1.0", "react-ace": "10.1.0",
@ -51,6 +53,7 @@
"react-github-btn": "1.2.1", "react-github-btn": "1.2.1",
"react-google-drive-picker": "^1.2.0", "react-google-drive-picker": "^1.2.0",
"react-markdown": "9.0.1", "react-markdown": "9.0.1",
"react-oidc-context": "2.3.1",
"react-router-dom": "6.2.1", "react-router-dom": "6.2.1",
"react-router-hash-link": "2.4.3", "react-router-hash-link": "2.4.3",
"react-table": "7.7.0", "react-table": "7.7.0",
@ -59,14 +62,15 @@
"yup": "0.32.11" "yup": "0.32.11"
}, },
"scripts": { "scripts": {
"build": "react-scripts build", "build": "PUBLIC_URL=. react-scripts build",
"clean": "rm -rf node_modules package-lock.json lib", "clean": "rm -rf node_modules package-lock.json lib",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force", "clean-and-install": "rm -rf node_modules/ && rm -rf package-lock.json && rm -rf lib/ && npm install --legacy-peer-deps && npm dedupe --force",
"npm-install": "npm install --legacy-peer-deps", "npm-install": "npm install --legacy-peer-deps",
"prepublishOnly": "tsc -p ./ --outDir lib/", "prepublishOnly": "tsc -p ./ --outDir lib/",
"start": "BROWSER=none react-scripts --max-http-header-size=65535 start", "start": "PUBLIC_URL=. BROWSER=none react-scripts --max-http-header-size=65535 start",
"test": "react-scripts test" "test": "react-scripts test",
"export": "rm -rf dist && PUBLIC_URL=. react-scripts build && rm -rf src/main/resources/material-dashboard && mkdir -p src/main/resources/material-dashboard && cp -r build/* src/main/resources/material-dashboard"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<revision>0.25.0</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>
@ -66,7 +66,7 @@
<dependency> <dependency>
<groupId>com.kingsrook.qqq</groupId> <groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId> <artifactId>qqq-backend-core</artifactId>
<version>0.25.0-integration-sprint-62-20250307-205536</version> <version>0.26.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>

View File

@ -36,6 +36,20 @@ import {BrowserRouter} from "react-router-dom";
const qController = Client.getInstance(); const qController = Client.getInstance();
function getBasePath(): string
{
// You can change this logic depending on how you detect your mount point
const path = window.location.pathname;
console.warn("Using hacked base path for QQQ application, please update this code to be better : path ["+ path +"].");
// Example: If app is deployed at /admin or /portal
if (path.startsWith("/admin")) return "/admin";
if (path.startsWith("/portal")) return "/portal"; // TODO: This is all temporary, we need to fix this properly
return "/";
}
if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1) if (document.location.search && document.location.search.indexOf("clearAuthenticationMetaDataLocalStorage") > -1)
{ {
qController.clearAuthenticationMetaDataLocalStorage(); qController.clearAuthenticationMetaDataLocalStorage();
@ -89,19 +103,19 @@ authenticationMetaDataPromise.then((authenticationMetaData) =>
if (authenticationMetaData.type === "AUTH_0") if (authenticationMetaData.type === "AUTH_0")
{ {
root.render(<BrowserRouter> root.render(<BrowserRouter basename={getBasePath()}>
<Auth0RouterBody /> <Auth0RouterBody />
</BrowserRouter>); </BrowserRouter>);
} }
else if (authenticationMetaData.type === "OAUTH2") else if (authenticationMetaData.type === "OAUTH2")
{ {
root.render(<BrowserRouter> root.render(<BrowserRouter basename={getBasePath()}>
<OAuth2RouterBody /> <OAuth2RouterBody />
</BrowserRouter>); </BrowserRouter>);
} }
else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK") else if (authenticationMetaData.type === "FULLY_ANONYMOUS" || authenticationMetaData.type === "MOCK")
{ {
root.render(<BrowserRouter> root.render(<BrowserRouter basename={getBasePath()}>
<AnonymousRouterBody /> <AnonymousRouterBody />
</BrowserRouter>); </BrowserRouter>);
} }

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.toLowerCase() === batch[j].toLowerCase())
{
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,10 @@
* 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 {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstance";
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 +32,26 @@ 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 HelpContent from "qqq/components/misc/HelpContent";
import {LoadingState} from "qqq/models/LoadingState";
import Client from "qqq/utils/qqq/Client";
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 = {};
const qController = Client.getInstance();
function FilterCriteriaPaster({type, onSave}: Props): JSX.Element function FilterCriteriaPaster({table, field, type, onSave}: Props): JSX.Element
{ {
enum Delimiter enum Delimiter
{ {
@ -68,6 +78,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 +91,13 @@ 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 [uniqueCount, setUniqueCount] = 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);
const [metaData, setMetaData] = useState(null as QInstance);
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
// handler for when paste icon is clicked in 'any' operator // // handler for when paste icon is clicked in 'any' operator //
@ -92,6 +113,7 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
setDelimiter(""); setDelimiter("");
setDelimiterCharacter(""); setDelimiterCharacter("");
setChipData([]); setChipData([]);
setChipValidity([]);
setInputText(""); setInputText("");
setDetectedText(""); setDetectedText("");
setCustomDelimiterValue(""); setCustomDelimiterValue("");
@ -106,18 +128,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();
@ -214,6 +261,12 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
useEffect(() => useEffect(() =>
{ {
(async () =>
{
const metaData = await qController.loadMetaData();
setMetaData(metaData);
})();
let currentDelimiter = delimiter; let currentDelimiter = delimiter;
let currentDelimiterCharacter = delimiterCharacter; let currentDelimiterCharacter = delimiterCharacter;
@ -246,10 +299,16 @@ function FilterCriteriaPaster({type, onSave}: Props): JSX.Element
let parts = inputText.split(regex); let parts = inputText.split(regex);
let chipData = [] as string[]; let chipData = [] as string[];
/////////////////////////////////////////////////////////////////
// use a map to keep track of the counts for each unique value //
/////////////////////////////////////////////////////////////////
const uniqueValuesMap: { [key: string]: number } = {};
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
// if delimiter is empty string, dont split anything // // if delimiter is empty string, dont split anything //
/////////////////////////////////////////////////////// ///////////////////////////////////////////////////////
setErrorText(""); setErrorText("");
let invalidCount = 0;
if (currentDelimiterCharacter !== "") if (currentDelimiterCharacter !== "")
{ {
for (let i = 0; i < parts.length; i++) for (let i = 0; i < parts.length; i++)
@ -259,20 +318,47 @@ 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 and add to invalid count //
/////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
if (type === "number" && Number.isNaN(Number(part))) if (chipValidity[i] != null && chipValidity[i] !== true)
{ {
setErrorText("Some values are not numbers"); if ((type === "number" && Number.isNaN(Number(part))) || type === "pvs")
{
invalidCount++;
}
}
else
{
let count = uniqueValuesMap[part] == null ? 0 : uniqueValuesMap[part];
uniqueValuesMap[part] = count + 1;
} }
} }
} }
} }
if (invalidCount > 0)
{
if (type === "number")
{
let suffix = invalidCount === 1 ? " value is not a number" : " values are not numbers";
setErrorText(invalidCount + suffix + "numbers and will not be added to the filter");
}
else if (type === "pvs")
{
let suffix = invalidCount === 1 ? " value was" : " values were";
setErrorText(invalidCount + suffix + " not found and will not be added to the filter");
}
}
setUniqueCount(Object.keys(uniqueValuesMap).length);
setChipData(chipData); setChipData(chipData);
}, [inputText, delimiterCharacter, customDelimiterValue, detectedText]); }, [inputText, delimiterCharacter, customDelimiterValue, detectedText, chipValidity]);
const slotName = type === "pvs" ? "bulkAddFilterValuesPossibleValueSource" : "bulkAddFilterValues";
const helpRoles = ["QUERY_SCREEN"];
const formattedHelpContent = <HelpContent helpContents={metaData?.helpContent?.get(slotName)} roles={helpRoles} heading={null} helpContentKey={`instanceLevel:true;slot:${slotName}`} />;
return ( return (
<Box> <Box>
@ -283,128 +369,155 @@ 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. {
Review the filter values in the box on the right. formattedHelpContent && <Box sx={{display: "flex", lineHeight: "1.7", textTransform: "none"}}>
If the filter values are not what are expected, try changing the separator using the dropdown below. <Typography sx={{display: "flex", lineHeight: "1.7", textTransform: "revert"}} variant="button">
</Typography> {formattedHelpContent}
</Typography>
</Box>
}
</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={4} lg={4}>
{
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={2} lg={2}>
{
chipData && chipData.length > 0 && (
<Typography sx={{textTransform: "revert"}} variant="button">{chipData.length.toLocaleString()} {chipData.length === 1 ? "value" : "values"} {uniqueCount && `(${uniqueCount} unique)`}</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 Values" 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");
} }
} }