@evoke-platform/ui-components 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +2 -2
  2. package/dist/published/components/custom/CriteriaBuilder/types.d.ts +0 -15
  3. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +0 -10
  4. package/dist/published/components/custom/CriteriaBuilder/utils.js +2 -161
  5. package/dist/published/components/custom/Form/utils.js +3 -2
  6. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +1 -1
  7. package/dist/published/components/custom/FormV2/FormRenderer.js +25 -27
  8. package/dist/published/components/custom/FormV2/FormRendererContainer.js +131 -100
  9. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.d.ts +5 -0
  10. package/dist/published/components/custom/FormV2/components/ConditionalQueryClientProvider.js +21 -0
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableField.js +86 -143
  12. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.d.ts +0 -2
  13. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/DropdownRepeatableFieldInput.js +1 -4
  14. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +105 -185
  15. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +36 -49
  16. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +18 -26
  17. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +18 -18
  18. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +17 -21
  19. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +96 -169
  20. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +0 -2
  21. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +57 -13
  22. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -1
  23. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +61 -29
  24. package/dist/published/components/custom/FormV2/components/utils.d.ts +23 -4
  25. package/dist/published/components/custom/FormV2/components/utils.js +136 -26
  26. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +28 -14
  27. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +40 -46
  28. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +2 -1
  29. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +56 -19
  30. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +7 -2
  31. package/dist/published/components/custom/index.d.ts +2 -0
  32. package/dist/published/components/custom/index.js +1 -0
  33. package/dist/published/components/custom/types.d.ts +15 -0
  34. package/dist/published/components/custom/types.js +1 -0
  35. package/dist/published/components/custom/util.d.ts +10 -0
  36. package/dist/published/components/custom/util.js +161 -1
  37. package/dist/published/index.d.ts +2 -2
  38. package/dist/published/index.js +1 -1
  39. package/package.json +3 -4
@@ -1,9 +1,9 @@
1
1
  import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
2
2
  import { WarningRounded } from '@mui/icons-material';
3
3
  import { isEmpty } from 'lodash';
4
- import React, { useEffect, useMemo } from 'react';
4
+ import React, { useMemo } from 'react';
5
5
  import useWidgetSize, { useFormContext } from '../../../../theme/hooks';
6
- import { TextField, Typography } from '../../../core';
6
+ import { Skeleton, TextField, Typography } from '../../../core';
7
7
  import { Box } from '../../../layout';
8
8
  import FormField from '../../FormField';
9
9
  import AccordionSections from './AccordionSections';
@@ -17,8 +17,9 @@ import { Image } from './FormFieldTypes/Image';
17
17
  import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
18
18
  import UserProperty from './FormFieldTypes/UserProperty';
19
19
  import FormSections from './FormSections';
