@evoke-platform/ui-components 1.10.0-dev.33 → 1.10.0-dev.34

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.
@@ -400,9 +400,31 @@ const CriteriaBuilder = (props) => {
400
400
  const fields = useMemo(() => {
401
401
  return properties
402
402
  .filter(({ type }) => type !== 'collection')
403
- .map((property) => {
403
+ .flatMap((property) => {
404
+ if (property.type === 'object') {
405
+ const result = [
406
+ {
407
+ name: `${property.id}.id`,
408
+ label: `${property.name} ID`,
409
+ inputType: property.type,
410
+ },
411
+ {
412
+ name: `${property.id}.name`,
413
+ label: `${property.name} Name`,
414
+ inputType: property.type,
415
+ },
416
+ ];
417
+ if (!property.objectId) {
418
+ result.push({
419
+ name: `${property.id}.objectId`,
420
+ label: `${property.name} Object ID`,
421
+ inputType: property.type,
422
+ });
423
+ }
424
+ return result;
425
+ }
404
426
  return {
405
- name: property.type === 'object' ? `${property.id}.id` : property.id,
427
+ name: property.id,
406
428
  label: property.name,
407
429
  inputType: property.type,
408
430
  ...(property.enum && {
@@ -56,6 +56,17 @@ const mockProperties = [
56
56
  name: 'Boolean',
57
57
  type: 'boolean',
58
58
  },
59
+ {
60
+ id: 'regularRelatedObject',
61
+ name: 'Regular Related Object',
62
+ type: 'object',
63
+ objectId: 'relatedObjectId',
64
+ },
65
+ {
66
+ id: 'dynamicRelatedObject',
67
+ name: 'Dynamic Related Object',
68
+ type: 'object',
69
+ },
59
70
  ];
60
71
  describe('CriteriaBuilder', () => {
61
72
  // Mock function for setCriteria
@@ -64,6 +75,40 @@ describe('CriteriaBuilder', () => {
64
75
  // Reset the mock before each test
65
76
  setCriteriaMock.mockReset();
66
77
  });
78
+ describe('when passed regular related object fields', () => {
79
+ it('should render the field ID', () => {
80
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
81
+ 'regularRelatedObject.id': 'relatedInstanceId',
82
+ }, setCriteria: setCriteriaMock }));
83
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Regular Related Object ID');
84
+ });
85
+ it('should render the field Name', () => {
86
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
87
+ 'regularRelatedObject.name': 'relatedInstanceName',
88
+ }, setCriteria: setCriteriaMock }));
89
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Regular Related Object Name');
90
+ });
91
+ });
92
+ describe('when passed dynamic related object fields', () => {
93
+ it('should render the field ID', () => {
94
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
95
+ 'dynamicRelatedObject.id': 'relatedInstanceId',
96
+ }, setCriteria: setCriteriaMock }));
97
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Dynamic Related Object ID');
98
+ });
99
+ it('should render the field Name', () => {
100
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
101
+ 'dynamicRelatedObject.name': 'relatedInstanceName',
102
+ }, setCriteria: setCriteriaMock }));
103
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Dynamic Related Object Name');
104
+ });
105
+ it('should render the field Object ID', () => {
106
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
107
+ 'dynamicRelatedObject.objectId': 'relatedInstanceObjectId',
108
+ }, setCriteria: setCriteriaMock }));
109
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Dynamic Related Object Object ID');
110
+ });
111
+ });
67
112
  describe('when passed single-select fields', () => {
68
113
  it('should render the field name', () => {
69
114
  render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
@@ -1,6 +1,6 @@
1
1
  import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
2
2
  import axios from 'axios';
3
- import { cloneDeep, get, isArray, isEmpty, isEqual, merge, omit, pick, set, uniq } from 'lodash';
3
+ import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set, uniq } from 'lodash';
4
4
  import React, { useEffect, useRef, useState } from 'react';
5
5
  import { Skeleton, Snackbar } from '../../core';
6
6
  import { Box } from '../../layout';
@@ -278,100 +278,83 @@ function FormRendererContainer(props) {
278
278
  };
279
279
  const getDefaultValues = async (entries, instanceData) => {
280
280
  const result = {};
281
- const processEntries = async (entries) => {
282
- if (!entries)
283
- return;
284
- for (const entry of entries) {
285
- if (entry.type === 'sections' || entry.type === 'columns') {
286
- const subEntries = entry.type === 'sections' ? entry.sections : entry.columns;
287
- for (const subEntry of subEntries) {
288
- if (subEntry.entries) {
289
- const nested = await getDefaultValues(subEntry.entries, instanceData);
290
- merge(result, nested);
291
- }
281
+ const unnestedEntries = getUnnestedEntries(entries);
282
+ for (const entry of unnestedEntries) {
283
+ if ((entry.type === 'input' || entry.type === 'inputField') &&
284
+ isAddressProperty(entry.parameterId || entry.input?.id)) {
285
+ const fieldId = getEntryId(entry);
286
+ if (!fieldId)
287
+ continue;
288
+ const fieldValue = get(instanceData, fieldId);
289
+ if ((isEmpty(instanceData) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
290
+ entry?.display?.defaultValue &&
291
+ parameters) {
292
+ const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
293
+ if (isArray(defaultValuesArray)) {
294
+ defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
295
+ set(result, fieldId, fieldValue);
296
+ });
292
297
  }
293
298
  }
294
- if ((entry.type === 'input' || entry.type === 'inputField') &&
295
- isAddressProperty(entry.parameterId || entry.input?.id)) {
296
- const fieldId = getEntryId(entry);
297
- if (!fieldId)
298
- return;
299
- const fieldValue = get(instanceData, fieldId);
300
- if ((isEmpty(instanceData) ||
301
- fieldValue === undefined ||
302
- fieldValue === null ||
303
- fieldValue === '') &&
304
- entry?.display?.defaultValue &&
305
- parameters) {
306
- const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
307
- if (isArray(defaultValuesArray)) {
308
- defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
309
- set(result, fieldId, fieldValue);
310
- });
311
- }
312
- }
313
- else if (fieldValue !== undefined && fieldValue !== null) {
314
- set(result, fieldId, fieldValue);
315
- }
299
+ else if (fieldValue !== undefined && fieldValue !== null) {
300
+ set(result, fieldId, fieldValue);
316
301
  }
317
- else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
318
- const fieldId = entry.type === 'input'
319
- ? entry.parameterId
320
- : entry.type === 'inputField'
321
- ? entry.input?.id
322
- : undefined;
323
- if (fieldId) {
324
- const fieldValue = instanceData?.[fieldId] ??
325
- instanceData?.metadata?.[fieldId];
326
- const parameter = parameters?.find((param) => param.id === fieldId);
327
- if (associatedObject?.propertyId === fieldId &&
328
- associatedObject?.instanceId &&
329
- parameter &&
330
- action?.type === 'create') {
331
- try {
332
- const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
333
- result[associatedObject.propertyId] = instance;
334
- }
335
- catch (error) {
336
- console.error(error);
337
- }
302
+ }
303
+ else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
304
+ const fieldId = entry.type === 'input'
305
+ ? entry.parameterId
306
+ : entry.type === 'inputField'
307
+ ? entry.input?.id
308
+ : undefined;
309
+ if (fieldId) {
310
+ const fieldValue = instanceData?.[fieldId] ??
311
+ instanceData?.metadata?.[fieldId];
312
+ const parameter = parameters?.find((param) => param.id === fieldId);
313
+ if (associatedObject?.propertyId === fieldId &&
314
+ associatedObject?.instanceId &&
315
+ parameter &&
316
+ action?.type === 'create') {
317
+ try {
318
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
319
+ result[associatedObject.propertyId] = instance;
338
320
  }
339
- else if (entry.type !== 'readonlyField' &&
340
- isEmptyWithDefault(fieldValue, entry, instanceData)) {
341
- if (fieldId && parameters && parameters.length > 0) {
342
- const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
343
- for (const { fieldId, fieldValue } of defaultValuesArray) {
344
- const parameter = parameters?.find((param) => param.id === fieldId);
345
- if (parameter?.type === 'object') {
346
- const dependentFields = await processValueUpdate(form?.entries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
347
- for (const field of dependentFields) {
348
- set(result, field.fieldId, field.fieldValue);
349
- }
321
+ catch (error) {
322
+ console.error(error);
323
+ }
324
+ }
325
+ else if (entry.type !== 'readonlyField' && isEmptyWithDefault(fieldValue, entry, instanceData)) {
326
+ if (fieldId && parameters && parameters.length > 0) {
327
+ const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
328
+ for (const { fieldId, fieldValue } of defaultValuesArray) {
329
+ const parameter = parameters?.find((param) => param.id === fieldId);
330
+ if (parameter?.type === 'object') {
331
+ const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
332
+ for (const field of dependentFields) {
333
+ set(result, field.fieldId, field.fieldValue);
350
334
  }
351
- set(result, fieldId, fieldValue);
352
335
  }
336
+ set(result, fieldId, fieldValue);
353
337
  }
354
338
  }
355
- else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
356
- result[fieldId] = false;
357
- }
358
- else if (fieldValue !== undefined && fieldValue !== null) {
359
- if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
360
- let RTFFieldValue = fieldValue;
361
- if (!fieldValue.trim().startsWith('{\\rtf')) {
362
- RTFFieldValue = plainTextToRtf(fieldValue);
363
- }
364
- result[fieldId] = RTFFieldValue;
365
- }
366
- else {
367
- result[fieldId] = fieldValue;
339
+ }
340
+ else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
341
+ result[fieldId] = false;
342
+ }
343
+ else if (fieldValue !== undefined && fieldValue !== null) {
344
+ if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
345
+ let RTFFieldValue = fieldValue;
346
+ if (!fieldValue.trim().startsWith('{\\rtf')) {
347
+ RTFFieldValue = plainTextToRtf(fieldValue);
368
348
  }
349
+ result[fieldId] = RTFFieldValue;
350
+ }
351
+ else {
352
+ result[fieldId] = fieldValue;
369
353
  }
370
354
  }
371
355
  }
372
356
  }
373
- };
374
- await processEntries(entries);
357
+ }
375
358
  return result;
376
359
  };
377
360
  const handleAutosave = async (fieldId) => {
@@ -423,7 +406,7 @@ function FormRendererContainer(props) {
423
406
  if (parameter) {
424
407
  if (parameter.type === 'object' && parameters && parameters.length > 0) {
425
408
  // On change of a related object, update default values dependent on that object
426
- const dependentFields = await processValueUpdate(form?.entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
409
+ const dependentFields = await processValueUpdate(entries, parameters, value, apiServices, id, formDataRef.current, userAccount);
427
410
  for (const field of dependentFields) {
428
411
  onChange(field.fieldId, field.fieldValue);
429
412
  }
@@ -1,10 +1,10 @@
1
1
  import { ApiServices, FormEntry, InputField, InputParameter, InputParameterReference, ObjectInstance, Reference, UserAccount } from '@evoke-platform/context';
2
2
  import { FieldValues } from 'react-hook-form';
3
- export declare function evalDefaultVals(parameters: InputParameter[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
3
+ export declare function evalDefaultVals(parameters: InputParameter[], unnestedEntries: FormEntry[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference): Promise<{
4
4
  fieldId: string;
5
5
  fieldValue: unknown;
6
6
  }[]>;
7
- export declare function processValueUpdate(entries: FormEntry[] | undefined, parameters: InputParameter[], updatedRelatedObjectValue: ObjectInstance | null | Reference, apiServices: ApiServices, changedEntryId?: string, formValues?: FieldValues, userAccount?: UserAccount): Promise<{
7
+ export declare function processValueUpdate(unnestedEntries: FormEntry[], parameters: InputParameter[], updatedRelatedObjectValue: ObjectInstance | null | Reference, apiServices: ApiServices, changedEntryId?: string, formValues?: FieldValues, userAccount?: UserAccount): Promise<{
8
8
  fieldId: string;
9
9
  fieldValue: unknown;
10
10
  }[]>;
@@ -1,7 +1,7 @@
1
1
  import { isArray, isEmpty, uniq } from 'lodash';
2
2
  import { DateTime } from 'luxon';
3
3
  import { getEntryId, getPrefixedUrl, isAddressProperty } from './utils';
4
- export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, formValues, updatedRelatedObjectValue) {
4
+ export async function evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, formValues, updatedRelatedObjectValue) {
5
5
  const updates = [];
6
6
  const parameter = parameters.find((param) => param.id === fieldId);
7
7
  const defaultValue = entry.display?.defaultValue;
@@ -15,9 +15,9 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
15
15
  const groups = regex.exec(item)?.groups;
16
16
  if (groups?.relatedObjectProperty && groups?.nestedProperty) {
17
17
  const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
18
- let relatedObject = updatedRelatedObjectValue;
19
- if (!relatedObject && !isEmpty(formValues)) {
20
- relatedObject = formValues[groups.relatedObjectProperty];
18
+ let relatedObjectInstance = updatedRelatedObjectValue;
19
+ if (!relatedObjectInstance && !isEmpty(formValues)) {
20
+ relatedObjectInstance = formValues[groups.relatedObjectProperty];
21
21
  }
22
22
  if (updatedRelatedObjectValue?.[groups.nestedProperty]) {
23
23
  fieldValue = uniq([
@@ -28,9 +28,14 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
28
28
  ]);
29
29
  updates.push({ fieldId, fieldValue });
30
30
  }
31
- else if (relatedObject && relatedObject.id && relatedObjectParameter) {
31
+ else if (relatedObjectInstance?.id && relatedObjectParameter) {
32
+ let relatedObjectId = relatedObjectParameter.objectId;
33
+ if (!relatedObjectId) {
34
+ const relatedObjectParamEntry = unnestedEntries.find((e) => getEntryId(e) === relatedObjectParameter.id);
35
+ relatedObjectId = relatedObjectParamEntry?.display?.relatedObjectId;
36
+ }
32
37
  const instance = await new Promise((resolve) => {
33
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectParameter.objectId}/instances/${relatedObject?.id}`), (error, instance) => {
38
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
34
39
  if (error) {
35
40
  console.error(error);
36
41
  return resolve(undefined);
@@ -64,17 +69,22 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
64
69
  const groups = regex.exec(defaultValue)?.groups;
65
70
  if (groups?.relatedObjectProperty && groups?.addressProperty && groups?.nestedAddressProperty) {
66
71
  const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
67
- let relatedObject = updatedRelatedObjectValue;
68
- if (!relatedObject && !isEmpty(formValues)) {
69
- relatedObject = formValues[groups.relatedObjectProperty];
72
+ let relatedObjectInstance = updatedRelatedObjectValue;
73
+ if (!relatedObjectInstance && !isEmpty(formValues)) {
74
+ relatedObjectInstance = formValues[groups.relatedObjectProperty];
70
75
  }
71
76
  if (updatedRelatedObjectValue?.[groups.addressProperty]?.[groups.nestedAddressProperty]) {
72
77
  fieldValue = updatedRelatedObjectValue?.[groups.addressProperty]?.[groups.nestedAddressProperty];
73
78
  updates.push({ fieldId, fieldValue });
74
79
  }
75
- else if (relatedObject && relatedObject.id && relatedObjectParameter) {
80
+ else if (relatedObjectInstance?.id && relatedObjectParameter) {
81
+ let relatedObjectId = relatedObjectParameter.objectId;
82
+ if (!relatedObjectId) {
83
+ const relatedObjectParamEntry = unnestedEntries.find((e) => getEntryId(e) === relatedObjectParameter.id);
84
+ relatedObjectId = relatedObjectParamEntry?.display?.relatedObjectId;
85
+ }
76
86
  const instance = await new Promise((resolve) => {
77
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectParameter.objectId}/instances/${relatedObject?.id}`), (error, instance) => {
87
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
78
88
  if (error) {
79
89
  console.error(error);
80
90
  return resolve(undefined);
@@ -99,17 +109,22 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
99
109
  const groups = regex.exec(defaultValue)?.groups;
100
110
  if (groups?.relatedObjectProperty && groups?.nestedProperty) {
101
111
  const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
102
- let relatedObject = updatedRelatedObjectValue;
103
- if (!relatedObject && !isEmpty(formValues)) {
104
- relatedObject = formValues[groups.relatedObjectProperty];
112
+ let relatedObjectInstance = updatedRelatedObjectValue;
113
+ if (!relatedObjectInstance && !isEmpty(formValues)) {
114
+ relatedObjectInstance = formValues[groups.relatedObjectProperty];
105
115
  }
106
116
  if (updatedRelatedObjectValue?.[groups.nestedProperty]) {
107
117
  fieldValue = updatedRelatedObjectValue[groups.nestedProperty];
108
118
  updates.push({ fieldId, fieldValue });
109
119
  }
110
- else if (relatedObject?.id && relatedObjectParameter) {
120
+ else if (relatedObjectInstance?.id && relatedObjectParameter) {
121
+ let relatedObjectId = relatedObjectParameter.objectId;
122
+ if (!relatedObjectId) {
123
+ const relatedObjectParamEntry = unnestedEntries.find((e) => getEntryId(e) === relatedObjectParameter.id);
124
+ relatedObjectId = relatedObjectParamEntry?.display?.relatedObjectId;
125
+ }
111
126
  const instance = await new Promise((resolve) => {
112
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectParameter.objectId}/instances/${relatedObject?.id}`), (error, instance) => {
127
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
113
128
  if (error) {
114
129
  console.error(error);
115
130
  return resolve(undefined);
@@ -153,16 +168,9 @@ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, ap
153
168
  }
154
169
  return updates;
155
170
  }
156
- export async function processValueUpdate(entries, parameters, updatedRelatedObjectValue, apiServices, changedEntryId, formValues, userAccount) {
171
+ export async function processValueUpdate(unnestedEntries, parameters, updatedRelatedObjectValue, apiServices, changedEntryId, formValues, userAccount) {
157
172
  const updates = [];
158
- for (const entry of entries || []) {
159
- if (entry.type === 'sections' || entry.type === 'columns') {
160
- const subEntries = entry.type === 'sections' ? entry.sections : entry.columns;
161
- for (const subEntry of subEntries) {
162
- const subUpdates = await processValueUpdate(subEntry.entries, parameters, updatedRelatedObjectValue, apiServices, changedEntryId, formValues, userAccount);
163
- updates.push(...subUpdates);
164
- }
165
- }
173
+ for (const entry of unnestedEntries) {
166
174
  if ((entry.type === 'input' || entry.type === 'inputField') && entry?.display?.defaultValue) {
167
175
  const parameterId = getEntryId(entry);
168
176
  if (!parameterId)
@@ -175,7 +183,7 @@ export async function processValueUpdate(entries, parameters, updatedRelatedObje
175
183
  groups?.addressProperty &&
176
184
  groups?.nestedAddressProperty &&
177
185
  changedEntryId === groups.relatedObjectProperty) {
178
- const result = await evalDefaultVals(parameters, entry, formValues?.[addressObject]?.[addressField], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
186
+ const result = await evalDefaultVals(parameters, unnestedEntries, entry, formValues?.[addressObject]?.[addressField], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
179
187
  updates.push(...result);
180
188
  }
181
189
  }
@@ -187,7 +195,7 @@ export async function processValueUpdate(entries, parameters, updatedRelatedObje
187
195
  if (groups?.relatedObjectProperty &&
188
196
  groups?.nestedProperty &&
189
197
  changedEntryId === groups.relatedObjectProperty) {
190
- const result = await evalDefaultVals(parameters, entry, entry.display.defaultValue, parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
198
+ const result = await evalDefaultVals(parameters, unnestedEntries, entry, entry.display.defaultValue, parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
191
199
  updates.push(...result);
192
200
  }
193
201
  }
@@ -198,7 +206,7 @@ export async function processValueUpdate(entries, parameters, updatedRelatedObje
198
206
  if (groups?.relatedObjectProperty &&
199
207
  groups?.nestedProperty &&
200
208
  changedEntryId === groups.relatedObjectProperty) {
201
- const result = await evalDefaultVals(parameters, entry, formValues?.[parameterId], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
209
+ const result = await evalDefaultVals(parameters, unnestedEntries, entry, formValues?.[parameterId], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
202
210
  updates.push(...result);
203
211
  }
204
212
  }
@@ -10,7 +10,7 @@ import { Box } from '../../../../../layout';
10
10
  import { getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
11
11
  import RelatedObjectInstance from './RelatedObjectInstance';
12
12
  const ObjectPropertyInput = (props) => {
13
- const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, } = props;
13
+ const { id, fieldDefinition, readOnly, error, mode, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, viewLayout, hasDescription, createActionId, formId, relatedObjectId, } = props;
14
14
  const { fetchedOptions, setFetchedOptions, parameters, fieldHeight, handleChange: handleChangeObjectField, onAutosave: onAutosave, instance, } = useFormContext();
15
15
  const { defaultPages, findDefaultPageSlugFor } = useApp();
16
16
  const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
@@ -88,7 +88,7 @@ const ObjectPropertyInput = (props) => {
88
88
  });
89
89
  if (updatedFilter.where) {
90
90
  setLoadingOptions(true);
91
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
91
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), async (error, instances) => {
92
92
  if (error) {
93
93
  console.error(error);
94
94
  setLoadingOptions(false);
@@ -114,7 +114,7 @@ const ObjectPropertyInput = (props) => {
114
114
  });
115
115
  }
116
116
  }
117
- }, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
117
+ }, [relatedObjectId, defaultValueCriteria, sortBy, orderBy]);
118
118
  const getDropdownOptions = useCallback(() => {
119
119
  if (((!fetchedOptions?.[`${id}Options`] ||
120
120
  (fetchedOptions?.[`${id}Options`]).length === 0) &&
@@ -129,7 +129,7 @@ const ObjectPropertyInput = (props) => {
129
129
  direction: 'asc',
130
130
  };
131
131
  updatedFilter.order = `${propertyId} ${direction}`;
132
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
132
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
133
133
  if (error) {
134
134
  console.error(error);
135
135
  setLoadingOptions(false);
@@ -143,7 +143,7 @@ const ObjectPropertyInput = (props) => {
143
143
  });
144
144
  }
145
145
  }, [
146
- fieldDefinition,
146
+ relatedObjectId,
147
147
  updatedCriteria,
148
148
  layout,
149
149
  fetchedOptions?.[`${id}Options`],
@@ -183,10 +183,10 @@ const ObjectPropertyInput = (props) => {
183
183
  }
184
184
  };
185
185
  fetchForm();
186
- }, [action, formId, id, fieldDefinition.objectId, apiServices, fetchedOptions]);
186
+ }, [action, formId, id, apiServices, fetchedOptions]);
187
187
  useEffect(() => {
188
188
  if (!fetchedOptions[`${id}RelatedObject`]) {
189
- apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
189
+ apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/effective?sanitizedVersion=true`), (error, object) => {
190
190
  if (error) {
191
191
  console.error(error);
192
192
  }
@@ -195,15 +195,15 @@ const ObjectPropertyInput = (props) => {
195
195
  }
196
196
  });
197
197
  }
198
- }, [fieldDefinition.objectId, fetchedOptions, id]);
198
+ }, [relatedObjectId, fetchedOptions, id]);
199
199
  useEffect(() => {
200
200
  (async () => {
201
201
  if (parameters && fetchedOptions[`${id}NavigationSlug`] === undefined) {
202
202
  const pages = await getDefaultPages(parameters, defaultPages, findDefaultPageSlugFor);
203
- if (fieldDefinition.objectId && pages[fieldDefinition.objectId]) {
204
- setNavigationSlug(pages[fieldDefinition.objectId]);
203
+ if (relatedObjectId && pages[relatedObjectId]) {
204
+ setNavigationSlug(pages[relatedObjectId]);
205
205
  setFetchedOptions({
206
- [`${id}NavigationSlug`]: pages[fieldDefinition.objectId],
206
+ [`${id}NavigationSlug`]: pages[relatedObjectId],
207
207
  });
208
208
  }
209
209
  else {
@@ -214,7 +214,7 @@ const ObjectPropertyInput = (props) => {
214
214
  }
215
215
  }
216
216
  })();
217
- }, [parameters, defaultPages, findDefaultPageSlugFor, fieldDefinition, fetchedOptions]);
217
+ }, [parameters, defaultPages, findDefaultPageSlugFor, relatedObjectId, fetchedOptions]);
218
218
  const handleClose = () => {
219
219
  setOpenCreateDialog(false);
220
220
  };
@@ -522,7 +522,7 @@ const ObjectPropertyInput = (props) => {
522
522
  event.stopPropagation();
523
523
  setOpenCreateDialog(true);
524
524
  }, "aria-label": `Add` }, "Add")))),
525
- React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError, fieldDefinition: fieldDefinition }),
525
+ React.createElement(RelatedObjectInstance, { open: openCreateDialog, title: form?.name ?? `Add ${fieldDefinition.name}`, handleClose: handleClose, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, formId: formId ?? form?.id, actionId: createActionId, setSnackbarError: setSnackbarError }),
526
526
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
527
527
  isError: snackbarError.isError,
528
528
  showAlert: false,
@@ -1,11 +1,11 @@
1
- import { InputParameter, Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
1
+ import { 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
5
  id: string;
6
6
  open: boolean;
7
7
  title: string;
8
- relatedObject: Obj | undefined;
8
+ relatedObject?: Obj;
9
9
  setSelectedInstance: (selectedInstance: ObjectInstance) => void;
10
10
  handleClose: () => void;
11
11
  mode: 'default' | 'existingOnly' | 'newOnly';
@@ -21,7 +21,6 @@ export type RelatedObjectInstanceProps = BaseProps & {
21
21
  layout?: TableViewLayout;
22
22
  formId?: string;
23
23
  actionId?: string;
24
- fieldDefinition: InputParameter;
25
24
  };
26
25
  declare const RelatedObjectInstance: (props: RelatedObjectInstanceProps) => React.JSX.Element;
27
26
  export default RelatedObjectInstance;
@@ -6,6 +6,7 @@ import { Close } from '../../../../../../icons';
6
6
  import useWidgetSize, { useFormContext } from '../../../../../../theme/hooks';
7
7
  import { Button, Dialog, DialogContent, DialogTitle, FormControlLabel, IconButton, Radio, RadioGroup, } from '../../../../../core';
8
8
  import Box from '../../../../../layout/Box/Box';
9
+ import ErrorComponent from '../../../../ErrorComponent';
9
10
  import FormRenderer from '../../../FormRenderer';
10
11
  import FormRendererContainer from '../../../FormRendererContainer';
11
12
  import Body from '../../Body';
@@ -28,7 +29,7 @@ const styles = {
28
29
  },
29
30
  };
30
31
  const RelatedObjectInstance = (props) => {
31
- const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, fieldDefinition, setSnackbarError, setOptions, options, } = props;
32
+ const { relatedObject, open, title, id, setSelectedInstance, handleClose, mode, displayOption, filter, layout, formId, actionId, setSnackbarError, setOptions, options, } = props;
32
33
  const { handleChange: handleChangeObjectField, onAutosave, richTextEditor, fieldHeight, width } = useFormContext();
33
34
  const [selectedRow, setSelectedRow] = useState();
34
35
  const [relationType, setRelationType] = useState(displayOption === 'dropdown' || mode === 'newOnly' ? 'new' : 'existing');
@@ -120,7 +121,7 @@ const RelatedObjectInstance = (props) => {
120
121
  }, value: relationType },
121
122
  React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
122
123
  React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))) : null;
123
- const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: fieldDefinition.objectId, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
124
+ const DialogForm = useCallback(() => (React.createElement(FormRendererContainer, { formId: formId, display: { fieldHeight: fieldHeight ?? 'medium' }, actionId: actionId, objectId: relatedObject.id, onSubmit: createNewInstance, onDiscardChanges: onClose, onSubmitError: handleSubmitError, richTextEditor: richTextEditor, renderHeader: () => null, renderBody: (bodyProps) => (React.createElement(DialogContent, { sx: styles.dialogContent },
124
125
  relationType === 'new' ? (React.createElement("div", { ref: validationErrorsRef }, !isEmpty(bodyProps.errors) && bodyProps.shouldShowValidationErrors ? (React.createElement(FormRenderer.ValidationErrors, { errors: bodyProps.errors, sx: {
125
126
  my: isSm || isXs ? 2 : 3,
126
127
  } })) : null)) : null,
@@ -136,7 +137,7 @@ const RelatedObjectInstance = (props) => {
136
137
  React.createElement(RadioButtons, null),
137
138
  defaultContainer)),
138
139
  status === 'ready' && defaultContainer));
139
- }, sx: { border: 'none' } })), [formId, actionId, fieldDefinition, fieldHeight, richTextEditor, RadioButtons]);
140
+ }, sx: { border: 'none' } })), [formId, actionId, relatedObject, fieldHeight, richTextEditor, RadioButtons]);
140
141
  return (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: open, onClose: (e, reason) => reason !== 'backdropClick' && handleClose(), sx: {
141
142
  background: 'none',
142
143
  }, PaperProps: {
@@ -159,7 +160,7 @@ const RelatedObjectInstance = (props) => {
159
160
  title,
160
161
  React.createElement(IconButton, { onClick: onClose, "aria-label": "Close" },
161
162
  React.createElement(Close, { fontSize: "small" })))),
162
- relationType === 'new' ? (React.createElement(DialogForm, null)) : ((mode === 'default' || mode === 'existingOnly') &&
163
+ relationType === 'new' ? (relatedObject ? (React.createElement(DialogForm, null)) : (React.createElement(ErrorComponent, { code: "Misconfigured" }))) : ((mode === 'default' || mode === 'existingOnly') &&
163
164
  relatedObject && (React.createElement(React.Fragment, null,
164
165
  React.createElement(DialogContent, { sx: styles.dialogContent },
165
166
  shouldShowRadioButtons && React.createElement(RadioButtons, null),
@@ -85,7 +85,7 @@ export function RecursiveEntryRenderer(props) {
85
85
  }
86
86
  else if (fieldDefinition.type === 'object') {
87
87
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
88
- React.createElement(ObjectPropertyInput, { fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: validation?.criteria
88
+ React.createElement(ObjectPropertyInput, { relatedObjectId: !fieldDefinition.objectId ? display?.relatedObjectId : fieldDefinition.objectId, fieldDefinition: fieldDefinition, id: entryId, mode: display?.mode || 'default', error: !!errors?.[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, readOnly: entry.type === 'readonlyField', filter: validation?.criteria
89
89
  ? updateCriteriaInputs(validation.criteria, getValues(), userAccount)
90
90
  : undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
91
91
  ? display?.defaultValue.sortBy
@@ -54,6 +54,7 @@ export type ObjectPropertyInputProps = {
54
54
  hasDescription?: boolean;
55
55
  createActionId?: string;
56
56
  formId?: string;
57
+ relatedObjectId?: string;
57
58
  };
58
59
  export type Page = {
59
60
  id: string;
@@ -622,14 +622,14 @@ export const deleteDocuments = async (submittedFields, requestSuccess, apiServic
622
622
  * Returns the cleaned submission ready for submitting.
623
623
  */
624
624
  export async function formatSubmission(submission, apiServices, objectId, instanceId, form, setSnackbarError) {
625
+ const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
625
626
  for (const [key, value] of Object.entries(submission)) {
627
+ const entry = allEntries?.find((entry) => getEntryId(entry) === key);
626
628
  if (isArray(value)) {
627
629
  // Only upload if array contains File instances (not SavedDocumentReference)
628
630
  const fileInArray = value.some((item) => item instanceof File);
629
631
  if (fileInArray && instanceId && apiServices && objectId) {
630
632
  try {
631
- const allEntries = getUnnestedEntries(form?.entries ?? []) ?? [];
632
- const entry = allEntries?.find((entry) => getEntryId(entry) === key);
633
633
  const uploadedDocuments = await uploadDocuments(value, {
634
634
  type: '',
635
635
  view_permission: '',
@@ -654,10 +654,16 @@ export async function formatSubmission(submission, apiServices, objectId, instan
654
654
  else if (typeof value === 'object' && value !== null) {
655
655
  if (Object.values(value).every((v) => v === undefined)) {
656
656
  submission[key] = undefined;
657
- // only submit the name and id of a related object
657
+ // only submit the name and id of a regular related object
658
+ // and include objectId if it is a dynamic related object
658
659
  }
659
660
  else if ('id' in value && 'name' in value) {
660
- submission[key] = pick(value, 'id', 'name');
661
+ submission[key] =
662
+ entry &&
663
+ ['input', 'inputField'].includes(entry.type) &&
664
+ entry.display?.relatedObjectId
665
+ ? pick(value, 'id', 'name', 'objectId')
666
+ : pick(value, 'id', 'name');
661
667
  }
662
668
  else if (value instanceof LocalDateTime) {
663
669
  submission[key] = normalizeDateTime(value);
@@ -363,7 +363,7 @@ describe('FormRenderer', () => {
363
363
  });
364
364
  });
365
365
  });
366
- describe('when passed a related object entry', () => {
366
+ describe('when passed a regular related object entry', () => {
367
367
  const setupTestMocks = (object, form, instances) => {
368
368
  server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
369
369
  };
@@ -1127,6 +1127,132 @@ describe('FormRenderer', () => {
1127
1127
  });
1128
1128
  });
1129
1129
  });
1130
+ describe('when passed a dynamic related object entry', () => {
1131
+ const setupTestMocks = (object, form, instances) => {
1132
+ server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])));
1133
+ };
1134
+ const form = {
1135
+ id: 'relatedObjectTestForm',
1136
+ name: 'Related Object Test Form',
1137
+ entries: [
1138
+ {
1139
+ type: 'input',
1140
+ parameterId: 'specialtyType',
1141
+ display: {
1142
+ label: 'Speciality Type',
1143
+ createActionId: '_create',
1144
+ relatedObjectId: 'specialtyType',
1145
+ },
1146
+ },
1147
+ ],
1148
+ actionId: '_update',
1149
+ objectId: 'relatedObjectTestForm',
1150
+ };
1151
+ beforeEach(async () => {
1152
+ const relatedObjectTestFormObject = {
1153
+ id: 'relatedObjectTestForm',
1154
+ name: 'Related Object Test Form',
1155
+ actions: [
1156
+ {
1157
+ id: '_update',
1158
+ name: 'Update',
1159
+ type: 'update',
1160
+ parameters: [
1161
+ {
1162
+ id: 'specialtyType',
1163
+ name: 'Related Object',
1164
+ type: 'object',
1165
+ },
1166
+ ],
1167
+ outputEvent: 'updated',
1168
+ },
1169
+ ],
1170
+ properties: [
1171
+ {
1172
+ id: 'specialtyType',
1173
+ name: 'Related Object',
1174
+ type: 'object',
1175
+ },
1176
+ ],
1177
+ };
1178
+ setupTestMocks(relatedObjectTestFormObject, form);
1179
+ const specialtyTypeForm = {
1180
+ id: 'specialtyTypeForm',
1181
+ name: 'Specialty Type Form',
1182
+ entries: [
1183
+ {
1184
+ type: 'content',
1185
+ html: '<div>Specialty Type Form Content</div>',
1186
+ },
1187
+ {
1188
+ type: 'input',
1189
+ parameterId: 'requiredField',
1190
+ display: {
1191
+ label: 'Required Field',
1192
+ required: true,
1193
+ },
1194
+ },
1195
+ ],
1196
+ actionId: '_create',
1197
+ objectId: 'specialtyType',
1198
+ display: {
1199
+ submitLabel: 'Create Specialty Type',
1200
+ },
1201
+ };
1202
+ const specialtyTypeObject = {
1203
+ id: 'specialtyType',
1204
+ name: 'Specialty Type',
1205
+ actions: [
1206
+ {
1207
+ id: '_create',
1208
+ name: 'Create',
1209
+ type: 'create',
1210
+ parameters: [
1211
+ {
1212
+ id: 'requiredField',
1213
+ name: 'Required Field',
1214
+ type: 'string',
1215
+ required: true,
1216
+ },
1217
+ ],
1218
+ outputEvent: 'created',
1219
+ defaultFormId: 'specialtyTypeForm',
1220
+ },
1221
+ {
1222
+ id: '_update',
1223
+ name: 'Update',
1224
+ type: 'update',
1225
+ parameters: [],
1226
+ outputEvent: 'updated',
1227
+ },
1228
+ ],
1229
+ properties: [],
1230
+ };
1231
+ setupTestMocks(specialtyTypeObject, specialtyTypeForm);
1232
+ });
1233
+ it('displays form if user switches to creating a new record', async () => {
1234
+ const user = userEvent.setup();
1235
+ render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1236
+ await user.click(await screen.findByRole('button', { name: 'Add' }));
1237
+ await screen.findByRole('radiogroup', { name: 'Relation Type' });
1238
+ const existingRecordButton = await screen.findByRole('radio', { name: /existing/i });
1239
+ expect(existingRecordButton).toBeChecked();
1240
+ const newRecordButton = await screen.findByRole('radio', { name: /new/i });
1241
+ await user.click(newRecordButton);
1242
+ await screen.findByText('Specialty Type Form Content');
1243
+ });
1244
+ it('displays a not found error in record creation mode if a form could not be found', async () => {
1245
+ const user = userEvent.setup();
1246
+ server.use(http.get('/api/data/forms/specialtyTypeForm', () => {
1247
+ return HttpResponse.json({ error: 'Not found' }, { status: 404 });
1248
+ }));
1249
+ render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1250
+ await user.click(await screen.findByRole('button', { name: 'Add' }));
1251
+ const newRecordButton = await screen.findByRole('radio', { name: /new/i });
1252
+ await user.click(newRecordButton);
1253
+ await screen.findByText(/not found/i);
1254
+ });
1255
+ });
1130
1256
  describe('when passed a one-to-many collection entry', () => {
1131
1257
  const setupTestMocks = (object, form, instances) => {
1132
1258
  server.use(http.get(`/api/data/objects/${object.id}/effective`, () => HttpResponse.json(object)), http.get(`/api/data/forms/${form.id}`, () => HttpResponse.json(form)), http.get(`/api/data/forms?filter={"where":{"actionId":"${form.actionId}","objectId":"${object.id}"}}`, () => HttpResponse.json([form])), http.get(`/api/data/objects/${object.id}/instances`, () => HttpResponse.json(instances || [])), http.get(`/api/data/objects/${object.id}/instances/checkAccess`, () => HttpResponse.json({
@@ -57,6 +57,12 @@ const defaultProperties = [
57
57
  required: false,
58
58
  objectId: 'application',
59
59
  },
60
+ {
61
+ id: 'dynamicObject',
62
+ name: 'Dynamic Object',
63
+ type: 'object',
64
+ required: false,
65
+ },
60
66
  {
61
67
  id: 'applicantName',
62
68
  name: 'Application Info',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.10.0-dev.33",
3
+ "version": "1.10.0-dev.34",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",