mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-21 06:38:43 +00:00
Compare commits
72 Commits
snapshot-f
...
snapshot-i
Author | SHA1 | Date | |
---|---|---|---|
a48f2d5274 | |||
6dfc839c30 | |||
66fc4785da | |||
53f8bff40c | |||
726906061d | |||
2514c463a6 | |||
b41a9a6fe6 | |||
cfca47054e | |||
b48ef70c5e | |||
81efb7e18d | |||
387f09f4ad | |||
4fd50936ea | |||
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 | |||
dc20c3d5ec | |||
71a9c6470a | |||
765d40aef1 | |||
d9f1642f0a | |||
858540427d | |||
eecb2d4489 | |||
5a6293cfdf | |||
868022408c | |||
d090a665ff | |||
f112cf5543 | |||
0c2dcb1215 | |||
418f7957a2 | |||
8be8bf367a | |||
1ca1313a25 | |||
4533815535 | |||
4230f34b15 | |||
e08e37222b | |||
0ffada6aec | |||
9f04d897a1 | |||
e604f47231 | |||
93f5bb688c | |||
3fa017e8b9 | |||
9d5af539b9 | |||
97bab57974 | |||
d9de96ea7f | |||
ff839d85fd | |||
d31215f6c0 | |||
262855b9c0 | |||
4d082c3c57 | |||
45b6b42836 | |||
47fb7cc2e3 | |||
647c63f5a3 |
@ -115,7 +115,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(main|integration.*)/
|
||||
ignore: /(main|dev|integration.*)/
|
||||
tags:
|
||||
ignore: /(version|snapshot)-.*/
|
||||
deploy:
|
||||
@ -124,7 +124,7 @@ workflows:
|
||||
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
|
||||
filters:
|
||||
branches:
|
||||
only: /(main|integration.*)/
|
||||
only: /(main|dev|integration.*)/
|
||||
tags:
|
||||
only: /(version|snapshot)-.*/
|
||||
|
||||
|
8578
package-lock.json
generated
8578
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@
|
||||
"@auth0/auth0-react": "1.10.2",
|
||||
"@emotion/react": "11.7.1",
|
||||
"@emotion/styled": "11.6.0",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.104",
|
||||
"@kingsrook/qqq-frontend-core": "1.0.110",
|
||||
"@mui/icons-material": "5.4.1",
|
||||
"@mui/material": "5.11.1",
|
||||
"@mui/styles": "5.11.1",
|
||||
|
6
pom.xml
6
pom.xml
@ -29,7 +29,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<revision>0.20.0-SNAPSHOT</revision>
|
||||
<revision>0.23.0-SNAPSHOT</revision>
|
||||
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
@ -66,7 +66,7 @@
|
||||
<dependency>
|
||||
<groupId>com.kingsrook.qqq</groupId>
|
||||
<artifactId>qqq-backend-core</artifactId>
|
||||
<version>0.20.0-20240308.165846-65</version>
|
||||
<version>0.21.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
@ -154,11 +154,11 @@
|
||||
<versionTagPrefix>version-</versionTagPrefix>
|
||||
</gitFlowConfig>
|
||||
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
|
||||
<postReleaseGoals>install</postReleaseGoals> <!-- Let CI run deploys -->
|
||||
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
|
||||
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
||||
<versionProperty>revision</versionProperty>
|
||||
<skipUpdateVersion>true</skipUpdateVersion>
|
||||
<skipTestProject>true</skipTestProject> <!-- we allow CI to do the tests -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
|
@ -30,14 +30,17 @@ import MDButton from "qqq/components/legacy/MDButton";
|
||||
|
||||
export const standardWidth = "150px";
|
||||
|
||||
const standardML = {xs: 1, md: 3};
|
||||
|
||||
interface QCreateNewButtonProps
|
||||
{
|
||||
tablePath: string;
|
||||
}
|
||||
|
||||
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
||||
{
|
||||
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`}>
|
||||
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||
Create New
|
||||
@ -54,6 +57,7 @@ interface QSaveButtonProps
|
||||
onClickHandler?: any,
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
QSaveButton.defaultProps = {
|
||||
label: "Save",
|
||||
iconName: "save"
|
||||
@ -62,7 +66,7 @@ QSaveButton.defaultProps = {
|
||||
export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
||||
{
|
||||
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}>
|
||||
{label}
|
||||
</MDButton>
|
||||
@ -72,17 +76,18 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
|
||||
|
||||
interface QDeleteButtonProps
|
||||
{
|
||||
onClickHandler: any
|
||||
disabled?: boolean
|
||||
onClickHandler: any;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
QDeleteButton.defaultProps = {
|
||||
disabled: false
|
||||
};
|
||||
|
||||
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
|
||||
{
|
||||
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}>
|
||||
Delete
|
||||
</MDButton>
|
||||
@ -93,7 +98,7 @@ export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): J
|
||||
export function QEditButton(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box ml={3} width={standardWidth}>
|
||||
<Box ml={standardML} width={standardWidth}>
|
||||
<Link to="edit">
|
||||
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
||||
Edit
|
||||
@ -132,7 +137,7 @@ interface QCancelButtonProps
|
||||
onClickHandler: any;
|
||||
disabled: boolean;
|
||||
label?: string;
|
||||
iconName?: string
|
||||
iconName?: string;
|
||||
}
|
||||
|
||||
export function QCancelButton({
|
||||
@ -140,7 +145,7 @@ export function QCancelButton({
|
||||
}: QCancelButtonProps): JSX.Element
|
||||
{
|
||||
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}>
|
||||
{label}
|
||||
</MDButton>
|
||||
@ -155,15 +160,15 @@ QCancelButton.defaultProps = {
|
||||
|
||||
interface QSubmitButtonProps
|
||||
{
|
||||
label?: string
|
||||
iconName?: string
|
||||
disabled: boolean
|
||||
label?: string;
|
||||
iconName?: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
|
||||
{
|
||||
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}>
|
||||
{label}
|
||||
</MDButton>
|
||||
|
@ -172,17 +172,14 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
||||
<Grid item xs={12} sm={6} key={fieldName}>
|
||||
{labelElement}
|
||||
<DynamicSelect
|
||||
tableName={field.possibleValueProps.tableName}
|
||||
processName={field.possibleValueProps.processName}
|
||||
possibleValueSourceName={field.possibleValueProps.possibleValueSourceName}
|
||||
fieldName={field.possibleValueProps.fieldName}
|
||||
fieldPossibleValueProps={field.possibleValueProps}
|
||||
isEditable={field.isEditable}
|
||||
fieldLabel=""
|
||||
initialValue={values[fieldName]}
|
||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
||||
bulkEditMode={bulkEditMode}
|
||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||
otherValues={otherValuesMap}
|
||||
useCase="form"
|
||||
/>
|
||||
{formattedHelpContent}
|
||||
</Grid>
|
||||
|
@ -40,6 +40,8 @@ interface Props
|
||||
value: any;
|
||||
type: string;
|
||||
isEditable?: boolean;
|
||||
placeholder?: string;
|
||||
backgroundColor?: string;
|
||||
|
||||
[key: string]: any;
|
||||
|
||||
@ -49,7 +51,7 @@ interface Props
|
||||
}
|
||||
|
||||
function QDynamicFormField({
|
||||
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, formFieldObject, ...rest
|
||||
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, placeholder, backgroundColor, formFieldObject, ...rest
|
||||
}: Props): JSX.Element
|
||||
{
|
||||
const [switchChecked, setSwitchChecked] = useState(false);
|
||||
@ -65,18 +67,30 @@ function QDynamicFormField({
|
||||
inputLabelProps.shrink = true;
|
||||
}
|
||||
|
||||
const inputProps = {};
|
||||
const inputProps: any = {};
|
||||
if (displayFormat && displayFormat.startsWith("$"))
|
||||
{
|
||||
// @ts-ignore
|
||||
inputProps.startAdornment = <InputAdornment position="start">$</InputAdornment>;
|
||||
}
|
||||
if (displayFormat && displayFormat.endsWith("%%"))
|
||||
{
|
||||
// @ts-ignore
|
||||
inputProps.endAdornment = <InputAdornment position="end">%</InputAdornment>;
|
||||
}
|
||||
|
||||
if (placeholder)
|
||||
{
|
||||
inputProps.placeholder = placeholder
|
||||
}
|
||||
|
||||
if(backgroundColor)
|
||||
{
|
||||
inputProps.sx = {
|
||||
"&.MuiInputBase-root": {
|
||||
backgroundColor: backgroundColor
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const handleOnWheel = (e) =>
|
||||
{
|
||||
|
@ -22,6 +22,7 @@
|
||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||
import * as Yup from "yup";
|
||||
|
||||
|
||||
@ -129,18 +130,11 @@ class DynamicFormUtils
|
||||
|
||||
if (effectivelyIsRequired)
|
||||
{
|
||||
if (field.possibleValueSourceName)
|
||||
{
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
|
||||
// rather, it's more like "null is how empty will be treated" or some-such... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (Yup.string().required(`${field.label} is required.`).nullable(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
return (Yup.string().required(`${field.label} is required.`));
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
|
||||
// rather, it's more like "null is how empty will be treated" or some-such... //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
return (Yup.string().required(`${field.label ?? "This field"} is required.`).nullable(true));
|
||||
}
|
||||
return (null);
|
||||
}
|
||||
@ -155,47 +149,49 @@ class DynamicFormUtils
|
||||
{
|
||||
const field = qFields[i];
|
||||
|
||||
if(!dynamicFormFields[field.name])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
// 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)
|
||||
{
|
||||
initialDisplayValue = displayValues.get(field.name);
|
||||
props.initialDisplayValue = displayValues.get(field.name);
|
||||
}
|
||||
|
||||
if (tableName)
|
||||
if(field.inlinePossibleValueSource)
|
||||
{
|
||||
dynamicFormFields[field.name].possibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
tableName: tableName,
|
||||
fieldName: field.name,
|
||||
initialDisplayValue: initialDisplayValue,
|
||||
};
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// handle an inline PVS - which is a list of possible value objects //
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
props.possibleValues = field.inlinePossibleValueSource;
|
||||
}
|
||||
else if (tableName)
|
||||
{
|
||||
props.tableName = tableName;
|
||||
}
|
||||
else if (processName)
|
||||
{
|
||||
dynamicFormFields[field.name].possibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
processName: processName,
|
||||
fieldName: field.name,
|
||||
initialDisplayValue: initialDisplayValue,
|
||||
};
|
||||
props.processName = processName;
|
||||
}
|
||||
else
|
||||
{
|
||||
dynamicFormFields[field.name].possibleValueProps =
|
||||
{
|
||||
isPossibleValue: true,
|
||||
initialDisplayValue: initialDisplayValue,
|
||||
fieldName: field.name,
|
||||
possibleValueSourceName: field.possibleValueSourceName
|
||||
};
|
||||
props.possibleValueSourceName = field.possibleValueSourceName;
|
||||
}
|
||||
|
||||
dynamicFormFields[field.name].possibleValueProps = props;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,20 +30,17 @@ import TextField from "@mui/material/TextField";
|
||||
import {ErrorMessage, useFormikContext} from "formik";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
interface Props
|
||||
{
|
||||
tableName?: string;
|
||||
processName?: string;
|
||||
fieldName?: string;
|
||||
possibleValueSourceName?: string;
|
||||
fieldPossibleValueProps: FieldPossibleValueProps;
|
||||
overrideId?: string;
|
||||
fieldLabel: string;
|
||||
inForm: boolean;
|
||||
initialValue?: any;
|
||||
initialDisplayValue?: string;
|
||||
initialValues?: QPossibleValue[];
|
||||
onChange?: any;
|
||||
isEditable?: boolean;
|
||||
@ -53,16 +50,12 @@ interface Props
|
||||
otherValues?: Map<string, any>;
|
||||
variant: "standard" | "outlined";
|
||||
initiallyOpen: boolean;
|
||||
useCase: "form" | "filter";
|
||||
}
|
||||
|
||||
DynamicSelect.defaultProps = {
|
||||
tableName: null,
|
||||
processName: null,
|
||||
fieldName: null,
|
||||
possibleValueSourceName: null,
|
||||
inForm: true,
|
||||
initialValue: null,
|
||||
initialDisplayValue: null,
|
||||
initialValues: undefined,
|
||||
onChange: null,
|
||||
isEditable: true,
|
||||
@ -102,8 +95,10 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
|
||||
|
||||
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 [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState(null);
|
||||
@ -171,6 +166,35 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
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(() =>
|
||||
{
|
||||
if (firstRender)
|
||||
@ -194,7 +218,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
(async () =>
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
@ -217,7 +241,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
};
|
||||
}, [searchTerm]);
|
||||
|
||||
// todo - finish... call it in onOpen?
|
||||
|
||||
/***************************************************************************
|
||||
** todo - finish... call it in onOpen?
|
||||
***************************************************************************/
|
||||
const reloadIfOtherValuesAreChanged = () =>
|
||||
{
|
||||
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
||||
@ -226,8 +253,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
{
|
||||
setLoading(true);
|
||||
setOptions([]);
|
||||
|
||||
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);
|
||||
setOptions([...results]);
|
||||
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) =>
|
||||
{
|
||||
// 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) =>
|
||||
{
|
||||
setSearchTerm(null);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||
{
|
||||
// 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; }[] =>
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
@ -282,6 +327,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
return (options);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
// @ts-ignore
|
||||
const renderOption = (props: Object, option: any, {selected}) =>
|
||||
{
|
||||
@ -330,6 +379,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const bulkEditSwitchChanged = () =>
|
||||
{
|
||||
const newSwitchValue = !switchChecked;
|
||||
@ -350,7 +403,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
const autocomplete = (
|
||||
<Box>
|
||||
<Autocomplete
|
||||
id={overrideId ?? fieldName ?? possibleValueSourceName}
|
||||
id={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"}
|
||||
sx={autocompleteSX}
|
||||
open={open}
|
||||
fullWidth
|
||||
@ -430,7 +483,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
||||
inForm &&
|
||||
<Box mt={0.75}>
|
||||
<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>
|
||||
</Box>
|
||||
}
|
||||
|
@ -502,7 +502,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
/////////////////////////////////////////////////
|
||||
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.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
|
||||
@ -602,7 +602,7 @@ function EntityForm(props: Props): JSX.Element
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
if (fieldMetaData.possibleValueSourceName)
|
||||
{
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
|
||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], undefined, "form");
|
||||
if (results && results.length > 0)
|
||||
{
|
||||
defaultDisplayValues.set(fieldName, results[0].label);
|
||||
@ -1152,11 +1152,11 @@ function EntityForm(props: Props): JSX.Element
|
||||
<Grid container spacing={3}>
|
||||
{
|
||||
!props.isModal &&
|
||||
<Grid item xs={12} lg={3}>
|
||||
<Grid item xs={12} lg={3} className="recordSidebar">
|
||||
<QRecordSidebar tableSections={tableSections} />
|
||||
</Grid>
|
||||
}
|
||||
<Grid item xs={12} lg={props.isModal ? 12 : 9}>
|
||||
<Grid item xs={12} lg={props.isModal ? 12 : 9} className={props.isModal ? "" : "recordWithSidebar"}>
|
||||
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
|
@ -64,13 +64,14 @@ function Footer({company, links}: Props): JSX.Element
|
||||
<Box
|
||||
width="100%"
|
||||
display="flex"
|
||||
flexDirection={{xs: "column", lg: "row"}}
|
||||
flexDirection={{xs: "column", md: "row"}}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
px={1.5}
|
||||
style={{
|
||||
position: "fixed", bottom: "0px", zIndex: -1, marginBottom: "10px",
|
||||
}}
|
||||
left={{xs: "0", xl: "auto"}}
|
||||
>
|
||||
{
|
||||
href && name &&
|
||||
|
@ -25,6 +25,7 @@ import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
||||
@ -225,6 +226,19 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
|
||||
const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// set the right-half of the navbar up so that below the 'md' breakpoint, it just disappears //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
const navbarRowRight = (theme: Theme, {isMini}: any) =>
|
||||
{
|
||||
return {
|
||||
[theme.breakpoints.down("md")]: {
|
||||
display: "none",
|
||||
},
|
||||
...navbarRow(theme, isMini)
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position={absolute ? "absolute" : navbarType}
|
||||
@ -241,7 +255,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
|
||||
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
||||
</Box>
|
||||
{isMini ? null : (
|
||||
<Box sx={(theme) => navbarRow(theme, {isMini})}>
|
||||
<Box sx={(theme) => navbarRowRight(theme, {isMini})}>
|
||||
<Box mt={"-0.25rem"} pb={"0.75rem"} pr={2} mr={-2} sx={{"& *": {cursor: "pointer !important"}}}>
|
||||
{renderHistory()}
|
||||
</Box>
|
||||
|
74
src/qqq/components/misc/ErrorBoundary.tsx
Normal file
74
src/qqq/components/misc/ErrorBoundary.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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 React, {Component, ErrorInfo} from "react";
|
||||
|
||||
interface Props
|
||||
{
|
||||
errorElement?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State
|
||||
{
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
** Component that you can wrap around other components that might throw an error,
|
||||
** to give some isolation, rather than breaking a whole page.
|
||||
** Credit: https://medium.com/@bobjunior542/how-to-use-error-boundaries-in-react-js-with-typescript-ee90ec814bf1
|
||||
*******************************************************************************/
|
||||
class ErrorBoundary extends Component<Props, State>
|
||||
{
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
constructor(props: Props)
|
||||
{
|
||||
super(props);
|
||||
this.state = {hasError: false};
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo)
|
||||
{
|
||||
console.error("ErrorBoundary caught an error: ", error, errorInfo);
|
||||
this.setState({hasError: true});
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
render()
|
||||
{
|
||||
if (this.state.hasError)
|
||||
{
|
||||
return this.props.errorElement ?? <span>(Error)</span>;
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
@ -22,6 +22,7 @@
|
||||
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
|
||||
import Box from "@mui/material/Box";
|
||||
import parse from "html-react-parser";
|
||||
import ErrorBoundary from "qqq/components/misc/ErrorBoundary";
|
||||
import React, {useContext} from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import QContext from "QContext";
|
||||
@ -128,6 +129,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
||||
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
|
||||
|
||||
let content = null;
|
||||
let errorContent = "Error rendering help content.";
|
||||
if (helpHelpActive)
|
||||
{
|
||||
if (!selectedHelpContent)
|
||||
@ -135,6 +137,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
||||
selectedHelpContent = new QHelpContent({content: ""});
|
||||
}
|
||||
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
|
||||
errorContent += ` [${helpContentKey ?? "?"}]`;
|
||||
}
|
||||
else if(selectedHelpContent)
|
||||
{
|
||||
@ -148,7 +151,9 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
|
||||
{
|
||||
return <Box display="inline" className="helpContent">
|
||||
{heading && <span className="header">{heading}</span>}
|
||||
{formatHelpContent(content, selectedHelpContent.format)}
|
||||
<ErrorBoundary errorElement={<i>{errorContent}</i>}>
|
||||
{formatHelpContent(content, selectedHelpContent.format)}
|
||||
</ErrorBoundary>
|
||||
</Box>;
|
||||
}
|
||||
|
||||
|
@ -84,7 +84,7 @@ function ProcessSummaryResults({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box m={3} mt={6}>
|
||||
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||
<Grid container>
|
||||
<Grid item xs={0} lg={2} />
|
||||
<Grid item xs={12} lg={8}>
|
||||
|
@ -273,7 +273,7 @@ function ValidationReview({
|
||||
);
|
||||
|
||||
return (
|
||||
<Box m={3}>
|
||||
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} lg={6}>
|
||||
<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...)
|
||||
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;
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export function EvaluatedExpression({field, expression}: EvaluatedExpressionProp
|
||||
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;
|
||||
|
@ -367,16 +367,15 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
) : (
|
||||
<Box width={"100%"}>
|
||||
<DynamicSelect
|
||||
tableName={table.name}
|
||||
fieldName={field.name}
|
||||
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
|
||||
overrideId={field.name + "-single-" + criteria.id}
|
||||
key={field.name + "-single-" + criteria.id}
|
||||
fieldLabel="Value"
|
||||
initialValue={selectedPossibleValue?.id}
|
||||
initialDisplayValue={selectedPossibleValue?.label}
|
||||
inForm={false}
|
||||
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
||||
variant="standard"
|
||||
useCase="filter"
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@ -401,8 +400,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
}
|
||||
return <Box>
|
||||
<DynamicSelect
|
||||
tableName={table.name}
|
||||
fieldName={field.name}
|
||||
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
||||
overrideId={field.name + "-multi-" + criteria.id}
|
||||
key={field.name + "-multi-" + criteria.id}
|
||||
isMultiple
|
||||
@ -412,6 +410,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
||||
inForm={false}
|
||||
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
||||
variant="standard"
|
||||
useCase="filter"
|
||||
/>
|
||||
</Box>;
|
||||
}
|
||||
|
@ -40,16 +40,17 @@ import Snackbar from "@mui/material/Snackbar";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import FormData from "form-data";
|
||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||
import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm";
|
||||
import ScriptTestForm from "qqq/components/scripts/ScriptTestForm";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
|
||||
import "ace-builds/src-noconflict/ace";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
import React, {useEffect, useReducer, useRef, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
|
||||
export interface ScriptEditorProps
|
||||
@ -69,15 +70,15 @@ const qController = Client.getInstance();
|
||||
|
||||
function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
||||
{
|
||||
const rs: {[name: string]: string} = {};
|
||||
const rs: { [name: string]: string } = {};
|
||||
|
||||
if(!scriptTypeFileSchemaList)
|
||||
if (!scriptTypeFileSchemaList)
|
||||
{
|
||||
console.log("Missing scriptTypeFileSchemaList");
|
||||
}
|
||||
else
|
||||
{
|
||||
let files = scriptRevisionRecord?.associatedRecords?.get("files")
|
||||
let files = scriptRevisionRecord?.associatedRecords?.get("files");
|
||||
|
||||
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
||||
{
|
||||
@ -88,7 +89,7 @@ function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFi
|
||||
for (let j = 0; j < files?.length; j++)
|
||||
{
|
||||
let file = files[j];
|
||||
if(file.values.get("fileName") == name)
|
||||
if (file.values.get("fileName") == name)
|
||||
{
|
||||
contents = file.values.get("contents");
|
||||
}
|
||||
@ -103,9 +104,9 @@ function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFi
|
||||
|
||||
function buildFileTypeMap(scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
||||
{
|
||||
const rs: {[name: string]: string} = {};
|
||||
const rs: { [name: string]: string } = {};
|
||||
|
||||
if(!scriptTypeFileSchemaList)
|
||||
if (!scriptTypeFileSchemaList)
|
||||
{
|
||||
console.log("Missing scriptTypeFileSchemaList");
|
||||
}
|
||||
@ -125,21 +126,21 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
{
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
|
||||
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
|
||||
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
|
||||
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null)
|
||||
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null);
|
||||
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null);
|
||||
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null);
|
||||
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null);
|
||||
|
||||
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
|
||||
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
|
||||
const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema);
|
||||
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]])
|
||||
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList))
|
||||
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList))
|
||||
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]]);
|
||||
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList));
|
||||
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList));
|
||||
console.log(`file types: ${JSON.stringify(fileTypes)}`);
|
||||
|
||||
const [commitMessage, setCommitMessage] = useState("")
|
||||
const [commitMessage, setCommitMessage] = useState("");
|
||||
const [openTool, setOpenTool] = useState(null);
|
||||
const [errorAlert, setErrorAlert] = useState("")
|
||||
const [errorAlert, setErrorAlert] = useState("");
|
||||
const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
const ref = useRef();
|
||||
@ -241,19 +242,19 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
// need this to make Ace recognize new height.
|
||||
setTimeout(() =>
|
||||
{
|
||||
window.dispatchEvent(new Event("resize"))
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const saveClicked = (overrideCommitMessage?: string) =>
|
||||
{
|
||||
if(!apiName || !apiVersion)
|
||||
if (!apiName || !apiVersion)
|
||||
{
|
||||
setErrorAlert("You must select a value for both API Name and API Version.")
|
||||
setErrorAlert("You must select a value for both API Name and API Version.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!commitMessage && !overrideCommitMessage)
|
||||
if (!commitMessage && !overrideCommitMessage)
|
||||
{
|
||||
setPromptForCommitMessageOpen(true);
|
||||
return;
|
||||
@ -267,18 +268,18 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
formData.append("scriptId", scriptId);
|
||||
formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
|
||||
|
||||
if(apiName)
|
||||
if (apiName)
|
||||
{
|
||||
formData.append("apiName", apiName);
|
||||
}
|
||||
|
||||
if(apiVersion)
|
||||
if (apiVersion)
|
||||
{
|
||||
formData.append("apiVersion", apiVersion);
|
||||
}
|
||||
|
||||
|
||||
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
|
||||
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
|
||||
formData.append("fileNames", fileNamesFromSchema.join(","));
|
||||
|
||||
for (let fileName in fileContents)
|
||||
@ -299,58 +300,58 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
|
||||
if (processResult instanceof QJobError)
|
||||
{
|
||||
const jobError = processResult as QJobError
|
||||
setErrorAlert(jobError.userFacingError ?? jobError.error)
|
||||
const jobError = processResult as QJobError;
|
||||
setErrorAlert(jobError.userFacingError ?? jobError.error);
|
||||
setClosing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
closeCallback(null, "saved", "Saved New Script Version");
|
||||
}
|
||||
catch(e)
|
||||
catch (e)
|
||||
{
|
||||
// @ts-ignore
|
||||
setErrorAlert(e.message ?? "Unexpected error saving script")
|
||||
setErrorAlert(e.message ?? "Unexpected error saving script");
|
||||
setClosing(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
};
|
||||
|
||||
const cancelClicked = () =>
|
||||
{
|
||||
setClosing(true);
|
||||
closeCallback(null, "cancelled");
|
||||
}
|
||||
};
|
||||
|
||||
const updateCode = (value: string, event: any, index: number) =>
|
||||
{
|
||||
fileContents[openEditorFileNames[index]] = value;
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
setCommitMessage(event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) =>
|
||||
{
|
||||
setPromptForCommitMessageOpen(false);
|
||||
|
||||
if(wasSaveClicked)
|
||||
if (wasSaveClicked)
|
||||
{
|
||||
setCommitMessage(message)
|
||||
setCommitMessage(message);
|
||||
saveClicked(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
setClosing(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changeApiName = (apiNamePossibleValue?: QPossibleValue) =>
|
||||
{
|
||||
if(apiNamePossibleValue)
|
||||
if (apiNamePossibleValue)
|
||||
{
|
||||
setApiName(apiNamePossibleValue.id);
|
||||
}
|
||||
@ -358,11 +359,11 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
{
|
||||
setApiName(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) =>
|
||||
{
|
||||
if(apiVersionPossibleValue)
|
||||
if (apiVersionPossibleValue)
|
||||
{
|
||||
setApiVersion(apiVersionPossibleValue.id);
|
||||
}
|
||||
@ -370,33 +371,33 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
{
|
||||
setApiVersion(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
|
||||
{
|
||||
openEditorFileNames[index] = event.target.value
|
||||
openEditorFileNames[index] = event.target.value;
|
||||
setOpenEditorFileNames(openEditorFileNames);
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const splitEditorClicked = () =>
|
||||
{
|
||||
openEditorFileNames.push(availableFileNames[0])
|
||||
openEditorFileNames.push(availableFileNames[0]);
|
||||
setOpenEditorFileNames(openEditorFileNames);
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const closeEditorClicked = (index: number) =>
|
||||
{
|
||||
openEditorFileNames.splice(index, 1)
|
||||
openEditorFileNames.splice(index, 1);
|
||||
setOpenEditorFileNames(openEditorFileNames);
|
||||
forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
const computeEditorWidth = (): string =>
|
||||
{
|
||||
return (100 / openEditorFileNames.length) + "%"
|
||||
}
|
||||
return (100 / openEditorFileNames.length) + "%";
|
||||
};
|
||||
|
||||
return (
|
||||
<Box className="scriptEditor" sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
||||
@ -408,7 +409,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
{
|
||||
return;
|
||||
}
|
||||
setErrorAlert("")
|
||||
setErrorAlert("");
|
||||
}} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
||||
<Alert color="error" onClose={() => setErrorAlert("")}>
|
||||
{errorAlert}
|
||||
@ -440,10 +441,10 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
||||
<Grid container alignItems="flex-end">
|
||||
<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 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>
|
||||
</Grid>
|
||||
<Box display="flex" sx={{height: "100%"}}>
|
||||
@ -464,19 +465,19 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
<Box>
|
||||
{
|
||||
openEditorFileNames.length > 1 &&
|
||||
<Tooltip title="Close this editor split" enterDelay={500}>
|
||||
<IconButton size="small" onClick={() => closeEditorClicked(index)}>
|
||||
<Icon>close</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Close this editor split" enterDelay={500}>
|
||||
<IconButton size="small" onClick={() => closeEditorClicked(index)}>
|
||||
<Icon>close</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
{
|
||||
index == openEditorFileNames.length - 1 &&
|
||||
<Tooltip title="Open a new editor split" enterDelay={500}>
|
||||
<IconButton size="small" onClick={splitEditorClicked}>
|
||||
<Icon>vertical_split</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Open a new editor split" enterDelay={500}>
|
||||
<IconButton size="small" onClick={splitEditorClicked}>
|
||||
<Icon>vertical_split</Icon>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
@ -526,29 +527,29 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
<CommitMessagePrompt isOpen={promptForCommitMessageOpen} closeHandler={closePromptForCommitMessage}/>
|
||||
<CommitMessagePrompt isOpen={promptForCommitMessageOpen} closeHandler={closePromptForCommitMessage} />
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void})
|
||||
function CommitMessagePrompt(props: { isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void })
|
||||
{
|
||||
const [commitMessage, setCommitMessage] = useState("No commit message given")
|
||||
const [commitMessage, setCommitMessage] = useState("No commit message given");
|
||||
|
||||
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
{
|
||||
setCommitMessage(event.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||
{
|
||||
if(e.key === "Enter")
|
||||
if (e.key === "Enter")
|
||||
{
|
||||
props.closeHandler(true, commitMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@ -579,10 +580,10 @@ function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClic
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<QCancelButton onClickHandler={() => props.closeHandler(false)} disabled={false} />
|
||||
<QSaveButton label="Save" onClickHandler={() => props.closeHandler(true, commitMessage)} disabled={false}/>
|
||||
<QSaveButton label="Save" onClickHandler={() => props.closeHandler(true, commitMessage)} disabled={false} />
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default ScriptEditor;
|
||||
|
@ -391,12 +391,12 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
|
||||
<Box display="flex" flexDirection="row" alignItems="center">
|
||||
<Box width="550px" pr={2} mb={-1.5}>
|
||||
<DynamicSelect
|
||||
possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
|
||||
fieldPossibleValueProps={{possibleValueSourceName: shareableTableMetaData.audiencePossibleValueSourceName, initialDisplayValue: selectedAudienceOption?.label}}
|
||||
fieldLabel="User or Group" // todo should come from shareableTableMetaData
|
||||
initialValue={selectedAudienceOption?.id}
|
||||
initialDisplayValue={selectedAudienceOption?.label}
|
||||
inForm={false}
|
||||
onChange={handleAudienceChange}
|
||||
useCase="form"
|
||||
/>
|
||||
</Box>
|
||||
{/*
|
||||
|
@ -22,16 +22,25 @@
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {Box, Skeleton} from "@mui/material";
|
||||
import Card from "@mui/material/Card";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import parse from "html-react-parser";
|
||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
||||
import React from "react";
|
||||
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
|
||||
interface CompositeData
|
||||
export interface CompositeData
|
||||
{
|
||||
blockId: string;
|
||||
blocks: BlockData[];
|
||||
styleOverrides?: any;
|
||||
layout?: string;
|
||||
overlayHtml?: string;
|
||||
overlayStyleOverrides?: any;
|
||||
modalMode: string;
|
||||
styles?: any;
|
||||
}
|
||||
|
||||
|
||||
@ -39,13 +48,15 @@ interface CompositeWidgetProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
data: CompositeData;
|
||||
actionCallback?: (blockData: BlockData, eventValues?: { [name: string]: any }) => boolean;
|
||||
values?: { [key: string]: any };
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Widget which is a list of Blocks.
|
||||
*******************************************************************************/
|
||||
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element
|
||||
export default function CompositeWidget({widgetMetaData, data, actionCallback, values}: CompositeWidgetProps): JSX.Element
|
||||
{
|
||||
if (!data || !data.blocks)
|
||||
{
|
||||
@ -71,6 +82,12 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
boxStyle.flexWrap = "wrap";
|
||||
boxStyle.gap = "0.5rem";
|
||||
}
|
||||
else if (layout == "FLEX_ROW")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
boxStyle.flexDirection = "row";
|
||||
boxStyle.gap = "0.5rem";
|
||||
}
|
||||
else if (layout == "FLEX_ROW_SPACE_BETWEEN")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
@ -78,6 +95,14 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
boxStyle.justifyContent = "space-between";
|
||||
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")
|
||||
{
|
||||
boxStyle.display = "flex";
|
||||
@ -97,20 +122,96 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
||||
boxStyle.borderRadius = "0.5rem";
|
||||
boxStyle.background = "#FFFFFF";
|
||||
}
|
||||
|
||||
if (data?.styleOverrides)
|
||||
{
|
||||
boxStyle = {...boxStyle, ...data.styleOverrides};
|
||||
}
|
||||
|
||||
return (<Box sx={boxStyle} className="compositeWidget">
|
||||
if (data.styles?.backgroundColor)
|
||||
{
|
||||
boxStyle.backgroundColor = ProcessWidgetBlockUtils.processColorFromStyleMap(data.styles.backgroundColor);
|
||||
}
|
||||
|
||||
if (data.styles?.padding)
|
||||
{
|
||||
boxStyle.paddingTop = data.styles?.padding.top + "px"
|
||||
boxStyle.paddingBottom = data.styles?.padding.bottom + "px"
|
||||
boxStyle.paddingLeft = data.styles?.padding.left + "px"
|
||||
boxStyle.paddingRight = data.styles?.padding.right + "px"
|
||||
}
|
||||
|
||||
let overlayStyle: any = {};
|
||||
|
||||
if (data?.overlayStyleOverrides)
|
||||
{
|
||||
overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides};
|
||||
}
|
||||
|
||||
const content = (
|
||||
<>
|
||||
{
|
||||
data?.overlayHtml &&
|
||||
<Box sx={overlayStyle} className="blockWidgetOverlay">{parse(data.overlayHtml)}</Box>
|
||||
}
|
||||
<Box sx={boxStyle} className="compositeWidget">
|
||||
{
|
||||
data.blocks.map((block: BlockData, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} actionCallback={actionCallback} values={values} />
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
if (data.modalMode)
|
||||
{
|
||||
const [isModalOpen, setIsModalOpen] = useState(values && (values[data.blockId] == true));
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const controlCallback = (newValue: boolean) =>
|
||||
{
|
||||
data.blocks.map((block: BlockData, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</Box>);
|
||||
setIsModalOpen(newValue);
|
||||
};
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
const modalOnClose = (event: object, reason: string) =>
|
||||
{
|
||||
values[data.blockId] = false;
|
||||
setIsModalOpen(false);
|
||||
actionCallback({blockTypeName: "BUTTON", values: {}}, {controlCode: `hideModal:${data.blockId}`});
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// register the control-callback function - so when buttons are clicked, we can be told //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
useEffect(() =>
|
||||
{
|
||||
if (actionCallback)
|
||||
{
|
||||
actionCallback(null, {
|
||||
registerControlCallbackName: data.blockId,
|
||||
registerControlCallbackFunction: controlCallback
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (<Modal open={isModalOpen} onClose={modalOnClose}>
|
||||
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
|
||||
<Card sx={{my: 5, mx: "auto", p: "1rem", maxWidth: "1024px"}}>
|
||||
{content}
|
||||
</Card>
|
||||
</Box>
|
||||
</Modal>);
|
||||
}
|
||||
else
|
||||
{
|
||||
return content;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,15 +18,18 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
import {Alert, Skeleton} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import Tab from "@mui/material/Tab";
|
||||
import Tabs from "@mui/material/Tabs";
|
||||
import parse from "html-react-parser";
|
||||
import QContext from "QContext";
|
||||
import EntityForm from "qqq/components/forms/EntityForm";
|
||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||
import TabPanel from "qqq/components/misc/TabPanel";
|
||||
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
||||
@ -43,7 +46,7 @@ import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidg
|
||||
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
|
||||
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
||||
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
|
||||
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget";
|
||||
import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
|
||||
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
|
||||
import StepperCard from "qqq/components/widgets/misc/StepperCard";
|
||||
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
||||
@ -71,6 +74,9 @@ interface Props
|
||||
childUrlParams?: string;
|
||||
parentWidgetMetaData?: QWidgetMetaData;
|
||||
wrapWidgetsInTabPanels: boolean;
|
||||
actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean;
|
||||
initialWidgetDataList: any[];
|
||||
values?: { [key: string]: any };
|
||||
}
|
||||
|
||||
DashboardWidgets.defaultProps = {
|
||||
@ -82,11 +88,14 @@ DashboardWidgets.defaultProps = {
|
||||
childUrlParams: "",
|
||||
parentWidgetMetaData: null,
|
||||
wrapWidgetsInTabPanels: false,
|
||||
actionCallback: null,
|
||||
initialWidgetDataList: null,
|
||||
values: {}
|
||||
};
|
||||
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels}: Props): JSX.Element
|
||||
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels, actionCallback, initialWidgetDataList, values}: Props): JSX.Element
|
||||
{
|
||||
const [widgetData, setWidgetData] = useState([] as any[]);
|
||||
const [widgetData, setWidgetData] = useState(initialWidgetDataList == null ? [] as any[] : initialWidgetDataList);
|
||||
const [widgetCounter, setWidgetCounter] = useState(0);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
@ -94,6 +103,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
|
||||
const {accentColor} = useContext(QContext);
|
||||
|
||||
/////////////////////////
|
||||
// modal form controls //
|
||||
/////////////////////////
|
||||
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
|
||||
const [modalTable, setModalTable] = useState(null as QTableMetaData);
|
||||
|
||||
let initialSelectedTab = 0;
|
||||
let selectedTabKey: string = null;
|
||||
if (parentWidgetMetaData && wrapWidgetsInTabPanels)
|
||||
@ -114,7 +129,15 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (initialWidgetDataList && initialWidgetDataList.length > 0)
|
||||
{
|
||||
// todo actually, should this check each element of the array, down in the loop? yeah, when we need to, do it that way.
|
||||
console.log("We already have initial widget data, so not fetching from backend.");
|
||||
return;
|
||||
}
|
||||
|
||||
setWidgetData([]);
|
||||
|
||||
for (let i = 0; i < widgetMetaDataList.length; i++)
|
||||
{
|
||||
const widgetMetaData = widgetMetaDataList[i];
|
||||
@ -151,7 +174,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
|
||||
const reloadWidget = async (index: number, data: string) =>
|
||||
{
|
||||
(async () =>
|
||||
await (async () =>
|
||||
{
|
||||
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
||||
setCurrentUrlParams(urlParams);
|
||||
@ -270,6 +293,148 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
return (rs);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
const closeEditChildForm = (event: object, reason: string) =>
|
||||
{
|
||||
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
setShowEditChildForm(null);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function deleteChildRecord(name: string, widgetIndex: number, rowIndex: number)
|
||||
{
|
||||
updateChildRecordList(name, "delete", rowIndex);
|
||||
actionCallback(widgetData[widgetIndex]);
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function openEditChildRecord(name: string, widgetData: any, rowIndex: number)
|
||||
{
|
||||
let defaultValues = widgetData.queryOutput.records[rowIndex].values;
|
||||
|
||||
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
|
||||
if (!disabledFields)
|
||||
{
|
||||
disabledFields = widgetData.defaultValuesForNewChildRecords;
|
||||
}
|
||||
|
||||
doOpenEditChildForm(name, widgetData.childTableMetaData, rowIndex, defaultValues, disabledFields);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function openAddChildRecord(name: string, widgetData: any)
|
||||
{
|
||||
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
|
||||
if (!disabledFields)
|
||||
{
|
||||
disabledFields = widgetData.defaultValuesForNewChildRecords;
|
||||
}
|
||||
|
||||
doOpenEditChildForm(name, widgetData.childTableMetaData, null, null, disabledFields);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function doOpenEditChildForm(widgetName: string, table: QTableMetaData, rowIndex: number, defaultValues: any, disabledFields: any)
|
||||
{
|
||||
const showEditChildForm: any = {};
|
||||
showEditChildForm.widgetName = widgetName;
|
||||
showEditChildForm.table = table;
|
||||
showEditChildForm.rowIndex = rowIndex;
|
||||
showEditChildForm.defaultValues = defaultValues;
|
||||
showEditChildForm.disabledFields = disabledFields;
|
||||
setShowEditChildForm(showEditChildForm);
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function submitEditChildForm(values: any)
|
||||
{
|
||||
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
|
||||
let widgetIndex = determineChildRecordListIndex(showEditChildForm.widgetName);
|
||||
actionCallback(widgetData[widgetIndex]);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function determineChildRecordListIndex(widgetName: string): number
|
||||
{
|
||||
let widgetIndex = -1;
|
||||
for (var i = 0; i < widgetMetaDataList.length; i++)
|
||||
{
|
||||
const widgetMetaData = widgetMetaDataList[i];
|
||||
if (widgetMetaData.name == widgetName)
|
||||
{
|
||||
widgetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (widgetIndex);
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any)
|
||||
{
|
||||
////////////////////////////////////////////////
|
||||
// find the correct child record widget index //
|
||||
////////////////////////////////////////////////
|
||||
let widgetIndex = determineChildRecordListIndex(widgetName);
|
||||
|
||||
if (!widgetData[widgetIndex].queryOutput.records)
|
||||
{
|
||||
widgetData[widgetIndex].queryOutput.records = [];
|
||||
}
|
||||
|
||||
const newChildListWidgetData: ChildRecordListData = widgetData[widgetIndex];
|
||||
if (!newChildListWidgetData.queryOutput.records)
|
||||
{
|
||||
newChildListWidgetData.queryOutput.records = [];
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case "insert":
|
||||
newChildListWidgetData.queryOutput.records.push({values: values});
|
||||
break;
|
||||
case "edit":
|
||||
newChildListWidgetData.queryOutput.records[rowIndex] = {values: values};
|
||||
break;
|
||||
case "delete":
|
||||
newChildListWidgetData.queryOutput.records.splice(rowIndex, 1);
|
||||
break;
|
||||
}
|
||||
newChildListWidgetData.totalRows = newChildListWidgetData.queryOutput.records.length;
|
||||
widgetData[widgetIndex] = newChildListWidgetData;
|
||||
setWidgetData(widgetData);
|
||||
|
||||
setShowEditChildForm(null);
|
||||
}
|
||||
|
||||
|
||||
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
|
||||
{
|
||||
const labelAdditionalComponentsRight: LabelComponent[] = [];
|
||||
@ -309,7 +474,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
)
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "alert" && widgetData[i]?.html && (
|
||||
widgetMetaData.type === "alert" && widgetData[i]?.html && !widgetData[i]?.hideWidget && (
|
||||
<Widget
|
||||
omitPadding={true}
|
||||
widgetMetaData={widgetMetaData}
|
||||
@ -319,7 +484,16 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<Alert severity={widgetData[i]?.alertType?.toLowerCase()}>{parse(widgetData[i]?.html)}</Alert>
|
||||
<Alert severity={widgetData[i]?.alertType?.toLowerCase()}>
|
||||
{parse(widgetData[i]?.html)}
|
||||
{widgetData[i]?.bulletList && (
|
||||
<div style={{fontSize: "14px"}}>
|
||||
{widgetData[i].bulletList.map((bullet: string, index: number) =>
|
||||
<li key={`widget-${i}-${index}`}>{parse(bullet)}</li>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Alert>
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
@ -501,9 +675,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
}
|
||||
{
|
||||
widgetMetaData.type === "divider" && (
|
||||
<Box>
|
||||
<DividerWidget />
|
||||
</Box>
|
||||
<DividerWidget />
|
||||
)
|
||||
}
|
||||
{
|
||||
@ -537,6 +709,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
widgetMetaData.type === "childRecordList" && (
|
||||
widgetData && widgetData[i] &&
|
||||
<RecordGridWidget
|
||||
disableRowClick={widgetData[i]?.disableRowClick}
|
||||
allowRecordEdit={widgetData[i]?.allowRecordEdit}
|
||||
allowRecordDelete={widgetData[i]?.allowRecordDelete}
|
||||
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, i, rowIndex)}
|
||||
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData[i], rowIndex)}
|
||||
addNewRecordCallback={() => openAddChildRecord(widgetMetaData.name, widgetData[i])}
|
||||
widgetMetaData={widgetMetaData}
|
||||
data={widgetData[i]}
|
||||
/>
|
||||
@ -563,7 +741,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||
>
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} />
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} actionCallback={actionCallback} values={values} />
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
@ -638,8 +816,28 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
|
||||
if (!omitWrappingGridContainer)
|
||||
{
|
||||
// @ts-ignore
|
||||
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
const gridProps: { [key: string]: any } = {};
|
||||
|
||||
for (let size of ["xs", "sm", "md", "lg", "xl", "xxl"])
|
||||
{
|
||||
const key = `gridCols:sizeClass:${size}`;
|
||||
if (widgetMetaData?.defaultValues?.has(key))
|
||||
{
|
||||
gridProps[size] = widgetMetaData?.defaultValues.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (!gridProps["xxl"])
|
||||
{
|
||||
gridProps["xxl"] = widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12;
|
||||
}
|
||||
|
||||
if (!gridProps["xs"])
|
||||
{
|
||||
gridProps["xs"] = 12;
|
||||
}
|
||||
|
||||
renderedWidget = (<Grid id={widgetMetaData.name} item {...gridProps} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
||||
{renderedWidget}
|
||||
</Grid>);
|
||||
}
|
||||
@ -690,6 +888,22 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
{
|
||||
showEditChildForm &&
|
||||
<Modal open={showEditChildForm as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
|
||||
<div className="modalEditForm">
|
||||
<EntityForm
|
||||
isModal={true}
|
||||
closeModalHandler={closeEditChildForm}
|
||||
table={showEditChildForm.table}
|
||||
defaultValues={showEditChildForm.defaultValues}
|
||||
disabledFields={showEditChildForm.disabledFields}
|
||||
onSubmitCallback={submitEditChildForm}
|
||||
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
</>
|
||||
) : null
|
||||
);
|
||||
|
@ -22,6 +22,9 @@
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {Alert, Skeleton} from "@mui/material";
|
||||
import ButtonBlock from "qqq/components/widgets/blocks/ButtonBlock";
|
||||
import AudioBlock from "qqq/components/widgets/blocks/AudioBlock";
|
||||
import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock";
|
||||
import React from "react";
|
||||
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||
@ -32,19 +35,22 @@ import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRow
|
||||
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
import ImageBlock from "./blocks/ImageBlock";
|
||||
|
||||
|
||||
interface WidgetBlockProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
block: BlockData;
|
||||
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
|
||||
values?: { [key: string]: any };
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** 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, values}: WidgetBlockProps): JSX.Element
|
||||
{
|
||||
if(!block)
|
||||
{
|
||||
@ -64,7 +70,7 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
||||
if(block.blockTypeName == "COMPOSITE")
|
||||
{
|
||||
// @ts-ignore - special case for composite type block...
|
||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} />);
|
||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} values={values} />);
|
||||
}
|
||||
|
||||
switch(block.blockTypeName)
|
||||
@ -83,6 +89,14 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
||||
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "BIG_NUMBER":
|
||||
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||
case "INPUT_FIELD":
|
||||
return (<InputFieldBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||
case "BUTTON":
|
||||
return (<ButtonBlock 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:
|
||||
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
||||
}
|
||||
|
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>
|
||||
);
|
||||
}
|
@ -21,18 +21,19 @@
|
||||
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {Tooltip} from "@mui/material";
|
||||
import React, {ReactElement, useContext} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
import {Box, Tooltip} from "@mui/material";
|
||||
import QContext from "QContext";
|
||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||
import React, {ReactElement, useContext} from "react";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
interface BlockElementWrapperProps
|
||||
{
|
||||
data: BlockData;
|
||||
metaData: QWidgetMetaData;
|
||||
slot: string
|
||||
slot: string;
|
||||
linkProps?: any;
|
||||
children: ReactElement;
|
||||
}
|
||||
@ -47,16 +48,16 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
||||
let link: BlockLink;
|
||||
let tooltip: BlockTooltip;
|
||||
|
||||
if(slot)
|
||||
if (slot)
|
||||
{
|
||||
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
||||
if(!link)
|
||||
if (!link)
|
||||
{
|
||||
link = data.link;
|
||||
}
|
||||
|
||||
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
||||
if(!tooltip)
|
||||
if (!tooltip)
|
||||
{
|
||||
tooltip = data.tooltip;
|
||||
}
|
||||
@ -67,9 +68,9 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
||||
tooltip = data.tooltip;
|
||||
}
|
||||
|
||||
if(!tooltip)
|
||||
if (!tooltip)
|
||||
{
|
||||
const helpRoles = ["ALL_SCREENS"]
|
||||
const helpRoles = ["ALL_SCREENS"];
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// the full keys in the helpContent table will look like: //
|
||||
@ -80,26 +81,39 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
||||
const key = data.blockId ? `${data.blockId},${slot}` : slot;
|
||||
const showHelp = helpHelpActive || hasHelpContent(metaData?.helpContent?.get(key), helpRoles);
|
||||
|
||||
if(showHelp)
|
||||
if (showHelp)
|
||||
{
|
||||
const formattedHelpContent = <HelpContent helpContents={metaData?.helpContent?.get(key)} roles={helpRoles} helpContentKey={`widget:${metaData?.name};slot:${key}`} />;
|
||||
tooltip = {title: formattedHelpContent, placement: "bottom"}
|
||||
tooltip = {title: formattedHelpContent, placement: "bottom"};
|
||||
}
|
||||
}
|
||||
|
||||
let rs = children;
|
||||
|
||||
if(link)
|
||||
if (link && link.href)
|
||||
{
|
||||
rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link>
|
||||
rs = <Link to={link.href} target={link.target} style={{color: "#546E7A"}} {...linkProps}>{rs}</Link>;
|
||||
}
|
||||
|
||||
if(tooltip)
|
||||
if (tooltip)
|
||||
{
|
||||
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom"
|
||||
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom";
|
||||
|
||||
// @ts-ignore - placement possible values
|
||||
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>
|
||||
if (tooltip.blockData)
|
||||
{
|
||||
// @ts-ignore - special case for composite type block...
|
||||
rs = <Tooltip title={
|
||||
<Box sx={{width: "200px"}}>
|
||||
<CompositeWidget widgetMetaData={metaData} data={tooltip?.blockData} />
|
||||
</Box>
|
||||
}>{rs}</Tooltip>;
|
||||
}
|
||||
else
|
||||
{
|
||||
// @ts-ignore - placement possible values
|
||||
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>;
|
||||
}
|
||||
}
|
||||
|
||||
return (rs);
|
||||
|
@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
|
||||
|
||||
|
||||
export interface BlockData
|
||||
@ -29,16 +30,19 @@ export interface BlockData
|
||||
|
||||
tooltip?: BlockTooltip;
|
||||
link?: BlockLink;
|
||||
tooltipMap?: {[slot: string]: BlockTooltip};
|
||||
linkMap?: {[slot: string]: BlockLink};
|
||||
tooltipMap?: { [slot: string]: BlockTooltip };
|
||||
linkMap?: { [slot: string]: BlockLink };
|
||||
|
||||
values: any;
|
||||
styles?: any;
|
||||
|
||||
conditional?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface BlockTooltip
|
||||
{
|
||||
blockData?: CompositeData;
|
||||
title: string | JSX.Element;
|
||||
placement: string;
|
||||
}
|
||||
@ -55,5 +59,6 @@ export interface StandardBlockComponentProps
|
||||
{
|
||||
widgetMetaData: QWidgetMetaData;
|
||||
data: BlockData;
|
||||
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
|
||||
}
|
||||
|
||||
|
86
src/qqq/components/widgets/blocks/ButtonBlock.tsx
Normal file
86
src/qqq/components/widgets/blocks/ButtonBlock.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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 ... a button...
|
||||
**
|
||||
*******************************************************************************/
|
||||
export default function ButtonBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
const startIcon = data.values.startIcon?.name ? <Icon>{data.values.startIcon.name}</Icon> : null;
|
||||
const endIcon = data.values.endIcon?.name ? <Icon>{data.values.endIcon.name}</Icon> : null;
|
||||
|
||||
function onClick()
|
||||
{
|
||||
if (actionCallback)
|
||||
{
|
||||
actionCallback(data, data.values);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("ButtonBlock onClick with no actionCallback present, so, noop");
|
||||
}
|
||||
}
|
||||
|
||||
let buttonVariant: "gradient" | "outlined" | "text" = "gradient";
|
||||
if (data.styles?.format == "outlined")
|
||||
{
|
||||
buttonVariant = "outlined";
|
||||
}
|
||||
else if (data.styles?.format == "text")
|
||||
{
|
||||
buttonVariant = "text";
|
||||
}
|
||||
else if (data.styles?.format == "filled")
|
||||
{
|
||||
buttonVariant = "gradient";
|
||||
}
|
||||
|
||||
// todo - button colors... but to do RGB's, might need to move away from MDButton?
|
||||
|
||||
return (
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||
<Box mx={1} my={1} minWidth={standardWidth}>
|
||||
<MDButton
|
||||
type="button"
|
||||
variant={buttonVariant}
|
||||
color="dark"
|
||||
size="small"
|
||||
fullWidth
|
||||
startIcon={startIcon}
|
||||
endIcon={endIcon}
|
||||
onClick={onClick}
|
||||
>
|
||||
{data.values.label ?? "Button"}
|
||||
</MDButton>
|
||||
</Box>
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
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>
|
||||
);
|
||||
}
|
139
src/qqq/components/widgets/blocks/InputFieldBlock.tsx
Normal file
139
src/qqq/components/widgets/blocks/InputFieldBlock.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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 dynamicFormFieldRest: {onBlur?: any, sx?: any} = {}
|
||||
if(autoFocus && blurCount == 0)
|
||||
{
|
||||
dynamicFormFieldRest.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=""
|
||||
placeholder={data.values?.placeholder}
|
||||
backgroundColor="#FFFFFF"
|
||||
formFieldObject={dynamicField}
|
||||
type={fieldMetaData.type}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
onKeyUp={eventHandler}
|
||||
{...dynamicFormFieldRest} />
|
||||
</>
|
||||
</BlockElementWrapper>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -19,8 +19,12 @@
|
||||
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
||||
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
|
||||
import React from "react";
|
||||
|
||||
/*******************************************************************************
|
||||
** Block that renders ... just some text.
|
||||
@ -29,9 +33,132 @@ import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockMo
|
||||
*******************************************************************************/
|
||||
export default function TextBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
|
||||
{
|
||||
let color = "rgba(0, 0, 0, 0.87)";
|
||||
if (data.styles?.color)
|
||||
{
|
||||
color = ProcessWidgetBlockUtils.processColorFromStyleMap(data.styles.color);
|
||||
}
|
||||
|
||||
let boxStyle = {};
|
||||
if (data.styles?.format == "alert")
|
||||
{
|
||||
boxStyle =
|
||||
{
|
||||
border: `1px solid ${color}`,
|
||||
background: `${color}40`,
|
||||
padding: "0.5rem",
|
||||
borderRadius: "0.5rem",
|
||||
};
|
||||
}
|
||||
else if (data.styles?.format == "banner")
|
||||
{
|
||||
boxStyle =
|
||||
{
|
||||
background: `${color}40`,
|
||||
padding: "0.5rem",
|
||||
};
|
||||
}
|
||||
|
||||
let fontSize = "1rem";
|
||||
if (data.styles?.size)
|
||||
{
|
||||
switch (data.styles.size.toLowerCase())
|
||||
{
|
||||
case "largest":
|
||||
fontSize = "3rem";
|
||||
break;
|
||||
case "headline":
|
||||
fontSize = "2rem";
|
||||
break;
|
||||
case "title":
|
||||
fontSize = "1.5rem";
|
||||
break;
|
||||
case "body":
|
||||
fontSize = "1rem";
|
||||
break;
|
||||
case "smallest":
|
||||
fontSize = "0.75rem";
|
||||
break;
|
||||
default:
|
||||
{
|
||||
if (data.styles.size.match(/^\d+$/))
|
||||
{
|
||||
fontSize = `${data.styles.size}px`;
|
||||
}
|
||||
else
|
||||
{
|
||||
fontSize = "1rem";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fontWeight = "400";
|
||||
if (data.styles?.weight)
|
||||
{
|
||||
switch (data.styles.weight.toLowerCase())
|
||||
{
|
||||
case "thin":
|
||||
case "100":
|
||||
fontWeight = "100";
|
||||
break;
|
||||
case "extralight":
|
||||
case "200":
|
||||
fontWeight = "200";
|
||||
break;
|
||||
case "light":
|
||||
case "300":
|
||||
fontWeight = "300";
|
||||
break;
|
||||
case "normal":
|
||||
case "400":
|
||||
fontWeight = "400";
|
||||
break;
|
||||
case "medium":
|
||||
case "500":
|
||||
fontWeight = "500";
|
||||
break;
|
||||
case "semibold":
|
||||
case "600":
|
||||
fontWeight = "600";
|
||||
break;
|
||||
case "bold":
|
||||
case "700":
|
||||
fontWeight = "700";
|
||||
break;
|
||||
case "extrabold":
|
||||
case "800":
|
||||
fontWeight = "800";
|
||||
break;
|
||||
case "black":
|
||||
case "900":
|
||||
fontWeight = "900";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const text = data.values.interpolatedText ?? data.values.text;
|
||||
const lines = text.split("\n");
|
||||
|
||||
const startIcon = data.values.startIcon?.name ? <Icon>{data.values.startIcon.name}</Icon> : null;
|
||||
const endIcon = data.values.endIcon?.name ? <Icon>{data.values.endIcon.name}</Icon> : null;
|
||||
|
||||
return (
|
||||
<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: fontSize, color: color, fontWeight: fontWeight}}>
|
||||
{lines.map((line: string, index: number) =>
|
||||
(
|
||||
<div key={index}>
|
||||
<>
|
||||
{index == 0 && startIcon ? {startIcon} : null}
|
||||
{line}
|
||||
{index == lines.length - 1 && endIcon ? {endIcon} : null}
|
||||
</>
|
||||
</div>
|
||||
))
|
||||
}</span>
|
||||
</Box>
|
||||
</BlockElementWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ export default function UpOrDownNumberBlock({widgetMetaData, data}: StandardBloc
|
||||
|
||||
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"}}>
|
||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">
|
||||
|
@ -50,6 +50,7 @@ import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
import "ace-builds/src-noconflict/ace";
|
||||
import "ace-builds/src-noconflict/mode-java";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
|
@ -19,13 +19,16 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import Divider from "@mui/material/Divider";
|
||||
|
||||
|
||||
function DividerWidget(): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Divider sx={{padding: "1px", background: "red"}}/>
|
||||
<Box pl={3} pt={3} pb={3} width="100%">
|
||||
<Divider sx={{width: "100%", height: "1px", background: "grey"}} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -87,6 +87,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
{
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
||||
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
|
||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||
|
||||
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)
|
||||
{
|
||||
@ -288,7 +289,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
function mayShowColumnsPreview(): boolean
|
||||
function mayShowColumns(): boolean
|
||||
{
|
||||
if (tableMetaData)
|
||||
{
|
||||
@ -356,14 +357,14 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
<Box pt="0.5rem">
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<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>
|
||||
{
|
||||
mayShowQueryPreview() &&
|
||||
mayShowQuery() &&
|
||||
<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}`}>
|
||||
{
|
||||
isEditable &&
|
||||
@ -382,11 +383,11 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
<h5>Columns</h5>
|
||||
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
||||
{
|
||||
mayShowColumnsPreview() &&
|
||||
mayShowColumns() && columns &&
|
||||
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"}>
|
||||
{
|
||||
isEditable &&
|
||||
@ -402,6 +403,21 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
||||
</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 &&
|
||||
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
||||
|
@ -39,15 +39,15 @@ import {Link, useNavigate} from "react-router-dom";
|
||||
|
||||
export interface ChildRecordListData extends WidgetData
|
||||
{
|
||||
title: string;
|
||||
queryOutput: { records: { values: any }[] };
|
||||
childTableMetaData: QTableMetaData;
|
||||
tablePath: string;
|
||||
viewAllLink: string;
|
||||
totalRows: number;
|
||||
canAddChildRecord: boolean;
|
||||
defaultValuesForNewChildRecords: { [fieldName: string]: any };
|
||||
disabledFieldsForNewChildRecords: { [fieldName: string]: any };
|
||||
title?: string;
|
||||
queryOutput?: { records: { values: any }[] };
|
||||
childTableMetaData?: QTableMetaData;
|
||||
tablePath?: string;
|
||||
viewAllLink?: string;
|
||||
totalRows?: number;
|
||||
canAddChildRecord?: boolean;
|
||||
defaultValuesForNewChildRecords?: { [fieldName: string]: any };
|
||||
disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
|
||||
}
|
||||
|
||||
interface Props
|
||||
@ -176,7 +176,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
setCsv(csv);
|
||||
setFileName(fileName);
|
||||
}
|
||||
}, [data]);
|
||||
}, [JSON.stringify(data?.queryOutput)]);
|
||||
|
||||
///////////////////
|
||||
// view all link //
|
||||
@ -304,7 +304,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
||||
>
|
||||
<Box mx={-3} mb={-3}>
|
||||
<Box>
|
||||
<Box>
|
||||
<DataGridPro
|
||||
autoHeight
|
||||
|
@ -19,15 +19,12 @@
|
||||
*/
|
||||
|
||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||
import {tooltipClasses, TooltipProps} from "@mui/material";
|
||||
import {Box, tooltipClasses, TooltipProps} from "@mui/material";
|
||||
import Autocomplete from "@mui/material/Autocomplete";
|
||||
import Box from "@mui/material/Box";
|
||||
import Icon from "@mui/material/Icon";
|
||||
import {styled} from "@mui/material/styles";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import parse from "html-react-parser";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
@ -166,7 +163,7 @@ function DataTable({
|
||||
})}
|
||||
>
|
||||
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
|
||||
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon>
|
||||
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_left"}</Icon>
|
||||
</span>
|
||||
) : null,
|
||||
},
|
||||
@ -312,7 +309,7 @@ function DataTable({
|
||||
{
|
||||
boxStyle = isFooter
|
||||
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
|
||||
: {flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
||||
: {height: fixedHeight ? `${fixedHeight}px` : "auto", flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
|
||||
}
|
||||
|
||||
let innerBoxStyle = {};
|
||||
@ -321,143 +318,139 @@ function DataTable({
|
||||
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// note - at one point, we had the table's sx including: whiteSpace: "nowrap"... //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
|
||||
<Table {...getTableProps()}>
|
||||
<Table {...getTableProps()} component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: gridTemplateColumns}}>
|
||||
{
|
||||
includeHead && (
|
||||
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
|
||||
{headerGroups.map((headerGroup: any, i: number) => (
|
||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", alignItems: "flex-end", gridTemplateColumns: gridTemplateColumns}}>
|
||||
{headerGroup.headers.map((column: any) => (
|
||||
column.type !== "hidden" && (
|
||||
<DataTableHeadCell
|
||||
key={i++}
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
align={column.align ? column.align : "left"}
|
||||
sorted={setSortedValue(column)}
|
||||
tooltip={column.tooltip}
|
||||
>
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</Box>
|
||||
headerGroups.map((headerGroup: any, i: number) => (
|
||||
headerGroup.headers.map((column: any) => (
|
||||
column.type !== "hidden" && (
|
||||
<DataTableHeadCell
|
||||
sx={{position: "sticky", top: 0, background: "white", zIndex: 10, alignItems: "flex-end"}}
|
||||
key={i++}
|
||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||
align={column.align ? column.align : "left"}
|
||||
sorted={setSortedValue(column)}
|
||||
tooltip={column.tooltip}
|
||||
>
|
||||
{column.render("header")}
|
||||
</DataTableHeadCell>
|
||||
)
|
||||
))
|
||||
))
|
||||
)
|
||||
}
|
||||
<TableBody {...getTableBodyProps()}>
|
||||
{rows.map((row: any, key: any) =>
|
||||
{rows.map((row: any, key: any) =>
|
||||
{
|
||||
prepareRow(row);
|
||||
|
||||
let overrideNoEndBorder = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// don't do an end-border on nested rows - unless they're the last one in a set //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if (row.depth > 0)
|
||||
{
|
||||
prepareRow(row);
|
||||
|
||||
let overrideNoEndBorder = false;
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
// don't do an end-border on nested rows - unless they're the last one in a set //
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
if (row.depth > 0)
|
||||
overrideNoEndBorder = true;
|
||||
if (key + 1 < rows.length && rows[key + 1].depth == 0)
|
||||
{
|
||||
overrideNoEndBorder = true;
|
||||
if (key + 1 < rows.length && rows[key + 1].depth == 0)
|
||||
{
|
||||
overrideNoEndBorder = false;
|
||||
}
|
||||
overrideNoEndBorder = false;
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////
|
||||
// don't do end-border on the footer //
|
||||
///////////////////////////////////////
|
||||
if (isFooter)
|
||||
{
|
||||
overrideNoEndBorder = true;
|
||||
}
|
||||
///////////////////////////////////////
|
||||
// don't do end-border on the footer //
|
||||
///////////////////////////////////////
|
||||
if (isFooter)
|
||||
{
|
||||
overrideNoEndBorder = true;
|
||||
}
|
||||
|
||||
let background = "initial";
|
||||
if (isFooter)
|
||||
{
|
||||
background = "#EEEEEE";
|
||||
}
|
||||
else if (row.depth > 0 || row.isExpanded)
|
||||
{
|
||||
background = "#FAFAFA";
|
||||
}
|
||||
let background = "initial";
|
||||
if (isFooter)
|
||||
{
|
||||
background = "#EEEEEE";
|
||||
}
|
||||
else if (row.depth > 0 || row.isExpanded)
|
||||
{
|
||||
background = "#FAFAFA";
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: background}} key={key} {...row.getRowProps()}>
|
||||
{row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
key={key}
|
||||
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
||||
depth={row.depth}
|
||||
align={cell.column.align ? cell.column.align : "left"}
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{
|
||||
cell.column.type === "default" && (
|
||||
cell.value && "number" === typeof cell.value ? (
|
||||
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
||||
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "htmlAndTooltip" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||
<Box>
|
||||
{parse(cell.value)}
|
||||
</Box>
|
||||
</NoMaxWidthTooltip>
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "html" && (
|
||||
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "composite" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "block" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(cell.column.id === "__expander") && cell.render("cell")
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
|
||||
</TableBody>
|
||||
return (
|
||||
row.cells.map((cell: any) => (
|
||||
cell.column.type !== "hidden" && (
|
||||
<DataTableBodyCell
|
||||
key={key}
|
||||
sx={{verticalAlign: "top", background: background}}
|
||||
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
||||
depth={row.depth}
|
||||
align={cell.column.align ? cell.column.align : "left"}
|
||||
{...cell.getCellProps()}
|
||||
>
|
||||
{
|
||||
cell.column.type === "default" && (
|
||||
cell.value && "number" === typeof cell.value ? (
|
||||
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
||||
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "htmlAndTooltip" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||
<Box>
|
||||
{parse(cell.value)}
|
||||
</Box>
|
||||
</NoMaxWidthTooltip>
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "html" && (
|
||||
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "composite" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "block" && (
|
||||
<DefaultCell isFooter={isFooter}>
|
||||
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
||||
</DefaultCell>
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} total={row.values["imageTotal"].toLocaleString()} totalType={row.values["imageTotalType"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||
)
|
||||
}
|
||||
{
|
||||
(cell.column.id === "__expander") && cell.render("cell")
|
||||
}
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
))
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</Box></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer sx={{boxShadow: "none", height: fixedHeight ? `${fixedHeight}px` : "auto"}}>
|
||||
<TableContainer sx={{boxShadow: "none", height: (fixedHeight && !fixedStickyLastRow) ? `${fixedHeight}px` : "auto"}}>
|
||||
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
||||
|
96
src/qqq/components/widgets/tables/ModalEditForm.tsx
Normal file
96
src/qqq/components/widgets/tables/ModalEditForm.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2022. 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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
import Modal from "@mui/material/Modal";
|
||||
import EntityForm from "qqq/components/forms/EntityForm";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import React, {useEffect, useReducer, useState} from "react";
|
||||
|
||||
|
||||
////////////////////////////////
|
||||
// structure of expected data //
|
||||
////////////////////////////////
|
||||
export interface ModalEditFormData
|
||||
{
|
||||
tableName: string;
|
||||
defaultValues?: { [key: string]: string };
|
||||
disabledFields?: { [key: string]: boolean } | string[];
|
||||
overrideHeading?: string;
|
||||
onSubmitCallback?: (values: any) => void;
|
||||
initialShowModalValue?: boolean;
|
||||
}
|
||||
|
||||
const qController = Client.getInstance();
|
||||
|
||||
function ModalEditForm({tableName, defaultValues, disabledFields, overrideHeading, onSubmitCallback, initialShowModalValue}: ModalEditFormData,): JSX.Element
|
||||
{
|
||||
const [showModal, setShowModal] = useState(initialShowModalValue);
|
||||
const [table, setTable] = useState(null as QTableMetaData);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if (!tableName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
(async () =>
|
||||
{
|
||||
const tableMetaData = await qController.loadTableMetaData(tableName);
|
||||
setTable(tableMetaData);
|
||||
forceUpdate();
|
||||
})();
|
||||
}, [tableName]);
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
const closeEditChildForm = (event: object, reason: string) =>
|
||||
{
|
||||
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
setShowModal(null);
|
||||
};
|
||||
|
||||
return (
|
||||
table && showModal &&
|
||||
<Modal open={showModal as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
|
||||
<div className="modalEditForm">
|
||||
<EntityForm
|
||||
isModal={true}
|
||||
closeModalHandler={closeEditChildForm}
|
||||
table={table}
|
||||
defaultValues={defaultValues}
|
||||
disabledFields={disabledFields}
|
||||
onSubmitCallback={onSubmitCallback}
|
||||
overrideHeading={overrideHeading}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ModalEditForm;
|
@ -93,41 +93,25 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
|
||||
/>
|
||||
: noRowsFoundHTML ?
|
||||
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
||||
<MDTypography
|
||||
variant="subtitle2"
|
||||
color="secondary"
|
||||
fontWeight="regular"
|
||||
>
|
||||
{
|
||||
noRowsFoundHTML ? (
|
||||
parse(noRowsFoundHTML)
|
||||
) : "No rows found"
|
||||
}
|
||||
<MDTypography variant="subtitle2" color="secondary" fontWeight="regular">
|
||||
{noRowsFoundHTML ? (parse(noRowsFoundHTML)) : "No rows found"}
|
||||
</MDTypography>
|
||||
</Box>
|
||||
:
|
||||
<TableContainer sx={{boxShadow: "none"}}>
|
||||
<Table>
|
||||
<Box component="thead">
|
||||
<TableRow sx={{alignItems: "flex-end"}} key="header">
|
||||
{Array(8).fill(0).map((_, i) =>
|
||||
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
|
||||
<Skeleton width="100%" />
|
||||
</DataTableHeadCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</Box>
|
||||
<TableBody>
|
||||
{Array(5).fill(0).map((_, i) =>
|
||||
<TableRow sx={{verticalAlign: "top"}} key={`row-${i}`}>
|
||||
{Array(8).fill(0).map((_, j) =>
|
||||
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
||||
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
||||
</DataTableBodyCell>
|
||||
)}
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
<Table component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"}}>
|
||||
{Array(8).fill(0).map((_, i) =>
|
||||
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
|
||||
<Skeleton width="100%" />
|
||||
</DataTableHeadCell>
|
||||
)}
|
||||
{Array(5).fill(0).map((_, i) =>
|
||||
Array(8).fill(0).map((_, j) =>
|
||||
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
||||
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
||||
</DataTableBodyCell>
|
||||
)
|
||||
)}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Box from "@mui/material/Box";
|
||||
import {Box} from "@mui/material";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import colors from "qqq/assets/theme/base/colors";
|
||||
import {ReactNode} from "react";
|
||||
@ -30,13 +30,14 @@ interface Props
|
||||
children: ReactNode;
|
||||
noBorder?: boolean;
|
||||
align?: "left" | "right" | "center";
|
||||
sx?: any;
|
||||
}
|
||||
|
||||
function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
|
||||
{
|
||||
return (
|
||||
<Box
|
||||
component="td"
|
||||
component="div"
|
||||
textAlign={align}
|
||||
py={1.5}
|
||||
px={1.5}
|
||||
@ -54,7 +55,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
},
|
||||
"&:last-child": {
|
||||
paddingRight: "1rem"
|
||||
}
|
||||
}, ...sx
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
@ -72,6 +73,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
||||
DataTableBodyCell.defaultProps = {
|
||||
noBorder: false,
|
||||
align: "left",
|
||||
sx: {}
|
||||
};
|
||||
|
||||
export default DataTableBodyCell;
|
||||
|
@ -44,18 +44,14 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="th"
|
||||
component="div"
|
||||
width={width}
|
||||
py={1.5}
|
||||
px={1.5}
|
||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||
"&:nth-of-type(1)": {
|
||||
paddingLeft: "1rem"
|
||||
},
|
||||
"&:last-child": {
|
||||
paddingRight: "1rem"
|
||||
},
|
||||
position: "sticky", top: 0, background: "white",
|
||||
zIndex: 1 // so if body rows scroll behind it, they don't show through
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
|
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 {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||
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 {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||
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 {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||
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 Grid from "@mui/material/Grid";
|
||||
import Step from "@mui/material/Step";
|
||||
import StepLabel from "@mui/material/StepLabel";
|
||||
import Stepper from "@mui/material/Stepper";
|
||||
import {Theme} from "@mui/material/styles";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
||||
import FormData from "form-data";
|
||||
@ -60,8 +63,12 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
||||
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
||||
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
||||
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 {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
|
||||
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 Client from "qqq/utils/qqq/Client";
|
||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||
@ -89,14 +96,18 @@ const INITIAL_RETRY_MILLIS = 1_500;
|
||||
const RETRY_MAX_MILLIS = 12_000;
|
||||
const BACKOFF_AMOUNT = 1.5;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// define a function that we can make referenes to, which we'll overwrite //
|
||||
// with formik's setFieldValue function, once we're inside formik. //
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// define some functions that we can make reference to, which we'll overwrite //
|
||||
// with functions from formik, once we're inside formik. //
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
||||
{
|
||||
};
|
||||
|
||||
let formikSetTouched = ({}: any, touched: boolean): void =>
|
||||
{
|
||||
};
|
||||
|
||||
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
||||
|
||||
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
||||
@ -120,6 +131,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
||||
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
|
||||
const [newStep, setNewStep] = useState(null);
|
||||
const [stepInstanceCounter, setStepInstanceCounter] = useState(0);
|
||||
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
|
||||
const [needInitialLoad, setNeedInitialLoad] = useState(true);
|
||||
const [lastForcedReInit, setLastForcedReInit] = useState(null as number);
|
||||
@ -136,8 +148,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
);
|
||||
const [showErrorDetail, setShowErrorDetail] = 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 [controlCallbacks, setControlCallbacks] = useState({} as { [name: string]: () => void });
|
||||
|
||||
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
|
||||
|
||||
@ -155,8 +169,30 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
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 //
|
||||
@ -175,7 +211,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const [recordConfig, setRecordConfig] = useState({} as any);
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
||||
const [records, setRecords] = useState([] as QRecord[]);
|
||||
const [records, setRecords] = useState([] as any);
|
||||
const [childRecordData, setChildRecordData] = useState(null as ChildRecordListData);
|
||||
|
||||
//////////////////////////////
|
||||
// state for bulk edit form //
|
||||
@ -294,36 +331,205 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
*******************************************************************************/
|
||||
function renderWidget(widgetName: string)
|
||||
{
|
||||
const widgetMetaData = qInstance.widgets.get(widgetName);
|
||||
if (!widgetMetaData)
|
||||
{
|
||||
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
|
||||
}
|
||||
|
||||
if (!renderedWidgets[activeStep.name])
|
||||
{
|
||||
renderedWidgets[activeStep.name] = {};
|
||||
setRenderedWidgets(renderedWidgets);
|
||||
}
|
||||
|
||||
if (renderedWidgets[activeStep.name][widgetName])
|
||||
let isChildRecordWidget = widgetMetaData.type == "childRecordList";
|
||||
if (!isChildRecordWidget && renderedWidgets[activeStep.name][widgetName])
|
||||
{
|
||||
return renderedWidgets[activeStep.name][widgetName];
|
||||
}
|
||||
|
||||
const widgetMetaData = qInstance.widgets.get(widgetName);
|
||||
if (!widgetMetaData)
|
||||
{
|
||||
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
|
||||
}
|
||||
|
||||
const queryStringParts: string[] = [];
|
||||
for (let name in processValues)
|
||||
{
|
||||
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`);
|
||||
}
|
||||
|
||||
let initialWidgetDataList = null;
|
||||
if (processValues[widgetName])
|
||||
{
|
||||
processValues[widgetName].hasPermission = true;
|
||||
initialWidgetDataList = [processValues[widgetName]];
|
||||
}
|
||||
|
||||
let actionCallback = blockWidgetActionCallback;
|
||||
if (isChildRecordWidget)
|
||||
{
|
||||
actionCallback = childRecordListWidgetActionCallBack;
|
||||
|
||||
if (childRecordData)
|
||||
{
|
||||
initialWidgetDataList = [childRecordData];
|
||||
}
|
||||
}
|
||||
|
||||
const renderedWidget = (<Box m={-2}>
|
||||
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} />
|
||||
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} initialWidgetDataList={initialWidgetDataList} values={processValues} actionCallback={actionCallback} />
|
||||
</Box>);
|
||||
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
|
||||
return renderedWidget;
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function handleControlCode(controlCode: string)
|
||||
{
|
||||
const split = controlCode.split(":", 2);
|
||||
let controlCallbackName: string;
|
||||
let controlCallbackValue: any;
|
||||
if (split.length == 2)
|
||||
{
|
||||
if (split[0] == "showModal")
|
||||
{
|
||||
processValues[split[1]] = true;
|
||||
controlCallbackName = split[1];
|
||||
controlCallbackValue = true;
|
||||
}
|
||||
else if (split[0] == "hideModal")
|
||||
{
|
||||
processValues[split[1]] = false;
|
||||
controlCallbackName = split[1];
|
||||
controlCallbackValue = false;
|
||||
}
|
||||
else if (split[0] == "toggleModal")
|
||||
{
|
||||
const currentValue = processValues[split[1]];
|
||||
processValues[split[1]] = !!!currentValue;
|
||||
controlCallbackName = split[1];
|
||||
controlCallbackValue = processValues[split[1]];
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`Unexpected part[0] (before colon) in controlCode: [${controlCode}]`);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log(`Expected controlCode to have 2 colon-delimited parts, but was: [${controlCode}]`);
|
||||
}
|
||||
|
||||
if (controlCallbackName && controlCallbacks[controlCallbackName])
|
||||
{
|
||||
// @ts-ignore ... args are hard
|
||||
controlCallbacks[controlCallbackName](controlCallbackValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** callback used by child list widget
|
||||
***************************************************************************/
|
||||
function childRecordListWidgetActionCallBack(data: any): boolean
|
||||
{
|
||||
console.log(`in childRecordListWidgetActionCallBack: ${JSON.stringify(data)}`);
|
||||
setChildRecordData(data as ChildRecordListData);
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
** 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 (eventValues?.registerControlCallbackName && eventValues?.registerControlCallbackFunction)
|
||||
{
|
||||
controlCallbacks[eventValues.registerControlCallbackName] = eventValues.registerControlCallbackFunction;
|
||||
setControlCallbacks(controlCallbacks);
|
||||
return (true);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// we don't validate these on the android frontend, and it seems fine - just let the app validate it? //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ///////////////////////////////////////////////////////////////////////////////
|
||||
// // 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);
|
||||
// }
|
||||
|
||||
let doSubmit = false;
|
||||
if (blockData?.blockTypeName == "BUTTON" && eventValues?.actionCode)
|
||||
{
|
||||
doSubmit = true;
|
||||
}
|
||||
else if (blockData?.blockTypeName == "BUTTON" && eventValues?.controlCode)
|
||||
{
|
||||
handleControlCode(eventValues.controlCode);
|
||||
doSubmit = false;
|
||||
}
|
||||
else if (blockData?.blockTypeName == "INPUT_FIELD")
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if action callback was fired from an input field, assume that means we're good to submit. //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////
|
||||
doSubmit = true;
|
||||
}
|
||||
|
||||
//////////////////
|
||||
// ok - submit! //
|
||||
//////////////////
|
||||
if (doSubmit)
|
||||
{
|
||||
handleFormSubmit(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} values={processValues} />
|
||||
</Box>;
|
||||
|
||||
setRenderedWidgets(renderedWidgets);
|
||||
|
||||
return (renderedWidgets[key]);
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// generate the main form body content for a step //
|
||||
////////////////////////////////////////////////////
|
||||
@ -382,7 +588,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
if (qJobRunning || step === null)
|
||||
{
|
||||
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={12} lg={6}>
|
||||
<Card>
|
||||
@ -477,6 +683,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
|
||||
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
|
||||
const isFormatScanner = step?.format?.toLowerCase() == "scanner";
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -485,7 +692,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
// hide label on widgets - the Widget component itself provides the label //
|
||||
// for modals, show the process label, but not for full-screen processes (for them, it is in the breadcrumb) //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
!isWidget &&
|
||||
!isWidget && !isFormatScanner &&
|
||||
<MDTypography variant={isWidget ? "h6" : "h5"} component="div" fontWeight="bold">
|
||||
{(isModal) ? `${overrideLabel ?? process.label}: ` : ""}
|
||||
{step?.label}
|
||||
@ -763,8 +970,29 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
}
|
||||
{
|
||||
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>
|
||||
@ -864,6 +1092,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
setActiveStepIndex(newIndex);
|
||||
setOverrideOnLastStep(null);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// reset formik touched data, so a field that's repeated doesn't immediately show a 'dirty' state //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
formikSetTouched({}, false);
|
||||
|
||||
if (steps)
|
||||
{
|
||||
const activeStep = steps[newIndex];
|
||||
@ -899,7 +1132,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
|
||||
{
|
||||
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))
|
||||
@ -909,6 +1152,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
|
||||
}
|
||||
|
||||
if (doesStepHaveComponent(activeStep, QComponentType.WIDGET))
|
||||
{
|
||||
ProcessWidgetBlockUtils.addFieldsForCompositeWidget(activeStep, processValues, (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 //
|
||||
///////////////////////////////////////////////////
|
||||
@ -994,7 +1247,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
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 //
|
||||
@ -1067,6 +1320,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
const {records} = response;
|
||||
setRecords(records);
|
||||
|
||||
if (!childRecordData || childRecordData.length == 0)
|
||||
{
|
||||
setChildRecordData(convertRecordsToChildRecordData(records));
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// re-construct the recordConfig object, so the setState call triggers a new rendering //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1088,6 +1346,71 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
}
|
||||
}, [needRecords]);
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
function convertRecordsToChildRecordData(records: QRecord[])
|
||||
{
|
||||
const frontendRecords = [] as any[];
|
||||
records.forEach((record: QRecord) =>
|
||||
{
|
||||
const object = {
|
||||
"tableName": record.tableName,
|
||||
"recordLabel": record.recordLabel,
|
||||
"errors": record.errors,
|
||||
"warnings": record.warnings,
|
||||
"values": Object.fromEntries(record.values),
|
||||
"displayValues": Object.fromEntries(record.displayValues),
|
||||
};
|
||||
frontendRecords.push(object);
|
||||
});
|
||||
const newChildListData = {} as ChildRecordListData;
|
||||
newChildListData.queryOutput = {records: frontendRecords};
|
||||
return (newChildListData);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
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 //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1112,13 +1435,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) //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
let frontendSteps = steps;
|
||||
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
||||
const updatedFrontendStepList = qJobComplete.processMetaDataAdjustment?.updatedFrontendStepList;
|
||||
if (updatedFrontendStepList)
|
||||
{
|
||||
setSteps(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) //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1159,7 +1488,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
setJobUUID(null);
|
||||
setNewStep(nextStepName);
|
||||
setStepInstanceCounter(1 + stepInstanceCounter);
|
||||
setProcessValues(newValues);
|
||||
setRenderedWidgets({});
|
||||
setQJobRunning(null);
|
||||
|
||||
if (formikSetFieldValueFunction)
|
||||
@ -1413,10 +1744,35 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
setNewStep(activeStepIndex - 1);
|
||||
};
|
||||
|
||||
////////////////////////////////////////////
|
||||
// handle user submitting changed records //
|
||||
////////////////////////////////////////////
|
||||
const doSubmit = async (formData: FormData) =>
|
||||
{
|
||||
const formDataHeaders = {
|
||||
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
|
||||
};
|
||||
|
||||
setTimeout(async () =>
|
||||
{
|
||||
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
||||
|
||||
const processResponse = await Client.getInstance().processStep(
|
||||
processName,
|
||||
processUUID,
|
||||
activeStep.name,
|
||||
formData,
|
||||
formDataHeaders
|
||||
);
|
||||
setLastProcessResponse(processResponse);
|
||||
});
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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 handleFormSubmit = async (values: any) =>
|
||||
{
|
||||
setFormError(null);
|
||||
|
||||
@ -1455,28 +1811,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
formData.append("bulkEditEnabledFields", bulkEditEnabledFields.join(","));
|
||||
}
|
||||
|
||||
const formDataHeaders = {
|
||||
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
|
||||
};
|
||||
/////////////////////////////////////////////////////////////
|
||||
// convert to regular objects so that they can be jsonized //
|
||||
/////////////////////////////////////////////////////////////
|
||||
if (childRecordData)
|
||||
{
|
||||
formData.append("frontendRecords", JSON.stringify(childRecordData.queryOutput.records));
|
||||
}
|
||||
|
||||
setProcessValues({});
|
||||
setRecords([]);
|
||||
setOverrideOnLastStep(null);
|
||||
setLastProcessResponse(new QJobRunning({message: "Working..."}));
|
||||
|
||||
setTimeout(async () =>
|
||||
{
|
||||
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
||||
|
||||
const processResponse = await Client.getInstance().processStep(
|
||||
processName,
|
||||
processUUID,
|
||||
activeStep.name,
|
||||
formData,
|
||||
formDataHeaders,
|
||||
);
|
||||
setLastProcessResponse(processResponse);
|
||||
});
|
||||
doSubmit(formData);
|
||||
};
|
||||
|
||||
|
||||
@ -1506,27 +1854,54 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
};
|
||||
|
||||
|
||||
const mainCardStyles: any = {};
|
||||
const formStyles: any = {};
|
||||
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";
|
||||
formStyles.display = "flex";
|
||||
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 nextButtonIcon = "arrow_forward";
|
||||
if (overrideOnLastStep !== null)
|
||||
@ -1549,23 +1924,24 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationScheme}
|
||||
validation={validationFunction}
|
||||
onSubmit={handleSubmit}
|
||||
onSubmit={handleFormSubmit}
|
||||
>
|
||||
{({
|
||||
values, errors, touched, isSubmitting, setFieldValue,
|
||||
values, errors, touched, isSubmitting, setFieldValue, setTouched
|
||||
}) =>
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////
|
||||
// once we're in the formik form, use its setFieldValue function //
|
||||
// over top of the default one we created globally //
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////
|
||||
// once we're in the formik form, capture some of its functions //
|
||||
// over top of the default ones we created globally //
|
||||
//////////////////////////////////////////////////////////////////
|
||||
formikSetFieldValueFunction = setFieldValue;
|
||||
formikSetTouched = setTouched;
|
||||
|
||||
return (
|
||||
<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"}}}>
|
||||
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
||||
{steps.map((step) => (
|
||||
@ -1600,21 +1976,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
{/********************************
|
||||
** 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 ? (
|
||||
<Box />
|
||||
) : (
|
||||
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
|
||||
)}
|
||||
{processError || qJobRunning || !activeStep ? (
|
||||
{processError || qJobRunning || !activeStep || activeStep?.format?.toLowerCase() == "scanner" ? (
|
||||
<Box />
|
||||
) : (
|
||||
<>
|
||||
{formError && (
|
||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
|
||||
{formError}
|
||||
</MDTypography>
|
||||
)}
|
||||
{
|
||||
noMoreSteps && <QCancelButton
|
||||
onClickHandler={() => handleCancelClicked(true)}
|
||||
@ -1650,9 +2021,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
||||
|
||||
const body = (
|
||||
<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}>
|
||||
{form}
|
||||
{formError && <Alert severity="error" onClose={() => setFormError(null)} sx={{position: "fixed", top: "40px", left: "10vw", width: "calc(80vw)", zIndex: "99999"}}>{formError}</Alert>}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
268
src/qqq/pages/processes/ProcessWidgetBlockUtils.tsx
Normal file
268
src/qqq/pages/processes/ProcessWidgetBlockUtils.tsx
Normal file
@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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 == "BUTTON")
|
||||
{
|
||||
//////////////////////////////////////////
|
||||
// look at actionCodes on 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, processValues: any, 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 or a widget w/ its data in the processValues, then, 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);
|
||||
}
|
||||
else if (component.type == QComponentType.WIDGET && processValues[component.values?.widgetName])
|
||||
{
|
||||
recursiveHelper(processValues[component.values?.widgetName] as unknown as CompositeData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static processColorFromStyleMap(colorFromStyleMap?: string): string
|
||||
{
|
||||
if (colorFromStyleMap)
|
||||
{
|
||||
switch (colorFromStyleMap.toUpperCase())
|
||||
{
|
||||
case "SUCCESS":
|
||||
return("#2BA83F");
|
||||
case "WARNING":
|
||||
return("#FBA132");
|
||||
case "ERROR":
|
||||
return("#FB4141");
|
||||
case "INFO":
|
||||
return("#458CFF");
|
||||
case "MUTED":
|
||||
return("#7b809a");
|
||||
default:
|
||||
{
|
||||
if (colorFromStyleMap.match(/^[0-9A-F]{6}$/))
|
||||
{
|
||||
return(`#${colorFromStyleMap}`);
|
||||
}
|
||||
else if (colorFromStyleMap.match(/^[0-9A-F]{8}$/))
|
||||
{
|
||||
return(`#${colorFromStyleMap}`);
|
||||
}
|
||||
else
|
||||
{
|
||||
return(colorFromStyleMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -121,7 +121,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
|
||||
}
|
||||
|
||||
const valueCounts = [] as QRecord[];
|
||||
for(let i = 0; i < result.values.valueCounts.length; i++)
|
||||
for(let i = 0; i < result.values.valueCounts?.length; i++)
|
||||
{
|
||||
let valueRecord = new QRecord(result.values.valueCounts[i]);
|
||||
|
||||
|
@ -779,13 +779,12 @@ function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData
|
||||
}}
|
||||
>
|
||||
<DynamicSelect
|
||||
tableName={tableName}
|
||||
fieldName={field.name}
|
||||
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
|
||||
fieldLabel="Value"
|
||||
initialValue={selectedPossibleValue?.id}
|
||||
initialDisplayValue={selectedPossibleValue?.label}
|
||||
inForm={false}
|
||||
onChange={handleChange}
|
||||
useCase="filter"
|
||||
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
||||
/>
|
||||
</Box>
|
||||
@ -847,13 +846,12 @@ function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaDa
|
||||
}}
|
||||
>
|
||||
<DynamicSelect
|
||||
tableName={tableName}
|
||||
fieldName={field.name}
|
||||
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: null}}
|
||||
isMultiple={true}
|
||||
fieldLabel="Value"
|
||||
initialValues={selectedPossibleValues}
|
||||
inForm={false}
|
||||
onChange={handleChange}
|
||||
useCase="filter"
|
||||
/>
|
||||
</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 {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||
import {Alert, Collapse, Menu, Typography} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import {Alert, Box, Collapse, Menu, Typography} from "@mui/material";
|
||||
import Button from "@mui/material/Button";
|
||||
import Card from "@mui/material/Card";
|
||||
import Divider from "@mui/material/Divider";
|
||||
@ -92,6 +91,7 @@ interface Props
|
||||
launchProcess?: QProcessMetaData;
|
||||
usage?: QueryScreenUsage;
|
||||
isModal?: boolean;
|
||||
isPreview?: boolean;
|
||||
initialQueryFilter?: QQueryFilter;
|
||||
initialColumns?: QQueryColumns;
|
||||
allowVariables?: boolean;
|
||||
@ -126,7 +126,7 @@ const getLoadingScreen = (isModal: boolean) =>
|
||||
**
|
||||
** 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 [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.
|
||||
*******************************************************************************/
|
||||
@ -2232,12 +2244,25 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
||||
return (
|
||||
<GridToolbarContainer>
|
||||
<div>
|
||||
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||
</div>
|
||||
<div style={{position: "relative"}}>
|
||||
{/* @ts-ignore */}
|
||||
<GridToolbarDensitySelector nonce={undefined} />
|
||||
<Tooltip title="Refresh Query">
|
||||
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||
</Tooltip>
|
||||
</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" &&
|
||||
@ -2872,7 +2897,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
||||
}
|
||||
|
||||
{
|
||||
metaData && tableMetaData &&
|
||||
!isPreview && metaData && tableMetaData &&
|
||||
<BasicAndAdvancedQueryControls
|
||||
ref={basicAndAdvancedQueryControlsRef}
|
||||
metaData={metaData}
|
||||
|
@ -34,6 +34,7 @@ import BaseLayout from "qqq/layouts/BaseLayout";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
||||
|
||||
import "ace-builds/src-noconflict/ace";
|
||||
import "ace-builds/src-noconflict/mode-java";
|
||||
import "ace-builds/src-noconflict/mode-javascript";
|
||||
import "ace-builds/src-noconflict/mode-json";
|
||||
|
@ -787,3 +787,20 @@ input[type="search"]::-webkit-search-results-decoration
|
||||
{
|
||||
margin: 2rem 1rem;
|
||||
}
|
||||
|
||||
/* default styles for a block widget overlay */
|
||||
.blockWidgetOverlay
|
||||
{
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
top: 15px;
|
||||
height: 0;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.blockWidgetOverlay a
|
||||
{
|
||||
color: #0062FF !important;
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ export default class DataGridUtils
|
||||
{
|
||||
console.log(`row-click mouse-up happened ${diff} x or y pixels away from the mouse-down - so not considering it a click.`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -133,13 +133,13 @@ export default class DataGridUtils
|
||||
row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
|
||||
});
|
||||
|
||||
if(tableMetaData.exposedJoins)
|
||||
if (tableMetaData.exposedJoins)
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||
{
|
||||
const join = tableMetaData.exposedJoins[i];
|
||||
|
||||
if(join?.joinTable?.fields?.values())
|
||||
if (join?.joinTable?.fields?.values())
|
||||
{
|
||||
const fields = [...join.joinTable.fields.values()];
|
||||
fields.forEach((field) =>
|
||||
@ -151,15 +151,15 @@ export default class DataGridUtils
|
||||
}
|
||||
}
|
||||
|
||||
if(!row["id"])
|
||||
if (!row["id"])
|
||||
{
|
||||
row["id"] = record.values.get(tableMetaData.primaryKeyField) ?? row[tableMetaData.primaryKeyField];
|
||||
if(row["id"] === null || row["id"] === undefined)
|
||||
if (row["id"] === null || row["id"] === undefined)
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// DataGrid gets very upset about a null or undefined here, so, try to make it happier //
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(!allowEmptyId)
|
||||
if (!allowEmptyId)
|
||||
{
|
||||
row["id"] = "--";
|
||||
}
|
||||
@ -170,7 +170,7 @@ export default class DataGridUtils
|
||||
});
|
||||
|
||||
return (rows);
|
||||
}
|
||||
};
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
@ -180,24 +180,24 @@ export default class DataGridUtils
|
||||
const columns = [] as GridColDef[];
|
||||
this.addColumnsForTable(tableMetaData, linkBase, columns, columnSort, null, null);
|
||||
|
||||
if(metaData)
|
||||
if (metaData)
|
||||
{
|
||||
if(tableMetaData.exposedJoins)
|
||||
if (tableMetaData.exposedJoins)
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||
{
|
||||
const join = tableMetaData.exposedJoins[i];
|
||||
let joinTableName = join.joinTable.name;
|
||||
if(metaData.tables.has(joinTableName) && metaData.tables.get(joinTableName).readPermission)
|
||||
if (metaData.tables.has(joinTableName) && metaData.tables.get(joinTableName).readPermission)
|
||||
{
|
||||
let joinLinkBase = null;
|
||||
joinLinkBase = metaData.getTablePath(join.joinTable);
|
||||
if(joinLinkBase)
|
||||
if (joinLinkBase)
|
||||
{
|
||||
joinLinkBase += joinLinkBase.endsWith("/") ? "" : "/";
|
||||
}
|
||||
|
||||
if(join?.joinTable?.fields?.values())
|
||||
if (join?.joinTable?.fields?.values())
|
||||
{
|
||||
this.addColumnsForTable(join.joinTable, joinLinkBase, columns, columnSort, joinTableName + ".", join.label + ": ");
|
||||
}
|
||||
@ -220,7 +220,7 @@ export default class DataGridUtils
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// this sorted by sections - e.g., manual sorting by the meta-data... //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
if(columnSort === "bySection")
|
||||
if (columnSort === "bySection")
|
||||
{
|
||||
for (let i = 0; i < tableMetaData.sections.length; i++)
|
||||
{
|
||||
@ -241,19 +241,23 @@ export default class DataGridUtils
|
||||
///////////////////////////
|
||||
// sort by labels... mmm //
|
||||
///////////////////////////
|
||||
sortedKeys.push(...tableMetaData.fields.keys())
|
||||
sortedKeys.push(...tableMetaData.fields.keys());
|
||||
sortedKeys.sort((a: string, b: string): number =>
|
||||
{
|
||||
return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label))
|
||||
})
|
||||
return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label));
|
||||
});
|
||||
}
|
||||
|
||||
sortedKeys.forEach((key) =>
|
||||
{
|
||||
const field = tableMetaData.fields.get(key);
|
||||
if(field.isHeavy)
|
||||
if (!field)
|
||||
{
|
||||
if(field.type == QFieldType.BLOB)
|
||||
return;
|
||||
}
|
||||
if (field.isHeavy)
|
||||
{
|
||||
if (field.type == QFieldType.BLOB)
|
||||
{
|
||||
////////////////////////////////////////////////////////
|
||||
// assume we DO want heavy blobs - as download links. //
|
||||
@ -270,7 +274,7 @@ export default class DataGridUtils
|
||||
|
||||
const column = this.makeColumnFromField(field, tableMetaData, namePrefix, labelPrefix);
|
||||
|
||||
if(key === tableMetaData.primaryKeyField && linkBase && namePrefix == null)
|
||||
if (key === tableMetaData.primaryKeyField && linkBase && namePrefix == null)
|
||||
{
|
||||
columns.splice(0, 0, column);
|
||||
}
|
||||
@ -346,9 +350,9 @@ export default class DataGridUtils
|
||||
(cellValues.value)
|
||||
);
|
||||
|
||||
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
|
||||
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
|
||||
const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here?
|
||||
if(showHelp)
|
||||
if (showHelp)
|
||||
{
|
||||
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={headerName} helpContentKey={`table:${tableMetaData.name};field:${fieldName}`} />;
|
||||
column.renderHeader = (params: GridColumnHeaderParams) => (
|
||||
@ -361,7 +365,7 @@ export default class DataGridUtils
|
||||
}
|
||||
|
||||
return (column);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -390,7 +394,7 @@ export default class DataGridUtils
|
||||
}
|
||||
}
|
||||
|
||||
if(field.possibleValueSourceName)
|
||||
if (field.possibleValueSourceName)
|
||||
{
|
||||
return (200);
|
||||
}
|
||||
@ -415,6 +419,6 @@ export default class DataGridUtils
|
||||
}
|
||||
|
||||
return (200);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
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>
|
||||
);
|
||||
}
|
@ -19,7 +19,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility functions for basic html/webpage/browser things.
|
||||
@ -68,10 +69,15 @@ export default class HtmlUtils
|
||||
** it was originally built like this when we had to submit full access token to backend...
|
||||
**
|
||||
*******************************************************************************/
|
||||
static downloadUrlViaIFrame = (url: string, filename: string) =>
|
||||
static downloadUrlViaIFrame = (field: QFieldMetaData, url: string, filename: string) =>
|
||||
{
|
||||
if(url.startsWith("data:"))
|
||||
if (url.startsWith("data:") || url.startsWith("http"))
|
||||
{
|
||||
if (url.startsWith("http"))
|
||||
{
|
||||
url += encodeURIComponent(`?response-content-disposition=attachment; ${filename}`);
|
||||
}
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.download = filename;
|
||||
link.href = url;
|
||||
@ -93,8 +99,14 @@ export default class HtmlUtils
|
||||
// todo - onload event handler to let us know when done?
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
var method = "get";
|
||||
if (QFieldType.BLOB == field.type)
|
||||
{
|
||||
method = "post";
|
||||
}
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.setAttribute("method", "post");
|
||||
form.setAttribute("method", method);
|
||||
form.setAttribute("action", url);
|
||||
form.setAttribute("target", "downloadIframe");
|
||||
iframe.appendChild(form);
|
||||
@ -117,7 +129,7 @@ export default class HtmlUtils
|
||||
*******************************************************************************/
|
||||
static openInNewWindow = (url: string, filename: string) =>
|
||||
{
|
||||
if(url.startsWith("data:"))
|
||||
if (url.startsWith("data:"))
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -153,4 +165,4 @@ export default class HtmlUtils
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -133,7 +133,7 @@ class FilterUtils
|
||||
}
|
||||
else
|
||||
{
|
||||
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values);
|
||||
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, "filter");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,18 +28,17 @@ import "datejs"; // https://github.com/datejs/Datejs
|
||||
import {Chip, ClickAwayListener, Icon} from "@mui/material";
|
||||
import Box from "@mui/material/Box";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import {makeStyles} from "@mui/styles";
|
||||
import parse from "html-react-parser";
|
||||
import React, {Fragment, useReducer, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import {Link} from "react-router-dom";
|
||||
import HtmlUtils from "qqq/utils/HtmlUtils";
|
||||
import Client from "qqq/utils/qqq/Client";
|
||||
|
||||
import "ace-builds/src-noconflict/ace";
|
||||
import "ace-builds/src-noconflict/mode-sql";
|
||||
import React, {Fragment, useReducer, useState} from "react";
|
||||
import AceEditor from "react-ace";
|
||||
import "ace-builds/src-noconflict/mode-velocity";
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
/*******************************************************************************
|
||||
** Utility class for working with QQQ Values
|
||||
@ -198,7 +197,7 @@ class ValueUtils
|
||||
);
|
||||
}
|
||||
|
||||
if (field.type == QFieldType.BLOB)
|
||||
if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
|
||||
{
|
||||
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
|
||||
}
|
||||
@ -219,7 +218,7 @@ class ValueUtils
|
||||
|
||||
if (field.type === QFieldType.DATE_TIME)
|
||||
{
|
||||
if(displayValue && displayValue != rawValue)
|
||||
if (displayValue && displayValue != rawValue)
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// if the date-time actually has a displayValue set, and it isn't just the //
|
||||
@ -268,7 +267,15 @@ class ValueUtils
|
||||
{
|
||||
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.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
|
||||
}
|
||||
// @ts-ignore
|
||||
return (`${date.toString("yyyy-MM-dd")}`);
|
||||
@ -466,7 +473,7 @@ class ValueUtils
|
||||
*******************************************************************************/
|
||||
public static cleanForCsv(param: any): string
|
||||
{
|
||||
if(param === undefined || param === null)
|
||||
if (param === undefined || param === null)
|
||||
{
|
||||
return ("");
|
||||
}
|
||||
@ -491,7 +498,7 @@ class ValueUtils
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// little private component here, for rendering an AceEditor with some buttons/controls/state //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function CodeViewer({name, mode, code}: {name: string; mode: string; code: string;}): JSX.Element
|
||||
function CodeViewer({name, mode, code}: { name: string; mode: string; code: string; }): JSX.Element
|
||||
{
|
||||
const [activeCode, setActiveCode] = useState(code);
|
||||
const [isFormatted, setIsFormatted] = useState(false);
|
||||
@ -588,7 +595,7 @@ function CodeViewer({name, mode, code}: {name: string; mode: string; code: strin
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// little private component here, for rendering "secret-ish" values, that you can click to reveal or copy //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
function RevealComponent({fieldName, value, usage}: {fieldName: string, value: string, usage: string;}): JSX.Element
|
||||
function RevealComponent({fieldName, value, usage}: { fieldName: string, value: string, usage: string; }): JSX.Element
|
||||
{
|
||||
const [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
@ -645,7 +652,7 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s
|
||||
</Tooltip>
|
||||
</ClickAwayListener>
|
||||
</Box>
|
||||
):(
|
||||
) : (
|
||||
<Box display="inline"><Icon onClick={(e) => handleRevealIconClick(e, fieldName)} sx={{cursor: "pointer", fontSize: "15px !important", position: "relative", top: "3px", marginRight: "5px"}}>visibility_off</Icon>{displayValue}</Box>
|
||||
)
|
||||
)
|
||||
@ -672,7 +679,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
||||
const download = (event: React.MouseEvent<HTMLSpanElement>) =>
|
||||
{
|
||||
event.stopPropagation();
|
||||
HtmlUtils.downloadUrlViaIFrame(url, filename);
|
||||
HtmlUtils.downloadUrlViaIFrame(field, url, filename);
|
||||
};
|
||||
|
||||
const open = (event: React.MouseEvent<HTMLSpanElement>) =>
|
||||
@ -681,7 +688,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
||||
HtmlUtils.openInNewWindow(url, filename);
|
||||
};
|
||||
|
||||
if(!filename || !url)
|
||||
if (!filename || !url)
|
||||
{
|
||||
return (<React.Fragment />);
|
||||
}
|
||||
@ -696,10 +703,22 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
||||
usage == "view" && filename
|
||||
}
|
||||
<Tooltip placement={tooltipPlacement} title="Open file">
|
||||
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => open(e)}>open_in_new</Icon>
|
||||
{
|
||||
field.type == QFieldType.BLOB ? (
|
||||
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => open(e)}>open_in_new</Icon>
|
||||
) : (
|
||||
<a style={{color: "inherit"}} rel="noopener noreferrer" href={url} target="_blank"><Icon className={"blobIcon"} fontSize="small">open_in_new</Icon></a>
|
||||
)
|
||||
}
|
||||
</Tooltip>
|
||||
<Tooltip placement={tooltipPlacement} title="Download file">
|
||||
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => download(e)}>save_alt</Icon>
|
||||
{
|
||||
field.type == QFieldType.BLOB ? (
|
||||
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => download(e)}>save_alt</Icon>
|
||||
) : (
|
||||
<a style={{color: "inherit"}} href={url} download="test.pdf"><Icon className={"blobIcon"} fontSize="small">save_alt</Icon></a>
|
||||
)
|
||||
}
|
||||
</Tooltip>
|
||||
{
|
||||
usage == "query" && filename
|
||||
@ -709,5 +728,4 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default ValueUtils;
|
||||
|
@ -57,4 +57,5 @@ module.exports = function (app)
|
||||
app.use("/images", getRequestHandler());
|
||||
app.use("/api*", getRequestHandler());
|
||||
app.use("/*api", getRequestHandler());
|
||||
app.use("/qqq/*", getRequestHandler());
|
||||
};
|
||||
|
@ -56,8 +56,8 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
|
||||
"label": "Sample Table Widget",
|
||||
"footerHTML": "<span class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit' aria-hidden='true'><span class='dashboard-schedule-icon'>schedule</span></span>Updated at 2023-10-17 09:11:38 AM CDT",
|
||||
"columns": [
|
||||
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" },
|
||||
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" }
|
||||
{ "type": "html", "header": "Id", "accessor": "id", "width": "30px" },
|
||||
{ "type": "html", "header": "Name", "accessor": "name", "width": "1fr" }
|
||||
],
|
||||
"rows": [
|
||||
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
|
||||
@ -83,7 +83,7 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
|
||||
// assert that the table widget rendered its header and some contents //
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget table a", "Homer S.");
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget a", "Homer S.");
|
||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
|
||||
|
||||
/////////////////////////////
|
||||
|
Reference in New Issue
Block a user