20
- import { entryIsVisible, fetchCollectionData, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
20
+ import { entryIsVisible, fetchInitialMiddleObjectInstances, fetchMiddleObject, filterEmptySections, getEntryId, getFieldDefinition, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
21
21
  import HtmlView from './HtmlView';
22
+ import { useQuery } from '@tanstack/react-query';
22
23
  function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors, validation) {
23
24
  return {
24
25
  inputId: entryId,
@@ -38,7 +39,7 @@ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, displ
38
39
  }
39
40
  export function RecursiveEntryRenderer(props) {
40
41
  const { entry } = props;
41
- const { fetchedOptions, setFetchedOptions, 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, triggerFieldReset, associatedObject, width, } = useFormContext();
42
43
  const { isBelow, breakpoints } = useWidgetSize({
43
44
  scroll: false,
44
45
  defaultWidth: width,
@@ -50,19 +51,55 @@ export function RecursiveEntryRenderer(props) {
50
51
  const entryId = getEntryId(entry) || 'defaultId';
51
52
  const display = 'display' in entry ? entry.display : undefined;
52
53
  const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues ? getValues(entryId) : undefined;
53
- const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
54
- const middleObject = fetchedOptions[`${entryId}MiddleObject`];
55
54
  const fieldDefinition = useMemo(() => {
56
55
  if (!object)
57
56
  return undefined;
58
57
  return getFieldDefinition(entry, object, parameters);
59
58
  }, [entry, parameters, object]);
60
59
  const validation = fieldDefinition?.validation || {};
61
- useEffect(() => {
62
- if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
63
- fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
64
- }
65
- }, [fieldDefinition, instance]);
60
+ const { data: middleObject } = useQuery({
61
+ queryKey: [fieldDefinition?.objectId, 'MiddleObject'],
62
+ queryFn: () => fetchMiddleObject(fieldDefinition, apiServices),
63
+ staleTime: Infinity,
64
+ enabled: !!(fieldDefinition?.objectId &&
65
+ fieldDefinition?.type === 'collection' &&
66
+ fieldDefinition?.manyToManyPropertyId),
67
+ meta: {
68
+ errorMessage: 'Failed to fetch middle object: ',
69
+ },
70
+ });
71
+ const { data: initialMiddleObjectInstances = [], isLoading: isLoadingInstances } = useQuery({
72
+ queryKey: [
73
+ fieldDefinition?.objectId,
74
+ instance?.id,
75
+ fieldDefinition?.relatedPropertyId,
76
+ 'InitialMiddleObjectInstances',
77
+ ],
78
+ queryFn: () => fetchInitialMiddleObjectInstances(apiServices, fieldDefinition, instance?.id),
79
+ staleTime: Infinity,
80
+ enabled: !!(fieldDefinition?.objectId &&
81
+ instance?.id &&
82
+ fieldDefinition?.type === 'collection' &&
83
+ fieldDefinition?.manyToManyPropertyId &&
84
+ fieldDefinition?.relatedPropertyId),
85
+ meta: {
86
+ errorMessage: 'Failed to fetch middle object instances: ',
87
+ },
88
+ });
89
+ const memorizedCriteria = useMemo(() => {
90
+ return 'criteria' in validation && validation.criteria && getValues
91
+ ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
92
+ : undefined;
93
+ }, [validation, getValues && getValues(), userAccount, instance]);
94
+ const memorizedDefaultValueCriteria = useMemo(() => {
95
+ return display?.defaultValue &&
96
+ typeof display.defaultValue === 'object' &&
97
+ 'criteria' in display.defaultValue &&
98
+ display.defaultValue.criteria &&
99
+ getValues
100
+ ? updateCriteriaInputs(display.defaultValue.criteria, getValues(), userAccount, instance)
101
+ : undefined;
102
+ }, [display, getValues && getValues(), userAccount, instance]);
66
103
  if (associatedObject?.propertyId === entryId)
67
104
  return null;
68
105
  // If the entry is hidden, clear its value and any nested values, and skip rendering
@@ -83,15 +120,15 @@ export function RecursiveEntryRenderer(props) {
83
120
  }
84
121
  else if (fieldDefinition.type === 'object') {
85
122
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
86
- 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: 'criteria' in validation && validation.criteria
87
- ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
88
- : undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
89
- ? display?.defaultValue.sortBy
90
- : undefined, orderBy: typeof display?.defaultValue === 'object' && 'orderBy' in display.defaultValue
91
- ? display?.defaultValue.orderBy
92
- : undefined, defaultValueCriteria: typeof display?.defaultValue === 'object' && 'criteria' in display.defaultValue
93
- ? display?.defaultValue?.criteria
94
- : undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description, formId: display?.createFormId, createActionId: display?.createActionId })));
123
+ 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: memorizedCriteria, sortBy: display?.defaultValue &&
124
+ typeof display.defaultValue === 'object' &&
125
+ 'sortBy' in display.defaultValue
126
+ ? display.defaultValue.sortBy
127
+ : undefined, orderBy: display?.defaultValue &&
128
+ typeof display.defaultValue === 'object' &&
129
+ 'orderBy' in display.defaultValue
130
+ ? display.defaultValue.orderBy
131
+ : undefined, defaultValueCriteria: memorizedDefaultValueCriteria, viewLayout: display?.viewLayout, hasDescription: !!display?.description, formId: display?.createFormId, createActionId: display?.createActionId })));
95
132
  }
