@evoke-platform/ui-components 1.8.0-dev.1 → 1.8.0-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/published/components/core/TextField/TextField.js +3 -2
- package/dist/published/components/custom/DataGrid/DataGrid.d.ts +1 -0
- package/dist/published/components/custom/DataGrid/DataGrid.js +2 -1
- package/dist/published/components/custom/DataGrid/Toolbar.d.ts +1 -0
- package/dist/published/components/custom/DataGrid/Toolbar.js +3 -2
- package/dist/published/components/custom/DataGrid/index.d.ts +1 -0
- package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +4 -0
- package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.js +4 -0
- package/dist/published/components/custom/Form/utils.js +76 -44
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +6 -2
- package/dist/published/components/custom/FormV2/FormRenderer.js +13 -14
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +7 -2
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +61 -109
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +4 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +9 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +12 -24
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +5 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +80 -30
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +51 -27
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +5 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +45 -7
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +8 -6
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +3 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +1 -3
- package/dist/published/components/custom/FormV2/components/types.d.ts +7 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +27 -2
- package/dist/published/components/custom/FormV2/components/utils.js +108 -2
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +173 -0
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.d.ts +1 -0
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +96 -0
- package/dist/published/components/custom/FormV2/tests/test-data.d.ts +16 -0
- package/dist/published/components/custom/FormV2/tests/test-data.js +394 -0
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/FormRenderer.stories.d.ts +7 -0
- package/dist/published/stories/FormRenderer.stories.js +65 -0
- package/dist/published/stories/FormRendererContainer.stories.d.ts +7 -0
- package/dist/published/stories/FormRendererContainer.stories.js +56 -0
- package/dist/published/stories/FormRendererData.d.ts +116 -0
- package/dist/published/stories/FormRendererData.js +925 -0
- package/dist/published/stories/sharedMswHandlers.d.ts +1 -0
- package/dist/published/stories/sharedMswHandlers.js +100 -0
- package/dist/published/theme/hooks.d.ts +4 -0
- package/package.json +12 -4
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { useApiServices, useApp, useNavigate, } from '@evoke-platform/context';
|
|
2
2
|
import cleanDeep from 'clean-deep';
|
|
3
|
-
import { cloneDeep, debounce, isEmpty, isNil } from 'lodash';
|
|
3
|
+
import { cloneDeep, debounce, isEmpty, isEqual, isNil } from 'lodash';
|
|
4
4
|
import Handlebars from 'no-eval-handlebars';
|
|
5
|
-
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { Close } from '../../../../../../icons';
|
|
7
7
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
8
|
-
import { Autocomplete, Button, Dialog, IconButton, Link, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
|
|
8
|
+
import { Autocomplete, Button, Dialog, DialogContent, DialogTitle, IconButton, Link, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
|
|
9
9
|
import { Box } from '../../../../../layout';
|
|
10
10
|
import { encodePageSlug, getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
|
|
11
11
|
import RelatedObjectInstance from './RelatedObjectInstance';
|
|
12
12
|
const ObjectPropertyInput = (props) => {
|
|
13
|
-
const { id, fieldDefinition, nestedFieldsView, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, } = props;
|
|
13
|
+
const { id, fieldDefinition, nestedFieldsView, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, } = props;
|
|
14
14
|
const { fetchedOptions, setFetchedOptions, parameters, fieldHeight, handleChange: handleChangeObjectField, instance, } = useFormContext();
|
|
15
15
|
const { defaultPages, findDefaultPageSlugFor } = useApp();
|
|
16
16
|
const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
|
|
@@ -30,15 +30,12 @@ const ObjectPropertyInput = (props) => {
|
|
|
30
30
|
showAlert: false,
|
|
31
31
|
isError: true,
|
|
32
32
|
});
|
|
33
|
-
const
|
|
34
|
-
const action = relatedObject?.actions?.find((action) => action.id === DEFAULT_CREATE_ACTION);
|
|
33
|
+
const action = relatedObject?.actions?.find((action) => action.id === createActionId);
|
|
35
34
|
const apiServices = useApiServices();
|
|
36
35
|
const navigateTo = useNavigate();
|
|
37
|
-
const updatedCriteria =
|
|
38
|
-
? {
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
: undefined;
|
|
36
|
+
const updatedCriteria = useMemo(() => {
|
|
37
|
+
return filter ? { where: transformToWhere(filter) } : undefined;
|
|
38
|
+
}, [filter]);
|
|
42
39
|
useEffect(() => {
|
|
43
40
|
if (relatedObject) {
|
|
44
41
|
let defaultViewLayout;
|
|
@@ -94,10 +91,12 @@ const ObjectPropertyInput = (props) => {
|
|
|
94
91
|
}
|
|
95
92
|
}, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
|
|
96
93
|
const getDropdownOptions = useCallback((name) => {
|
|
97
|
-
if ((!fetchedOptions[`${id}Options`] ||
|
|
98
|
-
fetchedOptions[`${id}Options`].length === 0) &&
|
|
99
|
-
!hasFetched)
|
|
94
|
+
if (((!fetchedOptions?.[`${id}Options`] ||
|
|
95
|
+
(fetchedOptions?.[`${id}Options`]).length === 0) &&
|
|
96
|
+
!hasFetched) ||
|
|
97
|
+
!isEqual(fetchedOptions?.[`${id}UpdatedCriteria`], updatedCriteria)) {
|
|
100
98
|
setLoadingOptions(true);
|
|
99
|
+
setFetchedOptions && setFetchedOptions({ [`${id}UpdatedCriteria`]: updatedCriteria });
|
|
101
100
|
const updatedFilter = cloneDeep(updatedCriteria) || {};
|
|
102
101
|
updatedFilter.limit = 100;
|
|
103
102
|
const { propertyId, direction } = layout?.sort ?? {
|
|
@@ -146,17 +145,38 @@ const ObjectPropertyInput = (props) => {
|
|
|
146
145
|
return () => debouncedGetDropdownOptions.cancel();
|
|
147
146
|
}, [dropdownInput]);
|
|
148
147
|
useEffect(() => {
|
|
149
|
-
if (action?.defaultFormId) {
|
|
148
|
+
if (formId || action?.defaultFormId) {
|
|
150
149
|
apiServices
|
|
151
|
-
.get(getPrefixedUrl(
|
|
150
|
+
.get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
|
|
152
151
|
.then((evokeForm) => {
|
|
153
152
|
setForm(evokeForm);
|
|
154
153
|
})
|
|
155
154
|
.catch((error) => {
|
|
156
|
-
console.error(
|
|
155
|
+
console.error(error);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
else if (action) {
|
|
159
|
+
apiServices
|
|
160
|
+
.get(getPrefixedUrl('/forms'), {
|
|
161
|
+
params: {
|
|
162
|
+
filter: {
|
|
163
|
+
where: {
|
|
164
|
+
actionId: action.id,
|
|
165
|
+
objectId: fieldDefinition.objectId,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
.then((matchingForms) => {
|
|
171
|
+
if (matchingForms.length === 1) {
|
|
172
|
+
setForm(matchingForms[0]);
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
.catch((error) => {
|
|
176
|
+
console.error(error);
|
|
157
177
|
});
|
|
158
178
|
}
|
|
159
|
-
}, [action]);
|
|
179
|
+
}, [action, formId]);
|
|
160
180
|
useEffect(() => {
|
|
161
181
|
if (!fetchedOptions[`${id}RelatedObject`]) {
|
|
162
182
|
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
|
|
@@ -254,7 +274,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
254
274
|
},
|
|
255
275
|
} },
|
|
256
276
|
mode !== 'newOnly' && children,
|
|
257
|
-
mode !== 'existingOnly' &&
|
|
277
|
+
mode !== 'existingOnly' && createActionId && (React.createElement(Button, { fullWidth: true, sx: {
|
|
258
278
|
justifyContent: 'flex-start',
|
|
259
279
|
pl: 2,
|
|
260
280
|
minHeight: '48px',
|
|
@@ -422,15 +442,19 @@ const ObjectPropertyInput = (props) => {
|
|
|
422
442
|
event.stopPropagation();
|
|
423
443
|
setOpenCreateDialog(true);
|
|
424
444
|
}, "aria-label": `Add` }, "Add")))),
|
|
425
|
-
openCreateDialog && (React.createElement(React.Fragment, null, nestedFieldsView ? (React.createElement(RelatedObjectInstance, { id: id, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, nestedFieldsView: nestedFieldsView, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout,
|
|
426
|
-
React.createElement(
|
|
427
|
-
|
|
428
|
-
fontSize: '22px',
|
|
445
|
+
openCreateDialog && (React.createElement(React.Fragment, null, nestedFieldsView ? (React.createElement(RelatedObjectInstance, { id: id, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, nestedFieldsView: nestedFieldsView, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: form?.id, actionId: createActionId, setSnackbarError: setSnackbarError, fieldDefinition: fieldDefinition })) : (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: openCreateDialog, onClose: (e, reason) => reason !== 'backdropClick' && handleClose },
|
|
446
|
+
React.createElement(DialogTitle, { sx: {
|
|
447
|
+
fontSize: '18px',
|
|
429
448
|
fontWeight: 700,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
449
|
+
paddingTop: '35px',
|
|
450
|
+
paddingBottom: '20px',
|
|
451
|
+
borderBottom: '1px solid #e9ecef',
|
|
452
|
+
} },
|
|
453
|
+
React.createElement(IconButton, { sx: { position: 'absolute', right: '17px', top: '22px' }, onClick: handleClose },
|
|
454
|
+
React.createElement(Close, { fontSize: "small" })),
|
|
455
|
+
form?.name ?? `Add ${fieldDefinition.name}`),
|
|
456
|
+
React.createElement(DialogContent, { sx: { padding: '0px' } },
|
|
457
|
+
React.createElement(RelatedObjectInstance, { handleClose: handleClose, setSelectedInstance: setSelectedInstance, nestedFieldsView: nestedFieldsView, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError, fieldDefinition: fieldDefinition })))))),
|
|
434
458
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
|
|
435
459
|
isError: snackbarError.isError,
|
|
436
460
|
showAlert: false,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InputParameter, Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { BaseProps } from '../../types';
|
|
4
4
|
export type RelatedObjectInstanceProps = BaseProps & {
|
|
5
|
-
relatedObject: Obj | undefined;
|
|
6
5
|
id: string;
|
|
6
|
+
relatedObject: Obj | undefined;
|
|
7
7
|
setSelectedInstance: (selectedInstance: ObjectInstance) => void;
|
|
8
8
|
handleClose: () => void;
|
|
9
9
|
mode: 'default' | 'existingOnly' | 'newOnly';
|
|
@@ -18,9 +18,9 @@ export type RelatedObjectInstanceProps = BaseProps & {
|
|
|
18
18
|
options: ObjectInstance[];
|
|
19
19
|
filter?: Record<string, unknown>;
|
|
20
20
|
layout?: TableViewLayout;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
formId?: string;
|
|
22
|
+
actionId?: string;
|
|
23
|
+
fieldDefinition: InputParameter;
|
|
24
24
|
};
|
|
25
25
|
declare const RelatedObjectInstance: (props: RelatedObjectInstanceProps) => React.JSX.Element;
|
|
26
26
|
export default RelatedObjectInstance;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { useApiServices } from '@evoke-platform/context';
|
|
1
2
|
import { InfoRounded } from '@mui/icons-material';
|
|
2
3
|
import React, { useState } from 'react';
|
|
3
4
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
4
5
|
import { Alert, Button, FormControlLabel, Radio, RadioGroup } from '../../../../../core';
|
|
5
6
|
import { Box, Grid } from '../../../../../layout';
|
|
7
|
+
import FormRendererContainer from '../../../FormRendererContainer';
|
|
8
|
+
import { formatSubmission, getPrefixedUrl } from '../../utils';
|
|
6
9
|
import InstanceLookup from './InstanceLookup';
|
|
7
10
|
const styles = {
|
|
8
11
|
actionButtons: {
|
|
@@ -14,11 +17,12 @@ const styles = {
|
|
|
14
17
|
},
|
|
15
18
|
};
|
|
16
19
|
const RelatedObjectInstance = (props) => {
|
|
17
|
-
const { relatedObject, id, setSelectedInstance, handleClose, nestedFieldsView, mode, displayOption, filter, layout,
|
|
18
|
-
const { handleChange: handleChangeObjectField } = useFormContext();
|
|
20
|
+
const { relatedObject, id, setSelectedInstance, handleClose, nestedFieldsView, mode, displayOption, filter, layout, formId, actionId, fieldDefinition, setSnackbarError, setOptions, options, } = props;
|
|
21
|
+
const { handleChange: handleChangeObjectField, richTextEditor, stickyFooter, fieldHeight } = useFormContext();
|
|
19
22
|
const [errors, setErrors] = useState([]);
|
|
20
23
|
const [selectedRow, setSelectedRow] = useState();
|
|
21
24
|
const [relationType, setRelationType] = useState(displayOption === 'dropdown' ? 'new' : 'existing');
|
|
25
|
+
const apiServices = useApiServices();
|
|
22
26
|
const linkExistingInstance = async () => {
|
|
23
27
|
if (selectedRow) {
|
|
24
28
|
setSelectedInstance(selectedRow);
|
|
@@ -30,19 +34,53 @@ const RelatedObjectInstance = (props) => {
|
|
|
30
34
|
handleClose();
|
|
31
35
|
setErrors([]);
|
|
32
36
|
};
|
|
37
|
+
const createNewInstance = async (submission) => {
|
|
38
|
+
if (!relatedObject) {
|
|
39
|
+
// Handle the case where relatedObject is undefined
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
submission = await formatSubmission(submission, apiServices, relatedObject.id);
|
|
43
|
+
try {
|
|
44
|
+
await apiServices
|
|
45
|
+
.post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
|
|
46
|
+
actionId: actionId,
|
|
47
|
+
input: submission,
|
|
48
|
+
})
|
|
49
|
+
.then((response) => {
|
|
50
|
+
handleChangeObjectField(id, response);
|
|
51
|
+
setSelectedInstance(response);
|
|
52
|
+
setSnackbarError({
|
|
53
|
+
showAlert: true,
|
|
54
|
+
message: 'New instance created',
|
|
55
|
+
isError: false,
|
|
56
|
+
});
|
|
57
|
+
setOptions(options.concat([response]));
|
|
58
|
+
onClose();
|
|
59
|
+
});
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
setSnackbarError({
|
|
64
|
+
showAlert: true,
|
|
65
|
+
message: err.response?.data?.error?.details?.[0]?.message ??
|
|
66
|
+
err.response?.data?.error?.message ??
|
|
67
|
+
`An error occurred. The new instance was not created.`,
|
|
68
|
+
isError: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
33
72
|
return (React.createElement(Box, { sx: {
|
|
34
73
|
background: nestedFieldsView ? '#F4F6F8' : 'none',
|
|
35
74
|
borderRadius: '8px',
|
|
36
75
|
} },
|
|
37
76
|
React.createElement(Box, { sx: {
|
|
38
|
-
padding: '8px 24px 0px',
|
|
39
77
|
'.MuiInputBase-root': { background: '#FFFF', borderRadius: '8px' },
|
|
40
78
|
} },
|
|
41
79
|
!nestedFieldsView &&
|
|
42
80
|
displayOption !== 'dropdown' &&
|
|
43
81
|
mode !== 'existingOnly' &&
|
|
44
82
|
mode !== 'newOnly' &&
|
|
45
|
-
|
|
83
|
+
actionId && (React.createElement(Grid, { container: true, sx: { paddingX: '24px' } },
|
|
46
84
|
React.createElement(Grid, { container: true, item: true },
|
|
47
85
|
React.createElement(RadioGroup, { row: true, "aria-labelledby": "related-object-link-type", onChange: (event) => {
|
|
48
86
|
event.target.value === 'existing' && setErrors([]);
|
|
@@ -57,9 +95,9 @@ const RelatedObjectInstance = (props) => {
|
|
|
57
95
|
"There are ",
|
|
58
96
|
React.createElement("strong", null, errors.length),
|
|
59
97
|
" errors")))) : undefined,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
React.createElement(InstanceLookup, { colspan: 12, nestedFieldsView: nestedFieldsView, setRelationType: setRelationType, object: relatedObject, setSelectedInstance: setSelectedRow, mode: mode, filter: filter, layout: layout }))))
|
|
98
|
+
relationType === 'new' || mode === 'newOnly' ? (React.createElement(Box, { sx: { width: '100%' } },
|
|
99
|
+
React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, stickyFooter: stickyFooter, objectId: fieldDefinition.objectId, onClose: onClose, onSubmit: createNewInstance, richTextEditor: richTextEditor }))) : ((mode === 'default' || mode === 'existingOnly') &&
|
|
100
|
+
relatedObject && (React.createElement(InstanceLookup, { colspan: 12, nestedFieldsView: nestedFieldsView, setRelationType: setRelationType, object: relatedObject, setSelectedInstance: setSelectedRow, mode: mode, filter: filter, layout: layout })))),
|
|
63
101
|
relationType !== 'new' && mode !== 'newOnly' && (React.createElement(Box, { sx: styles.actionButtons },
|
|
64
102
|
React.createElement(Button, { onClick: onClose, color: 'inherit', sx: {
|
|
65
103
|
border: '1px solid #ced4da',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
|
|
2
2
|
import { WarningRounded } from '@mui/icons-material';
|
|
3
|
-
import { cloneDeep } from 'lodash';
|
|
4
3
|
import React, { useEffect, useMemo } from 'react';
|
|
5
4
|
import { useResponsive } from '../../../../theme';
|
|
6
5
|
import { useFormContext } from '../../../../theme/hooks';
|
|
@@ -38,7 +37,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
|
|
|
38
37
|
}
|
|
39
38
|
export function RecursiveEntryRenderer(props) {
|
|
40
39
|
const { entry, isDocument } = props;
|
|
41
|
-
const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, } = useFormContext();
|
|
40
|
+
const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, associatedObject, } = useFormContext();
|
|
42
41
|
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
43
42
|
if (!entryIsVisible(entry, getValues(), instance)) {
|
|
44
43
|
return null;
|
|
@@ -70,7 +69,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
70
69
|
return undefined;
|
|
71
70
|
}
|
|
72
71
|
if (def?.enum && def.type === 'string') {
|
|
73
|
-
const cloned =
|
|
72
|
+
const cloned = structuredClone(def);
|
|
74
73
|
// single select must be made to be type choices for label and error handling
|
|
75
74
|
cloned.type = 'choices';
|
|
76
75
|
return cloned;
|
|
@@ -78,6 +77,8 @@ export function RecursiveEntryRenderer(props) {
|
|
|
78
77
|
return def;
|
|
79
78
|
}, [entry, parameters, object]);
|
|
80
79
|
const validation = fieldDefinition?.validation || {};
|
|
80
|
+
if (associatedObject?.propertyId === entryId)
|
|
81
|
+
return null;
|
|
81
82
|
useEffect(() => {
|
|
82
83
|
if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
|
|
83
84
|
fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
|
|
@@ -107,7 +108,9 @@ export function RecursiveEntryRenderer(props) {
|
|
|
107
108
|
? display?.defaultValue.orderBy
|
|
108
109
|
: undefined, defaultValueCriteria: typeof display?.defaultValue === 'object' && 'criteria' in display.defaultValue
|
|
109
110
|
? display?.defaultValue?.criteria
|
|
110
|
-
: undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description
|
|
111
|
+
: undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description,
|
|
112
|
+
// formId={display?.createFormId} // TODO: this should be added as part of the builder update
|
|
113
|
+
createActionId: '_create' })));
|
|
111
114
|
}
|
|
112
115
|
else if (fieldDefinition.type === 'user') {
|
|
113
116
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
@@ -116,7 +119,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
116
119
|
else if (fieldDefinition.type === 'collection') {
|
|
117
120
|
return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
118
121
|
React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] || initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: validation?.criteria, hasDescription: !!display?.description })))) : (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
|
|
119
|
-
React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout })));
|
|
122
|
+
React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry, createActionId: '_create', updateActionId: '_update', deleteActionId: '_delete' })));
|
|
120
123
|
}
|
|
121
124
|
else if (fieldDefinition.type === 'richText') {
|
|
122
125
|
return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, richTextEditor ? (React.createElement(richTextEditor, {
|
|
@@ -140,7 +143,6 @@ export function RecursiveEntryRenderer(props) {
|
|
|
140
143
|
else {
|
|
141
144
|
// Add `aria-describedby` to ensure screen readers read the description
|
|
142
145
|
// when the input is tabbed into.
|
|
143
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
146
|
const additionalProps = {};
|
|
145
147
|
if (fieldDefinition.enum && display?.description) {
|
|
146
148
|
additionalProps.renderInput = (params) => (React.createElement(TextField, { ...params, inputProps: {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { FieldErrors } from 'react-hook-form';
|
|
2
3
|
export type ValidationErrorDisplayProps = {
|
|
3
4
|
formId: string;
|
|
4
5
|
title?: string;
|
|
6
|
+
errors?: FieldErrors;
|
|
7
|
+
showSubmitError?: boolean;
|
|
5
8
|
};
|
|
6
9
|
declare function ValidationErrorDisplay(props: ValidationErrorDisplayProps): React.JSX.Element | null;
|
|
7
10
|
export default ValidationErrorDisplay;
|
package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useResponsive } from '../../../../../theme';
|
|
3
|
-
import { useFormContext } from '../../../../../theme/hooks';
|
|
4
3
|
import { List, ListItem, Typography } from '../../../../core';
|
|
5
4
|
import { Box } from '../../../../layout';
|
|
6
5
|
function ValidationErrorDisplay(props) {
|
|
7
|
-
const { formId, title } = props;
|
|
8
|
-
const { errors, showSubmitError } = useFormContext();
|
|
6
|
+
const { formId, title, errors, showSubmitError } = props;
|
|
9
7
|
const { isSm, isXs } = useResponsive();
|
|
10
8
|
function extractErrorMessages(errors) {
|
|
11
9
|
const messages = [];
|
|
@@ -38,7 +38,7 @@ export type SimpleEditorProps = {
|
|
|
38
38
|
};
|
|
39
39
|
export type ObjectPropertyInputProps = {
|
|
40
40
|
id: string;
|
|
41
|
-
fieldDefinition: InputParameter;
|
|
41
|
+
fieldDefinition: InputParameter | Property;
|
|
42
42
|
mode: 'default' | 'existingOnly' | 'newOnly';
|
|
43
43
|
nestedFieldsView?: boolean;
|
|
44
44
|
readOnly?: boolean;
|
|
@@ -53,6 +53,8 @@ export type ObjectPropertyInputProps = {
|
|
|
53
53
|
initialValue?: ObjectInstance | null;
|
|
54
54
|
viewLayout?: ViewLayoutEntityReference;
|
|
55
55
|
hasDescription?: boolean;
|
|
56
|
+
createActionId?: string;
|
|
57
|
+
formId?: string;
|
|
56
58
|
};
|
|
57
59
|
export type Page = {
|
|
58
60
|
id: string;
|
|
@@ -85,6 +87,10 @@ export type ExpandedSection = Section & {
|
|
|
85
87
|
export type EntryRendererProps = BaseProps & {
|
|
86
88
|
entry: FormEntry;
|
|
87
89
|
isDocument?: boolean;
|
|
90
|
+
associatedObject?: {
|
|
91
|
+
instanceId?: string;
|
|
92
|
+
propertyId?: string;
|
|
93
|
+
};
|
|
88
94
|
};
|
|
89
95
|
export type SectionsProps = {
|
|
90
96
|
entry: Sections;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { Action, ApiServices, Column, Columns, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
|
|
2
3
|
import { LocalDateTime } from '@js-joda/core';
|
|
3
4
|
import { FieldErrors, FieldValues } from 'react-hook-form';
|
|
4
5
|
import { AutocompleteOption } from '../../../core';
|
|
5
|
-
import { Document, DocumentData } from './types';
|
|
6
|
+
import { Document, DocumentData, SavedDocumentReference } from './types';
|
|
6
7
|
export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
|
|
7
8
|
export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
|
|
8
9
|
export declare function isAddressProperty(key: string): boolean;
|
|
@@ -61,3 +62,27 @@ export declare function formatDataToDoc(data: DocumentData): {
|
|
|
61
62
|
export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
|
|
62
63
|
export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
|
|
63
64
|
export declare const docProperties: Property[];
|
|
65
|
+
export declare const uploadDocuments: (files: (File | SavedDocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<SavedDocumentReference[]>;
|
|
66
|
+
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
67
|
+
showAlert: boolean;
|
|
68
|
+
message?: string;
|
|
69
|
+
isError: boolean;
|
|
70
|
+
}>>) => Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Transforms a form submission into a format safe for API submission.
|
|
73
|
+
*
|
|
74
|
+
* Responsibilities:
|
|
75
|
+
* - Uploads any files found in submission fields.
|
|
76
|
+
* - Normalizes related objects (keeping only id and name not the whole instance).
|
|
77
|
+
* - Converts an object of undefined address fields to undefined instead of an empty object.
|
|
78
|
+
* - Normalizes LocalDateTime values to API-friendly format.
|
|
79
|
+
* - Converts empty strings or undefined values to null.
|
|
80
|
+
* - Optionally reports file upload errors via snackbar.
|
|
81
|
+
*
|
|
82
|
+
* Returns the cleaned submission ready for submitting.
|
|
83
|
+
*/
|
|
84
|
+
export declare function formatSubmission(submission: FieldValues, apiServices?: ApiServices, objectId?: string, instanceId?: string, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
85
|
+
showAlert: boolean;
|
|
86
|
+
message?: string;
|
|
87
|
+
isError: boolean;
|
|
88
|
+
}>>): Promise<FieldValues>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LocalDateTime } from '@js-joda/core';
|
|
2
2
|
import jsonLogic from 'json-logic-js';
|
|
3
|
-
import { get, isArray, isEmpty, isObject, omit, startCase, transform } from 'lodash';
|
|
3
|
+
import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
|
|
4
4
|
import { DateTime } from 'luxon';
|
|
5
5
|
import Handlebars from 'no-eval-handlebars';
|
|
6
6
|
import { defaultRuleProcessorMongoDB, formatQuery, parseMongoDB } from 'react-querybuilder';
|
|
@@ -116,7 +116,7 @@ export const getEntryId = (entry) => {
|
|
|
116
116
|
};
|
|
117
117
|
export function getPrefixedUrl(url) {
|
|
118
118
|
const wcsMatchers = ['/apps', '/pages', '/widgets'];
|
|
119
|
-
const dataMatchers = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/locations'];
|
|
119
|
+
const dataMatchers = ['/objects', '/correspondenceTemplates', '/documents', '/payments', '/forms', '/locations'];
|
|
120
120
|
const signalrMatchers = ['/hubs'];
|
|
121
121
|
const accessManagementMatchers = ['/users'];
|
|
122
122
|
const workflowMatchers = ['/workflows'];
|
|
@@ -562,3 +562,109 @@ export const docProperties = [
|
|
|
562
562
|
type: 'string',
|
|
563
563
|
},
|
|
564
564
|
];
|
|
565
|
+
export const uploadDocuments = async (files, metadata, apiServices, instanceId, objectId) => {
|
|
566
|
+
const allDocuments = [];
|
|
567
|
+
const formData = new FormData();
|
|
568
|
+
for (const [index, file] of files.entries()) {
|
|
569
|
+
if ('size' in file) {
|
|
570
|
+
formData.append(`files[${index}]`, file);
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
allDocuments.push(file);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (metadata) {
|
|
577
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
578
|
+
formData.append(key, value);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const docs = await apiServices.post(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}/documents`), formData);
|
|
582
|
+
return allDocuments.concat(docs?.map((doc) => ({
|
|
583
|
+
id: doc.id,
|
|
584
|
+
name: doc.name,
|
|
585
|
+
})) ?? []);
|
|
586
|
+
};
|
|
587
|
+
export const deleteDocuments = async (submittedFields, requestSuccess, apiServices, object, instance, action, setSnackbarError) => {
|
|
588
|
+
const documentProperties = action?.parameters
|
|
589
|
+
? action.parameters.filter((param) => param.type === 'document')
|
|
590
|
+
: object?.properties?.filter((prop) => prop.type === 'document');
|
|
591
|
+
for (const docProperty of documentProperties ?? []) {
|
|
592
|
+
const savedValue = submittedFields[docProperty.id];
|
|
593
|
+
const originalValue = instance?.[docProperty.id];
|
|
594
|
+
const documentsToRemove = requestSuccess
|
|
595
|
+
? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
|
|
596
|
+
: (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
|
|
597
|
+
for (const doc of documentsToRemove) {
|
|
598
|
+
try {
|
|
599
|
+
await apiServices?.delete(getPrefixedUrl(`/objects/${object.id}/instances/${instance.id}/documents/${doc.id}`));
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
if (error) {
|
|
603
|
+
setSnackbarError &&
|
|
604
|
+
setSnackbarError({
|
|
605
|
+
showAlert: true,
|
|
606
|
+
message: `An error occurred while removing document '${doc.name}'`,
|
|
607
|
+
isError: true,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
/**
|
|
615
|
+
* Transforms a form submission into a format safe for API submission.
|
|
616
|
+
*
|
|
617
|
+
* Responsibilities:
|
|
618
|
+
* - Uploads any files found in submission fields.
|
|
619
|
+
* - Normalizes related objects (keeping only id and name not the whole instance).
|
|
620
|
+
* - Converts an object of undefined address fields to undefined instead of an empty object.
|
|
621
|
+
* - Normalizes LocalDateTime values to API-friendly format.
|
|
622
|
+
* - Converts empty strings or undefined values to null.
|
|
623
|
+
* - Optionally reports file upload errors via snackbar.
|
|
624
|
+
*
|
|
625
|
+
* Returns the cleaned submission ready for submitting.
|
|
626
|
+
*/
|
|
627
|
+
export async function formatSubmission(submission, apiServices, objectId, instanceId, setSnackbarError) {
|
|
628
|
+
for (const [key, value] of Object.entries(submission)) {
|
|
629
|
+
if (isArray(value)) {
|
|
630
|
+
const fileInArray = value.some((item) => item instanceof File);
|
|
631
|
+
if (fileInArray && instanceId && apiServices && objectId) {
|
|
632
|
+
try {
|
|
633
|
+
const uploadedDocuments = await uploadDocuments(value, {
|
|
634
|
+
type: '',
|
|
635
|
+
view_permission: '',
|
|
636
|
+
}, apiServices, instanceId, objectId);
|
|
637
|
+
submission[key] = uploadedDocuments;
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
if (err) {
|
|
641
|
+
setSnackbarError &&
|
|
642
|
+
setSnackbarError({
|
|
643
|
+
showAlert: true,
|
|
644
|
+
message: `An error occurred while uploading associated documents`,
|
|
645
|
+
isError: true,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return submission;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// if there are address fields with no value address needs to be set to undefined to be able to submit
|
|
652
|
+
}
|
|
653
|
+
else if (typeof value === 'object' && value !== null) {
|
|
654
|
+
if (Object.values(value).every((v) => v === undefined)) {
|
|
655
|
+
submission[key] = undefined;
|
|
656
|
+
// only submit the name and id of a related object
|
|
657
|
+
}
|
|
658
|
+
else if ('id' in value && 'name' in value) {
|
|
659
|
+
submission[key] = pick(value, 'id', 'name');
|
|
660
|
+
}
|
|
661
|
+
else if (value instanceof LocalDateTime) {
|
|
662
|
+
submission[key] = normalizeDateTime(value);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else if (value === '' || value === undefined) {
|
|
666
|
+
submission[key] = null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return submission;
|
|
670
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|