Merge pull request #26 from Kingsrook/feature/CE-551-change-logic-for-fed-ex

Feature/ce 551 change logic for fed ex
This commit is contained in:
2023-07-26 08:43:22 -05:00
committed by GitHub
12 changed files with 355 additions and 204 deletions

View File

@ -574,6 +574,7 @@ export default function App()
const [tableMetaData, setTableMetaData] = useState(null); const [tableMetaData, setTableMetaData] = useState(null);
const [tableProcesses, setTableProcesses] = useState(null); const [tableProcesses, setTableProcesses] = useState(null);
const [dotMenuOpen, setDotMenuOpen] = useState(false); const [dotMenuOpen, setDotMenuOpen] = useState(false);
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false);
return ( return (
appRoutes && ( appRoutes && (
@ -583,11 +584,13 @@ export default function App()
tableMetaData: tableMetaData, tableMetaData: tableMetaData,
tableProcesses: tableProcesses, tableProcesses: tableProcesses,
dotMenuOpen: dotMenuOpen, dotMenuOpen: dotMenuOpen,
keyboardHelpOpen: keyboardHelpOpen,
setPageHeader: (header: string | JSX.Element) => setPageHeader(header), setPageHeader: (header: string | JSX.Element) => setPageHeader(header),
setAccentColor: (accentColor: string) => setAccentColor(accentColor), setAccentColor: (accentColor: string) => setAccentColor(accentColor),
setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData), setTableMetaData: (tableMetaData: QTableMetaData) => setTableMetaData(tableMetaData),
setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses), setTableProcesses: (tableProcesses: QProcessMetaData[]) => setTableProcesses(tableProcesses),
setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent), setDotMenuOpen: (dotMenuOpent: boolean) => setDotMenuOpen(dotMenuOpent),
setKeyboardHelpOpen: (keyboardHelpOpen: boolean) => setKeyboardHelpOpen(keyboardHelpOpen),
pathToLabelMap: pathToLabelMap, pathToLabelMap: pathToLabelMap,
branding: branding branding: branding
}}> }}>

View File

@ -67,9 +67,8 @@ const CommandMenu = ({metaData}: Props) =>
const navigate = useNavigate(); const navigate = useNavigate();
const pathParts = location.pathname.replace(/\/+$/, "").split("/"); const pathParts = location.pathname.replace(/\/+$/, "").split("/");
const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, setTableMetaData, tableProcesses} = useContext(QContext); const {accentColor, tableMetaData, dotMenuOpen, setDotMenuOpen, keyboardHelpOpen, setKeyboardHelpOpen, setTableMetaData, tableProcesses} = useContext(QContext);
const [keyboardHelpOpen, setKeyboardHelpOpen] = useState(false)
const classes = useStyles(); const classes = useStyles();
function evalueKeyPress(e: KeyboardEvent) function evalueKeyPress(e: KeyboardEvent)
@ -351,6 +350,14 @@ const CommandMenu = ({metaData}: Props) =>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>?</span>Open Keyboard Shortcuts Help</Grid> <Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>?</span>Open Keyboard Shortcuts Help</Grid>
</Grid> </Grid>
<Typography variant="h6" pt={3}>Table Query</Typography>
<Grid container columnSpacing={5} rowSpacing={1}>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>n</span>Create a New Record</Grid>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>r</span>Refresh the Query</Grid>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>c</span>Open the Columns Panel</Grid>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>f</span>Open the Filter Panel</Grid>
</Grid>
<Typography variant="h6" pt={3}>Record View</Typography> <Typography variant="h6" pt={3}>Record View</Typography>
<Grid container columnSpacing={5} rowSpacing={1}> <Grid container columnSpacing={5} rowSpacing={1}>
<Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>n</span>Create a New Record</Grid> <Grid item xs={6} className={classes.item}><span className={classes.keyboardKey}>n</span>Create a New Record</Grid>

View File

