@evoke-platform/ui-components 1.9.1-testing.1 → 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.
Files changed (39) hide show
  1. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +20 -6
  2. package/dist/published/components/custom/FormV2/FormRenderer.js +142 -128
  3. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +21 -2
  4. package/dist/published/components/custom/FormV2/FormRendererContainer.js +44 -46
  5. package/dist/published/components/custom/FormV2/components/Body.d.ts +18 -0
  6. package/dist/published/components/custom/FormV2/components/Body.js +27 -0
  7. package/dist/published/components/custom/FormV2/components/Footer.d.ts +15 -0
  8. package/dist/published/components/custom/FormV2/components/Footer.js +77 -0
  9. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +2 -2
  10. package/dist/published/components/custom/FormV2/components/FormContext.js +1 -1
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +1 -1
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +64 -22
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +18 -16
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +7 -7
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.d.ts +0 -1
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +10 -8
  17. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +38 -49
  18. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -1
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +95 -51
  20. package/dist/published/components/custom/FormV2/components/Header.d.ts +29 -0
  21. package/dist/published/components/custom/FormV2/components/Header.js +63 -0
  22. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +3 -3
  23. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.d.ts +9 -0
  24. package/dist/published/components/custom/FormV2/components/ValidationFiles/{ValidationErrorDisplay.js → ValidationErrors.js} +11 -8
  25. package/dist/published/components/custom/FormV2/components/types.d.ts +1 -7
  26. package/dist/published/components/custom/FormV2/index.d.ts +3 -0
  27. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +6 -5
  28. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +4 -3
  29. package/dist/published/components/custom/index.d.ts +1 -1
  30. package/dist/published/components/custom/index.js +1 -1
  31. package/dist/published/index.d.ts +1 -1
  32. package/dist/published/stories/FormRenderer.stories.d.ts +24 -16
  33. package/dist/published/stories/FormRenderer.stories.js +2 -10
  34. package/dist/published/stories/FormRendererContainer.stories.d.ts +40 -10
  35. package/dist/published/theme/hooks.d.ts +12 -3
  36. package/package.json +1 -1
  37. package/dist/published/components/custom/FormV2/components/ActionButtons.d.ts +0 -17
  38. package/dist/published/components/custom/FormV2/components/ActionButtons.js +0 -111
  39. 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, Dialog, DialogContent, DialogTitle, IconButton, Link, ListItem, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
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, nestedFieldsView, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, } = props;
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
- return filter ? { where: transformToWhere(filter) } : undefined;
38
- }, [filter]);
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((name) => {
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
- }, [relatedObject, setLoadingOptions, setOptions, fieldDefinition, updatedCriteria, layout]);
134
+ }, [fieldDefinition, updatedCriteria, layout, fetchedOptions, hasFetched, id]);
135
+ const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [getDropdownOptions]);
135
136
  useEffect(() => {
136
137
  if (displayOption === 'dropdown') {
137
- getDropdownOptions();
138
+ debouncedGetDropdownOptions();
139
+ return () => debouncedGetDropdownOptions.cancel();
138
140
  }
139
- }, [fieldDefinition, updatedCriteria, displayOption]);
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
- }, [relatedObject, options, hasFetched]);
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
- } }, mode !== 'newOnly' && children));
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 && (selectedInstance || (nestedFieldsView && instance?.id)) ? (React.createElement(Tooltip, { title: `Unlink` },
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
- 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, PaperProps: {
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 { InfoRounded } from '@mui/icons-material';
3
- import React, { useState } from 'react';
4
- import { useFormContext } from '../../../../../../theme/hooks';
5
- import { Alert, Button, FormControlLabel, Radio, RadioGroup } from '../../../../../core';
6
- import { Box, Grid } from '../../../../../layout';
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: '8px 8px 24px 8px',
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, nestedFieldsView, mode, displayOption, filter, layout, formId, actionId, fieldDefinition, setSnackbarError, setOptions, options, } = props;
21
- const { handleChange: handleChangeObjectField, richTextEditor, stickyFooter, fieldHeight } = useFormContext();
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
- return (React.createElement(Box, { sx: {
73
- background: nestedFieldsView ? '#F4F6F8' : 'none',
74
- borderRadius: '8px',
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(Box, { sx: {
77
- '.MuiInputBase-root': { background: '#FFFF', borderRadius: '8px' },
127
+ React.createElement(DialogTitle, { sx: {
128
+ padding: 3,
129
+ borderBottom: '1px solid #e9ecef',
78
130
  } },
79
- !nestedFieldsView &&
80
- displayOption !== 'dropdown' &&
81
- mode !== 'existingOnly' &&
82
- mode !== 'newOnly' &&
83
- actionId && (React.createElement(Grid, { container: true, sx: { paddingX: '24px' } },
84
- React.createElement(Grid, { container: true, item: true },
85
- React.createElement(RadioGroup, { row: true, "aria-labelledby": "related-object-link-type", onChange: (event) => {
86
- event.target.value === 'existing' && setErrors([]);
87
- setRelationType(event.target.value);
88
- }, value: relationType },
89
- React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
90
- React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))))),
91
- errors?.length ? (React.createElement(Alert, { severity: "error", sx: { margin: '10px 0', borderRadius: '8px' }, icon: React.createElement(InfoRounded, { sx: { color: '#A22723' } }) }, errors.length === 1 ? (React.createElement(React.Fragment, null,
92
- "There is ",
93
- React.createElement("strong", null, "1"),
94
- " error")) : (React.createElement(React.Fragment, null,
95
- "There are ",
96
- React.createElement("strong", null, errors.length),
97
- " errors")))) : undefined,
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 })))),
101
- relationType !== 'new' && mode !== 'newOnly' && (React.createElement(Box, { sx: styles.actionButtons },
102
- React.createElement(Button, { onClick: onClose, color: 'inherit', sx: {
103
- border: '1px solid #ced4da',
104
- width: '75px',
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, isDocument } = props;
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, isDocument);
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)
@@ -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 ValidationErrorDisplay(props) {
5
- const { formId, title, errors, showSubmitError, isSmallerThanMd } = props;
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 showSubmitError && errorMessages.length > 0 ? (React.createElement(Box, { id: `validation-error-display-${formId}`, sx: {
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
- marginBottom: isSmallerThanMd ? 2 : 3,
33
- marginTop: !title ? (isSmallerThanMd ? -2 : -3) : undefined,
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 } }, msg)))))) : null;
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 ValidationErrorDisplay;
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 = BaseProps & {
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
  });