96
133
  else if (fieldDefinition.type === 'user') {
97
134
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
@@ -99,12 +136,9 @@ export function RecursiveEntryRenderer(props) {
99
136
  }
100
137
  else if (fieldDefinition.type === 'collection') {
101
138
  if (fieldDefinition?.manyToManyPropertyId) {
102
- if (middleObject && !isEmpty(middleObject)) {
103
- return (initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
104
- React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] ||
105
- initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: 'criteria' in validation && validation.criteria
106
- ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
107
- : undefined, hasDescription: !!display?.description }))));
139
+ if (!isEmpty(middleObject)) {
140
+ return isLoadingInstances ? (React.createElement(Skeleton, null)) : (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
141
+ React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, readOnly: entry.type === 'readonlyField', criteria: memorizedCriteria, hasDescription: !!display?.description })));
108
142
  }
109
143
  else {
110
144
  // when in the builder preview, the middle object won't be fetched so instead show an empty field
@@ -116,9 +150,7 @@ export function RecursiveEntryRenderer(props) {
116
150
  }
117
151
  else {
118
152
  return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, errors) },
119
- React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: 'criteria' in validation && validation.criteria
120
- ? updateCriteriaInputs(validation.criteria, getValues(), userAccount, instance)
121
- : undefined, viewLayout: display?.viewLayout, entry: entry })));
153
+ React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: memorizedCriteria, viewLayout: display?.viewLayout, entry: entry })));
122
154
  }
123
155
  }
124
156
  else if (fieldDefinition.type === 'richText') {
@@ -1,5 +1,5 @@
1
1
  /// <reference types="react" />
2
- import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
2
+ import { Action, ApiServices, Column, Columns, EvokeForm, FormEntry, InputField, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, Section, Sections, UserAccount, ObjWithRoot, PanelViewEntry } from '@evoke-platform/context';
3
3
  import { LocalDateTime } from '@js-joda/core';
4
4
  import { FieldErrors, FieldValues } from 'react-hook-form';
5
5
  import { ObjectProperty } from '../../../../types';
@@ -41,7 +41,8 @@ export declare function getDefaultPages(parameters: InputParameter[], defaultPag
41
41
  [x: string]: string;
42
42
  }>;
43
43
  export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount, instance?: Record<string, unknown>): Record<string, unknown>;
44
- export declare function fetchCollectionData(apiServices: ApiServices, fieldDefinition: InputParameter | Property, setFetchedOptions: (newData: FieldValues) => void, instanceId?: string, fetchedOptions?: Record<string, unknown>, initialMiddleObjectInstances?: ObjectInstance[]): Promise<void>;
44
+ export declare const fetchMiddleObject: (fieldDefinition: InputParameter | Property, apiServices: ApiServices) => Promise<ObjWithRoot | undefined>;
45
+ export declare const fetchInitialMiddleObjectInstances: (apiServices: ApiServices, fieldDefinition: InputParameter | Property, instanceId: string) => Promise<ObjectInstance[]>;
45
46
  export declare const getErrorCountForSection: (section: Section | Column, errors?: FieldErrors) => number;
46
47
  export declare const propertyToParameter: (property: Property) => InputParameter;
47
48
  export declare const propertyValidationToParameterValidation: (property: Property) => InputParameter['validation'];
@@ -86,7 +87,7 @@ export declare function formatSubmission(submission: FieldValues, apiServices?:
86
87
  propertyId: string;
87
88
  }, parameters?: InputParameter[]): Promise<FieldValues>;
88
89
  export declare function filterEmptySections(entry: Sections | Columns, instance?: FieldValues, formData?: FieldValues): Sections | Columns | null;
