Compare commits

..

1 Commits

Author SHA1 Message Date
9a6fcd8bb1 CE-1482: POC of workflow library 2024-08-09 11:44:54 -05:00
60 changed files with 6206 additions and 3810 deletions

5510
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.110",
"@kingsrook/qqq-frontend-core": "1.0.104",
"@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1",
"@mui/styles": "5.11.1",
@ -18,8 +18,8 @@
"@react-jvectormap/core": "1.0.1",
"@react-jvectormap/unitedstates": "1.0.1",
"@react-oauth/google": "0.2.8",
"@types/prop-types": "^15.7.5",
"@types/react": "18.0.0",
"@types/prop-types": "15.7.5",
"@types/react": "18.2.0",
"@types/react-dom": "18.0.0",
"@types/react-router-hash-link": "2.4.5",
"ace-builds": "1.12.3",
@ -33,7 +33,7 @@
"form-data": "4.0.0",
"formik": "2.2.9",
"html-react-parser": "1.4.8",
"html-to-text": "^9.0.5",
"html-to-text": "9.0.5",
"http-proxy-middleware": "2.0.6",
"jwt-decode": "3.1.2",
"rapidoc": "9.3.4",
@ -46,12 +46,16 @@
"react-dom": "18.0.0",
"react-ga4": "2.1.0",
"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-router-dom": "6.2.1",
"react-router-hash-link": "2.4.3",
"react-table": "7.7.0",
"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",
"yup": "0.32.11"
},

View File

@ -29,7 +29,7 @@
<packaging>jar</packaging>
<properties>
<revision>0.23.0-SNAPSHOT</revision>
<revision>0.21.0-SNAPSHOT</revision>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@ -66,7 +66,7 @@
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-backend-core</artifactId>
<version>0.21.0</version>
<version>0.20.0-20240308.165846-65</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
@ -154,11 +154,11 @@
<versionTagPrefix>version-</versionTagPrefix>
</gitFlowConfig>
<skipFeatureVersion>true</skipFeatureVersion> <!-- Keep feature names out of versions -->
<postReleaseGoals>install</postReleaseGoals> <!-- Let CI run deploys -->
<commitDevelopmentVersionAtStart>true</commitDevelopmentVersionAtStart>
<versionDigitToIncrement>1</versionDigitToIncrement> <!-- In general, we update the minor -->
<versionProperty>revision</versionProperty>
<skipUpdateVersion>true</skipUpdateVersion>
<skipTestProject>true</skipTestProject> <!-- we allow CI to do the tests -->
</configuration>
</plugin>

View File