@ -37,6 +37,9 @@ interface QContext
dotMenuOpen: boolean; dotMenuOpen: boolean;
setDotMenuOpen?: (dotMenuOpen: boolean) => void; setDotMenuOpen?: (dotMenuOpen: boolean) => void;
keyboardHelpOpen: boolean;
setKeyboardHelpOpen?: (keyboardHelpOpen: boolean) => void;
tableMetaData?: QTableMetaData; tableMetaData?: QTableMetaData;
setTableMetaData?: (tableMetaData: QTableMetaData) => void; setTableMetaData?: (tableMetaData: QTableMetaData) => void;
@ -54,6 +57,7 @@ const defaultState = {
pageHeader: "", pageHeader: "",
accentColor: "#0062FF", accentColor: "#0062FF",
dotMenuOpen: false, dotMenuOpen: false,
keyboardHelpOpen: false,
pathToLabelMap: {}, pathToLabelMap: {},
}; };

View File

@ -135,7 +135,7 @@ function QDynamicFormField({
/> />
<Box mt={0.75}> <Box mt={0.75}>
<MDTypography component="div" variant="caption" color="error" fontWeight="regular"> <MDTypography component="div" variant="caption" color="error" fontWeight="regular">
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} /></div>} {!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={name} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
</MDTypography> </MDTypography>
</Box> </Box>
</> </>

View File

@ -104,7 +104,18 @@ class DynamicFormUtils
{ {
if (field.isRequired) if (field.isRequired)
{ {
return (Yup.string().required(`${field.label} is required.`)); if(field.possibleValueSourceName)
{
////////////////////////////////////////////////////////////////////////////////////////////
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
// rather, it's more like "null is how empty will be treated" or some-such... //
////////////////////////////////////////////////////////////////////////////////////////////
return (Yup.string().required(`${field.label} is required.`).nullable(true));
}
else
{
return (Yup.string().required(`${field.label} is required.`));
}
} }
return (null); return (null);
} }

View File

@ -27,8 +27,9 @@ import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Switch from "@mui/material/Switch"; import Switch from "@mui/material/Switch";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import {useFormikContext} from "formik"; import {ErrorMessage, useFormikContext} from "formik";
import React, {useEffect, useState} from "react"; import React, {useEffect, useState} from "react";
import MDTypography from "qqq/components/legacy/MDTypography";
import Client from "qqq/utils/qqq/Client"; import Client from "qqq/utils/qqq/Client";
interface Props interface Props
@ -246,78 +247,88 @@ function DynamicSelect({tableName, processName, fieldName, overrideId, fieldLabe
// console.log(`default value: ${JSON.stringify(defaultValue)}`); // console.log(`default value: ${JSON.stringify(defaultValue)}`);
const autocomplete = ( const autocomplete = (
<Autocomplete <Box>
id={overrideId ?? fieldName} <Autocomplete
sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}} id={overrideId ?? fieldName}
open={open} sx={{background: isDisabled ? "#f0f2f5!important" : "initial"}}
fullWidth open={open}
onOpen={() => fullWidth
{ onOpen={() =>
setOpen(true);
// console.log("setting open...");
if(options.length == 0)
{ {
// console.log("no options yet, so setting search term to ''..."); setOpen(true);
setSearchTerm(""); // console.log("setting open...");
} if(options.length == 0)
}} {
onClose={() => // console.log("no options yet, so setting search term to ''...");
{ setSearchTerm("");
setOpen(false); }
}} }}
isOptionEqualToValue={(option, value) => option.id === value.id} onClose={() =>
getOptionLabel={(option) => {
{ setOpen(false);
// @ts-ignore }}
if(option && option.length) isOptionEqualToValue={(option, value) => option.id === value.id}
getOptionLabel={(option) =>
{ {
// @ts-ignore // @ts-ignore
option = option[0]; if(option && option.length)
} {
// @ts-ignore
option = option[0];
}
// @ts-ignore
return option.label
}}
options={options}
loading={loading}
onInputChange={inputChanged}
onBlur={handleBlur}
defaultValue={defaultValue}
// @ts-ignore // @ts-ignore
return option.label onChange={handleChanged}
}} noOptionsText={"No matches found"}
options={options} onKeyPress={e =>
loading={loading}
onInputChange={inputChanged}
onBlur={handleBlur}
defaultValue={defaultValue}
// @ts-ignore
onChange={handleChanged}
noOptionsText={"No matches found"}
onKeyPress={e =>
{
if (e.key === "Enter")
{ {
e.preventDefault(); if (e.key === "Enter")
} {
}} e.preventDefault();
renderOption={renderOption} }
filterOptions={filterOptions} }}
disabled={isDisabled} renderOption={renderOption}
multiple={isMultiple} filterOptions={filterOptions}
disableCloseOnSelect={isMultiple} disabled={isDisabled}
limitTags={5} multiple={isMultiple}
slotProps={{popper: {className: "DynamicSelectPopper"}}} disableCloseOnSelect={isMultiple}
renderInput={(params) => ( limitTags={5}
<TextField slotProps={{popper: {className: "DynamicSelectPopper"}}}
{...params} renderInput={(params) => (
label={fieldLabel} <TextField
variant="standard" {...params}
autoComplete="off" label={fieldLabel}
type="search" variant="standard"
InputProps={{ autoComplete="off"
...params.InputProps, type="search"
endAdornment: ( InputProps={{
<React.Fragment> ...params.InputProps,
{loading ? <CircularProgress color="inherit" size={20} /> : null} endAdornment: (
{params.InputProps.endAdornment} <React.Fragment>
</React.Fragment> {loading ? <CircularProgress color="inherit" size={20} /> : null}
), {params.InputProps.endAdornment}
}} </React.Fragment>
/> ),
)} }}
/> />
)}
/>
{
inForm &&
<Box mt={0.75}>
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
</MDTypography>
</Box>
}
</Box>
); );

View File

@ -32,8 +32,8 @@ import Box from "@mui/material/Box";
import Card from "@mui/material/Card"; import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Icon from "@mui/material/Icon"; import Icon from "@mui/material/Icon";
import {Form, Formik} from "formik"; import {Form, Formik, useFormikContext} from "formik";
import React, {useContext, useReducer, useState} from "react"; import React, {useContext, useEffect, useReducer, useState} from "react";
import {useLocation, useNavigate, useParams} from "react-router-dom"; import {useLocation, useNavigate, useParams} from "react-router-dom";
import * as Yup from "yup"; import * as Yup from "yup";
import QContext from "QContext"; import QContext from "QContext";
@ -77,7 +77,7 @@ function EntityForm(props: Props): JSX.Element
const [formTitle, setFormTitle] = useState(""); const [formTitle, setFormTitle] = useState("");
const [validations, setValidations] = useState({}); const [validations, setValidations] = useState({});
const [initialValues, setInitialValues] = useState({} as { [key: string]: string }); const [initialValues, setInitialValues] = useState({} as { [key: string]: any });
const [formFields, setFormFields] = useState(null as Map<string, any>); const [formFields, setFormFields] = useState(null as Map<string, any>);
const [t1sectionName, setT1SectionName] = useState(null as string); const [t1sectionName, setT1SectionName] = useState(null as string);
const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]); const [nonT1Sections, setNonT1Sections] = useState([] as QTableSection[]);
@ -233,27 +233,25 @@ function EntityForm(props: Props): JSX.Element
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// if default values were supplied for a new record, then populate initialValues, for formik. // // if default values were supplied for a new record, then populate initialValues, for formik. //
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
if(defaultValues) for (let i = 0; i < fieldArray.length; i++)
{ {
for (let i = 0; i < fieldArray.length; i++) const fieldMetaData = fieldArray[i];
const fieldName = fieldMetaData.name;
const defaultValue = (defaultValues && defaultValues[fieldName]) ? defaultValues[fieldName] : fieldMetaData.defaultValue;
if (defaultValue)
{ {
const fieldMetaData = fieldArray[i]; initialValues[fieldName] = defaultValue;
const fieldName = fieldMetaData.name;
if (defaultValues[fieldName])
{
initialValues[fieldName] = defaultValues[fieldName];
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// we need to set the initialDisplayValue for possible value fields with a default value // // we need to set the initialDisplayValue for possible value fields with a default value //
// so, look them up here now if needed // // so, look them up here now if needed //
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName) if (fieldMetaData.possibleValueSourceName)
{
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
if (results && results.length > 0)
{ {
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); defaultDisplayValues.set(fieldName, results[0].label);
if (results && results.length > 0)
{
defaultDisplayValues.set(fieldName, results[0].label);
}
} }
} }
} }
@ -598,6 +596,7 @@ function EntityForm(props: Props): JSX.Element
isSubmitting, isSubmitting,
}) => ( }) => (
<Form id={formId} autoComplete="off"> <Form id={formId} autoComplete="off">
<ScrollToFirstError />
<Box pb={3} pt={0}> <Box pb={3} pt={0}>
<Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation}> <Card id={`${t1sectionName}`} sx={{overflow: "visible", pb: 2, scrollMarginTop: "100px"}} elevation={cardElevation}>
@ -672,4 +671,43 @@ function EntityForm(props: Props): JSX.Element
} }
} }
function ScrollToFirstError(): JSX.Element
{
const {submitCount, isValid} = useFormikContext()
useEffect(() =>
{
/////////////////////////////////////////////////////////////////////////////
// Wrap the code in setTimeout to make sure it runs after the DOM has been //
// updated and has the error message elements. //
/////////////////////////////////////////////////////////////////////////////
setTimeout(() =>
{
////////////////////////////////////////
// Only run on submit or if not valid //
////////////////////////////////////////
if (submitCount === 0 || isValid)
{
return;
}
//////////////////////////////////
// Find the first error message //
//////////////////////////////////
const errorMessageSelector = "[data-field-error]";
const firstErrorMessage = document.querySelector(errorMessageSelector);
if (!firstErrorMessage)
{
console.warn(`Form failed validation but no error field was found with selector: ${errorMessageSelector}`);
return;
}
firstErrorMessage.scrollIntoView({block: "center"});
}, 100)
}, [submitCount, isValid])
return null;
}
export default EntityForm; export default EntityForm;