89
- export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[];
90
+ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[] | PanelViewEntry[], object: Obj, parameters?: InputParameter[]): FormEntry[] | PanelViewEntry[];
90
91
  /**
91
92
  * Converts a plain text string to RTF format suitable for a RichTextEditor.
92
93
  *
@@ -100,5 +101,23 @@ export declare function assignIdsToSectionsAndRichText(entries: FormEntry[], obj
100
101
  * This ensures that any plain text input will be safely represented in RTF without losing formatting or characters.
101
102
  */
102
103
  export declare function plainTextToRtf(plainText: string): string;
103
- export declare function getFieldDefinition(entry: FormEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
104
+ export declare function getFieldDefinition(entry: FormEntry | PanelViewEntry, object: Obj, parameters?: InputParameter[]): InputParameter | Property | undefined;
104
105
  export declare function obfuscateValue(value: unknown, property?: Partial<Property> | Partial<ObjectProperty>): unknown;
106
+ export declare function useFormById(formId: string, apiServices: ApiServices, errorMessage?: string): import("@tanstack/react-query/build/legacy/types").UseQueryResult<EvokeForm, Error>;
107
+ /**
108
+ * Extract all values from a criteria/filter object.
109
+ * Handles MongoDB-style query filters with operators like $eq, $in, $and, $or, etc.
110
+ *
111
+ * @param criteria - Filter object with examples:
112
+ * 1. { "propertyId": { "$eq": "value" } }
113
+ * 2. { "propertyId": { "$in": ["value1", "value2"] } }
114
+ * 3. { "propertyId": "value"}
115
+ * 4. { "$and": [ { "propertyId1": { "$eq": "value1" } }, { "propertyId2": { "$eq": "value2" } } ] }
116
+ * 5. { "$or": [ { "propertyId1": { "$eq": "value1" } }, { "propertyId2": { "$eq": "value2" } } ] }
117
+ * 6. Nested combinations of the above
118
+ *
119
+ * @returns Array of all values found in the criteria object
120
+ */
121
+ export declare const extractPresetValuesFromCriteria: (criteria: Record<string, unknown>) => string[];
122
+ export declare const extractAllCriteria: (flattenFormEntries: FormEntry[], parameters: InputParameter[]) => Record<string, unknown>[];
123
+ export declare const extractPresetValuesFromDynamicDefaultValues: (flattenFormEntries: FormEntry[]) => string[];
@@ -4,7 +4,9 @@ import { get, isArray, isEmpty, isObject, omit, pick, startCase, transform } fro
4
4
  import { DateTime } from 'luxon';
5
5
  import { nanoid } from 'nanoid';
6
6
  import Handlebars from 'no-eval-handlebars';
7
- import { defaultRuleProcessorMongoDB, formatQuery, parseMongoDB } from 'react-querybuilder';
7
+ import { defaultRuleProcessorMongoDB, formatQuery } from 'react-querybuilder';
8
+ import { parseMongoDB } from '../../util';
9
+ import { useQuery } from '@tanstack/react-query';
8
10
  export const scrollIntoViewWithOffset = (el, offset, container) => {
9
11
  const elementRect = el.getBoundingClientRect();
10
12
  const containerRect = container ? container.getBoundingClientRect() : document.body.getBoundingClientRect();
@@ -332,31 +334,19 @@ export function updateCriteriaInputs(criteria, data, user, instance) {
332
334
  },
333
335
  }));
334
336
  }
