Compare commits

...

73 Commits

Author SHA1 Message Date
501b8b34c9 Merge branch 'release/0.23.0' 2024-11-22 16:18:57 -06:00
efd1922ee3 Update versions for release 2024-11-22 15:59:20 -06:00
b4f8fb2e18 CE-1946: minor updates to padding, fixes, etc 2024-11-22 11:41:29 -06:00
204025c2a6 Merged feature/CE-1772-generate-labels-poc into dev 2024-11-21 12:31:44 -06:00
6dfc839c30 CE-1946: added child record widget data, minor divider styles 2024-11-19 20:40:16 -06:00
726906061d CE-1946: checkpoint initial commit 2024-11-19 15:07:10 -06:00
2514c463a6 Merge pull request #73 from Kingsrook/feature/CE-1727-mobile-first-uiux
Feature/ce 1727 mobile first uiux
2024-11-12 11:45:46 -06:00
b41a9a6fe6 CE-1960 Pass use-cases through more calls to qController.possibleValues - to fix filter-as-widget dropping possible-value fields w/ a defaultFilter 2024-11-07 10:19:55 -06:00
cfca47054e CE-1727 Remove unused (uncommitted) RevealBlock 2024-11-04 08:57:07 -06:00
b48ef70c5e Update to qqq-frontend-core 1.0.110 2024-11-04 08:52:55 -06:00
81efb7e18d CE-1727 Updates to processes rendering block-widgets, to get up to compatible with the android app 2024-11-04 08:48:46 -06:00
387f09f4ad CE-1772: updated value utils for non blob file downloads 2024-11-03 22:18:27 -06:00
4fd50936ea CE-1727 Add proxy for /qqq/*, for new versioned middleware 2024-10-31 16:07:10 -05:00
3adb8ab4ba CE-1727 - Improve (fix?) next/submit handling for validation screen and linear flow. 2024-09-20 12:17:30 -05:00
98a02cda96 CE-1727 - Add support for ad-hoc, composite/block widgets; support processFlow (e.g., non-linear); some mobile-style adjustments 2024-09-20 11:07:22 -05:00
aee4becda5 CE-1727 - Initial checkin 2024-09-20 11:06:29 -05:00
f13c2c276f CE-1727 - mobile style adjust 2024-09-20 11:06:14 -05:00
a99272767b CE-1727 - Update qfc to 1.0.107 (add process.stepFlow) 2024-09-20 11:05:54 -05:00
a3236b426e CE-1727 - minor mobile placement adjustments 2024-09-20 10:44:41 -05:00
597fde977f CE-1727 - in Yup required message, show 'This field' instead of 'undefined' if field doesn't have a label 2024-09-20 10:44:26 -05:00
e303ed0b43 CE-1727 - Initial checkin 2024-09-20 10:43:57 -05:00
2b057768b3 CE-1727 - Style adjustments re: mobile processes 2024-09-20 10:43:37 -05:00
504a43d9c3 CE-1727 - Add layout FLEX_ROW_CENTER; Add actionCallback 2024-09-20 10:43:19 -05:00
33e56f823d CE-1727 - Add BlockData.conditional attribute; Add actionCallback to StandardBlockComponentProps 2024-09-20 10:42:55 -05:00
dc8fdb33dc CE-1727 - New blocks and styles for (mobile-style) widgets in processes, plus callbacks and contextValues 2024-09-20 10:42:22 -05:00
efa67da7f9 CE-1727 - Switch from use of updatedFrontendStepList to processMetaDataAdjustment, which can include updatedFields (specifically adding to change a dynamic possible value source, but, seems more flexible than just that) 2024-09-09 16:04:16 -05:00
3dc92aec88 CE-1727 - Add inlinePossibleValueSources option to fields; pass more data into DynamicSelect in a named object, so it's a little less loosely defined. 2024-09-09 16:01:30 -05:00
d2705c3aed Avoid null-pointer in TableUtils.getSectionsForRecordSidebar callback, if instance has no widgets 2024-09-05 19:05:40 -05:00
1d965bcdee Merge tag 'version-0.22.0' into dev
Tag release
2024-09-04 20:15:14 -05:00
894a9c2afc Merge branch 'release/0.22.0' 2024-09-04 20:15:14 -05:00
d25f124d87 Update for next development version 2024-09-04 20:00:14 -05:00
fd5055e502 Update versions for release 2024-09-04 20:00:10 -05:00
326367fbe0 Merge pull request #72 from Kingsrook/feature/CE-1647-update-filter-widget-to-show-preview
CE-1647: added preview to query filter widget
2024-09-04 16:17:40 -05:00
bb6f818457 Merge pull request #71 from Kingsrook/feature/CE-1646-possible-value-filter-bug
Feature/ce 1646 possible value filter bug
2024-09-04 16:17:25 -05:00
1cd6e07907 Merge pull request #70 from Kingsrook/feature/CE-1643-query-date-bugs
Feature/ce 1643 query date bugs
2024-09-04 16:16:59 -05:00
e839da6123 CE-1405 - Put margin-left on this, so if its used in a big number block, and it wraps, it does right, i think 2024-08-29 16:03:26 -05:00
34a4fc19b4 CE-1647: added preview to query filter widget 2024-08-27 17:52:34 -05:00
2cc7e9ebe1 CE-1643 Add a timezone conversion to the formatDate function for the case where it took a string rather than a Date as input, in which case, the new Date() call would be appying a timezone, and making us off-by-one (for some side of the prime merdian i think) 2024-08-23 15:13:44 -05:00
128a748b63 CE-1643 Add fontVariantNumeric: "tabular-nums" to the thing with numbers that count up, so it's awesome. 2024-08-23 15:11:52 -05:00
1284e3a22c CE-1643 change default operator for DATEs to be equals 2024-08-23 15:11:26 -05:00
ae358b9067 Merge tag 'version-0.21.0' into dev
Tag release
2024-08-23 14:42:25 -05:00
dc20c3d5ec Turn off postReleaseGoals=install in gitflow-maven-plugin 2024-08-23 14:41:55 -05:00
71a9c6470a Merge branch 'release/0.21.0' 2024-08-23 14:39:48 -05:00
765d40aef1 Add skipTestProject to gitflow-maven-plugin 2024-08-23 14:39:39 -05:00
d9f1642f0a Update for next development version 2024-08-23 14:10:54 -05:00
858540427d Update versions for release 2024-08-23 14:10:51 -05:00
eecb2d4489 Update qqq-backend-core dep to 0.21.0 2024-08-23 14:10:29 -05:00
5a6293cfdf Merged dev into feature/CE-1646-possible-value-filter-bug 2024-08-23 10:27:45 -05:00
868022408c Merge pull request #68 from Kingsrook/feature/CE-1555-ops-overview-fix-accordion
Feature/ce 1555 ops overview fix accordion
2024-08-23 10:26:53 -05:00
d090a665ff Merge pull request #69 from Kingsrook/feature/CE-1556-ops-overview-enhanced-tooltips
Feature/ce 1556 ops overview enhanced tooltips
2024-08-23 10:24:15 -05:00
f112cf5543 Remove sold border 2024-08-23 10:24:00 -05:00
0c2dcb1215 Update qqq-frontend-core to 1.0.105 2024-08-23 10:07:18 -05:00
418f7957a2 CE-1646 pass useCase (filter or form) into DynamicSelect and down to possibleValue backend calls 2024-08-23 08:51:24 -05:00
8be8bf367a CE-1405 / CE-1479 - Add missing ? 2024-08-21 08:52:57 -05:00
1ca1313a25 CE-1405 / CE-1479 - Let widget meta data default values set more grid cols per size classes 2024-08-21 08:35:35 -05:00
4533815535 CE-1554: added ability to overlay html over a block widget 2024-08-20 15:42:58 -05:00
4230f34b15 Only output Link if link has an href (else page blows up) 2024-08-20 10:07:45 -05:00
e08e37222b CE-1556: updated to try to use composite block data within tooltips 2024-08-13 16:22:12 -05:00
0ffada6aec CE-1555: updates 'accordian' behavior of tables 2024-08-12 12:09:29 -05:00
9f04d897a1 Merge branch 'feature/style-cleanups-20240725' into feature/CE-1555-ops-overview-fix-accordion 2024-08-09 13:44:52 -05:00
e604f47231 Fixes for data table css redo (a z-index on headers, and use background color (as sx prop) in body cells 2024-07-26 10:34:18 -05:00
93f5bb688c Merge pull request #66 from Kingsrook/feature/CE-1460-export-and-join-bugs
Feature/ce 1460 export and join bugs
2024-07-25 11:53:35 -05:00
3fa017e8b9 Update selector and widths per css change 2024-07-25 09:29:17 -05:00
9d5af539b9 Re-do css on table skeleton, per changes in the included DataTable*Cell components 2024-07-25 09:29:13 -05:00
97bab57974 Re-do css on tables, to do the whole table as divs with display: grid 2024-07-25 08:37:37 -05:00
d9de96ea7f Make whole top-right bar display:none at under md breakpoints 2024-07-25 08:36:13 -05:00
ff839d85fd Merged dev into feature/CE-1460-export-and-join-bugs 2024-07-09 11:36:32 -05:00
d31215f6c0 Added dev for build, not test 2024-07-09 10:09:25 -05:00
262855b9c0 Increase version to 0.21.0-SNAPSHOT 2024-07-09 10:08:32 -05:00
4d082c3c57 Update revision to 0.20.0, to publish that release version 2024-07-05 20:38:36 -05:00
45b6b42836 Add a ? in case no valueCount records came back - which, can happen for a join-field where there were no matching join records. 2024-07-05 12:42:42 -05:00
47fb7cc2e3 Merge pull request #65 from Kingsrook/feature/CE-1402-field-case-change-behaviors
Feature/ce 1402 field case change behaviors
2024-07-03 16:29:05 -05:00
647c63f5a3 Add ErrorBoundary, and wrap HelpContent with it 2024-06-25 13:32:13 -05:00
57 changed files with 22115 additions and 5273 deletions

View File

@ -115,7 +115,7 @@ workflows:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ] context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
filters: filters:
branches: branches:
ignore: /(main|integration.*)/ ignore: /(main|dev|integration.*)/
tags: tags:
ignore: /(version|snapshot)-.*/ ignore: /(version|snapshot)-.*/
deploy: deploy:
@ -124,7 +124,7 @@ workflows:
context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ] context: [ qqq-maven-registry-credentials, kingsrook-slack, build-qqq-sample-app ]
filters: filters:
branches: branches:
only: /(main|integration.*)/ only: /(main|dev|integration.*)/
tags: tags:
only: /(version|snapshot)-.*/ only: /(version|snapshot)-.*/

