mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 22:58:43 +00:00
Compare commits
32 Commits
version-0.
...
snapshot-f
Author | SHA1 | Date | |
---|---|---|---|
3adb8ab4ba | |||
98a02cda96 | |||
aee4becda5 | |||
f13c2c276f | |||
a99272767b | |||
a3236b426e | |||
597fde977f | |||
e303ed0b43 | |||
2b057768b3 | |||
504a43d9c3 | |||
33e56f823d | |||
dc8fdb33dc | |||
efa67da7f9 | |||
3dc92aec88 | |||
d2705c3aed | |||
1d965bcdee | |||
894a9c2afc | |||
d25f124d87 | |||
fd5055e502 | |||
326367fbe0 | |||
bb6f818457 | |||
1cd6e07907 | |||
e839da6123 | |||
34a4fc19b4 | |||
2cc7e9ebe1 | |||
128a748b63 | |||
1284e3a22c | |||
ae358b9067 | |||
d9f1642f0a | |||
5a6293cfdf | |||
0c2dcb1215 | |||
418f7957a2 |
@ -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.104",
|
"@kingsrook/qqq-frontend-core": "1.0.108",
|
||||||
"@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",
|
||||||
|
2
pom.xml
2
pom.xml
@ -29,7 +29,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.21.0</revision>
|
<revision>0.23.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>
|
||||||
|
@ -30,14 +30,17 @@ import MDButton from "qqq/components/legacy/MDButton";
|
|||||||
|
|
||||||
export const standardWidth = "150px";
|
export const standardWidth = "150px";
|
||||||
|
|
||||||
|
const standardML = {xs: 1, md: 3};
|
||||||
|
|
||||||
interface QCreateNewButtonProps
|
interface QCreateNewButtonProps
|
||||||
{
|
{
|
||||||
tablePath: string;
|
tablePath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box display="inline-block" ml={3} mr={0} width={standardWidth}>
|
<Box display="inline-block" ml={standardML} mr={0} width={standardWidth}>
|
||||||
<Link to={`${tablePath}/create`}>
|
<Link to={`${tablePath}/create`}>
|
||||||
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||||
Create New
|
Create New
|
||||||
@ -54,6 +57,7 @@ interface QSaveButtonProps
|
|||||||
onClickHandler?: any,
|
onClickHandler?: any,
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
QSaveButton.defaultProps = {
|
QSaveButton.defaultProps = {
|
||||||
label: "Save",
|
label: "Save",
|
||||||
iconName: "save"
|
iconName: "save"
|
||||||
@ -62,7 +66,7 @@ QSaveButton.defaultProps = {
|
|||||||
export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||||
{label}
|
{label}
|
||||||
</MDButton>
|
</MDButton>
|
||||||
@ -72,17 +76,18 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
|
|||||||
|
|
||||||
interface QDeleteButtonProps
|
interface QDeleteButtonProps
|
||||||
{
|
{
|
||||||
onClickHandler: any
|
onClickHandler: any;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDeleteButton.defaultProps = {
|
QDeleteButton.defaultProps = {
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
|
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
||||||
Delete
|
Delete
|
||||||
</MDButton>
|
</MDButton>
|
||||||
@ -93,7 +98,7 @@ export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): J
|
|||||||
export function QEditButton(): JSX.Element
|
export function QEditButton(): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<Link to="edit">
|
<Link to="edit">
|
||||||
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
||||||
Edit
|
Edit
|
||||||
@ -132,7 +137,7 @@ interface QCancelButtonProps
|
|||||||
onClickHandler: any;
|
onClickHandler: any;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
iconName?: string
|
iconName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QCancelButton({
|
export function QCancelButton({
|
||||||
@ -140,7 +145,7 @@ export function QCancelButton({
|
|||||||
}: QCancelButtonProps): JSX.Element
|
}: QCancelButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml="auto" width={standardWidth}>
|
<Box ml={standardML} mb={2} width={standardWidth}>
|
||||||
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
|
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
|
||||||
{label}
|
{label}
|
||||||
</MDButton>
|
</MDButton>
|
||||||
@ -155,15 +160,15 @@ QCancelButton.defaultProps = {
|
|||||||
|
|
||||||
interface QSubmitButtonProps
|
interface QSubmitButtonProps
|
||||||
{
|
{
|
||||||
label?: string
|
label?: string;
|
||||||
iconName?: string
|
iconName?: string;
|
||||||
disabled: boolean
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
|
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||||
{label}
|
{label}
|
||||||
</MDButton>
|
</MDButton>
|
||||||
|
@ -172,17 +172,14 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
<Grid item xs={12} sm={6} key={fieldName}>
|
<Grid item xs={12} sm={6} key={fieldName}>
|
||||||
{labelElement}
|
{labelElement}
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={field.possibleValueProps.tableName}
|
fieldPossibleValueProps={field.possibleValueProps}
|
||||||
processName={field.possibleValueProps.processName}
|
|
||||||
possibleValueSourceName={field.possibleValueProps.possibleValueSourceName}
|
|
||||||
fieldName={field.possibleValueProps.fieldName}
|
|
||||||
isEditable={field.isEditable}
|
isEditable={field.isEditable}
|
||||||
fieldLabel=""
|
fieldLabel=""
|
||||||
initialValue={values[fieldName]}
|
initialValue={values[fieldName]}
|
||||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
|
||||||
bulkEditMode={bulkEditMode}
|
bulkEditMode={bulkEditMode}
|
||||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||||
otherValues={otherValuesMap}
|
otherValues={otherValuesMap}
|
||||||
|
useCase="form"
|
||||||
/>
|
/>
|
||||||
{formattedHelpContent}
|
{formattedHelpContent}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
|
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
|
|
||||||
|
|
||||||
@ -135,11 +136,11 @@ class DynamicFormUtils
|
|||||||
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
|
// 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... //
|
// 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));
|
return (Yup.string().required(`${field.label ?? "This field"} is required.`).nullable(true));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return (Yup.string().required(`${field.label} is required.`));
|
return (Yup.string().required(`${field.label ?? "This field"} is required.`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (null);
|
return (null);
|
||||||
@ -155,47 +156,49 @@ class DynamicFormUtils
|
|||||||
{
|
{
|
||||||
const field = qFields[i];
|
const field = qFields[i];
|
||||||
|
|
||||||
|
if(!dynamicFormFields[field.name])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
// add props for possible value fields //
|
// add props for possible value fields //
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
if (field.possibleValueSourceName && dynamicFormFields[field.name])
|
if (field.possibleValueSourceName || field.inlinePossibleValueSource)
|
||||||
{
|
{
|
||||||
let initialDisplayValue = null;
|
let props: FieldPossibleValueProps =
|
||||||
|
{
|
||||||
|
isPossibleValue: true,
|
||||||
|
fieldName: field.name,
|
||||||
|
initialDisplayValue: null
|
||||||
|
}
|
||||||
|
|
||||||
if (displayValues)
|
if (displayValues)
|
||||||
{
|
{
|
||||||
initialDisplayValue = displayValues.get(field.name);
|
props.initialDisplayValue = displayValues.get(field.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tableName)
|
if(field.inlinePossibleValueSource)
|
||||||
{
|
{
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
//////////////////////////////////////////////////////////////////////
|
||||||
{
|
// handle an inline PVS - which is a list of possible value objects //
|
||||||
isPossibleValue: true,
|
//////////////////////////////////////////////////////////////////////
|
||||||
tableName: tableName,
|
props.possibleValues = field.inlinePossibleValueSource;
|
||||||
fieldName: field.name,
|
}
|
||||||
initialDisplayValue: initialDisplayValue,
|
else if (tableName)
|
||||||
};
|
{
|
||||||
|
props.tableName = tableName;
|
||||||
}
|
}
|
||||||
else if (processName)
|
else if (processName)
|
||||||
{
|
{
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
props.processName = processName;
|
||||||
{
|
|
||||||
isPossibleValue: true,
|
|
||||||
processName: processName,
|
|
||||||
fieldName: field.name,
|
|
||||||
initialDisplayValue: initialDisplayValue,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
props.possibleValueSourceName = field.possibleValueSourceName;
|
||||||
{
|
|
||||||
isPossibleValue: true,
|
|
||||||
initialDisplayValue: initialDisplayValue,
|
|
||||||
fieldName: field.name,
|
|
||||||
possibleValueSourceName: field.possibleValueSourceName
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dynamicFormFields[field.name].possibleValueProps = props;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,20 +30,17 @@ import TextField from "@mui/material/TextField";
|
|||||||
import {ErrorMessage, useFormikContext} from "formik";
|
import {ErrorMessage, useFormikContext} from "formik";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
tableName?: string;
|
fieldPossibleValueProps: FieldPossibleValueProps;
|
||||||
processName?: string;
|
|
||||||
fieldName?: string;
|
|
||||||
possibleValueSourceName?: string;
|
|
||||||
overrideId?: string;
|
overrideId?: string;
|
||||||
fieldLabel: string;
|
fieldLabel: string;
|
||||||
inForm: boolean;
|
inForm: boolean;
|
||||||
initialValue?: any;
|
initialValue?: any;
|
||||||
initialDisplayValue?: string;
|
|
||||||
initialValues?: QPossibleValue[];
|
initialValues?: QPossibleValue[];
|
||||||
onChange?: any;
|
onChange?: any;
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
@ -53,16 +50,12 @@ interface Props
|
|||||||
otherValues?: Map<string, any>;
|
otherValues?: Map<string, any>;
|
||||||
variant: "standard" | "outlined";
|
variant: "standard" | "outlined";
|
||||||
initiallyOpen: boolean;
|
initiallyOpen: boolean;
|
||||||
|
useCase: "form" | "filter";
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicSelect.defaultProps = {
|
DynamicSelect.defaultProps = {
|
||||||
tableName: null,
|
|
||||||
processName: null,
|
|
||||||
fieldName: null,
|
|
||||||
possibleValueSourceName: null,
|
|
||||||
inForm: true,
|
inForm: true,
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
initialDisplayValue: null,
|
|
||||||
initialValues: undefined,
|
initialValues: undefined,
|
||||||
onChange: null,
|
onChange: null,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
@ -102,8 +95,10 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
|
|||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
|
function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm, initialValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props)
|
||||||
{
|
{
|
||||||
|
const {fieldName, initialDisplayValue, possibleValueSourceName, possibleValues, processName, tableName} = fieldPossibleValueProps;
|
||||||
|
|
||||||
const [open, setOpen] = useState(initiallyOpen);
|
const [open, setOpen] = useState(initiallyOpen);
|
||||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState(null);
|
const [searchTerm, setSearchTerm] = useState(null);
|
||||||
@ -171,6 +166,35 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
setFieldValueRef = setFieldValue;
|
setFieldValueRef = setFieldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
|
||||||
|
{
|
||||||
|
return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const loadResults = async (): Promise<QPossibleValue[]> =>
|
||||||
|
{
|
||||||
|
if(possibleValues)
|
||||||
|
{
|
||||||
|
return filterInlinePossibleValues(searchTerm, possibleValues)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
@ -194,7 +218,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
// console.log(`doing a search with ${searchTerm}`);
|
// console.log(`doing a search with ${searchTerm}`);
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
|
const results: QPossibleValue[] = await loadResults();
|
||||||
|
|
||||||
if (tableMetaData == null && tableName)
|
if (tableMetaData == null && tableName)
|
||||||
{
|
{
|
||||||
@ -217,7 +241,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
};
|
};
|
||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
// todo - finish... call it in onOpen?
|
|
||||||
|
/***************************************************************************
|
||||||
|
** todo - finish... call it in onOpen?
|
||||||
|
***************************************************************************/
|
||||||
const reloadIfOtherValuesAreChanged = () =>
|
const reloadIfOtherValuesAreChanged = () =>
|
||||||
{
|
{
|
||||||
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
||||||
@ -226,8 +253,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
{
|
{
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
|
||||||
console.log("Refreshing possible values...");
|
console.log("Refreshing possible values...");
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
|
const results: QPossibleValue[] = await loadResults();
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setOptions([...results]);
|
setOptions([...results]);
|
||||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||||
@ -235,6 +264,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||||
{
|
{
|
||||||
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
||||||
@ -245,11 +278,19 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const handleBlur = (x: any) =>
|
const handleBlur = (x: any) =>
|
||||||
{
|
{
|
||||||
setSearchTerm(null);
|
setSearchTerm(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||||
{
|
{
|
||||||
// console.log("handleChanged. value is:");
|
// console.log("handleChanged. value is:");
|
||||||
@ -273,6 +314,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] =>
|
const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] =>
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -282,6 +327,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
return (options);
|
return (options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const renderOption = (props: Object, option: any, {selected}) =>
|
const renderOption = (props: Object, option: any, {selected}) =>
|
||||||
{
|
{
|
||||||
@ -330,6 +379,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const bulkEditSwitchChanged = () =>
|
const bulkEditSwitchChanged = () =>
|
||||||
{
|
{
|
||||||
const newSwitchValue = !switchChecked;
|
const newSwitchValue = !switchChecked;
|
||||||
@ -350,7 +403,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
const autocomplete = (
|
const autocomplete = (
|
||||||
<Box>
|
<Box>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
id={overrideId ?? fieldName ?? possibleValueSourceName}
|
id={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"}
|
||||||
sx={autocompleteSX}
|
sx={autocompleteSX}
|
||||||
open={open}
|
open={open}
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -430,7 +483,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
inForm &&
|
inForm &&
|
||||||
<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={fieldName ?? possibleValueSourceName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
@ -502,7 +502,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/////////////////////////////////////////////////
|
/////////////////////////////////////////////////
|
||||||
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
|
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
|
||||||
{
|
{
|
||||||
const widget = metaData?.widgets.get(section.widgetName);
|
const widget = metaData?.widgets?.get(section.widgetName);
|
||||||
if (widget)
|
if (widget)
|
||||||
{
|
{
|
||||||
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
|
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
|
||||||
@ -1152,11 +1152,11 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{
|
{
|
||||||
!props.isModal &&
|
!props.isModal &&
|
||||||
<Grid item xs={12} lg={3}>
|
<Grid item xs={12} lg={3} className="recordSidebar">
|
||||||
<QRecordSidebar tableSections={tableSections} />
|
<QRecordSidebar tableSections={tableSections} />
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
<Grid item xs={12} lg={props.isModal ? 12 : 9}>
|
<Grid item xs={12} lg={props.isModal ? 12 : 9} className={props.isModal ? "" : "recordWithSidebar"}>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
@ -64,13 +64,14 @@ function Footer({company, links}: Props): JSX.Element
|
|||||||
<Box
|
<Box
|
||||||
width="100%"
|
width="100%"
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection={{xs: "column", lg: "row"}}
|
flexDirection={{xs: "column", md: "row"}}
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
px={1.5}
|
px={1.5}
|
||||||
style={{
|
style={{
|
||||||
position: "fixed", bottom: "0px", zIndex: -1, marginBottom: "10px",
|
position: "fixed", bottom: "0px", zIndex: -1, marginBottom: "10px",
|
||||||
}}
|
}}
|
||||||
|
left={{xs: "0", xl: "auto"}}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
href && name &&
|
href && name &&
|
||||||
|
@ -84,7 +84,7 @@ function ProcessSummaryResults({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box m={3} mt={6}>
|
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={0} lg={2} />
|
<Grid item xs={0} lg={2} />
|
||||||
<Grid item xs={12} lg={8}>
|
<Grid item xs={12} lg={8}>
|
||||||
|
@ -273,7 +273,7 @@ function ValidationReview({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box m={3}>
|
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12} lg={6}>
|
<Grid item xs={12} lg={6}>
|
||||||
<MDTypography color="body" variant="button">
|
<MDTypography color="body" variant="button">
|
||||||
|
@ -183,7 +183,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
|||||||
{
|
{
|
||||||
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
|
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
|
||||||
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
|
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
|
||||||
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
|
if (field?.type == QFieldType.DATE_TIME)
|
||||||
{
|
{
|
||||||
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export function EvaluatedExpression({field, expression}: EvaluatedExpressionProp
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</>;
|
return <span style={{fontVariantNumeric: "tabular-nums"}}>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOUR_MS = 60 * 60 * 1000;
|
const HOUR_MS = 60 * 60 * 1000;
|
||||||
|
@ -367,16 +367,15 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
) : (
|
) : (
|
||||||
<Box width={"100%"}>
|
<Box width={"100%"}>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={table.name}
|
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
|
||||||
fieldName={field.name}
|
|
||||||
overrideId={field.name + "-single-" + criteria.id}
|
overrideId={field.name + "-single-" + criteria.id}
|
||||||
key={field.name + "-single-" + criteria.id}
|
key={field.name + "-single-" + criteria.id}
|
||||||
fieldLabel="Value"
|
fieldLabel="Value"
|
||||||
initialValue={selectedPossibleValue?.id}
|
initialValue={selectedPossibleValue?.id}
|
||||||
initialDisplayValue={selectedPossibleValue?.label}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
|
useCase="filter"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
@ -401,8 +400,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
}
|
}
|
||||||
return <Box>
|
return <Box>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={table.name}
|
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
||||||
fieldName={field.name}
|
|
||||||
overrideId={field.name + "-multi-" + criteria.id}
|
overrideId={field.name + "-multi-" + criteria.id}
|
||||||
key={field.name + "-multi-" + criteria.id}
|
key={field.name + "-multi-" + criteria.id}
|
||||||
isMultiple
|
isMultiple
|
||||||
@ -412,6 +410,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
|
useCase="filter"
|
||||||
/>
|
/>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
@ -440,10 +440,10 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
||||||
<Grid container alignItems="flex-end">
|
<Grid container alignItems="flex-end">
|
||||||
<Box maxWidth={"50%"} minWidth={300}>
|
<Box maxWidth={"50%"} minWidth={300}>
|
||||||
<DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} />
|
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiName", initialDisplayValue: apiNameLabel}} initialValue={apiName} fieldLabel={"API Name *"} inForm={false} onChange={changeApiName} useCase="form" />
|
||||||
</Box>
|
</Box>
|
||||||
<Box maxWidth={"50%"} minWidth={300} pl={2}>
|
<Box maxWidth={"50%"} minWidth={300} pl={2}>
|
||||||
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
|
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiVersion", initialDisplayValue: apiVersionLabel}} initialValue={apiVersion} fieldLabel={"API Version *"} inForm={false} onChange={changeApiVersion} useCase="form" />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Box display="flex" sx={{height: "100%"}}>
|
<Box display="flex" sx={{height: "100%"}}>
|
||||||
|
@ -391,12 +391,12 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
|
|||||||
<Box display="flex" flexDirection="row" alignItems="center">
|
<Box display="flex" flexDirection="row" alignItems="center">
|
||||||
<Box width="550px" pr={2} mb={-1.5}>
|
<Box width="550px" pr={2} mb={-1.5}>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
|
fieldPossibleValueProps={{possibleValueSourceName: shareableTableMetaData.audiencePossibleValueSourceName, initialDisplayValue: selectedAudienceOption?.label}}
|
||||||
fieldLabel="User or Group" // todo should come from shareableTableMetaData
|
fieldLabel="User or Group" // todo should come from shareableTableMetaData
|
||||||
initialValue={selectedAudienceOption?.id}
|
initialValue={selectedAudienceOption?.id}
|
||||||
initialDisplayValue={selectedAudienceOption?.label}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={handleAudienceChange}
|
onChange={handleAudienceChange}
|
||||||
|
useCase="form"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{/*
|
{/*
|
||||||
|
@ -42,13 +42,14 @@ interface CompositeWidgetProps
|
|||||||
{
|
{
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData;
|
||||||
data: CompositeData;
|
data: CompositeData;
|
||||||
|
actionCallback?: (blockData: BlockData) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Widget which is a list of Blocks.
|
** Widget which is a list of Blocks.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element
|
export default function CompositeWidget({widgetMetaData, data, actionCallback}: CompositeWidgetProps): JSX.Element
|
||||||
{
|
{
|
||||||
if (!data || !data.blocks)
|
if (!data || !data.blocks)
|
||||||
{
|
{
|
||||||
@ -81,6 +82,14 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
boxStyle.justifyContent = "space-between";
|
boxStyle.justifyContent = "space-between";
|
||||||
boxStyle.gap = "0.25rem";
|
boxStyle.gap = "0.25rem";
|
||||||
}
|
}
|
||||||
|
else if (layout == "FLEX_ROW_CENTER")
|
||||||
|
{
|
||||||
|
boxStyle.display = "flex";
|
||||||
|
boxStyle.flexDirection = "row";
|
||||||
|
boxStyle.justifyContent = "center";
|
||||||
|
boxStyle.gap = "0.25rem";
|
||||||
|
boxStyle.flexWrap = "wrap";
|
||||||
|
}
|
||||||
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
||||||
{
|
{
|
||||||
boxStyle.display = "flex";
|
boxStyle.display = "flex";
|
||||||
@ -122,7 +131,7 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
{
|
{
|
||||||
data.blocks.map((block: BlockData, index) => (
|
data.blocks.map((block: BlockData, index) => (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
<WidgetBlock widgetMetaData={widgetMetaData} block={block} actionCallback={actionCallback} />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,9 @@
|
|||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {Alert, Skeleton} from "@mui/material";
|
import {Alert, Skeleton} from "@mui/material";
|
||||||
|
import ActionButtonBlock from "qqq/components/widgets/blocks/ActionButtonBlock";
|
||||||
|
import AudioBlock from "qqq/components/widgets/blocks/AudioBlock";
|
||||||
|
import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
||||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
@ -32,19 +35,21 @@ import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRow
|
|||||||
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
||||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
||||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||||
|
import ImageBlock from "./blocks/ImageBlock";
|
||||||
|
|
||||||
|
|
||||||
interface WidgetBlockProps
|
interface WidgetBlockProps
|
||||||
{
|
{
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData;
|
||||||
block: BlockData;
|
block: BlockData;
|
||||||
|
actionCallback?: (blockData: BlockData) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Component to render a single Block in the widget framework!
|
** Component to render a single Block in the widget framework!
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element
|
export default function WidgetBlock({widgetMetaData, block, actionCallback}: WidgetBlockProps): JSX.Element
|
||||||
{
|
{
|
||||||
if(!block)
|
if(!block)
|
||||||
{
|
{
|
||||||
@ -64,7 +69,7 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
|||||||
if(block.blockTypeName == "COMPOSITE")
|
if(block.blockTypeName == "COMPOSITE")
|
||||||
{
|
{
|
||||||
// @ts-ignore - special case for composite type block...
|
// @ts-ignore - special case for composite type block...
|
||||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} />);
|
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(block.blockTypeName)
|
switch(block.blockTypeName)
|
||||||
@ -83,6 +88,14 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
|||||||
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||||
case "BIG_NUMBER":
|
case "BIG_NUMBER":
|
||||||
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||||
|
case "INPUT_FIELD":
|
||||||
|
return (<InputFieldBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||||
|
case "ACTION_BUTTON":
|
||||||
|
return (<ActionButtonBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||||
|
case "AUDIO":
|
||||||
|
return (<AudioBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||||
|
case "IMAGE":
|
||||||
|
return (<ImageBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||||
default:
|
default:
|
||||||
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
||||||
}
|
}
|
||||||
|
60
src/qqq/components/widgets/blocks/ActionButtonBlock.tsx
Normal file
60
src/qqq/components/widgets/blocks/ActionButtonBlock.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Icon from "@mui/material/Icon";
|
||||||
|
import {standardWidth} from "qqq/components/buttons/DefaultButtons";
|
||||||
|
import MDButton from "qqq/components/legacy/MDButton";
|
||||||
|
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||||
|
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Block that renders ... an action button...
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function ActionButtonBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
|
||||||
|
{
|
||||||
|
const icon = data.values.iconName ? <Icon>{data.values.iconName}</Icon> : null;
|
||||||
|
|
||||||
|
function onClick()
|
||||||
|
{
|
||||||
|
if(actionCallback)
|
||||||
|
{
|
||||||
|
actionCallback(data, {actionCode: data.values?.actionCode})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("ActionButtonBlock onClick with no actionCallback present, so, noop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||||
|
<Box mx={1} my={1} minWidth={standardWidth}>
|
||||||
|
<MDButton type="button" variant="gradient" color="dark" size="small" fullWidth startIcon={icon} onClick={onClick}>
|
||||||
|
{data.values.label ?? "Action"}
|
||||||
|
</MDButton>
|
||||||
|
</Box>
|
||||||
|
</BlockElementWrapper>
|
||||||
|
);
|
||||||
|
}
|
40
src/qqq/components/widgets/blocks/AudioBlock.tsx
Normal file
40
src/qqq/components/widgets/blocks/AudioBlock.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||||
|
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
|
import DumpJsonBox from "qqq/utils/DumpJsonBox";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Block that renders ... an audio tag
|
||||||
|
**
|
||||||
|
** <audio src=${path} ${autoPlay} ${showControls} />
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function AudioBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||||
|
<audio src={data.values?.path} autoPlay={data.values?.autoPlay} controls={data.values?.showControls} />
|
||||||
|
</BlockElementWrapper>
|
||||||
|
);
|
||||||
|
}
|
@ -35,6 +35,8 @@ export interface BlockData
|
|||||||
|
|
||||||
values: any;
|
values: any;
|
||||||
styles?: any;
|
styles?: any;
|
||||||
|
|
||||||
|
conditional?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -57,5 +59,6 @@ export interface StandardBlockComponentProps
|
|||||||
{
|
{
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData;
|
||||||
data: BlockData;
|
data: BlockData;
|
||||||
|
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
59
src/qqq/components/widgets/blocks/ImageBlock.tsx
Normal file
59
src/qqq/components/widgets/blocks/ImageBlock.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||||
|
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
|
import DumpJsonBox from "qqq/utils/DumpJsonBox";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Block that renders ... an image tag
|
||||||
|
**
|
||||||
|
** <audio src=${path} ${autoPlay} ${showControls} />
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function AudioBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||||
|
{
|
||||||
|
let imageStyle: any = {};
|
||||||
|
|
||||||
|
if(data.styles?.width)
|
||||||
|
{
|
||||||
|
imageStyle.width = data.styles?.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(data.styles?.height)
|
||||||
|
{
|
||||||
|
imageStyle.height = data.styles?.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(data.styles?.bordered)
|
||||||
|
{
|
||||||
|
imageStyle.border = "1px solid #C0C0C0";
|
||||||
|
imageStyle.borderRadius = "0.5rem";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||||
|
<img src={data.values?.path} alt={data.values?.alt} style={imageStyle} />
|
||||||
|
</BlockElementWrapper>
|
||||||
|
);
|
||||||
|
}
|
128
src/qqq/components/widgets/blocks/InputFieldBlock.tsx
Normal file
128
src/qqq/components/widgets/blocks/InputFieldBlock.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
|
||||||
|
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
|
||||||
|
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||||
|
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
|
import React, {SyntheticEvent, useState} from "react";
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Block that renders ... a text input
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
export default function InputFieldBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
|
||||||
|
{
|
||||||
|
const [blurCount, setBlurCount] = useState(0)
|
||||||
|
|
||||||
|
const fieldMetaData = new QFieldMetaData(data.values.fieldMetaData);
|
||||||
|
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
|
||||||
|
|
||||||
|
let autoFocus = data.values.autoFocus as boolean
|
||||||
|
let value = data.values.value;
|
||||||
|
if(value == null || value == undefined)
|
||||||
|
{
|
||||||
|
value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for an autoFocus field... //
|
||||||
|
// we're finding that if we blur it when clicking an action button, that //
|
||||||
|
// an un-desirable "now it's been touched, so show an error" happens. //
|
||||||
|
// so let us remove the default blur handler, for the first (auto) focus/blur //
|
||||||
|
// cycle, and we seem to have a better time. //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let onBlurRest: {onBlur?: any} = {}
|
||||||
|
if(autoFocus && blurCount == 0)
|
||||||
|
{
|
||||||
|
onBlurRest.onBlur = (event: React.SyntheticEvent) =>
|
||||||
|
{
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
setBlurCount(blurCount + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function eventHandler(event: KeyboardEvent)
|
||||||
|
{
|
||||||
|
if(data.values.submitOnEnter && event.key == "Enter")
|
||||||
|
{
|
||||||
|
// @ts-ignore target.value...
|
||||||
|
const inputValue = event.target.value?.trim()
|
||||||
|
|
||||||
|
// todo - make this behavior opt-in for inputBlocks?
|
||||||
|
if(inputValue && `${inputValue}`.startsWith("->"))
|
||||||
|
{
|
||||||
|
const actionCode = inputValue.substring(2);
|
||||||
|
if(actionCallback)
|
||||||
|
{
|
||||||
|
actionCallback(data, {actionCode: actionCode, _fieldToClearIfError: fieldMetaData.name});
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
// return, so we don't submit the actionCode as text //
|
||||||
|
///////////////////////////////////////////////////////
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(fieldMetaData.isRequired && inputValue == "")
|
||||||
|
{
|
||||||
|
console.log("input field is required, but missing value, so not submitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(actionCallback)
|
||||||
|
{
|
||||||
|
console.log("InputFieldBlock calling actionCallback for submitOnEnter");
|
||||||
|
|
||||||
|
let values: {[name: string]: any} = {};
|
||||||
|
values[fieldMetaData.name] = inputValue;
|
||||||
|
|
||||||
|
actionCallback(data, values);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
console.log("InputFieldBlock was set as submitOnEnter, but no actionCallback was present, so, noop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelElement = <Box fontSize="1rem" fontWeight="500" marginBottom="0.25rem">
|
||||||
|
<label htmlFor={fieldMetaData.name}>{fieldMetaData.label}</label>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mt="0.5rem">
|
||||||
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||||
|
<>
|
||||||
|
{labelElement}
|
||||||
|
<QDynamicFormField name={fieldMetaData.name} displayFormat={null} label="" formFieldObject={dynamicField} type={fieldMetaData.type} value={value} autoFocus={autoFocus} onKeyUp={eventHandler} {...onBlurRest} />
|
||||||
|
</>
|
||||||
|
</BlockElementWrapper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -19,8 +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 Box from "@mui/material/Box";
|
||||||
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
|
import DumpJsonBox from "qqq/utils/DumpJsonBox";
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Block that renders ... just some text.
|
** Block that renders ... just some text.
|
||||||
@ -29,9 +31,54 @@ import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockMo
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export default function TextBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
export default function TextBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||||
{
|
{
|
||||||
|
let color = "rgba(0, 0, 0, 0.87)";
|
||||||
|
if (data.styles?.standardColor)
|
||||||
|
{
|
||||||
|
switch (data.styles?.standardColor)
|
||||||
|
{
|
||||||
|
case "SUCCESS":
|
||||||
|
color = "#2BA83F";
|
||||||
|
break;
|
||||||
|
case "WARNING":
|
||||||
|
color = "#FBA132";
|
||||||
|
break;
|
||||||
|
case "ERROR":
|
||||||
|
color = "#FB4141";
|
||||||
|
break;
|
||||||
|
case "INFO":
|
||||||
|
color = "#458CFF";
|
||||||
|
break;
|
||||||
|
case "MUTED":
|
||||||
|
color = "#7b809a";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let boxStyle = {};
|
||||||
|
if (data.styles?.isAlert)
|
||||||
|
{
|
||||||
|
boxStyle =
|
||||||
|
{
|
||||||
|
border: `1px solid ${color}`,
|
||||||
|
background: `${color}40`,
|
||||||
|
padding: "0.5rem",
|
||||||
|
borderRadius: "0.5rem",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = data.values.interpolatedText ?? data.values.text;
|
||||||
|
const lines = text.split("\n");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||||
<span style={{fontSize: "1.000rem"}}>{data.values.text}</span>
|
<Box display="inline-block" lineHeight="1.2" sx={boxStyle}>
|
||||||
|
<span style={{fontSize: "1rem", color: color}}>
|
||||||
|
{lines.map((line: string, index: number) =>
|
||||||
|
(
|
||||||
|
<div key={index}>{line}</div>
|
||||||
|
))
|
||||||
|
}</span>
|
||||||
|
</Box>
|
||||||
</BlockElementWrapper>
|
</BlockElementWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ export default function UpOrDownNumberBlock({widgetMetaData, data}: StandardBloc
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline"}}>
|
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline", marginLeft: "auto"}}>
|
||||||
|
|
||||||
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
|
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
|
||||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">
|
||||||
|
@ -87,6 +87,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
{
|
{
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
||||||
|
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
|
||||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
|
|
||||||
const [alertContent, setAlertContent] = useState(null as string);
|
const [alertContent, setAlertContent] = useState(null as string);
|
||||||
@ -272,7 +273,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function mayShowQueryPreview(): boolean
|
function mayShowQuery(): boolean
|
||||||
{
|
{
|
||||||
if (tableMetaData)
|
if (tableMetaData)
|
||||||
{
|
{
|
||||||
@ -288,7 +289,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function mayShowColumnsPreview(): boolean
|
function mayShowColumns(): boolean
|
||||||
{
|
{
|
||||||
if (tableMetaData)
|
if (tableMetaData)
|
||||||
{
|
{
|
||||||
@ -356,14 +357,14 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
<Box pt="0.5rem">
|
<Box pt="0.5rem">
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
<h5>Query Filter</h5>
|
<h5>Query Filter</h5>
|
||||||
<Box fontSize="0.75rem" fontWeight="700">{mayShowQueryPreview() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
mayShowQueryPreview() &&
|
mayShowQuery() &&
|
||||||
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
|
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!mayShowQueryPreview() &&
|
!mayShowQuery() &&
|
||||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}>
|
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}>
|
||||||
{
|
{
|
||||||
isEditable &&
|
isEditable &&
|
||||||
@ -382,11 +383,11 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
<h5>Columns</h5>
|
<h5>Columns</h5>
|
||||||
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
||||||
{
|
{
|
||||||
mayShowColumnsPreview() &&
|
mayShowColumns() && columns &&
|
||||||
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
|
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!mayShowColumnsPreview() &&
|
!mayShowColumns() &&
|
||||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
||||||
{
|
{
|
||||||
isEditable &&
|
isEditable &&
|
||||||
@ -402,6 +403,21 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{!hidePreview && !isEditable && frontendQueryFilter && tableMetaData && (
|
||||||
|
<Box pt="1rem">
|
||||||
|
<h5>Preview</h5>
|
||||||
|
<RecordQuery
|
||||||
|
allowVariables={widgetData?.allowVariables}
|
||||||
|
ref={recordQueryRef}
|
||||||
|
table={tableMetaData}
|
||||||
|
isPreview={true}
|
||||||
|
usage="reportSetup"
|
||||||
|
isModal={true}
|
||||||
|
initialQueryFilter={frontendQueryFilter}
|
||||||
|
initialColumns={columns}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
modalOpen &&
|
modalOpen &&
|
||||||
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
||||||
|
38
src/qqq/models/fields/FieldPossibleValueProps.ts
Normal file
38
src/qqq/models/fields/FieldPossibleValueProps.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Properties attached to a (formik?) form field, to denote how it behaves as
|
||||||
|
** as related to a possible value source.
|
||||||
|
*******************************************************************************/
|
||||||
|
export interface FieldPossibleValueProps
|
||||||
|
{
|
||||||
|
isPossibleValue?: boolean;
|
||||||
|
possibleValues?: QPossibleValue[];
|
||||||
|
initialDisplayValue: string | null;
|
||||||
|
fieldName?: string;
|
||||||
|
tableName?: string;
|
||||||
|
processName?: string;
|
||||||
|
possibleValueSourceName?: string;
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
|
|||||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
||||||
@ -36,12 +37,14 @@ import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJob
|
|||||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
||||||
|
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 Step from "@mui/material/Step";
|
import Step from "@mui/material/Step";
|
||||||
import StepLabel from "@mui/material/StepLabel";
|
import StepLabel from "@mui/material/StepLabel";
|
||||||
import Stepper from "@mui/material/Stepper";
|
import Stepper from "@mui/material/Stepper";
|
||||||
|
import {Theme} from "@mui/material/styles";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
@ -60,8 +63,11 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
|||||||
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
||||||
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
||||||
import ValidationReview from "qqq/components/processes/ValidationReview";
|
import ValidationReview from "qqq/components/processes/ValidationReview";
|
||||||
|
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
|
import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget";
|
||||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
|
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
|
||||||
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
@ -89,14 +95,18 @@ const INITIAL_RETRY_MILLIS = 1_500;
|
|||||||
const RETRY_MAX_MILLIS = 12_000;
|
const RETRY_MAX_MILLIS = 12_000;
|
||||||
const BACKOFF_AMOUNT = 1.5;
|
const BACKOFF_AMOUNT = 1.5;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
// define a function that we can make referenes to, which we'll overwrite //
|
// define some functions that we can make referene to, which we'll overwrite //
|
||||||
// with formik's setFieldValue function, once we're inside formik. //
|
// with functions from formik, once we're inside formik. //
|
||||||
////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let formikSetTouched = ({}: any, touched: boolean): void =>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
||||||
|
|
||||||
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
||||||
@ -120,6 +130,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
||||||
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
|
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
|
||||||
const [newStep, setNewStep] = useState(null);
|
const [newStep, setNewStep] = useState(null);
|
||||||
|
const [stepInstanceCounter, setStepInstanceCounter] = useState(0);
|
||||||
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
|
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
|
||||||
const [needInitialLoad, setNeedInitialLoad] = useState(true);
|
const [needInitialLoad, setNeedInitialLoad] = useState(true);
|
||||||
const [lastForcedReInit, setLastForcedReInit] = useState(null as number);
|
const [lastForcedReInit, setLastForcedReInit] = useState(null as number);
|
||||||
@ -136,6 +147,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
);
|
);
|
||||||
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
||||||
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
||||||
|
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
|
||||||
|
|
||||||
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
|
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
|
||||||
|
|
||||||
@ -155,8 +167,30 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
|
const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
|
||||||
|
|
||||||
const onLastStep = activeStepIndex === steps.length - 2;
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
const noMoreSteps = activeStepIndex === steps.length - 1;
|
// determine if we're on the last-step or not (e.g., to decide "Submit" vs "Next") //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let onLastStep = false;
|
||||||
|
if (processMetaData?.stepFlow == "LINEAR" && activeStepIndex === steps.length - 2)
|
||||||
|
{
|
||||||
|
onLastStep = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// determine if any 'next' button appears //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
let noMoreSteps = false;
|
||||||
|
if (processMetaData?.stepFlow == "LINEAR" && activeStepIndex === steps.length - 1)
|
||||||
|
{
|
||||||
|
noMoreSteps = true;
|
||||||
|
}
|
||||||
|
if(processValues["noMoreSteps"])
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
// this, to allow a non-linear process to request this behavior //
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
noMoreSteps = true;
|
||||||
|
}
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// form state //
|
// form state //
|
||||||
@ -324,6 +358,69 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
return renderedWidget;
|
return renderedWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** callback used by widget blocks, e.g., for input-text-enter-on-submit,
|
||||||
|
** and action buttons.
|
||||||
|
***************************************************************************/
|
||||||
|
function blockWidgetActionCallback(blockData: BlockData, eventValues?: { [name: string]: any }): boolean
|
||||||
|
{
|
||||||
|
console.log(`in blockWidgetActionCallback, called by block: ${JSON.stringify(blockData)}`);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the eventValues included an actionCode - validate it before proceeding //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (eventValues && eventValues.actionCode && !ProcessWidgetBlockUtils.isActionCodeValid(eventValues.actionCode, activeStep, processValues))
|
||||||
|
{
|
||||||
|
setFormError("Unrecognized action code: " + eventValues.actionCode);
|
||||||
|
|
||||||
|
if (eventValues["_fieldToClearIfError"])
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if the eventValues included a _fieldToClearIfError, well, then do that. //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
formikSetFieldValueFunction(eventValues["_fieldToClearIfError"], "", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////
|
||||||
|
// ok - submit! //
|
||||||
|
//////////////////
|
||||||
|
handleSubmit(eventValues);
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** in a memoized-fashion (YUNO useMemo?), render a component that is an
|
||||||
|
** adHoc widget (e.g., composite)
|
||||||
|
***************************************************************************/
|
||||||
|
function renderAdHocWidget(componentValues: any, componentIndex: number)
|
||||||
|
{
|
||||||
|
const key = activeStep.name + "-" + stepInstanceCounter + "-" + componentIndex;
|
||||||
|
if (renderedWidgets[key])
|
||||||
|
{
|
||||||
|
return renderedWidgets[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgetMetaData = new QWidgetMetaData({name: "adHoc"});
|
||||||
|
const compositeWidgetData = JSON.parse(JSON.stringify(componentValues)) as CompositeData;
|
||||||
|
compositeWidgetData.styleOverrides = {py: "0.5rem", display: "flex", flexDirection: "column", gap: "0.5rem"};
|
||||||
|
|
||||||
|
ProcessWidgetBlockUtils.dynamicEvaluationOfCompositeWidgetData(compositeWidgetData, processValues);
|
||||||
|
|
||||||
|
renderedWidgets[key] = <Box key={key} pt={2}>
|
||||||
|
<CompositeWidget widgetMetaData={widgetMetaData} data={compositeWidgetData} actionCallback={blockWidgetActionCallback} />
|
||||||
|
</Box>;
|
||||||
|
|
||||||
|
setRenderedWidgets(renderedWidgets);
|
||||||
|
|
||||||
|
return (renderedWidgets[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
// generate the main form body content for a step //
|
// generate the main form body content for a step //
|
||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
@ -382,7 +479,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
if (qJobRunning || step === null)
|
if (qJobRunning || step === null)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Grid m={3} mt={9} container>
|
<Grid m={3} mt={9} container maxWidth="calc(100% - 3rem)">
|
||||||
<Grid item xs={0} lg={3} />
|
<Grid item xs={0} lg={3} />
|
||||||
<Grid item xs={12} lg={6}>
|
<Grid item xs={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
@ -763,8 +860,29 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
component.type === QComponentType.WIDGET && (
|
component.type === QComponentType.WIDGET && (
|
||||||
component.values?.widgetName &&
|
<>
|
||||||
renderWidget(component.values?.widgetName)
|
{
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
// if a widget name is given, render that widget //
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
component.values?.widgetName &&
|
||||||
|
renderWidget(component.values?.widgetName)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
// if the widget is marked as adHoc, render it as such //
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
component.values?.isAdHocWidget &&
|
||||||
|
renderAdHocWidget(component.values, index)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
// if neither of those, then programmer error //
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
!(component.values?.widgetName || component.values?.isAdHocWidget) &&
|
||||||
|
<Alert severity="error">Error: Component is marked as WIDGET type, but does not specify a <u>widgetName</u>, nor the <u>isAdHocWidget</u> flag.</Alert>
|
||||||
|
}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -864,6 +982,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
setActiveStepIndex(newIndex);
|
setActiveStepIndex(newIndex);
|
||||||
setOverrideOnLastStep(null);
|
setOverrideOnLastStep(null);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// reset formik touched data, so a field that's repeated doesn't immediately show a 'dirty' state //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
formikSetTouched({}, false);
|
||||||
|
|
||||||
if (steps)
|
if (steps)
|
||||||
{
|
{
|
||||||
const activeStep = steps[newIndex];
|
const activeStep = steps[newIndex];
|
||||||
@ -899,7 +1022,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
|
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
|
||||||
{
|
{
|
||||||
addField("doFullValidation", {type: "radio"}, "true", null);
|
addField("doFullValidation", {type: "radio"}, "true", null);
|
||||||
setOverrideOnLastStep(false);
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// so - if we're on the validation screen, and we don't have a validationSummary right now, //
|
||||||
|
// and the process supports doing full validation - then the user will choose, via radio, //
|
||||||
|
// if this is the last step or not - and by default that radio will be true, to make this //
|
||||||
|
// NOT the last step - so set this value. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(!processValues["validationSummary"] && processValues["supportsFullValidation"])
|
||||||
|
{
|
||||||
|
setOverrideOnLastStep(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doesStepHaveComponent(activeStep, QComponentType.GOOGLE_DRIVE_SELECT_FOLDER))
|
if (doesStepHaveComponent(activeStep, QComponentType.GOOGLE_DRIVE_SELECT_FOLDER))
|
||||||
@ -909,6 +1042,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
|
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (doesStepHaveComponent(activeStep, QComponentType.WIDGET))
|
||||||
|
{
|
||||||
|
ProcessWidgetBlockUtils.addFieldsForCompositeWidget(activeStep, (fieldMetaData) =>
|
||||||
|
{
|
||||||
|
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
|
||||||
|
const validation = DynamicFormUtils.getValidationForField(fieldMetaData);
|
||||||
|
addField(fieldMetaData.name, dynamicField, processValues[fieldMetaData.name], validation)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
// if this step has form fields, set up the form //
|
// if this step has form fields, set up the form //
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
@ -994,7 +1137,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
setValidationFunction(() => true);
|
setValidationFunction(() => true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [newStep]);
|
}, [newStep, stepInstanceCounter]); // maybe we could just use stepInstanceCounter...
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if there are records to load: build a record config, and set the needRecords state flag //
|
// if there are records to load: build a record config, and set the needRecords state flag //
|
||||||
@ -1088,6 +1231,47 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
}, [needRecords]);
|
}, [needRecords]);
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function updateFieldsInProcess(steps: QFrontendStepMetaData[], updatedFields: Map<string, QFieldMetaData>)
|
||||||
|
{
|
||||||
|
if (updatedFields)
|
||||||
|
{
|
||||||
|
updatedFields.forEach((field) => previouslySeenUpdatedFieldMetaDataMap.set(field.name, field));
|
||||||
|
setPreviouslySeenUpdatedFieldMetaDataMap(previouslySeenUpdatedFieldMetaDataMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let step of steps)
|
||||||
|
{
|
||||||
|
if (step && step.formFields)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < step.formFields.length; i++)
|
||||||
|
{
|
||||||
|
let field = step.formFields[i];
|
||||||
|
if (previouslySeenUpdatedFieldMetaDataMap.has(field.name))
|
||||||
|
{
|
||||||
|
step.formFields[i] = previouslySeenUpdatedFieldMetaDataMap.get(field.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processValues.inputFieldList)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < processValues.inputFieldList.length; i++)
|
||||||
|
{
|
||||||
|
let field = new QFieldMetaData(processValues.inputFieldList[i]);
|
||||||
|
if (previouslySeenUpdatedFieldMetaDataMap.has(field.name))
|
||||||
|
{
|
||||||
|
processValues.inputFieldList[i] = previouslySeenUpdatedFieldMetaDataMap.get(field.name); // todo - uh, not an object?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
|
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -1112,13 +1296,19 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let frontendSteps = steps;
|
let frontendSteps = steps;
|
||||||
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
const updatedFrontendStepList = qJobComplete.processMetaDataAdjustment?.updatedFrontendStepList;
|
||||||
if (updatedFrontendStepList)
|
if (updatedFrontendStepList)
|
||||||
{
|
{
|
||||||
setSteps(updatedFrontendStepList);
|
|
||||||
frontendSteps = updatedFrontendStepList;
|
frontendSteps = updatedFrontendStepList;
|
||||||
|
setSteps(frontendSteps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// always merge the new updatedFields map (if there is one) with existing updates and existing fields //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
updateFieldsInProcess(frontendSteps, qJobComplete.processMetaDataAdjustment?.updatedFields);
|
||||||
|
setSteps(frontendSteps);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
// if the next screen has any PVS fields - look up their labels (display values) //
|
// if the next screen has any PVS fields - look up their labels (display values) //
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -1159,7 +1349,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
setJobUUID(null);
|
setJobUUID(null);
|
||||||
setNewStep(nextStepName);
|
setNewStep(nextStepName);
|
||||||
|
setStepInstanceCounter(1 + stepInstanceCounter);
|
||||||
setProcessValues(newValues);
|
setProcessValues(newValues);
|
||||||
|
setRenderedWidgets({});
|
||||||
setQJobRunning(null);
|
setQJobRunning(null);
|
||||||
|
|
||||||
if (formikSetFieldValueFunction)
|
if (formikSetFieldValueFunction)
|
||||||
@ -1415,8 +1607,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// handle user submitting the form - which in qqq means moving forward from any screen. //
|
// handle user submitting the form - which in qqq means moving forward from any screen. //
|
||||||
|
// caller can pass in a map of values to be added to the form data too //
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////
|
||||||
const handleSubmit = async (values: any, actions: any) =>
|
const handleSubmit = async (values: any) =>
|
||||||
{
|
{
|
||||||
setFormError(null);
|
setFormError(null);
|
||||||
|
|
||||||
@ -1506,27 +1699,54 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const mainCardStyles: any = {};
|
|
||||||
const formStyles: any = {};
|
const formStyles: any = {};
|
||||||
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
if(isWidget)
|
||||||
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
|
|
||||||
{
|
{
|
||||||
mainCardStyles.background = "#FFFFFF";
|
|
||||||
mainCardStyles.boxShadow = "none";
|
|
||||||
}
|
|
||||||
if (isWidget)
|
|
||||||
{
|
|
||||||
mainCardStyles.background = "none";
|
|
||||||
mainCardStyles.boxShadow = "none";
|
|
||||||
mainCardStyles.border = "none";
|
|
||||||
mainCardStyles.minHeight = "";
|
|
||||||
mainCardStyles.alignItems = "stretch";
|
|
||||||
mainCardStyles.flexGrow = 1;
|
|
||||||
mainCardStyles.display = "flex";
|
|
||||||
formStyles.display = "flex";
|
formStyles.display = "flex";
|
||||||
formStyles.flexGrow = 1;
|
formStyles.flexGrow = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function makeMainCardStyles(theme: Theme)
|
||||||
|
{
|
||||||
|
const mainCardStyles: any = {};
|
||||||
|
|
||||||
|
if(!isWidget && !isModal)
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// remove margin around card for non-widget, non-modal, small //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
mainCardStyles[theme.breakpoints.down("sm")] = {
|
||||||
|
marginLeft: "-1.5rem",
|
||||||
|
marginRight: "-1.5rem",
|
||||||
|
borderRadius: "0"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
||||||
|
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
|
||||||
|
{
|
||||||
|
mainCardStyles.background = "#FFFFFF";
|
||||||
|
mainCardStyles.boxShadow = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWidget)
|
||||||
|
{
|
||||||
|
mainCardStyles.background = "none";
|
||||||
|
mainCardStyles.boxShadow = "none";
|
||||||
|
mainCardStyles.border = "none";
|
||||||
|
mainCardStyles.minHeight = "";
|
||||||
|
mainCardStyles.alignItems = "stretch";
|
||||||
|
mainCardStyles.flexGrow = 1;
|
||||||
|
mainCardStyles.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainCardStyles
|
||||||
|
}
|
||||||
|
|
||||||
let nextButtonLabel = "Next";
|
let nextButtonLabel = "Next";
|
||||||
let nextButtonIcon = "arrow_forward";
|
let nextButtonIcon = "arrow_forward";
|
||||||
if (overrideOnLastStep !== null)
|
if (overrideOnLastStep !== null)
|
||||||
@ -1552,20 +1772,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{({
|
{({
|
||||||
values, errors, touched, isSubmitting, setFieldValue,
|
values, errors, touched, isSubmitting, setFieldValue, setTouched
|
||||||
}) =>
|
}) =>
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
// once we're in the formik form, use its setFieldValue function //
|
// once we're in the formik form, capture some of its functions //
|
||||||
// over top of the default one we created globally //
|
// over top of the default ones we created globally //
|
||||||
///////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
formikSetFieldValueFunction = setFieldValue;
|
formikSetFieldValueFunction = setFieldValue;
|
||||||
|
formikSetTouched = setTouched;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form style={formStyles} id={formId} autoComplete="off">
|
<Form style={formStyles} id={formId} autoComplete="off">
|
||||||
<Card sx={mainCardStyles}>
|
<Card sx={makeMainCardStyles}>
|
||||||
{
|
{
|
||||||
!isWidget && (
|
!isWidget && processMetaData?.stepFlow == "LINEAR" && (
|
||||||
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
|
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
|
||||||
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
||||||
{steps.map((step) => (
|
{steps.map((step) => (
|
||||||
@ -1600,7 +1821,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{/********************************
|
{/********************************
|
||||||
** back &| next/submit buttons **
|
** back &| next/submit buttons **
|
||||||
********************************/}
|
********************************/}
|
||||||
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
<Box mt={3} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
||||||
{true || activeStepIndex === 0 ? (
|
{true || activeStepIndex === 0 ? (
|
||||||
<Box />
|
<Box />
|
||||||
) : (
|
) : (
|
||||||
@ -1610,11 +1831,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
<Box />
|
<Box />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{formError && (
|
|
||||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
|
|
||||||
{formError}
|
|
||||||
</MDTypography>
|
|
||||||
)}
|
|
||||||
{
|
{
|
||||||
noMoreSteps && <QCancelButton
|
noMoreSteps && <QCancelButton
|
||||||
onClickHandler={() => handleCancelClicked(true)}
|
onClickHandler={() => handleCancelClicked(true)}
|
||||||
@ -1650,9 +1866,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<Box py={3} mb={20} className="processRun">
|
<Box py={3} mb={20} className="processRun">
|
||||||
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
|
<Grid container justifyContent="center" alignItems="center" mt={{xs: 0, md: 6}} sx={{height: "100%"}}>
|
||||||
<Grid item xs={12} lg={10} xl={8}>
|
<Grid item xs={12} lg={10} xl={8}>
|
||||||
{form}
|
{form}
|
||||||
|
{formError && <Alert severity="error" onClose={() => setFormError(null)} sx={{position: "fixed", top: "40px", left: "10vw", width: "calc(80vw)", zIndex: "99999"}}>{formError}</Alert>}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
225
src/qqq/pages/processes/ProcessWidgetBlockUtils.tsx
Normal file
225
src/qqq/pages/processes/ProcessWidgetBlockUtils.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QComponentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QComponentType";
|
||||||
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
|
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
|
||||||
|
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Utility functions used by ProcessRun for working with ad-hoc, block &
|
||||||
|
** composite type widgets.
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
export default class ProcessWidgetBlockUtils
|
||||||
|
{
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static isActionCodeValid(actionCode: string, step: QFrontendStepMetaData, processValues: any): boolean
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
// private recursive function to walk the composite tree //
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
function recursiveIsActionCodeValidForCompositeData(compositeWidgetData: CompositeData): boolean
|
||||||
|
{
|
||||||
|
for (let i = 0; i < compositeWidgetData.blocks.length; i++)
|
||||||
|
{
|
||||||
|
const block = compositeWidgetData.blocks[i];
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// skip the block if it has a 'conditional', which isn't true //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
const conditionalFieldName = block.conditional;
|
||||||
|
if (conditionalFieldName)
|
||||||
|
{
|
||||||
|
const value = processValues[conditionalFieldName];
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.blockTypeName == "COMPOSITE")
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// recursive call for composites, but only return if a true is found (in case a subsequent block has a true) //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const isValidForThisBlock = recursiveIsActionCodeValidForCompositeData(block as unknown as CompositeData);
|
||||||
|
if (isValidForThisBlock)
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
// else, continue...
|
||||||
|
}
|
||||||
|
else if (block.blockTypeName == "ACTION_BUTTON")
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
// actually look at actionCodes on action button blocks //
|
||||||
|
//////////////////////////////////////////////////////////
|
||||||
|
if (block.values?.actionCode == actionCode)
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// if code wasn't found, it is invalid //
|
||||||
|
/////////////////////////////////////////
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
// iterate over all components in the current step //
|
||||||
|
/////////////////////////////////////////////////////
|
||||||
|
for (let i = 0; i < step.components.length; i++)
|
||||||
|
{
|
||||||
|
const component = step.components[i];
|
||||||
|
if (component.type == "WIDGET" && component.values?.isAdHocWidget)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for ad-hoc widget components, check if this actionCode exists on any action-button blocks //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const isValidForThisWidget = recursiveIsActionCodeValidForCompositeData(component.values);
|
||||||
|
if (isValidForThisWidget)
|
||||||
|
{
|
||||||
|
return (true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////
|
||||||
|
// upon fallthrough, it's a false //
|
||||||
|
////////////////////////////////////
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** perform evaluations on a compositeWidget's data, given current process
|
||||||
|
** values, to do dynamic stuff, like:
|
||||||
|
** - removing fields with un-true conditions
|
||||||
|
***************************************************************************/
|
||||||
|
public static dynamicEvaluationOfCompositeWidgetData(compositeWidgetData: CompositeData, processValues: any)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < compositeWidgetData.blocks.length; i++)
|
||||||
|
{
|
||||||
|
const block = compositeWidgetData.blocks[i];
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
// if the block has a conditional, evaluate, and remove if untrue //
|
||||||
|
////////////////////////////////////////////////////////////////////
|
||||||
|
const conditionalFieldName = block.conditional;
|
||||||
|
if (conditionalFieldName)
|
||||||
|
{
|
||||||
|
const value = processValues[conditionalFieldName];
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
console.debug(`Splicing away block based on [${conditionalFieldName}]...`);
|
||||||
|
compositeWidgetData.blocks.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (block.blockTypeName == "COMPOSITE")
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// make recursive calls for composites //
|
||||||
|
/////////////////////////////////////////
|
||||||
|
ProcessWidgetBlockUtils.dynamicEvaluationOfCompositeWidgetData(block as unknown as CompositeData, processValues);
|
||||||
|
}
|
||||||
|
else if (block.blockTypeName == "INPUT_FIELD")
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for input fields, put the process's value for the field-name into the block's values object as '.value' //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const fieldName = block.values?.fieldMetaData?.name;
|
||||||
|
if (processValues.hasOwnProperty(fieldName))
|
||||||
|
{
|
||||||
|
block.values.value = processValues[fieldName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (block.blockTypeName == "TEXT")
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// for text-blocks - interpolate ${fieldName} expressions into their process-values //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let text = block.values?.text;
|
||||||
|
if (text)
|
||||||
|
{
|
||||||
|
for (let key of Object.keys(processValues))
|
||||||
|
{
|
||||||
|
text = text.replaceAll("${" + key + "}", processValues[key]);
|
||||||
|
}
|
||||||
|
block.values.interpolatedText = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
public static addFieldsForCompositeWidget(step: QFrontendStepMetaData, addFieldCallback: (fieldMetaData: QFieldMetaData) => void)
|
||||||
|
{
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
// private recursive function to walk the composite tree //
|
||||||
|
///////////////////////////////////////////////////////////
|
||||||
|
function recursiveHelper(widgetData: CompositeData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (let block of widgetData.blocks)
|
||||||
|
{
|
||||||
|
if (block.blockTypeName == "COMPOSITE")
|
||||||
|
{
|
||||||
|
recursiveHelper(block as unknown as CompositeData);
|
||||||
|
}
|
||||||
|
else if (block.blockTypeName == "INPUT_FIELD")
|
||||||
|
{
|
||||||
|
const fieldMetaData = new QFieldMetaData(block.values?.fieldMetaData);
|
||||||
|
addFieldCallback(fieldMetaData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e)
|
||||||
|
{
|
||||||
|
console.log("Error adding fields for compositeWidget: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
// foreach component, if it's an adhoc widget, call recursive helper on it //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////
|
||||||
|
for (let component of step.components)
|
||||||
|
{
|
||||||
|
if (component.type == QComponentType.WIDGET && component.values?.isAdHocWidget)
|
||||||
|
{
|
||||||
|
recursiveHelper(component.values as unknown as CompositeData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -779,13 +779,12 @@ function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={tableName}
|
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
|
||||||
fieldName={field.name}
|
|
||||||
fieldLabel="Value"
|
fieldLabel="Value"
|
||||||
initialValue={selectedPossibleValue?.id}
|
initialValue={selectedPossibleValue?.id}
|
||||||
initialDisplayValue={selectedPossibleValue?.label}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
useCase="filter"
|
||||||
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -847,13 +846,12 @@ function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaDa
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={tableName}
|
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: null}}
|
||||||
fieldName={field.name}
|
|
||||||
isMultiple={true}
|
isMultiple={true}
|
||||||
fieldLabel="Value"
|
fieldLabel="Value"
|
||||||
initialValues={selectedPossibleValues}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
useCase="filter"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -33,8 +33,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
|
|||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, Collapse, Menu, Typography} from "@mui/material";
|
import {Alert, Box, Collapse, Menu, Typography} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
@ -92,6 +91,7 @@ interface Props
|
|||||||
launchProcess?: QProcessMetaData;
|
launchProcess?: QProcessMetaData;
|
||||||
usage?: QueryScreenUsage;
|
usage?: QueryScreenUsage;
|
||||||
isModal?: boolean;
|
isModal?: boolean;
|
||||||
|
isPreview?: boolean;
|
||||||
initialQueryFilter?: QQueryFilter;
|
initialQueryFilter?: QQueryFilter;
|
||||||
initialColumns?: QQueryColumns;
|
initialColumns?: QQueryColumns;
|
||||||
allowVariables?: boolean;
|
allowVariables?: boolean;
|
||||||
@ -126,7 +126,7 @@ const getLoadingScreen = (isModal: boolean) =>
|
|||||||
**
|
**
|
||||||
** Yuge component. The best. Lots of very smart people are saying so.
|
** Yuge component. The best. Lots of very smart people are saying so.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||||
{
|
{
|
||||||
const tableName = table.name;
|
const tableName = table.name;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@ -884,6 +884,18 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Opens a new query screen in a new window with the current filter
|
||||||
|
*******************************************************************************/
|
||||||
|
const openFilterInNewWindow = () =>
|
||||||
|
{
|
||||||
|
let filterForBackend = JSON.parse(JSON.stringify(view.queryFilter));
|
||||||
|
filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend);
|
||||||
|
const url = `${metaData?.getTablePathByName(tableName)}?filter=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
||||||
|
window.open(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** This is the method that actually executes a query to update the data in the table.
|
** This is the method that actually executes a query to update the data in the table.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -2232,12 +2244,25 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
|||||||
return (
|
return (
|
||||||
<GridToolbarContainer>
|
<GridToolbarContainer>
|
||||||
<div>
|
<div>
|
||||||
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
<Tooltip title="Refresh Query">
|
||||||
</div>
|
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||||
<div style={{position: "relative"}}>
|
</Tooltip>
|
||||||
{/* @ts-ignore */}
|
|
||||||
<GridToolbarDensitySelector nonce={undefined} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
!isPreview && (
|
||||||
|
<div style={{position: "relative"}}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<GridToolbarDensitySelector nonce={undefined} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isPreview && (
|
||||||
|
<Tooltip title="Open In New Window">
|
||||||
|
<Button id="open-filter-in-new-window-button" onClick={() => openFilterInNewWindow()} startIcon={<Icon>launch</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
usage == "queryScreen" &&
|
usage == "queryScreen" &&
|
||||||
@ -2872,7 +2897,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
metaData && tableMetaData &&
|
!isPreview && metaData && tableMetaData &&
|
||||||
<BasicAndAdvancedQueryControls
|
<BasicAndAdvancedQueryControls
|
||||||
ref={basicAndAdvancedQueryControlsRef}
|
ref={basicAndAdvancedQueryControlsRef}
|
||||||
metaData={metaData}
|
metaData={metaData}
|
||||||
|
50
src/qqq/utils/DumpJsonBox.tsx
Normal file
50
src/qqq/utils/DumpJsonBox.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface DumpJsonBoxProps
|
||||||
|
{
|
||||||
|
data: any;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
** Utillity for debugging an object as JSON
|
||||||
|
***************************************************************************/
|
||||||
|
export default function DumpJsonBox({data, title}: DumpJsonBoxProps): JSX.Element
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<Box border="1px solid gray" my="1rem" borderRadius="0.5rem">
|
||||||
|
{
|
||||||
|
title &&
|
||||||
|
<Box borderBottom="1px solid gray" mb="0.5rem" px="0.25rem" borderRadius="0.5rem 0.5rem 0 0" fontSize="1rem" fontWeight="600kkk" sx={{backgroundColor: "#D0D0D0"}}>
|
||||||
|
{title}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
<Box maxHeight="200px" p="0.25rem" overflow="auto" sx={{whiteSpace: "pre-wrap", fontFamily: "monospace", fontSize: "0.75rem", lineHeight: "1.2"}}>
|
||||||
|
{JSON.stringify(data, null, 3)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -268,7 +268,15 @@ class ValueUtils
|
|||||||
{
|
{
|
||||||
if (!(date instanceof Date))
|
if (!(date instanceof Date))
|
||||||
{
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// so, a new Date here will interpret the string as being at midnight UTC, but //
|
||||||
|
// the data object will be in the user/browser timezone. //
|
||||||
|
// so "2024-08-22", for a user in US/Central, will be "2024-08-21T19:00:00-0500". //
|
||||||
|
// correct for that by adding the date's timezone offset (converted from minutes //
|
||||||
|
// to millis) back to it //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
|
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000)
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (`${date.toString("yyyy-MM-dd")}`);
|
return (`${date.toString("yyyy-MM-dd")}`);
|
||||||
|
Reference in New Issue
Block a user