@evoke-platform/ui-components 1.15.0 → 1.15.1

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.
@@ -390,7 +390,10 @@ const CriteriaBuilder = (props) => {
390
390
  return defaultRuleProcessorMongoDB(newRule, options);
391
391
  },
392
392
  }));
393
- if (!isEmpty(difference(newCriteria, { $and: [{ $expr: true }] }))) {
393
+ if (isEmpty(difference(newCriteria, criteria))) {
394
+ return;
395
+ }
396
+ else if (!isEmpty(difference(newCriteria, { $and: [{ $expr: true }] }))) {
394
397
  setCriteria(newCriteria);
395
398
  }
396
399
  else {
@@ -30,7 +30,6 @@ const FormRendererInternal = (props) => {
30
30
  const [fetchedOptions, setFetchedOptions] = useState({});
31
31
  const [expandAll, setExpandAll] = useState();
32
32
  const [action, setAction] = useState();
33
- const [triggerFieldReset, setTriggerFieldReset] = useState(false);
34
33
  const [isInitializing, setIsInitializing] = useState(true);
35
34
  const [parameters, setParameters] = useState();
36
35
  const validationContainerRef = useRef(null);
@@ -83,9 +82,6 @@ const FormRendererInternal = (props) => {
83
82
  setValue(key, value[key], { shouldValidate: true });
84
83
  }
85
84
  }
86
- if (triggerFieldReset === true) {
87
- setTriggerFieldReset(false);
88
- }
89
85
  }
90
86
  }, [value]);
