@evoke-platform/ui-components 1.8.0-testing.4 → 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.
- package/dist/published/components/custom/FormV2/FormRenderer.d.ts +4 -0
- package/dist/published/components/custom/FormV2/FormRenderer.js +13 -14
- package/dist/published/components/custom/FormV2/FormRendererContainer.d.ts +4 -0
- package/dist/published/components/custom/FormV2/FormRendererContainer.js +60 -108
- package/dist/published/components/custom/FormV2/components/FormContext.d.ts +4 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.d.ts +9 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +12 -24
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.d.ts +5 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +80 -30
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +1 -1
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +51 -27
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +5 -5
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +45 -7
- package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +7 -4
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.d.ts +3 -0
- package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js +1 -3
- package/dist/published/components/custom/FormV2/components/types.d.ts +7 -1
- package/dist/published/components/custom/FormV2/components/utils.d.ts +27 -2
- package/dist/published/components/custom/FormV2/components/utils.js +107 -1
- package/dist/published/theme/hooks.d.ts +4 -0
- package/package.json +2 -2
|
@@ -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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
114
|
+
else if (action) {
|
|
115
115
|
apiServices
|
|
116
|
-
.get(getPrefixedUrl(
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
467
|
-
|
|
468
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
12
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
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' } }
|
|
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 {
|
|
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 {
|
|
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
|
|
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 (
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
if (
|
|
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:
|
|
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: `_${
|
|
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 (
|
|
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 ${
|
|
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:
|
|
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
|
-
|
|
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),
|
|
523
|
-
(dialogType === 'create'
|
|
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: {
|
|
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
|
|
34
|
-
const action = relatedObject?.actions?.find((action) => action.id === DEFAULT_CREATE_ACTION);
|
|
33
|
+
const action = relatedObject?.actions?.find((action) => action.id === createActionId);
|
|
35
34
|
const apiServices = useApiServices();
|
|
36
35
|
const navigateTo = useNavigate();
|
|
37
|
-
const updatedCriteria =
|
|
38
|
-
? {
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
: undefined;
|
|
36
|
+
const updatedCriteria = useMemo(() => {
|
|
37
|
+
return filter ? { where: transformToWhere(filter) } : undefined;
|
|
38
|
+
}, [filter]);
|
|
42
39
|
useEffect(() => {
|
|
43
40
|
if (relatedObject) {
|
|
44
41
|
let defaultViewLayout;
|
|
@@ -94,10 +91,12 @@ const ObjectPropertyInput = (props) => {
|
|
|
94
91
|
}
|
|
95
92
|
}, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
|
|
96
93
|
const getDropdownOptions = useCallback((name) => {
|
|
97
|
-
if ((!fetchedOptions[`${id}Options`] ||
|
|
98
|
-
fetchedOptions[`${id}Options`].length === 0) &&
|
|
99
|
-
!hasFetched)
|
|
94
|
+
if (((!fetchedOptions?.[`${id}Options`] ||
|
|
95
|
+
(fetchedOptions?.[`${id}Options`]).length === 0) &&
|
|
96
|
+
!hasFetched) ||
|
|
97
|
+
!isEqual(fetchedOptions?.[`${id}UpdatedCriteria`], updatedCriteria)) {
|
|
100
98
|
setLoadingOptions(true);
|
|
99
|
+
setFetchedOptions && setFetchedOptions({ [`${id}UpdatedCriteria`]: updatedCriteria });
|
|
101
100
|
const updatedFilter = cloneDeep(updatedCriteria) || {};
|
|
102
101
|
updatedFilter.limit = 100;
|
|
103
102
|
const { propertyId, direction } = layout?.sort ?? {
|
|
@@ -146,17 +145,38 @@ const ObjectPropertyInput = (props) => {
|
|
|
146
145
|
return () => debouncedGetDropdownOptions.cancel();
|
|
147
146
|
}, [dropdownInput]);
|
|
148
147
|
useEffect(() => {
|
|
149
|
-
if (action?.defaultFormId) {
|
|
148
|
+
if (formId || action?.defaultFormId) {
|
|
150
149
|
apiServices
|
|
151
|
-
.get(getPrefixedUrl(`data/forms/${action
|
|
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(
|
|
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' &&
|
|
277
|
+
mode !== 'existingOnly' && createActionId && (React.createElement(Button, { fullWidth: true, sx: {
|
|
258
278
|
justifyContent: 'flex-start',
|
|
259
279
|
pl: 2,
|
|
260
280
|
minHeight: '48px',
|
|
@@ -422,15 +442,19 @@ const ObjectPropertyInput = (props) => {
|
|
|
422
442
|
event.stopPropagation();
|
|
423
443
|
setOpenCreateDialog(true);
|
|
424
444
|
}, "aria-label": `Add` }, "Add")))),
|
|
425
|
-
openCreateDialog && (React.createElement(React.Fragment, null, nestedFieldsView ? (React.createElement(RelatedObjectInstance, { id: id, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, nestedFieldsView: nestedFieldsView, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout,
|
|
426
|
-
React.createElement(
|
|
427
|
-
|
|
428
|
-
fontSize: '22px',
|
|
445
|
+
openCreateDialog && (React.createElement(React.Fragment, null, nestedFieldsView ? (React.createElement(RelatedObjectInstance, { id: id, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, nestedFieldsView: nestedFieldsView, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: form?.id, actionId: createActionId, setSnackbarError: setSnackbarError, fieldDefinition: fieldDefinition })) : (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: openCreateDialog, onClose: (e, reason) => reason !== 'backdropClick' && handleClose },
|
|
446
|
+
React.createElement(DialogTitle, { sx: {
|
|
447
|
+
fontSize: '18px',
|
|
429
448
|
fontWeight: 700,
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
449
|
+
paddingTop: '35px',
|
|
450
|
+
paddingBottom: '20px',
|
|
451
|
+
borderBottom: '1px solid #e9ecef',
|
|
452
|
+
} },
|
|
453
|
+
React.createElement(IconButton, { sx: { position: 'absolute', right: '17px', top: '22px' }, onClick: handleClose },
|
|
454
|
+
React.createElement(Close, { fontSize: "small" })),
|
|
455
|
+
form?.name ?? `Add ${fieldDefinition.name}`),
|
|
456
|
+
React.createElement(DialogContent, { sx: { padding: '0px' } },
|
|
457
|
+
React.createElement(RelatedObjectInstance, { handleClose: handleClose, setSelectedInstance: setSelectedInstance, nestedFieldsView: nestedFieldsView, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError, fieldDefinition: fieldDefinition })))))),
|
|
434
458
|
React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
|
|
435
459
|
isError: snackbarError.isError,
|
|
436
460
|
showAlert: false,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { InputParameter, Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { BaseProps } from '../../types';
|
|
4
4
|
export type RelatedObjectInstanceProps = BaseProps & {
|
|
5
|
-
relatedObject: Obj | undefined;
|
|
6
5
|
id: string;
|
|
6
|
+
relatedObject: Obj | undefined;
|
|
7
7
|
setSelectedInstance: (selectedInstance: ObjectInstance) => void;
|
|
8
8
|
handleClose: () => void;
|
|
9
9
|
mode: 'default' | 'existingOnly' | 'newOnly';
|
|
@@ -18,9 +18,9 @@ export type RelatedObjectInstanceProps = BaseProps & {
|
|
|
18
18
|
options: ObjectInstance[];
|
|
19
19
|
filter?: Record<string, unknown>;
|
|
20
20
|
layout?: TableViewLayout;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
formId?: string;
|
|
22
|
+
actionId?: string;
|
|
23
|
+
fieldDefinition: InputParameter;
|
|
24
24
|
};
|
|
25
25
|
declare const RelatedObjectInstance: (props: RelatedObjectInstanceProps) => React.JSX.Element;
|
|
26
26
|
export default RelatedObjectInstance;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { useApiServices } from '@evoke-platform/context';
|
|
1
2
|
import { InfoRounded } from '@mui/icons-material';
|
|
2
3
|
import React, { useState } from 'react';
|
|
3
4
|
import { useFormContext } from '../../../../../../theme/hooks';
|
|
4
5
|
import { Alert, Button, FormControlLabel, Radio, RadioGroup } from '../../../../../core';
|
|
5
6
|
import { Box, Grid } from '../../../../../layout';
|
|
7
|
+
import FormRendererContainer from '../../../FormRendererContainer';
|
|
8
|
+
import { formatSubmission, getPrefixedUrl } from '../../utils';
|
|
6
9
|
import InstanceLookup from './InstanceLookup';
|
|
7
10
|
const styles = {
|
|
8
11
|
actionButtons: {
|
|
@@ -14,11 +17,12 @@ const styles = {
|
|
|
14
17
|
},
|
|
15
18
|
};
|
|
16
19
|
const RelatedObjectInstance = (props) => {
|
|
17
|
-
const { relatedObject, id, setSelectedInstance, handleClose, nestedFieldsView, mode, displayOption, filter, layout,
|
|
18
|
-
const { handleChange: handleChangeObjectField } = useFormContext();
|
|
20
|
+
const { relatedObject, id, setSelectedInstance, handleClose, nestedFieldsView, mode, displayOption, filter, layout, formId, actionId, fieldDefinition, setSnackbarError, setOptions, options, } = props;
|
|
21
|
+
const { handleChange: handleChangeObjectField, richTextEditor, stickyFooter, fieldHeight } = useFormContext();
|
|
19
22
|
const [errors, setErrors] = useState([]);
|
|
20
23
|
const [selectedRow, setSelectedRow] = useState();
|
|
21
24
|
const [relationType, setRelationType] = useState(displayOption === 'dropdown' ? 'new' : 'existing');
|
|
25
|
+
const apiServices = useApiServices();
|
|
22
26
|
const linkExistingInstance = async () => {
|
|
23
27
|
if (selectedRow) {
|
|
24
28
|
setSelectedInstance(selectedRow);
|
|
@@ -30,19 +34,53 @@ const RelatedObjectInstance = (props) => {
|
|
|
30
34
|
handleClose();
|
|
31
35
|
setErrors([]);
|
|
32
36
|
};
|
|
37
|
+
const createNewInstance = async (submission) => {
|
|
38
|
+
if (!relatedObject) {
|
|
39
|
+
// Handle the case where relatedObject is undefined
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
submission = await formatSubmission(submission, apiServices, relatedObject.id);
|
|
43
|
+
try {
|
|
44
|
+
await apiServices
|
|
45
|
+
.post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
|
|
46
|
+
actionId: actionId,
|
|
47
|
+
input: submission,
|
|
48
|
+
})
|
|
49
|
+
.then((response) => {
|
|
50
|
+
handleChangeObjectField(id, response);
|
|
51
|
+
setSelectedInstance(response);
|
|
52
|
+
setSnackbarError({
|
|
53
|
+
showAlert: true,
|
|
54
|
+
message: 'New instance created',
|
|
55
|
+
isError: false,
|
|
56
|
+
});
|
|
57
|
+
setOptions(options.concat([response]));
|
|
58
|
+
onClose();
|
|
59
|
+
});
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
setSnackbarError({
|
|
64
|
+
showAlert: true,
|
|
65
|
+
message: err.response?.data?.error?.details?.[0]?.message ??
|
|
66
|
+
err.response?.data?.error?.message ??
|
|
67
|
+
`An error occurred. The new instance was not created.`,
|
|
68
|
+
isError: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
};
|
|
33
72
|
return (React.createElement(Box, { sx: {
|
|
34
73
|
background: nestedFieldsView ? '#F4F6F8' : 'none',
|
|
35
74
|
borderRadius: '8px',
|
|
36
75
|
} },
|
|
37
76
|
React.createElement(Box, { sx: {
|
|
38
|
-
padding: '8px 24px 0px',
|
|
39
77
|
'.MuiInputBase-root': { background: '#FFFF', borderRadius: '8px' },
|
|
40
78
|
} },
|
|
41
79
|
!nestedFieldsView &&
|
|
42
80
|
displayOption !== 'dropdown' &&
|
|
43
81
|
mode !== 'existingOnly' &&
|
|
44
82
|
mode !== 'newOnly' &&
|
|
45
|
-
|
|
83
|
+
actionId && (React.createElement(Grid, { container: true, sx: { paddingX: '24px' } },
|
|
46
84
|
React.createElement(Grid, { container: true, item: true },
|
|
47
85
|
React.createElement(RadioGroup, { row: true, "aria-labelledby": "related-object-link-type", onChange: (event) => {
|
|
48
86
|
event.target.value === 'existing' && setErrors([]);
|
|
@@ -57,9 +95,9 @@ const RelatedObjectInstance = (props) => {
|
|
|
57
95
|
"There are ",
|
|
58
96
|
React.createElement("strong", null, errors.length),
|
|
59
97
|
" errors")))) : undefined,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
React.createElement(InstanceLookup, { colspan: 12, nestedFieldsView: nestedFieldsView, setRelationType: setRelationType, object: relatedObject, setSelectedInstance: setSelectedRow, mode: mode, filter: filter, layout: layout }))))
|
|
98
|
+
relationType === 'new' || mode === 'newOnly' ? (React.createElement(Box, { sx: { width: '100%' } },
|
|
99
|
+
React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, stickyFooter: stickyFooter, objectId: fieldDefinition.objectId, onClose: onClose, onSubmit: createNewInstance, richTextEditor: richTextEditor }))) : ((mode === 'default' || mode === 'existingOnly') &&
|
|
100
|
+
relatedObject && (React.createElement(InstanceLookup, { colspan: 12, nestedFieldsView: nestedFieldsView, setRelationType: setRelationType, object: relatedObject, setSelectedInstance: setSelectedRow, mode: mode, filter: filter, layout: layout })))),
|
|
63
101
|
relationType !== 'new' && mode !== 'newOnly' && (React.createElement(Box, { sx: styles.actionButtons },
|
|
64
102
|
React.createElement(Button, { onClick: onClose, color: 'inherit', sx: {
|
|
65
103
|
border: '1px solid #ced4da',
|
|
@@ -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;
|
package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrorDisplay.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useResponsive } from '../../../../../theme';
|
|
3
|
-
import { useFormContext } from '../../../../../theme/hooks';
|
|
4
3
|
import { List, ListItem, Typography } from '../../../../core';
|
|
5
4
|
import { Box } from '../../../../layout';
|
|
6
5
|
function ValidationErrorDisplay(props) {
|
|
7
|
-
const { formId, title } = props;
|
|
8
|
-
const { errors, showSubmitError } = useFormContext();
|
|
6
|
+
const { formId, title, errors, showSubmitError } = props;
|
|
9
7
|
const { isSm, isXs } = useResponsive();
|
|
10
8
|
function extractErrorMessages(errors) {
|
|
11
9
|
const messages = [];
|
|
@@ -38,7 +38,7 @@ export type SimpleEditorProps = {
|
|
|
38
38
|
};
|
|
39
39
|
export type ObjectPropertyInputProps = {
|
|
40
40
|
id: string;
|
|
41
|
-
fieldDefinition: InputParameter;
|
|
41
|
+
fieldDefinition: InputParameter | Property;
|
|
42
42
|
mode: 'default' | 'existingOnly' | 'newOnly';
|
|
43
43
|
nestedFieldsView?: boolean;
|
|
44
44
|
readOnly?: boolean;
|
|
@@ -53,6 +53,8 @@ export type ObjectPropertyInputProps = {
|
|
|
53
53
|
initialValue?: ObjectInstance | null;
|
|
54
54
|
viewLayout?: ViewLayoutEntityReference;
|
|
55
55
|
hasDescription?: boolean;
|
|
56
|
+
createActionId?: string;
|
|
57
|
+
formId?: string;
|
|
56
58
|
};
|
|
57
59
|
export type Page = {
|
|
58
60
|
id: string;
|
|
@@ -85,6 +87,10 @@ export type ExpandedSection = Section & {
|
|
|
85
87
|
export type EntryRendererProps = BaseProps & {
|
|
86
88
|
entry: FormEntry;
|
|
87
89
|
isDocument?: boolean;
|
|
90
|
+
associatedObject?: {
|
|
91
|
+
instanceId?: string;
|
|
92
|
+
propertyId?: string;
|
|
93
|
+
};
|
|
88
94
|
};
|
|
89
95
|
export type SectionsProps = {
|
|
90
96
|
entry: Sections;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { Action, ApiServices, Column, Columns, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
|
|
2
3
|
import { LocalDateTime } from '@js-joda/core';
|
|
3
4
|
import { FieldErrors, FieldValues } from 'react-hook-form';
|
|
4
5
|
import { AutocompleteOption } from '../../../core';
|
|
5
|
-
import { Document, DocumentData } from './types';
|
|
6
|
+
import { Document, DocumentData, SavedDocumentReference } from './types';
|
|
6
7
|
export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
|
|
7
8
|
export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
|
|
8
9
|
export declare function isAddressProperty(key: string): boolean;
|
|
@@ -61,3 +62,27 @@ export declare function formatDataToDoc(data: DocumentData): {
|
|
|
61
62
|
export declare function getUnnestedEntries(entries: FormEntry[]): FormEntry[];
|
|
62
63
|
export declare const isEmptyWithDefault: (fieldValue: unknown, entry: InputParameterReference | InputField, instance: Record<string, unknown> | object) => boolean | "" | 0 | undefined;
|
|
63
64
|
export declare const docProperties: Property[];
|
|
65
|
+
export declare const uploadDocuments: (files: (File | SavedDocumentReference)[], metadata: Record<string, string>, apiServices: ApiServices, instanceId: string, objectId: string) => Promise<SavedDocumentReference[]>;
|
|
66
|
+
export declare const deleteDocuments: (submittedFields: FieldValues, requestSuccess: boolean, apiServices: ApiServices, object: Obj, instance: FieldValues, action?: Action, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
67
|
+
showAlert: boolean;
|
|
68
|
+
message?: string;
|
|
69
|
+
isError: boolean;
|
|
70
|
+
}>>) => Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Transforms a form submission into a format safe for API submission.
|
|
73
|
+
*
|
|
74
|
+
* Responsibilities:
|
|
75
|
+
* - Uploads any files found in submission fields.
|
|
76
|
+
* - Normalizes related objects (keeping only id and name not the whole instance).
|
|
77
|
+
* - Converts an object of undefined address fields to undefined instead of an empty object.
|
|
78
|
+
* - Normalizes LocalDateTime values to API-friendly format.
|
|
79
|
+
* - Converts empty strings or undefined values to null.
|
|
80
|
+
* - Optionally reports file upload errors via snackbar.
|
|
81
|
+
*
|
|
82
|
+
* Returns the cleaned submission ready for submitting.
|
|
83
|
+
*/
|
|
84
|
+
export declare function formatSubmission(submission: FieldValues, apiServices: ApiServices, objectId: string, instanceId?: string, setSnackbarError?: React.Dispatch<React.SetStateAction<{
|
|
85
|
+
showAlert: boolean;
|
|
86
|
+
message?: string;
|
|
87
|
+
isError: boolean;
|
|
88
|
+
}>>): Promise<FieldValues>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LocalDateTime } from '@js-joda/core';
|
|
2
2
|
import jsonLogic from 'json-logic-js';
|
|
3
|
-
import { get, isArray, isEmpty, isObject, omit, startCase, transform } from 'lodash';
|
|
3
|
+
import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } from 'lodash';
|
|
4
4
|
import { DateTime } from 'luxon';
|
|
5
5
|
import Handlebars from 'no-eval-handlebars';
|
|
6
6
|
import { defaultRuleProcessorMongoDB, formatQuery, parseMongoDB } from 'react-querybuilder';
|
|
@@ -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
|
+
"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-
|
|
93
|
+
"@evoke-platform/context": "^1.3.2-0",
|
|
94
94
|
"react": "^18.1.0",
|
|
95
95
|
"react-dom": "^18.1.0"
|
|
96
96
|
},
|