335
- export async function fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instanceId, fetchedOptions, initialMiddleObjectInstances) {
336
- try {
337
- if ((fetchedOptions && !fetchedOptions[`${fieldDefinition.id}MiddleObject`]) || !fetchedOptions) {
338
- const fetchedMiddleObject = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
339
- params: {
340
- filter: { fields: ['properties', 'actions', 'rootObjectId'] },
341
- },
342
- });
343
- setFetchedOptions({ [`${fieldDefinition.id}MiddleObject`]: fetchedMiddleObject });
344
- }
345
- // Fetch the initial middle object instances
346
- const filter = instanceId ? getMiddleObjectFilter(fieldDefinition, instanceId) : {};
347
- if (!isArray(initialMiddleObjectInstances)) {
348
- const fetchedInitialMiddleObjectInstances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
349
- params: { filter: JSON.stringify(filter) },
350
- });
351
- setFetchedOptions({
352
- [`${fieldDefinition.id}InitialMiddleObjectInstances`]: fetchedInitialMiddleObjectInstances,
353
- });
354
- }
355
- }
356
- catch (error) {
357
- console.error('Error fetching collection data:', error);
358
- }
359
- }
337
+ export const fetchMiddleObject = async (fieldDefinition, apiServices) => {
338
+ return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
339
+ params: {
340
+ filter: { fields: ['properties', 'actions', 'rootObjectId'] },
341
+ },
342
+ });
343
+ };
344
+ export const fetchInitialMiddleObjectInstances = async (apiServices, fieldDefinition, instanceId) => {
345
+ const filter = getMiddleObjectFilter(fieldDefinition, instanceId);
346
+ return await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
347
+ params: { filter: JSON.stringify(filter) },
348
+ });
349
+ };
360
350
  export const getErrorCountForSection = (section, errors) => {
361
351
  const entries = section.entries || [];
362
352
  return isArray(section.entries)
@@ -881,3 +871,123 @@ function applyMaskToObfuscatedValue(value, mask) {
881
871
  }
882
872
  return maskedValue;
883
873
  }
