@evoke-platform/ui-components 1.9.1-testing.0 → 1.10.0-testing.0
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/custom/FormV2/FormRenderer.d.ts +20 -6
- package/dist/published/components/custom/FormV2/FormRenderer.js +142 -128
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +21 -2
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +44 -46
- package/dist/published/components/custom/FormV2/components/Body.d.ts +18 -0
- package/dist/published/components/custom/FormV2/components/Body.js +27 -0
- package/dist/published/components/custom/FormV2/components/Footer.d.ts +15 -0
- package/dist/published/components/custom/FormV2/components/Footer.js +77 -0
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +2 -2
- package/dist/published/components/custom/FormV2/components/FormContext.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +64 -22
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +18 -16
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +7 -7
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.d.ts +0 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +10 -8
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +38 -49
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +95 -51
- package/dist/published/components/custom/FormV2/components/Header.d.ts +29 -0
- package/dist/published/components/custom/FormV2/components/Header.js +63 -0
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +3 -3
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.d.ts +9 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/{ValidationErrorDisplay.js → ValidationErrors.js} +11 -8
- package/dist/published/components/custom/FormV2/components/types.d.ts +1 -7
- package/dist/published/components/custom/FormV2/index.d.ts +3 -0
- package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +6 -5
- package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +4 -3
- package/dist/published/components/custom/index.d.ts +1 -1
- package/dist/published/components/custom/index.js +1 -1
- package/dist/published/index.d.ts +1 -1
- package/dist/published/stories/FormRenderer.stories.d.ts +24 -16
- package/dist/published/stories/FormRenderer.stories.js +2 -10
- package/dist/published/stories/FormRendererContainer.stories.d.ts +40 -10
- package/dist/published/theme/hooks.d.ts +12 -3
- package/package.json +2 -2
- package/dist/published/components/custom/FormV2/components/ActionButtons.d.ts +0 -17
- package/dist/published/components/custom/FormV2/components/ActionButtons.js +0 -111
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +0 -11
|
@@ -5,12 +5,12 @@ import Handlebars from 'no-eval-handlebars';
|
|
|
5
5
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { Close } from '../../../../../../icons';
|
|
7
7
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
8
|
-
import { Autocomplete, Button,
|
|
8
|
+
import { Autocomplete, Button, IconButton, Link, ListItem, 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,
|
|
13
|
+
const { id, fieldDefinition, 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);
|
|
@@ -34,8 +34,21 @@ const ObjectPropertyInput = (props) => {
|
|
|
34
34
|
const apiServices = useApiServices();
|
|
35
35
|
const navigateTo = useNavigate();
|
|
36
36
|
const updatedCriteria = useMemo(() => {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
let criteria = filter ? { where: transformToWhere(filter) } : undefined;
|
|
38
|
+
if (dropdownInput) {
|
|
39
|
+
const nameQuery = transformToWhere({
|
|
40
|
+
name: {
|
|
41
|
+
like: dropdownInput,
|
|
42
|
+
options: 'i',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
criteria = {
|
|
46
|
+
...criteria,
|
|
47
|
+
where: criteria?.where ? { and: [criteria.where, nameQuery] } : nameQuery,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return criteria;
|
|
51
|
+
}, [filter, dropdownInput]);
|
|
39
52
|
const listboxRef = useRef(null);
|
|
40
53
|
useEffect(() => {
|
|
41
54
|
if (relatedObject) {
|
|
@@ -91,13 +104,13 @@ const ObjectPropertyInput = (props) => {
|
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
}, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
|
|
94
|
-
const getDropdownOptions = useCallback((
|
|
107
|
+
const getDropdownOptions = useCallback(() => {
|
|
95
108
|
if (((!fetchedOptions?.[`${id}Options`] ||
|
|
96
109
|
(fetchedOptions?.[`${id}Options`]).length === 0) &&
|
|
97
110
|
!hasFetched) ||
|
|
98
111
|
!isEqual(fetchedOptions?.[`${id}UpdatedCriteria`], updatedCriteria)) {
|
|
112
|
+
setFetchedOptions({ [`${id}UpdatedCriteria`]: updatedCriteria });
|
|
99
113
|
setLoadingOptions(true);
|
|
100
|
-
setFetchedOptions && setFetchedOptions({ [`${id}UpdatedCriteria`]: updatedCriteria });
|
|
101
114
|
const updatedFilter = cloneDeep(updatedCriteria) || {};
|
|
102
115
|
updatedFilter.limit = 100;
|
|
103
116
|
const { propertyId, direction } = layout?.sort ?? {
|
|
@@ -105,19 +118,6 @@ const ObjectPropertyInput = (props) => {
|
|
|
105
118
|
direction: 'asc',
|
|
106
119
|
};
|
|
107
120
|
updatedFilter.order = `${propertyId} ${direction}`;
|
|
108
|
-
const where = name
|
|
109
|
-
? transformToWhere({
|
|
110
|
-
name: {
|
|
111
|
-
like: name,
|
|
112
|
-
options: 'i',
|
|
113
|
-
},
|
|
114
|
-
})
|
|
115
|
-
: {};
|
|
116
|
-
updatedFilter.where = updatedFilter.where
|
|
117
|
-
? {
|
|
118
|
-
and: [updatedFilter.where, where],
|
|
119
|
-
}
|
|
120
|
-
: where;
|
|
121
121
|
apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
|
|
122
122
|
if (error) {
|
|
123
123
|
console.error(error);
|
|
@@ -131,20 +131,17 @@ const ObjectPropertyInput = (props) => {
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
|
-
}, [
|
|
134
|
+
}, [fieldDefinition, updatedCriteria, layout, fetchedOptions, hasFetched, id]);
|
|
135
|
+
const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
|
|
135
136
|
useEffect(() => {
|
|
136
137
|
if (displayOption === 'dropdown') {
|
|
137
|
-
|
|
138
|
+
debouncedGetDropdownOptions();
|
|
139
|
+
return () => debouncedGetDropdownOptions.cancel();
|
|
138
140
|
}
|
|
139
|
-
}, [
|
|
141
|
+
}, [displayOption, debouncedGetDropdownOptions]);
|
|
140
142
|
useEffect(() => {
|
|
141
143
|
setSelectedInstance(initialValue);
|
|
142
144
|
}, [initialValue]);
|
|
143
|
-
const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [updatedCriteria]);
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
debouncedGetDropdownOptions(dropdownInput);
|
|
146
|
-
return () => debouncedGetDropdownOptions.cancel();
|
|
147
|
-
}, [dropdownInput]);
|
|
148
145
|
useEffect(() => {
|
|
149
146
|
if (formId || action?.defaultFormId) {
|
|
150
147
|
apiServices
|
|
@@ -189,16 +186,17 @@ const ObjectPropertyInput = (props) => {
|
|
|
189
186
|
}
|
|
190
187
|
});
|
|
191
188
|
}
|
|
192
|
-
}, [fieldDefinition]);
|
|
189
|
+
}, [fieldDefinition.objectId, fetchedOptions, id]);
|
|
193
190
|
useEffect(() => {
|
|
194
191
|
const fetchDefaultPages = async () => {
|
|
195
|
-
if (parameters) {
|
|
192
|
+
if (parameters && !fetchedOptions['allDefaultPages']) {
|
|
196
193
|
const pages = await getDefaultPages(parameters, defaultPages, findDefaultPageSlugFor);
|
|
197
194
|
setAllDefaultPages(pages);
|
|
195
|
+
setFetchedOptions({ [`allDefaultPages`]: pages });
|
|
198
196
|
}
|
|
199
197
|
};
|
|
200
198
|
fetchDefaultPages();
|
|
201
|
-
}, []);
|
|
199
|
+
}, [fetchedOptions, parameters, defaultPages, findDefaultPageSlugFor]);
|
|
202
200
|
useEffect(() => {
|
|
203
201
|
if (fieldDefinition.objectId &&
|
|
204
202
|
allDefaultPages &&
|
|
@@ -214,7 +212,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
214
212
|
}
|
|
215
213
|
});
|
|
216
214
|
}
|
|
217
|
-
}, []);
|
|
215
|
+
}, [fieldDefinition, allDefaultPages, fetchedOptions, id]);
|
|
218
216
|
const handleClose = () => {
|
|
219
217
|
setOpenCreateDialog(false);
|
|
220
218
|
};
|
|
@@ -242,7 +240,12 @@ const ObjectPropertyInput = (props) => {
|
|
|
242
240
|
[`${id}NavigationSlug`]: navigationSlug,
|
|
243
241
|
});
|
|
244
242
|
}
|
|
245
|
-
|
|
243
|
+
if (appId && !fetchedOptions[`${id}AppId`]) {
|
|
244
|
+
setFetchedOptions({
|
|
245
|
+
[`${id}AppId`]: appId,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}, [relatedObject, options, hasFetched, navigationSlug, fetchedOptions, appId, id]);
|
|
246
249
|
const dropdownOptions = [
|
|
247
250
|
...options.map((o) => ({ label: o.name, value: o.id })),
|
|
248
251
|
...(mode !== 'existingOnly' && relatedObject?.actions?.some((a) => a.id === createActionId)
|
|
@@ -289,7 +292,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
289
292
|
paddingLeft: '24px',
|
|
290
293
|
color: 'rgba(145, 158, 171, 1)',
|
|
291
294
|
},
|
|
292
|
-
} },
|
|
295
|
+
} }, children));
|
|
293
296
|
}, sx: {
|
|
294
297
|
'& button.MuiButtonBase-root': {
|
|
295
298
|
...(!loadingOptions &&
|
|
@@ -463,7 +466,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
463
466
|
}, variant: "body2", href: navigationSlug && !isModal
|
|
464
467
|
? `${'/app'}/${appId}/${navigationSlug.replace(':instanceId', selectedInstance?.id ?? '')}`
|
|
465
468
|
: undefined, "aria-label": selectedInstance?.name }, selectedInstance?.name ? selectedInstance?.name : readOnly && 'None')),
|
|
466
|
-
!readOnly &&
|
|
469
|
+
!readOnly && selectedInstance ? (React.createElement(Tooltip, { title: `Unlink` },
|
|
467
470
|
React.createElement("span", null,
|
|
468
471
|
React.createElement(IconButton, { onClick: (event) => {
|
|
469
472
|
event.stopPropagation();
|
|
@@ -491,21 +494,7 @@ const ObjectPropertyInput = (props) => {
|
|
|
491
494
|
event.stopPropagation();
|
|
492
495
|
setOpenCreateDialog(true);
|
|
493
496
|
}, "aria-label": `Add` }, "Add")))),
|
|
494
|
-
|
|
495
|
-
sx: { maxWidth: '950px', width: '100%' },
|
|
496
|
-
}, open: openCreateDialog, onClose: (e, reason) => reason !== 'backdropClick' && handleClose },
|
|
497
|
-
React.createElement(DialogTitle, { sx: {
|
|
498
|
-
fontSize: '18px',
|
|
499
|
-
fontWeight: 700,
|
|
500
|
-
paddingTop: '35px',
|
|
501
|
-
paddingBottom: '20px',
|
|
502
|
-
borderBottom: '1px solid #e9ecef',
|
|
503
|
-
} },
|
|
504
|
-
React.createElement(IconButton, { sx: { position: 'absolute', right: '17px', top: '22px' }, onClick: handleClose },
|
|
505
|
-
React.createElement(Close, { fontSize: "small" })),
|
|
506
|
-
form?.name ?? `Add ${fieldDefinition.name}`),
|
|
507
|
-
React.createElement(DialogContent, { sx: { padding: '0px' } },
|
|
508
|
-
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 })))))),
|
|
497
|
+
React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, 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 }),
|
|
509
498
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
|
|
510
499
|
isError: snackbarError.isError,
|
|
511
500
|
showAlert: false,
|
|
@@ -3,6 +3,8 @@ import React from 'react';
|
|
|
3
3
|
import { BaseProps } from '../../types';
|
|
4
4
|
export type RelatedObjectInstanceProps = BaseProps & {
|
|
5
5
|
id: string;
|
|
6
|
+
open: boolean;
|
|
7
|
+
title: string;
|
|
6
8
|
relatedObject: Obj | undefined;
|
|
7
9
|
setSelectedInstance: (selectedInstance: ObjectInstance) => void;
|
|
8
10
|
handleClose: () => void;
|
|
@@ -12,7 +14,6 @@ export type RelatedObjectInstanceProps = BaseProps & {
|
|
|
12
14
|
message?: string;
|
|
13
15
|
isError: boolean;
|
|
14
16
|
}>>;
|
|
15
|
-
nestedFieldsView?: boolean;
|
|
16
17
|
displayOption?: 'dropdown' | 'dialogBox';
|
|
17
18
|
setOptions: (options: ObjectInstance[]) => void;
|
|
18
19
|
options: ObjectInstance[];
|
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
import { useApiServices } from '@evoke-platform/context';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { DialogActions } from '@mui/material';
|
|
3
|
+
import { isEmpty } from 'lodash';
|
|
4
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
5
|
+
import { Close } from '../../../../../../icons';
|
|
6
|
+
import useWidgetSize, { useFormContext } from '../../../../../../theme/hooks';
|
|
7
|
+
import { Button, Dialog, DialogContent, DialogTitle, FormControlLabel, IconButton, Radio, RadioGroup, } from '../../../../../core';
|
|
8
|
+
import Box from '../../../../../layout/Box/Box';
|
|
9
|
+
import FormRenderer from '../../../FormRenderer';
|
|
7
10
|
import FormRendererContainer from '../../../FormRendererContainer';
|
|
11
|
+
import Body from '../../Body';
|
|
12
|
+
import Footer from '../../Footer';
|
|
13
|
+
import { AccordionActions } from '../../Header';
|
|
8
14
|
import { formatSubmission, getPrefixedUrl } from '../../utils';
|
|
9
15
|
import InstanceLookup from './InstanceLookup';
|
|
10
16
|
const styles = {
|
|
11
17
|
actionButtons: {
|
|
12
|
-
padding:
|
|
13
|
-
marginRight: '18px',
|
|
18
|
+
padding: 3,
|
|
14
19
|
display: 'flex',
|
|
15
20
|
justifyContent: 'flex-end',
|
|
16
21
|
alignItems: 'center',
|
|
22
|
+
borderTop: '1px solid #f4f6f8',
|
|
23
|
+
},
|
|
24
|
+
dialogContent: {
|
|
25
|
+
maxHeight: 'calc(100vh - 340px)',
|
|
26
|
+
padding: '0 24px 30px 24px',
|
|
27
|
+
'.MuiInputBase-root': { background: '#FFFF', borderRadius: '8px' },
|
|
17
28
|
},
|
|
18
29
|
};
|
|
19
30
|
const RelatedObjectInstance = (props) => {
|
|
20
|
-
const { relatedObject, id, setSelectedInstance, handleClose,
|
|
21
|
-
const { handleChange: handleChangeObjectField, richTextEditor,
|
|
22
|
-
const [errors, setErrors] = useState([]);
|
|
31
|
+
const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, fieldDefinition, setSnackbarError, setOptions, options, } = props;
|
|
32
|
+
const { handleChange: handleChangeObjectField, richTextEditor, fieldHeight, width } = useFormContext();
|
|
23
33
|
const [selectedRow, setSelectedRow] = useState();
|
|
24
|
-
const [relationType, setRelationType] = useState(displayOption === 'dropdown' ? 'new' : 'existing');
|
|
34
|
+
const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
|
|
25
35
|
const apiServices = useApiServices();
|
|
36
|
+
const validationErrorsRef = useRef(null);
|
|
37
|
+
const { breakpoints } = useWidgetSize({
|
|
38
|
+
scroll: false,
|
|
39
|
+
defaultWidth: width,
|
|
40
|
+
});
|
|
41
|
+
const { isXs, isSm } = breakpoints;
|
|
26
42
|
const linkExistingInstance = async () => {
|
|
27
43
|
if (selectedRow) {
|
|
28
44
|
setSelectedInstance(selectedRow);
|
|
@@ -32,7 +48,6 @@ const RelatedObjectInstance = (props) => {
|
|
|
32
48
|
};
|
|
33
49
|
const onClose = () => {
|
|
34
50
|
handleClose();
|
|
35
|
-
setErrors([]);
|
|
36
51
|
};
|
|
37
52
|
const createNewInstance = async (submission) => {
|
|
38
53
|
if (!relatedObject) {
|
|
@@ -69,46 +84,75 @@ const RelatedObjectInstance = (props) => {
|
|
|
69
84
|
});
|
|
70
85
|
}
|
|
71
86
|
};
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
const handleSubmitError = () => {
|
|
88
|
+
if (validationErrorsRef.current) {
|
|
89
|
+
validationErrorsRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const shouldShowRadioButtons = displayOption !== 'dropdown' && mode !== 'existingOnly' && mode !== 'newOnly' && actionId;
|
|
93
|
+
const RadioButtons = () => shouldShowRadioButtons ? (React.createElement(RadioGroup, { row: true, "aria-label": "Relation Type", onChange: (event) => {
|
|
94
|
+
const { value } = event.target;
|
|
95
|
+
if (value === 'new' || value === 'existing') {
|
|
96
|
+
setRelationType(value);
|
|
97
|
+
}
|
|
98
|
+
}, value: relationType },
|
|
99
|
+
React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
|
|
100
|
+
React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null;
|
|
101
|
+
const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: fieldDefinition.objectId, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
|
|
102
|
+
relationType === 'new' ? (React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(FormRenderer.ValidationErrors, { errors: bodyProps.errors, sx: {
|
|
103
|
+
my: isSm || isXs ? 2 : 3,
|
|
104
|
+
} })) : null)) : null,
|
|
105
|
+
React.createElement(RadioButtons, null),
|
|
106
|
+
bodyProps.hasAccordions && React.createElement(AccordionActions, { ...bodyProps }),
|
|
107
|
+
React.createElement(Body, { ...bodyProps, sx: { padding: 0 } }))), renderFooter: (footerProps) => (React.createElement(DialogActions, { sx: {
|
|
108
|
+
padding: 0,
|
|
109
|
+
borderTop: '1px solid #f4f6f8',
|
|
110
|
+
} },
|
|
111
|
+
React.createElement(Footer, { ...footerProps, discardChangesButtonLabel: "Cancel", sx: { borderTop: 'none' } }))), renderContainer: ({ status, defaultContainer }) => {
|
|
112
|
+
return (React.createElement(React.Fragment, null,
|
|
113
|
+
(status === 'loading' || status === 'error') && (React.createElement(DialogContent, null,
|
|
114
|
+
React.createElement(RadioButtons, null),
|
|
115
|
+
defaultContainer)),
|
|
116
|
+
status === 'ready' && defaultContainer));
|
|
117
|
+
}, sx: { border: 'none' } })), [formId, actionId, fieldDefinition, fieldHeight, richTextEditor, RadioButtons]);
|
|
118
|
+
return (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: open, onClose: (e, reason) => reason !== 'backdropClick' && handleClose(), sx: {
|
|
119
|
+
background: 'none',
|
|
120
|
+
}, PaperProps: {
|
|
121
|
+
sx: {
|
|
122
|
+
borderRadius: '16px',
|
|
123
|
+
maxWidth: '950px',
|
|
124
|
+
width: '100%',
|
|
125
|
+
},
|
|
75
126
|
} },
|
|
76
|
-
React.createElement(
|
|
77
|
-
|
|
127
|
+
React.createElement(DialogTitle, { sx: {
|
|
128
|
+
padding: 3,
|
|
129
|
+
borderBottom: '1px solid #e9ecef',
|
|
78
130
|
} },
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
marginRight: '0px',
|
|
106
|
-
color: 'black',
|
|
107
|
-
} }, "Cancel"),
|
|
108
|
-
React.createElement(Button, { sx: {
|
|
109
|
-
marginLeft: '8px',
|
|
110
|
-
width: '85px',
|
|
111
|
-
'&:hover': { boxShadow: 'none' },
|
|
112
|
-
}, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))));
|
|
131
|
+
React.createElement(Box, { sx: {
|
|
132
|
+
display: 'flex',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
justifyContent: 'space-between',
|
|
135
|
+
flex: '1 1 100%',
|
|
136
|
+
} },
|
|
137
|
+
title,
|
|
138
|
+
React.createElement(IconButton, { onClick: onClose, "aria-label": "Close" },
|
|
139
|
+
React.createElement(Close, { fontSize: "small" })))),
|
|
140
|
+
relationType === 'new' ? (React.createElement(DialogForm, null)) : ((mode === 'default' || mode === 'existingOnly') &&
|
|
141
|
+
relatedObject && (React.createElement(React.Fragment, null,
|
|
142
|
+
React.createElement(DialogContent, { sx: styles.dialogContent },
|
|
143
|
+
shouldShowRadioButtons && React.createElement(RadioButtons, null),
|
|
144
|
+
React.createElement(InstanceLookup, { colspan: 12, setRelationType: setRelationType, object: relatedObject, setSelectedInstance: setSelectedRow, mode: mode, filter: filter, layout: layout })),
|
|
145
|
+
React.createElement(DialogActions, { sx: styles.actionButtons },
|
|
146
|
+
React.createElement(Button, { onClick: onClose, color: 'inherit', sx: {
|
|
147
|
+
border: '1px solid #ced4da',
|
|
148
|
+
width: '75px',
|
|
149
|
+
marginRight: '0px',
|
|
150
|
+
color: 'black',
|
|
151
|
+
} }, "Cancel"),
|
|
152
|
+
React.createElement(Button, { sx: {
|
|
153
|
+
marginLeft: '8px',
|
|
154
|
+
width: '85px',
|
|
155
|
+
'&:hover': { boxShadow: 'none' },
|
|
156
|
+
}, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))))));
|
|
113
157
|
};
|
|
114
158
|
export default RelatedObjectInstance;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Action, EvokeForm } from '@evoke-platform/context';
|
|
2
|
+
import { SxProps } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { FieldErrors } from 'react-hook-form';
|
|
5
|
+
import { ExpandedSection } from './types';
|
|
6
|
+
export type HeaderProps = {
|
|
7
|
+
hasAccordions: boolean;
|
|
8
|
+
shouldShowValidationErrors: boolean;
|
|
9
|
+
title?: string;
|
|
10
|
+
expandedSections?: ExpandedSection[];
|
|
11
|
+
onExpandAll?: () => void;
|
|
12
|
+
onCollapseAll?: () => void;
|
|
13
|
+
form: EvokeForm;
|
|
14
|
+
errors?: FieldErrors;
|
|
15
|
+
action?: Action;
|
|
16
|
+
sx?: SxProps;
|
|
17
|
+
};
|
|
18
|
+
declare const Header: React.FC<HeaderProps>;
|
|
19
|
+
export type TitleProps = {
|
|
20
|
+
title?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare const Title: React.FC<TitleProps>;
|
|
23
|
+
export type AccordionActionsProps = {
|
|
24
|
+
expandedSections?: ExpandedSection[];
|
|
25
|
+
onExpandAll?: () => void;
|
|
26
|
+
onCollapseAll?: () => void;
|
|
27
|
+
};
|
|
28
|
+
export declare const AccordionActions: React.FC<AccordionActionsProps>;
|
|
29
|
+
export default Header;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { isEmpty } from 'lodash';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
|
|
4
|
+
import Button from '../../../core/Button/Button';
|
|
5
|
+
import { Typography } from '../../../core/Typography';
|
|
6
|
+
import Box from '../../../layout/Box/Box';
|
|
7
|
+
import ValidationErrors from './ValidationFiles/ValidationErrors';
|
|
8
|
+
const Header = (props) => {
|
|
9
|
+
const { title, errors, hasAccordions, shouldShowValidationErrors, form, sx } = props;
|
|
10
|
+
const { width } = useFormContext();
|
|
11
|
+
const { breakpoints, isBelow } = useWidgetSize({
|
|
12
|
+
scroll: false,
|
|
13
|
+
defaultWidth: width,
|
|
14
|
+
});
|
|
15
|
+
const isSmallerThanMd = isBelow('md');
|
|
16
|
+
const { isXs, isSm } = breakpoints;
|
|
17
|
+
return (React.createElement(Box, { sx: {
|
|
18
|
+
paddingX: isSmallerThanMd ? 2 : 3,
|
|
19
|
+
paddingTop: '0px',
|
|
20
|
+
display: 'flex',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
flexWrap: 'wrap',
|
|
23
|
+
paddingY: isSm || isXs ? 2 : 3,
|
|
24
|
+
// when rendering the default delete action, we don't want a border
|
|
25
|
+
borderBottom: !form.id ? undefined : '1px solid #e9ecef',
|
|
26
|
+
gap: isSm || isXs ? 2 : 3,
|
|
27
|
+
...sx,
|
|
28
|
+
} },
|
|
29
|
+
title && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
|
|
30
|
+
React.createElement(Title, { ...props }))),
|
|
31
|
+
hasAccordions && (React.createElement(Box, { sx: { flex: '1 1 100%' } },
|
|
32
|
+
React.createElement(AccordionActions, { ...props }))),
|
|
33
|
+
shouldShowValidationErrors && !isEmpty(errors) ? React.createElement(ValidationErrors, { errors: errors }) : null));
|
|
34
|
+
};
|
|
35
|
+
// Default slot components for convenience
|
|
36
|
+
export const Title = ({ title }) => (React.createElement(Typography, { sx: {
|
|
37
|
+
fontSize: '20px',
|
|
38
|
+
lineHeight: '30px',
|
|
39
|
+
fontWeight: 700,
|
|
40
|
+
flexGrow: 1,
|
|
41
|
+
} }, title));
|
|
42
|
+
export const AccordionActions = ({ onExpandAll, onCollapseAll, expandedSections }) => {
|
|
43
|
+
return (React.createElement(React.Fragment, null,
|
|
44
|
+
React.createElement(Button, { variant: "text", size: "small", disableRipple: true, disabled: expandedSections?.every((section) => section.expanded === true), onClick: onExpandAll, sx: {
|
|
45
|
+
color: '#212B36',
|
|
46
|
+
borderRight: '1px solid #e5e8eb',
|
|
47
|
+
borderRadius: '0px',
|
|
48
|
+
'&:hover': {
|
|
49
|
+
backgroundColor: 'transparent',
|
|
50
|
+
},
|
|
51
|
+
fontWeight: 400,
|
|
52
|
+
fontSize: '14px',
|
|
53
|
+
} }, "Expand all"),
|
|
54
|
+
React.createElement(Button, { variant: "text", size: "small", disableRipple: true, disabled: expandedSections?.every((section) => section.expanded === false), onClick: onCollapseAll, sx: {
|
|
55
|
+
color: '#212B36',
|
|
56
|
+
'&:hover': {
|
|
57
|
+
backgroundColor: 'transparent',
|
|
58
|
+
},
|
|
59
|
+
fontWeight: 400,
|
|
60
|
+
fontSize: '14px',
|
|
61
|
+
} }, "Collapse all")));
|
|
62
|
+
};
|
|
63
|
+
export default Header;
|
|
@@ -36,8 +36,8 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
export function RecursiveEntryRenderer(props) {
|
|
39
|
-
const { entry
|
|
40
|
-
const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, associatedObject, width, } = useFormContext();
|
|
39
|
+
const { entry } = props;
|
|
40
|
+
const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, associatedObject, form, width, } = useFormContext();
|
|
41
41
|
// If the entry is hidden, clear its value and any nested values, and skip rendering
|
|
42
42
|
if (!entryIsVisible(entry, getValues(), instance)) {
|
|
43
43
|
return null;
|
|
@@ -56,7 +56,7 @@ export function RecursiveEntryRenderer(props) {
|
|
|
56
56
|
const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
|
|
57
57
|
const middleObject = fetchedOptions[`${entryId}MiddleObject`];
|
|
58
58
|
const fieldDefinition = useMemo(() => {
|
|
59
|
-
return getFieldDefinition(entry, object, parameters,
|
|
59
|
+
return getFieldDefinition(entry, object, parameters, form?.id === 'documentForm');
|
|
60
60
|
}, [entry, parameters, object]);
|
|
61
61
|
const validation = fieldDefinition?.validation || {};
|
|
62
62
|
if (associatedObject?.propertyId === entryId)
|
package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SxProps } from '@mui/material/styles';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { FieldErrors } from 'react-hook-form';
|
|
4
|
+
export type ValidationErrorsProps = {
|
|
5
|
+
errors: FieldErrors;
|
|
6
|
+
sx?: SxProps;
|
|
7
|
+
};
|
|
8
|
+
declare function ValidationErrors(props: ValidationErrorsProps): React.JSX.Element;
|
|
9
|
+
export default ValidationErrors;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { List, ListItem, Typography } from '../../../../core';
|
|
3
3
|
import { Box } from '../../../../layout';
|
|
4
|
-
function
|
|
5
|
-
const {
|
|
4
|
+
function ValidationErrors(props) {
|
|
5
|
+
const { errors, sx } = props;
|
|
6
6
|
function extractErrorMessages(errors) {
|
|
7
7
|
const messages = [];
|
|
8
8
|
for (const key in errors) {
|
|
9
9
|
const error = errors[key];
|
|
10
|
-
if (error?.message) {
|
|
10
|
+
if (error?.message && typeof error?.message === 'string') {
|
|
11
11
|
messages.push(error.message);
|
|
12
12
|
}
|
|
13
13
|
else if (error) {
|
|
@@ -22,15 +22,15 @@ function ValidationErrorDisplay(props) {
|
|
|
22
22
|
return messages;
|
|
23
23
|
}
|
|
24
24
|
const errorMessages = extractErrorMessages(errors);
|
|
25
|
-
return
|
|
25
|
+
return (React.createElement(Box, { sx: {
|
|
26
26
|
backgroundColor: '#f8d7da',
|
|
27
27
|
borderColor: '#f5c6cb',
|
|
28
28
|
color: '#721c24',
|
|
29
29
|
border: '1px solid #721c24',
|
|
30
30
|
padding: '8px 24px',
|
|
31
31
|
borderRadius: '4px',
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
flex: 1,
|
|
33
|
+
...sx,
|
|
34
34
|
} },
|
|
35
35
|
React.createElement(Typography, { sx: { color: '#721c24', mt: '16px', mb: '8px' } }, "Please fix the following errors before submitting:"),
|
|
36
36
|
React.createElement(List, { sx: {
|
|
@@ -38,6 +38,9 @@ function ValidationErrorDisplay(props) {
|
|
|
38
38
|
paddingLeft: '40px',
|
|
39
39
|
mb: '8px',
|
|
40
40
|
fontFamily: 'Arial, Helvetica, sans-serif',
|
|
41
|
-
} }, errorMessages.map((msg, index) => (React.createElement(ListItem, { key: index, sx: { display: 'list-item', p: 0 } },
|
|
41
|
+
} }, errorMessages.map((msg, index) => (React.createElement(ListItem, { key: index, sx: { display: 'list-item', p: 0 } },
|
|
42
|
+
React.createElement(Typography, { sx: {
|
|
43
|
+
color: '#721c24',
|
|
44
|
+
} }, msg)))))));
|
|
42
45
|
}
|
|
43
|
-
export default
|
|
46
|
+
export default ValidationErrors;
|
|
@@ -40,7 +40,6 @@ export type ObjectPropertyInputProps = {
|
|
|
40
40
|
id: string;
|
|
41
41
|
fieldDefinition: InputParameter | Property;
|
|
42
42
|
mode: 'default' | 'existingOnly' | 'newOnly';
|
|
43
|
-
nestedFieldsView?: boolean;
|
|
44
43
|
readOnly?: boolean;
|
|
45
44
|
error?: boolean;
|
|
46
45
|
displayOption?: 'dropdown' | 'dialogBox';
|
|
@@ -89,13 +88,8 @@ export type ExpandedSections = Omit<Sections, 'sections'> & {
|
|
|
89
88
|
sections: ExpandedSection[];
|
|
90
89
|
id: string;
|
|
91
90
|
};
|
|
92
|
-
export type EntryRendererProps =
|
|
91
|
+
export type EntryRendererProps = {
|
|
93
92
|
entry: FormEntry;
|
|
94
|
-
isDocument?: boolean;
|
|
95
|
-
associatedObject?: {
|
|
96
|
-
instanceId?: string;
|
|
97
|
-
propertyId?: string;
|
|
98
|
-
};
|
|
99
93
|
};
|
|
100
94
|
export type SectionsProps = {
|
|
101
95
|
entry: ExpandedSections;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
export type { BodyProps } from './components/Body';
|
|
2
|
+
export type { FooterProps } from './components/Footer';
|
|
1
3
|
export { FormContext } from './components/FormContext';
|
|
4
|
+
export type { HeaderProps } from './components/Header';
|
|
2
5
|
export { RecursiveEntryRenderer } from './components/RecursiveEntryRenderer';
|
|
3
6
|
export { default as FormRenderer } from './FormRenderer';
|
|
4
7
|
export { default as FormRendererContainer } from './FormRendererContainer';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as matchers from '@testing-library/jest-dom/matchers';
|
|
2
2
|
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import { isEqual, set } from 'lodash';
|
|
4
|
+
import { isEmpty, isEqual, set } from 'lodash';
|
|
5
5
|
import { http, HttpResponse } from 'msw';
|
|
6
6
|
import { setupServer } from 'msw/node';
|
|
7
7
|
import React from 'react';
|
|
@@ -46,7 +46,7 @@ describe('Form component', () => {
|
|
|
46
46
|
if (sanitizedVersion === 'true') {
|
|
47
47
|
return HttpResponse.json(accessibility508Object);
|
|
48
48
|
}
|
|
49
|
-
}), http.get('/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('/api/data/objects/license/instances', () => {
|
|
49
|
+
}), http.get('/api/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('/api/data/objects/license/instances', () => {
|
|
50
50
|
return HttpResponse.json([rnLicense, npLicense]);
|
|
51
51
|
}), http.get('/api/data/objects/specialtyType/instances', (req) => {
|
|
52
52
|
const filter = new URL(req.request.url).searchParams.get('filter');
|
|
@@ -55,7 +55,7 @@ describe('Form component', () => {
|
|
|
55
55
|
// The two objects in the array of conditions in the "where" filter represent the potential filters that can be applied when retrieving "specialty" instances.
|
|
56
56
|
// The first object is for the the validation criteria, but it is empty if the "license" field, which is referenced in the validation criteria, hasn't been filled out yet.
|
|
57
57
|
// The second object is for the search criteria which the user enters in the "specialty" field, but it is empty if no search text has been entered.
|
|
58
|
-
if (isEqual(whereFilter, { and: [{}, {}] }))
|
|
58
|
+
if (isEqual(whereFilter, { and: [{}, {}] }) || isEmpty(whereFilter))
|
|
59
59
|
return HttpResponse.json([
|
|
60
60
|
rnSpecialtyType1,
|
|
61
61
|
rnSpecialtyType2,
|
|
@@ -234,6 +234,8 @@ describe('Form component', () => {
|
|
|
234
234
|
await user.tab();
|
|
235
235
|
// Open dropdown with Enter
|
|
236
236
|
await user.keyboard('{Enter}');
|
|
237
|
+
// Wait for options to load
|
|
238
|
+
await screen.findByText('RN License');
|
|
237
239
|
// Navigate to first option
|
|
238
240
|
await user.keyboard('{ArrowDown}');
|
|
239
241
|
// Select option with Enter
|
|
@@ -369,8 +371,7 @@ describe('Form component', () => {
|
|
|
369
371
|
it('supports navigating to Add New option with arrow keys and opens modal', async () => {
|
|
370
372
|
const user = userEvent.setup();
|
|
371
373
|
render(React.createElement(MemoryRouter, null,
|
|
372
|
-
React.createElement(FormRenderer, { form: UpdateAccessibilityFormOne, onChange: () => { } })
|
|
373
|
-
","));
|
|
374
|
+
React.createElement(FormRenderer, { form: UpdateAccessibilityFormOne, onChange: () => { } })));
|
|
374
375
|
await waitFor(() => {
|
|
375
376
|
expect(screen.getByLabelText('Name')).toBeInTheDocument();
|
|
376
377
|
});
|