24404
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", "@auth0/auth0-react": "1.10.2",
"@emotion/react": "11.7.1", "@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0", "@emotion/styled": "11.6.0",
"@kingsrook/qqq-frontend-core": "1.0.104", "@kingsrook/qqq-frontend-core": "1.0.110",
"@mui/icons-material": "5.4.1", "@mui/icons-material": "5.4.1",
"@mui/material": "5.11.1", "@mui/material": "5.11.1",
"@mui/styles": "5.11.1", "@mui/styles": "5.11.1",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -502,7 +502,7 @@ function EntityForm(props: Props): JSX.Element
///////////////////////////////////////////////// /////////////////////////////////////////////////
const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) => const tableSections = TableUtils.getSectionsForRecordSidebar(tableMetaData, [...tableMetaData.fields.keys()], (section: QTableSection) =>
{ {
const widget = metaData?.widgets.get(section.widgetName); const widget = metaData?.widgets?.get(section.widgetName);
if (widget) if (widget)
{ {
if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName")) if (widget.type == "childRecordList" && widget.defaultValues?.has("manageAssociationName"))
@ -602,7 +602,7 @@ function EntityForm(props: Props): JSX.Element
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
if (fieldMetaData.possibleValueSourceName) if (fieldMetaData.possibleValueSourceName)
{ {
const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]]); const results: QPossibleValue[] = await qController.possibleValues(tableName, null, fieldName, null, [initialValues[fieldName]], undefined, "form");
if (results && results.length > 0) if (results && results.length > 0)
{ {
defaultDisplayValues.set(fieldName, results[0].label); defaultDisplayValues.set(fieldName, results[0].label);
@ -818,9 +818,9 @@ function EntityForm(props: Props): JSX.Element
{ {
actions.setSubmitting(true); actions.setSubmitting(true);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there anre return. // // if there's a callback (e.g., for a modal nested on another create/edit screen), then just pass our data back there and return. //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (props.onSubmitCallback) if (props.onSubmitCallback)
{ {
props.onSubmitCallback(values); props.onSubmitCallback(values);
@ -1152,11 +1152,11 @@ function EntityForm(props: Props): JSX.Element
<Grid container spacing={3}> <Grid container spacing={3}>
{ {
!props.isModal && !props.isModal &&
<Grid item xs={12} lg={3}> <Grid item xs={12} lg={3} className="recordSidebar">
<QRecordSidebar tableSections={tableSections} /> <QRecordSidebar tableSections={tableSections} />
</Grid> </Grid>
} }
<Grid item xs={12} lg={props.isModal ? 12 : 9}> <Grid item xs={12} lg={props.isModal ? 12 : 9} className={props.isModal ? "" : "recordWithSidebar"}>
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
@ -1290,7 +1290,7 @@ function EntityForm(props: Props): JSX.Element
table={showEditChildForm.table} table={showEditChildForm.table}
defaultValues={showEditChildForm.defaultValues} defaultValues={showEditChildForm.defaultValues}
disabledFields={showEditChildForm.disabledFields} disabledFields={showEditChildForm.disabledFields}
onSubmitCallback={submitEditChildForm} onSubmitCallback={props.onSubmitCallback ? props.onSubmitCallback : submitEditChildForm}
overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`} overrideHeading={`${showEditChildForm.rowIndex != null ? "Editing" : "Creating New"} ${showEditChildForm.table.label}`}
/> />
</div> </div>

View File

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

View File

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

View File

@ -0,0 +1,74 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import React, {Component, ErrorInfo} from "react";
interface Props
{
errorElement?: React.ReactNode;
children: React.ReactNode;
}
interface State
{
hasError: boolean;
}
/*******************************************************************************
** Component that you can wrap around other components that might throw an error,
** to give some isolation, rather than breaking a whole page.
** Credit: https://medium.com/@bobjunior542/how-to-use-error-boundaries-in-react-js-with-typescript-ee90ec814bf1
*******************************************************************************/
class ErrorBoundary extends Component<Props, State>
{
/***************************************************************************
*
***************************************************************************/
constructor(props: Props)
{
super(props);
this.state = {hasError: false};
}
/***************************************************************************
*
***************************************************************************/
componentDidCatch(error: Error, errorInfo: ErrorInfo)
{
console.error("ErrorBoundary caught an error: ", error, errorInfo);
this.setState({hasError: true});
}
/***************************************************************************
*
***************************************************************************/
render()
{
if (this.state.hasError)
{
return this.props.errorElement ?? <span>(Error)</span>;
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@ -22,6 +22,7 @@
import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent"; import {QHelpContent} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QHelpContent";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import parse from "html-react-parser"; import parse from "html-react-parser";
import ErrorBoundary from "qqq/components/misc/ErrorBoundary";
import React, {useContext} from "react"; import React, {useContext} from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import QContext from "QContext"; import QContext from "QContext";
@ -128,6 +129,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles); let selectedHelpContent = getMatchingHelpContent(helpContentsArray, roles);
let content = null; let content = null;
let errorContent = "Error rendering help content.";
if (helpHelpActive) if (helpHelpActive)
{ {
if (!selectedHelpContent) if (!selectedHelpContent)
@ -135,6 +137,7 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
selectedHelpContent = new QHelpContent({content: ""}); selectedHelpContent = new QHelpContent({content: ""});
} }
content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`; content = selectedHelpContent.content + ` [${helpContentKey ?? "?"}]`;
errorContent += ` [${helpContentKey ?? "?"}]`;
} }
else if(selectedHelpContent) else if(selectedHelpContent)
{ {
@ -148,7 +151,9 @@ function HelpContent({helpContents, roles, heading, helpContentKey}: Props): JSX
{ {
return <Box display="inline" className="helpContent"> return <Box display="inline" className="helpContent">
{heading && <span className="header">{heading}</span>} {heading && <span className="header">{heading}</span>}
<ErrorBoundary errorElement={<i>{errorContent}</i>}>
{formatHelpContent(content, selectedHelpContent.format)} {formatHelpContent(content, selectedHelpContent.format)}
</ErrorBoundary>
</Box>; </Box>;
} }

View File

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

View File

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

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...) // todo - sometimes i want contains instead of equals on strings (client.name, for example...)
let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS; let defaultOperator = field?.possibleValueSourceName ? QCriteriaOperator.IN : QCriteriaOperator.EQUALS;
if (field?.type == QFieldType.DATE_TIME || field?.type == QFieldType.DATE) if (field?.type == QFieldType.DATE_TIME)
{ {
defaultOperator = QCriteriaOperator.GREATER_THAN; defaultOperator = QCriteriaOperator.GREATER_THAN;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -22,16 +22,25 @@
import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData"; import {QWidgetMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QWidgetMetaData";
import {Box, Skeleton} from "@mui/material"; import {Box, Skeleton} from "@mui/material";
import Card from "@mui/material/Card";
import Modal from "@mui/material/Modal";
import parse from "html-react-parser";
import {BlockData} from "qqq/components/widgets/blocks/BlockModels"; import {BlockData} from "qqq/components/widgets/blocks/BlockModels";
import WidgetBlock from "qqq/components/widgets/WidgetBlock"; import WidgetBlock from "qqq/components/widgets/WidgetBlock";
import React from "react"; import ProcessWidgetBlockUtils from "qqq/pages/processes/ProcessWidgetBlockUtils";
import React, {useEffect, useState} from "react";
interface CompositeData export interface CompositeData
{ {
blockId: string;
blocks: BlockData[]; blocks: BlockData[];
styleOverrides?: any; styleOverrides?: any;
layout?: string; layout?: string;
overlayHtml?: string;
overlayStyleOverrides?: any;
modalMode: string;
styles?: any;
} }
@ -39,13 +48,15 @@ interface CompositeWidgetProps
{ {
widgetMetaData: QWidgetMetaData; widgetMetaData: QWidgetMetaData;
data: CompositeData; data: CompositeData;
actionCallback?: (blockData: BlockData, eventValues?: { [name: string]: any }) => boolean;
values?: { [key: string]: any };
} }
/******************************************************************************* /*******************************************************************************
** Widget which is a list of Blocks. ** Widget which is a list of Blocks.
*******************************************************************************/ *******************************************************************************/
export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetProps): JSX.Element export default function CompositeWidget({widgetMetaData, data, actionCallback, values}: CompositeWidgetProps): JSX.Element
{ {
if (!data || !data.blocks) if (!data || !data.blocks)
{ {
@ -71,6 +82,12 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
boxStyle.flexWrap = "wrap"; boxStyle.flexWrap = "wrap";
boxStyle.gap = "0.5rem"; boxStyle.gap = "0.5rem";
} }
else if (layout == "FLEX_ROW")
{
boxStyle.display = "flex";
boxStyle.flexDirection = "row";
boxStyle.gap = "0.5rem";
}
else if (layout == "FLEX_ROW_SPACE_BETWEEN") else if (layout == "FLEX_ROW_SPACE_BETWEEN")
{ {
boxStyle.display = "flex"; boxStyle.display = "flex";
@ -78,6 +95,14 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
boxStyle.justifyContent = "space-between"; boxStyle.justifyContent = "space-between";
boxStyle.gap = "0.25rem"; boxStyle.gap = "0.25rem";
} }
else if (layout == "FLEX_ROW_CENTER")
{
boxStyle.display = "flex";
boxStyle.flexDirection = "row";
boxStyle.justifyContent = "center";
boxStyle.gap = "0.25rem";
boxStyle.flexWrap = "wrap";
}
else if (layout == "TABLE_SUB_ROW_DETAILS") else if (layout == "TABLE_SUB_ROW_DETAILS")
{ {
boxStyle.display = "flex"; boxStyle.display = "flex";
@ -97,20 +122,96 @@ export default function CompositeWidget({widgetMetaData, data}: CompositeWidgetP
boxStyle.borderRadius = "0.5rem"; boxStyle.borderRadius = "0.5rem";
boxStyle.background = "#FFFFFF"; boxStyle.background = "#FFFFFF";
} }
if (data?.styleOverrides) if (data?.styleOverrides)
{ {
boxStyle = {...boxStyle, ...data.styleOverrides}; boxStyle = {...boxStyle, ...data.styleOverrides};
} }
return (<Box sx={boxStyle} className="compositeWidget"> if (data.styles?.backgroundColor)
{
boxStyle.backgroundColor = ProcessWidgetBlockUtils.processColorFromStyleMap(data.styles.backgroundColor);
}
if (data.styles?.padding)
{
boxStyle.paddingTop = data.styles?.padding.top + "px"
boxStyle.paddingBottom = data.styles?.padding.bottom + "px"
boxStyle.paddingLeft = data.styles?.padding.left + "px"
boxStyle.paddingRight = data.styles?.padding.right + "px"
}
let overlayStyle: any = {};
if (data?.overlayStyleOverrides)
{
overlayStyle = {...overlayStyle, ...data.overlayStyleOverrides};
}
const content = (
<>
{
data?.overlayHtml &&
<Box sx={overlayStyle} className="blockWidgetOverlay">{parse(data.overlayHtml)}</Box>
}
<Box sx={boxStyle} className="compositeWidget">
{ {
data.blocks.map((block: BlockData, index) => ( data.blocks.map((block: BlockData, index) => (
<React.Fragment key={index}> <React.Fragment key={index}>
<WidgetBlock widgetMetaData={widgetMetaData} block={block} /> <WidgetBlock widgetMetaData={widgetMetaData} block={block} actionCallback={actionCallback} values={values} />
</React.Fragment> </React.Fragment>
)) ))
} }
</Box>); </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;
}
} }

View File

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

View File

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

View File

@ -0,0 +1,40 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
import DumpJsonBox from "qqq/utils/DumpJsonBox";
import React from "react";
/*******************************************************************************
** Block that renders ... an audio tag
**
** <audio src=${path} ${autoPlay} ${showControls} />
*******************************************************************************/
export default function AudioBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
{
return (
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<audio src={data.values?.path} autoPlay={data.values?.autoPlay} controls={data.values?.showControls} />
</BlockElementWrapper>
);
}

View File

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

View File

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

View File

@ -0,0 +1,86 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import Icon from "@mui/material/Icon";
import {standardWidth} from "qqq/components/buttons/DefaultButtons";
import MDButton from "qqq/components/legacy/MDButton";
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
import React from "react";
/*******************************************************************************
** Block that renders ... a button...
**
*******************************************************************************/
export default function ButtonBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
{
const startIcon = data.values.startIcon?.name ? <Icon>{data.values.startIcon.name}</Icon> : null;
const endIcon = data.values.endIcon?.name ? <Icon>{data.values.endIcon.name}</Icon> : null;
function onClick()
{
if (actionCallback)
{
actionCallback(data, data.values);
}
else
{
console.log("ButtonBlock onClick with no actionCallback present, so, noop");
}
}
let buttonVariant: "gradient" | "outlined" | "text" = "gradient";
if (data.styles?.format == "outlined")
{
buttonVariant = "outlined";
}
else if (data.styles?.format == "text")
{
buttonVariant = "text";
}
else if (data.styles?.format == "filled")
{
buttonVariant = "gradient";
}
// todo - button colors... but to do RGB's, might need to move away from MDButton?
return (
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<Box mx={1} my={1} minWidth={standardWidth}>
<MDButton
type="button"
variant={buttonVariant}
color="dark"
size="small"
fullWidth
startIcon={startIcon}
endIcon={endIcon}
onClick={onClick}
>
{data.values.label ?? "Button"}
</MDButton>
</Box>
</BlockElementWrapper>
);
}

View File

@ -0,0 +1,59 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
import DumpJsonBox from "qqq/utils/DumpJsonBox";
import React from "react";
/*******************************************************************************
** Block that renders ... an image tag
**
** <audio src=${path} ${autoPlay} ${showControls} />
*******************************************************************************/
export default function AudioBlock({widgetMetaData, data}: StandardBlockComponentProps): JSX.Element
{
let imageStyle: any = {};
if(data.styles?.width)
{
imageStyle.width = data.styles?.width;
}
if(data.styles?.height)
{
imageStyle.height = data.styles?.height;
}
if(data.styles?.bordered)
{
imageStyle.border = "1px solid #C0C0C0";
imageStyle.borderRadius = "0.5rem";
}
return (
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<img src={data.values?.path} alt={data.values?.alt} style={imageStyle} />
</BlockElementWrapper>
);
}

View File

@ -0,0 +1,139 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import Box from "@mui/material/Box";
import QDynamicFormField from "qqq/components/forms/DynamicFormField";
import DynamicFormUtils from "qqq/components/forms/DynamicFormUtils";
import BlockElementWrapper from "qqq/components/widgets/blocks/BlockElementWrapper";
import {StandardBlockComponentProps} from "qqq/components/widgets/blocks/BlockModels";
import React, {SyntheticEvent, useState} from "react";
/*******************************************************************************
** Block that renders ... a text input
**
*******************************************************************************/
export default function InputFieldBlock({widgetMetaData, data, actionCallback}: StandardBlockComponentProps): JSX.Element
{
const [blurCount, setBlurCount] = useState(0)
const fieldMetaData = new QFieldMetaData(data.values.fieldMetaData);
const dynamicField = DynamicFormUtils.getDynamicField(fieldMetaData);
let autoFocus = data.values.autoFocus as boolean
let value = data.values.value;
if(value == null || value == undefined)
{
value = "";
}
////////////////////////////////////////////////////////////////////////////////
// for an autoFocus field... //
// we're finding that if we blur it when clicking an action button, that //
// an un-desirable "now it's been touched, so show an error" happens. //
// so let us remove the default blur handler, for the first (auto) focus/blur //
// cycle, and we seem to have a better time. //
////////////////////////////////////////////////////////////////////////////////
let dynamicFormFieldRest: {onBlur?: any, sx?: any} = {}
if(autoFocus && blurCount == 0)
{
dynamicFormFieldRest.onBlur = (event: React.SyntheticEvent) =>
{
event.stopPropagation();
event.preventDefault();
setBlurCount(blurCount + 1);
}
}
/***************************************************************************
**
***************************************************************************/
function eventHandler(event: KeyboardEvent)
{
if(data.values.submitOnEnter && event.key == "Enter")
{
// @ts-ignore target.value...
const inputValue = event.target.value?.trim()
// todo - make this behavior opt-in for inputBlocks?
if(inputValue && `${inputValue}`.startsWith("->"))
{
const actionCode = inputValue.substring(2);
if(actionCallback)
{
actionCallback(data, {actionCode: actionCode, _fieldToClearIfError: fieldMetaData.name});
///////////////////////////////////////////////////////
// return, so we don't submit the actionCode as text //
///////////////////////////////////////////////////////
return;
}
}
if(fieldMetaData.isRequired && inputValue == "")
{
console.log("input field is required, but missing value, so not submitting");
return;
}
if(actionCallback)
{
console.log("InputFieldBlock calling actionCallback for submitOnEnter");
let values: {[name: string]: any} = {};
values[fieldMetaData.name] = inputValue;
actionCallback(data, values);
}
else
{
console.log("InputFieldBlock was set as submitOnEnter, but no actionCallback was present, so, noop");
}
}
}
const labelElement = <Box fontSize="1rem" fontWeight="500" marginBottom="0.25rem">
<label htmlFor={fieldMetaData.name}>{fieldMetaData.label}</label>
</Box>
return (
<Box mt="0.5rem">
<BlockElementWrapper metaData={widgetMetaData} data={data} slot="">
<>
{labelElement}
<QDynamicFormField
name={fieldMetaData.name}
displayFormat={null}
label=""
placeholder={data.values?.placeholder}
backgroundColor="#FFFFFF"
formFieldObject={dynamicField}
type={fieldMetaData.type}
value={value}
autoFocus={autoFocus}
onKeyUp={eventHandler}
{...dynamicFormFieldRest} />
</>
</BlockElementWrapper>
</Box>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,96 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2022. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QTableMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QTableMetaData";
import Modal from "@mui/material/Modal";
import EntityForm from "qqq/components/forms/EntityForm";
import Client from "qqq/utils/qqq/Client";
import React, {useEffect, useReducer, useState} from "react";
////////////////////////////////
// structure of expected data //
////////////////////////////////
export interface ModalEditFormData
{
tableName: string;
defaultValues?: { [key: string]: string };
disabledFields?: { [key: string]: boolean } | string[];
overrideHeading?: string;
onSubmitCallback?: (values: any) => void;
initialShowModalValue?: boolean;
}
const qController = Client.getInstance();
function ModalEditForm({tableName, defaultValues, disabledFields, overrideHeading, onSubmitCallback, initialShowModalValue}: ModalEditFormData,): JSX.Element
{
const [showModal, setShowModal] = useState(initialShowModalValue);
const [table, setTable] = useState(null as QTableMetaData);
const [, forceUpdate] = useReducer((x) => x + 1, 0);
useEffect(() =>
{
if (!tableName)
{
return;
}
(async () =>
{
const tableMetaData = await qController.loadTableMetaData(tableName);
setTable(tableMetaData);
forceUpdate();
})();
}, [tableName]);
/*******************************************************************************
**
*******************************************************************************/
const closeEditChildForm = (event: object, reason: string) =>
{
if (reason === "backdropClick" || reason === "escapeKeyDown")
{
return;
}
setShowModal(null);
};
return (
table && showModal &&
<Modal open={showModal as boolean} onClose={(event, reason) => closeEditChildForm(event, reason)}>
<div className="modalEditForm">
<EntityForm
isModal={true}
closeModalHandler={closeEditChildForm}
table={table}
defaultValues={defaultValues}
disabledFields={disabledFields}
onSubmitCallback={onSubmitCallback}
overrideHeading={overrideHeading}
/>
</div>
</Modal>
);
}
export default ModalEditForm;

View File

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

View File

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

View File

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

View File

@ -0,0 +1,38 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QPossibleValue} from "@kingsrook/qqq-frontend-core/lib/model/QPossibleValue";
/*******************************************************************************
** Properties attached to a (formik?) form field, to denote how it behaves as
** as related to a possible value source.
*******************************************************************************/
export interface FieldPossibleValueProps
{
isPossibleValue?: boolean;
possibleValues?: QPossibleValue[];
initialDisplayValue: string | null;
fieldName?: string;
tableName?: string;
processName?: string;
possibleValueSourceName?: string;
}

View File

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

View File

@ -0,0 +1,268 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {QComponentType} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QComponentType";
import {QFieldMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFieldMetaData";
import {QFrontendStepMetaData} from "@kingsrook/qqq-frontend-core/lib/model/metaData/QFrontendStepMetaData";
import {CompositeData} from "qqq/components/widgets/CompositeWidget";
/*******************************************************************************
** Utility functions used by ProcessRun for working with ad-hoc, block &
** composite type widgets.
**
*******************************************************************************/
export default class ProcessWidgetBlockUtils
{
/*******************************************************************************
**
*******************************************************************************/
public static isActionCodeValid(actionCode: string, step: QFrontendStepMetaData, processValues: any): boolean
{
///////////////////////////////////////////////////////////
// private recursive function to walk the composite tree //
///////////////////////////////////////////////////////////
function recursiveIsActionCodeValidForCompositeData(compositeWidgetData: CompositeData): boolean
{
for (let i = 0; i < compositeWidgetData.blocks.length; i++)
{
const block = compositeWidgetData.blocks[i];
////////////////////////////////////////////////////////////////
// skip the block if it has a 'conditional', which isn't true //
////////////////////////////////////////////////////////////////
const conditionalFieldName = block.conditional;
if (conditionalFieldName)
{
const value = processValues[conditionalFieldName];
if (!value)
{
continue;
}
}
if (block.blockTypeName == "COMPOSITE")
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// recursive call for composites, but only return if a true is found (in case a subsequent block has a true) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
const isValidForThisBlock = recursiveIsActionCodeValidForCompositeData(block as unknown as CompositeData);
if (isValidForThisBlock)
{
return (true);
}
// else, continue...
}
else if (block.blockTypeName == "BUTTON")
{
//////////////////////////////////////////
// look at actionCodes on button blocks //
//////////////////////////////////////////
if (block.values?.actionCode == actionCode)
{
return (true);
}
}
}
/////////////////////////////////////////
// if code wasn't found, it is invalid //
/////////////////////////////////////////
return false;
}
/////////////////////////////////////////////////////
// iterate over all components in the current step //
/////////////////////////////////////////////////////
for (let i = 0; i < step.components.length; i++)
{
const component = step.components[i];
if (component.type == "WIDGET" && component.values?.isAdHocWidget)
{
///////////////////////////////////////////////////////////////////////////////////////////////
// for ad-hoc widget components, check if this actionCode exists on any action-button blocks //
///////////////////////////////////////////////////////////////////////////////////////////////
const isValidForThisWidget = recursiveIsActionCodeValidForCompositeData(component.values);
if (isValidForThisWidget)
{
return (true);
}
}
}
////////////////////////////////////
// upon fallthrough, it's a false //
////////////////////////////////////
return false;
}
/***************************************************************************
** perform evaluations on a compositeWidget's data, given current process
** values, to do dynamic stuff, like:
** - removing fields with un-true conditions
***************************************************************************/
public static dynamicEvaluationOfCompositeWidgetData(compositeWidgetData: CompositeData, processValues: any)
{
for (let i = 0; i < compositeWidgetData.blocks.length; i++)
{
const block = compositeWidgetData.blocks[i];
////////////////////////////////////////////////////////////////////
// if the block has a conditional, evaluate, and remove if untrue //
////////////////////////////////////////////////////////////////////
const conditionalFieldName = block.conditional;
if (conditionalFieldName)
{
const value = processValues[conditionalFieldName];
if (!value)
{
console.debug(`Splicing away block based on [${conditionalFieldName}]...`);
compositeWidgetData.blocks.splice(i, 1);
i--;
continue;
}
}
if (block.blockTypeName == "COMPOSITE")
{
/////////////////////////////////////////
// make recursive calls for composites //
/////////////////////////////////////////
ProcessWidgetBlockUtils.dynamicEvaluationOfCompositeWidgetData(block as unknown as CompositeData, processValues);
}
else if (block.blockTypeName == "INPUT_FIELD")
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// for input fields, put the process's value for the field-name into the block's values object as '.value' //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
const fieldName = block.values?.fieldMetaData?.name;
if (processValues.hasOwnProperty(fieldName))
{
block.values.value = processValues[fieldName];
}
}
else if (block.blockTypeName == "TEXT")
{
//////////////////////////////////////////////////////////////////////////////////////
// for text-blocks - interpolate ${fieldName} expressions into their process-values //
//////////////////////////////////////////////////////////////////////////////////////
let text = block.values?.text;
if (text)
{
for (let key of Object.keys(processValues))
{
text = text.replaceAll("${" + key + "}", processValues[key]);
}
block.values.interpolatedText = text;
}
}
}
}
/***************************************************************************
**
***************************************************************************/
public static addFieldsForCompositeWidget(step: QFrontendStepMetaData, processValues: any, addFieldCallback: (fieldMetaData: QFieldMetaData) => void)
{
///////////////////////////////////////////////////////////
// private recursive function to walk the composite tree //
///////////////////////////////////////////////////////////
function recursiveHelper(widgetData: CompositeData)
{
try
{
for (let block of widgetData.blocks)
{
if (block.blockTypeName == "COMPOSITE")
{
recursiveHelper(block as unknown as CompositeData);
}
else if (block.blockTypeName == "INPUT_FIELD")
{
const fieldMetaData = new QFieldMetaData(block.values?.fieldMetaData);
addFieldCallback(fieldMetaData);
}
}
}
catch (e)
{
console.log("Error adding fields for compositeWidget: " + e);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// foreach component, if it's an adhoc widget or a widget w/ its data in the processValues, then, call recursive helper on it //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
for (let component of step.components)
{
if (component.type == QComponentType.WIDGET && component.values?.isAdHocWidget)
{
recursiveHelper(component.values as unknown as CompositeData);
}
else if (component.type == QComponentType.WIDGET && processValues[component.values?.widgetName])
{
recursiveHelper(processValues[component.values?.widgetName] as unknown as CompositeData);
}
}
}
/***************************************************************************
**
***************************************************************************/
public static processColorFromStyleMap(colorFromStyleMap?: string): string
{
if (colorFromStyleMap)
{
switch (colorFromStyleMap.toUpperCase())
{
case "SUCCESS":
return("#2BA83F");
case "WARNING":
return("#FBA132");
case "ERROR":
return("#FB4141");
case "INFO":
return("#458CFF");
case "MUTED":
return("#7b809a");
default:
{
if (colorFromStyleMap.match(/^[0-9A-F]{6}$/))
{
return(`#${colorFromStyleMap}`);
}
else if (colorFromStyleMap.match(/^[0-9A-F]{8}$/))
{
return(`#${colorFromStyleMap}`);
}
else
{
return(colorFromStyleMap);
}
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -205,6 +205,8 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext); const {accentColor, setPageHeader, tableMetaData, setTableMetaData, tableProcesses, setTableProcesses, dotMenuOpen, keyboardHelpOpen, helpHelpActive, recordAnalytics, userId: currentUserId} = useContext(QContext);
const CREATE_CHILD_KEY = "createChild";
if (localStorage.getItem(tableVariantLocalStorageKey)) if (localStorage.getItem(tableVariantLocalStorageKey))
{ {
tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey)); tableVariant = JSON.parse(localStorage.getItem(tableVariantLocalStorageKey));
@ -307,12 +309,19 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
// the path for a process looks like: .../table/id/process // // the path for a process looks like: .../table/id/process //
// the path for creating a child record looks like: .../table/id/createChild/:childTableName // // the path for creating a child record looks like: .../table/id/createChild/:childTableName //
// the path for creating a child record in a process looks like: //
// .../table/id/processName#/createChild=... //
/////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////
let hasChildRecordKey = pathParts.some(p => p.includes(CREATE_CHILD_KEY));
if (!hasChildRecordKey)
{
hasChildRecordKey = hashParts.some(h => h.includes(CREATE_CHILD_KEY));
}
////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if our tableName is in the -3 index, try to open process // // if our tableName is in the -3 index, and there is no token for updating child records, try to open process //
////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 3] === tableName) if (!hasChildRecordKey && pathParts[pathParts.length - 3] === tableName)
{ {
const processName = pathParts[pathParts.length - 1]; const processName = pathParts[pathParts.length - 1];
const processList = allTableProcesses.filter(p => p.name.endsWith(processName)); const processList = allTableProcesses.filter(p => p.name.endsWith(processName));
@ -349,7 +358,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
// if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form // // if our table is in the -4 index, and there's `createChild` in the -2 index, try to open a createChild form //
// e.g., person/42/createChild/address (to create an address under person 42) // // e.g., person/42/createChild/address (to create an address under person 42) //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if (pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == "createChild") if (pathParts[pathParts.length - 4] === tableName && pathParts[pathParts.length - 2] == CREATE_CHILD_KEY)
{ {
(async () => (async () =>
{ {
@ -368,7 +377,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
for (let i = 0; i < hashParts.length; i++) for (let i = 0; i < hashParts.length; i++)
{ {
const parts = hashParts[i].split("="); const parts = hashParts[i].split("=");
if (parts.length > 1 && parts[0] == "createChild") if (parts.length > 1 && parts[0] == CREATE_CHILD_KEY)
{ {
(async () => (async () =>
{ {
@ -831,7 +840,7 @@ function RecordView({table, record: overrideRecord, launchProcess}: Props): JSX.
const ownerId = record.values.get(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName); const ownerId = record.values.get(tableMetaData.shareableTableMetaData.thisTableOwnerIdFieldName);
if (ownerId != currentUserId) if (ownerId != currentUserId)
{ {
disabledTooltipText = `Only the owner of a ${tableMetaData.label} may share it.` disabledTooltipText = `Only the owner of a ${tableMetaData.label} may share it.`;
shareDisabled = true; shareDisabled = true;
} }
else else

View File

@ -787,3 +787,20 @@ input[type="search"]::-webkit-search-results-decoration
{ {
margin: 2rem 1rem; margin: 2rem 1rem;
} }
/* default styles for a block widget overlay */
.blockWidgetOverlay
{
font-weight: 400;
position: relative;
top: 15px;
height: 0;
display: flex;
font-size: 14px;
flex-direction: column;
align-items: center;
}
.blockWidgetOverlay a
{
color: #0062FF !important;
}

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

View File

@ -0,0 +1,50 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Box from "@mui/material/Box";
import React from "react";
interface DumpJsonBoxProps
{
data: any;
title?: string;
}
/***************************************************************************
** Utillity for debugging an object as JSON
***************************************************************************/
export default function DumpJsonBox({data, title}: DumpJsonBoxProps): JSX.Element
{
return (
<Box border="1px solid gray" my="1rem" borderRadius="0.5rem">
{
title &&
<Box borderBottom="1px solid gray" mb="0.5rem" px="0.25rem" borderRadius="0.5rem 0.5rem 0 0" fontSize="1rem" fontWeight="600kkk" sx={{backgroundColor: "#D0D0D0"}}>
{title}
</Box>
}
<Box maxHeight="200px" p="0.25rem" overflow="auto" sx={{whiteSpace: "pre-wrap", fontFamily: "monospace", fontSize: "0.75rem", lineHeight: "1.2"}}>
{JSON.stringify(data, null, 3)}
</Box>
</Box>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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