874
+ export function useFormById(formId, apiServices, errorMessage) {
875
+ return useQuery({
876
+ queryKey: ['form', formId],
877
+ enabled: formId !== '_auto_' && !!formId,
878
+ staleTime: Infinity,
879
+ queryFn: () => apiServices.get(getPrefixedUrl(`/forms/${formId}`)),
880
+ meta: {
881
+ errorMessage,
882
+ },
883
+ });
884
+ }
885
+ /**
886
+ * Extract all values from a criteria/filter object.
887
+ * Handles MongoDB-style query filters with operators like $eq, $in, $and, $or, etc.
888
+ *
889
+ * @param criteria - Filter object with examples:
890
+ * 1. { "propertyId": { "$eq": "value" } }
891
+ * 2. { "propertyId": { "$in": ["value1", "value2"] } }
892
+ * 3. { "propertyId": "value"}
893
+ * 4. { "$and": [ { "propertyId1": { "$eq": "value1" } }, { "propertyId2": { "$eq": "value2" } } ] }
894
+ * 5. { "$or": [ { "propertyId1": { "$eq": "value1" } }, { "propertyId2": { "$eq": "value2" } } ] }
895
+ * 6. Nested combinations of the above
896
+ *
897
+ * @returns Array of all values found in the criteria object
898
+ */
899
+ export const extractPresetValuesFromCriteria = (criteria) => {
900
+ const values = [];
901
+ if (!isObject(criteria) || isArray(criteria)) {
902
+ return values;
903
+ }
904
+ for (const [key, value] of Object.entries(criteria)) {
905
+ if (key === '$and' || key === '$or') {
906
+ // Handle logical operators with array of conditions
907
+ if (isArray(value)) {
908
+ for (const item of value) {
909
+ if (isObject(item)) {
910
+ values.push(...extractPresetValuesFromCriteria(item));
911
+ }
912
+ }
913
+ }
914
+ }
915
+ else if (key.startsWith('$')) {
916
+ // Handle query operators like $eq, $in, $ne, etc.
917
+ if (isArray(value)) {
918
+ // For operators like $in that contain arrays
919
+ values.push(...value.filter((v) => typeof v === 'string' && v.startsWith('{{{') && v.endsWith('}}}')));
920
+ }
921
+ else {
922
+ // For operators like $eq that contain single values
923
+ if (typeof value === 'string' && value.startsWith('{{{') && value.endsWith('}}}'))
924
+ values.push(value);
925
+ }
926
+ }
927
+ else if (isObject(value)) {
928
+ values.push(...extractPresetValuesFromCriteria(value));
929
+ }
930
+ else {
931
+ // Direct property value
932
+ if (typeof value === 'string' && value.startsWith('{{{') && value.endsWith('}}}'))
933
+ values.push(value);
934
+ }
935
+ }
936
+ return values;
937
+ };
938
+ export const extractAllCriteria = (flattenFormEntries, parameters) => {
939
+ const allCriterias = [];
940
+ for (const entry of flattenFormEntries) {
941
+ if (entry.type === 'input' && entry.parameterId) {
942
+ const param = parameters?.find((param) => param.id === entry.parameterId &&
943
+ param.type === 'object' &&
944
+ param.objectId &&
945
+ !isEmpty(param.validation?.criteria));
946
+ if (param) {
947
+ allCriterias.push(param.validation.criteria);
948
+ }
949
+ const defaultValue = entry.display?.defaultValue;
950
+ if (isObject(defaultValue) && 'criteria' in defaultValue) {
951
+ const defaultValueCriteria = defaultValue.criteria;
952
+ if (!isEmpty(defaultValueCriteria)) {
953
+ allCriterias.push(defaultValueCriteria);
954
+ }
955
+ }
956
+ }
957
+ else if (entry.type === 'inputField' && ['collection', 'object'].includes(entry.input?.type || '')) {
958
+ const validationCriteria = entry.input?.validation?.criteria;
959
+ if (!isEmpty(validationCriteria)) {
960
+ allCriterias.push(validationCriteria);
961
+ }
962
+ const defaultValue = entry.display?.defaultValue;
963
+ if (isObject(defaultValue) && 'criteria' in defaultValue) {
964
+ const defaultValueCriteria = defaultValue.criteria;
965
+ if (!isEmpty(defaultValueCriteria)) {
966
+ allCriterias.push(defaultValueCriteria);
967
+ }
968
+ }
969
+ }
970
+ }
971
+ return allCriterias;
972
+ };
973
+ export const extractPresetValuesFromDynamicDefaultValues = (flattenFormEntries) => {
974
+ const allPresetValues = [];
975
+ for (const entry of flattenFormEntries) {
976
+ if (entry.type === 'input' || entry.type === 'inputField') {
977
+ const defaultValue = entry.display?.defaultValue;
978
+ if (defaultValue) {
979
+ if (typeof defaultValue === 'string' && defaultValue.startsWith('{{') && defaultValue.endsWith('}}')) {
980
+ allPresetValues.push(defaultValue);
981
+ }
982
+ else if (Array.isArray(defaultValue)) {
983
+ for (const val of defaultValue) {
984
+ if (typeof val === 'string' && val.startsWith('{{') && val.endsWith('}}')) {
985
+ allPresetValues.push(val);
986
+ }
987
+ }
988
+ }
989
+ }
990
+ }
991
+ }
992
+ return allPresetValues;
993
+ };
@@ -1,3 +1,4 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
1
2
  import { render as baseRender, screen, waitFor, within } from '@testing-library/react';
2
3
  import userEvent from '@testing-library/user-event';
3
4
  import { isEmpty, isEqual, set } from 'lodash';
@@ -14,12 +15,9 @@ global.ResizeObserver = class ResizeObserver {
14
15
  unobserve() { }
15
16
  disconnect() { }
16
17
  };