91
87
  const handleReset = () => {
@@ -95,7 +91,6 @@ const FormRendererInternal = (props) => {
95
91
  else {
96
92
  reset(instance); // clears react-hook-form state back to default values
97
93
  }
98
- setTriggerFieldReset(true);
99
94
  };
100
95
  useEffect(() => {
101
96
  handleValidation(entries, register, getValues(), action?.parameters, instance);
@@ -209,7 +204,6 @@ const FormRendererInternal = (props) => {
209
204
  fieldHeight,
210
205
  handleChange: onChange,
211
206
  onAutosave,
212
- triggerFieldReset,
213
207
  showSubmitError: isSubmitted,
214
208
  associatedObject,
215
209
  form,
@@ -1,8 +1,8 @@
1
1
  import { useApiServices, useApp, useAuthenticationContext, useNavigate, useObject, } from '@evoke-platform/context';
2
2
  import { useQuery, useQueryClient } from '@tanstack/react-query';
3
3
  import axios from 'axios';
4
- import { cloneDeep, get, isArray, isEmpty, isEqual, omit, pick, set } from 'lodash';
5
- import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import { cloneDeep, get, isArray, isEmpty, isEqual, isObject, pick, set } from 'lodash';
5
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { Skeleton, Snackbar } from '../../core';
7
7
  import { Box } from '../../layout';
8
8
  import ErrorComponent from '../ErrorComponent';
@@ -11,6 +11,7 @@ import { evalDefaultVals, processValueUpdate } from './components/DefaultValues'
11
11
  import Header from './components/Header';
12
12
  import { convertPropertiesToParams, createFileLinks, deleteDocuments, encodePageSlug, extractAllCriteria, extractPresetValuesFromCriteria, extractPresetValuesFromDynamicDefaultValues, formatSubmission, getEntryId, getPrefixedUrl, getUnnestedEntries, isAddressProperty, isEmptyWithDefault, plainTextToRtf, useFormById, } from './components/utils';
13
13
  import FormRenderer from './FormRenderer';
14
+ import { DepGraph } from 'dependency-graph';
14
15
  // Wrapper to provide QueryClient context for FormRendererContainer if this is not a nested form
15
16
  function FormRendererContainer(props) {
16
17
  return (React.createElement(ConditionalQueryClientProvider, null,
@@ -23,7 +24,7 @@ function FormRendererContainerInner(props) {
23
24
  const navigateTo = useNavigate();
24
25
  const queryClient = useQueryClient();
25
26
  const { id: appId } = useApp();
26
- const [parameters, setParameters] = useState();
27
+ const [parameters, setParameters] = useState([]);
27
28
  const formDataRef = useRef();
28
29
  // We only need the setter to force a re-render when form data updates; the value itself
29
30
  // is intentionally not referenced elsewhere to avoid stale reads (we use formDataRef).
@@ -53,7 +54,54 @@ function FormRendererContainerInner(props) {
53
54
  const [isSaving, setIsSaving] = useState(false);
54
55
  const [lastSavedData, setLastSavedData] = useState({});
55
56
  const [uniquePresetValues, setUniquePresetValues] = useState([]);
56
- const flattenFormEntries = useMemo(() => getUnnestedEntries(form?.entries || []), [form?.entries]);
57
+ const flattenFormEntries = useMemo(() => {
58
+ const graph = new DepGraph({ circular: true });
59
+ const unnestedEntries = getUnnestedEntries(form?.entries || []);
60
+ const nonInputEntries = [];
61
+ for (const entry of unnestedEntries) {
62
+ const entryId = getEntryId(entry);
63
+ if (entryId && entry.type !== 'readonlyField') {
64
+ graph.addNode(entryId, entry);
65
+ }
66
+ else {
67
+ nonInputEntries.push(entry);
68
+ }
69
+ }
70
+ for (const entry of unnestedEntries) {
71
+ const entryId = getEntryId(entry);
72
+ if (entryId && (entry.type === 'input' || entry.type === 'inputField')) {
73
+ const { defaultValue } = entry.display || {};
74
+ let presetValues = [];
75
+ if (typeof defaultValue === 'string') {
76
+ presetValues = extractPresetValuesFromDynamicDefaultValues([entry]);
77
+ }
78
+ else if (isObject(defaultValue) && 'criteria' in defaultValue && !isEmpty(defaultValue.criteria)) {
79
+ presetValues = extractPresetValuesFromCriteria(defaultValue.criteria);
80
+ }
81
+ presetValues.forEach((presetValue) => {
82
+ const fragments = presetValue.replace(/{{{input.|}}}|{{input.|}}/g, '').split('.');
83
+ // preset value references top level fields i.e name or dateOfBirth
84
+ if (fragments.length === 1 && graph.hasNode(fragments[0])) {
85
+ graph.addDependency(entryId, fragments[0]);
86
+ }
87
+ else if (fragments.length > 1) {
88
+ // preset value references nested fields i.e address.line1 or person.address.line1
89
+ const addressKeys = ['line1', 'line2', 'city', 'county', 'state', 'zipCode', 'country'];
90
+ if (addressKeys.includes(fragments[1]) && graph.hasNode(`${fragments[0]}.${fragments[1]}`)) {
91
+ graph.addDependency(entryId, `${fragments[0]}.${fragments[1]}`);
92
+ }
93
+ else if (graph.hasNode(fragments[0])) {
94
+ graph.addDependency(entryId, fragments[0]);
95
+ }
96
+ }
97
+ });
98
+ }
99
+ }
100
+ return graph
101
+ .overallOrder()
102
+ .map((id) => graph.getNodeData(id))
103
+ .concat(nonInputEntries);
104
+ }, [form?.entries]);
57
105
  const userAccount = useAuthenticationContext()?.account;
58
106
  const objectStore = useObject(form?.objectId ?? objectId);
59
107
  const onError = (err) => {
@@ -71,7 +119,7 @@ function FormRendererContainerInner(props) {
71
119
  });
72
120
  // trigger refetch on success
73
121
  const { data: instance, error: instanceError } = useQuery({
74
- queryKey: [objectId, instanceId, 'instance'],
122
+ queryKey: [objectId, instanceId, 'instance', uniquePresetValues],
75
123
  queryFn: async () => {
76
124
  const instance = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}`), {
77
125
  params: {
@@ -93,6 +141,87 @@ function FormRendererContainerInner(props) {
93
141
  staleTime: Infinity,
94
142
  enabled: !!instanceId && !!sanitizedObject,
95
143
  });
144
+ const getDefaultValues = useCallback(async (unnestedEntries, instanceData) => {
145
+ const result = cloneDeep(instanceData);
146
+ for (const entry of unnestedEntries) {
147
+ const fieldId = getEntryId(entry);
148
+ if (!fieldId)
149
+ continue;
150
+ const fieldValue = get(result, fieldId);
151
+ if ((entry.type === 'input' || entry.type === 'inputField') &&
152
+ isAddressProperty(entry.parameterId || entry.input?.id)) {
153
+ if ((isEmpty(result) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
154
+ entry?.display?.defaultValue &&
155
+ parameters) {
156
+ const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, result);
157
+ if (isArray(defaultValuesArray)) {
158
+ defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
159
+ set(result, fieldId, fieldValue);
160
+ });
161
+ }
162
+ }
163
+ else if (fieldValue !== undefined && fieldValue !== null) {
164
+ set(result, fieldId, fieldValue);
165
+ }
166
+ }
167
+ else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
168
+ const parameter = parameters?.find((param) => param.id === fieldId);
169
+ if (associatedObject?.propertyId === fieldId &&
170
+ associatedObject?.instanceId &&
171
+ parameter &&
172
+ action?.type === 'create') {
173
+ try {
174
+ const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`), {
175
+ params: {
176
+ expand: uniquePresetValues.filter((value) => value.startsWith(`{{{input.${fieldId}.`) ||
177
+ value.startsWith(`{{input.${fieldId}.`)),
178
+ },
179
+ });
180
+ result[associatedObject.propertyId] = instance;
181
+ }
182
+ catch (error) {
183
+ console.error(error);
184
+ }
185
+ }
186
+ else if (entry.type === 'formlet') {
187
+ // TODO: this should eventually fetch the formletId then get the fields and default values of those fields
188
+ }
189
+ else if (entry.type !== 'readonlyField') {
190
+ if (isEmptyWithDefault(fieldValue, entry, result)) {
191
+ if (fieldId && parameters && parameters.length > 0) {
192
+ const defaultValuesArray = await evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, result);
193
+ for (const { fieldId, fieldValue } of defaultValuesArray) {
194
+ const parameter = parameters?.find((param) => param.id === fieldId);
195
+ if (parameter?.type === 'object') {
196
+ const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
197
+ for (const field of dependentFields) {
198
+ set(result, field.fieldId, field.fieldValue);
199
+ }
200
+ }
201
+ set(result, fieldId, fieldValue);
202
+ }
203
+ }
204
+ }
205
+ else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
206
+ result[fieldId] = false;
207
+ }
208
+ else if (fieldValue !== undefined && fieldValue !== null) {
209
+ if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
210
+ let RTFFieldValue = fieldValue;
211
+ if (!fieldValue.trim().startsWith('{\\rtf')) {
212
+ RTFFieldValue = plainTextToRtf(fieldValue);
213
+ }
214
+ result[fieldId] = RTFFieldValue;
215
+ }
216
+ else {
217
+ result[fieldId] = fieldValue;
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ return result;
224
+ }, [action, parameters, associatedObject, uniquePresetValues, formDataRef, apiServices, userAccount]);
96
225
  useEffect(() => {
97
226
  if (!sanitizedObject)
98
227
  return;
@@ -113,7 +242,7 @@ function FormRendererContainerInner(props) {
113
242
  else {
114
243
  setError('Action could not be found');
115
244
  }
116
- }, [sanitizedObject, actionId, form?.actionId, instanceId]);
245
+ }, [sanitizedObject, actionId, form?.actionId, instanceId, flattenFormEntries, parameters]);
117
246
  const { data: navigationSlug } = useQuery({
118
247
  queryKey: [appId, 'navigationSlug'],
119
248
  queryFn: () => apiServices.get(getPrefixedUrl(`/apps/${appId}/pages/${encodePageSlug(pageNavigation)}`)),
@@ -159,11 +288,12 @@ function FormRendererContainerInner(props) {
159
288
  onError(error);
160
289
  }, [sanitizedObjectError, fetchedFormError, instanceError]);
161
290
  useEffect(() => {
162
- if (!form)
291
+ if (!form || !action)
163
292
  return;
164
293
  // If no parameters are defined, then the action is synced with object properties
165
- const getParamsFromObject = sanitizedObject && !action?.parameters;
166
- setParameters(getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action?.parameters);
294
+ const getParamsFromObject = sanitizedObject && !action.parameters;
295
+ const parameters = (getParamsFromObject ? convertPropertiesToParams(sanitizedObject) : action.parameters) ?? [];
296
+ setParameters(parameters.filter((param) => param.type !== 'collection' && !param.formula));
167
297
  }, [form, action?.parameters, sanitizedObject]);
168
298
  useEffect(() => {
169
299
  const getInitialValues = async () => {
@@ -175,7 +305,7 @@ function FormRendererContainerInner(props) {
175
305
  }
176
306
  };
177
307
  getInitialValues();
178
- }, [instanceId, instance, flattenFormEntries]);
308
+ }, [instanceId, instance, flattenFormEntries, getDefaultValues]);
179
309
  const onSubmissionSuccess = (updatedInstance) => {
180
310
  setSnackbarError({
181
311
  showAlert: true,
@@ -222,9 +352,7 @@ function FormRendererContainerInner(props) {
222
352
  if (action?.type === 'create') {
223
353
  const response = await apiServices.post(getPrefixedUrl(`/objects/${form.objectId}/instances/actions`), {
224
354
  actionId: form.actionId,
225
- input: omit(submission, sanitizedObject?.properties
226
- ?.filter((property) => property.formula || property.type === 'collection')
227
- .map((property) => property.id) ?? []),
355
+ input: pick(submission, parameters.map((parameter) => parameter.id)),
228
356
  });
229
357
  if (response) {
230
358
  // Manually link files to created instance.
@@ -235,9 +363,7 @@ function FormRendererContainerInner(props) {
235
363
  else if (instanceId && action) {
236
364
  const response = await objectStore.instanceAction(instanceId, {
237
365
  actionId: action.id,
238
- input: omit(submission, sanitizedObject?.properties
239
- ?.filter((property) => property.formula || property.type === 'collection')
240
- .map((property) => property.id) ?? []),
366
+ input: pick(submission, parameters.map((parameter) => parameter.id)),
241
367
  });
242
368
  if (sanitizedObject && instance) {
243
369
  onSubmissionSuccess(response);
@@ -258,79 +384,6 @@ function FormRendererContainerInner(props) {
258
384
  throw error; // Throw error so caller knows submission failed
259
385
  }
260
386
  };
261
- const getDefaultValues = async (unnestedEntries, instanceData) => {
262
- const result = {};
263
- for (const entry of unnestedEntries) {
264
- const fieldId = getEntryId(entry);
265
- if (!fieldId)
266
- continue;
267
- const fieldValue = get(instanceData, fieldId);
268
- if ((entry.type === 'input' || entry.type === 'inputField') &&
269
- isAddressProperty(entry.parameterId || entry.input?.id)) {
270
- if ((isEmpty(instanceData) || fieldValue === undefined || fieldValue === null || fieldValue === '') &&
271
- entry?.display?.defaultValue &&
272
- parameters) {
273
- const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
274
- if (isArray(defaultValuesArray)) {
275
- defaultValuesArray.forEach(({ fieldId, fieldValue }) => {
276
- set(result, fieldId, fieldValue);
277
- });
278
- }
279
- }
280
- else if (fieldValue !== undefined && fieldValue !== null) {
281
- set(result, fieldId, fieldValue);
282
- }
283
- }
284
- else if (entry.type !== 'sections' && entry.type !== 'columns' && entry.type !== 'content') {
285
- const parameter = parameters?.find((param) => param.id === fieldId);
286
- if (associatedObject?.propertyId === fieldId &&
287
- associatedObject?.instanceId &&
288
- parameter &&
289
- action?.type === 'create') {
290
- try {
291
- const instance = await apiServices.get(getPrefixedUrl(`/objects/${parameter.objectId}/instances/${associatedObject.instanceId}`));
292
- result[associatedObject.propertyId] = instance;
293
- }
294
- catch (error) {
295
- console.error(error);
296
- }
297
- }
298
- else if (entry.type !== 'readonlyField') {
299
- if (isEmptyWithDefault(fieldValue, entry, instanceData)) {
300
- if (fieldId && parameters && parameters.length > 0) {
301
- const defaultValuesArray = await evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, instanceData);
302
- for (const { fieldId, fieldValue } of defaultValuesArray) {
303
- const parameter = parameters?.find((param) => param.id === fieldId);
304
- if (parameter?.type === 'object') {
305
- const dependentFields = await processValueUpdate(unnestedEntries, parameters, fieldValue, apiServices, fieldId, formDataRef.current, userAccount);
306
- for (const field of dependentFields) {
307
- set(result, field.fieldId, field.fieldValue);
308
- }
309
- }
310
- set(result, fieldId, fieldValue);
311
- }
312
- }
313
- }
314
- else if (parameter?.type === 'boolean' && (fieldValue === undefined || fieldValue === null)) {
315
- result[fieldId] = false;
316
- }
317
- else if (fieldValue !== undefined && fieldValue !== null) {
318
- if (parameter?.type === 'richText' && typeof fieldValue === 'string') {
319
- let RTFFieldValue = fieldValue;
320
- if (!fieldValue.trim().startsWith('{\\rtf')) {
321
- RTFFieldValue = plainTextToRtf(fieldValue);
322
- }
323
- result[fieldId] = RTFFieldValue;
324
- }
325
- else {
326
- result[fieldId] = fieldValue;
327
- }
328
- }
329
- }
330
- }
331
- }
332
- return result;
333
- };
334
387
  const removeUneditedProtectedValues = (data) => {
335
388
  const protectedProperties = sanitizedObject?.properties?.filter((prop) => prop.protection?.maskChar);
336
389
  if (!protectedProperties || protectedProperties.length === 0) {
@@ -385,21 +438,19 @@ function FormRendererContainerInner(props) {
385
438
  !flattenFormEntries.some((e) => (e.type === 'input' && e.parameterId === id) || (e.type === 'inputField' && e.input.id === id));
386
439
  if (isReadOnlyField)
387
440
  return;
388
- if (parameter) {
389
- if (parameter.type === 'object' && parameters && parameters.length > 0) {
441
+ if (parameter?.type === 'string' && parameter.enum && value) {
442
+ // If a single select property has a sortBy option that isn't NONE the value gets spread and doesn't save properly,
443
+ // this will make it correctly save the value
444
+ value = value.value ? value.value : value;
445
+ }
446
+ if (!isEqual(value, get(formDataRef.current, id))) {
447
+ if (parameter?.type === 'object' && parameters && parameters.length > 0) {
390
448
  // On change of a related object, update default values dependent on that object
391
449
  const dependentFields = await processValueUpdate(flattenFormEntries, parameters, value, apiServices, id, formDataRef.current, userAccount);
392
450
  for (const field of dependentFields) {
393
451
  onChange(field.fieldId, field.fieldValue);
394
452
  }
395
453
  }
396
- else if (parameter.type === 'string' && parameter.enum && value) {
397
- // If a single select property has a sortBy option that isn't NONE the value gets spread and doesn't save properly,
398
- // this will make it correctly save the value
399
- value = value.value ? value.value : value;
400
- }
401
- }
402
- if (!isEqual(value, get(formDataRef.current, id))) {
403
454
  const newData = { ...formDataRef.current };
404
455
  set(newData, id, value);
405
456
  setFormData(newData);
@@ -1,6 +1,6 @@
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[], unnestedEntries: FormEntry[], 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[], entry: InputParameterReference | InputField, fieldValue: unknown, fieldId: string, apiServices: ApiServices, userAccount?: UserAccount, formValues?: FieldValues, updatedRelatedObjectValue?: ObjectInstance | null | Reference, updatedRelatedObjectParamId?: string): Promise<{
4
4
  fieldId: string;
5
5
  fieldValue: unknown;
6
6
  }[]>;
@@ -1,7 +1,7 @@
1
- import { isArray, isEmpty, uniq } from 'lodash';
1
+ import { isArray, isEmpty, isObject, uniq } from 'lodash';
2
2
  import { DateTime } from 'luxon';
3
- import { getEntryId, getPrefixedUrl, isAddressProperty } from './utils';
4
- export async function evalDefaultVals(parameters, unnestedEntries, entry, fieldValue, fieldId, apiServices, userAccount, formValues, updatedRelatedObjectValue) {
3
+ import { getEntryId, getPrefixedUrl, isAddressProperty, transformToWhere, updateCriteriaInputs } from './utils';
4
+ export async function evalDefaultVals(parameters, entry, fieldValue, fieldId, apiServices, userAccount, formValues, updatedRelatedObjectValue, updatedRelatedObjectParamId) {
5
5
  const updates = [];
6
6
  const parameter = parameters.find((param) => param.id === fieldId);
7
7
  const defaultValue = entry.display?.defaultValue;
@@ -14,7 +14,6 @@ export async function evalDefaultVals(parameters, unnestedEntries, entry, fieldV
14
14
  const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedProperty>[a-zA-Z][a-zA-Z0-9_]*)}}$/;
15
15
  const groups = regex.exec(item)?.groups;
16
16
  if (groups?.relatedObjectProperty && groups?.nestedProperty) {
17
- const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
18
17
  let relatedObjectInstance = updatedRelatedObjectValue;
19
18
  if (!relatedObjectInstance && !isEmpty(formValues)) {
20
19
  relatedObjectInstance = formValues[groups.relatedObjectProperty];
@@ -28,30 +27,14 @@ export async function evalDefaultVals(parameters, unnestedEntries, entry, fieldV
28
27
  ]);
29
28
  updates.push({ fieldId, fieldValue });
30
29
  }
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
- }
37
- const instance = await new Promise((resolve) => {
38
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
39
- if (error) {
40
- console.error(error);
41
- return resolve(undefined);
42
- }
43
- resolve(instance);
44
- });
45
- });
46
- if (instance) {
47
- fieldValue = uniq([
48
- ...staticValues,
49
- ...(isArray(instance[groups.nestedProperty])
50
- ? instance[groups.nestedProperty]
51
- : []),
52
- ]);
53
- updates.push({ fieldId, fieldValue });
54
- }
30
+ else if (relatedObjectInstance) {
31
+ fieldValue = uniq([
32
+ ...staticValues,
33
+ ...(isArray(relatedObjectInstance[groups.nestedProperty])
34
+ ? relatedObjectInstance[groups.nestedProperty]
35
+ : []),
36
+ ]);
37
+ updates.push({ fieldId, fieldValue });
55
38
  }
56
39
  else {
57
40
  updates.push({ fieldId, fieldValue: staticValues });
@@ -65,42 +48,23 @@ export async function evalDefaultVals(parameters, unnestedEntries, entry, fieldV
65
48
  }
66
49
  else if (typeof defaultValue === 'string' && /^{{.*}}$/.test(defaultValue)) {
67
50
  if (isAddressProperty(fieldId)) {
68
- const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<addressProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedAddressProperty>line1|line2|city|county|state|zipCode)}}$/;
51
+ const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<addressProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedAddressProperty>line1|line2|city|county|state|zipCode|country)}}$/;
69
52
  const groups = regex.exec(defaultValue)?.groups;
70
53
  if (groups?.relatedObjectProperty && groups?.addressProperty && groups?.nestedAddressProperty) {
71
- const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
72
54
  let relatedObjectInstance = updatedRelatedObjectValue;
73
55
  if (!relatedObjectInstance && !isEmpty(formValues)) {
74
56
  relatedObjectInstance = formValues[groups.relatedObjectProperty];
75
57
  }
76
- if (updatedRelatedObjectValue?.[groups.addressProperty]?.[groups.nestedAddressProperty]) {
58
+ if (updatedRelatedObjectValue === null) {
59
+ updates.push({ fieldId, fieldValue: '' });
60
+ }
61
+ else if (updatedRelatedObjectValue?.[groups.addressProperty]?.[groups.nestedAddressProperty]) {
77
62
  fieldValue = updatedRelatedObjectValue?.[groups.addressProperty]?.[groups.nestedAddressProperty];
78
63
  updates.push({ fieldId, fieldValue });
79
64
  }
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
- }
86
- const instance = await new Promise((resolve) => {
87
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
88
- if (error) {
89
- console.error(error);
90
- return resolve(undefined);
91
- }
92
- resolve(instance);
93
- });
94
- });
95
- // Clear dependent fields only if value is explicitly null (user cleared it).
96
- // If updatedRelatedObjectValue is undefined (not triggered by onChange), use the value from the instance.
97
- if (updatedRelatedObjectValue === null) {
98
- updates.push({ fieldId, fieldValue: '' });
99
- }
100
- else if (instance) {
101
- fieldValue = instance?.[groups.addressProperty]?.[groups.nestedAddressProperty];
102
- updates.push({ fieldId, fieldValue });
103
- }
65
+ else if (relatedObjectInstance) {
66
+ fieldValue = relatedObjectInstance?.[groups.addressProperty]?.[groups.nestedAddressProperty];
67
+ updates.push({ fieldId, fieldValue });
104
68
  }
105
69
  }
106
70
  }
@@ -108,44 +72,44 @@ export async function evalDefaultVals(parameters, unnestedEntries, entry, fieldV
108
72
  const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedProperty>[a-zA-Z][a-zA-Z0-9_]*)}}$/;
109
73
  const groups = regex.exec(defaultValue)?.groups;
110
74
  if (groups?.relatedObjectProperty && groups?.nestedProperty) {
111
- const relatedObjectParameter = parameters.find((param) => param.id === groups?.relatedObjectProperty);
112
75
  let relatedObjectInstance = updatedRelatedObjectValue;
113
76
  if (!relatedObjectInstance && !isEmpty(formValues)) {
114
77
  relatedObjectInstance = formValues[groups.relatedObjectProperty];
115
78
  }
116
- if (updatedRelatedObjectValue?.[groups.nestedProperty]) {
79
+ if (updatedRelatedObjectValue === null) {
80
+ updates.push({ fieldId, fieldValue: null });
81
+ }
82
+ else if (updatedRelatedObjectValue?.[groups.nestedProperty]) {
117
83
  fieldValue = updatedRelatedObjectValue[groups.nestedProperty];
118
84
  updates.push({ fieldId, fieldValue });
119
85
  }
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
- }
126
- const instance = await new Promise((resolve) => {
127
- apiServices.get(getPrefixedUrl(`/objects/${relatedObjectId}/instances/${relatedObjectInstance?.id}`), (error, instance) => {
128
- if (error) {
129
- console.error(error);
130
- return resolve(undefined);
131
- }
132
- resolve(instance);
133
- });
134
- });
135
- // Clear dependent fields only if value is explicitly null (user cleared it).
136
- // If updatedRelatedObjectValue is undefined (not triggered by onChange), use the value from the instance.
137
- if (updatedRelatedObjectValue === null) {
138
- updates.push({ fieldId, fieldValue: null });
139
- }
140
- else if (instance) {
141
- fieldValue = instance?.[groups.nestedProperty] || null;
142
- updates.push({ fieldId, fieldValue });
143
- }
86
+ else if (relatedObjectInstance) {
87
+ fieldValue = relatedObjectInstance[groups.nestedProperty] || null;
88
+ updates.push({ fieldId, fieldValue });
144
89
  }
145
90
  }
146
91
  }
147
92
  // all other default values set here
148
93
  }
94
+ else if (isObject(defaultValue) && 'criteria' in defaultValue && !isEmpty(defaultValue.criteria)) {
95
+ const criteria = defaultValue.criteria;
96
+ const evaluatedCriteria = updateCriteriaInputs(criteria, { ...formValues, [updatedRelatedObjectParamId || '']: updatedRelatedObjectValue }, userAccount);
97
+ try {
98
+ const instances = await apiServices.get(getPrefixedUrl(`/objects/${parameter?.objectId || entry.display?.relatedObjectId}/instances`), {
99
+ params: {
100
+ filter: {
101
+ where: transformToWhere(evaluatedCriteria),
102
+ limit: 1,
103
+ },
104
+ },
105
+ });
106
+ updates.push({ fieldId, fieldValue: instances[0] ?? null });
107
+ }
108
+ catch (error) {
109
+ updates.push({ fieldId, fieldValue: null });
110
+ console.error(error);
111
+ }
112
+ }
149
113
  else if (parameter?.type !== 'object') {
150
114
  let updatedValue = defaultValue;
151
115
  // handles current default values ie: "Current logged in user", "Today" etc.
@@ -175,38 +139,45 @@ export async function processValueUpdate(unnestedEntries, parameters, updatedRel
175
139
  const parameterId = getEntryId(entry);
176
140
  if (!parameterId)
177
141
  return [];
142
+ if (parameterId === changedEntryId) {
143
+ continue;
144
+ }
145
+ const defaultValue = entry.display.defaultValue;
178
146
  if (isAddressProperty(parameterId)) {
179
- const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<addressProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedAddressProperty>line1|line2|city|county|state|zipCode)}}$/;
147
+ const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<addressProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedAddressProperty>line1|line2|city|county|state|zipCode|country)}}$/;
180
148
  const groups = regex.exec(entry.display.defaultValue)?.groups;
