@evoke-platform/ui-components 1.4.0-testing.1 → 1.4.0-testing.2

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.
@@ -1,14 +1,14 @@
1
1
  import { useApp, useAuthenticationContext, } from '@evoke-platform/context';
2
2
  import { Components, Form as FormIO, Utils } from '@formio/react';
3
3
  import { flatten } from 'flat';
4
- import { isEmpty, isEqual, toPairs } from 'lodash';
4
+ import { isEmpty, isEqual, isObject, pick, toPairs } from 'lodash';
5
5
  import React, { useEffect, useRef, useState } from 'react';
6
6
  import '../../../../styles/form-component.css';
7
7
  import { Skeleton, Snackbar } from '../../../core';
8
8
  import { Box } from '../../../layout';
9
9
  import { ButtonComponent, DocumentComponent, FormFieldComponent, ImageComponent, ObjectComponent, RepeatableFieldComponent, UserComponent, ViewOnlyComponent, } from '../FormComponents';
10
10
  import { CriteriaComponent } from '../FormComponents/CriteriaComponent/CriteriaComponent';
11
- import { addObjectPropertiesToComponentProps, buildComponentPropsFromDocumentProperties, buildComponentPropsFromObjectProperties, convertFormToComponents, getFlattenEntries, getPrefixedUrl, } from '../utils';
11
+ import { addObjectPropertiesToComponentProps, buildComponentPropsFromDocumentProperties, buildComponentPropsFromObjectProperties, convertFormToComponents, flattenFormComponents, getAllCriteriaInputs, getFlattenEntries, getPrefixedUrl, } from '../utils';
12
12
  const usePrevious = (value) => {
13
13
  const ref = useRef();
14
14
  useEffect(() => {
@@ -101,24 +101,16 @@ export function Form(props) {
101
101
  const buildComponents = async () => {
102
102
  const action = object?.actions?.find((action) => action.id === actionId);
103
103
  let input;
104
- const parameters = associatedObject?.propertyId && action?.parameters
105
- ? action.parameters.filter((param) => param.id !== associatedObject.propertyId)
106
- : action?.parameters;
107
- if (parameters && object) {
104
+ if (action?.parameters && object) {
108
105
  input = action?.form?.entries
109
- ? convertFormToComponents(action.form.entries, parameters, object)
110
- : parameters.filter((param) => object.properties?.some((prop) => prop.id === param.id));
106
+ ? convertFormToComponents(action.form.entries, action.parameters, object)
107
+ : action.parameters.filter((param) => object.properties?.some((prop) => prop.id === param.id));
111
108
  }
112
109
  else {
113
110
  input = action?.inputProperties ?? (action?.type === 'delete' ? [] : undefined);
114
111
  }
115
- let visibleObjectProperties = object?.properties;
116
- if (associatedObject) {
117
- // Eliminates the associated object's field from the form
118
- visibleObjectProperties = visibleObjectProperties?.filter((property) => property.id !== associatedObject.propertyId);
119
- }
120
112
  let foundDefaultPages = defaultPages;
121
- const relatedObjectProperties = visibleObjectProperties?.filter((property) => property.type === 'object');
113
+ const relatedObjectProperties = object?.properties?.filter((property) => property.type === 'object');
122
114
  if (relatedObjectProperties) {
123
115
  foundDefaultPages = await relatedObjectProperties.reduce(async (acc, property) => {
124
116
  const result = await acc;
@@ -132,23 +124,25 @@ export function Form(props) {
132
124
  }, Promise.resolve({}));
133
125
  }
134
126
  const allDefaultPages = { ...defaultPages, ...foundDefaultPages };
135
- if (input && visibleObjectProperties) {
127
+ // visibleObjectProperties
128
+ if (input && object?.properties) {
129
+ const allCriteriaInputs = getAllCriteriaInputs(action?.inputProperties ? flattenFormComponents(action.inputProperties) : action?.parameters ?? []);
136
130
  if (input.length || action?.type !== 'delete') {
137
131
  // formIO builder-configured input properties exist
138
- const newComponentProps = await addObjectPropertiesToComponentProps(visibleObjectProperties, input, instance, {
132
+ const newComponentProps = await addObjectPropertiesToComponentProps(object.properties, input, allCriteriaInputs, instance, {
139
133
  ...objectInputCommonProps,
140
134
  defaultPages: allDefaultPages,
141
135
  navigateTo,
142
136
  apiServices,
143
137
  user: userAccount,
144
- }, undefined, isReadOnly, allDefaultPages, navigateTo, queryAddresses, apiServices, !!closeModal, fieldHeight, richTextEditor);
138
+ }, associatedObject, undefined, isReadOnly, allDefaultPages, navigateTo, queryAddresses, apiServices, !!closeModal, fieldHeight, richTextEditor);
145
139
  if (!hideButtons && !isReadOnly) {
146
140
  newComponentProps.push(BottomButtons);
147
141
  }
148
142
  setComponentProps(newComponentProps);
149
143
  }
150
144
  else {
151
- const components = await addObjectPropertiesToComponentProps(visibleObjectProperties, [
145
+ const components = await addObjectPropertiesToComponentProps(object.properties, [
152
146
  {
153
147
  html: `<p>${action?.type === 'delete' ? 'This action cannot be undone.' : 'Are you sure?'}</p>`,
154
148
  label: 'Content',
@@ -216,13 +210,13 @@ export function Form(props) {
216
210
  addons: [],
217
211
  id: 'eahbwo',
218
212
  },
219
- ], instance, {
213
+ ], undefined, instance, {
220
214
  ...objectInputCommonProps,
221
215
  defaultPages: allDefaultPages,
222
216
  navigateTo,
223
217
  apiServices,
224
218
  user: userAccount,
225
- }, undefined, undefined, undefined, undefined, undefined, undefined, !!closeModal, fieldHeight, richTextEditor);
219
+ }, undefined, undefined, undefined, undefined, undefined, undefined, undefined, !!closeModal, fieldHeight, richTextEditor);
226
220
  if (!hideButtons) {
227
221
  components.push(BottomButtons);
228
222
  }
@@ -343,7 +337,12 @@ export function Form(props) {
343
337
  submittedFields[field] = null;
344
338
  }
345
339
  else {
346
- submittedFields[field] = value;
340
+ if (isObject(value) && 'id' in value && 'name' in value) {
341
+ submittedFields[field] = pick(value, 'id', 'name');
342
+ }
343
+ else {
344
+ submittedFields[field] = value;
345
+ }
347
346
  }
348
347
  }
349
348
  //OPTIMIZATION TODO: See if type can be inferred from the event target
@@ -6,6 +6,7 @@ interface ObjectComponentProps extends BaseFormComponentProps {
6
6
  mode: 'default' | 'existingOnly';
7
7
  defaultValueCriteria?: Record<string, unknown>;
8
8
  initialValue?: string;
9
+ allCriteriaInputs: string[];
9
10
  richTextEditor?: typeof ReactComponent;
10
11
  }
11
12
  export declare class ObjectComponent extends ReactComponent {
@@ -20,6 +21,7 @@ export declare class ObjectComponent extends ReactComponent {
20
21
  updatedDefaultValueCriteria: Record<string, unknown>;
21
22
  constructor(component: ObjectComponentProps, options: any, data: any);
22
23
  init(): void;
24
+ expandInstance(): Promise<void>;
23
25
  clearErrors(): void;
24
26
  handleValidation(): void;
25
27
  hasErrors(): boolean;
@@ -1,10 +1,9 @@
1
1
  import { ReactComponent } from '@formio/react';
2
- import dot from 'dot-object';
3
- import { cloneDeep, isEmpty, pick } from 'lodash';
2
+ import { cloneDeep, isEmpty, isNil, isObject, pick } from 'lodash';
4
3
  import React from 'react';
5
4
  import ReactDOM from 'react-dom';
6
5
  import { FormComponentWrapper } from '../../Common';
7
- import { getAllCriteriaInputs, getPrefixedUrl, transformToWhere, updateCriteriaInputs } from '../../utils';
6
+ import { getCriteriaInputs, getPrefixedUrl, populateInstanceWithNestedData, transformToWhere, updateCriteriaInputs, } from '../../utils';
8
7
  import { ObjectPropertyInput } from './ObjectPropertyInput';
9
8
  export class ObjectComponent extends ReactComponent {
10
9
  constructor(component, options, data) {
@@ -17,9 +16,9 @@ export class ObjectComponent extends ReactComponent {
17
16
  delete this.errorDetails['api-error'];
18
17
  const updatedValue = pick(value, 'id', 'name');
19
18
  // set the value on the form instance at this.root.data
20
- this.setValue(updatedValue ?? '');
19
+ this.setValue(!isNil(updatedValue) ? updatedValue : '');
21
20
  // update the value in the component instance
22
- this.updateValue(updatedValue ?? {}, { modified: true });
21
+ this.updateValue(!isNil(updatedValue) ? updatedValue : {}, { modified: true });
23
22
  this.handleValidation();
24
23
  this.emit('changed-' + this.component.key, value);
25
24
  this.attach(this.element);
@@ -33,25 +32,44 @@ export class ObjectComponent extends ReactComponent {
33
32
  this.handleChangeObjectProperty = this.handleChangeObjectProperty.bind(this);
34
33
  }
35
34
  init() {
36
- const data = dot.dot(this.root._data);
37
35
  if (this.criteria) {
38
- const inputProps = getAllCriteriaInputs(this.criteria);
36
+ const inputProps = getCriteriaInputs(this.criteria);
37
+ this.updatedCriteria = updateCriteriaInputs(this.criteria, this.root._data, this.component.user);
39
38
  for (const inputProp of inputProps) {
40
- // Parse data to update criteria when form is loaded.
41
- updateCriteriaInputs(this.updatedCriteria, inputProp, data[inputProp], true);
42
39
  // Parse data to update criteria when form field is updated
43
40
  // Need to parse all fields again.
44
41
  const compKeyFragments = inputProp.split('.');
45
42
  let compKey = compKeyFragments[0];
46
- if (['line1', 'line2', 'city', 'state', 'zipCode'].includes(compKeyFragments[1])) {
43
+ const property = this.component.properties.find((c) => c.id === compKey);
44
+ if (property?.type === 'address' &&
45
+ ['line1', 'line2', 'city', 'state', 'zipCode'].includes(compKeyFragments[1])) {
47
46
  compKey = inputProp;
48
47
  }
49
- this.on(`changed-${compKey}`, () => {
50
- const data = dot.dot(this.root._data);
51
- this.updatedCriteria = cloneDeep(this.criteria) ?? {};
52
- for (const inputProp of inputProps) {
53
- updateCriteriaInputs(this.updatedCriteria, inputProp, data[inputProp], true);
48
+ this.on(`changed-${compKey}`, async (value) => {
49
+ const data = this.root._data;
50
+ if (property?.type === 'object' && isObject(value) && 'id' in value && 'objectId' in value) {
51
+ const paths = this.component.allCriteriaInputs
52
+ .filter((input) => input.split('.')[0] === compKey)
53
+ .map((p) => p.split('.').slice(1).join('.'));
54
+ let instance = value;
55
+ if (!isEmpty(paths)) {
56
+ instance = await populateInstanceWithNestedData(value['id'], value['objectId'], paths, this.component.apiServices);
57
+ }
58
+ data[compKey] = instance;
54
59
  }
60
+ else {
61
+ if (compKey.includes('.')) {
62
+ const keyFragments = compKey.split('.');
63
+ if (!data[keyFragments[0]]) {
64
+ data[keyFragments[0]] = {};
65
+ }
66
+ data[keyFragments[0]][keyFragments[1]] = value;
67
+ }
68
+ else {
69
+ data[compKey] = value;
70
+ }
71
+ }
72
+ this.updatedCriteria = updateCriteriaInputs(this.criteria ?? {}, data, this.component.user);
55
73
  if (this.visible) {
56
74
  this.attachReact(this.element);
57
75
  }
@@ -59,25 +77,43 @@ export class ObjectComponent extends ReactComponent {
59
77
  }
60
78
  }
61
79
  if (this.defaultValueCriteria) {
62
- const inputProps = getAllCriteriaInputs(this.defaultValueCriteria);
63
- updateCriteriaInputs(this.updatedDefaultValueCriteria, 'user.id', this.component.user?.id ?? '');
80
+ const inputProps = getCriteriaInputs(this.defaultValueCriteria);
81
+ this.updatedDefaultValueCriteria = updateCriteriaInputs(this.defaultValueCriteria, this.root._data, this.component.user);
64
82
  for (const inputProp of inputProps) {
65
- // Parse data to update criteria when form is loaded.
66
- updateCriteriaInputs(this.updatedDefaultValueCriteria, inputProp, data[inputProp], true);
67
83
  // Parse data to update criteria when form field is updated
68
84
  // Need to parse all fields again.
69
85
  const compKeyFragments = inputProp.split('.');
70
86
  let compKey = compKeyFragments[0];
71
- if (['line1', 'line2', 'city', 'state', 'zipCode'].includes(compKeyFragments[1])) {
87
+ const property = this.component.properties.find((c) => c.id === compKey);
88
+ if (property?.type === 'address' &&
89
+ ['line1', 'line2', 'city', 'state', 'zipCode'].includes(compKeyFragments[1])) {
72
90
  compKey = inputProp;
73
91
  }
74
- this.on(`changed-${compKey}`, () => {
75
- const data = dot.dot(this.root._data);
76
- this.updatedDefaultValueCriteria = cloneDeep(this.defaultValueCriteria) ?? {};
77
- updateCriteriaInputs(this.updatedDefaultValueCriteria, 'user.id', this.component.user?.id ?? '');
78
- for (const inputProp of inputProps) {
79
- updateCriteriaInputs(this.updatedDefaultValueCriteria, inputProp, data[inputProp], true);
92
+ this.on(`changed-${compKey}`, async (value) => {
93
+ const data = this.root._data;
94
+ if (property?.type === 'object' && isObject(value) && 'id' in value && 'objectId' in value) {
95
+ const paths = this.component.allCriteriaInputs
96
+ .filter((input) => input.split('.')[0] === compKey)
97
+ .map((p) => p.split('.').slice(1).join('.'));
98
+ let instance = value;
99
+ if (!isEmpty(paths)) {
100
+ instance = await populateInstanceWithNestedData(value['id'], value['objectId'], paths, this.component.apiServices);
101
+ }
102
+ data[compKey] = instance;
103
+ }
104
+ else {
105
+ if (compKey.includes('.')) {
106
+ const keyFragments = compKey.split('.');
107
+ if (!data[keyFragments[0]]) {
108
+ data[keyFragments[0]] = {};
109
+ }
110
+ data[keyFragments[0]][keyFragments[1]] = value;
111
+ }
112
+ else {
113
+ data[compKey] = value;
114
+ }
80
115
  }
116
+ this.updatedDefaultValueCriteria = updateCriteriaInputs(this.defaultValueCriteria ?? {}, data, this.component.user);
81
117
  if (this.visible) {
82
118
  this.attachReact(this.element);
83
119
  }
@@ -124,6 +160,19 @@ export class ObjectComponent extends ReactComponent {
124
160
  this.attach(this.element);
125
161
  this.attachReact(this.element);
126
162
  });
163
+ if (this.component.defaultValue) {
164
+ this.expandInstance();
165
+ }
166
+ }
167
+ async expandInstance() {
168
+ const { property, apiServices, defaultValue } = this.component;
169
+ const paths = this.component.allCriteriaInputs
170
+ .filter((input) => input.split('.')[0] === this.component.key)
171
+ .map((p) => p.split('.').slice(1).join('.'));
172
+ if (!isEmpty(paths)) {
173
+ const instance = await populateInstanceWithNestedData(defaultValue.id, property.objectId, paths, apiServices);
174
+ this.handleChangeObjectProperty(property.id, instance);
175
+ }
127
176
  }
128
177
  clearErrors() {
129
178
  this.errorDetails = {};
@@ -1,4 +1,4 @@
1
- import { Action, ApiServices, Obj, Property, UserAccount } from '@evoke-platform/context';
1
+ import { Action, ApiServices, Obj, UserAccount } from '@evoke-platform/context';
2
2
  import React from 'react';
3
3
  import { Address, ObjectPropertyInputProps } from '../../types';
4
4
  export type ActionDialogProps = {
@@ -13,9 +13,12 @@ export type ActionDialogProps = {
13
13
  object: Obj;
14
14
  objectInputCommonProps: ObjectPropertyInputProps;
15
15
  instanceId?: string;
16
- relatedProperty?: Property;
17
16
  apiServices: ApiServices;
18
17
  queryAddresses?: (query: string) => Promise<Address[]>;
19
18
  user?: UserAccount;
19
+ associatedObject?: {
20
+ instanceId: string;
21
+ propertyId: string;
22
+ };
20
23
  };
21
24
  export declare const ActionDialog: (props: ActionDialogProps) => React.JSX.Element;
@@ -36,7 +36,7 @@ const styles = {
36
36
  },
37
37
  };
38
38
  export const ActionDialog = (props) => {
39
- const { open, onClose, action, instanceInput, handleSubmit, apiServices, object, instanceId, relatedProperty, objectInputCommonProps, queryAddresses, user, } = props;
39
+ const { open, onClose, action, instanceInput, handleSubmit, apiServices, object, instanceId, objectInputCommonProps, queryAddresses, associatedObject, user, } = props;
40
40
  const [updatedObject, setUpdatedObject] = useState();
41
41
  const [hasAccess, setHasAccess] = useState(false);
42
42
  const [loading, setLoading] = useState(false);
@@ -57,18 +57,16 @@ export const ActionDialog = (props) => {
57
57
  }, [object, instanceId]);
58
58
  useEffect(() => {
59
59
  const input = (action.form?.entries && action.parameters) || action?.inputProperties
60
- ? (action.form?.entries && action.parameters
60
+ ? action.form?.entries && action.parameters
61
61
  ? convertFormToComponents(action.form.entries, action.parameters, object)
62
- : action?.inputProperties)?.filter((inputProperty) => inputProperty?.key !== relatedProperty?.relatedPropertyId)
62
+ : action?.inputProperties
63
63
  : undefined;
64
64
  const updatedAction = {
65
65
  ...action,
66
66
  form: input ? { entries: convertComponentsToForm(input) } : undefined,
67
67
  };
68
- const properties = object.properties?.filter((prop) => prop.id !== relatedProperty?.relatedPropertyId);
69
68
  setUpdatedObject({
70
69
  ...object,
71
- properties,
72
70
  actions: concat(object.actions?.filter((a) => a.id !== action.id) ?? [], [updatedAction]),
73
71
  });
74
72
  }, [object]);
@@ -78,7 +76,7 @@ export const ActionDialog = (props) => {
78
76
  React.createElement(IconButton, { sx: styles.closeIcon, onClick: onClose },
79
77
  React.createElement(Close, { fontSize: "small" })),
80
78
  action && hasAccess && !loading ? action?.name : ''),
81
- React.createElement(DialogContent, null, hasAccess ? (React.createElement(Box, { sx: { width: '100%', marginTop: '10px' } }, (updatedObject || isDeleteAction) && (React.createElement(Form, { actionId: action.id, actionType: action.type, apiServices: objectInputCommonProps.apiServices, object: !isDeleteAction ? updatedObject : object, instance: instanceInput, onSave: async (data, setSubmitting) => handleSubmit(action.type, data, instanceId, setSubmitting), objectInputCommonProps: objectInputCommonProps, closeModal: onClose, queryAddresses: queryAddresses, user: user, submitButtonLabel: isDeleteAction ? 'Delete' : undefined })))) : (React.createElement(React.Fragment, null, loading ? (React.createElement(React.Fragment, null,
79
+ React.createElement(DialogContent, null, hasAccess ? (React.createElement(Box, { sx: { width: '100%', marginTop: '10px' } }, (updatedObject || isDeleteAction) && (React.createElement(Form, { actionId: action.id, actionType: action.type, apiServices: objectInputCommonProps.apiServices, object: !isDeleteAction ? updatedObject : object, instance: instanceInput, onSave: async (data, setSubmitting) => handleSubmit(action.type, data, instanceId, setSubmitting), objectInputCommonProps: objectInputCommonProps, closeModal: onClose, queryAddresses: queryAddresses, user: user, submitButtonLabel: isDeleteAction ? 'Delete' : undefined, associatedObject: associatedObject })))) : (React.createElement(React.Fragment, null, loading ? (React.createElement(React.Fragment, null,
82
80
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
83
81
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }),
84
82
  React.createElement(Skeleton, { height: '30px', animation: 'wave' }))) : (React.createElement(ErrorComponent, { code: 'AccessDenied', message: 'You do not have permission to perform this action.', styles: { boxShadow: 'none' } })))))));
@@ -405,7 +405,9 @@ const RepeatableField = (props) => {
405
405
  relatedObject && openDialog && (React.createElement(ActionDialog, { object: relatedObject, open: openDialog, apiServices: apiServices, onClose: () => setOpenDialog(false), instanceInput: dialogType === 'update' ? relatedInstances.find((i) => i.id === selectedRow) ?? {} : {}, handleSubmit: save,
406
406
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
407
407
  objectInputCommonProps: { apiServices }, action: relatedObject?.actions?.find((a) => a.id ===
408
- (dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, relatedProperty: property, queryAddresses: queryAddresses, user: user })),
408
+ (dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, queryAddresses: queryAddresses, user: user, associatedObject: instance.id && property.relatedPropertyId
409
+ ? { instanceId: instance.id, propertyId: property.relatedPropertyId }
410
+ : undefined })),
409
411
  React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({ isError: snackbarError.isError, showAlert: false }), message: snackbarError.message, error: snackbarError.isError })));
410
412
  };
411
413
  export default RepeatableField;
@@ -1,11 +1,10 @@
1
1
  import { ApiBaseUrlProvider, NotificationProvider, } from '@evoke-platform/context';
2
2
  import { ReactComponent } from '@formio/react';
3
- import dot from 'dot-object';
4
- import { cloneDeep } from 'lodash';
3
+ import { cloneDeep, isEmpty, isObject } from 'lodash';
5
4
  import React from 'react';
6
5
  import ReactDOM from 'react-dom';
7
6
  import { FormComponentWrapper } from '../../Common';
8
- import { getAllCriteriaInputs, updateCriteriaInputs } from '../../utils';
7
+ import { getCriteriaInputs, populateInstanceWithNestedData, updateCriteriaInputs } from '../../utils';
9
8
  import { DropdownRepeatableField } from './ManyToMany/DropdownRepeatableField';
10
9
  import RepeatableField from './RepeatableField';
11
10
  const apiBaseUrl = process.env.REACT_APP_API_ROOT || `${window.location.origin}/api`;
@@ -23,24 +22,43 @@ export class RepeatableFieldComponent extends ReactComponent {
23
22
  }
24
23
  init() {
25
24
  if (this.criteria) {
26
- const inputProps = getAllCriteriaInputs(this.criteria);
27
- const data = dot.dot(this.root._data);
25
+ const inputProps = getCriteriaInputs(this.criteria);
26
+ this.updatedCriteria = updateCriteriaInputs(this.updatedCriteria, this.root._data, this.component.user);
28
27
  for (const inputProp of inputProps) {
29
- // Parse data to update criteria when form is loaded.
30
- updateCriteriaInputs(this.updatedCriteria, inputProp, data[inputProp], true);
31
28
  // Parse data to update criteria when form field is updated
32
29
  // Need to parse all fields again.
33
30
  const compKeyFragments = inputProp.split('.');
34
31
  let compKey = compKeyFragments[0];
35
- if (['line1', 'line2', 'city', 'state', 'zipCode'].includes(compKeyFragments[1])) {
32
+ const property = this.component.properties.find((c) => c.id === compKey);
33
+ if (property?.type === 'address' &&
34
+ ['line1', 'line2', 'city', 'state', 'zipCode'].includes(compKeyFragments[1])) {
36
35
  compKey = inputProp;
37
36
  }
38
- this.on(`changed-${compKey}`, () => {
39
- const data = dot.dot(this.root._data);
40
- this.updatedCriteria = cloneDeep(this.criteria) ?? {};
41
- for (const inputProp of inputProps) {
42
- updateCriteriaInputs(this.updatedCriteria, inputProp, data[inputProp], true);
37
+ this.on(`changed-${compKey}`, async (value) => {
38
+ const data = this.root._data;
39
+ if (property?.type === 'object' && isObject(value) && 'id' in value && 'objectId' in value) {
40
+ const paths = this.component.allCriteriaInputs
41
+ .filter((input) => input.split('.')[0] === compKey)
42
+ .map((p) => p.split('.').slice(1).join('.'));
43
+ let instance = value;
44
+ if (!isEmpty(paths)) {
45
+ instance = await populateInstanceWithNestedData(value['id'], value['objectId'], paths, this.component.apiServices);
46
+ }
47
+ data[compKey] = instance;
43
48
  }
49
+ else {
50
+ if (compKey.includes('.')) {
51
+ const keyFragments = compKey.split('.');
52
+ if (!data[keyFragments[0]]) {
53
+ data[keyFragments[0]] = {};
54
+ }
55
+ data[keyFragments[0]][keyFragments[1]] = value;
56
+ }
57
+ else {
58
+ data[compKey] = value;
59
+ }
60
+ }
61
+ this.updatedCriteria = updateCriteriaInputs(this.criteria ?? {}, data, this.component.user);
44
62
  this.attachReact(this.element);
45
63
  });
46
64
  }
@@ -0,0 +1,112 @@
1
+ import { ApiServices } from '@evoke-platform/context';
2
+ import { render, screen, waitFor, within } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import axios from 'axios';
5
+ import { isEqual } from 'lodash';
6
+ import { http, HttpResponse } from 'msw';
7
+ import { setupServer } from 'msw/node';
8
+ import React from 'react';
9
+ import { it } from 'vitest';
10
+ import Form from '../Common/Form';
11
+ import { licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, } from './test-data';
12
+ const removePoppers = () => {
13
+ const portalSelectors = ['.MuiAutocomplete-popper'];
14
+ portalSelectors.forEach((selector) => {
15
+ // eslint-disable-next-line testing-library/no-node-access
16
+ document.querySelectorAll(selector).forEach((el) => el.remove());
17
+ });
18
+ };
19
+ describe('Form component', () => {
20
+ let server;
21
+ let apiServices;
22
+ beforeAll(() => {
23
+ server = setupServer(http.get('/data/objects/specialtyType/effective', () => HttpResponse.json(specialtyTypeObject)), http.get('/data/objects/license/effective', () => HttpResponse.json(licenseObject)), http.get('/data/objects/license/instances', () => {
24
+ return HttpResponse.json([rnLicense, npLicense]);
25
+ }), http.get('/data/objects/specialtyType/instances', (req) => {
26
+ const filter = new URL(req.request.url).searchParams.get('filter');
27
+ if (filter) {
28
+ const whereFilter = JSON.parse(filter).where;
29
+ // The two objects in the array of conditions in the "where" filter represent the potential filters that can be applied when retrieving "specialty" instances.
30
+ // The first object is for the the validation criteria, but it is empty if the "license" field, which is referenced in the validation criteria, hasn't been filled out yet.
31
+ // The second object is for the search criteria which the user enters in the "specialty" field, but it is empty if no search text has been entered.
32
+ if (isEqual(whereFilter, { and: [{}, {}] }))
33
+ return HttpResponse.json([
34
+ rnSpecialtyType1,
35
+ rnSpecialtyType2,
36
+ npSpecialtyType1,
37
+ npSpecialtyType2,
38
+ ]);
39
+ else if (isEqual(whereFilter, { and: [{ 'licenseType.id': 'rnLicenseType' }, {}] }))
40
+ return HttpResponse.json([rnSpecialtyType1, rnSpecialtyType2]);
41
+ else if (isEqual(whereFilter, { and: [{ 'licenseType.id': 'npLicenseType' }, {}] }))
42
+ return HttpResponse.json([npSpecialtyType1, npSpecialtyType2]);
43
+ }
44
+ }));
45
+ server.listen();
46
+ });
47
+ beforeEach(() => {
48
+ apiServices = new ApiServices(axios.create());
49
+ });
50
+ afterAll(() => {
51
+ server.close();
52
+ });
53
+ afterEach(() => {
54
+ server.resetHandlers();
55
+ removePoppers();
56
+ });
57
+ describe('validation criteria', () => {
58
+ it(`filters related object field with validation criteria that references a related object's nested data`, async () => {
59
+ const user = userEvent.setup();
60
+ server.use(http.get('/data/objects/license/instances/rnLicense', (req) => {
61
+ const expand = new URL(req.request.url).searchParams.get('expand');
62
+ if (expand === 'licenseType.id') {
63
+ return HttpResponse.json(rnLicense);
64
+ }
65
+ }));
66
+ render(React.createElement(Form, { actionId: '_create', actionType: 'create', object: specialtyObject, apiServices: apiServices }));
67
+ const license = await screen.findByRole('combobox', { name: 'License' });
68
+ // Validate that specialty type dropdown is rendering all options
69
+ let specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' });
70
+ await user.click(specialtyType);
71
+ let openAutocomplete = await screen.findByRole('listbox');
72
+ await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
73
+ await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #2' });
74
+ await within(openAutocomplete).findByRole('option', { name: 'NP Specialty Type #1' });
75
+ await within(openAutocomplete).findByRole('option', { name: 'NP Specialty Type #2' });
76
+ // Close the specialty type dropdown
77
+ removePoppers();
78
+ // Select a license from the dropdown
79
+ await user.click(license);
80
+ const rnLicenseOption = await screen.findByRole('option', { name: 'RN License' });
81
+ await user.click(rnLicenseOption);
82
+ // Validate that specialty type dropdown is only rendering specialty types that are associated with the selected license.
83
+ specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' });
84
+ await user.click(specialtyType);
85
+ openAutocomplete = await screen.findByRole('listbox');
86
+ await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
87
+ await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #2' });
88
+ await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #1' })).to.be.null);
89
+ await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #2' })).to.be.null);
90
+ });
91
+ it(`filters related object field with validation criteria that references a defaulted related object's nested data`, async () => {
92
+ const user = userEvent.setup();
93
+ server.use(http.get('/data/objects/license/instances/rnLicense', (req) => {
94
+ const expand = new URL(req.request.url).searchParams.get('expand');
95
+ if (expand === 'licenseType.id') {
96
+ return HttpResponse.json(rnLicense);
97
+ }
98
+ }));
99
+ render(React.createElement(Form, { actionId: '_create', actionType: 'create', object: specialtyObject, apiServices: apiServices, associatedObject: { propertyId: 'license', instanceId: 'rnLicense' } }));
100
+ // Validate that the license field is hidden
101
+ await waitFor(() => expect(screen.queryByRole('combobox', { name: 'License' })).to.be.null);
102
+ // Validate that specialty type dropdown is only rendering specialty types that are associated with the selected license.
103
+ const specialtyType = await screen.findByRole('combobox', { name: 'Specialty Type' });
104
+ await user.click(specialtyType);
105
+ const openAutocomplete = await screen.findByRole('listbox');
106
+ await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #1' });
107
+ await within(openAutocomplete).findByRole('option', { name: 'RN Specialty Type #2' });
108
+ await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #1' })).to.be.null);
109
+ await waitFor(() => expect(within(openAutocomplete).queryByRole('option', { name: 'NP Specialty Type #2' })).to.be.null);
110
+ });
111
+ });
112
+ });
@@ -0,0 +1,13 @@
1
+ import { Obj, ObjectInstance } from '@evoke-platform/context';
2
+ export declare const licenseObject: Obj;
3
+ export declare const licenseTypeObject: Obj;
4
+ export declare const specialtyObject: Obj;
5
+ export declare const specialtyTypeObject: Obj;
6
+ export declare const rnLicense: ObjectInstance;
7
+ export declare const npLicense: ObjectInstance;
8
+ export declare const rnLicenseType: ObjectInstance;
9
+ export declare const npLicesneType: ObjectInstance;
10
+ export declare const rnSpecialtyType1: ObjectInstance;
11
+ export declare const rnSpecialtyType2: ObjectInstance;
12
+ export declare const npSpecialtyType1: ObjectInstance;
13
+ export declare const npSpecialtyType2: ObjectInstance;
@@ -0,0 +1,282 @@
1
+ // Objects
2
+ export const licenseObject = {
3
+ id: 'license',
4
+ name: 'License',
5
+ properties: [
6
+ {
7
+ id: 'name',
8
+ name: 'License Number',
9
+ type: 'string',
10
+ },
11
+ {
12
+ id: 'status',
13
+ name: 'Status',
14
+ type: 'string',
15
+ enum: ['Active', 'Inactive'],
16
+ },
17
+ {
18
+ id: 'licenseType',
19
+ name: 'License Type',
20
+ type: 'object',
21
+ objectId: 'licenseType',
22
+ },
23
+ ],
24
+ actions: [
25
+ {
26
+ id: '_update',
27
+ name: 'Update',
28
+ type: 'update',
29
+ outputEvent: 'License Updated',
30
+ },
31
+ {
32
+ id: '_delete',
33
+ name: 'Delete',
34
+ type: 'delete',
35
+ outputEvent: 'License Deleted',
36
+ },
37
+ {
38
+ id: '_create',
39
+ name: 'Create',
40
+ type: 'create',
41
+ outputEvent: 'License Created',
42
+ },
43
+ ],
44
+ };
45
+ export const licenseTypeObject = {
46
+ id: 'licenseType',
47
+ name: 'License Type',
48
+ properties: [
49
+ {
50
+ id: 'name',
51
+ name: 'Name',
52
+ type: 'string',
53
+ },
54
+ {
55
+ id: 'licenseNumberPrefix',
56
+ name: 'License Number Prefix',
57
+ type: 'string',
58
+ },
59
+ ],
60
+ actions: [
61
+ {
62
+ id: '_update',
63
+ name: 'Update',
64
+ type: 'update',
65
+ outputEvent: 'License Type Updated',
66
+ },
67
+ {
68
+ id: '_delete',
69
+ name: 'Delete',
70
+ type: 'delete',
71
+ outputEvent: 'License Type Deleted',
72
+ },
73
+ {
74
+ id: '_create',
75
+ name: 'Create',
76
+ type: 'create',
77
+ outputEvent: 'License Type Created',
78
+ },
79
+ ],
80
+ };
81
+ export const specialtyObject = {
82
+ id: 'specialty',
83
+ name: 'Specialty',
84
+ properties: [
85
+ {
86
+ id: 'name',
87
+ name: 'Name',
88
+ type: 'string',
89
+ },
90
+ {
91
+ id: 'specialtyType',
92
+ name: 'Specialty Type',
93
+ type: 'object',
94
+ objectId: 'specialtyType',
95
+ },
96
+ {
97
+ id: 'license',
98
+ name: 'License',
99
+ type: 'object',
100
+ objectId: 'license',
101
+ },
102
+ ],
103
+ actions: [
104
+ {
105
+ id: '_update',
106
+ name: 'Update',
107
+ type: 'update',
108
+ outputEvent: 'Specialty Updated',
109
+ },
110
+ {
111
+ id: '_delete',
112
+ name: 'Delete',
113
+ type: 'delete',
114
+ outputEvent: 'Specialty Deleted',
115
+ },
116
+ {
117
+ id: '_create',
118
+ name: 'Create',
119
+ type: 'create',
120
+ outputEvent: 'Specialty Created',
121
+ parameters: [
122
+ {
123
+ id: 'name',
124
+ name: 'Name',
125
+ type: 'string',
126
+ },
127
+ {
128
+ id: 'specialtyType',
129
+ name: 'Specialty Type',
130
+ type: 'object',
131
+ objectId: 'specialtyType',
132
+ validation: {
133
+ criteria: {
134
+ $and: [{ 'licenseType.id': '{{{input.license.licenseType.id}}}' }],
135
+ },
136
+ },
137
+ },
138
+ {
139
+ id: 'license',
140
+ name: 'License',
141
+ type: 'object',
142
+ objectId: 'license',
143
+ },
144
+ ],
145
+ form: {
146
+ entries: [
147
+ {
148
+ parameterId: 'name',
149
+ type: 'input',
150
+ display: {
151
+ label: 'Name',
152
+ },
153
+ },
154
+ {
155
+ parameterId: 'specialtyType',
156
+ type: 'input',
157
+ display: {
158
+ label: 'Specialty Type',
159
+ relatedObjectDisplay: 'dropdown',
160
+ },
161
+ },
162
+ {
163
+ parameterId: 'license',
164
+ type: 'input',
165
+ display: {
166
+ label: 'License',
167
+ relatedObjectDisplay: 'dropdown',
168
+ },
169
+ },
170
+ ],
171
+ },
172
+ },
173
+ ],
174
+ };
175
+ export const specialtyTypeObject = {
176
+ id: 'specialtyType',
177
+ name: 'Specialty Type',
178
+ properties: [
179
+ {
180
+ id: 'name',
181
+ name: 'Name',
182
+ type: 'string',
183
+ },
184
+ {
185
+ id: 'licenseType',
186
+ name: 'License Type',
187
+ type: 'object',
188
+ objectId: 'licenseType',
189
+ },
190
+ ],
191
+ actions: [
192
+ {
193
+ id: '_update',
194
+ name: 'Update',
195
+ type: 'update',
196
+ outputEvent: 'Specialty Type Updated',
197
+ },
198
+ {
199
+ id: '_delete',
200
+ name: 'Delete',
201
+ type: 'delete',
202
+ outputEvent: 'Specialty Type Deleted',
203
+ },
204
+ {
205
+ id: '_create',
206
+ name: 'Create',
207
+ type: 'create',
208
+ outputEvent: 'Specialty Type Created',
209
+ },
210
+ ],
211
+ };
212
+ // Instances
213
+ export const rnLicense = {
214
+ id: 'rnLicense',
215
+ objectId: 'license',
216
+ name: 'RN License',
217
+ licenseType: {
218
+ id: 'rnLicenseType',
219
+ name: 'RN License Type',
220
+ },
221
+ status: 'Active',
222
+ };
223
+ export const npLicense = {
224
+ id: 'npLicense',
225
+ objectId: 'license',
226
+ name: 'NP License',
227
+ licenseType: {
228
+ id: 'npLicenseType',
229
+ name: 'NP License Type',
230
+ },
231
+ status: 'Active',
232
+ };
233
+ export const rnLicenseType = {
234
+ id: 'rnLicenseType',
235
+ objectId: 'licenseType',
236
+ name: 'RN License Type',
237
+ licenseNumberPrefix: 'RN',
238
+ };
239
+ export const npLicesneType = {
240
+ id: 'npLicenseType',
241
+ objectId: 'licenseType',
242
+ name: 'NP License Type',
243
+ licenseNumberPrefix: 'NP',
244
+ };
245
+ export const rnSpecialtyType1 = {
246
+ id: 'specialtyType1',
247
+ objectId: 'specialtyType',
248
+ name: 'RN Specialty Type #1',
249
+ int: 1,
250
+ licenseType: {
251
+ id: 'rnLicenseType',
252
+ name: 'RN License Type',
253
+ },
254
+ };
255
+ export const rnSpecialtyType2 = {
256
+ id: 'specialtyType2',
257
+ objectId: 'specialtyType',
258
+ name: 'RN Specialty Type #2',
259
+ int: 1,
260
+ licenseType: {
261
+ id: 'rnLicenseType',
262
+ name: 'RN License Type',
263
+ },
264
+ };
265
+ export const npSpecialtyType1 = {
266
+ id: 'specialtyType3',
267
+ objectId: 'specialtyType',
268
+ name: 'NP Specialty Type #1',
269
+ licenseType: {
270
+ id: 'npLicenseType',
271
+ name: 'NP License Type',
272
+ },
273
+ };
274
+ export const npSpecialtyType2 = {
275
+ id: 'specialtyType4',
276
+ objectId: 'specialtyType',
277
+ name: 'NP Specialty Type #2',
278
+ licenseType: {
279
+ id: 'npLicenseType',
280
+ name: 'NP License Type',
281
+ },
282
+ };
@@ -1,4 +1,4 @@
1
- import { ActionInput, ActionInputType, ApiServices, FormEntry, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, PropertyType } from '@evoke-platform/context';
1
+ import { ActionInput, ActionInputType, ApiServices, FormEntry, InputParameter, InputParameterReference, Obj, ObjectInstance, Property, PropertyType, UserAccount } from '@evoke-platform/context';
2
2
  import { ReactComponent } from '@formio/react';
3
3
  import { LocalDateTime } from '@js-joda/core';
4
4
  import { AutocompleteOption } from '../../core';
@@ -21,7 +21,11 @@ export declare function getMiddleObject(instance: ObjectInstance, property: Prop
21
21
  } | undefined;
22
22
  export declare function getMiddleInstance(instanceId: string, property: Property, middleObjectInstances: ObjectInstance[]): ObjectInstance | undefined;
23
23
  export declare function getPrefixedUrl(url: string): string;
24
- export declare function addObjectPropertiesToComponentProps(properties: Property[], formComponents: any[], instance?: ObjectInstance, objectPropertyInputProps?: ObjectPropertyInputProps, autoSave?: (data: Record<string, unknown>) => void, readOnly?: boolean, defaultPages?: Record<string, string>, navigateTo?: (path: string) => void, queryAddresses?: (query: string) => Promise<Address[]>, apiServices?: ApiServices, isModal?: boolean, fieldHeight?: 'small' | 'medium', richTextEditor?: typeof ReactComponent): Promise<ActionInput[]>;
24
+ export declare function flattenFormComponents(components?: ActionInput[]): ActionInput[];
25
+ export declare function addObjectPropertiesToComponentProps(properties: Property[], formComponents: any[], allCriteriaInputs?: string[], instance?: ObjectInstance, objectPropertyInputProps?: ObjectPropertyInputProps, associatedObject?: {
26
+ instanceId?: string;
27
+ propertyId?: string;
28
+ }, autoSave?: (data: Record<string, unknown>) => void, readOnly?: boolean, defaultPages?: Record<string, string>, navigateTo?: (path: string) => void, queryAddresses?: (query: string) => Promise<Address[]>, apiServices?: ApiServices, isModal?: boolean, fieldHeight?: 'small' | 'medium', richTextEditor?: typeof ReactComponent): Promise<ActionInput[]>;
25
29
  export declare function getDefaultValue(initialValue: unknown, selectOptions?: AutocompleteOption[]): unknown;
26
30
  export declare const buildComponentPropsFromObjectProperties: (properties: Property[], objectId: string, instance?: ObjectInstance, objectPropertyInputProps?: ObjectPropertyInputProps, hasActionPermissions?: boolean, autoSave?: ((data: Record<string, unknown>) => void) | undefined, readOnly?: boolean, queryAddresses?: ((query: string) => Promise<Address[]>) | undefined, isModal?: boolean, fieldHeight?: 'small' | 'medium', richTextEditor?: typeof ReactComponent) => unknown[];
27
31
  export declare const buildComponentPropsFromDocumentProperties: (documentProperties: [string, unknown][], readOnly?: boolean, autoSave?: ((data: Record<string, unknown>) => void) | undefined, fieldHeight?: 'small' | 'medium') => {
@@ -35,8 +39,10 @@ export declare const buildComponentPropsFromDocumentProperties: (documentPropert
35
39
  }[];
36
40
  export declare const OPERATOR_MAP: Record<string, string>;
37
41
  export declare function transformToWhere(mongoQuery: object): Record<string, unknown>;
38
- export declare function updateCriteriaInputs(criteria: Record<string, unknown>, field: string, fieldValue: string, isInputField?: boolean): void;
39
- export declare function getAllCriteriaInputs(criteria: Record<string, unknown>): string[];
42
+ export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount): Record<string, unknown>;
43
+ export declare function getCriteriaInputs(criteria?: Record<string, unknown>): string[];
44
+ export declare function getAllCriteriaInputs(components: ActionInput[] | InputParameter[]): string[];
45
+ export declare function populateInstanceWithNestedData(instanceId: string, objectId: string, paths: string[], apiServices: ApiServices): Promise<ObjectInstance>;
40
46
  export declare function isPropertyVisible(conditional: {
41
47
  when: string;
42
48
  show: boolean;
@@ -1,6 +1,10 @@
1
1
  import { camelCase, get, isArray, isEmpty, isNil, isObject, isUndefined, pick, startCase, transform, uniq, } from 'lodash';
2
2
  import { DateTime } from 'luxon';
3
3
  import { nanoid } from 'nanoid';
4
+ import Handlebars from 'no-eval-handlebars';
5
+ import qs from 'qs';
6
+ import { defaultRuleProcessorMongoDB, formatQuery, parseMongoDB, } from 'react-querybuilder';
7
+ import escape from 'string-escape-regex';
4
8
  // The following functions are used to build the FormIO form components from the form parameters.
5
9
  export function determineComponentType(properties, parameter) {
6
10
  if (!parameter)
@@ -163,6 +167,7 @@ export function convertFormToComponents(entries, parameters, object) {
163
167
  name: topLevelProperty?.name
164
168
  ? `${topLevelProperty.name} ${startCase(property.id.split('.')[1])}`
165
169
  : startCase(property.id.split('.')[1]),
170
+ type: topLevelProperty?.type,
166
171
  };
167
172
  }
168
173
  const type = determineComponentType(object.properties ?? [], parameter);
@@ -482,11 +487,26 @@ export function getPrefixedUrl(url) {
482
487
  console.error('Invalid URL');
483
488
  return url;
484
489
  }
490
+ export function flattenFormComponents(components) {
491
+ if (!components)
492
+ return [];
493
+ return components.reduce((acc, component) => {
494
+ if (component.type === 'Section') {
495
+ return acc.concat(component.components?.flatMap((components) => flattenFormComponents(components.components)) ?? []);
496
+ }
497
+ else if (component.type === 'Columns') {
498
+ return acc.concat(component.columns?.flatMap((column) => flattenFormComponents(column.components)) ?? []);
499
+ }
500
+ else {
501
+ return acc.concat(component);
502
+ }
503
+ }, []);
504
+ }
485
505
  // The following function adds the object properties to the form components.
486
506
  // This function is used when there is no form configured in the form builder.
487
507
  export async function addObjectPropertiesToComponentProps(properties,
488
508
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
489
- formComponents, instance, objectPropertyInputProps, autoSave, readOnly, defaultPages, navigateTo, queryAddresses, apiServices, isModal, fieldHeight, richTextEditor) {
509
+ formComponents, allCriteriaInputs, instance, objectPropertyInputProps, associatedObject, autoSave, readOnly, defaultPages, navigateTo, queryAddresses, apiServices, isModal, fieldHeight, richTextEditor) {
490
510
  return [
491
511
  ...(await Promise.all(formComponents
492
512
  ?.filter((component) => !isUndefined(component) && !isNil(component))
@@ -624,6 +644,8 @@ formComponents, instance, objectPropertyInputProps, autoSave, readOnly, defaultP
624
644
  ...(component.type === 'Object' && objectPropertyInputProps),
625
645
  defaultValue,
626
646
  property,
647
+ allCriteriaInputs,
648
+ properties,
627
649
  type: `${readOnly ? 'ViewOnly' : ''}${component.type}`,
628
650
  readOnly: component.readOnly || readOnly || property?.formula,
629
651
  multiple: ['array', 'document'].includes(property.type),
@@ -648,13 +670,24 @@ formComponents, instance, objectPropertyInputProps, autoSave, readOnly, defaultP
648
670
  : undefined,
649
671
  };
650
672
  }
651
- const defaultValue = getDefaultValue(isNil(instanceValue) ? component.initialValue : instanceValue, component?.data?.values);
673
+ let defaultValue = getDefaultValue(isNil(instanceValue) ? component.initialValue : instanceValue, component?.data?.values);
674
+ // If "associatedObject" is defined, that means the form is associating an existing instance with the current instance.
675
+ // Set the associated instance as a default value and hide the field.
676
+ if (associatedObject?.instanceId &&
677
+ associatedObject?.propertyId &&
678
+ associatedObject?.propertyId === property.id) {
679
+ defaultValue = { id: associatedObject.instanceId };
680
+ component.hidden = true;
681
+ }
652
682
  return {
653
683
  ...component,
654
684
  ...(component.type === 'Object' && objectPropertyInputProps),
655
685
  type: `${readOnly ? 'ViewOnly' : ''}${component.type}`,
656
686
  defaultValue,
687
+ allCriteriaInputs,
688
+ properties,
657
689
  property,
690
+ associatedObject,
658
691
  readOnly: component.readOnly || readOnly || property?.formula,
659
692
  multiple: ['array', 'document'].includes(property.type),
660
693
  instance: instance,
@@ -693,7 +726,7 @@ formComponents, instance, objectPropertyInputProps, autoSave, readOnly, defaultP
693
726
  }
694
727
  if (component.columns) {
695
728
  for (const column of component.columns) {
696
- column.components = await addObjectPropertiesToComponentProps(properties, column.components, instance, objectPropertyInputProps, autoSave, readOnly, undefined, undefined, queryAddresses, apiServices, isModal, fieldHeight, richTextEditor);
729
+ column.components = await addObjectPropertiesToComponentProps(properties, column.components, allCriteriaInputs, instance, objectPropertyInputProps, associatedObject, autoSave, readOnly, undefined, undefined, queryAddresses, apiServices, isModal, fieldHeight, richTextEditor);
697
730
  }
698
731
  return component;
699
732
  }
@@ -713,6 +746,14 @@ formComponents, instance, objectPropertyInputProps, autoSave, readOnly, defaultP
713
746
  if (item.type.includes('RepeatableField') && !!instance) {
714
747
  item.instance = instance;
715
748
  }
749
+ // If "associatedObject" is defined, that means the form is associating an existing instance with the current instance.
750
+ // Set the associated instance as a default value and hide the field.
751
+ if (associatedObject?.instanceId &&
752
+ associatedObject?.propertyId &&
753
+ item.property.id === associatedObject.propertyId) {
754
+ item.defaultValue = { id: associatedObject.instanceId };
755
+ item.hidden = true;
756
+ }
716
757
  }
717
758
  if (nestedFieldProperty) {
718
759
  item.type = `${readOnly ? 'ViewOnly' : ''}${item.type}`;
@@ -725,6 +766,7 @@ formComponents, instance, objectPropertyInputProps, autoSave, readOnly, defaultP
725
766
  item.user = objectPropertyInputProps?.user;
726
767
  item.defaultPages = defaultPages;
727
768
  item.navigateTo = navigateTo;
769
+ item.allCriteriaInputs = allCriteriaInputs;
728
770
  item.isModal = isModal;
729
771
  item.fieldHeight = fieldHeight;
730
772
  item.richTextEditor = ['RepeatableField', 'Object'].includes(item.type)
@@ -751,7 +793,7 @@ formComponents, instance, objectPropertyInputProps, autoSave, readOnly, defaultP
751
793
  }
752
794
  }
753
795
  return {
754
- components: await addObjectPropertiesToComponentProps(properties, component.components, instance, objectPropertyInputProps, autoSave, readOnly, defaultPages, navigateTo, queryAddresses, apiServices, isModal, fieldHeight, richTextEditor),
796
+ components: await addObjectPropertiesToComponentProps(properties, component.components, allCriteriaInputs, instance, objectPropertyInputProps, associatedObject, autoSave, readOnly, defaultPages, navigateTo, queryAddresses, apiServices, isModal, fieldHeight, richTextEditor),
755
797
  ...component,
756
798
  ...(component.type === 'Object' && objectPropertyInputProps),
757
799
  type: `${readOnly ? 'ViewOnly' : ''}${component.type}`,
@@ -1127,61 +1169,100 @@ export function transformToWhere(mongoQuery) {
1127
1169
  result[newKey] = isObject(value) ? transformToWhere(value) : value;
1128
1170
  });
1129
1171
  }
1130
- export function updateCriteriaInputs(criteria, field, fieldValue, isInputField) {
1131
- for (const [key, value] of Object.entries(criteria)) {
1132
- if (isArray(value)) {
1133
- for (const index in value) {
1134
- if (isObject(value[index])) {
1135
- updateCriteriaInputs(value[index], field, fieldValue, isInputField);
1136
- }
1137
- else {
1138
- value[index] =
1139
- typeof value[index] === 'string'
1140
- ? value[index]
1141
- ?.replaceAll(!isInputField ? `{{{${field}}}}` : `{{{input.${field}}}}`, fieldValue ?? '')
1142
- .trim() || undefined
1143
- : value ?? undefined;
1144
- }
1145
- }
1146
- }
1147
- else if (isObject(value)) {
1148
- updateCriteriaInputs(value, field, fieldValue, isInputField);
1172
+ function compileQueryValues(query, data) {
1173
+ if ('rules' in query && Array.isArray(query.rules)) {
1174
+ const updatedRules = query.rules
1175
+ .map((item) => compileQueryValues(item, data))
1176
+ .filter((item) => item !== undefined);
1177
+ if (updatedRules?.length) {
1178
+ return {
1179
+ ...query,
1180
+ rules: updatedRules,
1181
+ };
1149
1182
  }
1150
1183
  else {
1151
- criteria[key] =
1152
- typeof value === 'string'
1153
- ? value
1154
- ?.replaceAll(!isInputField ? `{{{${field}}}}` : `{{{input.${field}}}}`, fieldValue ?? '')
1155
- .trim() || undefined
1156
- : value ?? undefined;
1184
+ return undefined;
1157
1185
  }
1158
1186
  }
1159
- }
1160
- export function getAllCriteriaInputs(criteria) {
1161
- const result = [];
1162
- for (const [, value] of Object.entries(criteria)) {
1163
- if (isArray(value)) {
1164
- for (const item of value) {
1165
- if (item && typeof item === 'object') {
1166
- const inputProps = getAllCriteriaInputs(item);
1167
- inputProps && result.push(...inputProps);
1168
- }
1169
- else {
1170
- const inputProps = typeof item === 'string' ? item.match(/{{{input\..*}}}/g) : undefined;
1171
- inputProps && result.push(...inputProps);
1172
- }
1187
+ else {
1188
+ if ('value' in query && typeof query.value === 'string') {
1189
+ const template = Handlebars.compileAST(query.value);
1190
+ const result = template(data);
1191
+ if (result !== '') {
1192
+ return {
1193
+ ...query,
1194
+ value: result,
1195
+ };
1196
+ }
1197
+ else {
1198
+ return undefined;
1173
1199
  }
1174
1200
  }
1175
- else if (value && typeof value === 'object') {
1176
- const inputProps = getAllCriteriaInputs(value);
1177
- inputProps && result.push(...inputProps);
1178
- }
1179
- else {
1180
- const inputProps = typeof value === 'string' ? value.match(/{{{input\..*}}}/g) : undefined;
1181
- inputProps && result.push(...inputProps);
1182
- }
1201
+ return query;
1202
+ }
1203
+ }
1204
+ export function updateCriteriaInputs(criteria, data, user) {
1205
+ const dataSet = {
1206
+ input: {
1207
+ ...data,
1208
+ },
1209
+ user: user,
1210
+ };
1211
+ const compiledQuery = compileQueryValues(parseMongoDB(criteria), dataSet);
1212
+ // The "compiledQueryValues" function filters out rules that have a value of "undefined".
1213
+ // If "compiledQuery" is "undefined", that means that all rules and nested rules have a value of "undefined".
1214
+ // Therefore, the resulting query is empty.
1215
+ return !compiledQuery
1216
+ ? {}
1217
+ : JSON.parse(formatQuery(compiledQuery, {
1218
+ format: 'mongodb',
1219
+ ruleProcessor: (rule, options) => {
1220
+ let updatedRule = rule;
1221
+ if (['contains', 'beginsWith', 'endsWith'].includes(rule.operator)) {
1222
+ updatedRule = { ...rule, value: escape(rule.value) };
1223
+ }
1224
+ return defaultRuleProcessorMongoDB(updatedRule, options);
1225
+ },
1226
+ }));
1227
+ }
1228
+ function getRuleValues(query) {
1229
+ const values = [];
1230
+ if ('rules' in query && Array.isArray(query.rules)) {
1231
+ query.rules.forEach((item) => {
1232
+ values.push(...getRuleValues(item));
1233
+ });
1234
+ }
1235
+ else {
1236
+ values.push(query.value);
1237
+ }
1238
+ return values;
1239
+ }
1240
+ export function getCriteriaInputs(criteria) {
1241
+ if (!criteria)
1242
+ return [];
1243
+ const values = getRuleValues(parseMongoDB(criteria)).filter((val) => typeof val === 'string' && val.match(/{{{input\..*}}}/g));
1244
+ return uniq(values).map((item) => item.replace('{{{input.', '').replace('}}}', ''));
1245
+ }
1246
+ export function getAllCriteriaInputs(components) {
1247
+ const allComponents = components;
1248
+ const results = allComponents.reduce((inputs, c) => inputs.concat(getCriteriaInputs(c.validate?.criteria ??
1249
+ c.validation?.criteria)), []);
1250
+ return results;
1251
+ }
1252
+ export async function populateInstanceWithNestedData(instanceId, objectId, paths, apiServices) {
1253
+ let instance = { id: instanceId, objectId };
1254
+ try {
1255
+ instance = await apiServices.get(getPrefixedUrl(`/objects/${objectId}/instances/${instanceId}`), {
1256
+ params: {
1257
+ expand: paths,
1258
+ },
1259
+ paramsSerializer: (params) => qs.stringify(params, { arrayFormat: 'repeat' }),
1260
+ });
1261
+ }
1262
+ catch (err) {
1263
+ console.log(err);
1183
1264
  }
1184
- return uniq(result.map((item) => item.replace('{{{input.', '').replace('}}}', '')));
1265
+ return instance;
1185
1266
  }
1186
1267
  export function isPropertyVisible(conditional, formData) {
1187
1268
  const isConditional = !!conditional?.when;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.4.0-testing.1",
3
+ "version": "1.4.0-testing.2",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",
@@ -51,7 +51,6 @@
51
51
  "@testing-library/jest-dom": "^6.4.2",
52
52
  "@testing-library/react": "^14.2.2",
53
53
  "@testing-library/user-event": "^14.5.2",
54
- "@types/dot-object": "^2.1.6",
55
54
  "@types/flat": "^5.0.5",
56
55
  "@types/jest": "^28.1.4",
57
56
  "@types/luxon": "^3.4.2",
@@ -111,12 +110,12 @@
111
110
  "devexpress-richedit": "^23.1.5",
112
111
  "devextreme": "^23.1.5",
113
112
  "devextreme-dist": "^23.1.5",
114
- "dot-object": "^2.1.5",
115
113
  "eslint-plugin-no-inline-styles": "^1.0.5",
116
114
  "flat": "^6.0.1",
117
115
  "formiojs": "^4.15.0-rc.23",
118
116
  "html-react-parser": "^5.1.18",
119
117
  "luxon": "^2.5.2",
118
+ "msw": "^2.7.3",
120
119
  "nanoid": "^5.0.8",
121
120
  "nanoid-dictionary": "^4.3.0",
122
121
  "no-eval-handlebars": "^1.0.2",