17
- const WithProviders = ({ children }) => {
18
- return React.createElement(MemoryRouter, null, children);
19
- };
20
- const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
21
18
  describe('FormRenderer', () => {
22
19
  let server;
20
+ let queryClient;
23
21
  beforeAll(() => {
24
22
  server = setupServer(http.get('/api/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/api/data/objects/specialtyType/effective', (req) => {
25
23
  const sanitizedVersion = new URL(req.request.url).searchParams.get('sanitizedVersion');
@@ -65,12 +63,28 @@ describe('FormRenderer', () => {
65
63
  }), http.get('/api/accessManagement/users', () => HttpResponse.json(users)));
66
64
  server.listen();
67
65
  });
68
- afterAll(() => {
69
- server.close();
66
+ beforeEach(() => {
67
+ // Create a fresh QueryClient for each test, need to pass `retry: false` to avoid retries interfering with error state tests
68
+ queryClient = new QueryClient({
69
+ defaultOptions: {
70
+ queries: {
71
+ retry: false,
72
+ },
73
+ },
74
+ });
75
+ });
76
+ afterEach(() => {
77
+ server.resetHandlers();
78
+ queryClient.clear();
70
79
  });
71
80
  afterEach(() => {
72
81
  server.resetHandlers();
73
82
  });
83
+ const WithProviders = ({ children }) => {
84
+ return (React.createElement(QueryClientProvider, { client: queryClient },
85
+ React.createElement(MemoryRouter, null, children)));
86
+ };
87
+ const render = (ui, options) => baseRender(ui, { wrapper: WithProviders, ...options });
74
88
  describe('validation criteria', () => {
75
89
  it(`filters related object field with validation criteria that references a related object's nested data`, async () => {
76
90
  const user = userEvent.setup();
@@ -414,7 +428,7 @@ describe('FormRenderer', () => {
414
428
  type: 'input',
415
429
  parameterId: 'specialtyType',
416
430
  display: {
417
- label: 'Speciality Type',
431
+ label: 'Specialty Type',
418
432
  mode: 'existingOnly',
419
433
  },
420
434
  },
@@ -500,7 +514,7 @@ describe('FormRenderer', () => {
500
514
  type: 'input',
501
515
  parameterId: 'specialtyType',
502
516
  display: {
503
- label: 'Speciality Type',
517
+ label: 'Specialty Type',
504
518
  },
505
519
  },
506
520
  ],
@@ -586,7 +600,7 @@ describe('FormRenderer', () => {
586
600
  type: 'input',
587
601
  parameterId: 'specialtyType',
588
602
  display: {
589
- label: 'Speciality Type',
603
+ label: 'Specialty Type',
590
604
  relatedObjectDisplay: 'dialogBox',
591
605
  mode: 'existingOnly',
592
606
  },
@@ -654,7 +668,7 @@ describe('FormRenderer', () => {
654
668
  type: 'input',
655
669
  parameterId: 'specialtyType',
656
670
  display: {
657
- label: 'Speciality Type',
671
+ label: 'Specialty Type',
658
672
  relatedObjectDisplay: 'dialogBox',
659
673
  mode: 'existingOnly',
660
674
  },
@@ -684,7 +698,7 @@ describe('FormRenderer', () => {
684
698
  type: 'input',
685
699
  parameterId: 'specialtyType',
686
700
  display: {
687
- label: 'Speciality Type',
701
+ label: 'Specialty Type',
688
702
  createActionId: '_create',
689
703
  },
690
704
  },
@@ -944,7 +958,7 @@ describe('FormRenderer', () => {
944
958
  type: 'input',
945
959
  parameterId: 'specialtyType',
946
960
  display: {
947
- label: 'Speciality Type',
961
+ label: 'Specialty Type',
948
962
  relatedObjectDisplay: 'dropdown',
949
963
  mode: 'existingOnly',
950
964
  },
@@ -1021,7 +1035,7 @@ describe('FormRenderer', () => {
1021
1035
  const user = userEvent.setup();
1022
1036
  render(React.createElement(FormRenderer, { form: form, onChange: () => { } }));
1023
1037
  // Navigate to and open dropdown
1024
- const dropdown = await screen.findByRole('combobox', { name: 'Speciality Type' });
1038
+ const dropdown = await screen.findByRole('combobox', { name: 'Specialty Type' });
1025
1039
  await user.click(dropdown);
1026
1040
  await screen.findByRole('listbox');
1027
1041
  // Verify that the existing option is present
@@ -1176,7 +1190,7 @@ describe('FormRenderer', () => {
1176
1190
  type: 'input',
1177
1191
  parameterId: 'specialtyType',
1178
1192
  display: {
1179
- label: 'Speciality Type',
1193
+ label: 'Specialty Type',
1180
1194
  createActionId: '_create',
1181
1195
  relatedObjectId: 'specialtyType',
1182
1196
  },