@evoke-platform/ui-components 1.8.0-dev.1 → 1.8.0-dev.11

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 (52) hide show
  1. package/dist/published/components/core/TextField/TextField.js +3 -2
  2. package/dist/published/components/custom/DataGrid/DataGrid.d.ts +1 -0
  3. package/dist/published/components/custom/DataGrid/DataGrid.js +2 -1
  4. package/dist/published/components/custom/DataGrid/Toolbar.d.ts +1 -0
  5. package/dist/published/components/custom/DataGrid/Toolbar.js +3 -2
  6. package/dist/published/components/custom/DataGrid/index.d.ts +1 -0
  7. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +48 -39
  8. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/RelatedObjectInstance.js +1 -1
  9. package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.d.ts +1 -1
  10. package/dist/published/components/custom/Form/FormComponents/UserComponent/UserProperty.js +22 -6
  11. package/dist/published/components/custom/Form/tests/Form.test.js +192 -2
  12. package/dist/published/components/custom/Form/tests/test-data.d.ts +7 -0
  13. package/dist/published/components/custom/Form/tests/test-data.js +138 -0
  14. package/dist/published/components/custom/Form/utils.js +76 -44
  15. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +6 -2
  16. package/dist/published/components/custom/FormV2/FormRenderer.js +13 -14
  17. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +7 -2
  18. package/dist/published/components/custom/FormV2/FormRendererContainer.js +61 -109
  19. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +4 -0
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +9 -5
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +12 -24
  22. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +5 -1
  23. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +80 -30
  24. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +1 -1
  25. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
  26. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +51 -27
  27. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +5 -5
  28. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +45 -7
  29. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +8 -6
  30. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +3 -0
  31. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +1 -3
  32. package/dist/published/components/custom/FormV2/components/types.d.ts +7 -1
  33. package/dist/published/components/custom/FormV2/components/utils.d.ts +27 -2
  34. package/dist/published/components/custom/FormV2/components/utils.js +108 -2
  35. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.d.ts +1 -0
  36. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +173 -0
  37. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.d.ts +1 -0
  38. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +96 -0
  39. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +16 -0
  40. package/dist/published/components/custom/FormV2/tests/test-data.js +394 -0
  41. package/dist/published/components/custom/index.d.ts +1 -0
  42. package/dist/published/index.d.ts +1 -1
  43. package/dist/published/stories/FormRenderer.stories.d.ts +7 -0
  44. package/dist/published/stories/FormRenderer.stories.js +65 -0
  45. package/dist/published/stories/FormRendererContainer.stories.d.ts +7 -0
  46. package/dist/published/stories/FormRendererContainer.stories.js +56 -0
  47. package/dist/published/stories/FormRendererData.d.ts +116 -0
  48. package/dist/published/stories/FormRendererData.js +925 -0
  49. package/dist/published/stories/sharedMswHandlers.d.ts +1 -0
  50. package/dist/published/stories/sharedMswHandlers.js +100 -0
  51. package/dist/published/theme/hooks.d.ts +4 -0
  52. 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 DEFAULT_CREATE_ACTION = '_create';
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 = filter
38
- ? {
39
- where: transformToWhere(filter),
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(`data/forms/${action.defaultFormId}`))
150
+ .get(getPrefixedUrl(`/forms/${formId || action?.defaultFormId}`))
152
151
  .then((evokeForm) => {
153
152
  setForm(evokeForm);
154
153
  })
155
154
  .catch((error) => {
156
- console.error('Error fetching form:', 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' && form && (React.createElement(Button, { fullWidth: true, sx: {
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, form: form, action: action, setSnackbarError: setSnackbarError, selectedInstance: selectedInstance })) : (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: openCreateDialog, onClose: (e, reason) => reason !== 'backdropClick' && handleClose },
426
- React.createElement(Typography, { sx: {
427
- marginTop: '28px',
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
- marginLeft: '24px',
431
- marginBottom: '10px',
432
- } }, `Add ${fieldDefinition.name}`),
433
- 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, form: form, action: action, setSnackbarError: setSnackbarError, selectedInstance: selectedInstance }))))),
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 { Action, EvokeForm, Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
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
- form?: EvokeForm;
22
- action?: Action;
23
- selectedInstance?: ObjectInstance;
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, form, } = props;
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
- form && (React.createElement(Grid, { container: true },
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
- (relationType === 'new' || mode === 'newOnly') && form ? (React.createElement(Box, { id: 'related-object-wrapper', sx: { width: '100%' } })) : (relatedObject &&
61
- mode !== 'newOnly' && (React.createElement(React.Fragment, null,
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 = cloneDeep(def);
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;
@@ -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
- import { ApiServices, Column, Columns, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
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
+ }