@ -30,17 +30,14 @@ import MDButton from "qqq/components/legacy/MDButton";
export const standardWidth = "150px";
const standardML = {xs: 1, md: 3};
interface QCreateNewButtonProps
{
tablePath: string;
}
export function QCreateNewButton({tablePath}: QCreateNewButtonProps): JSX.Element
{
return (
<Box display="inline-block" ml={standardML} mr={0} width={standardWidth}>
<Box display="inline-block" ml={3} mr={0} width={standardWidth}>
<Link to={`${tablePath}/create`}>
<MDButton variant="gradient" color="info" fullWidth startIcon={<Icon>add</Icon>}>
Create New
@ -57,7 +54,6 @@ interface QSaveButtonProps
onClickHandler?: any,
disabled: boolean
}
QSaveButton.defaultProps = {
label: "Save",
iconName: "save"
@ -66,7 +62,7 @@ QSaveButton.defaultProps = {
export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveButtonProps): JSX.Element
{
return (
<Box ml={standardML} width={standardWidth}>
<Box ml={3} width={standardWidth}>
<MDButton type="submit" variant="gradient" color="info" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
{label}
</MDButton>
@ -76,18 +72,17 @@ export function QSaveButton({label, iconName, onClickHandler, disabled}: QSaveBu
interface QDeleteButtonProps
{
onClickHandler: any;
disabled?: boolean;
onClickHandler: any
disabled?: boolean
}
QDeleteButton.defaultProps = {
disabled: false
};
export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): JSX.Element
{
return (
<Box ml={standardML} width={standardWidth}>
<Box ml={3} width={standardWidth}>
<MDButton variant="gradient" color="primary" size="small" onClick={onClickHandler} fullWidth startIcon={<Icon>delete</Icon>} disabled={disabled}>
Delete
</MDButton>
@ -98,7 +93,7 @@ export function QDeleteButton({onClickHandler, disabled}: QDeleteButtonProps): J
export function QEditButton(): JSX.Element
{
return (
<Box ml={standardML} width={standardWidth}>
<Box ml={3} width={standardWidth}>
<Link to="edit">
<MDButton variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>edit</Icon>}>
Edit
@ -137,7 +132,7 @@ interface QCancelButtonProps
onClickHandler: any;
disabled: boolean;
label?: string;
iconName?: string;
iconName?: string
}
export function QCancelButton({
@ -145,7 +140,7 @@ export function QCancelButton({
}: QCancelButtonProps): JSX.Element
{
return (
<Box ml={standardML} mb={2} width={standardWidth}>
<Box ml="auto" width={standardWidth}>
<MDButton type="button" variant="outlined" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} onClick={onClickHandler} disabled={disabled}>
{label}
</MDButton>
@ -160,15 +155,15 @@ QCancelButton.defaultProps = {
interface QSubmitButtonProps
{
label?: string;
iconName?: string;
disabled: boolean;
label?: string
iconName?: string
disabled: boolean
}
export function QSubmitButton({label, iconName, disabled}: QSubmitButtonProps): JSX.Element
{
return (
<Box ml={standardML} width={standardWidth}>
<Box ml={3} width={standardWidth}>
<MDButton type="submit" variant="gradient" color="dark" size="small" fullWidth startIcon={<Icon>{iconName}</Icon>} disabled={disabled}>
{label}
</MDButton>

View File

@ -172,14 +172,17 @@ function QDynamicForm({formData, formLabel, bulkEditMode, bulkEditSwitchChangeHa
<Grid item xs={12} sm={6} key={fieldName}>
{labelElement}
<DynamicSelect
fieldPossibleValueProps={field.possibleValueProps}
tableName={field.possibleValueProps.tableName}
processName={field.possibleValueProps.processName}
possibleValueSourceName={field.possibleValueProps.possibleValueSourceName}
fieldName={field.possibleValueProps.fieldName}
isEditable={field.isEditable}
fieldLabel=""
initialValue={values[fieldName]}
initialDisplayValue={field.possibleValueProps.initialDisplayValue}
bulkEditMode={bulkEditMode}
bulkEditSwitchChangeHandler={bulkEditSwitchChanged}
otherValues={otherValuesMap}
useCase="form"
/>
{formattedHelpContent}
</Grid>

View File

@ -40,8 +40,6 @@ interface Props
value: any;
type: string;
isEditable?: boolean;
placeholder?: string;
backgroundColor?: string;
[key: string]: any;
@ -51,7 +49,7 @@ interface Props
}
function QDynamicFormField({
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, placeholder, backgroundColor, formFieldObject, ...rest
label, name, displayFormat, value, bulkEditMode, bulkEditSwitchChangeHandler, type, isEditable, formFieldObject, ...rest
}: Props): JSX.Element
{
const [switchChecked, setSwitchChecked] = useState(false);
@ -67,30 +65,18 @@ function QDynamicFormField({
inputLabelProps.shrink = true;
}
const inputProps: any = {};
const inputProps = {};
if (displayFormat && displayFormat.startsWith("$"))
{
// @ts-ignore
inputProps.startAdornment = <InputAdornment position="start">$</InputAdornment>;
}
if (displayFormat && displayFormat.endsWith("%%"))
{
// @ts-ignore
inputProps.endAdornment = <InputAdornment position="end">%</InputAdornment>;
}
if (placeholder)
{
inputProps.placeholder = placeholder
}
if(backgroundColor)
{
inputProps.sx = {
"&.MuiInputBase-root": {
backgroundColor: backgroundColor
}
};
}
// @ts-ignore
const handleOnWheel = (e) =>
{

View File

@ -22,7 +22,6 @@
import {AdornmentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/AdornmentType";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
import * as Yup from "yup";
@ -129,12 +128,19 @@ class DynamicFormUtils
const effectivelyIsRequired = field.isRequired && effectiveIsEditable;
if (effectivelyIsRequired)
{
if (field.possibleValueSourceName)
{
////////////////////////////////////////////////////////////////////////////////////////////
// the "nullable(true)" here doesn't mean that you're allowed to set the field to null... //
// rather, it's more like "null is how empty will be treated" or some-such... //
////////////////////////////////////////////////////////////////////////////////////////////
return (Yup.string().required(`${field.label ?? "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);
}
@ -149,49 +155,47 @@ class DynamicFormUtils
{
const field = qFields[i];
if(!dynamicFormFields[field.name])
{
continue;
}
/////////////////////////////////////////
// add props for possible value fields //
/////////////////////////////////////////
if (field.possibleValueSourceName || field.inlinePossibleValueSource)
if (field.possibleValueSourceName && dynamicFormFields[field.name])
{
let props: FieldPossibleValueProps =
{
isPossibleValue: true,
fieldName: field.name,
initialDisplayValue: null
}
let initialDisplayValue = null;
if (displayValues)
{
props.initialDisplayValue = displayValues.get(field.name);
initialDisplayValue = displayValues.get(field.name);
}
if(field.inlinePossibleValueSource)
if (tableName)
{
//////////////////////////////////////////////////////////////////////
// handle an inline PVS - which is a list of possible value objects //
//////////////////////////////////////////////////////////////////////
props.possibleValues = field.inlinePossibleValueSource;
}
else if (tableName)
dynamicFormFields[field.name].possibleValueProps =
{
props.tableName = tableName;
isPossibleValue: true,
tableName: tableName,
fieldName: field.name,
initialDisplayValue: initialDisplayValue,
};
}
else if (processName)
{
props.processName = processName;
dynamicFormFields[field.name].possibleValueProps =
{
isPossibleValue: true,
processName: processName,
fieldName: field.name,
initialDisplayValue: initialDisplayValue,
};
}
else
{
props.possibleValueSourceName = field.possibleValueSourceName;
dynamicFormFields[field.name].possibleValueProps =
{
isPossibleValue: true,
initialDisplayValue: initialDisplayValue,
fieldName: field.name,
possibleValueSourceName: field.possibleValueSourceName
};
}
dynamicFormFields[field.name].possibleValueProps = props;
}
}
}

View File

@ -30,17 +30,20 @@ import TextField from "@mui/material/TextField";
import {ErrorMessage, useFormikContext} from "formik";
import colors from "qqq/assets/theme/base/colors";
import MDTypography from "qqq/components/legacy/MDTypography";
import {FieldPossibleValueProps} from "qqq/models/fields/FieldPossibleValueProps";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useState} from "react";
interface Props
{
fieldPossibleValueProps: FieldPossibleValueProps;
tableName?: string;
processName?: string;
fieldName?: string;
possibleValueSourceName?: string;
overrideId?: string;
fieldLabel: string;
inForm: boolean;
initialValue?: any;
initialDisplayValue?: string;
initialValues?: QPossibleValue[];
onChange?: any;
isEditable?: boolean;
@ -50,12 +53,16 @@ interface Props
otherValues?: Map<string, any>;
variant: "standard" | "outlined";
initiallyOpen: boolean;
useCase: "form" | "filter";
}
DynamicSelect.defaultProps = {
tableName: null,
processName: null,
fieldName: null,
possibleValueSourceName: null,
inForm: true,
initialValue: null,
initialDisplayValue: null,
initialValues: undefined,
onChange: null,
isEditable: true,
@ -95,10 +102,8 @@ export const getAutocompleteOutlinedStyle = (isDisabled: boolean) =>
const qController = Client.getInstance();
function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm, initialValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen, useCase}: Props)
function DynamicSelect({tableName, processName, fieldName, possibleValueSourceName, overrideId, fieldLabel, inForm, initialValue, initialDisplayValue, initialValues, onChange, isEditable, isMultiple, bulkEditMode, bulkEditSwitchChangeHandler, otherValues, variant, initiallyOpen}: Props)
{
const {fieldName, initialDisplayValue, possibleValueSourceName, possibleValues, processName, tableName} = fieldPossibleValueProps;
const [open, setOpen] = useState(initiallyOpen);
const [options, setOptions] = useState<readonly QPossibleValue[]>([]);
const [searchTerm, setSearchTerm] = useState(null);
@ -166,35 +171,6 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
setFieldValueRef = setFieldValue;
}
/*******************************************************************************
**
*******************************************************************************/
const filterInlinePossibleValues = (searchTerm: string, possibleValues: QPossibleValue[]): QPossibleValue[] =>
{
return possibleValues.filter(pv => pv.label?.toLowerCase().startsWith(searchTerm));
}
/***************************************************************************
**
***************************************************************************/
const loadResults = async (): Promise<QPossibleValue[]> =>
{
if(possibleValues)
{
return filterInlinePossibleValues(searchTerm, possibleValues)
}
else
{
return await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues, useCase);
}
}
/***************************************************************************
**
***************************************************************************/
useEffect(() =>
{
if (firstRender)
@ -218,7 +194,7 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
(async () =>
{
// console.log(`doing a search with ${searchTerm}`);
const results: QPossibleValue[] = await loadResults();
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
if (tableMetaData == null && tableName)
{
@ -241,10 +217,7 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
};
}, [searchTerm]);
/***************************************************************************
** todo - finish... call it in onOpen?
***************************************************************************/
// todo - finish... call it in onOpen?
const reloadIfOtherValuesAreChanged = () =>
{
if (JSON.stringify(Object.fromEntries(otherValues)) != otherValuesWhenResultsWereLoaded)
@ -253,10 +226,8 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
{
setLoading(true);
setOptions([]);
console.log("Refreshing possible values...");
const results: QPossibleValue[] = await loadResults();
const results: QPossibleValue[] = await qController.possibleValues(tableName, processName, possibleValueSourceName ?? fieldName, searchTerm ?? "", null, otherValues);
setLoading(false);
setOptions([...results]);
setOtherValuesWhenResultsWereLoaded(JSON.stringify(Object.fromEntries(otherValues)));
@ -264,10 +235,6 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
}
};
/***************************************************************************
**
***************************************************************************/
const inputChanged = (event: React.SyntheticEvent, value: string, reason: string) =>
{
// console.log(`input changed. Reason: ${reason}, setting search term to ${value}`);
@ -278,19 +245,11 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
}
};
/***************************************************************************
**
***************************************************************************/
const handleBlur = (x: any) =>
{
setSearchTerm(null);
};
/***************************************************************************
**
***************************************************************************/
const handleChanged = (event: React.SyntheticEvent, value: any | any[], reason: string, details?: string) =>
{
// console.log("handleChanged. value is:");
@ -314,10 +273,6 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
}
};
/***************************************************************************
**
***************************************************************************/
const filterOptions = (options: { id: any; label: string; }[], state: FilterOptionsState<{ id: any; label: string; }>): { id: any; label: string; }[] =>
{
/////////////////////////////////////////////////////////////////////////////////
@ -327,10 +282,6 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
return (options);
};
/***************************************************************************
**
***************************************************************************/
// @ts-ignore
const renderOption = (props: Object, option: any, {selected}) =>
{
@ -379,10 +330,6 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
);
};
/***************************************************************************
**
***************************************************************************/
const bulkEditSwitchChanged = () =>
{
const newSwitchValue = !switchChecked;
@ -403,7 +350,7 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
const autocomplete = (
<Box>
<Autocomplete
id={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"}
id={overrideId ?? fieldName ?? possibleValueSourceName}
sx={autocompleteSX}
open={open}
fullWidth
@ -483,7 +430,7 @@ function DynamicSelect({fieldPossibleValueProps, overrideId, fieldLabel, inForm,
inForm &&
<Box mt={0.75}>
<MDTypography component="div" variant="caption" color="error" fontWeight="regular">
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={overrideId ?? fieldName ?? possibleValueSourceName ?? "anonymous"} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
{!isDisabled && <div className="fieldErrorMessage"><ErrorMessage name={fieldName ?? possibleValueSourceName} render={msg => <span data-field-error="true">{msg}</span>} /></div>}
</MDTypography>
</Box>
}

View File

@ -502,7 +502,7 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
{
const widget = metaData?.widgets?.get(section.widgetName);
const widget = metaData?.widgets.get(section.widgetName);
if (widget)
{
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
@ -602,7 +602,7 @@ function EntityForm(props: Props): JSX.Element
///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName)
{
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], undefined, "form");
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]);
if (results && results.length > 0)
{
defaultDisplayValues.set(fieldName, results[0].label);
@ -1152,11 +1152,11 @@ function EntityForm(props: Props): JSX.Element
<Grid container spacing={3}>
{
!props.isModal &&
<Grid item xs={12} lg={3} className="recordSidebar">
<Grid item xs={12} lg={3}>
<QRecordSidebar tableSections={tableSections} />
</Grid>
}
<Grid item xs={12} lg={props.isModal ? 12 : 9} className={props.isModal ? "" : "recordWithSidebar"}>
<Grid item xs={12} lg={props.isModal ? 12 : 9}>
<Formik
initialValues={initialValues}

View File

@ -64,14 +64,13 @@ function Footer({company, links}: Props): JSX.Element
<Box
width="100%"
display="flex"
flexDirection={{xs: "column", md: "row"}}
flexDirection={{xs: "column", lg: "row"}}
justifyContent="space-between"
alignItems="center"
px={1.5}
style={{
position: "fixed", bottom: "0px", zIndex: -1, marginBottom: "10px",
}}
left={{xs: "0", xl: "auto"}}
>
{
href && name &&

View File

@ -25,7 +25,6 @@ import Autocomplete from "@mui/material/Autocomplete";
import Icon from "@mui/material/Icon";
import IconButton from "@mui/material/IconButton";
import ListItemIcon from "@mui/material/ListItemIcon";
import {Theme} from "@mui/material/styles";
import TextField from "@mui/material/TextField";
import Toolbar from "@mui/material/Toolbar";
import React, {useContext, useEffect, useRef, useState} from "react";
@ -226,19 +225,6 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
const breadcrumbTitle = fullPathToLabel(fullPath, route[route.length - 1]);
///////////////////////////////////////////////////////////////////////////////////////////////
// set the right-half of the navbar up so that below the 'md' breakpoint, it just disappears //
///////////////////////////////////////////////////////////////////////////////////////////////
const navbarRowRight = (theme: Theme, {isMini}: any) =>
{
return {
[theme.breakpoints.down("md")]: {
display: "none",
},
...navbarRow(theme, isMini)
}
};
return (
<AppBar
position={absolute ? "absolute" : navbarType}
@ -255,7 +241,7 @@ function NavBar({absolute, light, isMini}: Props): JSX.Element
<QBreadcrumbs icon="home" title={breadcrumbTitle} route={route} light={light} />
</Box>
{isMini ? null : (
<Box sx={(theme) => navbarRowRight(theme, {isMini})}>
<Box sx={(theme) => navbarRow(theme, {isMini})}>
<Box mt={"-0.25rem"} pb={"0.75rem"} pr={2} mr={-2} sx={{"& *": {cursor: "pointer !important"}}}>
{renderHistory()}
</Box>

View File

@ -84,7 +84,7 @@ function ProcessSummaryResults({
);
return (
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
<Box m={3} mt={6}>
<Grid container>
<Grid item xs={0} lg={2} />
<Grid item xs={12} lg={8}>

View File

@ -273,7 +273,7 @@ function ValidationReview({
);
return (
<Box m={{xs: 0, md: 3}} mt={"3rem!important"}>
<Box m={3}>
<Grid container spacing={2}>
<Grid item xs={12} lg={6}>
<MDTypography color="body" variant="button">

View File

@ -183,7 +183,7 @@ const BasicAndAdvancedQueryControls = forwardRef((props: BasicAndAdvancedQueryCo
{
// todo - sometimes i want contains instead of equals on strings (client.name, for example...)
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
if (field?.type == QFieldType.DATE_TIME)
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE)
{
defaultOperator = QCriteriaOperator.GREATER_THAN;
}

View File

@ -50,7 +50,7 @@ export function EvaluatedExpression({field, expression}: EvaluatedExpressionProp
return () => clearInterval(interval);
}, []);
return <span style={{fontVariantNumeric: "tabular-nums"}}>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</span>;
return <>{`${evaluateExpression(timeForEvaluations, field, expression)}`}</>;
}
const HOUR_MS = 60 * 60 * 1000;

View File

@ -367,15 +367,16 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
) : (
<Box width={"100%"}>
<DynamicSelect
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
tableName={table.name}
fieldName={field.name}
overrideId={field.name + "-single-" + criteria.id}
key={field.name + "-single-" + criteria.id}
fieldLabel="Value"
initialValue={selectedPossibleValue?.id}
initialDisplayValue={selectedPossibleValue?.label}
inForm={false}
onChange={(value: any) => valueChangeHandler(null, 0, value)}
variant="standard"
useCase="filter"
/>
</Box>
)
@ -400,7 +401,8 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
}
return <Box>
<DynamicSelect
fieldPossibleValueProps={{tableName: table.name, fieldName: field.name, initialDisplayValue: null}}
tableName={table.name}
fieldName={field.name}
overrideId={field.name + "-multi-" + criteria.id}
key={field.name + "-multi-" + criteria.id}
isMultiple
@ -410,7 +412,6 @@ function FilterCriteriaRowValues({operatorOption, criteria, field, table, valueC
inForm={false}
onChange={(value: any) => valueChangeHandler(null, "all", value)}
variant="standard"
useCase="filter"
/>
</Box>;
}

View File

@ -40,17 +40,16 @@ import Snackbar from "@mui/material/Snackbar";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import FormData from "form-data";
import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace";
import {QCancelButton, QSaveButton} from "qqq/components/buttons/DefaultButtons";
import DynamicSelect from "qqq/components/forms/DynamicSelect";
import ScriptDocsForm from "qqq/components/scripts/ScriptDocsForm";
import ScriptTestForm from "qqq/components/scripts/ScriptTestForm";
import Client from "qqq/utils/qqq/Client";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/theme-github";
import React, {useEffect, useReducer, useRef, useState} from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/ext-language_tools";
export interface ScriptEditorProps
@ -78,7 +77,7 @@ function buildInitialFileContentsMap(scriptRevisionRecord: QRecord, scriptTypeFi
}
else
{
let files = scriptRevisionRecord?.associatedRecords?.get("files");
let files = scriptRevisionRecord?.associatedRecords?.get("files")
for (let i = 0; i < scriptTypeFileSchemaList.length; i++)
{
@ -126,21 +125,21 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{
const [closing, setClosing] = useState(false);
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null);
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null);
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null);
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null);
const [apiName, setApiName] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiName") : null)
const [apiNameLabel, setApiNameLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiName") : null)
const [apiVersion, setApiVersion] = useState(scriptRevisionRecord ? scriptRevisionRecord.values.get("apiVersion") : null)
const [apiVersionLabel, setApiVersionLabel] = useState(scriptRevisionRecord ? scriptRevisionRecord.displayValues.get("apiVersion") : null)
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
const [availableFileNames, setAvailableFileNames] = useState(fileNamesFromSchema);
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]]);
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList));
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList));
const [openEditorFileNames, setOpenEditorFileNames] = useState([fileNamesFromSchema[0]])
const [fileContents, setFileContents] = useState(buildInitialFileContentsMap(scriptRevisionRecord, scriptTypeFileSchemaList))
const [fileTypes, setFileTypes] = useState(buildFileTypeMap(scriptTypeFileSchemaList))
console.log(`file types: ${JSON.stringify(fileTypes)}`);
const [commitMessage, setCommitMessage] = useState("");
const [commitMessage, setCommitMessage] = useState("")
const [openTool, setOpenTool] = useState(null);
const [errorAlert, setErrorAlert] = useState("");
const [errorAlert, setErrorAlert] = useState("")
const [promptForCommitMessageOpen, setPromptForCommitMessageOpen] = useState(false);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const ref = useRef();
@ -242,7 +241,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
// need this to make Ace recognize new height.
setTimeout(() =>
{
window.dispatchEvent(new Event("resize"));
window.dispatchEvent(new Event("resize"))
}, 100);
};
@ -250,7 +249,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{
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;
}
@ -279,7 +278,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
}
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"));
const fileNamesFromSchema = scriptTypeFileSchemaList.map((schemaRecord) => schemaRecord.values.get("name"))
formData.append("fileNames", fileNamesFromSchema.join(","));
for (let fileName in fileContents)
@ -300,8 +299,8 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
if (processResult instanceof QJobError)
{
const jobError = processResult as QJobError;
setErrorAlert(jobError.userFacingError ?? jobError.error);
const jobError = processResult as QJobError
setErrorAlert(jobError.userFacingError ?? jobError.error)
setClosing(false);
return;
}
@ -311,28 +310,28 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
catch(e)
{
// @ts-ignore
setErrorAlert(e.message ?? "Unexpected error saving script");
setErrorAlert(e.message ?? "Unexpected error saving script")
setClosing(false);
}
})();
};
}
const cancelClicked = () =>
{
setClosing(true);
closeCallback(null, "cancelled");
};
}
const updateCode = (value: string, event: any, index: number) =>
{
fileContents[openEditorFileNames[index]] = value;
forceUpdate();
};
}
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
{
setCommitMessage(event.target.value);
};
}
const closePromptForCommitMessage = (wasSaveClicked: boolean, message?: string) =>
{
@ -340,14 +339,14 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
if(wasSaveClicked)
{
setCommitMessage(message);
setCommitMessage(message)
saveClicked(message);
}
else
{
setClosing(false);
}
};
}
const changeApiName = (apiNamePossibleValue?: QPossibleValue) =>
{
@ -359,7 +358,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{
setApiName(null);
}
};
}
const changeApiVersion = (apiVersionPossibleValue?: QPossibleValue) =>
{
@ -371,33 +370,33 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{
setApiVersion(null);
}
};
}
const handleSelectingFile = (event: SelectChangeEvent, index: number) =>
{
openEditorFileNames[index] = event.target.value;
openEditorFileNames[index] = event.target.value
setOpenEditorFileNames(openEditorFileNames);
forceUpdate();
};
}
const splitEditorClicked = () =>
{
openEditorFileNames.push(availableFileNames[0]);
openEditorFileNames.push(availableFileNames[0])
setOpenEditorFileNames(openEditorFileNames);
forceUpdate();
};
}
const closeEditorClicked = (index: number) =>
{
openEditorFileNames.splice(index, 1);
openEditorFileNames.splice(index, 1)
setOpenEditorFileNames(openEditorFileNames);
forceUpdate();
};
}
const computeEditorWidth = (): string =>
{
return (100 / openEditorFileNames.length) + "%";
};
return (100 / openEditorFileNames.length) + "%"
}
return (
<Box className="scriptEditor" sx={{position: "absolute", overflowY: "auto", height: "100%", width: "100%"}} p={6}>
@ -409,7 +408,7 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
{
return;
}
setErrorAlert("");
setErrorAlert("")
}} anchorOrigin={{vertical: "top", horizontal: "center"}}>
<Alert color="error" onClose={() => setErrorAlert("")}>
{errorAlert}
@ -441,10 +440,10 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
<Box sx={{height: openTool ? "45%" : "100%"}}>
<Grid container alignItems="flex-end">
<Box maxWidth={"50%"} minWidth={300}>
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiName", initialDisplayValue: apiNameLabel}} initialValue={apiName} fieldLabel={"API Name *"} inForm={false} onChange={changeApiName} useCase="form" />
<DynamicSelect fieldName={"apiName"} initialValue={apiName} initialDisplayValue={apiNameLabel} fieldLabel={"API Name *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiName} />
</Box>
<Box maxWidth={"50%"} minWidth={300} pl={2}>
<DynamicSelect fieldPossibleValueProps={{tableName: "scriptRevision", fieldName: "apiVersion", initialDisplayValue: apiVersionLabel}} initialValue={apiVersion} fieldLabel={"API Version *"} inForm={false} onChange={changeApiVersion} useCase="form" />
<DynamicSelect fieldName={"apiVersion"} initialValue={apiVersion} initialDisplayValue={apiVersionLabel} fieldLabel={"API Version *"} tableName={"scriptRevision"} inForm={false} onChange={changeApiVersion} />
</Box>
</Grid>
<Box display="flex" sx={{height: "100%"}}>
@ -536,12 +535,12 @@ function ScriptEditor({title, scriptId, scriptRevisionRecord, closeCallback, tab
function CommitMessagePrompt(props: {isOpen: boolean, closeHandler: (wasSaveClicked: boolean, message?: string) => void})
{
const [commitMessage, setCommitMessage] = useState("No commit message given");
const [commitMessage, setCommitMessage] = useState("No commit message given")
const updateCommitMessage = (event: React.ChangeEvent<HTMLInputElement>) =>
{
setCommitMessage(event.target.value);
};
}
const keyPressHandler = (e: React.KeyboardEvent<HTMLDivElement>) =>
{
@ -549,7 +548,7 @@ function CommitMessagePrompt(props: { isOpen: boolean, closeHandler: (wasSaveCli
{
props.closeHandler(true, commitMessage);
}
};
}
return (
<Dialog
@ -583,7 +582,7 @@ function CommitMessagePrompt(props: { isOpen: boolean, closeHandler: (wasSaveCli
<QSaveButton label="Save" onClickHandler={() => props.closeHandler(true, commitMessage)} disabled={false}/>
</DialogActions>
</Dialog>
);
)
}
export default ScriptEditor;

View File

@ -391,12 +391,12 @@ export default function ShareModal({open, onClose, tableMetaData, record}: Share
<Box display="flex" flexDirection="row" alignItems="center">
<Box width="550px" pr={2} mb={-1.5}>
<DynamicSelect
fieldPossibleValueProps={{possibleValueSourceName: shareableTableMetaData.audiencePossibleValueSourceName, initialDisplayValue: selectedAudienceOption?.label}}
possibleValueSourceName={shareableTableMetaData.audiencePossibleValueSourceName}
fieldLabel="User or Group" // todo should come from shareableTableMetaData
initialValue={selectedAudienceOption?.id}
initialDisplayValue={selectedAudienceOption?.label}
inForm={false}
onChange={handleAudienceChange}
useCase="form"
/>
</Box>
{/*

View File

@ -22,25 +22,16 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Box, Skeleton} from "@mui/material";
import Card from "@mui/material/Card";
import Modal from "@mui/material/Modal";
import parse from "html-react-parser";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import WidgetBlock from "qqq/components/widgets/WidgetBlock";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import React, {useEffect, useState} from "react";
import React from "react";
export interface CompositeData
interface CompositeData
{
blockId: string;
blocks: BlockData[];
styleOverrides?: any;
layout?: string;
overlayHtml?: string;
overlayStyleOverrides?: any;
modalMode: string;
styles?: any;
}
@ -48,15 +39,13 @@ interface CompositeWidgetProps
{
widgetMetaData: QWidgetMetaData;
data: CompositeData;
actionCallback?: (blockData: BlockData, eventValues?: { [name: string]: any }) => boolean;
values?: { [key: string]: any };
}
/*******************************************************************************
** Widget which is a list of Blocks.
*******************************************************************************/
export default function CompositeWidget({widgetMetaData, data, actionCallback, values}: CompositeWidgetProps): JSX.Element
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element
{
if (!data || !data.blocks)
{
@ -82,12 +71,6 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback, v
boxStyle.flexWrap = "wrap";
boxStyle.gap = "0.5rem";
}
else if (layout == "FLEX_ROW")
{
boxStyle.display = "flex";
boxStyle.flexDirection = "row";
boxStyle.gap = "0.5rem";
}
else if (layout == "FLEX_ROW_SPACE_BETWEEN")
{
boxStyle.display = "flex";
@ -95,14 +78,6 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback, v
boxStyle.justifyContent = "space-between";
boxStyle.gap = "0.25rem";
}
else if (layout == "FLEX_ROW_CENTER")
{
boxStyle.display = "flex";
boxStyle.flexDirection = "row";
boxStyle.justifyContent = "center";
boxStyle.gap = "0.25rem";
boxStyle.flexWrap = "wrap";
}
else if (layout == "TABLE_SUB_ROW_DETAILS")
{
boxStyle.display = "flex";
@ -122,96 +97,20 @@ export default function CompositeWidget({widgetMetaData, data, actionCallback, v
boxStyle.borderRadius = "0.5rem";
boxStyle.background = "#FFFFFF";
}
if (data?.styleOverrides)
{
boxStyle = {...boxStyle, ...data.styleOverrides};
}
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">
return (<Box sx={boxStyle} className="compositeWidget">
{
data.blocks.map((block: BlockData, index) => (
<React.Fragment key={index}>
<WidgetBlock widgetMetaData={widgetMetaData} block={block} actionCallback={actionCallback} values={values} />
<WidgetBlock widgetMetaData={widgetMetaData} block={block} />
</React.Fragment>
))
}
</Box>
</>
);
if (data.modalMode)
{
const [isModalOpen, setIsModalOpen] = useState(values && (values[data.blockId] == true));
/***************************************************************************
**
***************************************************************************/
const controlCallback = (newValue: boolean) =>
{
setIsModalOpen(newValue);
};
/***************************************************************************
**
***************************************************************************/
const modalOnClose = (event: object, reason: string) =>
{
values[data.blockId] = false;
setIsModalOpen(false);
actionCallback({blockTypeName: "BUTTON", values: {}}, {controlCode: `hideModal:${data.blockId}`});
};
//////////////////////////////////////////////////////////////////////////////////////////
// register the control-callback function - so when buttons are clicked, we can be told //
//////////////////////////////////////////////////////////////////////////////////////////
useEffect(() =>
{
if (actionCallback)
{
actionCallback(null, {
registerControlCallbackName: data.blockId,
registerControlCallbackFunction: controlCallback
});
}
}, []);
return (<Modal open={isModalOpen} onClose={modalOnClose}>
<Box sx={{position: "absolute", overflowY: "auto", maxHeight: "100%", width: "100%"}}>
<Card sx={{my: 5, mx: "auto", p: "1rem", maxWidth: "1024px"}}>
{content}
</Card>
</Box>
</Modal>);
}
else
{
return content;
}
</Box>);
}

View File

@ -18,18 +18,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {Alert, Skeleton} from "@mui/material";
import Box from "@mui/material/Box";
import Grid from "@mui/material/Grid";
import Modal from "@mui/material/Modal";
import Tab from "@mui/material/Tab";
import Tabs from "@mui/material/Tabs";
import parse from "html-react-parser";
import QContext from "QContext";
import EntityForm from "qqq/components/forms/EntityForm";
import MDTypography from "qqq/components/legacy/MDTypography";
import TabPanel from "qqq/components/misc/TabPanel";
import BarChart from "qqq/components/widgets/charts/barchart/BarChart";
@ -46,10 +43,11 @@ import FieldValueListWidget from "qqq/components/widgets/misc/FieldValueListWidg
import FilterAndColumnsSetupWidget from "qqq/components/widgets/misc/FilterAndColumnsSetupWidget";
import PivotTableSetupWidget from "qqq/components/widgets/misc/PivotTableSetupWidget";
import QuickSightChart from "qqq/components/widgets/misc/QuickSightChart";
import RecordGridWidget, {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import RecordGridWidget from "qqq/components/widgets/misc/RecordGridWidget";
import ScriptViewer from "qqq/components/widgets/misc/ScriptViewer";
import StepperCard from "qqq/components/widgets/misc/StepperCard";
import USMapWidget from "qqq/components/widgets/misc/USMapWidget";
import WorkflowViewer from "qqq/components/widgets/misc/WorkflowViewer";
import ParentWidget from "qqq/components/widgets/ParentWidget";
import MultiStatisticsCard from "qqq/components/widgets/statistics/MultiStatisticsCard";
import StatisticsCard from "qqq/components/widgets/statistics/StatisticsCard";
@ -74,9 +72,6 @@ interface Props
childUrlParams?: string;
parentWidgetMetaData?: QWidgetMetaData;
wrapWidgetsInTabPanels: boolean;
actionCallback?: (data: any, eventValues?: { [name: string]: any }) => boolean;
initialWidgetDataList: any[];
values?: { [key: string]: any };
}
DashboardWidgets.defaultProps = {
@ -88,14 +83,11 @@ DashboardWidgets.defaultProps = {
childUrlParams: "",
parentWidgetMetaData: null,
wrapWidgetsInTabPanels: false,
actionCallback: null,
initialWidgetDataList: null,
values: {}
};
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels, actionCallback, initialWidgetDataList, values}: Props): JSX.Element
function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, record, omitWrappingGridContainer, areChildren, childUrlParams, parentWidgetMetaData, wrapWidgetsInTabPanels}: Props): JSX.Element
{
const [widgetData, setWidgetData] = useState(initialWidgetDataList == null ? [] as any[] : initialWidgetDataList);
const [widgetData, setWidgetData] = useState([] as any[]);
const [widgetCounter, setWidgetCounter] = useState(0);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
@ -103,12 +95,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
const [haveLoadedParams, setHaveLoadedParams] = useState(false);
const {accentColor} = useContext(QContext);
/////////////////////////
// modal form controls //
/////////////////////////
const [showEditChildForm, setShowEditChildForm] = useState(null as any);
const [modalTable, setModalTable] = useState(null as QTableMetaData);
let initialSelectedTab = 0;
let selectedTabKey: string = null;
if (parentWidgetMetaData && wrapWidgetsInTabPanels)
@ -129,15 +115,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
useEffect(() =>
{
if (initialWidgetDataList && initialWidgetDataList.length > 0)
{
// todo actually, should this check each element of the array, down in the loop? yeah, when we need to, do it that way.
console.log("We already have initial widget data, so not fetching from backend.");
return;
}
setWidgetData([]);
for (let i = 0; i < widgetMetaDataList.length; i++)
{
const widgetMetaData = widgetMetaDataList[i];
@ -174,7 +152,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
const reloadWidget = async (index: number, data: string) =>
{
await (async () =>
(async () =>
{
const urlParams = getQueryParams(widgetMetaDataList[index], data);
setCurrentUrlParams(urlParams);
@ -293,148 +271,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
return (rs);
}
/*******************************************************************************
**
*******************************************************************************/
const closeEditChildForm = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
setShowEditChildForm(null);
};
/*******************************************************************************
**
*******************************************************************************/
function deleteChildRecord(name: string, widgetIndex: number, rowIndex: number)
{
updateChildRecordList(name, "delete", rowIndex);
actionCallback(widgetData[widgetIndex]);
};
/*******************************************************************************
**
*******************************************************************************/
function openEditChildRecord(name: string, widgetData: any, rowIndex: number)
{
let defaultValues = widgetData.queryOutput.records[rowIndex].values;
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
if (!disabledFields)
{
disabledFields = widgetData.defaultValuesForNewChildRecords;
}
doOpenEditChildForm(name, widgetData.childTableMetaData, rowIndex, defaultValues, disabledFields);
}
/*******************************************************************************
**
*******************************************************************************/
function openAddChildRecord(name: string, widgetData: any)
{
let disabledFields = widgetData.disabledFieldsForNewChildRecords;
if (!disabledFields)
{
disabledFields = widgetData.defaultValuesForNewChildRecords;
}
doOpenEditChildForm(name, widgetData.childTableMetaData, null, null, disabledFields);
}
/*******************************************************************************
**
*******************************************************************************/
function doOpenEditChildForm(widgetName: string, table: QTableMetaData, rowIndex: number, defaultValues: any, disabledFields: any)
{
const showEditChildForm: any = {};
showEditChildForm.widgetName = widgetName;
showEditChildForm.table = table;
showEditChildForm.rowIndex = rowIndex;
showEditChildForm.defaultValues = defaultValues;
showEditChildForm.disabledFields = disabledFields;
setShowEditChildForm(showEditChildForm);
}
/*******************************************************************************
**
*******************************************************************************/
function submitEditChildForm(values: any)
{
updateChildRecordList(showEditChildForm.widgetName, showEditChildForm.rowIndex == null ? "insert" : "edit", showEditChildForm.rowIndex, values);
let widgetIndex = determineChildRecordListIndex(showEditChildForm.widgetName);
actionCallback(widgetData[widgetIndex]);
}
/*******************************************************************************
**
*******************************************************************************/
function determineChildRecordListIndex(widgetName: string): number
{
let widgetIndex = -1;
for (var i = 0; i < widgetMetaDataList.length; i++)
{
const widgetMetaData = widgetMetaDataList[i];
if (widgetMetaData.name == widgetName)
{
widgetIndex = i;
break;
}
}
return (widgetIndex);
}
/*******************************************************************************
**
*******************************************************************************/
function updateChildRecordList(widgetName: string, action: "insert" | "edit" | "delete", rowIndex?: number, values?: any)
{
////////////////////////////////////////////////
// find the correct child record widget index //
////////////////////////////////////////////////
let widgetIndex = determineChildRecordListIndex(widgetName);
if (!widgetData[widgetIndex].queryOutput.records)
{
widgetData[widgetIndex].queryOutput.records = [];
}
const newChildListWidgetData: ChildRecordListData = widgetData[widgetIndex];
if (!newChildListWidgetData.queryOutput.records)
{
newChildListWidgetData.queryOutput.records = [];
}
switch (action)
{
case "insert":
newChildListWidgetData.queryOutput.records.push({values: values});
break;
case "edit":
newChildListWidgetData.queryOutput.records[rowIndex] = {values: values};
break;
case "delete":
newChildListWidgetData.queryOutput.records.splice(rowIndex, 1);
break;
}
newChildListWidgetData.totalRows = newChildListWidgetData.queryOutput.records.length;
widgetData[widgetIndex] = newChildListWidgetData;
setWidgetData(widgetData);
setShowEditChildForm(null);
}
const renderWidget = (widgetMetaData: QWidgetMetaData, i: number): JSX.Element =>
{
const labelAdditionalComponentsRight: LabelComponent[] = [];
@ -474,7 +310,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
)
}
{
widgetMetaData.type === "alert" && widgetData[i]?.html && !widgetData[i]?.hideWidget && (
widgetMetaData.type === "alert" && widgetData[i]?.html && (
<Widget
omitPadding={true}
widgetMetaData={widgetMetaData}
@ -484,16 +320,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
>
<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>
<Alert severity={widgetData[i]?.alertType?.toLowerCase()}>{parse(widgetData[i]?.html)}</Alert>
</Widget>
)
}
@ -675,7 +502,9 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
}
{
widgetMetaData.type === "divider" && (
<Box>
<DividerWidget />
</Box>
)
}
{
@ -709,12 +538,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
widgetMetaData.type === "childRecordList" && (
widgetData && widgetData[i] &&
<RecordGridWidget
disableRowClick={widgetData[i]?.disableRowClick}
allowRecordEdit={widgetData[i]?.allowRecordEdit}
allowRecordDelete={widgetData[i]?.allowRecordDelete}
deleteRecordCallback={(rowIndex) => deleteChildRecord(widgetMetaData.name, i, rowIndex)}
editRecordCallback={(rowIndex) => openEditChildRecord(widgetMetaData.name, widgetData[i], rowIndex)}
addNewRecordCallback={() => openAddChildRecord(widgetMetaData.name, widgetData[i])}
widgetMetaData={widgetMetaData}
data={widgetData[i]}
/>
@ -741,7 +564,7 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelAdditionalComponentsLeft={labelAdditionalComponentsLeft}
>
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} actionCallback={actionCallback} values={values} />
<CompositeWidget widgetMetaData={widgetMetaData} data={widgetData[i]} />
</Widget>
)
}
@ -759,6 +582,14 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
</Widget>
)
}
{
widgetMetaData.type === "workflow" && (
widgetData && widgetData[i] && widgetData[i].queryParams &&
<Widget widgetMetaData={widgetMetaData}>
<WorkflowViewer workflowId={widgetData[i].queryParams.id} />
</Widget>
)
}
{
widgetMetaData.type === "dataBagViewer" && (
widgetData && widgetData[i] && widgetData[i].queryParams &&
@ -816,28 +647,8 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
if (!omitWrappingGridContainer)
{
const gridProps: { [key: string]: any } = {};
for (let size of ["xs", "sm", "md", "lg", "xl", "xxl"])
{
const key = `gridCols:sizeClass:${size}`;
if (widgetMetaData?.defaultValues?.has(key))
{
gridProps[size] = widgetMetaData?.defaultValues.get(key);
}
}
if (!gridProps["xxl"])
{
gridProps["xxl"] = widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12;
}
if (!gridProps["xs"])
{
gridProps["xs"] = 12;
}
renderedWidget = (<Grid id={widgetMetaData.name} item {...gridProps} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
// @ts-ignore
renderedWidget = (<Grid id={widgetMetaData.name} item xxl={widgetMetaData.gridColumns ? widgetMetaData.gridColumns : 12} xs={12} sx={{display: "flex", alignItems: "stretch", scrollMarginTop: "100px"}}>
{renderedWidget}
</Grid>);
}
@ -888,22 +699,6 @@ function DashboardWidgets({widgetMetaDataList, tableName, entityPrimaryKey, reco
</Grid>
)
}
{
showEditChildForm &&
<Modal open={showEditChildForm as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditChildForm}
table={showEditChildForm.table}
defaultValues={showEditChildForm.defaultValues}
disabledFields={showEditChildForm.disabledFields}
onSubmitCallback={submitEditChildForm}
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
/>
</div>
</Modal>
}
</>
) : null
);

View File

@ -22,9 +22,6 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Alert, Skeleton} from "@mui/material";
import ButtonBlock from "qqq/components/widgets/blocks/ButtonBlock";
import AudioBlock from "qqq/components/widgets/blocks/AudioBlock";
import InputFieldBlock from "qqq/components/widgets/blocks/InputFieldBlock";
import React from "react";
import BigNumberBlock from "qqq/components/widgets/blocks/BigNumberBlock";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
@ -35,22 +32,19 @@ import TableSubRowDetailRowBlock from "qqq/components/widgets/blocks/TableSubRow
import TextBlock from "qqq/components/widgets/blocks/TextBlock";
import UpOrDownNumberBlock from "qqq/components/widgets/blocks/UpOrDownNumberBlock";
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
import ImageBlock from "./blocks/ImageBlock";
interface WidgetBlockProps
{
widgetMetaData: QWidgetMetaData;
block: BlockData;
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
values?: { [key: string]: any };
}
/*******************************************************************************
** Component to render a single Block in the widget framework!
*******************************************************************************/
export default function WidgetBlock({widgetMetaData, block, actionCallback, values}: WidgetBlockProps): JSX.Element
export default function WidgetBlock({widgetMetaData, block}: WidgetBlockProps): JSX.Element
{
if(!block)
{
@ -70,7 +64,7 @@ export default function WidgetBlock({widgetMetaData, block, actionCallback, valu
if(block.blockTypeName == "COMPOSITE")
{
// @ts-ignore - special case for composite type block...
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} values={values} />);
return (<CompositeWidget widgetMetaData={widgetMetaData} data={block} />);
}
switch(block.blockTypeName)
@ -89,14 +83,6 @@ export default function WidgetBlock({widgetMetaData, block, actionCallback, valu
return (<DividerBlock widgetMetaData={widgetMetaData} data={block} />);
case "BIG_NUMBER":
return (<BigNumberBlock widgetMetaData={widgetMetaData} data={block} />);
case "INPUT_FIELD":
return (<InputFieldBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
case "BUTTON":
return (<ButtonBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
case "AUDIO":
return (<AudioBlock widgetMetaData={widgetMetaData} data={block} />);
case "IMAGE":
return (<ImageBlock widgetMetaData={widgetMetaData} data={block} actionCallback={actionCallback} />);
default:
return (<Alert sx={{m: "0.5rem"}} color="warning">Unsupported block type: {block.blockTypeName}</Alert>)
}

View File

@ -1,40 +0,0 @@
/*
* 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>
);
}

View File

@ -21,19 +21,18 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Box, Tooltip} from "@mui/material";
import {Tooltip} from "@mui/material";
import React, {ReactElement, useContext} from "react";
import {Link} from "react-router-dom";
import QContext from "QContext";
import HelpContent, {hasHelpContent} from "qqq/components/misc/HelpContent";
import {BlockData, BlockLink, BlockTooltip} from "qqq/components/widgets/blocks/BlockModels";
import CompositeWidget from "qqq/components/widgets/CompositeWidget";
import React, {ReactElement, useContext} from "react";
import {Link} from "react-router-dom";
interface BlockElementWrapperProps
{
data: BlockData;
metaData: QWidgetMetaData;
slot: string;
slot: string
linkProps?: any;
children: ReactElement;
}
@ -70,7 +69,7 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
if(!tooltip)
{
const helpRoles = ["ALL_SCREENS"];
const helpRoles = ["ALL_SCREENS"]
///////////////////////////////////////////////////////////////////////////////////////////////
// the full keys in the helpContent table will look like: //
@ -84,36 +83,23 @@ export default function BlockElementWrapper({data, metaData, slot, linkProps, ch
if(showHelp)
{
const formattedHelpContent = <HelpContent helpContents={metaData?.helpContent?.get(key)} roles={helpRoles} helpContentKey={`widget:${metaData?.name};slot:${key}`} />;
tooltip = {title: formattedHelpContent, placement: "bottom"};
tooltip = {title: formattedHelpContent, placement: "bottom"}
}
}
let rs = children;
if (link && link.href)
if(link)
{
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)
{
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom";
let placement = tooltip.placement ? tooltip.placement.toLowerCase() : "bottom"
// @ts-ignore - placement possible values
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>;
}
rs = <Tooltip title={tooltip.title} placement={placement}>{rs}</Tooltip>
}
return (rs);

View File

@ -20,7 +20,6 @@
*/
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
export interface BlockData
@ -35,14 +34,11 @@ export interface BlockData
values: any;
styles?: any;
conditional?: string;
}
export interface BlockTooltip
{
blockData?: CompositeData;
title: string | JSX.Element;
placement: string;
}
@ -59,6 +55,5 @@ export interface StandardBlockComponentProps
{
widgetMetaData: QWidgetMetaData;
data: BlockData;
actionCallback?: (blockData: BlockData, eventValues?: {[name: string]: any}) => boolean;
}

View File

@ -1,86 +0,0 @@
/*
* 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>
);
}

View File

@ -1,59 +0,0 @@
/*
* 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>
);
}

View File

@ -1,139 +0,0 @@
/*
* 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>
);
}

View File

@ -19,12 +19,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import React from "react";
/*******************************************************************************
** Block that renders ... just some text.
@ -33,132 +29,9 @@ import React from "react";
*******************************************************************************/
export default function TextBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
{
let color = "rgba(0, 0, 0, 0.87)";
if (data.styles?.color)
{
color = ProcessWidgetBlockUtils.processColorFromStyleMap(data.styles.color);
}
let boxStyle = {};
if (data.styles?.format == "alert")
{
boxStyle =
{
border: `1px solid ${color}`,
background: `${color}40`,
padding: "0.5rem",
borderRadius: "0.5rem",
};
}
else if (data.styles?.format == "banner")
{
boxStyle =
{
background: `${color}40`,
padding: "0.5rem",
};
}
let fontSize = "1rem";
if (data.styles?.size)
{
switch (data.styles.size.toLowerCase())
{
case "largest":
fontSize = "3rem";
break;
case "headline":
fontSize = "2rem";
break;
case "title":
fontSize = "1.5rem";
break;
case "body":
fontSize = "1rem";
break;
case "smallest":
fontSize = "0.75rem";
break;
default:
{
if (data.styles.size.match(/^\d+$/))
{
fontSize = `${data.styles.size}px`;
}
else
{
fontSize = "1rem";
}
}
}
}
let fontWeight = "400";
if (data.styles?.weight)
{
switch (data.styles.weight.toLowerCase())
{
case "thin":
case "100":
fontWeight = "100";
break;
case "extralight":
case "200":
fontWeight = "200";
break;
case "light":
case "300":
fontWeight = "300";
break;
case "normal":
case "400":
fontWeight = "400";
break;
case "medium":
case "500":
fontWeight = "500";
break;
case "semibold":
case "600":
fontWeight = "600";
break;
case "bold":
case "700":
fontWeight = "700";
break;
case "extrabold":
case "800":
fontWeight = "800";
break;
case "black":
case "900":
fontWeight = "900";
break;
}
}
const text = data.values.interpolatedText ?? data.values.text;
const lines = text.split("\n");
const startIcon = data.values.startIcon?.name ? <Icon>{data.values.startIcon.name}</Icon> : null;
const endIcon = data.values.endIcon?.name ? <Icon>{data.values.endIcon.name}</Icon> : null;
return (
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<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>
<span style={{fontSize: "1.000rem"}}>{data.values.text}</span>
</BlockElementWrapper>
);
}

View File

@ -58,7 +58,7 @@ export default function UpOrDownNumberBlock({widgetMetaData, data}: StandardBloc
return (
<>
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline", marginLeft: "auto"}}>
<div style={{display: "flex", flexDirection: data.styles.isStacked ? "column" : "row", alignItems: data.styles.isStacked ? "flex-end" : "baseline"}}>
<div style={{display: "flex", alignItems: "baseline", fontWeight: 700, fontSize: ".875rem"}}>
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="number">

View File

@ -50,7 +50,6 @@ import DeveloperModeUtils from "qqq/utils/DeveloperModeUtils";
import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-java";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-json";

View File

@ -19,16 +19,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
function DividerWidget(): JSX.Element
{
return (
<Box pl={3} pt={3} pb={3} width="100%">
<Divider sx={{width: "100%", height: "1px", background: "grey"}} />
</Box>
<Divider sx={{padding: "1px", background: "red"}}/>
);
}

View File

@ -87,7 +87,6 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
{
const [modalOpen, setModalOpen] = useState(false);
const [hideColumns, setHideColumns] = useState(widgetData?.hideColumns);
const [hidePreview, setHidePreview] = useState(widgetData?.hidePreview);
const [tableMetaData, setTableMetaData] = useState(null as QTableMetaData);
const [alertContent, setAlertContent] = useState(null as string);
@ -273,7 +272,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
/*******************************************************************************
**
*******************************************************************************/
function mayShowQuery(): boolean
function mayShowQueryPreview(): boolean
{
if (tableMetaData)
{
@ -289,7 +288,7 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
/*******************************************************************************
**
*******************************************************************************/
function mayShowColumns(): boolean
function mayShowColumnsPreview(): boolean
{
if (tableMetaData)
{
@ -357,14 +356,14 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
<Box pt="0.5rem">
<Box display="flex" justifyContent="space-between" alignItems="center">
<h5>Query Filter</h5>
<Box fontSize="0.75rem" fontWeight="700">{mayShowQuery() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
<Box fontSize="0.75rem" fontWeight="700">{mayShowQueryPreview() && getCurrentSortIndicator(frontendQueryFilter, tableMetaData, null)}</Box>
</Box>
{
mayShowQuery() &&
mayShowQueryPreview() &&
<AdvancedQueryPreview tableMetaData={tableMetaData} queryFilter={frontendQueryFilter} isEditable={false} isQueryTooComplex={frontendQueryFilter.subFilters?.length > 0} removeCriteriaByIndexCallback={null} />
}
{
!mayShowQuery() &&
!mayShowQueryPreview() &&
<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 &&
@ -383,11 +382,11 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
<h5>Columns</h5>
<Box display="flex" flexWrap="wrap" fontSize="1rem">
{
mayShowColumns() && columns &&
mayShowColumnsPreview() &&
columns.columns.map((column, i) => <React.Fragment key={`column-${i}`}>{renderColumn(column)}</React.Fragment>)
}
{
!mayShowColumns() &&
!mayShowColumnsPreview() &&
<Box width="100%" sx={{fontSize: "1rem", background: "#FFFFFF"}} minHeight={"2.375rem"} p={"0.5rem"} pb={"0.125rem"}>
{
isEditable &&
@ -403,21 +402,6 @@ export default function FilterAndColumnsSetupWidget({isEditable, widgetMetaData,
</Box>
</Box>
)}
{!hidePreview && !isEditable && frontendQueryFilter && tableMetaData && (
<Box pt="1rem">
<h5>Preview</h5>
<RecordQuery
allowVariables={widgetData?.allowVariables}
ref={recordQueryRef}
table={tableMetaData}
isPreview={true}
usage="reportSetup"
isModal={true}
initialQueryFilter={frontendQueryFilter}
initialColumns={columns}
/>
</Box>
)}
{
modalOpen &&
<Modal open={modalOpen} onClose={(event, reason) => closeEditor(event, reason)}>

View File

@ -39,15 +39,15 @@ import {Link, useNavigate} from "react-router-dom";
export interface ChildRecordListData extends WidgetData
{
title?: string;
queryOutput?: { records: { values: any }[] };
childTableMetaData?: QTableMetaData;
tablePath?: string;
viewAllLink?: string;
totalRows?: number;
canAddChildRecord?: boolean;
defaultValuesForNewChildRecords?: { [fieldName: string]: any };
disabledFieldsForNewChildRecords?: { [fieldName: string]: any };
title: string;
queryOutput: { records: { values: any }[] };
childTableMetaData: QTableMetaData;
tablePath: string;
viewAllLink: string;
totalRows: number;
canAddChildRecord: boolean;
defaultValuesForNewChildRecords: { [fieldName: string]: any };
disabledFieldsForNewChildRecords: { [fieldName: string]: any };
}
interface Props
@ -176,7 +176,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
setCsv(csv);
setFileName(fileName);
}
}, [JSON.stringify(data?.queryOutput)]);
}, [data]);
///////////////////
// view all link //
@ -304,7 +304,7 @@ function RecordGridWidget({widgetMetaData, data, addNewRecordCallback, disableRo
labelAdditionalComponentsRight={labelAdditionalComponentsRight}
labelBoxAdditionalSx={{position: "relative", top: "-0.375rem"}}
>
<Box>
<Box mx={-3} mb={-3}>
<Box>
<DataGridPro
autoHeight

View File

@ -0,0 +1,383 @@
/*
* 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>
);
}

View File

@ -19,12 +19,15 @@
*/
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Box, tooltipClasses, TooltipProps} from "@mui/material";
import {tooltipClasses, TooltipProps} from "@mui/material";
import Autocomplete from "@mui/material/Autocomplete";
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import {styled} from "@mui/material/styles";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableContainer from "@mui/material/TableContainer";
import TableRow from "@mui/material/TableRow";
import Tooltip from "@mui/material/Tooltip";
import parse from "html-react-parser";
import colors from "qqq/assets/theme/base/colors";
@ -163,7 +166,7 @@ function DataTable({
})}
>
{/* float this icon to keep it "out of the flow" - in other words, to keep it from making the row taller than it otherwise would be... */}
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_left"}</Icon>
<Icon fontSize="medium" sx={{float: "left"}}>{row.isExpanded ? "expand_less" : "chevron_right"}</Icon>
</span>
) : null,
},
@ -309,7 +312,7 @@ function DataTable({
{
boxStyle = isFooter
? {borderTop: `0.0625rem solid ${colors.grayLines.main};`, backgroundColor: "#EEEEEE"}
: {height: fixedHeight ? `${fixedHeight}px` : "auto", flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
: {flexGrow: 1, overflowY: "scroll", scrollbarGutter: "stable", marginBottom: "-1px"};
}
let innerBoxStyle = {};
@ -318,18 +321,16 @@ function DataTable({
innerBoxStyle = {overflowY: "auto", scrollbarGutter: "stable"};
}
///////////////////////////////////////////////////////////////////////////////////
// note - at one point, we had the table's sx including: whiteSpace: "nowrap"... //
///////////////////////////////////////////////////////////////////////////////////
return <Box sx={boxStyle}><Box sx={innerBoxStyle}>
<Table {...getTableProps()} component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: gridTemplateColumns}}>
<Table {...getTableProps()}>
{
includeHead && (
headerGroups.map((headerGroup: any, i: number) => (
headerGroup.headers.map((column: any) => (
<Box component="thead" sx={{position: "sticky", top: 0, background: "white", zIndex: 10}}>
{headerGroups.map((headerGroup: any, i: number) => (
<TableRow key={i} {...headerGroup.getHeaderGroupProps()} sx={{display: "grid", alignItems: "flex-end", gridTemplateColumns: gridTemplateColumns}}>
{headerGroup.headers.map((column: any) => (
column.type !== "hidden" && (
<DataTableHeadCell
sx={{position: "sticky", top: 0, background: "white", zIndex: 10, alignItems: "flex-end"}}
key={i++}
{...column.getHeaderProps(isSorted && column.getSortByToggleProps())}
align={column.align ? column.align : "left"}
@ -339,10 +340,13 @@ function DataTable({
{column.render("header")}
</DataTableHeadCell>
)
))
))
))}
</TableRow>
))}
</Box>
)
}
<TableBody {...getTableBodyProps()}>
{rows.map((row: any, key: any) =>
{
prepareRow(row);
@ -380,11 +384,11 @@ function DataTable({
}
return (
row.cells.map((cell: any) => (
<TableRow sx={{verticalAlign: "top", display: "grid", gridTemplateColumns: gridTemplateColumns, background: background}} key={key} {...row.getRowProps()}>
{row.cells.map((cell: any) => (
cell.column.type !== "hidden" && (
<DataTableBodyCell
key={key}
sx={{verticalAlign: "top", background: background}}
noBorder={noEndBorder || overrideNoEndBorder || row.isExpanded}
depth={row.depth}
align={cell.column.align ? cell.column.align : "left"}
@ -442,15 +446,18 @@ function DataTable({
}
</DataTableBodyCell>
)
))
))}
</TableRow>
);
})}
</TableBody>
</Table>
</Box></Box>;
}
return (
<TableContainer sx={{boxShadow: "none", height: (fixedHeight && !fixedStickyLastRow) ? `${fixedHeight}px` : "auto"}}>
<TableContainer sx={{boxShadow: "none", height: fixedHeight ? `${fixedHeight}px` : "auto"}}>
{entriesPerPage && ((hidePaginationDropdown !== undefined && !hidePaginationDropdown) || canSearch) ? (
<Box display="flex" justifyContent="space-between" alignItems="center" p={3}>
{entriesPerPage && (hidePaginationDropdown === undefined || !hidePaginationDropdown) && (

View File

@ -1,96 +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 {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;

View File

@ -93,25 +93,41 @@ function TableCard({noRowsFoundHTML, data, rowsPerPage, hidePaginationDropdown,
/>
: noRowsFoundHTML ?
<Box p={3} pt={0} pb={3} sx={{textAlign: "center"}}>
<MDTypography variant="subtitle2" color="secondary" fontWeight="regular">
{noRowsFoundHTML ? (parse(noRowsFoundHTML)) : "No rows found"}
<MDTypography
variant="subtitle2"
color="secondary"
fontWeight="regular"
>
{
noRowsFoundHTML ? (
parse(noRowsFoundHTML)
) : "No rows found"
}
</MDTypography>
</Box>
:
<TableContainer sx={{boxShadow: "none"}}>
<Table component="div" sx={{display: "grid", gridTemplateRows: "auto", gridTemplateColumns: "1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr"}}>
<Table>
<Box component="thead">
<TableRow sx={{alignItems: "flex-end"}} key="header">
{Array(8).fill(0).map((_, i) =>
<DataTableHeadCell key={`head-${i}`} sorted={false} width="auto" align="center">
<Skeleton width="100%" />
</DataTableHeadCell>
)}
</TableRow>
</Box>
<TableBody>
{Array(5).fill(0).map((_, i) =>
Array(8).fill(0).map((_, j) =>
<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>
</TableContainer>
}

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Box} from "@mui/material";
import Box from "@mui/material/Box";
import {Theme} from "@mui/material/styles";
import colors from "qqq/assets/theme/base/colors";
import {ReactNode} from "react";
@ -30,14 +30,13 @@ interface Props
children: ReactNode;
noBorder?: boolean;
align?: "left" | "right" | "center";
sx?: any;
}
function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
function DataTableBodyCell({noBorder, align, children}: Props): JSX.Element
{
return (
<Box
component="div"
component="td"
textAlign={align}
py={1.5}
px={1.5}
@ -55,7 +54,7 @@ function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
},
"&:last-child": {
paddingRight: "1rem"
}, ...sx
}
})}
>
<Box
@ -73,7 +72,6 @@ function DataTableBodyCell({noBorder, align, sx, children}: Props): JSX.Element
DataTableBodyCell.defaultProps = {
noBorder: false,
align: "left",
sx: {}
};
export default DataTableBodyCell;

View File

@ -44,14 +44,18 @@ function DataTableHeadCell({width, children, sorted, align, tooltip, ...rest}: P
return (
<Box
component="div"
component="th"
width={width}
py={1.5}
px={1.5}
sx={({palette: {light}, borders: {borderWidth}}: Theme) => ({
borderBottom: `${borderWidth[1]} solid ${colors.grayLines.main}`,
position: "sticky", top: 0, background: "white",
zIndex: 1 // so if body rows scroll behind it, they don't show through
"&:nth-of-type(1)": {
paddingLeft: "1rem"
},
"&:last-child": {
paddingRight: "1rem"
},
})}
>
<Box

View File

@ -0,0 +1,21 @@
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.
</>
);
}

View File

@ -0,0 +1,69 @@
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>
</>
)}
</>
);
}

View File

@ -0,0 +1,214 @@
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
};
}

View File

@ -0,0 +1,187 @@
/*
* 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;

View File

@ -0,0 +1,199 @@
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;

View File

@ -0,0 +1,75 @@
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)[];
}

View File

@ -1,38 +0,0 @@
/*
* 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;
}

View File

@ -29,7 +29,6 @@ import {QInstance} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QInstan
import {QProcessMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QProcessMetaData";
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import {QTableSection} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableSection";
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {QJobComplete} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobComplete";
import {QJobError} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobError";
import {QJobRunning} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJobRunning";
@ -37,14 +36,12 @@ import {QJobStarted} from "@kingsrook/qqq-frontend-core/lib/model/processes/QJob
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
import {QRecord} from "@kingsrook/qqq-frontend-core/lib/model/QRecord";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
import Box from "@mui/material/Box";
import {Alert, Box, Button, CircularProgress, Icon, TablePagination} from "@mui/material";
import Card from "@mui/material/Card";
import Grid from "@mui/material/Grid";
import Step from "@mui/material/Step";
import StepLabel from "@mui/material/StepLabel";
import Stepper from "@mui/material/Stepper";
import {Theme} from "@mui/material/styles";
import Typography from "@mui/material/Typography";
import {DataGridPro, GridColDef} from "@mui/x-data-grid-pro";
import FormData from "form-data";
@ -63,12 +60,8 @@ import QRecordSidebar from "qqq/components/misc/RecordSidebar";
import {GoogleDriveFolderPickerWrapper} from "qqq/components/processes/GoogleDriveFolderPickerWrapper";
import ProcessSummaryResults from "qqq/components/processes/ProcessSummaryResults";
import ValidationReview from "qqq/components/processes/ValidationReview";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import CompositeWidget, {CompositeData} from "qqq/components/widgets/CompositeWidget";
import DashboardWidgets from "qqq/components/widgets/DashboardWidgets";
import {ChildRecordListData} from "qqq/components/widgets/misc/RecordGridWidget";
import BaseLayout from "qqq/layouts/BaseLayout";
import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import {TABLE_VARIANT_LOCAL_STORAGE_KEY_ROOT} from "qqq/pages/records/query/RecordQuery";
import Client from "qqq/utils/qqq/Client";
import TableUtils from "qqq/utils/qqq/TableUtils";
@ -96,18 +89,14 @@ const INITIAL_RETRY_MILLIS = 1_500;
const RETRY_MAX_MILLIS = 12_000;
const BACKOFF_AMOUNT = 1.5;
////////////////////////////////////////////////////////////////////////////////
// define some functions that we can make reference to, which we'll overwrite //
// with functions from formik, once we're inside formik. //
////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////
// define a function that we can make referenes to, which we'll overwrite //
// with formik's setFieldValue function, once we're inside formik. //
////////////////////////////////////////////////////////////////////////////
let formikSetFieldValueFunction = (field: string, value: any, shouldValidate?: boolean): void =>
{
};
let formikSetTouched = ({}: any, touched: boolean): void =>
{
};
const cachedPossibleValueLabels: { [fieldName: string]: { [id: string | number]: string } } = {};
function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, isReport, recordIds, closeModalHandler, forceReInit, overrideLabel}: Props): JSX.Element
@ -131,7 +120,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [activeStepIndex, setActiveStepIndex] = useState(0);
const [activeStep, setActiveStep] = useState(null as QFrontendStepMetaData);
const [newStep, setNewStep] = useState(null);
const [stepInstanceCounter, setStepInstanceCounter] = useState(0);
const [steps, setSteps] = useState([] as QFrontendStepMetaData[]);
const [needInitialLoad, setNeedInitialLoad] = useState(true);
const [lastForcedReInit, setLastForcedReInit] = useState(null as number);
@ -148,10 +136,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
);
const [showErrorDetail, setShowErrorDetail] = useState(false);
const [showFullHelpText, setShowFullHelpText] = useState(false);
const [previouslySeenUpdatedFieldMetaDataMap, setPreviouslySeenUpdatedFieldMetaDataMap] = useState(new Map<string, QFieldMetaData>);
const [renderedWidgets, setRenderedWidgets] = useState({} as { [step: string]: { [widgetName: string]: any } });
const [controlCallbacks, setControlCallbacks] = useState({} as { [name: string]: () => void });
const {pageHeader, recordAnalytics, setPageHeader, helpHelpActive} = useContext(QContext);
@ -169,30 +155,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const [overrideOnLastStep, setOverrideOnLastStep] = useState(null as boolean);
/////////////////////////////////////////////////////////////////////////////////////
// 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;
}
const onLastStep = activeStepIndex === steps.length - 2;
const noMoreSteps = activeStepIndex === steps.length - 1;
////////////////
// form state //
@ -211,8 +175,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const [recordConfig, setRecordConfig] = useState({} as any);
const [pageNumber, setPageNumber] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [records, setRecords] = useState([] as any);
const [childRecordData, setChildRecordData] = useState(null as ChildRecordListData);
const [records, setRecords] = useState([] as QRecord[]);
//////////////////////////////
// state for bulk edit form //
@ -331,205 +294,36 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
*******************************************************************************/
function renderWidget(widgetName: string)
{
const widgetMetaData = qInstance.widgets.get(widgetName);
if (!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
if (!renderedWidgets[activeStep.name])
{
renderedWidgets[activeStep.name] = {};
setRenderedWidgets(renderedWidgets);
}
let isChildRecordWidget = widgetMetaData.type == "childRecordList";
if (!isChildRecordWidget && renderedWidgets[activeStep.name][widgetName])
if (renderedWidgets[activeStep.name][widgetName])
{
return renderedWidgets[activeStep.name][widgetName];
}
const widgetMetaData = qInstance.widgets.get(widgetName);
if (!widgetMetaData)
{
return (<Alert color="error">Unrecognized widget name: {widgetName}</Alert>);
}
const queryStringParts: string[] = [];
for (let name in processValues)
{
queryStringParts.push(`${name}=${encodeURIComponent(processValues[name])}`);
}
let initialWidgetDataList = null;
if (processValues[widgetName])
{
processValues[widgetName].hasPermission = true;
initialWidgetDataList = [processValues[widgetName]];
}
let actionCallback = blockWidgetActionCallback;
if (isChildRecordWidget)
{
actionCallback = childRecordListWidgetActionCallBack;
if (childRecordData)
{
initialWidgetDataList = [childRecordData];
}
}
const renderedWidget = (<Box m={-2}>
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} initialWidgetDataList={initialWidgetDataList} values={processValues} actionCallback={actionCallback} />
<DashboardWidgets widgetMetaDataList={[widgetMetaData]} omitWrappingGridContainer={true} childUrlParams={queryStringParts.join("&")} />
</Box>);
renderedWidgets[activeStep.name][widgetName] = renderedWidget;
return renderedWidget;
}
/***************************************************************************
**
***************************************************************************/
function handleControlCode(controlCode: string)
{
const split = controlCode.split(":", 2);
let controlCallbackName: string;
let controlCallbackValue: any;
if (split.length == 2)
{
if (split[0] == "showModal")
{
processValues[split[1]] = true;
controlCallbackName = split[1];
controlCallbackValue = true;
}
else if (split[0] == "hideModal")
{
processValues[split[1]] = false;
controlCallbackName = split[1];
controlCallbackValue = false;
}
else if (split[0] == "toggleModal")
{
const currentValue = processValues[split[1]];
processValues[split[1]] = !!!currentValue;
controlCallbackName = split[1];
controlCallbackValue = processValues[split[1]];
}
else
{
console.log(`Unexpected part[0] (before colon) in controlCode: [${controlCode}]`);
}
}
else
{
console.log(`Expected controlCode to have 2 colon-delimited parts, but was: [${controlCode}]`);
}
if (controlCallbackName && controlCallbacks[controlCallbackName])
{
// @ts-ignore ... args are hard
controlCallbacks[controlCallbackName](controlCallbackValue);
}
}
/***************************************************************************
** callback used by child list widget
***************************************************************************/
function childRecordListWidgetActionCallBack(data: any): boolean
{
console.log(`in childRecordListWidgetActionCallBack: ${JSON.stringify(data)}`);
setChildRecordData(data as ChildRecordListData);
return (true);
}
/***************************************************************************
** callback used by widget blocks, e.g., for input-text-enter-on-submit,
** and action buttons.
***************************************************************************/
function blockWidgetActionCallback(blockData: BlockData, eventValues?: { [name: string]: any }): boolean
{
console.log(`in blockWidgetActionCallback, called by block: ${JSON.stringify(blockData)}`);
if (eventValues?.registerControlCallbackName && eventValues?.registerControlCallbackFunction)
{
controlCallbacks[eventValues.registerControlCallbackName] = eventValues.registerControlCallbackFunction;
setControlCallbacks(controlCallbacks);
return (true);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// we don't validate these on the android frontend, and it seems fine - just let the app validate it? //
////////////////////////////////////////////////////////////////////////////////////////////////////////
// ///////////////////////////////////////////////////////////////////////////////
// // if the eventValues included an actionCode - validate it before proceeding //
// ///////////////////////////////////////////////////////////////////////////////
// if (eventValues && eventValues.actionCode && !ProcessWidgetBlockUtils.isActionCodeValid(eventValues.actionCode, activeStep, processValues))
// {
// setFormError("Unrecognized action code: " + eventValues.actionCode);
// if (eventValues["_fieldToClearIfError"])
// {
// /////////////////////////////////////////////////////////////////////////////
// // if the eventValues included a _fieldToClearIfError, well, then do that. //
// /////////////////////////////////////////////////////////////////////////////
// formikSetFieldValueFunction(eventValues["_fieldToClearIfError"], "", false);
// }
// return (false);
// }
let doSubmit = false;
if (blockData?.blockTypeName == "BUTTON" && eventValues?.actionCode)
{
doSubmit = true;
}
else if (blockData?.blockTypeName == "BUTTON" && eventValues?.controlCode)
{
handleControlCode(eventValues.controlCode);
doSubmit = false;
}
else if (blockData?.blockTypeName == "INPUT_FIELD")
{
///////////////////////////////////////////////////////////////////////////////////////////////
// if action callback was fired from an input field, assume that means we're good to submit. //
///////////////////////////////////////////////////////////////////////////////////////////////
doSubmit = true;
}
//////////////////
// ok - submit! //
//////////////////
if (doSubmit)
{
handleFormSubmit(eventValues);
return (true);
}
}
/***************************************************************************
** in a memoized-fashion (YUNO useMemo?), render a component that is an
** adHoc widget (e.g., composite)
***************************************************************************/
function renderAdHocWidget(componentValues: any, componentIndex: number)
{
const key = activeStep.name + "-" + stepInstanceCounter + "-" + componentIndex;
if (renderedWidgets[key])
{
return renderedWidgets[key];
}
const widgetMetaData = new QWidgetMetaData({name: "adHoc"});
const compositeWidgetData = JSON.parse(JSON.stringify(componentValues)) as CompositeData;
compositeWidgetData.styleOverrides = {py: "0.5rem", display: "flex", flexDirection: "column", gap: "0.5rem"};
ProcessWidgetBlockUtils.dynamicEvaluationOfCompositeWidgetData(compositeWidgetData, processValues);
renderedWidgets[key] = <Box key={key} pt={2}>
<CompositeWidget widgetMetaData={widgetMetaData} data={compositeWidgetData} actionCallback={blockWidgetActionCallback} values={processValues} />
</Box>;
setRenderedWidgets(renderedWidgets);
return (renderedWidgets[key]);
}
////////////////////////////////////////////////////
// generate the main form body content for a step //
////////////////////////////////////////////////////
@ -588,7 +382,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
if (qJobRunning || step === null)
{
return (
<Grid m={3} mt={9} container maxWidth="calc(100% - 3rem)">
<Grid m={3} mt={9} container>
<Grid item xs={0} lg={3} />
<Grid item xs={12} lg={6}>
<Card>
@ -683,7 +477,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
let helpRoles = ["PROCESS_SCREEN", "ALL_SCREENS"];
const showHelp = helpHelpActive || hasHelpContent(step.helpContents, helpRoles);
const formattedHelpContent = <HelpContent helpContents={step.helpContents} roles={helpRoles} helpContentKey={`process:${processName};step:${step?.name}`} />;
const isFormatScanner = step?.format?.toLowerCase() == "scanner";
return (
<>
@ -692,7 +485,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// hide label on widgets - the Widget component itself provides the label //
// for modals, show the process label, but not for full-screen processes (for them, it is in the breadcrumb) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
!isWidget && !isFormatScanner &&
!isWidget &&
<MDTypography variant={isWidget ? "h6" : "h5"} component="div" fontWeight="bold">
{(isModal) ? `${overrideLabel ?? process.label}: ` : ""}
{step?.label}
@ -970,29 +763,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}
{
component.type === QComponentType.WIDGET && (
<>
{
///////////////////////////////////////////////////
// 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>
@ -1092,11 +864,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setActiveStepIndex(newIndex);
setOverrideOnLastStep(null);
////////////////////////////////////////////////////////////////////////////////////////////////////
// reset formik touched data, so a field that's repeated doesn't immediately show a 'dirty' state //
////////////////////////////////////////////////////////////////////////////////////////////////////
formikSetTouched({}, false);
if (steps)
{
const activeStep = steps[newIndex];
@ -1132,18 +899,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
if (doesStepHaveComponent(activeStep, QComponentType.VALIDATION_REVIEW_SCREEN))
{
addField("doFullValidation", {type: "radio"}, "true", null);
//////////////////////////////////////////////////////////////////////////////////////////////
// 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))
{
@ -1152,16 +909,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
addField("googleDriveFolderName", {type: "hidden", omitFromQDynamicForm: true}, "", null);
}
if (doesStepHaveComponent(activeStep, QComponentType.WIDGET))
{
ProcessWidgetBlockUtils.addFieldsForCompositeWidget(activeStep, processValues, (fieldMetaData) =>
{
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
const validation = DynamicFormUtils.getValidationForField(fieldMetaData);
addField(fieldMetaData.name, dynamicField, processValues[fieldMetaData.name], validation);
});
}
///////////////////////////////////////////////////
// if this step has form fields, set up the form //
///////////////////////////////////////////////////
@ -1247,7 +994,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setValidationFunction(() => true);
}
}
}, [newStep, stepInstanceCounter]); // maybe we could just use stepInstanceCounter...
}, [newStep]);
/////////////////////////////////////////////////////////////////////////////////////////////
// if there are records to load: build a record config, and set the needRecords state flag //
@ -1320,11 +1067,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const {records} = response;
setRecords(records);
if (!childRecordData || childRecordData.length == 0)
{
setChildRecordData(convertRecordsToChildRecordData(records));
}
/////////////////////////////////////////////////////////////////////////////////////////
// re-construct the recordConfig object, so the setState call triggers a new rendering //
/////////////////////////////////////////////////////////////////////////////////////////
@ -1346,71 +1088,6 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
}
}, [needRecords]);
/***************************************************************************
**
***************************************************************************/
function convertRecordsToChildRecordData(records: QRecord[])
{
const frontendRecords = [] as any[];
records.forEach((record: QRecord) =>
{
const object = {
"tableName": record.tableName,
"recordLabel": record.recordLabel,
"errors": record.errors,
"warnings": record.warnings,
"values": Object.fromEntries(record.values),
"displayValues": Object.fromEntries(record.displayValues),
};
frontendRecords.push(object);
});
const newChildListData = {} as ChildRecordListData;
newChildListData.queryOutput = {records: frontendRecords};
return (newChildListData);
}
/***************************************************************************
**
***************************************************************************/
function updateFieldsInProcess(steps: QFrontendStepMetaData[], updatedFields: Map<string, QFieldMetaData>)
{
if (updatedFields)
{
updatedFields.forEach((field) => previouslySeenUpdatedFieldMetaDataMap.set(field.name, field));
setPreviouslySeenUpdatedFieldMetaDataMap(previouslySeenUpdatedFieldMetaDataMap);
}
for (let step of steps)
{
if (step && step.formFields)
{
for (let i = 0; i < step.formFields.length; i++)
{
let field = step.formFields[i];
if (previouslySeenUpdatedFieldMetaDataMap.has(field.name))
{
step.formFields[i] = previouslySeenUpdatedFieldMetaDataMap.get(field.name);
}
}
}
}
if (processValues.inputFieldList)
{
for (let i = 0; i < processValues.inputFieldList.length; i++)
{
let field = new QFieldMetaData(processValues.inputFieldList[i]);
if (previouslySeenUpdatedFieldMetaDataMap.has(field.name))
{
processValues.inputFieldList[i] = previouslySeenUpdatedFieldMetaDataMap.get(field.name); // todo - uh, not an object?
}
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle a response from the server - e.g., after starting a backend job, or getting its status/result //
//////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -1435,19 +1112,13 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
// if the process step sent a new frontend-step-list, then refresh what we have in state (constructing new full model objects) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
let frontendSteps = steps;
const updatedFrontendStepList = qJobComplete.processMetaDataAdjustment?.updatedFrontendStepList;
const updatedFrontendStepList = qJobComplete.updatedFrontendStepList;
if (updatedFrontendStepList)
{
setSteps(updatedFrontendStepList);
frontendSteps = updatedFrontendStepList;
setSteps(frontendSteps);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// always merge the new updatedFields map (if there is one) with existing updates and existing fields //
////////////////////////////////////////////////////////////////////////////////////////////////////////
updateFieldsInProcess(frontendSteps, qJobComplete.processMetaDataAdjustment?.updatedFields);
setSteps(frontendSteps);
///////////////////////////////////////////////////////////////////////////////////
// if the next screen has any PVS fields - look up their labels (display values) //
///////////////////////////////////////////////////////////////////////////////////
@ -1488,9 +1159,7 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setJobUUID(null);
setNewStep(nextStepName);
setStepInstanceCounter(1 + stepInstanceCounter);
setProcessValues(newValues);
setRenderedWidgets({});
setQJobRunning(null);
if (formikSetFieldValueFunction)
@ -1744,35 +1413,10 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
setNewStep(activeStepIndex - 1);
};
////////////////////////////////////////////
// handle user submitting changed records //
////////////////////////////////////////////
const doSubmit = async (formData: FormData) =>
{
const formDataHeaders = {
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
};
setTimeout(async () =>
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
processUUID,
activeStep.name,
formData,
formDataHeaders
);
setLastProcessResponse(processResponse);
});
};
//////////////////////////////////////////////////////////////////////////////////////////
// handle user submitting the form - which in qqq means moving forward from any screen. //
// caller can pass in a map of values to be added to the form data too //
//////////////////////////////////////////////////////////////////////////////////////////
const handleFormSubmit = async (values: any) =>
const handleSubmit = async (values: any, actions: any) =>
{
setFormError(null);
@ -1811,20 +1455,28 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
formData.append("bulkEditEnabledFields", bulkEditEnabledFields.join(","));
}
/////////////////////////////////////////////////////////////
// convert to regular objects so that they can be jsonized //
/////////////////////////////////////////////////////////////
if (childRecordData)
{
formData.append("frontendRecords", JSON.stringify(childRecordData.queryOutput.records));
}
const formDataHeaders = {
"content-type": "multipart/form-data; boundary=--------------------------320289315924586491558366",
};
setProcessValues({});
setRecords([]);
setOverrideOnLastStep(null);
setLastProcessResponse(new QJobRunning({message: "Working..."}));
doSubmit(formData);
setTimeout(async () =>
{
recordAnalytics({category: "processEvents", action: "processStep", label: activeStep.label});
const processResponse = await Client.getInstance().processStep(
processName,
processUUID,
activeStep.name,
formData,
formDataHeaders,
);
setLastProcessResponse(processResponse);
});
};
@ -1854,40 +1506,14 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
};
const formStyles: any = {};
if (isWidget)
{
formStyles.display = "flex";
formStyles.flexGrow = 1;
}
/***************************************************************************
**
***************************************************************************/
function makeMainCardStyles(theme: Theme)
{
const mainCardStyles: any = {};
if (!isWidget && !isModal)
{
////////////////////////////////////////////////////////////////
// remove margin around card for non-widget, non-modal, small //
////////////////////////////////////////////////////////////////
mainCardStyles[theme.breakpoints.down("sm")] = {
marginLeft: "-1.5rem",
marginRight: "-1.5rem",
borderRadius: "0"
};
}
const formStyles: any = {};
mainCardStyles.minHeight = `calc(100vh - ${isModal ? 150 : 400}px)`;
if (!processError && (qJobRunning || activeStep === null) && !isModal && !isWidget)
{
mainCardStyles.background = "#FFFFFF";
mainCardStyles.boxShadow = "none";
}
if (isWidget)
{
mainCardStyles.background = "none";
@ -1897,9 +1523,8 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
mainCardStyles.alignItems = "stretch";
mainCardStyles.flexGrow = 1;
mainCardStyles.display = "flex";
}
return mainCardStyles;
formStyles.display = "flex";
formStyles.flexGrow = 1;
}
let nextButtonLabel = "Next";
@ -1924,24 +1549,23 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
initialValues={initialValues}
validationSchema={validationScheme}
validation={validationFunction}
onSubmit={handleFormSubmit}
onSubmit={handleSubmit}
>
{({
values, errors, touched, isSubmitting, setFieldValue, setTouched
values, errors, touched, isSubmitting, setFieldValue,
}) =>
{
//////////////////////////////////////////////////////////////////
// once we're in the formik form, capture some of its functions //
// over top of the default ones we created globally //
//////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////
// once we're in the formik form, use its setFieldValue function //
// over top of the default one we created globally //
///////////////////////////////////////////////////////////////////
formikSetFieldValueFunction = setFieldValue;
formikSetTouched = setTouched;
return (
<Form style={formStyles} id={formId} autoComplete="off">
<Card sx={makeMainCardStyles}>
<Card sx={mainCardStyles}>
{
!isWidget && processMetaData?.stepFlow == "LINEAR" && (
!isWidget && (
<Box mx={2} mt={-3} sx={{"& .MuiStepper-horizontal": {minHeight: "5rem"}}}>
<Stepper activeStep={activeStepIndex} alternativeLabel>
{steps.map((step) => (
@ -1976,16 +1600,21 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
{/********************************
** back &| next/submit buttons **
********************************/}
<Box mt={3} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
<Box mt={6} width="100%" display="flex" justifyContent="space-between" position={isWidget ? "absolute" : "initial"} bottom={isWidget ? "3rem" : "initial"} right={isWidget ? "1.5rem" : "initial"}>
{true || activeStepIndex === 0 ? (
<Box />
) : (
<MDButton variant="gradient" color="light" onClick={handleBack}>back</MDButton>
)}
{processError || qJobRunning || !activeStep || activeStep?.format?.toLowerCase() == "scanner" ? (
{processError || qJobRunning || !activeStep ? (
<Box />
) : (
<>
{formError && (
<MDTypography component="div" variant="caption" color="error" fontWeight="regular" align="right" fullWidth>
{formError}
</MDTypography>
)}
{
noMoreSteps && <QCancelButton
onClickHandler={() => handleCancelClicked(true)}
@ -2021,10 +1650,9 @@ function ProcessRun({process, table, defaultProcessValues, isModal, isWidget, is
const body = (
<Box py={3} mb={20} className="processRun">
<Grid container justifyContent="center" alignItems="center" mt={{xs: 0, md: 6}} sx={{height: "100%"}}>
<Grid container justifyContent="center" alignItems="center" sx={{height: "100%", mt: 8}}>
<Grid item xs={12} lg={10} xl={8}>
{form}
{formError && <Alert severity="error" onClose={() => setFormError(null)} sx={{position: "fixed", top: "40px", left: "10vw", width: "calc(80vw)", zIndex: "99999"}}>{formError}</Alert>}
</Grid>
</Grid>
</Box>

View File

@ -1,268 +0,0 @@
/*
* 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);
}
}
}
}
}
}

View File

@ -121,7 +121,7 @@ function ColumnStats({tableMetaData, fieldMetaData, fieldTableName, filter}: Pro
}
const valueCounts = [] as QRecord[];
for(let i = 0; i < result.values.valueCounts?.length; i++)
for(let i = 0; i < result.values.valueCounts.length; i++)
{
let valueRecord = new QRecord(result.values.valueCounts[i]);

View File

@ -779,12 +779,13 @@ function InputPossibleValueSourceSingle(tableName: string, field: QFieldMetaData
}}
>
<DynamicSelect
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: selectedPossibleValue?.label}}
tableName={tableName}
fieldName={field.name}
fieldLabel="Value"
initialValue={selectedPossibleValue?.id}
initialDisplayValue={selectedPossibleValue?.label}
inForm={false}
onChange={handleChange}
useCase="filter"
// InputProps={applying ? {endAdornment: <Icon>sync</Icon>} : {}}
/>
</Box>
@ -846,12 +847,13 @@ function InputPossibleValueSourceMultiple(tableName: string, field: QFieldMetaDa
}}
>
<DynamicSelect
fieldPossibleValueProps={{tableName: tableName, fieldName: field.name, initialDisplayValue: null}}
tableName={tableName}
fieldName={field.name}
isMultiple={true}
fieldLabel="Value"
initialValues={selectedPossibleValues}
inForm={false}
onChange={handleChange}
useCase="filter"
/>
</Box>
);

View File

@ -33,7 +33,8 @@ import {QCriteriaOperator} from "@kingsrook/qqq-frontend-core/lib/model/query/QC
import {QFilterCriteria} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterCriteria";
import {QFilterOrderBy} from "@kingsrook/qqq-frontend-core/lib/model/query/QFilterOrderBy";
import {QQueryFilter} from "@kingsrook/qqq-frontend-core/lib/model/query/QQueryFilter";
import {Alert, Box, Collapse, Menu, Typography} from "@mui/material";
import {Alert, Collapse, Menu, Typography} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Card from "@mui/material/Card";
import Divider from "@mui/material/Divider";
@ -91,7 +92,6 @@ interface Props
launchProcess?: QProcessMetaData;
usage?: QueryScreenUsage;
isModal?: boolean;
isPreview?: boolean;
initialQueryFilter?: QQueryFilter;
initialColumns?: QQueryColumns;
allowVariables?: boolean;
@ -126,7 +126,7 @@ const getLoadingScreen = (isModal: boolean) =>
**
** Yuge component. The best. Lots of very smart people are saying so.
*******************************************************************************/
const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
const RecordQuery = forwardRef(({table, usage, isModal, allowVariables, initialQueryFilter, initialColumns}: Props, ref) =>
{
const tableName = table.name;
const [searchParams] = useSearchParams();
@ -884,18 +884,6 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
};
/*******************************************************************************
** 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.
*******************************************************************************/
@ -2244,25 +2232,12 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
return (
<GridToolbarContainer>
<div>
<Tooltip title="Refresh Query">
<Button id="refresh-button" onClick={() => updateTable("refresh button")} startIcon={<Icon>refresh</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
</Tooltip>
</div>
{
!isPreview && (
<div style={{position: "relative"}}>
{/* @ts-ignore */}
<GridToolbarDensitySelector nonce={undefined} />
</div>
)
}
{
isPreview && (
<Tooltip title="Open In New Window">
<Button id="open-filter-in-new-window-button" onClick={() => openFilterInNewWindow()} startIcon={<Icon>launch</Icon>} sx={{pl: "1rem", pr: "0.5rem", minWidth: "unset"}}></Button>
</Tooltip>
)
}
{
usage == "queryScreen" &&
@ -2897,7 +2872,7 @@ const RecordQuery = forwardRef(({table, usage, isModal, isPreview, allowVariable
}
{
!isPreview && metaData && tableMetaData &&
metaData && tableMetaData &&
<BasicAndAdvancedQueryControls
ref={basicAndAdvancedQueryControlsRef}
metaData={metaData}

View File

@ -34,7 +34,6 @@ import BaseLayout from "qqq/layouts/BaseLayout";
import Client from "qqq/utils/qqq/Client";
import ValueUtils from "qqq/utils/qqq/ValueUtils";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-java";
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/mode-json";

View File

@ -788,19 +788,503 @@ input[type="search"]::-webkit-search-results-decoration
margin: 2rem 1rem;
}
/* default styles for a block widget overlay */
.blockWidgetOverlay
{
font-weight: 400;
.sqd-designer-react {
width: 100vw;
height: 90vh;
}
.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;
top: 15px;
height: 0;
display: flex;
font-size: 14px;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
}
.blockWidgetOverlay a
{
color: #0062FF !important;
.sqd-designer,
.sqd-drag,
.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;
}

View File

@ -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.`);
}
};
}
/*******************************************************************************
**
@ -170,7 +170,7 @@ export default class DataGridUtils
});
return (rows);
};
}
/*******************************************************************************
**
@ -241,20 +241,16 @@ export default class DataGridUtils
///////////////////////////
// sort by labels... mmm //
///////////////////////////
sortedKeys.push(...tableMetaData.fields.keys());
sortedKeys.push(...tableMetaData.fields.keys())
sortedKeys.sort((a: string, b: string): number =>
{
return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label));
});
return (tableMetaData.fields.get(a).label.localeCompare(tableMetaData.fields.get(b).label))
})
}
sortedKeys.forEach((key) =>
{
const field = tableMetaData.fields.get(key);
if (!field)
{
return;
}
if(field.isHeavy)
{
if(field.type == QFieldType.BLOB)
@ -350,7 +346,7 @@ export default class DataGridUtils
(cellValues.value)
);
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"];
const helpRoles = ["QUERY_SCREEN", "READ_SCREENS", "ALL_SCREENS"]
const showHelp = hasHelpContent(field.helpContents, helpRoles); // todo - maybe - take helpHelpActive from context all the way down to here?
if(showHelp)
{
@ -365,7 +361,7 @@ export default class DataGridUtils
}
return (column);
};
}
/*******************************************************************************
@ -419,6 +415,6 @@ export default class DataGridUtils
}
return (200);
};
}
}

View File

@ -1,50 +0,0 @@
/*
* 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>
);
}

View File

@ -19,8 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFieldType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldType";
import Client from "qqq/utils/qqq/Client";
/*******************************************************************************
** Utility functions for basic html/webpage/browser things.
@ -69,15 +68,10 @@ export default class HtmlUtils
** it was originally built like this when we had to submit full access token to backend...
**
*******************************************************************************/
static downloadUrlViaIFrame = (field: QFieldMetaData, url: string, filename: string) =>
static downloadUrlViaIFrame = (url: string, filename: string) =>
{
if (url.startsWith("data:") || url.startsWith("http"))
if(url.startsWith("data:"))
{
if (url.startsWith("http"))
{
url += encodeURIComponent(`?response-content-disposition=attachment; ${filename}`);
}
const link = document.createElement("a");
link.download = filename;
link.href = url;
@ -99,14 +93,8 @@ export default class HtmlUtils
// todo - onload event handler to let us know when done?
document.body.appendChild(iframe);
var method = "get";
if (QFieldType.BLOB == field.type)
{
method = "post";
}
const form = document.createElement("form");
form.setAttribute("method", method);
form.setAttribute("method", "post");
form.setAttribute("action", url);
form.setAttribute("target", "downloadIframe");
iframe.appendChild(form);

View File

@ -133,7 +133,7 @@ class FilterUtils
}
else
{
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values, undefined, "filter");
values = await qController.possibleValues(fieldTable.name, null, field.name, "", values);
}
}

View File

@ -28,17 +28,18 @@ import "datejs"; // https://github.com/datejs/Datejs
import {Chip, ClickAwayListener, Icon} from "@mui/material";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import {makeStyles} from "@mui/styles";
import parse from "html-react-parser";
import React, {Fragment, useReducer, useState} from "react";
import AceEditor from "react-ace";
import {Link} from "react-router-dom";
import HtmlUtils from "qqq/utils/HtmlUtils";
import Client from "qqq/utils/qqq/Client";
import "ace-builds/src-noconflict/ace";
import "ace-builds/src-noconflict/mode-sql";
import React, {Fragment, useReducer, useState} from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-velocity";
import {Link} from "react-router-dom";
/*******************************************************************************
** Utility class for working with QQQ Values
@ -197,7 +198,7 @@ class ValueUtils
);
}
if (field.type == QFieldType.BLOB || field.hasAdornment(AdornmentType.FILE_DOWNLOAD))
if (field.type == QFieldType.BLOB)
{
return (<BlobComponent field={field} url={rawValue} filename={displayValue} usage={usage} />);
}
@ -267,15 +268,7 @@ class ValueUtils
{
if (!(date instanceof Date))
{
////////////////////////////////////////////////////////////////////////////////////
// so, a new Date here will interpret the string as being at midnight UTC, but //
// the data object will be in the user/browser timezone. //
// so "2024-08-22", for a user in US/Central, will be "2024-08-21T19:00:00-0500". //
// correct for that by adding the date's timezone offset (converted from minutes //
// to millis) back to it //
////////////////////////////////////////////////////////////////////////////////////
date = new Date(date);
date.setTime(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
}
// @ts-ignore
return (`${date.toString("yyyy-MM-dd")}`);
@ -679,7 +672,7 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
const download = (event: React.MouseEvent<HTMLSpanElement>) =>
{
event.stopPropagation();
HtmlUtils.downloadUrlViaIFrame(field, url, filename);
HtmlUtils.downloadUrlViaIFrame(url, filename);
};
const open = (event: React.MouseEvent<HTMLSpanElement>) =>
@ -703,22 +696,10 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
usage == "view" && filename
}
<Tooltip placement={tooltipPlacement} title="Open file">
{
field.type == QFieldType.BLOB ? (
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => open(e)}>open_in_new</Icon>
) : (
<a style={{color: "inherit"}} rel="noopener noreferrer" href={url} target="_blank"><Icon className={"blobIcon"} fontSize="small">open_in_new</Icon></a>
)
}
</Tooltip>
<Tooltip placement={tooltipPlacement} title="Download file">
{
field.type == QFieldType.BLOB ? (
<Icon className={"blobIcon"} fontSize="small" onClick={(e) => download(e)}>save_alt</Icon>
) : (
<a style={{color: "inherit"}} href={url} download="test.pdf"><Icon className={"blobIcon"} fontSize="small">save_alt</Icon></a>
)
}
</Tooltip>
{
usage == "query" && filename
@ -728,4 +709,5 @@ function BlobComponent({field, url, filename, usage}: BlobComponentProps): JSX.E
}
export default ValueUtils;

View File

@ -57,5 +57,4 @@ module.exports = function (app)
app.use("/images", getRequestHandler());
app.use("/api*", getRequestHandler());
app.use("/*api", getRequestHandler());
app.use("/qqq/*", getRequestHandler());
};

View File

@ -56,8 +56,8 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
"label": "Sample Table Widget",
"footerHTML": "<span class='material-icons-round notranslate MuiIcon-root MuiIcon-fontSizeInherit' aria-hidden='true'><span class='dashboard-schedule-icon'>schedule</span></span>Updated at 2023-10-17 09:11:38 AM CDT",
"columns": [
{ "type": "html", "header": "Id", "accessor": "id", "width": "30px" },
{ "type": "html", "header": "Name", "accessor": "name", "width": "1fr" }
{ "type": "html", "header": "Id", "accessor": "id", "width": "1%" },
{ "type": "html", "header": "Name", "accessor": "name", "width": "99%" }
],
"rows": [
{ "id": "1", "name": "<a href='/setup/person/1'>Homer S.</a>" },
@ -83,7 +83,7 @@ public class DashboardTableWidgetExportTest extends QBaseSeleniumTest
// assert that the table widget rendered its header and some contents //
////////////////////////////////////////////////////////////////////////
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget h6", "Sample Table Widget");
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget a", "Homer S.");
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget table a", "Homer S.");
qSeleniumLib.waitForSelectorContaining("#SampleTableWidget div", "Updated at 2023-10-17 09:11:38 AM CDT");
/////////////////////////////