View File

@ -31,7 +31,7 @@ import TextField from "@mui/material/TextField";
import {GridColDef, GridSlotsComponentsProps, useGridApiContext, useGridSelector} from "@mui/x-data-grid-pro"; import {GridColDef, GridSlotsComponentsProps, useGridApiContext, useGridSelector} from "@mui/x-data-grid-pro";
import {GridColumnsPanelProps} from "@mui/x-data-grid/components/panel/GridColumnsPanel"; import {GridColumnsPanelProps} from "@mui/x-data-grid/components/panel/GridColumnsPanel";
import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector"; import {gridColumnDefinitionsSelector, gridColumnVisibilityModelSelector} from "@mui/x-data-grid/hooks/features/columns/gridColumnsSelector";
import React, {createRef, forwardRef, useEffect, useReducer, useState} from "react"; import React, {createRef, forwardRef, useEffect, useReducer, useRef, useState} from "react";
declare module "@mui/x-data-grid" declare module "@mui/x-data-grid"
{ {
@ -55,6 +55,9 @@ export const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const someRef = createRef(); const someRef = createRef();
const textRef = useRef(null);
const [didInitialFocus, setDidInitialFocus] = useState(false)
const [openGroups, setOpenGroups] = useState(props.initialOpenedGroups || {}); const [openGroups, setOpenGroups] = useState(props.initialOpenedGroups || {});
const openGroupsBecauseOfFilter = {} as { [name: string]: boolean }; const openGroupsBecauseOfFilter = {} as { [name: string]: boolean };
const [lastScrollTop, setLastScrollTop] = useState(0); const [lastScrollTop, setLastScrollTop] = useState(0);
@ -68,6 +71,15 @@ export const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
console.log(`Open groups: ${JSON.stringify(openGroups)}`); console.log(`Open groups: ${JSON.stringify(openGroups)}`);
if(!didInitialFocus)
{
if(textRef.current)
{
textRef.current.select();
setDidInitialFocus(true);
}
}
if (props.tableMetaData.exposedJoins) if (props.tableMetaData.exposedJoins)
{ {
for (let i = 0; i < props.tableMetaData.exposedJoins.length; i++) for (let i = 0; i < props.tableMetaData.exposedJoins.length; i++)
@ -360,7 +372,7 @@ export const CustomColumnsPanel = forwardRef<any, GridColumnsPanelProps>(
return ( return (
<Box className="custom-columns-panel" style={{width: "350px", height: "450px"}}> <Box className="custom-columns-panel" style={{width: "350px", height: "450px"}}>
<Box height="55px" padding="5px" display="flex"> <Box height="55px" padding="5px" display="flex">
<TextField id="findColumn" label="Find column" placeholder="Column title" variant="standard" fullWidth={true} <TextField inputRef={textRef} id="findColumn" label="Find column" placeholder="Column title" variant="standard" fullWidth={true}
value={filterText} value={filterText}
onChange={(event) => filterTextChanged(event)} onChange={(event) => filterTextChanged(event)}
></TextField> ></TextField>

View File

@ -56,12 +56,114 @@ export interface OperatorOption
{ {
label: string; label: string;
value: QCriteriaOperator; value: QCriteriaOperator;
implicitValues?: [any]; implicitValues?: any[];
valueMode: ValueMode; valueMode: ValueMode;
} }
export const getDefaultCriteriaValue = () => [""]; export const getDefaultCriteriaValue = () => [""];
export const getOperatorOptions = (tableMetaData: QTableMetaData, fieldName: string): OperatorOption[] =>
{
const [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName);
let operatorOptions = [];
if (field && fieldTable)
{
//////////////////////////////////////////////////////
// setup array of options for operator Autocomplete //
//////////////////////////////////////////////////////
if (field.possibleValueSourceName)
{
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.PVS_MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.PVS_MULTI});
}
else
{
switch (field.type)
{
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than or equals", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "less than", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "less than or equals", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE});
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
break;
case QFieldType.DATE:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE_DATE});
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE});
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
//? operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN});
//? operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN});
break;
case QFieldType.DATE_TIME:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is at or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is at or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME});
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME});
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
break;
case QFieldType.BOOLEAN:
operatorOptions.push({label: "equals yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]});
operatorOptions.push({label: "equals no", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [false]});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
/*
? is yes or empty (is not no)
? is no or empty (is not yes)
*/
break;
case QFieldType.BLOB:
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
break;
default:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "contains ", value: QCriteriaOperator.CONTAINS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not contain", value: QCriteriaOperator.NOT_CONTAINS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "starts with", value: QCriteriaOperator.STARTS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not start with", value: QCriteriaOperator.NOT_STARTS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "ends with", value: QCriteriaOperator.ENDS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not end with", value: QCriteriaOperator.NOT_ENDS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
}
}
}
return (operatorOptions);
}
interface FilterCriteriaRowProps interface FilterCriteriaRowProps
{ {
id: number; id: number;
@ -120,105 +222,6 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
let operatorOptions: OperatorOption[] = []; let operatorOptions: OperatorOption[] = [];
function setOperatorOptions(fieldName: string)
{
const [field, fieldTable] = FilterUtils.getField(tableMetaData, fieldName);
operatorOptions = [];
if (field && fieldTable)
{
//////////////////////////////////////////////////////
// setup array of options for operator Autocomplete //
//////////////////////////////////////////////////////
if (field.possibleValueSourceName)
{
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.PVS_SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.PVS_MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.PVS_MULTI});
}
else
{
switch (field.type)
{
case QFieldType.DECIMAL:
case QFieldType.INTEGER:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "greater than or equals", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "less than", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "less than or equals", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE});
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
break;
case QFieldType.DATE:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is on or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is on or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE_DATE});
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE});
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
//? operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN});
//? operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN});
break;
case QFieldType.DATE_TIME:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is after", value: QCriteriaOperator.GREATER_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is at or after", value: QCriteriaOperator.GREATER_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is before", value: QCriteriaOperator.LESS_THAN, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is at or before", value: QCriteriaOperator.LESS_THAN_OR_EQUALS, valueMode: ValueMode.SINGLE_DATE_TIME});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME});
operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN, valueMode: ValueMode.DOUBLE_DATE_TIME});
//? operatorOptions.push({label: "is between", value: QCriteriaOperator.BETWEEN});
//? operatorOptions.push({label: "is not between", value: QCriteriaOperator.NOT_BETWEEN});
break;
case QFieldType.BOOLEAN:
operatorOptions.push({label: "equals yes", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [true]});
operatorOptions.push({label: "equals no", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.NONE, implicitValues: [false]});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
/*
? is yes or empty (is not no)
? is no or empty (is not yes)
*/
break;
case QFieldType.BLOB:
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
break;
default:
operatorOptions.push({label: "equals", value: QCriteriaOperator.EQUALS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not equal", value: QCriteriaOperator.NOT_EQUALS_OR_IS_NULL, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "contains ", value: QCriteriaOperator.CONTAINS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not contain", value: QCriteriaOperator.NOT_CONTAINS, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "starts with", value: QCriteriaOperator.STARTS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not start with", value: QCriteriaOperator.NOT_STARTS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "ends with", value: QCriteriaOperator.ENDS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "does not end with", value: QCriteriaOperator.NOT_ENDS_WITH, valueMode: ValueMode.SINGLE});
operatorOptions.push({label: "is empty", value: QCriteriaOperator.IS_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is not empty", value: QCriteriaOperator.IS_NOT_BLANK, valueMode: ValueMode.NONE});
operatorOptions.push({label: "is any of", value: QCriteriaOperator.IN, valueMode: ValueMode.MULTI});
operatorOptions.push({label: "is none of", value: QCriteriaOperator.NOT_IN, valueMode: ValueMode.MULTI});
}
}
}
}
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
// make currently selected values appear in the Autocompletes // // make currently selected values appear in the Autocompletes //
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
@ -240,8 +243,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
defaultFieldValue = {field: field, table: fieldTable, fieldName: criteria.fieldName}; defaultFieldValue = {field: field, table: fieldTable, fieldName: criteria.fieldName};
} }
setOperatorOptions(criteria.fieldName); operatorOptions = getOperatorOptions(tableMetaData, criteria.fieldName);
let newOperatorSelectedValue = operatorOptions.filter(option => let newOperatorSelectedValue = operatorOptions.filter(option =>
{ {
@ -294,7 +296,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
// update the operator options, and the operator on this criteria // // update the operator options, and the operator on this criteria //
//////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////
setOperatorOptions(criteria.fieldName); operatorOptions = getOperatorOptions(tableMetaData, criteria.fieldName);
if (operatorOptions.length) if (operatorOptions.length)
{ {
if (isFieldTypeDifferent(oldFieldName, criteria.fieldName)) if (isFieldTypeDifferent(oldFieldName, criteria.fieldName))
@ -473,7 +475,7 @@ export function FilterCriteriaRow({id, index, tableMetaData, metaData, criteria,
} }
else else
{ {
if(isNotSet(criteria.values[0])) if(!criteria.values || isNotSet(criteria.values[0]))
{ {
criteriaIsValid = false; criteriaIsValid = false;
criteriaStatusTooltip = "You must enter a value to complete the definition of this condition."; criteriaStatusTooltip = "You must enter a value to complete the definition of this condition.";

View File

@ -51,7 +51,7 @@ import MenuItem from "@mui/material/MenuItem";
import Modal from "@mui/material/Modal"; 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 {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector} from "@mui/x-data-grid-pro"; import {DataGridPro, GridCallbackDetails, GridColDef, GridColumnMenuContainer, GridColumnMenuProps, GridColumnOrderChangeParams, GridColumnPinningMenuItems, GridColumnsMenuItem, GridColumnVisibilityModel, GridDensity, GridEventListener, GridExportMenuItemProps, GridFilterMenuItem, GridFilterModel, GridPinnedColumns, gridPreferencePanelStateSelector, GridRowId, GridRowParams, GridRowsProp, GridSelectionModel, GridSortItem, GridSortModel, GridState, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarDensitySelector, GridToolbarExportContainer, GridToolbarFilterButton, HideGridColMenuItem, MuiEvent, SortGridMenuItems, useGridApiContext, useGridApiEventHandler, useGridSelector, useGridApiRef, GridPreferencePanelsValue} from "@mui/x-data-grid-pro";
import {GridRowModel} from "@mui/x-data-grid/models/gridRows"; import {GridRowModel} from "@mui/x-data-grid/models/gridRows";
import FormData from "form-data"; import FormData from "form-data";
import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react"; import React, {forwardRef, useContext, useEffect, useReducer, useRef, useState} from "react";
@ -252,12 +252,65 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
const [queryErrors, setQueryErrors] = useState({} as any); const [queryErrors, setQueryErrors] = useState({} as any);
const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date()); const [receivedQueryErrorTimestamp, setReceivedQueryErrorTimestamp] = useState(new Date());
const {setPageHeader} = useContext(QContext); const {setPageHeader, dotMenuOpen, keyboardHelpOpen} = useContext(QContext);
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
const gridApiRef = useGridApiRef();
///////////////////////
// Keyboard handling //
///////////////////////
useEffect(() =>
{
if(tableMetaData == null)
{
(async() =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTableMetaData(tableMetaData);
})();
}
const down = (e: KeyboardEvent) =>
{
const type = (e.target as any).type;
const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search");
if(validType && !dotMenuOpen && !keyboardHelpOpen && !activeModalProcess)
{
if (! e.metaKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{
e.preventDefault()
navigate(`${metaData?.getTablePathByName(tableName)}/create`);
}
else if (! e.metaKey && e.key === "r")
{
e.preventDefault()
updateTable();
}
else if (! e.metaKey && e.key === "c")
{
e.preventDefault()
gridApiRef.current.showPreferences(GridPreferencePanelsValue.columns)
}
else if (! e.metaKey && e.key === "f")
{
e.preventDefault()
gridApiRef.current.showFilterPanel()
}
}
}
document.addEventListener("keydown", down)
return () =>
{
document.removeEventListener("keydown", down)
}
}, [dotMenuOpen, keyboardHelpOpen, metaData, activeModalProcess])
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
// monitor location changes - if our url looks like a process, then open that process. // // monitor location changes - if our url looks like a process, then open that process. //
///////////////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////////////////
@ -1934,6 +1987,7 @@ function RecordQuery({table, launchProcess}: Props): JSX.Element
<Card> <Card>
<Box height="100%"> <Box height="100%">
<DataGridPro <DataGridPro
apiRef={gridApiRef}
components={{ components={{
Toolbar: CustomToolbar, Toolbar: CustomToolbar,
Pagination: CustomPagination, Pagination: CustomPagination,

View File

@ -116,7 +116,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget); const openActionsMenu = (event: any) => setActionsMenu(event.currentTarget);
const closeActionsMenu = () => setActionsMenu(null); const closeActionsMenu = () => setActionsMenu(null);
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen} = useContext(QContext); const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen} = useContext(QContext);
if (localStorage.getItem(tableVariantLocalStorageKey)) if (localStorage.getItem(tableVariantLocalStorageKey))
{ {
@ -138,7 +138,9 @@ function RecordView({table, launchProcess}: Props): JSX.Element
setShowAudit(false); setShowAudit(false);
}; };
// Toggle the menu when ⌘K is pressed ///////////////////////
// Keyboard handling //
///////////////////////
useEffect(() => useEffect(() =>
{ {
if(tableMetaData == null) if(tableMetaData == null)
@ -155,7 +157,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
const type = (e.target as any).type; const type = (e.target as any).type;
const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search"); const validType = (type !== "text" && type !== "textarea" && type !== "input" && type !== "search");
if(validType && !dotMenuOpen && !showAudit && !showEditChildForm) if(validType && !dotMenuOpen && !keyboardHelpOpen && !showAudit && !showEditChildForm)
{ {
if (! e.metaKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission) if (! e.metaKey && e.key === "n" && table.capabilities.has(Capability.TABLE_INSERT) && table.insertPermission)
{ {
@ -190,7 +192,7 @@ function RecordView({table, launchProcess}: Props): JSX.Element
{ {
document.removeEventListener("keydown", down) document.removeEventListener("keydown", down)
} }
}, [dotMenuOpen, showEditChildForm, showAudit, metaData]) }, [dotMenuOpen, keyboardHelpOpen, showEditChildForm, showAudit, metaData])
const gotoCreate = () => const gotoCreate = () =>
{ {

View File

@ -433,6 +433,13 @@ input[type="search"]::-webkit-search-results-decoration { display: none; }
line-height: 1.75; line-height: 1.75;
} }
.filterCriteriaRowColumnPopper .MuiAutocomplete-groupLabel
{
line-height: 1.75;
padding-top: 8px;
padding-bottom: 8px;
}
/* taller list box */ /* taller list box */
.filterCriteriaRowColumnPopper .MuiAutocomplete-listbox .filterCriteriaRowColumnPopper .MuiAutocomplete-listbox
{ {