mirror of
https://github.com/Kingsrook/qqq-frontend-material-dashboard.git
synced 2025-07-22 07:08:44 +00:00
Compare commits
67 Commits
wip/CE-148
...
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 | |||
45b6b42836 |
5512
package-lock.json
generated
5512
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -6,7 +6,7 @@
|
|||||||
"@auth0/auth0-react": "1.10.2",
|
"@auth0/auth0-react": "1.10.2",
|
||||||
"@emotion/react": "11.7.1",
|
"@emotion/react": "11.7.1",
|
||||||
"@emotion/styled": "11.6.0",
|
"@emotion/styled": "11.6.0",
|
||||||
"@kingsrook/qqq-frontend-core": "1.0.104",
|
"@kingsrook/qqq-frontend-core": "1.0.110",
|
||||||
"@mui/icons-material": "5.4.1",
|
"@mui/icons-material": "5.4.1",
|
||||||
"@mui/material": "5.11.1",
|
"@mui/material": "5.11.1",
|
||||||
"@mui/styles": "5.11.1",
|
"@mui/styles": "5.11.1",
|
||||||
@ -18,8 +18,8 @@
|
|||||||
"@react-jvectormap/core": "1.0.1",
|
"@react-jvectormap/core": "1.0.1",
|
||||||
"@react-jvectormap/unitedstates": "1.0.1",
|
"@react-jvectormap/unitedstates": "1.0.1",
|
||||||
"@react-oauth/google": "0.2.8",
|
"@react-oauth/google": "0.2.8",
|
||||||
"@types/prop-types": "15.7.5",
|
"@types/prop-types": "^15.7.5",
|
||||||
"@types/react": "18.2.0",
|
"@types/react": "18.0.0",
|
||||||
"@types/react-dom": "18.0.0",
|
"@types/react-dom": "18.0.0",
|
||||||
"@types/react-router-hash-link": "2.4.5",
|
"@types/react-router-hash-link": "2.4.5",
|
||||||
"ace-builds": "1.12.3",
|
"ace-builds": "1.12.3",
|
||||||
@ -33,7 +33,7 @@
|
|||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"formik": "2.2.9",
|
"formik": "2.2.9",
|
||||||
"html-react-parser": "1.4.8",
|
"html-react-parser": "1.4.8",
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
"jwt-decode": "3.1.2",
|
"jwt-decode": "3.1.2",
|
||||||
"rapidoc": "9.3.4",
|
"rapidoc": "9.3.4",
|
||||||
@ -46,16 +46,12 @@
|
|||||||
"react-dom": "18.0.0",
|
"react-dom": "18.0.0",
|
||||||
"react-ga4": "2.1.0",
|
"react-ga4": "2.1.0",
|
||||||
"react-github-btn": "1.2.1",
|
"react-github-btn": "1.2.1",
|
||||||
"react-google-drive-picker": "1.2.0",
|
"react-google-drive-picker": "^1.2.0",
|
||||||
"react-markdown": "9.0.1",
|
"react-markdown": "9.0.1",
|
||||||
"react-router-dom": "6.2.1",
|
"react-router-dom": "6.2.1",
|
||||||
"react-router-hash-link": "2.4.3",
|
"react-router-hash-link": "2.4.3",
|
||||||
"react-table": "7.7.0",
|
"react-table": "7.7.0",
|
||||||
"sass": "1.63.4",
|
"sass": "1.63.4",
|
||||||
"sequential-workflow-designer": "0.22.0",
|
|
||||||
"sequential-workflow-designer-react": "0.22.0",
|
|
||||||
"sequential-workflow-editor": "0.13.2",
|
|
||||||
"sequential-workflow-editor-model": "0.13.2",
|
|
||||||
"ts-md5": "1.2.11",
|
"ts-md5": "1.2.11",
|
||||||
"yup": "0.32.11"
|
"yup": "0.32.11"
|
||||||
},
|
},
|
||||||
|
6
pom.xml
6
pom.xml
@ -29,7 +29,7 @@
|
|||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>0.21.0-SNAPSHOT</revision>
|
<revision>0.23.0-SNAPSHOT</revision>
|
||||||
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.kingsrook.qqq</groupId>
|
<groupId>com.kingsrook.qqq</groupId>
|
||||||
<artifactId>qqq-backend-core</artifactId>
|
<artifactId>qqq-backend-core</artifactId>
|
||||||
<version>0.20.0-20240308.165846-65</version>
|
<version>0.21.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
@ -154,11 +154,11 @@
|
|||||||
<versionTagPrefix>version-</versionTagPrefix>
|
<versionTagPrefix>version-</versionTagPrefix>
|
||||||
</gitFlowConfig>
|
</gitFlowConfig>
|
||||||
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
|
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
|
||||||
<postReleaseGoals>install</postReleaseGoals> <!-- Let CI run deploys -->
|
|
||||||
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
|
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
|
||||||
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
|
||||||
<versionProperty>revision</versionProperty>
|
<versionProperty>revision</versionProperty>
|
||||||
<skipUpdateVersion>true</skipUpdateVersion>
|
<skipUpdateVersion>true</skipUpdateVersion>
|
||||||
|
<skipTestProject>true</skipTestProject> <!-- we allow CI to do the tests -->
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
|
@ -30,14 +30,17 @@ import MDButton from "qqq/components/legacy/MDButton";
|
|||||||
|
|
||||||
export const standardWidth = "150px";
|
export const standardWidth = "150px";
|
||||||
|
|
||||||
|
const standardML = {xs: 1, md: 3};
|
||||||
|
|
||||||
interface QCreateNewButtonProps
|
interface QCreateNewButtonProps
|
||||||
{
|
{
|
||||||
tablePath: string;
|
tablePath: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box display="inline-block" ml={3} mr={0} width={standardWidth}>
|
<Box display="inline-block" ml={standardML} mr={0} width={standardWidth}>
|
||||||
<Link to={`${tablePath}/create`}>
|
<Link to={`${tablePath}/create`}>
|
||||||
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
|
||||||
Create New
|
Create New
|
||||||
@ -54,6 +57,7 @@ interface QSaveButtonProps
|
|||||||
onClickHandler?: any,
|
onClickHandler?: any,
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
QSaveButton.defaultProps = {
|
QSaveButton.defaultProps = {
|
||||||
label: "Save",
|
label: "Save",
|
||||||
iconName: "save"
|
iconName: "save"
|
||||||
@ -62,7 +66,7 @@ QSaveButton.defaultProps = {
|
|||||||
export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||||
{label}
|
{label}
|
||||||
</MDButton>
|
</MDButton>
|
||||||
@ -72,17 +76,18 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
|
|||||||
|
|
||||||
interface QDeleteButtonProps
|
interface QDeleteButtonProps
|
||||||
{
|
{
|
||||||
onClickHandler: any
|
onClickHandler: any;
|
||||||
disabled?: boolean
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDeleteButton.defaultProps = {
|
QDeleteButton.defaultProps = {
|
||||||
disabled: false
|
disabled: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
|
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
|
||||||
Delete
|
Delete
|
||||||
</MDButton>
|
</MDButton>
|
||||||
@ -93,7 +98,7 @@ export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): J
|
|||||||
export function QEditButton(): JSX.Element
|
export function QEditButton(): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<Link to="edit">
|
<Link to="edit">
|
||||||
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
|
||||||
Edit
|
Edit
|
||||||
@ -132,7 +137,7 @@ interface QCancelButtonProps
|
|||||||
onClickHandler: any;
|
onClickHandler: any;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
iconName?: string
|
iconName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QCancelButton({
|
export function QCancelButton({
|
||||||
@ -140,7 +145,7 @@ export function QCancelButton({
|
|||||||
}: QCancelButtonProps): JSX.Element
|
}: QCancelButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml="auto" width={standardWidth}>
|
<Box ml={standardML} mb={2} width={standardWidth}>
|
||||||
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
|
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
|
||||||
{label}
|
{label}
|
||||||
</MDButton>
|
</MDButton>
|
||||||
@ -155,15 +160,15 @@ QCancelButton.defaultProps = {
|
|||||||
|
|
||||||
interface QSubmitButtonProps
|
interface QSubmitButtonProps
|
||||||
{
|
{
|
||||||
label?: string
|
label?: string;
|
||||||
iconName?: string
|
iconName?: string;
|
||||||
disabled: boolean
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
|
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box ml={3} width={standardWidth}>
|
<Box ml={standardML} width={standardWidth}>
|
||||||
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
|
||||||
{label}
|
{label}
|
||||||
</MDButton>
|
</MDButton>
|
||||||
|
@ -172,17 +172,14 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
|
|||||||
<Grid item xs={12} sm={6} key={fieldName}>
|
<Grid item xs={12} sm={6} key={fieldName}>
|
||||||
{labelElement}
|
{labelElement}
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={field.possibleValueProps.tableName}
|
fieldPossibleValueProps={field.possibleValueProps}
|
||||||
processName={field.possibleValueProps.processName}
|
|
||||||
possibleValueSourceName={field.possibleValueProps.possibleValueSourceName}
|
|
||||||
fieldName={field.possibleValueProps.fieldName}
|
|
||||||
isEditable={field.isEditable}
|
isEditable={field.isEditable}
|
||||||
fieldLabel=""
|
fieldLabel=""
|
||||||
initialValue={values[fieldName]}
|
initialValue={values[fieldName]}
|
||||||
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
|
|
||||||
bulkEditMode={bulkEditMode}
|
bulkEditMode={bulkEditMode}
|
||||||
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
|
||||||
otherValues={otherValuesMap}
|
otherValues={otherValuesMap}
|
||||||
|
useCase="form"
|
||||||
/>
|
/>
|
||||||
{formattedHelpContent}
|
{formattedHelpContent}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -40,6 +40,8 @@ interface Props
|
|||||||
value: any;
|
value: any;
|
||||||
type: string;
|
type: string;
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ interface Props
|
|||||||
}
|
}
|
||||||
|
|
||||||
function QDynamicFormField({
|
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
|
}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
const [switchChecked, setSwitchChecked] = useState(false);
|
const [switchChecked, setSwitchChecked] = useState(false);
|
||||||
@ -65,18 +67,30 @@ function QDynamicFormField({
|
|||||||
inputLabelProps.shrink = true;
|
inputLabelProps.shrink = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputProps = {};
|
const inputProps: any = {};
|
||||||
if (displayFormat && displayFormat.startsWith("$"))
|
if (displayFormat && displayFormat.startsWith("$"))
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
|
||||||
inputProps.startAdornment = <InputAdornment position="start">$</InputAdornment>;
|
inputProps.startAdornment = <InputAdornment position="start">$</InputAdornment>;
|
||||||
}
|
}
|
||||||
if (displayFormat && displayFormat.endsWith("%%"))
|
if (displayFormat && displayFormat.endsWith("%%"))
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
|
||||||
inputProps.endAdornment = <InputAdornment position="end">%</InputAdornment>;
|
inputProps.endAdornment = <InputAdornment position="end">%</InputAdornment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (placeholder)
|
||||||
|
{
|
||||||
|
inputProps.placeholder = placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
if(backgroundColor)
|
||||||
|
{
|
||||||
|
inputProps.sx = {
|
||||||
|
"&.MuiInputBase-root": {
|
||||||
|
backgroundColor: backgroundColor
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const handleOnWheel = (e) =>
|
const handleOnWheel = (e) =>
|
||||||
{
|
{
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
|
||||||
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
|
||||||
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
|
||||||
|
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
|
|
||||||
|
|
||||||
@ -129,18 +130,11 @@ class DynamicFormUtils
|
|||||||
|
|
||||||
if (effectivelyIsRequired)
|
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... //
|
||||||
// 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 (Yup.string().required(`${field.label} is required.`).nullable(true));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return (Yup.string().required(`${field.label} is required.`));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return (null);
|
return (null);
|
||||||
}
|
}
|
||||||
@ -155,47 +149,49 @@ class DynamicFormUtils
|
|||||||
{
|
{
|
||||||
const field = qFields[i];
|
const field = qFields[i];
|
||||||
|
|
||||||
|
if(!dynamicFormFields[field.name])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
// add props for possible value fields //
|
// add props for possible value fields //
|
||||||
/////////////////////////////////////////
|
/////////////////////////////////////////
|
||||||
if (field.possibleValueSourceName && dynamicFormFields[field.name])
|
if (field.possibleValueSourceName || field.inlinePossibleValueSource)
|
||||||
{
|
{
|
||||||
let initialDisplayValue = null;
|
let props: FieldPossibleValueProps =
|
||||||
|
{
|
||||||
|
isPossibleValue: true,
|
||||||
|
fieldName: field.name,
|
||||||
|
initialDisplayValue: null
|
||||||
|
}
|
||||||
|
|
||||||
if (displayValues)
|
if (displayValues)
|
||||||
{
|
{
|
||||||
initialDisplayValue = displayValues.get(field.name);
|
props.initialDisplayValue = displayValues.get(field.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tableName)
|
if(field.inlinePossibleValueSource)
|
||||||
{
|
{
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
//////////////////////////////////////////////////////////////////////
|
||||||
{
|
// handle an inline PVS - which is a list of possible value objects //
|
||||||
isPossibleValue: true,
|
//////////////////////////////////////////////////////////////////////
|
||||||
tableName: tableName,
|
props.possibleValues = field.inlinePossibleValueSource;
|
||||||
fieldName: field.name,
|
}
|
||||||
initialDisplayValue: initialDisplayValue,
|
else if (tableName)
|
||||||
};
|
{
|
||||||
|
props.tableName = tableName;
|
||||||
}
|
}
|
||||||
else if (processName)
|
else if (processName)
|
||||||
{
|
{
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
props.processName = processName;
|
||||||
{
|
|
||||||
isPossibleValue: true,
|
|
||||||
processName: processName,
|
|
||||||
fieldName: field.name,
|
|
||||||
initialDisplayValue: initialDisplayValue,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
dynamicFormFields[field.name].possibleValueProps =
|
props.possibleValueSourceName = field.possibleValueSourceName;
|
||||||
{
|
|
||||||
isPossibleValue: true,
|
|
||||||
initialDisplayValue: initialDisplayValue,
|
|
||||||
fieldName: field.name,
|
|
||||||
possibleValueSourceName: field.possibleValueSourceName
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dynamicFormFields[field.name].possibleValueProps = props;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,20 +30,17 @@ import TextField from "@mui/material/TextField";
|
|||||||
import {ErrorMessage, useFormikContext} from "formik";
|
import {ErrorMessage, useFormikContext} from "formik";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
|
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import React, {useEffect, useState} from "react";
|
import React, {useEffect, useState} from "react";
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
{
|
{
|
||||||
tableName?: string;
|
fieldPossibleValueProps: FieldPossibleValueProps;
|
||||||
processName?: string;
|
|
||||||
fieldName?: string;
|
|
||||||
possibleValueSourceName?: string;
|
|
||||||
overrideId?: string;
|
overrideId?: string;
|
||||||
fieldLabel: string;
|
fieldLabel: string;
|
||||||
inForm: boolean;
|
inForm: boolean;
|
||||||
initialValue?: any;
|
initialValue?: any;
|
||||||
initialDisplayValue?: string;
|
|
||||||
initialValues?: QPossibleValue[];
|
initialValues?: QPossibleValue[];
|
||||||
onChange?: any;
|
onChange?: any;
|
||||||
isEditable?: boolean;
|
isEditable?: boolean;
|
||||||
@ -53,16 +50,12 @@ interface Props
|
|||||||
otherValues?: Map<string, any>;
|
otherValues?: Map<string, any>;
|
||||||
variant: "standard" | "outlined";
|
variant: "standard" | "outlined";
|
||||||
initiallyOpen: boolean;
|
initiallyOpen: boolean;
|
||||||
|
useCase: "form" | "filter";
|
||||||
}
|
}
|
||||||
|
|
||||||
DynamicSelect.defaultProps = {
|
DynamicSelect.defaultProps = {
|
||||||
tableName: null,
|
|
||||||
processName: null,
|
|
||||||
fieldName: null,
|
|
||||||
possibleValueSourceName: null,
|
|
||||||
inForm: true,
|
inForm: true,
|
||||||
initialValue: null,
|
initialValue: null,
|
||||||
initialDisplayValue: null,
|
|
||||||
initialValues: undefined,
|
initialValues: undefined,
|
||||||
onChange: null,
|
onChange: null,
|
||||||
isEditable: true,
|
isEditable: true,
|
||||||
@ -102,8 +95,10 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
|
|||||||
|
|
||||||
const qController = Client.getInstance();
|
const qController = Client.getInstance();
|
||||||
|
|
||||||
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
|
function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm, initialValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props)
|
||||||
{
|
{
|
||||||
|
const {fieldName, initialDisplayValue, possibleValueSourceName, possibleValues, processName, tableName} = fieldPossibleValueProps;
|
||||||
|
|
||||||
const [open, setOpen] = useState(initiallyOpen);
|
const [open, setOpen] = useState(initiallyOpen);
|
||||||
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState(null);
|
const [searchTerm, setSearchTerm] = useState(null);
|
||||||
@ -171,6 +166,35 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
setFieldValueRef = setFieldValue;
|
setFieldValueRef = setFieldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
|
||||||
|
{
|
||||||
|
return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
const loadResults = async (): Promise<QPossibleValue[]> =>
|
||||||
|
{
|
||||||
|
if(possibleValues)
|
||||||
|
{
|
||||||
|
return filterInlinePossibleValues(searchTerm, possibleValues)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
@ -194,7 +218,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
(async () =>
|
(async () =>
|
||||||
{
|
{
|
||||||
// console.log(`doing a search with ${searchTerm}`);
|
// console.log(`doing a search with ${searchTerm}`);
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
|
const results: QPossibleValue[] = await loadResults();
|
||||||
|
|
||||||
if (tableMetaData == null && tableName)
|
if (tableMetaData == null && tableName)
|
||||||
{
|
{
|
||||||
@ -217,7 +241,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
};
|
};
|
||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
// todo - finish... call it in onOpen?
|
|
||||||
|
/***************************************************************************
|
||||||
|
** todo - finish... call it in onOpen?
|
||||||
|
***************************************************************************/
|
||||||
const reloadIfOtherValuesAreChanged = () =>
|
const reloadIfOtherValuesAreChanged = () =>
|
||||||
{
|
{
|
||||||
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
|
||||||
@ -226,8 +253,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
{
|
{
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
|
|
||||||
console.log("Refreshing possible values...");
|
console.log("Refreshing possible values...");
|
||||||
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
|
const results: QPossibleValue[] = await loadResults();
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setOptions([...results]);
|
setOptions([...results]);
|
||||||
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
|
||||||
@ -235,6 +264,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
|
||||||
{
|
{
|
||||||
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
|
||||||
@ -245,11 +278,19 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const handleBlur = (x: any) =>
|
const handleBlur = (x: any) =>
|
||||||
{
|
{
|
||||||
setSearchTerm(null);
|
setSearchTerm(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
|
||||||
{
|
{
|
||||||
// console.log("handleChanged. value is:");
|
// console.log("handleChanged. value is:");
|
||||||
@ -273,6 +314,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] =>
|
const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] =>
|
||||||
{
|
{
|
||||||
/////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -282,6 +327,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
return (options);
|
return (options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const renderOption = (props: Object, option: any, {selected}) =>
|
const renderOption = (props: Object, option: any, {selected}) =>
|
||||||
{
|
{
|
||||||
@ -330,6 +379,10 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
const bulkEditSwitchChanged = () =>
|
const bulkEditSwitchChanged = () =>
|
||||||
{
|
{
|
||||||
const newSwitchValue = !switchChecked;
|
const newSwitchValue = !switchChecked;
|
||||||
@ -350,7 +403,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
const autocomplete = (
|
const autocomplete = (
|
||||||
<Box>
|
<Box>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
id={overrideId ?? fieldName ?? possibleValueSourceName}
|
id={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"}
|
||||||
sx={autocompleteSX}
|
sx={autocompleteSX}
|
||||||
open={open}
|
open={open}
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -430,7 +483,7 @@ function DynamicSelect({tableName, processName, fieldName, possibleValueSourceNa
|
|||||||
inForm &&
|
inForm &&
|
||||||
<Box mt={0.75}>
|
<Box mt={0.75}>
|
||||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
|
||||||
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName ?? possibleValueSourceName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
|
@ -502,7 +502,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
/////////////////////////////////////////////////
|
/////////////////////////////////////////////////
|
||||||
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
|
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
|
||||||
{
|
{
|
||||||
const widget = metaData?.widgets.get(section.widgetName);
|
const widget = metaData?.widgets?.get(section.widgetName);
|
||||||
if (widget)
|
if (widget)
|
||||||
{
|
{
|
||||||
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
|
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
|
||||||
@ -602,7 +602,7 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if (fieldMetaData.possibleValueSourceName)
|
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)
|
if (results && results.length > 0)
|
||||||
{
|
{
|
||||||
defaultDisplayValues.set(fieldName, results[0].label);
|
defaultDisplayValues.set(fieldName, results[0].label);
|
||||||
@ -1152,11 +1152,11 @@ function EntityForm(props: Props): JSX.Element
|
|||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{
|
{
|
||||||
!props.isModal &&
|
!props.isModal &&
|
||||||
<Grid item xs={12} lg={3}>
|
<Grid item xs={12} lg={3} className="recordSidebar">
|
||||||
<QRecordSidebar tableSections={tableSections} />
|
<QRecordSidebar tableSections={tableSections} />
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
<Grid item xs={12} lg={props.isModal ? 12 : 9}>
|
<Grid item xs={12} lg={props.isModal ? 12 : 9} className={props.isModal ? "" : "recordWithSidebar"}>
|
||||||
|
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
@ -64,13 +64,14 @@ function Footer({company, links}: Props): JSX.Element
|
|||||||
<Box
|
<Box
|
||||||
width="100%"
|
width="100%"
|
||||||
display="flex"
|
display="flex"
|
||||||
flexDirection={{xs: "column", lg: "row"}}
|
flexDirection={{xs: "column", md: "row"}}
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
px={1.5}
|
px={1.5}
|
||||||
style={{
|
style={{
|
||||||
position: "fixed", bottom: "0px", zIndex: -1, marginBottom: "10px",
|
position: "fixed", bottom: "0px", zIndex: -1, marginBottom: "10px",
|
||||||
}}
|
}}
|
||||||
|
left={{xs: "0", xl: "auto"}}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
href && name &&
|
href && name &&
|
||||||
|
@ -25,6 +25,7 @@ import Autocomplete from "@mui/material/Autocomplete";
|
|||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import IconButton from "@mui/material/IconButton";
|
import IconButton from "@mui/material/IconButton";
|
||||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||||
|
import {Theme} from "@mui/material/styles";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Toolbar from "@mui/material/Toolbar";
|
import Toolbar from "@mui/material/Toolbar";
|
||||||
import React, {useContext, useEffect, useRef, useState} from "react";
|
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]);
|
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 (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position={absolute ? "absolute" : navbarType}
|
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} />
|
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
|
||||||
</Box>
|
</Box>
|
||||||
{isMini ? null : (
|
{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"}}}>
|
<Box mt={"-0.25rem"} pb={"0.75rem"} pr={2} mr={-2} sx={{"& *": {cursor: "pointer !important"}}}>
|
||||||
{renderHistory()}
|
{renderHistory()}
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -84,7 +84,7 @@ function ProcessSummaryResults({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box m={3} mt={6}>
|
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={0} lg={2} />
|
<Grid item xs={0} lg={2} />
|
||||||
<Grid item xs={12} lg={8}>
|
<Grid item xs={12} lg={8}>
|
||||||
|
@ -273,7 +273,7 @@ function ValidationReview({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box m={3}>
|
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12} lg={6}>
|
<Grid item xs={12} lg={6}>
|
||||||
<MDTypography color="body" variant="button">
|
<MDTypography color="body" variant="button">
|
||||||
|
@ -183,7 +183,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
|
|||||||
{
|
{
|
||||||
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
|
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
|
||||||
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
|
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
|
||||||
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
|
if (field?.type == QFieldType.DATE_TIME)
|
||||||
{
|
{
|
||||||
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
defaultOperator = QCriteriaOperator.GREATER_THAN;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export function EvaluatedExpression({field, expression}: EvaluatedExpressionProp
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</>;
|
return <span style={{fontVariantNumeric: "tabular-nums"}}>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HOUR_MS = 60 * 60 * 1000;
|
const HOUR_MS = 60 * 60 * 1000;
|
||||||
|
@ -367,16 +367,15 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
) : (
|
) : (
|
||||||
<Box width={"100%"}>
|
<Box width={"100%"}>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={table.name}
|
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
|
||||||
fieldName={field.name}
|
|
||||||
overrideId={field.name + "-single-" + criteria.id}
|
overrideId={field.name + "-single-" + criteria.id}
|
||||||
key={field.name + "-single-" + criteria.id}
|
key={field.name + "-single-" + criteria.id}
|
||||||
fieldLabel="Value"
|
fieldLabel="Value"
|
||||||
initialValue={selectedPossibleValue?.id}
|
initialValue={selectedPossibleValue?.id}
|
||||||
initialDisplayValue={selectedPossibleValue?.label}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
onChange={(value: any) => valueChangeHandler(null, 0, value)}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
|
useCase="filter"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
@ -401,8 +400,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
}
|
}
|
||||||
return <Box>
|
return <Box>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={table.name}
|
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
|
||||||
fieldName={field.name}
|
|
||||||
overrideId={field.name + "-multi-" + criteria.id}
|
overrideId={field.name + "-multi-" + criteria.id}
|
||||||
key={field.name + "-multi-" + criteria.id}
|
key={field.name + "-multi-" + criteria.id}
|
||||||
isMultiple
|
isMultiple
|
||||||
@ -412,6 +410,7 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
|
|||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
onChange={(value: any) => valueChangeHandler(null, "all", value)}
|
||||||
variant="standard"
|
variant="standard"
|
||||||
|
useCase="filter"
|
||||||
/>
|
/>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
|
@ -40,16 +40,17 @@ import Snackbar from "@mui/material/Snackbar";
|
|||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import FormData from "form-data";
|
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 {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
||||||
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
import DynamicSelect from "qqq/components/forms/DynamicSelect";
|
||||||
import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm";
|
import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm";
|
||||||
import ScriptTestForm from "qqq/components/scripts/ScriptTestForm";
|
import ScriptTestForm from "qqq/components/scripts/ScriptTestForm";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
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/mode-javascript";
|
||||||
import "ace-builds/src-noconflict/theme-github";
|
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";
|
import "ace-builds/src-noconflict/ext-language_tools";
|
||||||
|
|
||||||
export interface ScriptEditorProps
|
export interface ScriptEditorProps
|
||||||
@ -69,15 +70,15 @@ const qController = Client.getInstance();
|
|||||||
|
|
||||||
function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
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");
|
console.log("Missing scriptTypeFileSchemaList");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
let files = scriptRevisionRecord?.associatedRecords?.get("files")
|
let files = scriptRevisionRecord?.associatedRecords?.get("files");
|
||||||
|
|
||||||
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
|
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++)
|
for (let j = 0; j < files?.length; j++)
|
||||||
{
|
{
|
||||||
let file = files[j];
|
let file = files[j];
|
||||||
if(file.values.get("fileName") == name)
|
if (file.values.get("fileName") == name)
|
||||||
{
|
{
|
||||||
contents = file.values.get("contents");
|
contents = file.values.get("contents");
|
||||||
}
|
}
|
||||||
@ -103,9 +104,9 @@ function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFi
|
|||||||
|
|
||||||
function buildFileTypeMap(scriptTypeFileSchemaList: QRecord[]): { [name: string]: string }
|
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");
|
console.log("Missing scriptTypeFileSchemaList");
|
||||||
}
|
}
|
||||||
@ -125,21 +126,21 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
{
|
{
|
||||||
const [closing, setClosing] = useState(false);
|
const [closing, setClosing] = useState(false);
|
||||||
|
|
||||||
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
|
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null);
|
||||||
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
|
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null);
|
||||||
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
|
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null);
|
||||||
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.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 [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema);
|
||||||
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]])
|
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]]);
|
||||||
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList))
|
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList));
|
||||||
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList))
|
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList));
|
||||||
console.log(`file types: ${JSON.stringify(fileTypes)}`);
|
console.log(`file types: ${JSON.stringify(fileTypes)}`);
|
||||||
|
|
||||||
const [commitMessage, setCommitMessage] = useState("")
|
const [commitMessage, setCommitMessage] = useState("");
|
||||||
const [openTool, setOpenTool] = useState(null);
|
const [openTool, setOpenTool] = useState(null);
|
||||||
const [errorAlert, setErrorAlert] = useState("")
|
const [errorAlert, setErrorAlert] = useState("");
|
||||||
const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false);
|
const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false);
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
@ -241,19 +242,19 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
// need this to make Ace recognize new height.
|
// need this to make Ace recognize new height.
|
||||||
setTimeout(() =>
|
setTimeout(() =>
|
||||||
{
|
{
|
||||||
window.dispatchEvent(new Event("resize"))
|
window.dispatchEvent(new Event("resize"));
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveClicked = (overrideCommitMessage?: string) =>
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!commitMessage && !overrideCommitMessage)
|
if (!commitMessage && !overrideCommitMessage)
|
||||||
{
|
{
|
||||||
setPromptForCommitMessageOpen(true);
|
setPromptForCommitMessageOpen(true);
|
||||||
return;
|
return;
|
||||||
@ -267,18 +268,18 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
formData.append("scriptId", scriptId);
|
formData.append("scriptId", scriptId);
|
||||||
formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
|
formData.append("commitMessage", overrideCommitMessage ?? commitMessage);
|
||||||
|
|
||||||
if(apiName)
|
if (apiName)
|
||||||
{
|
{
|
||||||
formData.append("apiName", apiName);
|
formData.append("apiName", apiName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(apiVersion)
|
if (apiVersion)
|
||||||
{
|
{
|
||||||
formData.append("apiVersion", 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(","));
|
formData.append("fileNames", fileNamesFromSchema.join(","));
|
||||||
|
|
||||||
for (let fileName in fileContents)
|
for (let fileName in fileContents)
|
||||||
@ -299,58 +300,58 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
|
|
||||||
if (processResult instanceof QJobError)
|
if (processResult instanceof QJobError)
|
||||||
{
|
{
|
||||||
const jobError = processResult as QJobError
|
const jobError = processResult as QJobError;
|
||||||
setErrorAlert(jobError.userFacingError ?? jobError.error)
|
setErrorAlert(jobError.userFacingError ?? jobError.error);
|
||||||
setClosing(false);
|
setClosing(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeCallback(null, "saved", "Saved New Script Version");
|
closeCallback(null, "saved", "Saved New Script Version");
|
||||||
}
|
}
|
||||||
catch(e)
|
catch (e)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
setErrorAlert(e.message ?? "Unexpected error saving script")
|
setErrorAlert(e.message ?? "Unexpected error saving script");
|
||||||
setClosing(false);
|
setClosing(false);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
};
|
||||||
|
|
||||||
const cancelClicked = () =>
|
const cancelClicked = () =>
|
||||||
{
|
{
|
||||||
setClosing(true);
|
setClosing(true);
|
||||||
closeCallback(null, "cancelled");
|
closeCallback(null, "cancelled");
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateCode = (value: string, event: any, index: number) =>
|
const updateCode = (value: string, event: any, index: number) =>
|
||||||
{
|
{
|
||||||
fileContents[openEditorFileNames[index]] = value;
|
fileContents[openEditorFileNames[index]] = value;
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
|
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
setCommitMessage(event.target.value);
|
setCommitMessage(event.target.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) =>
|
const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) =>
|
||||||
{
|
{
|
||||||
setPromptForCommitMessageOpen(false);
|
setPromptForCommitMessageOpen(false);
|
||||||
|
|
||||||
if(wasSaveClicked)
|
if (wasSaveClicked)
|
||||||
{
|
{
|
||||||
setCommitMessage(message)
|
setCommitMessage(message);
|
||||||
saveClicked(message);
|
saveClicked(message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
setClosing(false);
|
setClosing(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const changeApiName = (apiNamePossibleValue?: QPossibleValue) =>
|
const changeApiName = (apiNamePossibleValue?: QPossibleValue) =>
|
||||||
{
|
{
|
||||||
if(apiNamePossibleValue)
|
if (apiNamePossibleValue)
|
||||||
{
|
{
|
||||||
setApiName(apiNamePossibleValue.id);
|
setApiName(apiNamePossibleValue.id);
|
||||||
}
|
}
|
||||||
@ -358,11 +359,11 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
{
|
{
|
||||||
setApiName(null);
|
setApiName(null);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) =>
|
const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) =>
|
||||||
{
|
{
|
||||||
if(apiVersionPossibleValue)
|
if (apiVersionPossibleValue)
|
||||||
{
|
{
|
||||||
setApiVersion(apiVersionPossibleValue.id);
|
setApiVersion(apiVersionPossibleValue.id);
|
||||||
}
|
}
|
||||||
@ -370,33 +371,33 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
{
|
{
|
||||||
setApiVersion(null);
|
setApiVersion(null);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
|
const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
|
||||||
{
|
{
|
||||||
openEditorFileNames[index] = event.target.value
|
openEditorFileNames[index] = event.target.value;
|
||||||
setOpenEditorFileNames(openEditorFileNames);
|
setOpenEditorFileNames(openEditorFileNames);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
};
|
||||||
|
|
||||||
const splitEditorClicked = () =>
|
const splitEditorClicked = () =>
|
||||||
{
|
{
|
||||||
openEditorFileNames.push(availableFileNames[0])
|
openEditorFileNames.push(availableFileNames[0]);
|
||||||
setOpenEditorFileNames(openEditorFileNames);
|
setOpenEditorFileNames(openEditorFileNames);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
};
|
||||||
|
|
||||||
const closeEditorClicked = (index: number) =>
|
const closeEditorClicked = (index: number) =>
|
||||||
{
|
{
|
||||||
openEditorFileNames.splice(index, 1)
|
openEditorFileNames.splice(index, 1);
|
||||||
setOpenEditorFileNames(openEditorFileNames);
|
setOpenEditorFileNames(openEditorFileNames);
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
};
|
||||||
|
|
||||||
const computeEditorWidth = (): string =>
|
const computeEditorWidth = (): string =>
|
||||||
{
|
{
|
||||||
return (100 / openEditorFileNames.length) + "%"
|
return (100 / openEditorFileNames.length) + "%";
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="scriptEditor" sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
<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;
|
return;
|
||||||
}
|
}
|
||||||
setErrorAlert("")
|
setErrorAlert("");
|
||||||
}} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
}} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
||||||
<Alert color="error" onClose={() => setErrorAlert("")}>
|
<Alert color="error" onClose={() => setErrorAlert("")}>
|
||||||
{errorAlert}
|
{errorAlert}
|
||||||
@ -440,10 +441,10 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
||||||
<Grid container alignItems="flex-end">
|
<Grid container alignItems="flex-end">
|
||||||
<Box maxWidth={"50%"} minWidth={300}>
|
<Box maxWidth={"50%"} minWidth={300}>
|
||||||
<DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} />
|
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiName", initialDisplayValue: apiNameLabel}} initialValue={apiName} fieldLabel={"API Name *"} inForm={false} onChange={changeApiName} useCase="form" />
|
||||||
</Box>
|
</Box>
|
||||||
<Box maxWidth={"50%"} minWidth={300} pl={2}>
|
<Box maxWidth={"50%"} minWidth={300} pl={2}>
|
||||||
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
|
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiVersion", initialDisplayValue: apiVersionLabel}} initialValue={apiVersion} fieldLabel={"API Version *"} inForm={false} onChange={changeApiVersion} useCase="form" />
|
||||||
</Box>
|
</Box>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Box display="flex" sx={{height: "100%"}}>
|
<Box display="flex" sx={{height: "100%"}}>
|
||||||
@ -464,19 +465,19 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
<Box>
|
<Box>
|
||||||
{
|
{
|
||||||
openEditorFileNames.length > 1 &&
|
openEditorFileNames.length > 1 &&
|
||||||
<Tooltip title="Close this editor split" enterDelay={500}>
|
<Tooltip title="Close this editor split" enterDelay={500}>
|
||||||
<IconButton size="small" onClick={() => closeEditorClicked(index)}>
|
<IconButton size="small" onClick={() => closeEditorClicked(index)}>
|
||||||
<Icon>close</Icon>
|
<Icon>close</Icon>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
index == openEditorFileNames.length - 1 &&
|
index == openEditorFileNames.length - 1 &&
|
||||||
<Tooltip title="Open a new editor split" enterDelay={500}>
|
<Tooltip title="Open a new editor split" enterDelay={500}>
|
||||||
<IconButton size="small" onClick={splitEditorClicked}>
|
<IconButton size="small" onClick={splitEditorClicked}>
|
||||||
<Icon>vertical_split</Icon>
|
<Icon>vertical_split</Icon>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@ -526,29 +527,29 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<CommitMessagePrompt isOpen={promptForCommitMessageOpen} closeHandler={closePromptForCommitMessage}/>
|
<CommitMessagePrompt isOpen={promptForCommitMessageOpen} closeHandler={closePromptForCommitMessage} />
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</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>) =>
|
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
{
|
{
|
||||||
setCommitMessage(event.target.value);
|
setCommitMessage(event.target.value);
|
||||||
}
|
};
|
||||||
|
|
||||||
const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) =>
|
||||||
{
|
{
|
||||||
if(e.key === "Enter")
|
if (e.key === "Enter")
|
||||||
{
|
{
|
||||||
props.closeHandler(true, commitMessage);
|
props.closeHandler(true, commitMessage);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -579,10 +580,10 @@ function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClic
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<QCancelButton onClickHandler={() => props.closeHandler(false)} disabled={false} />
|
<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>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ScriptEditor;
|
export default ScriptEditor;
|
||||||
|
@ -391,12 +391,12 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
|
|||||||
<Box display="flex" flexDirection="row" alignItems="center">
|
<Box display="flex" flexDirection="row" alignItems="center">
|
||||||
<Box width="550px" pr={2} mb={-1.5}>
|
<Box width="550px" pr={2} mb={-1.5}>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
|
fieldPossibleValueProps={{possibleValueSourceName: shareableTableMetaData.audiencePossibleValueSourceName, initialDisplayValue: selectedAudienceOption?.label}}
|
||||||
fieldLabel="User or Group" // todo should come from shareableTableMetaData
|
fieldLabel="User or Group" // todo should come from shareableTableMetaData
|
||||||
initialValue={selectedAudienceOption?.id}
|
initialValue={selectedAudienceOption?.id}
|
||||||
initialDisplayValue={selectedAudienceOption?.label}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={handleAudienceChange}
|
onChange={handleAudienceChange}
|
||||||
|
useCase="form"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{/*
|
{/*
|
||||||
|
@ -22,16 +22,25 @@
|
|||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {Box, Skeleton} from "@mui/material";
|
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 {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
|
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[];
|
blocks: BlockData[];
|
||||||
styleOverrides?: any;
|
styleOverrides?: any;
|
||||||
layout?: string;
|
layout?: string;
|
||||||
|
overlayHtml?: string;
|
||||||
|
overlayStyleOverrides?: any;
|
||||||
|
modalMode: string;
|
||||||
|
styles?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -39,13 +48,15 @@ interface CompositeWidgetProps
|
|||||||
{
|
{
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData;
|
||||||
data: CompositeData;
|
data: CompositeData;
|
||||||
|
actionCallback?: (blockData: BlockData, eventValues?: { [name: string]: any }) => boolean;
|
||||||
|
values?: { [key: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Widget which is a list of Blocks.
|
** Widget which is a list of Blocks.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element
|
export default function CompositeWidget({widgetMetaData, data, actionCallback, values}: CompositeWidgetProps): JSX.Element
|
||||||
{
|
{
|
||||||
if (!data || !data.blocks)
|
if (!data || !data.blocks)
|
||||||
{
|
{
|
||||||
@ -71,6 +82,12 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
boxStyle.flexWrap = "wrap";
|
boxStyle.flexWrap = "wrap";
|
||||||
boxStyle.gap = "0.5rem";
|
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")
|
else if (layout == "FLEX_ROW_SPACE_BETWEEN")
|
||||||
{
|
{
|
||||||
boxStyle.display = "flex";
|
boxStyle.display = "flex";
|
||||||
@ -78,6 +95,14 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
boxStyle.justifyContent = "space-between";
|
boxStyle.justifyContent = "space-between";
|
||||||
boxStyle.gap = "0.25rem";
|
boxStyle.gap = "0.25rem";
|
||||||
}
|
}
|
||||||
|
else if (layout == "FLEX_ROW_CENTER")
|
||||||
|
{
|
||||||
|
boxStyle.display = "flex";
|
||||||
|
boxStyle.flexDirection = "row";
|
||||||
|
boxStyle.justifyContent = "center";
|
||||||
|
boxStyle.gap = "0.25rem";
|
||||||
|
boxStyle.flexWrap = "wrap";
|
||||||
|
}
|
||||||
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
else if (layout == "TABLE_SUB_ROW_DETAILS")
|
||||||
{
|
{
|
||||||
boxStyle.display = "flex";
|
boxStyle.display = "flex";
|
||||||
@ -97,20 +122,96 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
|
|||||||
boxStyle.borderRadius = "0.5rem";
|
boxStyle.borderRadius = "0.5rem";
|
||||||
boxStyle.background = "#FFFFFF";
|
boxStyle.background = "#FFFFFF";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.styleOverrides)
|
if (data?.styleOverrides)
|
||||||
{
|
{
|
||||||
boxStyle = {...boxStyle, ...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) => (
|
setIsModalOpen(newValue);
|
||||||
<React.Fragment key={index}>
|
};
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
|
|
||||||
</React.Fragment>
|
/***************************************************************************
|
||||||
))
|
**
|
||||||
}
|
***************************************************************************/
|
||||||
</Box>);
|
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/>.
|
* 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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {Alert, Skeleton} from "@mui/material";
|
import {Alert, Skeleton} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Modal from "@mui/material/Modal";
|
||||||
import Tab from "@mui/material/Tab";
|
import Tab from "@mui/material/Tab";
|
||||||
import Tabs from "@mui/material/Tabs";
|
import Tabs from "@mui/material/Tabs";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
|
import EntityForm from "qqq/components/forms/EntityForm";
|
||||||
import MDTypography from "qqq/components/legacy/MDTypography";
|
import MDTypography from "qqq/components/legacy/MDTypography";
|
||||||
import TabPanel from "qqq/components/misc/TabPanel";
|
import TabPanel from "qqq/components/misc/TabPanel";
|
||||||
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
|
||||||
@ -43,11 +46,10 @@ import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidg
|
|||||||
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
|
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
|
||||||
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
|
||||||
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
|
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 ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
|
||||||
import StepperCard from "qqq/components/widgets/misc/StepperCard";
|
import StepperCard from "qqq/components/widgets/misc/StepperCard";
|
||||||
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
|
||||||
import WorkflowViewer from "qqq/components/widgets/misc/WorkflowViewer";
|
|
||||||
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
import ParentWidget from "qqq/components/widgets/ParentWidget";
|
||||||
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
|
||||||
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
|
||||||
@ -72,6 +74,9 @@ interface Props
|
|||||||
childUrlParams?: string;
|
childUrlParams?: string;
|
||||||
parentWidgetMetaData?: QWidgetMetaData;
|
parentWidgetMetaData?: QWidgetMetaData;
|
||||||
wrapWidgetsInTabPanels: boolean;
|
wrapWidgetsInTabPanels: boolean;
|
||||||
|
actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean;
|
||||||
|
initialWidgetDataList: any[];
|
||||||
|
values?: { [key: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
DashboardWidgets.defaultProps = {
|
DashboardWidgets.defaultProps = {
|
||||||
@ -83,11 +88,14 @@ DashboardWidgets.defaultProps = {
|
|||||||
childUrlParams: "",
|
childUrlParams: "",
|
||||||
parentWidgetMetaData: null,
|
parentWidgetMetaData: null,
|
||||||
wrapWidgetsInTabPanels: false,
|
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 [widgetCounter, setWidgetCounter] = useState(0);
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
|
|
||||||
@ -95,6 +103,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
|
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
|
||||||
const {accentColor} = useContext(QContext);
|
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 initialSelectedTab = 0;
|
||||||
let selectedTabKey: string = null;
|
let selectedTabKey: string = null;
|
||||||
if (parentWidgetMetaData && wrapWidgetsInTabPanels)
|
if (parentWidgetMetaData && wrapWidgetsInTabPanels)
|
||||||
@ -115,7 +129,15 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
|
|
||||||
useEffect(() =>
|
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([]);
|
setWidgetData([]);
|
||||||
|
|
||||||
for (let i = 0; i < widgetMetaDataList.length; i++)
|
for (let i = 0; i < widgetMetaDataList.length; i++)
|
||||||
{
|
{
|
||||||
const widgetMetaData = widgetMetaDataList[i];
|
const widgetMetaData = widgetMetaDataList[i];
|
||||||
@ -152,7 +174,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
|
|
||||||
const reloadWidget = async (index: number, data: string) =>
|
const reloadWidget = async (index: number, data: string) =>
|
||||||
{
|
{
|
||||||
(async () =>
|
await (async () =>
|
||||||
{
|
{
|
||||||
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
const urlParams = getQueryParams(widgetMetaDataList[index], data);
|
||||||
setCurrentUrlParams(urlParams);
|
setCurrentUrlParams(urlParams);
|
||||||
@ -271,6 +293,148 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
return (rs);
|
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 renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
|
||||||
{
|
{
|
||||||
const labelAdditionalComponentsRight: LabelComponent[] = [];
|
const labelAdditionalComponentsRight: LabelComponent[] = [];
|
||||||
@ -310,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
|
<Widget
|
||||||
omitPadding={true}
|
omitPadding={true}
|
||||||
widgetMetaData={widgetMetaData}
|
widgetMetaData={widgetMetaData}
|
||||||
@ -320,7 +484,16 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
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>
|
</Widget>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -502,9 +675,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "divider" && (
|
widgetMetaData.type === "divider" && (
|
||||||
<Box>
|
<DividerWidget />
|
||||||
<DividerWidget />
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@ -538,6 +709,12 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
widgetMetaData.type === "childRecordList" && (
|
widgetMetaData.type === "childRecordList" && (
|
||||||
widgetData && widgetData[i] &&
|
widgetData && widgetData[i] &&
|
||||||
<RecordGridWidget
|
<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}
|
widgetMetaData={widgetMetaData}
|
||||||
data={widgetData[i]}
|
data={widgetData[i]}
|
||||||
/>
|
/>
|
||||||
@ -564,7 +741,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
|
||||||
>
|
>
|
||||||
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} />
|
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} actionCallback={actionCallback} values={values} />
|
||||||
</Widget>
|
</Widget>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -582,14 +759,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
</Widget>
|
</Widget>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
widgetMetaData.type === "workflow" && (
|
|
||||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
|
||||||
<Widget widgetMetaData={widgetMetaData}>
|
|
||||||
<WorkflowViewer workflowId={widgetData[i].queryParams.id} />
|
|
||||||
</Widget>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
widgetMetaData.type === "dataBagViewer" && (
|
widgetMetaData.type === "dataBagViewer" && (
|
||||||
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
widgetData && widgetData[i] && widgetData[i].queryParams &&
|
||||||
@ -647,8 +816,28 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
|
|
||||||
if (!omitWrappingGridContainer)
|
if (!omitWrappingGridContainer)
|
||||||
{
|
{
|
||||||
// @ts-ignore
|
const gridProps: { [key: string]: any } = {};
|
||||||
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
|
|
||||||
|
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}
|
{renderedWidget}
|
||||||
</Grid>);
|
</Grid>);
|
||||||
}
|
}
|
||||||
@ -699,6 +888,22 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
|
|||||||
</Grid>
|
</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
|
) : null
|
||||||
);
|
);
|
||||||
|
@ -22,6 +22,9 @@
|
|||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {Alert, Skeleton} from "@mui/material";
|
import {Alert, Skeleton} from "@mui/material";
|
||||||
|
import 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 React from "react";
|
||||||
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
|
||||||
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
@ -32,19 +35,22 @@ import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRow
|
|||||||
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
|
||||||
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
|
||||||
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
|
||||||
|
import ImageBlock from "./blocks/ImageBlock";
|
||||||
|
|
||||||
|
|
||||||
interface WidgetBlockProps
|
interface WidgetBlockProps
|
||||||
{
|
{
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData;
|
||||||
block: BlockData;
|
block: BlockData;
|
||||||
|
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
|
||||||
|
values?: { [key: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Component to render a single Block in the widget framework!
|
** Component to render a single Block in the widget framework!
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element
|
export default function WidgetBlock({widgetMetaData, block, actionCallback, values}: WidgetBlockProps): JSX.Element
|
||||||
{
|
{
|
||||||
if(!block)
|
if(!block)
|
||||||
{
|
{
|
||||||
@ -64,7 +70,7 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
|||||||
if(block.blockTypeName == "COMPOSITE")
|
if(block.blockTypeName == "COMPOSITE")
|
||||||
{
|
{
|
||||||
// @ts-ignore - special case for composite type block...
|
// @ts-ignore - special case for composite type block...
|
||||||
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} />);
|
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} values={values} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(block.blockTypeName)
|
switch(block.blockTypeName)
|
||||||
@ -83,6 +89,14 @@ export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps):
|
|||||||
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||||
case "BIG_NUMBER":
|
case "BIG_NUMBER":
|
||||||
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
|
||||||
|
case "INPUT_FIELD":
|
||||||
|
return (<InputFieldBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
|
||||||
|
case "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:
|
default:
|
||||||
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
|
||||||
}
|
}
|
||||||
|
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 {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {Tooltip} from "@mui/material";
|
import {Box, Tooltip} from "@mui/material";
|
||||||
import React, {ReactElement, useContext} from "react";
|
|
||||||
import {Link} from "react-router-dom";
|
|
||||||
import QContext from "QContext";
|
import QContext from "QContext";
|
||||||
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
|
||||||
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
|
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
|
interface BlockElementWrapperProps
|
||||||
{
|
{
|
||||||
data: BlockData;
|
data: BlockData;
|
||||||
metaData: QWidgetMetaData;
|
metaData: QWidgetMetaData;
|
||||||
slot: string
|
slot: string;
|
||||||
linkProps?: any;
|
linkProps?: any;
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
}
|
}
|
||||||
@ -47,16 +48,16 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
|||||||
let link: BlockLink;
|
let link: BlockLink;
|
||||||
let tooltip: BlockTooltip;
|
let tooltip: BlockTooltip;
|
||||||
|
|
||||||
if(slot)
|
if (slot)
|
||||||
{
|
{
|
||||||
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
link = data.linkMap && data.linkMap[slot.toUpperCase()];
|
||||||
if(!link)
|
if (!link)
|
||||||
{
|
{
|
||||||
link = data.link;
|
link = data.link;
|
||||||
}
|
}
|
||||||
|
|
||||||
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
tooltip = data.tooltipMap && data.tooltipMap[slot.toUpperCase()];
|
||||||
if(!tooltip)
|
if (!tooltip)
|
||||||
{
|
{
|
||||||
tooltip = data.tooltip;
|
tooltip = data.tooltip;
|
||||||
}
|
}
|
||||||
@ -67,9 +68,9 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
|
|||||||
tooltip = data.tooltip;
|
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: //
|
// 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 key = data.blockId ? `${data.blockId},${slot}` : slot;
|
||||||
const showHelp = helpHelpActive || hasHelpContent(metaData?.helpContent?.get(key), helpRoles);
|
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}`} />;
|
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;
|
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
|
// @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);
|
return (rs);
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
|
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
|
||||||
|
|
||||||
|
|
||||||
export interface BlockData
|
export interface BlockData
|
||||||
@ -29,16 +30,19 @@ export interface BlockData
|
|||||||
|
|
||||||
tooltip?: BlockTooltip;
|
tooltip?: BlockTooltip;
|
||||||
link?: BlockLink;
|
link?: BlockLink;
|
||||||
tooltipMap?: {[slot: string]: BlockTooltip};
|
tooltipMap?: { [slot: string]: BlockTooltip };
|
||||||
linkMap?: {[slot: string]: BlockLink};
|
linkMap?: { [slot: string]: BlockLink };
|
||||||
|
|
||||||
values: any;
|
values: any;
|
||||||
styles?: any;
|
styles?: any;
|
||||||
|
|
||||||
|
conditional?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface BlockTooltip
|
export interface BlockTooltip
|
||||||
{
|
{
|
||||||
|
blockData?: CompositeData;
|
||||||
title: string | JSX.Element;
|
title: string | JSX.Element;
|
||||||
placement: string;
|
placement: string;
|
||||||
}
|
}
|
||||||
@ -55,5 +59,6 @@ export interface StandardBlockComponentProps
|
|||||||
{
|
{
|
||||||
widgetMetaData: QWidgetMetaData;
|
widgetMetaData: QWidgetMetaData;
|
||||||
data: BlockData;
|
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/>.
|
* 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 BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
|
||||||
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
|
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.
|
** 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
|
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 (
|
return (
|
||||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
|
||||||
<span style={{fontSize: "1.000rem"}}>{data.values.text}</span>
|
<Box display="inline-block" lineHeight="1.2" sx={boxStyle}>
|
||||||
|
<span style={{fontSize: 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>
|
</BlockElementWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ export default function UpOrDownNumberBlock({widgetMetaData, data}: StandardBloc
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline"}}>
|
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline", marginLeft: "auto"}}>
|
||||||
|
|
||||||
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
|
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
|
||||||
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">
|
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">
|
||||||
|
@ -50,6 +50,7 @@ import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
|
|||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
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-java";
|
||||||
import "ace-builds/src-noconflict/mode-javascript";
|
import "ace-builds/src-noconflict/mode-javascript";
|
||||||
import "ace-builds/src-noconflict/mode-json";
|
import "ace-builds/src-noconflict/mode-json";
|
||||||
|
@ -19,13 +19,16 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
|
|
||||||
|
|
||||||
function DividerWidget(): JSX.Element
|
function DividerWidget(): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
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 [modalOpen, setModalOpen] = useState(false);
|
||||||
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
|
||||||
|
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
|
||||||
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
|
||||||
|
|
||||||
const [alertContent, setAlertContent] = useState(null as string);
|
const [alertContent, setAlertContent] = useState(null as string);
|
||||||
@ -272,7 +273,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function mayShowQueryPreview(): boolean
|
function mayShowQuery(): boolean
|
||||||
{
|
{
|
||||||
if (tableMetaData)
|
if (tableMetaData)
|
||||||
{
|
{
|
||||||
@ -288,7 +289,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function mayShowColumnsPreview(): boolean
|
function mayShowColumns(): boolean
|
||||||
{
|
{
|
||||||
if (tableMetaData)
|
if (tableMetaData)
|
||||||
{
|
{
|
||||||
@ -356,14 +357,14 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
<Box pt="0.5rem">
|
<Box pt="0.5rem">
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||||
<h5>Query Filter</h5>
|
<h5>Query Filter</h5>
|
||||||
<Box fontSize="0.75rem" fontWeight="700">{mayShowQueryPreview() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{
|
{
|
||||||
mayShowQueryPreview() &&
|
mayShowQuery() &&
|
||||||
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
|
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!mayShowQueryPreview() &&
|
!mayShowQuery() &&
|
||||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}>
|
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.5rem"} p={"0.5rem"} pb={"0.125rem"} borderRadius="0.75rem" border={`1px solid ${colors.grayLines.main}`}>
|
||||||
{
|
{
|
||||||
isEditable &&
|
isEditable &&
|
||||||
@ -382,11 +383,11 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
<h5>Columns</h5>
|
<h5>Columns</h5>
|
||||||
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
<Box display="flex" flexWrap="wrap" fontSize="1rem">
|
||||||
{
|
{
|
||||||
mayShowColumnsPreview() &&
|
mayShowColumns() && columns &&
|
||||||
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
|
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!mayShowColumnsPreview() &&
|
!mayShowColumns() &&
|
||||||
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
|
||||||
{
|
{
|
||||||
isEditable &&
|
isEditable &&
|
||||||
@ -402,6 +403,21 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{!hidePreview && !isEditable && frontendQueryFilter && tableMetaData && (
|
||||||
|
<Box pt="1rem">
|
||||||
|
<h5>Preview</h5>
|
||||||
|
<RecordQuery
|
||||||
|
allowVariables={widgetData?.allowVariables}
|
||||||
|
ref={recordQueryRef}
|
||||||
|
table={tableMetaData}
|
||||||
|
isPreview={true}
|
||||||
|
usage="reportSetup"
|
||||||
|
isModal={true}
|
||||||
|
initialQueryFilter={frontendQueryFilter}
|
||||||
|
initialColumns={columns}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
modalOpen &&
|
modalOpen &&
|
||||||
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>
|
||||||
|
@ -39,15 +39,15 @@ import {Link, useNavigate} from "react-router-dom";
|
|||||||
|
|
||||||
export interface ChildRecordListData extends WidgetData
|
export interface ChildRecordListData extends WidgetData
|
||||||
{
|
{
|
||||||
title: string;
|
title?: string;
|
||||||
queryOutput: { records: { values: any }[] };
|
queryOutput?: { records: { values: any }[] };
|
||||||
childTableMetaData: QTableMetaData;
|
childTableMetaData?: QTableMetaData;
|
||||||
tablePath: string;
|
tablePath?: string;
|
||||||
viewAllLink: string;
|
viewAllLink?: string;
|
||||||
totalRows: number;
|
totalRows?: number;
|
||||||
canAddChildRecord: boolean;
|
canAddChildRecord?: boolean;
|
||||||
defaultValuesForNewChildRecords: { [fieldName: string]: any };
|
defaultValuesForNewChildRecords?: { [fieldName: string]: any };
|
||||||
disabledFieldsForNewChildRecords: { [fieldName: string]: any };
|
disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props
|
interface Props
|
||||||
@ -176,7 +176,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
setCsv(csv);
|
setCsv(csv);
|
||||||
setFileName(fileName);
|
setFileName(fileName);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [JSON.stringify(data?.queryOutput)]);
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
// view all link //
|
// view all link //
|
||||||
@ -304,7 +304,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
|
|||||||
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
|
||||||
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
|
||||||
>
|
>
|
||||||
<Box mx={-3} mb={-3}>
|
<Box>
|
||||||
<Box>
|
<Box>
|
||||||
<DataGridPro
|
<DataGridPro
|
||||||
autoHeight
|
autoHeight
|
||||||
|
@ -1,383 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {QException} from "@kingsrook/qqq-frontend-core/lib/exceptions/QException";
|
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
|
||||||
import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QCriteriaOperator";
|
|
||||||
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 {Chip} from "@mui/material";
|
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import Avatar from "@mui/material/Avatar";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Divider from "@mui/material/Divider";
|
|
||||||
import Grid from "@mui/material/Grid";
|
|
||||||
import List from "@mui/material/List";
|
|
||||||
import ListItem from "@mui/material/ListItem";
|
|
||||||
import ListItemAvatar from "@mui/material/ListItemAvatar";
|
|
||||||
import ListItemText from "@mui/material/ListItemText";
|
|
||||||
import Modal from "@mui/material/Modal";
|
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
|
||||||
import Tab from "@mui/material/Tab";
|
|
||||||
import Tabs from "@mui/material/Tabs";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import TabPanel from "qqq/components/misc/TabPanel";
|
|
||||||
import CustomWidthTooltip from "qqq/components/tooltips/CustomWidthTooltip";
|
|
||||||
import WorkflowEditor, {WorkflowEditorProps} from "qqq/components/workflows/WorkflowEditor";
|
|
||||||
import WorkflowPreview from "qqq/components/workflows/WorkflowPreview";
|
|
||||||
import {LoadingState} from "qqq/models/LoadingState";
|
|
||||||
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/mode-java";
|
|
||||||
import "ace-builds/src-noconflict/mode-javascript";
|
|
||||||
import "ace-builds/src-noconflict/mode-json";
|
|
||||||
import "ace-builds/src-noconflict/theme-github";
|
|
||||||
import React, {useReducer, useState} from "react";
|
|
||||||
import AceEditor from "react-ace";
|
|
||||||
import "ace-builds/src-noconflict/ext-language_tools";
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
|
||||||
|
|
||||||
// Declaring props types for ViewForm
|
|
||||||
interface Props
|
|
||||||
{
|
|
||||||
workflowId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkflowViewer.defaultProps =
|
|
||||||
{};
|
|
||||||
|
|
||||||
export default function WorkflowViewer({workflowId}: Props): JSX.Element
|
|
||||||
{
|
|
||||||
const [workflowRecord, setWorkflowRecord] = useState(null as QRecord);
|
|
||||||
const [asyncLoadInited, setAsyncLoadInited] = useState(false);
|
|
||||||
const [versionRecordList, setVersionRecordList] = useState(null as QRecord[]);
|
|
||||||
const [selectedRevisionRecord, setSelectedRevisionRecord] = useState(null as QRecord);
|
|
||||||
const [currentVersionId, setCurrentVersionId] = useState(null as number);
|
|
||||||
const [notFoundMessage, setNotFoundMessage] = useState(null);
|
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
|
||||||
const [editorProps, setEditorProps] = useState(null as WorkflowEditorProps);
|
|
||||||
const [successText, setSuccessText] = useState(null as string);
|
|
||||||
const [failText, setFailText] = useState(null as string);
|
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
||||||
|
|
||||||
const [loadingSelectedVersion, _] = useState(new LoadingState(forceUpdate, "loading"));
|
|
||||||
|
|
||||||
if (!asyncLoadInited)
|
|
||||||
{
|
|
||||||
setAsyncLoadInited(true);
|
|
||||||
|
|
||||||
(async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const workflowRecord = await qController.get("workflow", workflowId);
|
|
||||||
setWorkflowRecord(workflowRecord);
|
|
||||||
|
|
||||||
const criteria = [new QFilterCriteria("workflowId", QCriteriaOperator.EQUALS, [workflowId])];
|
|
||||||
const orderBys = [new QFilterOrderBy("sequenceNo", false)];
|
|
||||||
const filter = new QQueryFilter(criteria, orderBys, null, "AND", 0, 25);
|
|
||||||
const versions = await qController.query("workflowRevision", filter);
|
|
||||||
console.log("Fetched versions:");
|
|
||||||
console.log(versions);
|
|
||||||
setVersionRecordList(versions);
|
|
||||||
|
|
||||||
if (versions && versions.length > 0)
|
|
||||||
{
|
|
||||||
setCurrentVersionId(versions[0].values.get("id"));
|
|
||||||
const latestVersion = await qController.get("workflowRevision", versions[0].values.get("id"));
|
|
||||||
console.log("Fetched latestVersion:");
|
|
||||||
console.log(latestVersion);
|
|
||||||
setSelectedRevisionRecord(latestVersion);
|
|
||||||
loadingSelectedVersion.setNotLoading();
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
if (e instanceof QException)
|
|
||||||
{
|
|
||||||
if ((e as QException).status === 404)
|
|
||||||
{
|
|
||||||
setNotFoundMessage("Workflow data could not be found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setNotFoundMessage("Error loading workflow data: " + e);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
const editContents = (contents: string) =>
|
|
||||||
{
|
|
||||||
const editorProps = {} as WorkflowEditorProps;
|
|
||||||
editorProps.title = (contents ? "Editing Workflow: " : "Initializing Workflow: ") + workflowRecord?.values?.get("name");
|
|
||||||
editorProps.contents = contents;
|
|
||||||
editorProps.workflowId = workflowId;
|
|
||||||
setEditorProps(editorProps);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeEditingWorkflow = (event: object, reason: string, alert: string = null) =>
|
|
||||||
{
|
|
||||||
if (reason === "backdropClick" || reason === "escapeKeyDown")
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reason === "saved")
|
|
||||||
{
|
|
||||||
setAsyncLoadInited(false);
|
|
||||||
forceUpdate();
|
|
||||||
|
|
||||||
if (alert)
|
|
||||||
{
|
|
||||||
setSuccessText(alert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (reason === "failed")
|
|
||||||
{
|
|
||||||
setAsyncLoadInited(false);
|
|
||||||
forceUpdate();
|
|
||||||
|
|
||||||
if (alert)
|
|
||||||
{
|
|
||||||
setFailText(alert);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setEditorProps(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeTab = (newValue: number) =>
|
|
||||||
{
|
|
||||||
setSelectedTab(newValue);
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectVersion = (version: QRecord) =>
|
|
||||||
{
|
|
||||||
(async () =>
|
|
||||||
{
|
|
||||||
// fetch the full version
|
|
||||||
setSelectedRevisionRecord(version);
|
|
||||||
loadingSelectedVersion.setLoading();
|
|
||||||
|
|
||||||
const selectedVersion = await qController.get("workflowRevision", version.values.get("id"));
|
|
||||||
console.log("Fetched selectedVersion:");
|
|
||||||
console.log(selectedVersion);
|
|
||||||
setSelectedRevisionRecord(selectedVersion);
|
|
||||||
loadingSelectedVersion.setNotLoading();
|
|
||||||
forceUpdate();
|
|
||||||
})();
|
|
||||||
};
|
|
||||||
|
|
||||||
function getVersionsList(versionRecordList: QRecord[], selectedVersionRecord: QRecord)
|
|
||||||
{
|
|
||||||
return <List sx={{pl: 3, height: "400px", overflow: "auto"}}>
|
|
||||||
{
|
|
||||||
(versionRecordList == null || versionRecordList.length == 0) ?
|
|
||||||
<Typography variant="body2">
|
|
||||||
There are not any versions of this workflow.
|
|
||||||
</Typography>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
versionRecordList?.map((version: any) => (
|
|
||||||
<React.Fragment key={version.values.get("id")}>
|
|
||||||
<ListItem sx={{p: 1}} alignItems="flex-start" selected={selectedVersionRecord?.values?.get("id") == version.values.get("id")} onClick={(event) => selectVersion(version)}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar sx={{bgcolor: DeveloperModeUtils.revToColor("", workflowId, version.values.get("sequenceNo"))}}>{`${version.values.get("sequenceNo")}`}</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primaryTypographyProps={{fontSize: "1rem"}}
|
|
||||||
secondaryTypographyProps={{fontSize: ".85rem"}}
|
|
||||||
primary={
|
|
||||||
<div style={{overflow: "hidden", textOverflow: "ellipsis", maxHeight: "5rem"}} title={version.values.get("commitMessage")}>
|
|
||||||
{currentVersionId == version?.values?.get("id") && <Chip label="CURRENT" color="success" variant="outlined" size="small" sx={{mr: 1, fontSize: "0.75rem"}} />}
|
|
||||||
{version.values.get("commitMessage")}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
secondary={
|
|
||||||
<>
|
|
||||||
{ValueUtils.formatDateTime(version.values.get("createDate"))}
|
|
||||||
<br />
|
|
||||||
{version.values.get("author")}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
<Divider sx={{my: 0.5}} variant="inset" component="li" />
|
|
||||||
</React.Fragment>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</List>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let editButtonTooltip = "";
|
|
||||||
let editButtonText = "Create New Version";
|
|
||||||
if (currentVersionId)
|
|
||||||
{
|
|
||||||
if (currentVersionId === selectedRevisionRecord?.values?.get("id"))
|
|
||||||
{
|
|
||||||
editButtonTooltip = "If you make any changes to this workflow, a new version will be created when you hit Save.";
|
|
||||||
editButtonText = "Edit";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
editButtonTooltip = "If you want to make this previous Version active, bring up the Edit window, make any changes " +
|
|
||||||
"to the old Version if they are needed, then click Save. A new Version will be created, and set as Current.";
|
|
||||||
editButtonText = "Edit and Activate";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Grid container>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<Box>
|
|
||||||
{
|
|
||||||
<Box>
|
|
||||||
{
|
|
||||||
successText ? (
|
|
||||||
<Snackbar open={successText !== null && successText !== ""} autoHideDuration={6000} onClose={() => setSuccessText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
|
||||||
<Alert color="success" onClose={() => setSuccessText(null)}>
|
|
||||||
{successText}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
) : ("")
|
|
||||||
}
|
|
||||||
{
|
|
||||||
failText ? (
|
|
||||||
<Snackbar open={failText !== null && failText !== ""} autoHideDuration={6000} onClose={() => setFailText(null)} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
|
||||||
<Alert color="error" onClose={() => setFailText(null)}>
|
|
||||||
{failText}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
) : ("")
|
|
||||||
}
|
|
||||||
|
|
||||||
<Grid container spacing={3}>
|
|
||||||
<Grid item xs={12}>
|
|
||||||
<>
|
|
||||||
<Tabs
|
|
||||||
sx={{m: 0, mb: 1, mt: 0}}
|
|
||||||
value={selectedTab}
|
|
||||||
onChange={(event, newValue) => changeTab(newValue)}
|
|
||||||
variant="standard"
|
|
||||||
>
|
|
||||||
<Tab label="Versions" id="simple-tab-0" aria-controls="simple-tabpanel-0" />
|
|
||||||
<Tab label="Preview" id="simple-tab-1" aria-controls="simple-tabpanel-1" />
|
|
||||||
<Tab label="Something Else" id="simple-tab-2" aria-controls="simple-tabpanel-2" />
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<TabPanel index={0} value={selectedTab}>
|
|
||||||
<Grid container>
|
|
||||||
<Grid item xs={4}>
|
|
||||||
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
|
|
||||||
<Typography variant="h6" pl={3}>Versions</Typography>
|
|
||||||
</Box>
|
|
||||||
{getVersionsList(versionRecordList, selectedRevisionRecord)}
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Grid item xs={8}>
|
|
||||||
<Box display="flex" alignItems="center" justifyContent="space-between" gap={2} pb={1} height="40px">
|
|
||||||
{
|
|
||||||
selectedRevisionRecord ?
|
|
||||||
<Typography variant="h6">
|
|
||||||
Version {selectedRevisionRecord.values.get("sequenceNo")}
|
|
||||||
{
|
|
||||||
currentVersionId === selectedRevisionRecord.values.get("id")
|
|
||||||
? (<> (Current)</>)
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
</Typography>
|
|
||||||
: <></>
|
|
||||||
}
|
|
||||||
<CustomWidthTooltip title={editButtonTooltip}>
|
|
||||||
<Button sx={{py: 0}} onClick={() => editContents(selectedRevisionRecord?.values?.get("contents"))}>
|
|
||||||
{editButtonText}
|
|
||||||
</Button>
|
|
||||||
</CustomWidthTooltip>
|
|
||||||
</Box>
|
|
||||||
<WorkflowPreview />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel index={1} value={selectedTab}>
|
|
||||||
<Grid container height="440px">
|
|
||||||
<Grid item xs={4}>
|
|
||||||
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
|
|
||||||
<Typography variant="h6" pl={3}>Versions</Typography>
|
|
||||||
</Box>
|
|
||||||
{getVersionsList(versionRecordList, selectedRevisionRecord)}
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={8}>
|
|
||||||
<Box display="flex" alignItems="center" gap={2} pb={1} height="40px">
|
|
||||||
<Typography variant="h6" pl={3}>Data Preview (Version {selectedRevisionRecord?.values?.get("sequenceNo")})</Typography>
|
|
||||||
</Box>
|
|
||||||
<Box height="400px" overflow="auto" ml={1} fontSize="14px">
|
|
||||||
{
|
|
||||||
loadingSelectedVersion.isNotLoading() && selectedRevisionRecord && selectedRevisionRecord.values.get("contents") ? (
|
|
||||||
<>
|
|
||||||
<AceEditor
|
|
||||||
mode="json"
|
|
||||||
theme="github"
|
|
||||||
name={"viewData"}
|
|
||||||
readOnly
|
|
||||||
highlightActiveLine={false}
|
|
||||||
setOptions={{useWorker: false}}
|
|
||||||
editorProps={{$blockScrolling: true}}
|
|
||||||
width="100%"
|
|
||||||
height="400px"
|
|
||||||
value={selectedRevisionRecord?.values?.get("contents")}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
loadingSelectedVersion.isLoadingSlow() && selectedRevisionRecord && <Box fontSize="14px" pl={3}>Loading...</Box>
|
|
||||||
}
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</TabPanel>
|
|
||||||
</>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
{
|
|
||||||
editorProps &&
|
|
||||||
<Modal open={editorProps !== null} onClose={(event, reason) => closeEditingWorkflow(event, reason)}>
|
|
||||||
<WorkflowEditor
|
|
||||||
closeCallback={closeEditingWorkflow}
|
|
||||||
{...editorProps}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
}
|
|
||||||
</Box>
|
|
||||||
}
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
@ -19,15 +19,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
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 Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Icon from "@mui/material/Icon";
|
import Icon from "@mui/material/Icon";
|
||||||
import {styled} from "@mui/material/styles";
|
import {styled} from "@mui/material/styles";
|
||||||
import Table from "@mui/material/Table";
|
import Table from "@mui/material/Table";
|
||||||
import TableBody from "@mui/material/TableBody";
|
|
||||||
import TableContainer from "@mui/material/TableContainer";
|
import TableContainer from "@mui/material/TableContainer";
|
||||||
import TableRow from "@mui/material/TableRow";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import parse from "html-react-parser";
|
import parse from "html-react-parser";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
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... */}
|
{/* 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>
|
</span>
|
||||||
) : null,
|
) : null,
|
||||||
},
|
},
|
||||||
@ -312,7 +309,7 @@ function DataTable({
|
|||||||
{
|
{
|
||||||
boxStyle = isFooter
|
boxStyle = isFooter
|
||||||
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
|
? {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 = {};
|
let innerBoxStyle = {};
|
||||||
@ -321,143 +318,139 @@ function DataTable({
|
|||||||
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
|
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}>
|
return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
|
||||||
<Table {...getTableProps()}>
|
<Table {...getTableProps()} component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: gridTemplateColumns}}>
|
||||||
{
|
{
|
||||||
includeHead && (
|
includeHead && (
|
||||||
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
|
headerGroups.map((headerGroup: any, i: number) => (
|
||||||
{headerGroups.map((headerGroup: any, i: number) => (
|
headerGroup.headers.map((column: any) => (
|
||||||
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", alignItems: "flex-end", gridTemplateColumns: gridTemplateColumns}}>
|
column.type !== "hidden" && (
|
||||||
{headerGroup.headers.map((column: any) => (
|
<DataTableHeadCell
|
||||||
column.type !== "hidden" && (
|
sx={{position: "sticky", top: 0, background: "white", zIndex: 10, alignItems: "flex-end"}}
|
||||||
<DataTableHeadCell
|
key={i++}
|
||||||
key={i++}
|
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
||||||
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
|
align={column.align ? column.align : "left"}
|
||||||
align={column.align ? column.align : "left"}
|
sorted={setSortedValue(column)}
|
||||||
sorted={setSortedValue(column)}
|
tooltip={column.tooltip}
|
||||||
tooltip={column.tooltip}
|
>
|
||||||
>
|
{column.render("header")}
|
||||||
{column.render("header")}
|
</DataTableHeadCell>
|
||||||
</DataTableHeadCell>
|
)
|
||||||
)
|
))
|
||||||
))}
|
))
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
<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);
|
overrideNoEndBorder = true;
|
||||||
|
if (key + 1 < rows.length && rows[key + 1].depth == 0)
|
||||||
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;
|
overrideNoEndBorder = false;
|
||||||
if (key + 1 < rows.length && rows[key + 1].depth == 0)
|
|
||||||
{
|
|
||||||
overrideNoEndBorder = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
// don't do end-border on the footer //
|
// don't do end-border on the footer //
|
||||||
///////////////////////////////////////
|
///////////////////////////////////////
|
||||||
if (isFooter)
|
if (isFooter)
|
||||||
{
|
{
|
||||||
overrideNoEndBorder = true;
|
overrideNoEndBorder = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let background = "initial";
|
let background = "initial";
|
||||||
if (isFooter)
|
if (isFooter)
|
||||||
{
|
{
|
||||||
background = "#EEEEEE";
|
background = "#EEEEEE";
|
||||||
}
|
}
|
||||||
else if (row.depth > 0 || row.isExpanded)
|
else if (row.depth > 0 || row.isExpanded)
|
||||||
{
|
{
|
||||||
background = "#FAFAFA";
|
background = "#FAFAFA";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: background}} key={key} {...row.getRowProps()}>
|
row.cells.map((cell: any) => (
|
||||||
{row.cells.map((cell: any) => (
|
cell.column.type !== "hidden" && (
|
||||||
cell.column.type !== "hidden" && (
|
<DataTableBodyCell
|
||||||
<DataTableBodyCell
|
key={key}
|
||||||
key={key}
|
sx={{verticalAlign: "top", background: background}}
|
||||||
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
|
||||||
depth={row.depth}
|
depth={row.depth}
|
||||||
align={cell.column.align ? cell.column.align : "left"}
|
align={cell.column.align ? cell.column.align : "left"}
|
||||||
{...cell.getCellProps()}
|
{...cell.getCellProps()}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
cell.column.type === "default" && (
|
cell.column.type === "default" && (
|
||||||
cell.value && "number" === typeof cell.value ? (
|
cell.value && "number" === typeof cell.value ? (
|
||||||
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
<DefaultCell isFooter={isFooter}>{cell.value.toLocaleString()}</DefaultCell>
|
||||||
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
) : (<DefaultCell isFooter={isFooter}>{cell.render("Cell")}</DefaultCell>)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "htmlAndTooltip" && (
|
cell.column.type === "htmlAndTooltip" && (
|
||||||
<DefaultCell isFooter={isFooter}>
|
<DefaultCell isFooter={isFooter}>
|
||||||
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
<NoMaxWidthTooltip title={parse(row.values["tooltip"])}>
|
||||||
<Box>
|
<Box>
|
||||||
{parse(cell.value)}
|
{parse(cell.value)}
|
||||||
</Box>
|
</Box>
|
||||||
</NoMaxWidthTooltip>
|
</NoMaxWidthTooltip>
|
||||||
</DefaultCell>
|
</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "html" && (
|
cell.column.type === "html" && (
|
||||||
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
<DefaultCell isFooter={isFooter}>{parse(cell.value ?? "")}</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "composite" && (
|
cell.column.type === "composite" && (
|
||||||
<DefaultCell isFooter={isFooter}>
|
<DefaultCell isFooter={isFooter}>
|
||||||
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
<CompositeWidget widgetMetaData={widgetMetaData} data={cell.value} />
|
||||||
</DefaultCell>
|
</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "block" && (
|
cell.column.type === "block" && (
|
||||||
<DefaultCell isFooter={isFooter}>
|
<DefaultCell isFooter={isFooter}>
|
||||||
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
<WidgetBlock widgetMetaData={widgetMetaData} block={cell.value} />
|
||||||
</DefaultCell>
|
</DefaultCell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cell.column.type === "image" && row.values["imageTotal"] && (
|
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"]} />
|
<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"] && (
|
cell.column.type === "image" && !row.values["imageTotal"] && (
|
||||||
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
<ImageCell imageUrl={row.values["imageUrl"]} label={row.values["imageLabel"]} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
(cell.column.id === "__expander") && cell.render("cell")
|
(cell.column.id === "__expander") && cell.render("cell")
|
||||||
}
|
}
|
||||||
</DataTableBodyCell>
|
</DataTableBodyCell>
|
||||||
)
|
)
|
||||||
))}
|
))
|
||||||
</TableRow>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
|
||||||
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
</Table>
|
||||||
</Box></Box>;
|
</Box></Box>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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) ? (
|
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
|
||||||
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (
|
{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 ?
|
: noRowsFoundHTML ?
|
||||||
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
|
||||||
<MDTypography
|
<MDTypography variant="subtitle2" color="secondary" fontWeight="regular">
|
||||||
variant="subtitle2"
|
{noRowsFoundHTML ? (parse(noRowsFoundHTML)) : "No rows found"}
|
||||||
color="secondary"
|
|
||||||
fontWeight="regular"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
noRowsFoundHTML ? (
|
|
||||||
parse(noRowsFoundHTML)
|
|
||||||
) : "No rows found"
|
|
||||||
}
|
|
||||||
</MDTypography>
|
</MDTypography>
|
||||||
</Box>
|
</Box>
|
||||||
:
|
:
|
||||||
<TableContainer sx={{boxShadow: "none"}}>
|
<TableContainer sx={{boxShadow: "none"}}>
|
||||||
<Table>
|
<Table component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"}}>
|
||||||
<Box component="thead">
|
{Array(8).fill(0).map((_, i) =>
|
||||||
<TableRow sx={{alignItems: "flex-end"}} key="header">
|
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
|
||||||
{Array(8).fill(0).map((_, i) =>
|
<Skeleton width="100%" />
|
||||||
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
|
</DataTableHeadCell>
|
||||||
<Skeleton width="100%" />
|
)}
|
||||||
</DataTableHeadCell>
|
{Array(5).fill(0).map((_, i) =>
|
||||||
)}
|
Array(8).fill(0).map((_, j) =>
|
||||||
</TableRow>
|
<DataTableBodyCell key={`cell-${i}-${j}`} align="center">
|
||||||
</Box>
|
<DefaultCell isFooter={false}><Skeleton /></DefaultCell>
|
||||||
<TableBody>
|
</DataTableBodyCell>
|
||||||
{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>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Box from "@mui/material/Box";
|
import {Box} from "@mui/material";
|
||||||
import {Theme} from "@mui/material/styles";
|
import {Theme} from "@mui/material/styles";
|
||||||
import colors from "qqq/assets/theme/base/colors";
|
import colors from "qqq/assets/theme/base/colors";
|
||||||
import {ReactNode} from "react";
|
import {ReactNode} from "react";
|
||||||
@ -30,13 +30,14 @@ interface Props
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
align?: "left" | "right" | "center";
|
align?: "left" | "right" | "center";
|
||||||
|
sx?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="td"
|
component="div"
|
||||||
textAlign={align}
|
textAlign={align}
|
||||||
py={1.5}
|
py={1.5}
|
||||||
px={1.5}
|
px={1.5}
|
||||||
@ -54,7 +55,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
|||||||
},
|
},
|
||||||
"&:last-child": {
|
"&:last-child": {
|
||||||
paddingRight: "1rem"
|
paddingRight: "1rem"
|
||||||
}
|
}, ...sx
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
@ -72,6 +73,7 @@ function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
|
|||||||
DataTableBodyCell.defaultProps = {
|
DataTableBodyCell.defaultProps = {
|
||||||
noBorder: false,
|
noBorder: false,
|
||||||
align: "left",
|
align: "left",
|
||||||
|
sx: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DataTableBodyCell;
|
export default DataTableBodyCell;
|
||||||
|
@ -44,18 +44,14 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
component="th"
|
component="div"
|
||||||
width={width}
|
width={width}
|
||||||
py={1.5}
|
py={1.5}
|
||||||
px={1.5}
|
px={1.5}
|
||||||
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
|
||||||
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
|
||||||
"&:nth-of-type(1)": {
|
position: "sticky", top: 0, background: "white",
|
||||||
paddingLeft: "1rem"
|
zIndex: 1 // so if body rows scroll behind it, they don't show through
|
||||||
},
|
|
||||||
"&:last-child": {
|
|
||||||
paddingRight: "1rem"
|
|
||||||
},
|
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
import {ChangeEvent} from "react";
|
|
||||||
import {useRootEditor} from "sequential-workflow-designer-react";
|
|
||||||
import {WorkflowDefinition} from "./model";
|
|
||||||
|
|
||||||
export function RootEditor()
|
|
||||||
{
|
|
||||||
const {properties, setProperty, isReadonly} = useRootEditor<WorkflowDefinition>();
|
|
||||||
|
|
||||||
function onAlfaChanged(e: ChangeEvent)
|
|
||||||
{
|
|
||||||
setProperty("alfa", (e.target as HTMLInputElement).value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2>Optimization Workflow Editor</h2>
|
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.<br /><br />Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
import {ChangeEvent} from "react";
|
|
||||||
import {useStepEditor} from "sequential-workflow-designer-react";
|
|
||||||
import {SwitchStep, TaskStep, WarehouseOptimizationStep} from "./model";
|
|
||||||
|
|
||||||
export function StepEditor()
|
|
||||||
{
|
|
||||||
const {type, name, step, properties, isReadonly, setName, setProperty, notifyPropertiesChanged, notifyChildrenChanged} =
|
|
||||||
useStepEditor<TaskStep | SwitchStep | WarehouseOptimizationStep>();
|
|
||||||
|
|
||||||
function onNameChanged(e: ChangeEvent)
|
|
||||||
{
|
|
||||||
setName((e.target as HTMLInputElement).value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onXChanged(e: ChangeEvent)
|
|
||||||
{
|
|
||||||
setProperty("warehouse", (e.target as HTMLInputElement).value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onYChanged(e: ChangeEvent)
|
|
||||||
{
|
|
||||||
properties["wmsConnection"] = (e.target as HTMLInputElement).value;
|
|
||||||
notifyPropertiesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleExtraBranch()
|
|
||||||
{
|
|
||||||
const switchStep = step as SwitchStep;
|
|
||||||
if (switchStep.branches["extra"])
|
|
||||||
{
|
|
||||||
delete switchStep.branches["extra"];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
switchStep.branches["extra"] = [];
|
|
||||||
}
|
|
||||||
notifyChildrenChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h2>Step Editor</h2>
|
|
||||||
<h3>{type}</h3>
|
|
||||||
|
|
||||||
<h4>Pre-Script</h4>
|
|
||||||
<select>
|
|
||||||
<option>Pre Script #1</option>
|
|
||||||
<option>Pre Script #2</option>
|
|
||||||
<option>Pre Script #3</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<h4>Post-Script</h4>
|
|
||||||
<select>
|
|
||||||
<option>Post Script #1</option>
|
|
||||||
<option>Post Script #2</option>
|
|
||||||
<option>Post Script #3</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
{type === "switch" && (
|
|
||||||
<>
|
|
||||||
<h4>Extra branch</h4>
|
|
||||||
<button onClick={toggleExtraBranch} disabled={isReadonly}>
|
|
||||||
Toggle branch
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,214 +0,0 @@
|
|||||||
import {Branches, Uid} from "sequential-workflow-designer";
|
|
||||||
import {ContainerStep, OptimizationStepType, SwitchStep, TaskStep, WarehouseOptimizationStep} from "./model";
|
|
||||||
|
|
||||||
export function createTaskStep(): TaskStep
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
id: Uid.next(),
|
|
||||||
componentType: "task",
|
|
||||||
type: "task",
|
|
||||||
name: "blah",
|
|
||||||
properties: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////
|
|
||||||
// define all steps //
|
|
||||||
//////////////////////
|
|
||||||
export function createDetermineWarehouseRoutingStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Determine Warehouse", "determineWarehouseRouting");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineLineHaulLaneStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Determine Line Haul Lane", "determineLineHaulLane");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateLineItemsStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Validate Line Items", "validateLineItems");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineCoolingCategoryStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Determine Cooling Category", "determineCoolingCategory");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateOptimizationRulesStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Validate Optimization Rules", "validateOptimizationRules");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateAddressStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Validate Address", "validateAddress");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineCarrierServiceStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Determine Carrier Service", "determineCarrierService");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineTNTStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Determine TNT ", "determineTNT");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineOrderServiceDatesStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Determine Order Service Dates ", "determineOrderServiceDates");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOrderMatchesFilterSelectorStep(): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return createStep("Order Matches Filter Selector", "orderMatchesFilterSelector");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
////////////////////////
|
|
||||||
// define all outputs //
|
|
||||||
////////////////////////
|
|
||||||
export function createDetermineWarehouseRoutingOuptut(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {Edison: [], Patterson: [], Stockton: []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineLineHaulLaneOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {Chicago: [], Dallas: [], Sheboygan: []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateLineItemsOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {"Is Valid": [], "Not Valid": []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineCoolingCategoryOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {"Ambient": [], "Frozen": [], "Other": []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateOptimizationRulesOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {"Is Valid": [], "Not Valid": []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAddressValidationOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {"Is Valid": [], "Not Valid": []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineCarrierServiceOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {"Fedex Ground": [], "UPS Ground": [], "OnTrac Ground": []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineTNTOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {1: [], 2: [], 3: [], "4+": []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineOrderServiceDatesOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {Monday: [], Tuesday: [], Wednesday: []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOrderMatchesFilterSelectorOutput(): SwitchStep
|
|
||||||
{
|
|
||||||
return (createOutput("Output", {"Matches": [], "No Match": []}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////
|
|
||||||
// groups of steps + output //
|
|
||||||
//////////////////////////////
|
|
||||||
export function createDetermineWarehouseRoutingGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Determine Warehouse Routing", [createDetermineWarehouseRoutingStep(), createDetermineWarehouseRoutingOuptut()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineLineHaulLaneGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Determine Line Haul Lane", [createDetermineLineHaulLaneStep(), createDetermineLineHaulLaneOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateLineItemsGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Validate Line Items", [createValidateLineItemsStep(), createValidateLineItemsOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineCoolingCategoryGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Determine Cooling Category", [createDetermineCoolingCategoryStep(), createDetermineCoolingCategoryOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateOptimizationRulesGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Validate Optimization Rules", [createValidateOptimizationRulesStep(), createValidateOptimizationRulesOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValidateAddressGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Validate Address", [createValidateAddressStep(), createAddressValidationOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineCarrierServiceGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Determine Carrier Service", [createDetermineCarrierServiceStep(), createDetermineCarrierServiceOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineTNTGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Determine TNT", [createDetermineTNTStep(), createDetermineTNTOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createDetermineOrderServiceDatesGroup(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Determine Order Service Dates", [createDetermineOrderServiceDatesStep(), createDetermineOrderServiceDatesOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOrderMatchesFilterSelector(): ContainerStep
|
|
||||||
{
|
|
||||||
return (createGroup("Order Matches Filter Selector", [createOrderMatchesFilterSelectorStep(), createOrderMatchesFilterSelectorOutput()]));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
///////////
|
|
||||||
// utils //
|
|
||||||
///////////
|
|
||||||
export function createStep(name: string, type: OptimizationStepType): WarehouseOptimizationStep
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
id: Uid.next(),
|
|
||||||
componentType: "task",
|
|
||||||
type: type,
|
|
||||||
name: name,
|
|
||||||
properties: {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createOutput(name: string, branches: Branches): SwitchStep
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
id: Uid.next(),
|
|
||||||
componentType: "switch",
|
|
||||||
type: "switch",
|
|
||||||
name: name,
|
|
||||||
properties: {},
|
|
||||||
branches: branches
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createGroup(name: string, sequence: (WarehouseOptimizationStep | SwitchStep)[]): ContainerStep
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
id: Uid.next(),
|
|
||||||
componentType: "container",
|
|
||||||
type: "container",
|
|
||||||
name: name,
|
|
||||||
properties: {},
|
|
||||||
sequence: sequence
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,187 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
|
||||||
import {ToggleButtonGroup, Typography} from "@mui/material";
|
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Card from "@mui/material/Card";
|
|
||||||
import Grid from "@mui/material/Grid";
|
|
||||||
import Snackbar from "@mui/material/Snackbar";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import FormData from "form-data";
|
|
||||||
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
|
|
||||||
import WorkflowPreview from "qqq/components/workflows/WorkflowPreview";
|
|
||||||
import Client from "qqq/utils/qqq/Client";
|
|
||||||
import React, {useReducer, useState} from "react";
|
|
||||||
|
|
||||||
export interface WorkflowEditorProps
|
|
||||||
{
|
|
||||||
title: string;
|
|
||||||
workflowId: number;
|
|
||||||
contents: string;
|
|
||||||
closeCallback: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const qController = Client.getInstance();
|
|
||||||
|
|
||||||
function WorkflowEditor({title, workflowId, contents, closeCallback}: WorkflowEditorProps): JSX.Element
|
|
||||||
{
|
|
||||||
const [closing, setClosing] = useState(false);
|
|
||||||
const [updatedCode, setUpdatedCode] = useState(contents);
|
|
||||||
const [commitMessage, setCommitMessage] = useState("");
|
|
||||||
const [openTool, setOpenTool] = useState(null);
|
|
||||||
const [errorAlert, setErrorAlert] = useState("");
|
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
|
||||||
|
|
||||||
const changeOpenTool = (event: React.MouseEvent<HTMLElement>, newValue: string | null) =>
|
|
||||||
{
|
|
||||||
setOpenTool(newValue);
|
|
||||||
|
|
||||||
/////////////////////////////////////////////////
|
|
||||||
// need this to make Ace recognize new height. //
|
|
||||||
/////////////////////////////////////////////////
|
|
||||||
setTimeout(() =>
|
|
||||||
{
|
|
||||||
window.dispatchEvent(new Event("resize"));
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveClicked = () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
JSON.parse(updatedCode);
|
|
||||||
}
|
|
||||||
catch (e)
|
|
||||||
{
|
|
||||||
setErrorAlert("Cannot save Workflow Contents. Invalid json: " + e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setClosing(true);
|
|
||||||
|
|
||||||
(async () =>
|
|
||||||
{
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("workflowId", workflowId);
|
|
||||||
formData.append("contents", updatedCode);
|
|
||||||
formData.append("commitMessage", commitMessage);
|
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////
|
|
||||||
// we don't want this job to go async, so, pass a large timeout //
|
|
||||||
//////////////////////////////////////////////////////////////////
|
|
||||||
formData.append("_qStepTimeoutMillis", 60 * 1000);
|
|
||||||
|
|
||||||
const formDataHeaders = {
|
|
||||||
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
|
|
||||||
};
|
|
||||||
|
|
||||||
const processResult = await qController.processInit("storeWorkflowVersionProcess", formData, formDataHeaders);
|
|
||||||
if (processResult instanceof QJobError)
|
|
||||||
{
|
|
||||||
const jobError = processResult as QJobError;
|
|
||||||
closeCallback(null, "failed", jobError.userFacingError ?? jobError.error);
|
|
||||||
}
|
|
||||||
console.log("process result");
|
|
||||||
console.log(processResult);
|
|
||||||
|
|
||||||
closeCallback(null, "saved", "Saved New Workflow Version");
|
|
||||||
})();
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancelClicked = () =>
|
|
||||||
{
|
|
||||||
setClosing(true);
|
|
||||||
closeCallback(null, "cancelled");
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCode = (value: string, event: any) =>
|
|
||||||
{
|
|
||||||
console.log("Updating code");
|
|
||||||
setUpdatedCode(value);
|
|
||||||
forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
{
|
|
||||||
setCommitMessage(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
|
|
||||||
<Card sx={{height: "100%", p: 3}}>
|
|
||||||
|
|
||||||
<Snackbar open={errorAlert !== null && errorAlert !== ""} onClose={(event?: React.SyntheticEvent | Event, reason?: string) =>
|
|
||||||
{
|
|
||||||
if (reason === "clickaway")
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setErrorAlert("");
|
|
||||||
}} anchorOrigin={{vertical: "top", horizontal: "center"}}>
|
|
||||||
<Alert color="error" onClose={() => setErrorAlert("")}>
|
|
||||||
{errorAlert}
|
|
||||||
</Alert>
|
|
||||||
</Snackbar>
|
|
||||||
|
|
||||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
|
||||||
<Typography variant="h5" pb={1}>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Typography variant="body2" display="inline" pr={1}>
|
|
||||||
Tools:
|
|
||||||
</Typography>
|
|
||||||
<ToggleButtonGroup
|
|
||||||
value={openTool}
|
|
||||||
exclusive
|
|
||||||
onChange={changeOpenTool}
|
|
||||||
size="small"
|
|
||||||
sx={{pb: 1}}
|
|
||||||
>
|
|
||||||
</ToggleButtonGroup>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box sx={{height: openTool ? "45%" : "100%"}}>
|
|
||||||
<WorkflowPreview />
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box pt={1}>
|
|
||||||
<Grid container alignItems="flex-end">
|
|
||||||
<Box width="50%">
|
|
||||||
<TextField id="commitMessage" label="Commit Message" variant="standard" fullWidth value={commitMessage} onChange={updateCommitMessage} />
|
|
||||||
</Box>
|
|
||||||
<Grid container justifyContent="flex-end" spacing={3}>
|
|
||||||
<QCancelButton disabled={closing} onClickHandler={cancelClicked} />
|
|
||||||
<QSaveButton disabled={closing} onClickHandler={saveClicked} />
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</Box>
|
|
||||||
</Card>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WorkflowEditor;
|
|
@ -1,199 +0,0 @@
|
|||||||
import {useEffect, useMemo, useState} from "react";
|
|
||||||
import {ObjectCloner, Step, StepsConfiguration, ToolboxConfiguration, ValidatorConfiguration} from "sequential-workflow-designer";
|
|
||||||
import {SequentialWorkflowDesigner, useSequentialWorkflowDesignerController, wrapDefinition} from "sequential-workflow-designer-react";
|
|
||||||
import {WorkflowDefinition} from "./model";
|
|
||||||
import {RootEditor} from "./RootEditor";
|
|
||||||
import {StepEditor} from "./StepEditor";
|
|
||||||
import {createDetermineCarrierServiceGroup, createDetermineCoolingCategoryGroup, createDetermineLineHaulLaneGroup, createDetermineOrderServiceDatesGroup, createDetermineTNTGroup, createDetermineWarehouseRoutingGroup, createOrderMatchesFilterSelector, createTaskStep, createValidateAddressGroup, createValidateLineItemsGroup, createValidateOptimizationRulesGroup} from "./StepUtils";
|
|
||||||
|
|
||||||
const startDefinition: WorkflowDefinition = {
|
|
||||||
properties: {
|
|
||||||
alfa: "bravo"
|
|
||||||
},
|
|
||||||
sequence: []
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function WorkflowPreview()
|
|
||||||
{
|
|
||||||
const controller = useSequentialWorkflowDesignerController();
|
|
||||||
const toolboxConfiguration: ToolboxConfiguration = useMemo(
|
|
||||||
() => ({
|
|
||||||
groups: [{
|
|
||||||
name: "Optimization Steps", steps: [
|
|
||||||
createDetermineCarrierServiceGroup(),
|
|
||||||
createDetermineCoolingCategoryGroup(),
|
|
||||||
createDetermineLineHaulLaneGroup(),
|
|
||||||
createDetermineOrderServiceDatesGroup(),
|
|
||||||
createDetermineTNTGroup(),
|
|
||||||
createDetermineWarehouseRoutingGroup()
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Validators", steps: [
|
|
||||||
createValidateAddressGroup(),
|
|
||||||
createValidateLineItemsGroup(),
|
|
||||||
createValidateOptimizationRulesGroup()
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Utilities", steps: [
|
|
||||||
createOrderMatchesFilterSelector()
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const stepsConfiguration: StepsConfiguration = useMemo(
|
|
||||||
() => ({
|
|
||||||
iconUrlProvider: () => null
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const validatorConfiguration: ValidatorConfiguration = useMemo(
|
|
||||||
() => ({
|
|
||||||
step: (step: Step) => Boolean(step.name),
|
|
||||||
root: (definition: WorkflowDefinition) => Boolean(definition.properties.alfa)
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isVisible, setIsVisible] = useState(true);
|
|
||||||
const [isToolboxCollapsed, setIsToolboxCollapsed] = useState(false);
|
|
||||||
const [isEditorCollapsed, setIsEditorCollapsed] = useState(false);
|
|
||||||
const [definition, setDefinition] = useState(() => wrapDefinition(startDefinition));
|
|
||||||
const [selectedStepId, setSelectedStepId] = useState<string | null>(null);
|
|
||||||
const [isReadonly, setIsReadonly] = useState(false);
|
|
||||||
const [moveViewportToStep, setMoveViewportToStep] = useState<string | null>(null);
|
|
||||||
const definitionJson = JSON.stringify(definition.value, null, 2);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
console.log(`definition updated, isValid=${definition.isValid}`);
|
|
||||||
}, [definition]);
|
|
||||||
|
|
||||||
useEffect(() =>
|
|
||||||
{
|
|
||||||
if (moveViewportToStep)
|
|
||||||
{
|
|
||||||
if (controller.isReady())
|
|
||||||
{
|
|
||||||
controller.moveViewportToStep(moveViewportToStep);
|
|
||||||
}
|
|
||||||
setMoveViewportToStep(null);
|
|
||||||
}
|
|
||||||
}, [controller, moveViewportToStep]);
|
|
||||||
|
|
||||||
function toggleVisibilityClicked()
|
|
||||||
{
|
|
||||||
setIsVisible(!isVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSelectionClicked()
|
|
||||||
{
|
|
||||||
const id = definition.value.sequence[0].id;
|
|
||||||
setSelectedStepId(selectedStepId ? null : id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleIsReadonlyClicked()
|
|
||||||
{
|
|
||||||
setIsReadonly(!isReadonly);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleToolboxClicked()
|
|
||||||
{
|
|
||||||
setIsToolboxCollapsed(!isToolboxCollapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleEditorClicked()
|
|
||||||
{
|
|
||||||
setIsEditorCollapsed(!isEditorCollapsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveViewportToFirstStepClicked()
|
|
||||||
{
|
|
||||||
const fistStep = definition.value.sequence[0];
|
|
||||||
if (fistStep)
|
|
||||||
{
|
|
||||||
setMoveViewportToStep(fistStep.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function appendStepClicked()
|
|
||||||
{
|
|
||||||
const newStep = createTaskStep();
|
|
||||||
|
|
||||||
const newDefinition = ObjectCloner.deepClone(definition.value);
|
|
||||||
newDefinition.sequence.push(newStep);
|
|
||||||
// We need to wait for the controller to finish the operation before we can select the new step
|
|
||||||
await controller.replaceDefinition(newDefinition);
|
|
||||||
|
|
||||||
setSelectedStepId(newStep.id);
|
|
||||||
setMoveViewportToStep(newStep.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reloadDefinitionClicked()
|
|
||||||
{
|
|
||||||
const newDefinition = ObjectCloner.deepClone(startDefinition);
|
|
||||||
setSelectedStepId(null);
|
|
||||||
setDefinition(wrapDefinition(newDefinition));
|
|
||||||
}
|
|
||||||
|
|
||||||
function yesOrNo(value: boolean)
|
|
||||||
{
|
|
||||||
return value ? "✅ Yes" : "⛔ No";
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isVisible && (
|
|
||||||
<SequentialWorkflowDesigner
|
|
||||||
undoStackSize={10}
|
|
||||||
definition={definition}
|
|
||||||
onDefinitionChange={setDefinition}
|
|
||||||
selectedStepId={selectedStepId}
|
|
||||||
isReadonly={isReadonly}
|
|
||||||
onSelectedStepIdChanged={setSelectedStepId}
|
|
||||||
toolboxConfiguration={toolboxConfiguration}
|
|
||||||
isToolboxCollapsed={isToolboxCollapsed}
|
|
||||||
onIsToolboxCollapsedChanged={setIsToolboxCollapsed}
|
|
||||||
stepsConfiguration={stepsConfiguration}
|
|
||||||
validatorConfiguration={validatorConfiguration}
|
|
||||||
controlBar={true}
|
|
||||||
rootEditor={<RootEditor />}
|
|
||||||
stepEditor={<StepEditor />}
|
|
||||||
isEditorCollapsed={isEditorCollapsed}
|
|
||||||
onIsEditorCollapsedChanged={setIsEditorCollapsed}
|
|
||||||
controller={controller}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Definition: {definitionJson.length} bytes</li>
|
|
||||||
<li>Selected step: {selectedStepId}</li>
|
|
||||||
<li>Is readonly: {yesOrNo(isReadonly)}</li>
|
|
||||||
<li>Is valid: {definition.isValid === undefined ? "?" : yesOrNo(definition.isValid)}</li>
|
|
||||||
<li>Is toolbox collapsed: {yesOrNo(isToolboxCollapsed)}</li>
|
|
||||||
<li>Is editor collapsed: {yesOrNo(isEditorCollapsed)}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button onClick={toggleVisibilityClicked}>Toggle visibility</button>
|
|
||||||
<button onClick={reloadDefinitionClicked}>Reload definition</button>
|
|
||||||
<button onClick={toggleSelectionClicked}>Toggle selection</button>
|
|
||||||
<button onClick={toggleIsReadonlyClicked}>Toggle readonly</button>
|
|
||||||
<button onClick={toggleToolboxClicked}>Toggle toolbox</button>
|
|
||||||
<button onClick={toggleEditorClicked}>Toggle editor</button>
|
|
||||||
<button onClick={moveViewportToFirstStepClicked}>Move viewport to first step</button>
|
|
||||||
<button onClick={appendStepClicked}>Append step</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<textarea value={definitionJson} readOnly={true} cols={100} rows={15} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WorkflowPreview;
|
|
@ -1,75 +0,0 @@
|
|||||||
import {BranchedStep, Definition, Step} from "sequential-workflow-designer";
|
|
||||||
|
|
||||||
export interface WorkflowDefinition extends Definition
|
|
||||||
{
|
|
||||||
properties: {
|
|
||||||
alfa?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TaskStep extends Step
|
|
||||||
{
|
|
||||||
componentType: "task";
|
|
||||||
type: "task";
|
|
||||||
properties: {
|
|
||||||
x?: string;
|
|
||||||
y?: string;
|
|
||||||
warehouse?: string;
|
|
||||||
wmsConnection?: string;
|
|
||||||
wmsSystem?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OptimizationStepType =
|
|
||||||
"determineWarehouseRouting" |
|
|
||||||
"determineLineHaulLane" |
|
|
||||||
"validateLineItems" |
|
|
||||||
"determineCoolingCategory" |
|
|
||||||
"validateOptimizationRules" |
|
|
||||||
"validateAddress" |
|
|
||||||
"determineCarrierService" |
|
|
||||||
"determineTNT" |
|
|
||||||
"determineOrderServiceDates" |
|
|
||||||
"orderMatchesFilterSelector";
|
|
||||||
|
|
||||||
|
|
||||||
export interface WarehouseOptimizationStep extends Step
|
|
||||||
{
|
|
||||||
componentType: "task";
|
|
||||||
type: OptimizationStepType;
|
|
||||||
properties: {
|
|
||||||
x?: string;
|
|
||||||
y?: string;
|
|
||||||
warehouse?: string;
|
|
||||||
wmsConnection?: string;
|
|
||||||
wmsSystem?: string;
|
|
||||||
isValid?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SwitchStep extends BranchedStep
|
|
||||||
{
|
|
||||||
componentType: "switch";
|
|
||||||
type: "switch";
|
|
||||||
properties: {
|
|
||||||
x?: string;
|
|
||||||
y?: string;
|
|
||||||
warehouse?: string;
|
|
||||||
wmsConnection?: string;
|
|
||||||
wmsSystem?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContainerStep extends Step
|
|
||||||
{
|
|
||||||
componentType: "container";
|
|
||||||
type: "container";
|
|
||||||
properties: {
|
|
||||||
x?: string;
|
|
||||||
y?: string;
|
|
||||||
warehouse?: string;
|
|
||||||
wmsConnection?: string;
|
|
||||||
wmsSystem?: string;
|
|
||||||
};
|
|
||||||
sequence: (WarehouseOptimizationStep | SwitchStep)[];
|
|
||||||
}
|
|
38
src/qqq/models/fields/FieldPossibleValueProps.ts
Normal file
38
src/qqq/models/fields/FieldPossibleValueProps.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* QQQ - Low-code Application Framework for Engineers.
|
||||||
|
* Copyright (C) 2021-2024. Kingsrook, LLC
|
||||||
|
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||||
|
* contact@kingsrook.com
|
||||||
|
* https://github.com/Kingsrook/
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Properties attached to a (formik?) form field, to denote how it behaves as
|
||||||
|
** as related to a possible value source.
|
||||||
|
*******************************************************************************/
|
||||||
|
export interface FieldPossibleValueProps
|
||||||
|
{
|
||||||
|
isPossibleValue?: boolean;
|
||||||
|
possibleValues?: QPossibleValue[];
|
||||||
|
initialDisplayValue: string | null;
|
||||||
|
fieldName?: string;
|
||||||
|
tableName?: string;
|
||||||
|
processName?: string;
|
||||||
|
possibleValueSourceName?: string;
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
|
|||||||
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
|
||||||
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
|
||||||
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
|
||||||
|
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
|
||||||
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
|
||||||
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
|
||||||
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
|
||||||
@ -36,12 +37,14 @@ import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJob
|
|||||||
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
|
||||||
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Grid from "@mui/material/Grid";
|
import Grid from "@mui/material/Grid";
|
||||||
import Step from "@mui/material/Step";
|
import Step from "@mui/material/Step";
|
||||||
import StepLabel from "@mui/material/StepLabel";
|
import StepLabel from "@mui/material/StepLabel";
|
||||||
import Stepper from "@mui/material/Stepper";
|
import Stepper from "@mui/material/Stepper";
|
||||||
|
import {Theme} from "@mui/material/styles";
|
||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
|
||||||
import FormData from "form-data";
|
import FormData from "form-data";
|
||||||
@ -60,8 +63,12 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar";
|
|||||||
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
|
||||||
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
|
||||||
import ValidationReview from "qqq/components/processes/ValidationReview";
|
import ValidationReview from "qqq/components/processes/ValidationReview";
|
||||||
|
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
|
||||||
|
import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget";
|
||||||
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
|
||||||
|
import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
|
||||||
import BaseLayout from "qqq/layouts/BaseLayout";
|
import BaseLayout from "qqq/layouts/BaseLayout";
|
||||||
|
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
|
||||||
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import TableUtils from "qqq/utils/qqq/TableUtils";
|
import TableUtils from "qqq/utils/qqq/TableUtils";
|
||||||
@ -89,14 +96,18 @@ const INITIAL_RETRY_MILLIS = 1_500;
|
|||||||
const RETRY_MAX_MILLIS = 12_000;
|
const RETRY_MAX_MILLIS = 12_000;
|
||||||
const BACKOFF_AMOUNT = 1.5;
|
const BACKOFF_AMOUNT = 1.5;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// define a function that we can make referenes to, which we'll overwrite //
|
// define some functions that we can make reference to, which we'll overwrite //
|
||||||
// with formik's setFieldValue function, once we're inside formik. //
|
// with functions from formik, once we're inside formik. //
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let formikSetTouched = ({}: any, touched: boolean): void =>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
|
||||||
|
|
||||||
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
|
||||||
@ -120,6 +131,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
const [activeStepIndex, setActiveStepIndex] = useState(0);
|
||||||
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
|
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
|
||||||
const [newStep, setNewStep] = useState(null);
|
const [newStep, setNewStep] = useState(null);
|
||||||
|
const [stepInstanceCounter, setStepInstanceCounter] = useState(0);
|
||||||
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
|
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
|
||||||
const [needInitialLoad, setNeedInitialLoad] = useState(true);
|
const [needInitialLoad, setNeedInitialLoad] = useState(true);
|
||||||
const [lastForcedReInit, setLastForcedReInit] = useState(null as number);
|
const [lastForcedReInit, setLastForcedReInit] = useState(null as number);
|
||||||
@ -136,8 +148,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
);
|
);
|
||||||
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
const [showErrorDetail, setShowErrorDetail] = useState(false);
|
||||||
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
const [showFullHelpText, setShowFullHelpText] = useState(false);
|
||||||
|
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
|
||||||
|
|
||||||
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
|
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
|
||||||
|
const [controlCallbacks, setControlCallbacks] = useState({} as { [name: string]: () => void });
|
||||||
|
|
||||||
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
|
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 [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
|
||||||
|
|
||||||
const onLastStep = activeStepIndex === steps.length - 2;
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
const noMoreSteps = activeStepIndex === steps.length - 1;
|
// determine if we're on the last-step or not (e.g., to decide "Submit" vs "Next") //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
let onLastStep = false;
|
||||||
|
if (processMetaData?.stepFlow == "LINEAR" && activeStepIndex === steps.length - 2)
|
||||||
|
{
|
||||||
|
onLastStep = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////
|
||||||
|
// determine if any 'next' button appears //
|
||||||
|
////////////////////////////////////////////
|
||||||
|
let noMoreSteps = false;
|
||||||
|
if (processMetaData?.stepFlow == "LINEAR" && activeStepIndex === steps.length - 1)
|
||||||
|
{
|
||||||
|
noMoreSteps = true;
|
||||||
|
}
|
||||||
|
if (processValues["noMoreSteps"])
|
||||||
|
{
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
// this, to allow a non-linear process to request this behavior //
|
||||||
|
//////////////////////////////////////////////////////////////////
|
||||||
|
noMoreSteps = true;
|
||||||
|
}
|
||||||
|
|
||||||
////////////////
|
////////////////
|
||||||
// form state //
|
// form state //
|
||||||
@ -175,7 +211,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const [recordConfig, setRecordConfig] = useState({} as any);
|
const [recordConfig, setRecordConfig] = useState({} as any);
|
||||||
const [pageNumber, setPageNumber] = useState(0);
|
const [pageNumber, setPageNumber] = useState(0);
|
||||||
const [rowsPerPage, setRowsPerPage] = useState(10);
|
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 //
|
// state for bulk edit form //
|
||||||
@ -294,36 +331,205 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
function renderWidget(widgetName: string)
|
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])
|
if (!renderedWidgets[activeStep.name])
|
||||||
{
|
{
|
||||||
renderedWidgets[activeStep.name] = {};
|
renderedWidgets[activeStep.name] = {};
|
||||||
setRenderedWidgets(renderedWidgets);
|
setRenderedWidgets(renderedWidgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderedWidgets[activeStep.name][widgetName])
|
let isChildRecordWidget = widgetMetaData.type == "childRecordList";
|
||||||
|
if (!isChildRecordWidget && renderedWidgets[activeStep.name][widgetName])
|
||||||
{
|
{
|
||||||
return 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[] = [];
|
const queryStringParts: string[] = [];
|
||||||
for (let name in processValues)
|
for (let name in processValues)
|
||||||
{
|
{
|
||||||
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`);
|
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}>
|
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>);
|
</Box>);
|
||||||
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
|
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
|
||||||
return 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 //
|
// 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)
|
if (qJobRunning || step === null)
|
||||||
{
|
{
|
||||||
return (
|
return (
|
||||||
<Grid m={3} mt={9} container>
|
<Grid m={3} mt={9} container maxWidth="calc(100% - 3rem)">
|
||||||
<Grid item xs={0} lg={3} />
|
<Grid item xs={0} lg={3} />
|
||||||
<Grid item xs={12} lg={6}>
|
<Grid item xs={12} lg={6}>
|
||||||
<Card>
|
<Card>
|
||||||
@ -477,6 +683,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
|
||||||
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
|
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
|
||||||
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
|
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
|
||||||
|
const isFormatScanner = step?.format?.toLowerCase() == "scanner";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -485,7 +692,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
// hide label on widgets - the Widget component itself provides the label //
|
// 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) //
|
// 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">
|
<MDTypography variant={isWidget ? "h6" : "h5"} component="div" fontWeight="bold">
|
||||||
{(isModal) ? `${overrideLabel ?? process.label}: ` : ""}
|
{(isModal) ? `${overrideLabel ?? process.label}: ` : ""}
|
||||||
{step?.label}
|
{step?.label}
|
||||||
@ -763,8 +970,29 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
component.type === QComponentType.WIDGET && (
|
component.type === QComponentType.WIDGET && (
|
||||||
component.values?.widgetName &&
|
<>
|
||||||
renderWidget(component.values?.widgetName)
|
{
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
// if a widget name is given, render that widget //
|
||||||
|
///////////////////////////////////////////////////
|
||||||
|
component.values?.widgetName &&
|
||||||
|
renderWidget(component.values?.widgetName)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
// if the widget is marked as adHoc, render it as such //
|
||||||
|
/////////////////////////////////////////////////////////
|
||||||
|
component.values?.isAdHocWidget &&
|
||||||
|
renderAdHocWidget(component.values, index)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
// if neither of those, then programmer error //
|
||||||
|
////////////////////////////////////////////////
|
||||||
|
!(component.values?.widgetName || component.values?.isAdHocWidget) &&
|
||||||
|
<Alert severity="error">Error: Component is marked as WIDGET type, but does not specify a <u>widgetName</u>, nor the <u>isAdHocWidget</u> flag.</Alert>
|
||||||
|
}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -864,6 +1092,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
setActiveStepIndex(newIndex);
|
setActiveStepIndex(newIndex);
|
||||||
setOverrideOnLastStep(null);
|
setOverrideOnLastStep(null);
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// reset formik touched data, so a field that's repeated doesn't immediately show a 'dirty' state //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
formikSetTouched({}, false);
|
||||||
|
|
||||||
if (steps)
|
if (steps)
|
||||||
{
|
{
|
||||||
const activeStep = steps[newIndex];
|
const activeStep = steps[newIndex];
|
||||||
@ -899,7 +1132,17 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
|
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
|
||||||
{
|
{
|
||||||
addField("doFullValidation", {type: "radio"}, "true", null);
|
addField("doFullValidation", {type: "radio"}, "true", null);
|
||||||
setOverrideOnLastStep(false);
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// so - if we're on the validation screen, and we don't have a validationSummary right now, //
|
||||||
|
// and the process supports doing full validation - then the user will choose, via radio, //
|
||||||
|
// if this is the last step or not - and by default that radio will be true, to make this //
|
||||||
|
// NOT the last step - so set this value. //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if (!processValues["validationSummary"] && processValues["supportsFullValidation"])
|
||||||
|
{
|
||||||
|
setOverrideOnLastStep(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doesStepHaveComponent(activeStep, QComponentType.GOOGLE_DRIVE_SELECT_FOLDER))
|
if (doesStepHaveComponent(activeStep, QComponentType.GOOGLE_DRIVE_SELECT_FOLDER))
|
||||||
@ -909,6 +1152,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
|
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (doesStepHaveComponent(activeStep, QComponentType.WIDGET))
|
||||||
|
{
|
||||||
|
ProcessWidgetBlockUtils.addFieldsForCompositeWidget(activeStep, 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 //
|
// if this step has form fields, set up the form //
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
@ -994,7 +1247,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
setValidationFunction(() => true);
|
setValidationFunction(() => true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [newStep]);
|
}, [newStep, stepInstanceCounter]); // maybe we could just use stepInstanceCounter...
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// if there are records to load: build a record config, and set the needRecords state flag //
|
// if there are records to load: build a record config, and set the needRecords state flag //
|
||||||
@ -1067,6 +1320,11 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
const {records} = response;
|
const {records} = response;
|
||||||
setRecords(records);
|
setRecords(records);
|
||||||
|
|
||||||
|
if (!childRecordData || childRecordData.length == 0)
|
||||||
|
{
|
||||||
|
setChildRecordData(convertRecordsToChildRecordData(records));
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// re-construct the recordConfig object, so the setState call triggers a new rendering //
|
// 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]);
|
}, [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 //
|
// 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) //
|
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
let frontendSteps = steps;
|
let frontendSteps = steps;
|
||||||
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
|
const updatedFrontendStepList = qJobComplete.processMetaDataAdjustment?.updatedFrontendStepList;
|
||||||
if (updatedFrontendStepList)
|
if (updatedFrontendStepList)
|
||||||
{
|
{
|
||||||
setSteps(updatedFrontendStepList);
|
|
||||||
frontendSteps = updatedFrontendStepList;
|
frontendSteps = updatedFrontendStepList;
|
||||||
|
setSteps(frontendSteps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// always merge the new updatedFields map (if there is one) with existing updates and existing fields //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
updateFieldsInProcess(frontendSteps, qJobComplete.processMetaDataAdjustment?.updatedFields);
|
||||||
|
setSteps(frontendSteps);
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
// if the next screen has any PVS fields - look up their labels (display values) //
|
// if the next screen has any PVS fields - look up their labels (display values) //
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -1159,7 +1488,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
setJobUUID(null);
|
setJobUUID(null);
|
||||||
setNewStep(nextStepName);
|
setNewStep(nextStepName);
|
||||||
|
setStepInstanceCounter(1 + stepInstanceCounter);
|
||||||
setProcessValues(newValues);
|
setProcessValues(newValues);
|
||||||
|
setRenderedWidgets({});
|
||||||
setQJobRunning(null);
|
setQJobRunning(null);
|
||||||
|
|
||||||
if (formikSetFieldValueFunction)
|
if (formikSetFieldValueFunction)
|
||||||
@ -1413,10 +1744,35 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
setNewStep(activeStepIndex - 1);
|
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. //
|
// 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);
|
setFormError(null);
|
||||||
|
|
||||||
@ -1455,28 +1811,20 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
formData.append("bulkEditEnabledFields", bulkEditEnabledFields.join(","));
|
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({});
|
setProcessValues({});
|
||||||
setRecords([]);
|
setRecords([]);
|
||||||
setOverrideOnLastStep(null);
|
setOverrideOnLastStep(null);
|
||||||
setLastProcessResponse(new QJobRunning({message: "Working..."}));
|
setLastProcessResponse(new QJobRunning({message: "Working..."}));
|
||||||
|
|
||||||
setTimeout(async () =>
|
doSubmit(formData);
|
||||||
{
|
|
||||||
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
|
|
||||||
|
|
||||||
const processResponse = await Client.getInstance().processStep(
|
|
||||||
processName,
|
|
||||||
processUUID,
|
|
||||||
activeStep.name,
|
|
||||||
formData,
|
|
||||||
formDataHeaders,
|
|
||||||
);
|
|
||||||
setLastProcessResponse(processResponse);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -1506,27 +1854,54 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const mainCardStyles: any = {};
|
|
||||||
const formStyles: any = {};
|
const formStyles: any = {};
|
||||||
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
|
||||||
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
|
|
||||||
{
|
|
||||||
mainCardStyles.background = "#FFFFFF";
|
|
||||||
mainCardStyles.boxShadow = "none";
|
|
||||||
}
|
|
||||||
if (isWidget)
|
if (isWidget)
|
||||||
{
|
{
|
||||||
mainCardStyles.background = "none";
|
|
||||||
mainCardStyles.boxShadow = "none";
|
|
||||||
mainCardStyles.border = "none";
|
|
||||||
mainCardStyles.minHeight = "";
|
|
||||||
mainCardStyles.alignItems = "stretch";
|
|
||||||
mainCardStyles.flexGrow = 1;
|
|
||||||
mainCardStyles.display = "flex";
|
|
||||||
formStyles.display = "flex";
|
formStyles.display = "flex";
|
||||||
formStyles.flexGrow = 1;
|
formStyles.flexGrow = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************
|
||||||
|
**
|
||||||
|
***************************************************************************/
|
||||||
|
function makeMainCardStyles(theme: Theme)
|
||||||
|
{
|
||||||
|
const mainCardStyles: any = {};
|
||||||
|
|
||||||
|
if (!isWidget && !isModal)
|
||||||
|
{
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
// remove margin around card for non-widget, non-modal, small //
|
||||||
|
////////////////////////////////////////////////////////////////
|
||||||
|
mainCardStyles[theme.breakpoints.down("sm")] = {
|
||||||
|
marginLeft: "-1.5rem",
|
||||||
|
marginRight: "-1.5rem",
|
||||||
|
borderRadius: "0"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
|
||||||
|
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
|
||||||
|
{
|
||||||
|
mainCardStyles.background = "#FFFFFF";
|
||||||
|
mainCardStyles.boxShadow = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWidget)
|
||||||
|
{
|
||||||
|
mainCardStyles.background = "none";
|
||||||
|
mainCardStyles.boxShadow = "none";
|
||||||
|
mainCardStyles.border = "none";
|
||||||
|
mainCardStyles.minHeight = "";
|
||||||
|
mainCardStyles.alignItems = "stretch";
|
||||||
|
mainCardStyles.flexGrow = 1;
|
||||||
|
mainCardStyles.display = "flex";
|
||||||
|
}
|
||||||
|
|
||||||
|
return mainCardStyles;
|
||||||
|
}
|
||||||
|
|
||||||
let nextButtonLabel = "Next";
|
let nextButtonLabel = "Next";
|
||||||
let nextButtonIcon = "arrow_forward";
|
let nextButtonIcon = "arrow_forward";
|
||||||
if (overrideOnLastStep !== null)
|
if (overrideOnLastStep !== null)
|
||||||
@ -1549,23 +1924,24 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={validationScheme}
|
validationSchema={validationScheme}
|
||||||
validation={validationFunction}
|
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 //
|
// once we're in the formik form, capture some of its functions //
|
||||||
// over top of the default one we created globally //
|
// over top of the default ones we created globally //
|
||||||
///////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
formikSetFieldValueFunction = setFieldValue;
|
formikSetFieldValueFunction = setFieldValue;
|
||||||
|
formikSetTouched = setTouched;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form style={formStyles} id={formId} autoComplete="off">
|
<Form style={formStyles} id={formId} autoComplete="off">
|
||||||
<Card sx={mainCardStyles}>
|
<Card sx={makeMainCardStyles}>
|
||||||
{
|
{
|
||||||
!isWidget && (
|
!isWidget && processMetaData?.stepFlow == "LINEAR" && (
|
||||||
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
|
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
|
||||||
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
<Stepper activeStep={activeStepIndex} alternativeLabel>
|
||||||
{steps.map((step) => (
|
{steps.map((step) => (
|
||||||
@ -1600,21 +1976,16 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
{/********************************
|
{/********************************
|
||||||
** back &| next/submit buttons **
|
** back &| next/submit buttons **
|
||||||
********************************/}
|
********************************/}
|
||||||
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
<Box mt={3} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
|
||||||
{true || activeStepIndex === 0 ? (
|
{true || activeStepIndex === 0 ? (
|
||||||
<Box />
|
<Box />
|
||||||
) : (
|
) : (
|
||||||
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
|
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
|
||||||
)}
|
)}
|
||||||
{processError || qJobRunning || !activeStep ? (
|
{processError || qJobRunning || !activeStep || activeStep?.format?.toLowerCase() == "scanner" ? (
|
||||||
<Box />
|
<Box />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{formError && (
|
|
||||||
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
|
|
||||||
{formError}
|
|
||||||
</MDTypography>
|
|
||||||
)}
|
|
||||||
{
|
{
|
||||||
noMoreSteps && <QCancelButton
|
noMoreSteps && <QCancelButton
|
||||||
onClickHandler={() => handleCancelClicked(true)}
|
onClickHandler={() => handleCancelClicked(true)}
|
||||||
@ -1650,9 +2021,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
|
|||||||
|
|
||||||
const body = (
|
const body = (
|
||||||
<Box py={3} mb={20} className="processRun">
|
<Box py={3} mb={20} className="processRun">
|
||||||
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
|
<Grid container justifyContent="center" alignItems="center" mt={{xs: 0, md: 6}} sx={{height: "100%"}}>
|
||||||
<Grid item xs={12} lg={10} xl={8}>
|
<Grid item xs={12} lg={10} xl={8}>
|
||||||
{form}
|
{form}
|
||||||
|
{formError && <Alert severity="error" onClose={() => setFormError(null)} sx={{position: "fixed", top: "40px", left: "10vw", width: "calc(80vw)", zIndex: "99999"}}>{formError}</Alert>}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
|
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[];
|
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]);
|
let valueRecord = new QRecord(result.values.valueCounts[i]);
|
||||||
|
|
||||||
|
@ -779,13 +779,12 @@ function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={tableName}
|
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
|
||||||
fieldName={field.name}
|
|
||||||
fieldLabel="Value"
|
fieldLabel="Value"
|
||||||
initialValue={selectedPossibleValue?.id}
|
initialValue={selectedPossibleValue?.id}
|
||||||
initialDisplayValue={selectedPossibleValue?.label}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
useCase="filter"
|
||||||
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -847,13 +846,12 @@ function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaDa
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DynamicSelect
|
<DynamicSelect
|
||||||
tableName={tableName}
|
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: null}}
|
||||||
fieldName={field.name}
|
|
||||||
isMultiple={true}
|
isMultiple={true}
|
||||||
fieldLabel="Value"
|
fieldLabel="Value"
|
||||||
initialValues={selectedPossibleValues}
|
|
||||||
inForm={false}
|
inForm={false}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
useCase="filter"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -33,8 +33,7 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
|
|||||||
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
|
||||||
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
|
||||||
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
|
||||||
import {Alert, Collapse, Menu, Typography} from "@mui/material";
|
import {Alert, Box, Collapse, Menu, Typography} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import Card from "@mui/material/Card";
|
import Card from "@mui/material/Card";
|
||||||
import Divider from "@mui/material/Divider";
|
import Divider from "@mui/material/Divider";
|
||||||
@ -92,6 +91,7 @@ interface Props
|
|||||||
launchProcess?: QProcessMetaData;
|
launchProcess?: QProcessMetaData;
|
||||||
usage?: QueryScreenUsage;
|
usage?: QueryScreenUsage;
|
||||||
isModal?: boolean;
|
isModal?: boolean;
|
||||||
|
isPreview?: boolean;
|
||||||
initialQueryFilter?: QQueryFilter;
|
initialQueryFilter?: QQueryFilter;
|
||||||
initialColumns?: QQueryColumns;
|
initialColumns?: QQueryColumns;
|
||||||
allowVariables?: boolean;
|
allowVariables?: boolean;
|
||||||
@ -126,7 +126,7 @@ const getLoadingScreen = (isModal: boolean) =>
|
|||||||
**
|
**
|
||||||
** Yuge component. The best. Lots of very smart people are saying so.
|
** Yuge component. The best. Lots of very smart people are saying so.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
|
||||||
{
|
{
|
||||||
const tableName = table.name;
|
const tableName = table.name;
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
@ -884,6 +884,18 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Opens a new query screen in a new window with the current filter
|
||||||
|
*******************************************************************************/
|
||||||
|
const openFilterInNewWindow = () =>
|
||||||
|
{
|
||||||
|
let filterForBackend = JSON.parse(JSON.stringify(view.queryFilter));
|
||||||
|
filterForBackend = FilterUtils.prepQueryFilterForBackend(tableMetaData, filterForBackend);
|
||||||
|
const url = `${metaData?.getTablePathByName(tableName)}?filter=${encodeURIComponent(JSON.stringify(filterForBackend))}`;
|
||||||
|
window.open(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** This is the method that actually executes a query to update the data in the table.
|
** This is the method that actually executes a query to update the data in the table.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -2232,12 +2244,25 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
|||||||
return (
|
return (
|
||||||
<GridToolbarContainer>
|
<GridToolbarContainer>
|
||||||
<div>
|
<div>
|
||||||
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
<Tooltip title="Refresh Query">
|
||||||
</div>
|
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||||
<div style={{position: "relative"}}>
|
</Tooltip>
|
||||||
{/* @ts-ignore */}
|
|
||||||
<GridToolbarDensitySelector nonce={undefined} />
|
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
!isPreview && (
|
||||||
|
<div style={{position: "relative"}}>
|
||||||
|
{/* @ts-ignore */}
|
||||||
|
<GridToolbarDensitySelector nonce={undefined} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isPreview && (
|
||||||
|
<Tooltip title="Open In New Window">
|
||||||
|
<Button id="open-filter-in-new-window-button" onClick={() => openFilterInNewWindow()} startIcon={<Icon>launch</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
usage == "queryScreen" &&
|
usage == "queryScreen" &&
|
||||||
@ -2872,7 +2897,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQ
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
metaData && tableMetaData &&
|
!isPreview && metaData && tableMetaData &&
|
||||||
<BasicAndAdvancedQueryControls
|
<BasicAndAdvancedQueryControls
|
||||||
ref={basicAndAdvancedQueryControlsRef}
|
ref={basicAndAdvancedQueryControlsRef}
|
||||||
metaData={metaData}
|
metaData={metaData}
|
||||||
|
@ -34,6 +34,7 @@ import BaseLayout from "qqq/layouts/BaseLayout";
|
|||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
import ValueUtils from "qqq/utils/qqq/ValueUtils";
|
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-java";
|
||||||
import "ace-builds/src-noconflict/mode-javascript";
|
import "ace-builds/src-noconflict/mode-javascript";
|
||||||
import "ace-builds/src-noconflict/mode-json";
|
import "ace-builds/src-noconflict/mode-json";
|
||||||
|
@ -788,503 +788,19 @@ input[type="search"]::-webkit-search-results-decoration
|
|||||||
margin: 2rem 1rem;
|
margin: 2rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* default styles for a block widget overlay */
|
||||||
.sqd-designer-react {
|
.blockWidgetOverlay
|
||||||
width: 100vw;
|
{
|
||||||
height: 90vh;
|
font-weight: 400;
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-editor {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
input:read-only {
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-editor {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
input:read-only {
|
|
||||||
opacity: 0.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* internal */
|
|
||||||
.sqd-theme-light .sqd-toolbox {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-toolbox-header-title {
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-toolbox-filter {
|
|
||||||
background: #fff;
|
|
||||||
color: #000;
|
|
||||||
border: 1px solid #c3c3c3;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-toolbox-filter:focus {
|
|
||||||
border-color: #939393;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-toolbox-group-title {
|
|
||||||
color: #000;
|
|
||||||
background: #e5e5e5;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-toolbox-item {
|
|
||||||
color: #000;
|
|
||||||
border: 1px solid #c3c3c3;
|
|
||||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-toolbox-item:hover {
|
|
||||||
border-color: #939393;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-toolbox-item .sqd-toolbox-item-icon.sqd-no-icon {
|
|
||||||
background: #c6c6c6;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-control-bar {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.15);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-control-bar-button {
|
|
||||||
border: 1px solid #c3c3c3;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-control-bar-button:hover {
|
|
||||||
border-color: #939393;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-control-bar-button .sqd-icon-path {
|
|
||||||
fill: #000;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-control-bar-button.sqd-delete .sqd-icon-path {
|
|
||||||
fill: #e01a24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-smart-editor {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-smart-editor-toggle {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light.sqd-context-menu {
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-context-menu-group {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-context-menu-item {
|
|
||||||
color: #000;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-context-menu-item:hover {
|
|
||||||
background: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light.sqd-designer {
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-line-grid-path {
|
|
||||||
stroke: #e3e3e3;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-join {
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-region {
|
|
||||||
stroke: #cecece;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-dasharray: 3;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-region.sqd-selected {
|
|
||||||
stroke: #ed4800;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-dasharray: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-placeholder .sqd-placeholder-rect {
|
|
||||||
fill: #d8d8d8;
|
|
||||||
stroke: #6a6a6a;
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 3;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-placeholder.sqd-hover .sqd-placeholder-rect {
|
|
||||||
fill: #ed4800;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-placeholder-icon-path {
|
|
||||||
fill: #2b2b2b;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-placeholder.sqd-hover .sqd-placeholder-icon-path {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-validation-error {
|
|
||||||
fill: #ffa200;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-validation-error-icon-path {
|
|
||||||
fill: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-root-start-stop-circle {
|
|
||||||
fill: #2c18df;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-root-start-stop-icon {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-step-task .sqd-step-task-rect {
|
|
||||||
fill: #fff;
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke: #c3c3c3;
|
|
||||||
filter: drop-shadow(0 1.5px 1.5px rgba(0, 0, 0, 0.15));
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-task .sqd-step-task-rect.sqd-selected {
|
|
||||||
stroke: #ed4800;
|
|
||||||
stroke-width: 2;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-task .sqd-step-task-text {
|
|
||||||
fill: #000;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-task .sqd-step-task-empty-icon {
|
|
||||||
fill: #c6c6c6;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-task .sqd-input {
|
|
||||||
fill: #fff;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #000;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-task .sqd-output {
|
|
||||||
fill: #000;
|
|
||||||
stroke-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-step-switch > .sqd-label-primary > .sqd-label-text {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-switch > .sqd-label-primary > .sqd-label-rect {
|
|
||||||
fill: #2411db;
|
|
||||||
stroke-width: 0;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-switch > .sqd-label-secondary > .sqd-label-rect {
|
|
||||||
fill: #000;
|
|
||||||
stroke-width: 0;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-switch > .sqd-label-secondary > .sqd-label-text {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-switch > g > .sqd-input {
|
|
||||||
fill: #fff;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-theme-light .sqd-step-container > .sqd-label > .sqd-label-text {
|
|
||||||
fill: #fff;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-container > .sqd-label > .sqd-label-rect {
|
|
||||||
fill: #2411db;
|
|
||||||
stroke-width: 0;
|
|
||||||
}
|
|
||||||
.sqd-theme-light .sqd-step-container > g > .sqd-input {
|
|
||||||
fill: #fff;
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* .sqd-designer */
|
|
||||||
.sqd-designer {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
top: 15px;
|
||||||
|
height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
font-size: 14px;
|
||||||
height: 100%;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.blockWidgetOverlay a
|
||||||
.sqd-designer,
|
{
|
||||||
.sqd-drag,
|
color: #0062FF !important;
|
||||||
.sqd-context-menu {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-hidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-disabled {
|
|
||||||
opacity: 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .sqd-toolbox */
|
|
||||||
.sqd-toolbox,
|
|
||||||
.sqd-toolbox-filter {
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 20;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 250px;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-header {
|
|
||||||
position: relative;
|
|
||||||
padding: 15px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-header-title {
|
|
||||||
display: block;
|
|
||||||
font-size: 1.2em;
|
|
||||||
line-height: 1em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-toggle-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: 10px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin: -8px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-header:hover .sqd-toolbox-toggle-icon {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-scrollbox {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-scrollbox-body {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-filter {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 6px 8px;
|
|
||||||
outline: none;
|
|
||||||
width: 110px;
|
|
||||||
margin: 0 10px 10px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-group-title {
|
|
||||||
text-align: center;
|
|
||||||
padding: 5px 0;
|
|
||||||
margin: 0 10px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-item {
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0 10px 10px;
|
|
||||||
cursor: move;
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-item-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 5px;
|
|
||||||
margin-top: -10px;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-item-icon-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-toolbox-item-text {
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
padding: 10px 10px 10px 30px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-drag {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 9999999;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .sqd-control-bar */
|
|
||||||
.sqd-control-bar {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 20;
|
|
||||||
padding: 8px 0 8px 8px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-control-bar-button {
|
|
||||||
display: inline-block;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
margin-right: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-control-bar-button-icon {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin: 3px 0 0 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-control-bar-button.sqd-disabled .sqd-control-bar-button-icon {
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .sqd-smart-editor */
|
|
||||||
.sqd-smart-editor-toggle {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
z-index: 29;
|
|
||||||
width: 36px;
|
|
||||||
height: 64px;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-smart-editor-toggle-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin: -12px 0 0 -12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-smart-editor-toggle:hover .sqd-smart-editor-toggle-icon {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-smart-editor {
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-layout-desktop .sqd-smart-editor {
|
|
||||||
position: relative;
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-layout-desktop .sqd-smart-editor-toggle {
|
|
||||||
right: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-layout-desktop .sqd-smart-editor-toggle.sqd-collapsed {
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-layout-mobile .sqd-smart-editor {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 41px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-layout-mobile .sqd-smart-editor-toggle {
|
|
||||||
left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-layout-mobile .sqd-smart-editor-toggle.sqd-collapsed {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .sqd-context-menu */
|
|
||||||
.sqd-context-menu {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 2000000000;
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-context-menu-group,
|
|
||||||
.sqd-context-menu-item {
|
|
||||||
width: 130px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-context-menu-group {
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-context-menu-item {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 70ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .sqd-workspace */
|
|
||||||
.sqd-workspace {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
display: block;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-workspace-canvas {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
cursor: move;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-label-text {
|
|
||||||
text-anchor: middle;
|
|
||||||
dominant-baseline: central;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-placeholder .sqd-placeholder-rect {
|
|
||||||
transition: fill 100ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sqd-step-task-text {
|
|
||||||
text-anchor: left;
|
|
||||||
dominant-baseline: central;
|
|
||||||
}
|
}
|
||||||
|
@ -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.`);
|
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");
|
row[field.name] = ValueUtils.getDisplayValue(field, record, "query");
|
||||||
});
|
});
|
||||||
|
|
||||||
if(tableMetaData.exposedJoins)
|
if (tableMetaData.exposedJoins)
|
||||||
{
|
{
|
||||||
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||||
{
|
{
|
||||||
const join = tableMetaData.exposedJoins[i];
|
const join = tableMetaData.exposedJoins[i];
|
||||||
|
|
||||||
if(join?.joinTable?.fields?.values())
|
if (join?.joinTable?.fields?.values())
|
||||||
{
|
{
|
||||||
const fields = [...join.joinTable.fields.values()];
|
const fields = [...join.joinTable.fields.values()];
|
||||||
fields.forEach((field) =>
|
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];
|
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 //
|
// DataGrid gets very upset about a null or undefined here, so, try to make it happier //
|
||||||
/////////////////////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
if(!allowEmptyId)
|
if (!allowEmptyId)
|
||||||
{
|
{
|
||||||
row["id"] = "--";
|
row["id"] = "--";
|
||||||
}
|
}
|
||||||
@ -170,7 +170,7 @@ export default class DataGridUtils
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (rows);
|
return (rows);
|
||||||
}
|
};
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
@ -180,24 +180,24 @@ export default class DataGridUtils
|
|||||||
const columns = [] as GridColDef[];
|
const columns = [] as GridColDef[];
|
||||||
this.addColumnsForTable(tableMetaData, linkBase, columns, columnSort, null, null);
|
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++)
|
for (let i = 0; i < tableMetaData.exposedJoins.length; i++)
|
||||||
{
|
{
|
||||||
const join = tableMetaData.exposedJoins[i];
|
const join = tableMetaData.exposedJoins[i];
|
||||||
let joinTableName = join.joinTable.name;
|
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;
|
let joinLinkBase = null;
|
||||||
joinLinkBase = metaData.getTablePath(join.joinTable);
|
joinLinkBase = metaData.getTablePath(join.joinTable);
|
||||||
if(joinLinkBase)
|
if (joinLinkBase)
|
||||||
{
|
{
|
||||||
joinLinkBase += joinLinkBase.endsWith("/") ? "" : "/";
|
joinLinkBase += joinLinkBase.endsWith("/") ? "" : "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(join?.joinTable?.fields?.values())
|
if (join?.joinTable?.fields?.values())
|
||||||
{
|
{
|
||||||
this.addColumnsForTable(join.joinTable, joinLinkBase, columns, columnSort, joinTableName + ".", join.label + ": ");
|
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... //
|
// 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++)
|
for (let i = 0; i < tableMetaData.sections.length; i++)
|
||||||
{
|
{
|
||||||
@ -241,19 +241,23 @@ export default class DataGridUtils
|
|||||||
///////////////////////////
|
///////////////////////////
|
||||||
// sort by labels... mmm //
|
// sort by labels... mmm //
|
||||||
///////////////////////////
|
///////////////////////////
|
||||||
sortedKeys.push(...tableMetaData.fields.keys())
|
sortedKeys.push(...tableMetaData.fields.keys());
|
||||||
sortedKeys.sort((a: string, b: string): number =>
|
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) =>
|
sortedKeys.forEach((key) =>
|
||||||
{
|
{
|
||||||
const field = tableMetaData.fields.get(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. //
|
// 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);
|
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);
|
columns.splice(0, 0, column);
|
||||||
}
|
}
|
||||||
@ -346,9 +350,9 @@ export default class DataGridUtils
|
|||||||
(cellValues.value)
|
(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?
|
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}`} />;
|
const formattedHelpContent = <HelpContent helpContents={field.helpContents} roles={helpRoles} heading={headerName} helpContentKey={`table:${tableMetaData.name};field:${fieldName}`} />;
|
||||||
column.renderHeader = (params: GridColumnHeaderParams) => (
|
column.renderHeader = (params: GridColumnHeaderParams) => (
|
||||||
@ -361,7 +365,7 @@ export default class DataGridUtils
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (column);
|
return (column);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -390,7 +394,7 @@ export default class DataGridUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(field.possibleValueSourceName)
|
if (field.possibleValueSourceName)
|
||||||
{
|
{
|
||||||
return (200);
|
return (200);
|
||||||
}
|
}
|
||||||
@ -415,6 +419,6 @@ export default class DataGridUtils
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (200);
|
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/>.
|
* 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.
|
** 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...
|
** 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");
|
const link = document.createElement("a");
|
||||||
link.download = filename;
|
link.download = filename;
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@ -93,8 +99,14 @@ export default class HtmlUtils
|
|||||||
// todo - onload event handler to let us know when done?
|
// todo - onload event handler to let us know when done?
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
|
|
||||||
|
var method = "get";
|
||||||
|
if (QFieldType.BLOB == field.type)
|
||||||
|
{
|
||||||
|
method = "post";
|
||||||
|
}
|
||||||
|
|
||||||
const form = document.createElement("form");
|
const form = document.createElement("form");
|
||||||
form.setAttribute("method", "post");
|
form.setAttribute("method", method);
|
||||||
form.setAttribute("action", url);
|
form.setAttribute("action", url);
|
||||||
form.setAttribute("target", "downloadIframe");
|
form.setAttribute("target", "downloadIframe");
|
||||||
iframe.appendChild(form);
|
iframe.appendChild(form);
|
||||||
@ -117,7 +129,7 @@ export default class HtmlUtils
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
static openInNewWindow = (url: string, filename: string) =>
|
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
|
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 {Chip, ClickAwayListener, Icon} from "@mui/material";
|
||||||
import Box from "@mui/material/Box";
|
import Box from "@mui/material/Box";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import IconButton from "@mui/material/IconButton";
|
|
||||||
import Tooltip from "@mui/material/Tooltip";
|
import Tooltip from "@mui/material/Tooltip";
|
||||||
import {makeStyles} from "@mui/styles";
|
|
||||||
import parse from "html-react-parser";
|
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 HtmlUtils from "qqq/utils/HtmlUtils";
|
||||||
import Client from "qqq/utils/qqq/Client";
|
import Client from "qqq/utils/qqq/Client";
|
||||||
|
|
||||||
|
import "ace-builds/src-noconflict/ace";
|
||||||
import "ace-builds/src-noconflict/mode-sql";
|
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 "ace-builds/src-noconflict/mode-velocity";
|
||||||
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Utility class for working with QQQ Values
|
** 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} />);
|
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
|
||||||
}
|
}
|
||||||
@ -219,7 +218,7 @@ class ValueUtils
|
|||||||
|
|
||||||
if (field.type === QFieldType.DATE_TIME)
|
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 //
|
// 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))
|
if (!(date instanceof Date))
|
||||||
{
|
{
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// so, a new Date here will interpret the string as being at midnight UTC, but //
|
||||||
|
// the data object will be in the user/browser timezone. //
|
||||||
|
// so "2024-08-22", for a user in US/Central, will be "2024-08-21T19:00:00-0500". //
|
||||||
|
// correct for that by adding the date's timezone offset (converted from minutes //
|
||||||
|
// to millis) back to it //
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////
|
||||||
date = new Date(date);
|
date = new Date(date);
|
||||||
|
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return (`${date.toString("yyyy-MM-dd")}`);
|
return (`${date.toString("yyyy-MM-dd")}`);
|
||||||
@ -466,7 +473,7 @@ class ValueUtils
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static cleanForCsv(param: any): string
|
public static cleanForCsv(param: any): string
|
||||||
{
|
{
|
||||||
if(param === undefined || param === null)
|
if (param === undefined || param === null)
|
||||||
{
|
{
|
||||||
return ("");
|
return ("");
|
||||||
}
|
}
|
||||||
@ -491,7 +498,7 @@ class ValueUtils
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// little private component here, for rendering an AceEditor with some buttons/controls/state //
|
// 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 [activeCode, setActiveCode] = useState(code);
|
||||||
const [isFormatted, setIsFormatted] = useState(false);
|
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 //
|
// 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 [adornmentFieldsMap, setAdornmentFieldsMap] = useState(new Map<string, boolean>);
|
||||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
@ -645,7 +652,7 @@ function RevealComponent({fieldName, value, usage}: {fieldName: string, value: s
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ClickAwayListener>
|
</ClickAwayListener>
|
||||||
</Box>
|
</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>
|
<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>) =>
|
const download = (event: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
{
|
{
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
HtmlUtils.downloadUrlViaIFrame(url, filename);
|
HtmlUtils.downloadUrlViaIFrame(field, url, filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
const open = (event: React.MouseEvent<HTMLSpanElement>) =>
|
const open = (event: React.MouseEvent<HTMLSpanElement>) =>
|
||||||
@ -681,7 +688,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
|||||||
HtmlUtils.openInNewWindow(url, filename);
|
HtmlUtils.openInNewWindow(url, filename);
|
||||||
};
|
};
|
||||||
|
|
||||||
if(!filename || !url)
|
if (!filename || !url)
|
||||||
{
|
{
|
||||||
return (<React.Fragment />);
|
return (<React.Fragment />);
|
||||||
}
|
}
|
||||||
@ -696,10 +703,22 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
|||||||
usage == "view" && filename
|
usage == "view" && filename
|
||||||
}
|
}
|
||||||
<Tooltip placement={tooltipPlacement} title="Open file">
|
<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>
|
||||||
<Tooltip placement={tooltipPlacement} title="Download file">
|
<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>
|
</Tooltip>
|
||||||
{
|
{
|
||||||
usage == "query" && filename
|
usage == "query" && filename
|
||||||
@ -709,5 +728,4 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default ValueUtils;
|
export default ValueUtils;
|
||||||
|
@ -57,4 +57,5 @@ module.exports = function (app)
|
|||||||
app.use("/images", getRequestHandler());
|
app.use("/images", getRequestHandler());
|
||||||
app.use("/api*", getRequestHandler());
|
app.use("/api*", 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",
|
"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",
|
"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": [
|
"columns": [
|
||||||
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" },
|
{ "type": "html", "header": "Id", "accessor": "id", "width": "30px" },
|
||||||
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" }
|
{ "type": "html", "header": "Name", "accessor": "name", "width": "1fr" }
|
||||||
],
|
],
|
||||||
"rows": [
|
"rows": [
|
||||||
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
|
{ "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 //
|
// assert that the table widget rendered its header and some contents //
|
||||||
////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////
|
||||||
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
|
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");
|
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
Reference in New Issue
Block a user