@evoke-platform/ui-components 1.8.0-testing.3 → 1.8.0-testing.5

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 (22) hide show
  1. package/dist/published/components/core/TextField/TextField.js +3 -2
  2. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +4 -0
  3. package/dist/published/components/custom/FormV2/FormRenderer.js +13 -14
  4. package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +4 -0
  5. package/dist/published/components/custom/FormV2/FormRendererContainer.js +60 -108
  6. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +4 -0
  7. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +9 -5
  8. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +12 -24
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +5 -1
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +80 -30
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +51 -27
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +5 -5
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +45 -7
  15. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +7 -4
  16. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +3 -0
  17. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +1 -3
  18. package/dist/published/components/custom/FormV2/components/types.d.ts +7 -1
  19. package/dist/published/components/custom/FormV2/components/utils.d.ts +27 -2
  20. package/dist/published/components/custom/FormV2/components/utils.js +107 -1
  21. package/dist/published/theme/hooks.d.ts +4 -0
  22. package/package.json +2 -2
@@ -4,7 +4,8 @@ import UIThemeProvider from '../../../theme';
4
4
  import FieldError from '../FieldError';
5
5
  import Typography from '../Typography';
6
6
  const TextField = (props) => {
7
- const { id, variant, label, labelPlacement, readOnly, required, error, instructionText, errorMessage } = props;
7
+ const { labelPlacement, readOnly, instructionText, errorMessage, ...muiProps } = props;
8
+ const { id, variant, label, required, error } = muiProps;
8
9
  const readOnlyStyles = {
9
10
  '.MuiOutlinedInput-root': {
10
11
  paddingRight: '5px',
@@ -28,7 +29,7 @@ const TextField = (props) => {
28
29
  readOnly: readOnly,
29
30
  'aria-readonly': !!readOnly,
30
31
  'data-testid': 'label-outside',
31
- }, ...props, label: null, sx: readOnly
32
+ }, ...muiProps, label: null, sx: readOnly
32
33
  ? { ...readOnlyStyles, ...props.sx }
33
34
  : {
34
35
  '& fieldset': { borderRadius: '8px' },
@@ -14,6 +14,10 @@ export type FormProps = BaseProps & {
14
14
  instance?: ObjectInstance | Document;
15
15
  onChange: (id: string, value: unknown) => void;
16
16
  onValidationChange?: (errors: FieldErrors) => void;
17
+ associatedObject?: {
18
+ instanceId?: string;
19
+ propertyId?: string;
20
+ };
17
21
  };
18
22
  declare function FormRenderer(props: FormProps): React.JSX.Element;
19
23
  export default FormRenderer;
@@ -1,5 +1,5 @@
1
1
  import { useObject } from '@evoke-platform/context';
2
- import { isEqual } from 'lodash';
2
+ import { isEmpty, isEqual } from 'lodash';
3
3
  import React, { useEffect, useMemo, useState } from 'react';
4
4
  import { useForm } from 'react-hook-form';
5
5
  import { useResponsive } from '../../../theme';
@@ -12,7 +12,7 @@ import { convertDocToParameters, convertPropertiesToParams } from './components/
12
12
  import { handleValidation } from './components/ValidationFiles/Validation';
13
13
  import ValidationErrorDisplay from './components/ValidationFiles/ValidationErrorDisplay';
14
14
  function FormRenderer(props) {
15
- const { onSubmit, value, fieldHeight, richTextEditor, hideButtons, stickyFooter, onCancel, form, instance, onChange, onValidationChange, } = props;
15
+ const { onSubmit, value, fieldHeight, richTextEditor, hideButtons, stickyFooter, onCancel, form, instance, onChange, onValidationChange, associatedObject, } = props;
16
16
  const { entries, name: title, objectId, actionId, display } = form;
17
17
  const { register, unregister, setValue, reset, handleSubmit, formState: { errors, isSubmitted }, getValues, } = useForm({
18
18
  defaultValues: value,
@@ -99,10 +99,9 @@ function FormRenderer(props) {
99
99
  }, []);
100
100
  if (entries && parameters && (!actionId || action)) {
101
101
  return (React.createElement(React.Fragment, null,
102
- React.createElement(Box, { sx: {
102
+ ((isSubmitted && !isEmpty(errors)) || (isSmallerThanMd && hasSections) || title) && (React.createElement(Box, { sx: {
103
103
  paddingX: isSmallerThanMd ? 2 : 3,
104
104
  paddingTop: '0px',
105
- borderBottom: '2px solid #F4F6F8',
106
105
  } },
107
106
  React.createElement(Box, { sx: {
108
107
  display: 'flex',
@@ -111,13 +110,14 @@ function FormRenderer(props) {
111
110
  flexWrap: 'wrap',
112
111
  paddingY: isSm || isXs ? 2 : 3,
113
112
  } },
114
- React.createElement(Typography, { sx: {
113
+ title && (React.createElement(Typography, { sx: {
115
114
  fontSize: '20px',
116
115
  lineHeight: '30px',
117
116
  fontWeight: 700,
118
117
  flexGrow: '1',
119
- } }, title),
118
+ } }, title)),
120
119
  isSmallerThanMd && hasSections && (React.createElement(Box, { sx: {
120
+ color: '#212B36',
121
121
  display: 'flex',
122
122
  alignItems: 'center',
123
123
  maxHeight: '22px',
@@ -140,7 +140,7 @@ function FormRenderer(props) {
140
140
  fontWeight: 400,
141
141
  fontSize: '14px',
142
142
  }, onClick: handleCollapseAll }, "Collapse all")))),
143
- React.createElement(ValidationErrorDisplay, { formId: form.id, title: title })),
143
+ React.createElement(ValidationErrorDisplay, { formId: form.id, title: title, errors: errors, showSubmitError: isSubmitted }))),
144
144
  React.createElement(FormContext.Provider, { value: {
145
145
  fetchedOptions,
146
146
  setFetchedOptions: updateFetchedOptions,
@@ -159,26 +159,25 @@ function FormRenderer(props) {
159
159
  handleChange: onChange,
160
160
  triggerFieldReset,
161
161
  showSubmitError: isSubmitted,
162
+ associatedObject,
162
163
  } },
163
164
  React.createElement(Box, { sx: {
164
- padding: isModal ? '0px' : isSm || isXs ? 2 : 3,
165
- paddingBottom: '0px',
166
- paddingTop: !hasSections ? undefined : '0px',
165
+ paddingX: isSm || isXs ? 2 : 3,
166
+ // when rendering the default delete action, we don't want a border
167
+ borderTop: !form.id || isModal ? undefined : '1px solid #e9ecef',
167
168
  } },
168
169
  entries.map((entry, index) => (React.createElement(RecursiveEntryRenderer, { key: index, entry: entry, isDocument: !!(form.id === 'documentForm') }))),
169
170
  !hideButtons && (actionId || form.id === 'documentForm') && onSubmit && (React.createElement(Box, { sx: {
170
171
  ...(stickyFooter === false ? { position: 'static' } : { position: 'sticky' }),
171
- bottom: isModal ? -5 : isSmallerThanMd ? 0 : 24,
172
+ bottom: isModal || isSmallerThanMd ? 0 : 24,
172
173
  zIndex: 1000,
173
174
  borderTop: action?.type !== 'delete' ? '1px solid #f4f6f8' : 'none',
174
175
  backgroundColor: '#fff',
175
- paddingY: isSmallerThanMd ? '16px' : '20px',
176
- paddingX: isSmallerThanMd ? '16px' : '20px',
176
+ padding: isSmallerThanMd ? '16px' : '20px',
177
177
  display: 'flex',
178
178
  justifyContent: isXs ? 'center' : 'flex-end',
179
179
  alignItems: 'center',
180
180
  marginX: isSmallerThanMd ? -2 : -3,
181
- marginBottom: '1px',
182
181
  borderRadius: '0px 0px 6px 6px',
183
182
  } },
184
183
  React.createElement(ActionButtons, { onSubmit: onSubmit, handleSubmit: handleSubmit, isModal: isModal, actionType: action?.type, submitButtonLabel: display?.submitLabel, onReset: handleReset, unregister: unregister, entries: entries, setValue: setValue, formId: form.id })))))));
@@ -16,6 +16,10 @@ export type FormProps = BaseProps & {
16
16
  richTextEditor?: ComponentType<SimpleEditorProps>;
17
17
  onClose?: () => void;
18
18
  onSubmit?: (submission: Record<string, unknown>) => Promise<void>;
19
+ associatedObject?: {
20
+ instanceId?: string;
21
+ propertyId?: string;
22
+ };
19
23
  };
20
24
  declare function FormRendererContainer(props: FormProps): React.JSX.Element;
21
25
  export default FormRendererContainer;
@@ -1,5 +1,4 @@
1
1
  import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
2
- import { LocalDateTime } from '@js-joda/core';
3
2
  import axios from 'axios';
4
3
  import { get, isArray, isEmpty, isEqual, merge, omit, pick, set, uniq } from 'lodash';
5
4
  import React, { useEffect, useState } from 'react';
@@ -7,10 +6,10 @@ import { Skeleton, Snackbar } from '../../core';
7
6
  import { Box } from '../../layout';
8
7
  import ErrorComponent from '../ErrorComponent';
9
8
  import { evalDefaultVals, processValueUpdate } from './components/DefaultValues';
10
- import { convertDocToEntries, encodePageSlug, formatDataToDoc, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, normalizeDateTime, } from './components/utils';
9
+ import { convertDocToEntries, deleteDocuments, encodePageSlug, formatDataToDoc, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, } from './components/utils';
11
10
  import FormRenderer from './FormRenderer';
12
11
  function FormRendererContainer(props) {
13
- const { instanceId, pageNavigation, documentId, dataType, display, formId, stickyFooter, objectId, actionId, richTextEditor, } = props;
12
+ const { instanceId, pageNavigation, documentId, dataType, display, formId, stickyFooter, objectId, actionId, richTextEditor, onClose, onSubmit, associatedObject, } = props;
14
13
  const apiServices = useApiServices();
15
14
  const navigateTo = useNavigate();
16
15
  const { id: appId, defaultPages } = useApp();
@@ -101,7 +100,8 @@ function FormRendererContainer(props) {
101
100
  .get(getPrefixedUrl(`data/forms/${formId || action?.defaultFormId}`))
102
101
  .then((evokeForm) => {
103
102
  if (evokeForm?.actionId === actionId) {
104
- setForm(evokeForm);
103
+ const form = onClose ? { ...evokeForm, name: '' } : evokeForm;
104
+ setForm(form);
105
105
  }
106
106
  else {
107
107
  setError(true);
@@ -111,24 +111,50 @@ function FormRendererContainer(props) {
111
111
  onError(error);
112
112
  });
113
113
  }
114
- else if (action && !action?.defaultFormId) {
114
+ else if (action) {
115
115
  apiServices
116
- .get(getPrefixedUrl(`data/forms?filter[where][actionId]=${action.id}`))
116
+ .get(getPrefixedUrl('data/forms'), {
117
+ params: {
118
+ filter: {
119
+ where: {
120
+ actionId: action.id,
121
+ objectId: objectId,
122
+ },
123
+ },
124
+ },
125
+ })
117
126
  .then((matchingForms) => {
118
127
  if (matchingForms.length === 1) {
119
- if (matchingForms[0]?.actionId === actionId) {
120
- setForm(matchingForms[0]);
121
- }
122
- else {
123
- setError(true);
124
- }
128
+ const form = onClose ? { ...matchingForms[0], name: '' } : matchingForms[0];
129
+ setForm(form);
130
+ // use this default form if no delete form is found
131
+ }
132
+ else if (action.type === 'delete' && instance) {
133
+ setForm({
134
+ id: '',
135
+ name: '',
136
+ entries: [
137
+ {
138
+ type: 'content',
139
+ html: `<p>You are about to delete <strong>${instance.name}</strong>. Deleted records can't be restored. Are you sure you want to continue?</p>`,
140
+ },
141
+ ],
142
+ objectId: objectId,
143
+ actionId: '_delete',
144
+ display: {
145
+ submitLabel: 'Delete',
146
+ },
147
+ });
148
+ }
149
+ else if (instance) {
150
+ setError(true);
125
151
  }
126
152
  })
127
153
  .catch((error) => {
128
154
  onError(error);
129
155
  });
130
156
  }
131
- }, [action]);
157
+ }, [action, objectId, instance]);
132
158
  useEffect(() => {
133
159
  if (form?.id === 'documentForm') {
134
160
  setParameters([
@@ -174,54 +200,6 @@ function FormRendererContainer(props) {
174
200
  };
175
201
  getInitialValues();
176
202
  }, [form, instance, sanitizedObject]);
177
- const uploadDocuments = async (files, metadata) => {
178
- const allDocuments = [];
179
- const formData = new FormData();
180
- for (const [index, file] of files.entries()) {
181
- if ('size' in file) {
182
- formData.append(`files[${index}]`, file);
183
- }
184
- else {
185
- allDocuments.push(file);
186
- }
187
- }
188
- if (metadata) {
189
- for (const [key, value] of Object.entries(metadata)) {
190
- formData.append(key, value);
191
- }
192
- }
193
- const docs = await apiServices?.post(getPrefixedUrl(`/objects/${form?.objectId}/instances/${instanceId}/documents`), formData);
194
- return allDocuments.concat(docs?.map((doc) => ({
195
- id: doc.id,
196
- name: doc.name,
197
- })) ?? []);
198
- };
199
- const deleteDocuments = async (submittedFields, requestSuccess, action) => {
200
- const documentProperties = action?.parameters
201
- ? action.parameters.filter((param) => param.type === 'document')
202
- : sanitizedObject?.properties?.filter((prop) => prop.type === 'document');
203
- for (const docProperty of documentProperties ?? []) {
204
- const savedValue = submittedFields[docProperty.id];
205
- const originalValue = instance?.[docProperty.id];
206
- const documentsToRemove = requestSuccess
207
- ? (originalValue?.filter((file) => !savedValue?.some((f) => f.id === file.id)) ?? [])
208
- : (savedValue?.filter((file) => !originalValue?.some((f) => f.id === file.id)) ?? []);
209
- for (const doc of documentsToRemove) {
210
- try {
211
- await apiServices?.delete(getPrefixedUrl(`/objects/${form?.objectId}/instances/${instanceId}/documents/${doc.id}`));
212
- }
213
- catch (error) {
214
- if (error) {
215
- setSnackbarError({
216
- showAlert: true,
217
- message: `An error occurred while removing document '${doc.name}'`,
218
- isError: true,
219
- });
220
- }
221
- }
222
- }
223
- }
224
- };
225
203
  const onSubmissionSuccess = (updatedInstance) => {
226
204
  setSnackbarError({
227
205
  showAlert: true,
@@ -247,47 +225,7 @@ function FormRendererContainer(props) {
247
225
  const saveHandler = async (submission) => {
248
226
  if (!form)
249
227
  return;
250
- // checks for a file upload and handles getting the new format to upload
251
- for (const [key, value] of Object.entries(submission)) {
252
- if (isArray(value)) {
253
- const fileInArray = value.some((item) => item instanceof File);
254
- if (fileInArray) {
255
- try {
256
- const uploadedDocuments = await uploadDocuments(value, {
257
- type: '',
258
- view_permission: '',
259
- });
260
- submission[key] = uploadedDocuments;
261
- }
262
- catch (err) {
263
- if (err) {
264
- setSnackbarError({
265
- showAlert: true,
266
- message: `An error occurred while uploading associated documents`,
267
- isError: true,
268
- });
269
- }
270
- return;
271
- }
272
- }
273
- // if there are address fields with no value address needs to be set to undefined to be able to submit
274
- }
275
- else if (typeof value === 'object' && value !== null) {
276
- if (Object.values(value).every((v) => v === undefined)) {
277
- submission[key] = undefined;
278
- // only submit the name and id of a related object
279
- }
280
- else if ('id' in value && 'name' in value) {
281
- submission[key] = pick(value, 'id', 'name');
282
- }
283
- }
284
- else if ((value === '' && !document) || value === undefined) {
285
- submission[key] = null;
286
- }
287
- else if (value instanceof LocalDateTime) {
288
- submission[key] = normalizeDateTime(value);
289
- }
290
- }
228
+ submission = await formatSubmission(submission, apiServices, objectId, instanceId, setSnackbarError);
291
229
  if (document) {
292
230
  submission = formatDataToDoc(submission);
293
231
  }
@@ -331,9 +269,9 @@ function FormRendererContainer(props) {
331
269
  ?.filter((property) => !property.formula && property.type !== 'collection')
332
270
  .map((property) => property.id) ?? []),
333
271
  });
334
- if (response) {
272
+ if (response && sanitizedObject && instance) {
335
273
  onSubmissionSuccess(response);
336
- deleteDocuments(submission, !!response, action ?? undefined);
274
+ deleteDocuments(submission, !!response, apiServices, sanitizedObject, instance, action ?? undefined, setSnackbarError);
337
275
  }
338
276
  }
339
277
  }
@@ -393,7 +331,17 @@ function FormRendererContainer(props) {
393
331
  const fieldValue = instanceData?.[fieldId] ??
394
332
  instanceData?.metadata?.[fieldId];
395
333
  const parameter = parameters?.find((param) => param.id === fieldId);
396
- if (entry.type !== 'readonlyField' && isEmptyWithDefault(fieldValue, entry, instanceData)) {
334
+ if (associatedObject?.propertyId === fieldId && associatedObject?.instanceId && parameter) {
335
+ try {
336
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
337
+ result[associatedObject.propertyId] = instance;
338
+ }
339
+ catch (error) {
340
+ console.error(error);
341
+ }
342
+ }
343
+ else if (entry.type !== 'readonlyField' &&
344
+ isEmptyWithDefault(fieldValue, entry, instanceData)) {
397
345
  if (fieldId && parameters && parameters.length > 0) {
398
346
  const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
399
347
  for (const { fieldId, fieldValue } of defaultValuesArray) {
@@ -463,9 +411,13 @@ function FormRendererContainer(props) {
463
411
  })();
464
412
  }
465
413
  const isLoading = (instanceId && !formData && !document) || !form || !sanitizedObject;
466
- return !error ? (React.createElement(React.Fragment, null,
467
- !isLoading ? (React.createElement(React.Fragment, null,
468
- React.createElement(FormRenderer, { onSubmit: saveHandler, hideButtons: document && !hasDocumentUpdateAccess, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value: formData, stickyFooter: stickyFooter, form: form, instance: dataType !== 'documents' ? instance : document, onChange: onChange, onCancel: onCancel }))) : (React.createElement(Box, { sx: { padding: '20px' } },
414
+ return !error ? (React.createElement(Box, { sx: {
415
+ backgroundColor: '#ffffff',
416
+ borderRadius: '6px',
417
+ padding: '0px',
418
+ border: !isLoading && !onClose ? '1px solid #dbe0e4' : undefined,
419
+ } },
420
+ !isLoading ? (React.createElement(FormRenderer, { onSubmit: onSubmit ?? saveHandler, hideButtons: document && !hasDocumentUpdateAccess, richTextEditor: richTextEditor, fieldHeight: display?.fieldHeight ?? 'medium', value: formData, stickyFooter: stickyFooter, form: form, instance: dataType !== 'documents' ? instance : document, onChange: onChange, onCancel: onClose ?? onCancel, associatedObject: associatedObject })) : (React.createElement(Box, { sx: { padding: '20px' } },
469
421
  React.createElement(Box, { display: 'flex', width: '100%', justifyContent: 'space-between' },
470
422
  React.createElement(Skeleton, { width: '78%', sx: { borderRadius: '8px', height: '40px' } }),
471
423
  React.createElement(Skeleton, { width: '20%', sx: { borderRadius: '8px', height: '40px' } })),
@@ -20,6 +20,10 @@ type FormContextType = {
20
20
  fieldHeight?: 'small' | 'medium';
21
21
  triggerFieldReset?: boolean;
22
22
  showSubmitError?: boolean;
23
+ associatedObject?: {
24
+ instanceId?: string;
25
+ propertyId?: string;
26
+ };
23
27
  };
24
28
  export declare const FormContext: import("react").Context<FormContextType>;
25
29
  export {};
@@ -1,14 +1,18 @@
1
- import { Action, ActionType, EvokeForm, InputParameter, Obj } from '@evoke-platform/context';
1
+ import { Action, InputParameter, Obj } from '@evoke-platform/context';
2
2
  import React from 'react';
3
+ import { FieldValues } from 'react-hook-form';
3
4
  export type ActionDialogProps = {
4
5
  open: boolean;
5
6
  onClose: () => void;
6
7
  action: Action;
7
- instanceInput: Record<string, unknown>;
8
- handleSubmit: (actionType: ActionType, input: Record<string, unknown> | undefined, instanceId?: string, setSubmitting?: (value: boolean) => void) => void;
8
+ handleSubmit: (action: Action, input: FieldValues, instanceId?: string, setSubmitting?: (value: boolean) => void) => void;
9
9
  object: Obj;
10
10
  instanceId?: string;
11
- relatedParameter?: InputParameter;
12
- relatedForm?: EvokeForm;
11
+ relatedParameter: InputParameter;
12
+ relatedFormId?: string;
13
+ associatedObject?: {
14
+ instanceId?: string;
15
+ propertyId?: string;
16
+ };
13
17
  };
14
18
  export declare const ActionDialog: (props: ActionDialogProps) => React.JSX.Element;
@@ -1,9 +1,11 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
2
  import { Close } from '@mui/icons-material';
3
3
  import React, { useEffect, useState } from 'react';
4
+ import { useFormContext } from '../../../../../../theme/hooks';
4
5
  import { Dialog, DialogContent, DialogTitle, IconButton, Skeleton } from '../../../../../core';
5
6
  import { Box } from '../../../../../layout';
6
7
  import ErrorComponent from '../../../../ErrorComponent';
8
+ import FormRendererContainer from '../../../FormRendererContainer';
7
9
  import { getPrefixedUrl } from '../../utils';
8
10
  const styles = {
9
11
  button: {
@@ -36,12 +38,11 @@ const styles = {
36
38
  },
37
39
  };
38
40
  export const ActionDialog = (props) => {
39
- const { open, onClose, action, object, instanceId, relatedForm, instanceInput } = props;
41
+ const { open, onClose, action, object, instanceId, relatedFormId, relatedParameter, handleSubmit, associatedObject, } = props;
40
42
  const [loading, setLoading] = useState(false);
41
43
  const [hasAccess, setHasAccess] = useState();
42
- const [form, setForm] = useState();
44
+ const { stickyFooter, fieldHeight, richTextEditor } = useFormContext();
43
45
  const apiServices = useApiServices();
44
- const isDeleteAction = action.type === 'delete';
45
46
  useEffect(() => {
46
47
  if (instanceId) {
47
48
  setLoading(true);
@@ -57,31 +58,18 @@ export const ActionDialog = (props) => {
57
58
  setLoading(false);
58
59
  }
59
60
  }, [object, instanceId]);
60
- useEffect(() => {
61
- setForm(isDeleteAction && !form
62
- ? {
63
- id: '',
64
- name: '',
65
- entries: [
66
- {
67
- type: 'content',
68
- html: `<p>You are about to delete ${instanceInput?.name}. Deleted records can't be restored. Are you sure you want to continue?</p>`,
69
- },
70
- ],
71
- objectId: object.id,
72
- actionId: '_delete',
73
- display: {
74
- submitLabel: 'Delete',
75
- },
76
- }
77
- : relatedForm);
78
- }, [relatedForm, action, form]);
61
+ const handleFormSave = async (data) => {
62
+ return handleSubmit(action, data, instanceId);
63
+ };
79
64
  return (React.createElement(Dialog, { maxWidth: 'md', fullWidth: true, open: open, onClose: (e, reason) => reason !== 'backdropClick' && onClose() },
80
- React.createElement(DialogTitle, { sx: styles.dialogTitle },
65
+ React.createElement(DialogTitle, { sx: { ...styles.dialogTitle, borderBottom: action.type === 'delete' ? undefined : '1px solid #e9ecef' } },
81
66
  React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose },
82
67
  React.createElement(Close, { fontSize: "small" })),
83
68
  action && hasAccess && !loading ? action?.name : ''),
84
- React.createElement(DialogContent, { sx: { paddingBottom: loading ? undefined : '0px' } }, hasAccess ? (React.createElement(Box, { sx: { width: '100%', marginTop: '10px' } })) : (React.createElement(React.Fragment, null, loading ? (React.createElement(React.Fragment, null,
69
+ React.createElement(DialogContent, { sx: { paddingBottom: loading ? undefined : '0px' } }, hasAccess ? (React.createElement(Box, { sx: { width: '100%', marginTop: '10px' } },
70
+ React.createElement(FormRendererContainer, { instanceId: instanceId, formId: relatedFormId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: action.id, stickyFooter: stickyFooter,
71
+ // relatedParameter will have an objectId here
72
+ objectId: relatedParameter.objectId, onClose: onClose, onSubmit: handleFormSave, richTextEditor: richTextEditor, associatedObject: associatedObject }))) : (React.createElement(React.Fragment, null, loading ? (React.createElement(React.Fragment, null,
85
73
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
86
74
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
87
75
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }))) : (React.createElement(ErrorComponent, { code: 'AccessDenied', message: 'You do not have permission to perform this action.', styles: { boxShadow: 'none' } })))))));
@@ -1,10 +1,14 @@
1
- import { InputParameter, Property, ViewLayoutEntityReference } from '@evoke-platform/context';
1
+ import { InputField, InputParameter, InputParameterReference, Property, ReadonlyField, ViewLayoutEntityReference } from '@evoke-platform/context';
2
2
  import React from 'react';
3
3
  export type ObjectPropertyInputProps = {
4
4
  fieldDefinition: InputParameter | Property;
5
5
  canUpdateProperty: boolean;
6
6
  criteria?: object;
7
7
  viewLayout?: ViewLayoutEntityReference;
8
+ entry: InputField | InputParameterReference | ReadonlyField;
9
+ createActionId?: string;
10
+ updateActionId?: string;
11
+ deleteActionId?: string;
8
12
  };
9
13
  declare const RepeatableField: (props: ObjectPropertyInputProps) => React.JSX.Element;
10
14
  export default RepeatableField;
@@ -1,6 +1,5 @@
1
1
  import { useApiServices, useNotification, } from '@evoke-platform/context';
2
- import { LocalDateTime } from '@js-joda/core';
3
- import { get, isEqual, isObject, pick, startCase } from 'lodash';
2
+ import { get, isEqual, pick, startCase } from 'lodash';
4
3
  import { DateTime } from 'luxon';
5
4
  import React, { useCallback, useEffect, useState } from 'react';
6
5
  import sift from 'sift';
@@ -11,7 +10,7 @@ import { Accordion, AccordionDetails, AccordionSummary, Button, IconButton, Skel
11
10
  import { Box } from '../../../../../layout';
12
11
  import { getReadableQuery } from '../../../../CriteriaBuilder';
13
12
  import { retrieveCustomErrorMessage } from '../../../../Form/utils';
14
- import { getPrefixedUrl, normalizeDateTime, transformToWhere } from '../../utils';
13
+ import { deleteDocuments, formatSubmission, getPrefixedUrl, transformToWhere } from '../../utils';
15
14
  import { ActionDialog } from './ActionDialog';
16
15
  import { DocumentViewerCell } from './DocumentViewerCell';
17
16
  const styles = {
@@ -36,7 +35,7 @@ const styles = {
36
35
  },
37
36
  };
38
37
  const RepeatableField = (props) => {
39
- const { fieldDefinition, canUpdateProperty, criteria, viewLayout } = props;
38
+ const { fieldDefinition, canUpdateProperty, criteria, viewLayout, entry, createActionId, updateActionId, deleteActionId, } = props;
40
39
  const { fetchedOptions, setFetchedOptions, instance } = useFormContext();
41
40
  const { instanceChanges } = useNotification();
42
41
  const apiServices = useApiServices();
@@ -54,11 +53,49 @@ const RepeatableField = (props) => {
54
53
  const [hasCreateAction, setHasCreateAction] = useState(fetchedOptions[`${fieldDefinition.id}HasCreateAction`] || false);
55
54
  const [loading, setLoading] = useState((relatedObject && relatedInstances) || !fieldDefinition ? false : true);
56
55
  const [tableViewLayout, setTableViewLayout] = useState(fetchedOptions[`${fieldDefinition.id}TableViewLayout`]);
56
+ const [createForm, setCreateForm] = useState(fetchedOptions[`${fieldDefinition.id}-createForm`]);
57
+ const [updateForm, setUpdateForm] = useState(fetchedOptions[`${fieldDefinition.id}-updateForm`]);
58
+ const [deleteForm, setDeleteForm] = useState(fetchedOptions[`${fieldDefinition.id}-deleteForm`]);
57
59
  const [snackbarError, setSnackbarError] = useState({
58
60
  showAlert: false,
59
61
  isError: false,
60
62
  });
61
- const DEFAULT_CREATE_ACTION = '_create';
63
+ const createAction = relatedObject?.actions?.find((item) => item.id === createActionId);
64
+ const updateAction = relatedObject?.actions?.find((item) => item.id === updateActionId);
65
+ const deleteAction = relatedObject?.actions?.find((item) => item.id === deleteActionId);
66
+ function getForm(setForm, action, formId) {
67
+ if (formId || action?.defaultFormId) {
68
+ apiServices
69
+ .get(getPrefixedUrl(`data/forms/${formId || action?.defaultFormId}`))
70
+ .then((evokeForm) => {
71
+ setForm(evokeForm);
72
+ })
73
+ .catch((error) => {
74
+ console.error(error);
75
+ });
76
+ }
77
+ else if (action) {
78
+ apiServices
79
+ .get(getPrefixedUrl('data/forms'), {
80
+ params: {
81
+ filter: {
82
+ where: {
83
+ actionId: action.id,
84
+ objectId: fieldDefinition.objectId,
85
+ },
86
+ },
87
+ },
88
+ })
89
+ .then((matchingForms) => {
90
+ if (matchingForms.length === 1) {
91
+ setForm(matchingForms[0]);
92
+ }
93
+ })
94
+ .catch((error) => {
95
+ console.error(error);
96
+ });
97
+ }
98
+ }
62
99
  const fetchRelatedInstances = useCallback(async (refetch = false) => {
63
100
  let relatedObject;
64
101
  if (fieldDefinition.objectId) {
@@ -168,6 +205,14 @@ const RepeatableField = (props) => {
168
205
  if (relatedObject)
169
206
  fetchCriteriaObjects();
170
207
  }, [fetchCriteriaObjects, relatedObject]);
208
+ useEffect(() => {
209
+ if (createAction && !createForm)
210
+ getForm(setCreateForm, createAction); // TODO: pass entry.display?.createForm as a third argument
211
+ if (updateAction && !updateForm)
212
+ getForm(setUpdateForm, updateAction); // TODO: pass entry.display?.updateForm as a third argument
213
+ if (deleteAction && !deleteForm)
214
+ getForm(setDeleteForm, deleteAction); // TODO: pass entry.display?.deleteForm as a third argument
215
+ }, [entry.display, createAction, updateAction, deleteAction]);
171
216
  useEffect(() => {
172
217
  if (relatedObject?.rootObjectId) {
173
218
  // pass true here so while it doesn't refetch on every tab change it does refetch on changes made
@@ -226,11 +271,7 @@ const RepeatableField = (props) => {
226
271
  })
227
272
  .then((checkAccess) => {
228
273
  const action = relatedObject.actions?.find((item) => item.id === '_create');
229
- if (action &&
230
- fieldDefinition.relatedPropertyId &&
231
- // TODO: replace with the entries create form or defaultFormId of the
232
- // default create action, keeping it like this to get minimum changes out so other can use it
233
- !!fieldDefinition.createForm) {
274
+ if (action && fieldDefinition.relatedPropertyId) {
234
275
  const { relatedObjectProperty, criteria } = retrieveCriteria(fieldDefinition.relatedPropertyId, action, relatedObject);
235
276
  if (!criteria || JSON.stringify(criteria).includes('{{{input.') || !relatedObjectProperty) {
236
277
  setHasCreateAction(checkAccess.result);
@@ -301,22 +342,17 @@ const RepeatableField = (props) => {
301
342
  },
302
343
  'min-width': '44px',
303
344
  }, variant: "text", onClick: () => setReloadOnErrorTrigger((prevState) => !prevState) }, "Retry")));
304
- const save = async (actionType, input, instanceId) => {
305
- // date-time fields are stored in the database in ISO format so convert all
306
- // LocalDateTime objects to ISO format.
307
- if (isObject(input)) {
308
- input = Object.entries(input).reduce((agg, [key, value]) => Object.assign(agg, {
309
- [key]: value instanceof LocalDateTime ? normalizeDateTime(value) : value,
310
- }), {});
311
- }
312
- if (actionType === 'create') {
345
+ const save = async (action, input, instanceId) => {
346
+ // when save is called we know that fieldDefinition is a parameter and fieldDefinition.objectId is defined
347
+ input = await formatSubmission(input, apiServices, fieldDefinition.objectId, instanceId);
348
+ if (action.type === 'create' && createActionId) {
313
349
  const updatedInput = {
314
350
  ...input,
315
351
  [fieldDefinition?.relatedPropertyId]: { id: instance?.id },
316
352
  };
317
353
  try {
318
354
  const instance = await apiServices.post(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances/actions`), {
319
- actionId: DEFAULT_CREATE_ACTION,
355
+ actionId: createActionId,
320
356
  input: updatedInput,
321
357
  });
322
358
  const hasAccess = fieldDefinition?.relatedPropertyId && fieldDefinition.relatedPropertyId in instance;
@@ -337,13 +373,16 @@ const RepeatableField = (props) => {
337
373
  else {
338
374
  const relatedObjectId = relatedObject?.id;
339
375
  try {
340
- await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${instanceId}/actions`), {
341
- actionId: `_${actionType}`,
376
+ const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${instanceId}/actions`), {
377
+ actionId: `_${action.type}`,
342
378
  input: pick(input, relatedObject?.properties
343
379
  ?.filter((property) => !property.formula && property.type !== 'collection')
344
380
  .map((property) => property.id) ?? []),
345
381
  });
346
- if (actionType === 'delete') {
382
+ if (response && relatedObject && instance) {
383
+ deleteDocuments(input, !!response, apiServices, relatedObject, instance, action);
384
+ }
385
+ if (action.type === 'delete') {
347
386
  setRelatedInstances((prevInstances) => prevInstances.filter((instance) => instance.id !== instanceId));
348
387
  }
349
388
  else {
@@ -357,7 +396,7 @@ const RepeatableField = (props) => {
357
396
  setSnackbarError({
358
397
  showAlert: true,
359
398
  message: retrieveCustomErrorMessage(err) ??
360
- `An error occurred while ${actionType === 'delete' ? ' deleting' : ' updating'} an instance`,
399
+ `An error occurred while ${action.type === 'delete' ? ' deleting' : ' updating'} an instance`,
361
400
  isError: true,
362
401
  });
363
402
  }
@@ -501,7 +540,7 @@ const RepeatableField = (props) => {
501
540
  cursor: 'pointer',
502
541
  },
503
542
  }
504
- : {}, onClick: !!fieldDefinition.updatedForm && // TODO: replace with the entries update form
543
+ : {}, onClick: updateActionId &&
505
544
  canUpdateProperty &&
506
545
  prop.id === 'name'
507
546
  ? () => editRow(relatedInstance.id)
@@ -511,16 +550,27 @@ const RepeatableField = (props) => {
511
550
  users?.find((user) => get(relatedInstance, `${prop.id.split('.')[0]}.id`) === user.id)?.status === 'Inactive' && (React.createElement("span", null, ' (Inactive)'))))));
512
551
  }),
513
552
  canUpdateProperty && (React.createElement(TableCell, { sx: { width: '80px' } },
514
- !!fieldDefinition.updateForm && ( // TODO: replace with the entries update form
515
- React.createElement(IconButton, { "aria-label": `edit-collection-instance-${index}`, onClick: () => editRow(relatedInstance.id) },
553
+ updateActionId && (React.createElement(IconButton, { "aria-label": `edit-collection-instance-${index}`, onClick: () => editRow(relatedInstance.id) },
516
554
  React.createElement(Tooltip, { title: "Edit" },
517
555
  React.createElement(Edit, null)))),
518
556
  React.createElement(IconButton, { "aria-label": `delete-collection-instance-${index}`, onClick: () => deleteRow(relatedInstance.id) },
519
557
  React.createElement(Tooltip, { title: "Delete" },
520
558
  React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } })))))))))))),
521
- hasCreateAction && (React.createElement(Button, { variant: "contained", sx: styles.addButton, onClick: addRow, "aria-label": 'Add' }, "Add"))),
522
- relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, onClose: () => setOpenDialog(false), instanceInput: relatedInstances.find((i) => i.id === selectedRow) ?? {}, handleSubmit: save, action: relatedObject?.actions?.find((a) => a.id ===
523
- (dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, relatedParameter: fieldDefinition })),
559
+ hasCreateAction && createActionId && (React.createElement(Button, { variant: "contained", sx: styles.addButton, onClick: addRow, "aria-label": 'Add' }, "Add"))),
560
+ relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, onClose: () => setOpenDialog(false), handleSubmit: save, action: relatedObject?.actions?.find((a) => a.id ===
561
+ (dialogType === 'create'
562
+ ? createActionId
563
+ : dialogType === 'update'
564
+ ? updateActionId
565
+ : deleteActionId)), relatedFormId: dialogType === 'create'
566
+ ? createForm?.id
567
+ : dialogType === 'update'
568
+ ? updateForm?.id
569
+ : dialogType === 'delete'
570
+ ? deleteForm?.id
571
+ : undefined, instanceId: selectedRow, relatedParameter: fieldDefinition, associatedObject: instance?.id && fieldDefinition.relatedPropertyId
572
+ ? { instanceId: instance.id, propertyId: fieldDefinition.relatedPropertyId }
573
+ : undefined })),
524
574
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
525
575
  };
526
576
  export default RepeatableField;
@@ -193,7 +193,7 @@ const InstanceLookup = (props) => {
193
193
  setRows([]);
194
194
  }
195
195
  }, [filter, searchString.length]);
196
- return (React.createElement(Grid, { container: true, sx: { paddingBottom: '30px' } },
196
+ return (React.createElement(Grid, { container: true, sx: { padding: '30px 24px' } },
197
197
  React.createElement(Grid, { item: true, xs: 12 }, searchableColumns.length ? (React.createElement(SearchField, { searchString: searchString, setSearchString: setSearchString, filter: filter, setFilter: setFilter, searchableColumns: searchableColumns })) : (React.createElement(Typography, { sx: { fontSize: '16px', fontWeight: '700' } }, "There are no searchable properties configured for this object"))),
198
198
  React.createElement(BuilderGrid, { item: 'instances', rows: rows, columns: retrieveColumns(layout), onRowClick: (params) => setSelectedInstance(params.row), initialSort: {
199
199
  field: object.viewLayout?.table?.sort?.colId ?? 'name',
@@ -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(`data/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('data/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',
@@ -38,7 +38,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
38
38
  }
39
39
  export function RecursiveEntryRenderer(props) {
40
40
  const { entry, isDocument } = props;
41
- const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, } = useFormContext();
41
+ const { fetchedOptions, setFetchedOptions, object, getValues, errors, instance, richTextEditor, parameters, handleChange, fieldHeight, triggerFieldReset, associatedObject, } = useFormContext();
42
42
  // If the entry is hidden, clear its value and any nested values, and skip rendering
43
43
  if (!entryIsVisible(entry, getValues(), instance)) {
44
44
  return null;
@@ -78,6 +78,8 @@ export function RecursiveEntryRenderer(props) {
78
78
  return def;
79
79
  }, [entry, parameters, object]);
80
80
  const validation = fieldDefinition?.validation || {};
81
+ if (associatedObject?.propertyId === entryId)
82
+ return null;
81
83
  useEffect(() => {
82
84
  if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
83
85
  fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
@@ -107,7 +109,9 @@ export function RecursiveEntryRenderer(props) {
107
109
  ? display?.defaultValue.orderBy
108
110
  : undefined, defaultValueCriteria: typeof display?.defaultValue === 'object' && 'criteria' in display.defaultValue
109
111
  ? display?.defaultValue?.criteria
110
- : undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description })));
112
+ : undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description,
113
+ // formId={display?.createFormId} // TODO: this should be added as part of the builder update
114
+ createActionId: '_create' })));
111
115
  }
112
116
  else if (fieldDefinition.type === 'user') {
113
117
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -116,7 +120,7 @@ export function RecursiveEntryRenderer(props) {
116
120
  else if (fieldDefinition.type === 'collection') {
117
121
  return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
118
122
  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 })));
123
+ React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, entry: entry, createActionId: '_create', updateActionId: '_update', deleteActionId: '_delete' })));
120
124
  }
121
125
  else if (fieldDefinition.type === 'richText') {
122
126
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, richTextEditor ? (React.createElement(richTextEditor, {
@@ -140,7 +144,6 @@ export function RecursiveEntryRenderer(props) {
140
144
  else {
141
145
  // Add `aria-describedby` to ensure screen readers read the description
142
146
  // when the input is tabbed into.
143
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
147
  const additionalProps = {};
145
148
  if (fieldDefinition.enum && display?.description) {
146
149
  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';
@@ -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) {
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
+ }
@@ -126,4 +126,8 @@ export declare function useFormContext(): {
126
126
  fieldHeight?: "medium" | "small" | undefined;
127
127
  triggerFieldReset?: boolean | undefined;
128
128
  showSubmitError?: boolean | undefined;
129
+ associatedObject?: {
130
+ instanceId?: string | undefined;
131
+ propertyId?: string | undefined;
132
+ } | undefined;
129
133
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.8.0-testing.3",
3
+ "version": "1.8.0-testing.5",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -90,7 +90,7 @@
90
90
  "yalc": "^1.0.0-pre.53"
91
91
  },
92
92
  "peerDependencies": {
93
- "@evoke-platform/context": "^1.3.2-testing.0",
93
+ "@evoke-platform/context": "^1.3.2-0",
94
94
  "react": "^18.1.0",
95
95
  "react-dom": "^18.1.0"
96
96
  },