181
149
  const [addressObject, addressField] = parameterId.split('.');
182
150
  if (groups?.relatedObjectProperty &&
183
151
  groups?.addressProperty &&
184
152
  groups?.nestedAddressProperty &&
185
153
  changedEntryId === groups.relatedObjectProperty) {
186
- const result = await evalDefaultVals(parameters, unnestedEntries, entry, formValues?.[addressObject]?.[addressField], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
154
+ const result = await evalDefaultVals(parameters, entry, formValues?.[addressObject]?.[addressField], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue, changedEntryId);
187
155
  updates.push(...result);
188
156
  }
189
157
  }
190
- else if (isArray(entry.display.defaultValue) &&
191
- entry.display.defaultValue.some((item) => /^{{.*}}$/.test(item))) {
192
- for (const item of entry.display.defaultValue.filter((item) => /^{{.*}}$/.test(item))) {
158
+ else if (isArray(defaultValue) && defaultValue.some((item) => /^{{.*}}$/.test(item))) {
159
+ for (const item of defaultValue.filter((item) => /^{{.*}}$/.test(item))) {
193
160
  const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedProperty>[a-zA-Z][a-zA-Z0-9_]*)}}$/;
194
161
  const groups = regex.exec(item)?.groups;
195
162
  if (groups?.relatedObjectProperty &&
196
163
  groups?.nestedProperty &&
197
164
  changedEntryId === groups.relatedObjectProperty) {
198
- const result = await evalDefaultVals(parameters, unnestedEntries, entry, entry.display.defaultValue, parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
165
+ const result = await evalDefaultVals(parameters, entry, defaultValue, parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue, changedEntryId);
199
166
  updates.push(...result);
200
167
  }
201
168
  }
202
169
  }
170
+ else if (isObject(defaultValue) && 'criteria' in defaultValue && !isEmpty(defaultValue.criteria)) {
171
+ const result = await evalDefaultVals(parameters, entry, formValues?.[parameterId], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue, changedEntryId);
172
+ updates.push(...result);
173
+ }
203
174
  else {
204
175
  const regex = /^{{input\.(?<relatedObjectProperty>[a-zA-Z][a-zA-Z0-9_]*)\.(?<nestedProperty>[a-zA-Z][a-zA-Z0-9_]*)}}$/;
205
- const groups = regex.exec(entry.display.defaultValue)?.groups;
176
+ const groups = regex.exec(defaultValue)?.groups;
206
177
  if (groups?.relatedObjectProperty &&
207
178
  groups?.nestedProperty &&
208
179
  changedEntryId === groups.relatedObjectProperty) {
209
- const result = await evalDefaultVals(parameters, unnestedEntries, entry, formValues?.[parameterId], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue);
180
+ const result = await evalDefaultVals(parameters, entry, formValues?.[parameterId], parameterId, apiServices, userAccount, formValues, updatedRelatedObjectValue, changedEntryId);
210
181
  updates.push(...result);
211
182
  }
212
183
  }
@@ -18,7 +18,6 @@ type FormContextType = {
18
18
  handleChange?: (name: string, value: unknown) => void | Promise<void>;
19
19
  onAutosave?: (fieldId: string) => void | Promise<void>;
20
20
  fieldHeight?: 'small' | 'medium';
21
- triggerFieldReset?: boolean;
22
21
  showSubmitError?: boolean;
23
22
  associatedObject?: {
24
23
  instanceId: string;
@@ -10,7 +10,6 @@ export const FormContext = createContext({
10
10
  expandedSections: [],
11
11
  parameters: [],
12
12
  showSubmitError: false,
13
- triggerFieldReset: false,
14
13
  handleChange: () => { },
15
14
  fieldHeight: 'medium',
16
15
  form: {},
@@ -7,5 +7,5 @@ type CriteriaProps = {
7
7
  error?: boolean;
8
8
  };
9
9
  type CriteriaValue = Record<string, unknown>;
10
- export default function Criteria(props: CriteriaProps): React.JSX.Element;
11
- export {};
10
+ declare const Criteria: (props: CriteriaProps) => React.JSX.Element;
11
+ export default Criteria;
@@ -6,7 +6,7 @@ import { Button, CircularProgress, Skeleton, Typography } from '../../../../core
6
6
  import { Box } from '../../../../layout';
7
7
  import CriteriaBuilder from '../../../CriteriaBuilder';
8
8
  import { addressProperties, getPrefixedUrl } from '../utils';
9
- export default function Criteria(props) {
9
+ const Criteria = (props) => {
10
10
  const { value, canUpdateProperty, fieldDefinition, error } = props;
11
11
  const apiServices = useApiServices();
12
12
  const { handleChange, onAutosave } = useFormContext();
@@ -90,4 +90,5 @@ export default function Criteria(props) {
90
90
  type: 'date-time',
91
91
  },
92
92
  ], enablePresetValues: true }))) : (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No criteria"));
93
- }
93
+ };
94
+ export default Criteria;
@@ -56,11 +56,9 @@ const styles = {
56
56
  export const Image = (props) => {
57
57
  const { id, canUpdateProperty, error, value, hasDescription } = props;
58
58
  const { handleChange, onAutosave: onAutosave } = useFormContext();
59
- const [image, setImage] = useState();
59
+ const [image, setImage] = useState(value ?? null);
60
60
  useEffect(() => {
61
- if (typeof value === 'string') {
62
- setImage(value);
63
- }
61
+ setImage(value ?? null);
64
62
  }, [value]);
65
63
  const handleUpload = async (file) => {
66
64
  // max file size 300KB
@@ -1,7 +1,7 @@
1
1
  import { useApiServices } from '@evoke-platform/context';
2
2
  import { DialogActions } from '@mui/material';
3
3
  import { useQueryClient } from '@tanstack/react-query';
4
- import { isEmpty } from 'lodash';
4
+ import { isEmpty, pick } from 'lodash';
5
5
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { Close } from '../../../../../../icons';
7
7
  import useWidgetSize, { useFormContext } from '../../../../../../theme/hooks';
@@ -95,11 +95,15 @@ const RelatedObjectInstance = (props) => {
95
95
  // Handle the case where relatedObject is undefined
96
96
  return;
97
97
  }
98
+ const action = relatedObject.actions?.find((act) => act.id === actionId);
99
+ const parameters = action?.parameters || [];
98
100
  submission = await formatSubmission(submission, apiServices, relatedObject.id);
99
101
  try {
100
102
  const response = await apiServices.post(getPrefixedUrl(`/objects/${relatedObject.id}/instances/actions`), {
101
103
  actionId: actionId,
102
- input: submission,
104
+ input: pick(submission, parameters
105
+ .filter((param) => param.type !== 'collection' && !param.formula)
106
+ .map((param) => param.id)),
103
107
  });
104
108
  const expandedInstance = await apiServices.get(getPrefixedUrl(`/objects/${relatedObject.id}/instances/${response.id}`), {
105
109
  params: {
@@ -39,7 +39,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
39
39
  }
40
40
  export function RecursiveEntryRenderer(props) {
41
41
  const { entry } = props;
42
- const { object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, triggerFieldReset, associatedObject, width, } = useFormContext();
42
+ const { object, getValues, errors, instance, richTextEditor: RichTextEditor, parameters, handleChange, onAutosave, fieldHeight, associatedObject, width, } = useFormContext();
43
43
  const { isBelow, breakpoints } = useWidgetSize({
44
44
  scroll: false,
45
45
  defaultWidth: width,
@@ -154,7 +154,7 @@ export function RecursiveEntryRenderer(props) {
154
154
  }
155
155
  }
156
156
  else if (fieldDefinition.type === 'richText') {
157
- return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor ? (React.createElement(RichTextEditor
157
+ return (React.createElement(FieldWrapper, { key: `${entryId}-${fieldValue}`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) }, RichTextEditor ? (React.createElement(RichTextEditor
158
158
  // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
159
159
  , {
160
160
  // RichTexts get a uniqueId when the form is loaded to prevent issues with multiple rich text fields on one form
@@ -170,7 +170,7 @@ export function RecursiveEntryRenderer(props) {
170
170
  }
171
171
  else if (fieldDefinition.type === 'criteria') {
172
172
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
173
- React.createElement(Criteria, { key: triggerFieldReset ? `${entryId}-reset-true` : `${entryId}-reset-false`, fieldDefinition: fieldDefinition, value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), error: !!errors?.[entryId] })));
173
+ React.createElement(Criteria, { key: `${entryId}-${fieldValue}`, fieldDefinition: fieldDefinition, value: fieldValue, canUpdateProperty: !(entry.type === 'readonlyField'), error: !!errors?.[entryId] })));
174
174
  }
175
175
  else {
176
176
  // Add `aria-describedby` to ensure screen readers read the description
@@ -198,12 +198,11 @@ export function RecursiveEntryRenderer(props) {
198
198
  * Key remounts the field if a value is changed.
199
199
  * Needed to clear certain fields on discard changes.
200
200
  */
201
- key: (fieldDefinition.type === 'choices' ||
201
+ key: fieldDefinition.type === 'choices' ||
202
202
  fieldDefinition?.type === 'time' ||
203
- fieldDefinition?.type === 'boolean') &&
204
- triggerFieldReset
205
- ? `${entryId}-reset-true`
206
- : `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
203
+ fieldDefinition?.type === 'boolean'
204
+ ? `${entryId}-${fieldValue}`
205
+ : undefined, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors), errorMessage: undefined },
207
206
  React.createElement(FormField, { id: entryId,
208
207
  // TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
209
208
  property: fieldDefinition, defaultValue: fieldValue, onChange: handleChange, onBlur: () => {
@@ -403,7 +403,7 @@ export const convertPropertiesToParams = (object) => {
403
403
  ]
404
404
  : [...acc, property];
405
405
  }, [])
406
- .filter(({ id, type }) => type !== 'collection')
406
+ .filter((prop) => prop.type !== 'collection' && !prop.formula)
407
407
  .flat()
408
408
  .sort((a, b) => a.name.localeCompare(b.name))
409
409
  .map(propertyToParameter);
@@ -630,8 +630,8 @@ export async function formatSubmission(submission, apiServices, objectId, instan
630
630
  // if there are address fields with no value address needs to be set to undefined to be able to submit
631
631
  }
632
632
  else if (typeof value === 'object' && value !== null) {
633
- if (Object.values(value).every((v) => v === undefined)) {
634
- submission[key] = undefined;
633
+ if (Object.values(value).every((v) => v === undefined || v === '')) {
634
+ submission[key] = null;
635
635
  // only submit the name and id of a regular related object
636
636
  // and include objectId if it is a dynamic related object
637
637
  }
@@ -150,12 +150,11 @@ export declare function useFormContext(): {
150
150
  handleChange?: ((name: string, value: unknown) => void | Promise<void>) | undefined;
151
151
  onAutosave?: ((fieldId: string) => void | Promise<void>) | undefined;
152
152
  fieldHeight?: "medium" | "small" | undefined;
153
- triggerFieldReset?: boolean | undefined;
154
153
  showSubmitError?: boolean | undefined;
155
154
  associatedObject?: {
156
155
  instanceId: string;
157
- propertyId: string; /** Extra large screens (1536px and up) */
158
- } | undefined; /** Extra large screens (1536px and up) */
156
+ propertyId: string;
157
+ } | undefined;
159
158
  form?: import("@evoke-platform/context").EvokeForm | undefined;
160
159
  width: number;
161
160
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.15.0",
3
+ "version": "1.15.1",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -112,6 +112,7 @@
112
112
  "@react-querybuilder/material": "^6.5.0",
113
113
  "@tanstack/react-query": "^5.90.20",
114
114
  "clean-deep": "^3.4.0",
115
+ "dependency-graph": "^1.0.0",
115
116
  "devexpress-richedit": "^23.1.5",
116
117
  "devextreme": "^23.1.5",
117
118
  "devextreme-dist": "^23.1.5",