@evoke-platform/ui-components 1.3.0-testing.0 → 1.4.0-dev.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 (42) hide show
  1. package/dist/published/components/core/Alert/Alert.js +1 -1
  2. package/dist/published/components/core/Autocomplete/Autocomplete.js +3 -3
  3. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +2 -18
  4. package/dist/published/components/custom/CriteriaBuilder/PropertyTree.js +2 -2
  5. package/dist/published/components/custom/CriteriaBuilder/ValueEditor.js +23 -8
  6. package/dist/published/components/custom/CriteriaBuilder/index.d.ts +2 -1
  7. package/dist/published/components/custom/CriteriaBuilder/index.js +2 -1
  8. package/dist/published/components/custom/CriteriaBuilder/utils.d.ts +13 -0
  9. package/dist/published/components/custom/CriteriaBuilder/utils.js +58 -1
  10. package/dist/published/components/custom/Form/Common/Form.js +37 -26
  11. package/dist/published/components/custom/Form/Common/FormComponentWrapper.js +2 -1
  12. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectComponent.d.ts +2 -0
  13. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectComponent.js +75 -26
  14. package/dist/published/components/custom/Form/FormComponents/ObjectComponent/ObjectPropertyInput.js +3 -2
  15. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/ActionDialog.d.ts +5 -2
  16. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/ActionDialog.js +4 -6
  17. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +4 -2
  18. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableFieldComponent.js +31 -13
  19. package/dist/published/components/custom/Form/tests/Form.test.d.ts +1 -0
  20. package/dist/published/components/custom/Form/tests/Form.test.js +158 -0
  21. package/dist/published/components/custom/Form/tests/test-data.d.ts +13 -0
  22. package/dist/published/components/custom/Form/tests/test-data.js +381 -0
  23. package/dist/published/components/custom/Form/utils.d.ts +10 -4
  24. package/dist/published/components/custom/Form/utils.js +206 -90
  25. package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +6 -2
  26. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -2
  27. package/dist/published/components/custom/HistoryLog/DisplayedProperty.d.ts +2 -1
  28. package/dist/published/components/custom/HistoryLog/DisplayedProperty.js +5 -2
  29. package/dist/published/components/custom/HistoryLog/HistoryData.d.ts +1 -0
  30. package/dist/published/components/custom/HistoryLog/HistoryData.js +9 -3
  31. package/dist/published/components/custom/HistoryLog/index.js +24 -2
  32. package/dist/published/components/custom/Menubar/Menubar.d.ts +2 -3
  33. package/dist/published/components/custom/Menubar/Menubar.js +15 -12
  34. package/dist/published/components/custom/index.d.ts +1 -1
  35. package/dist/published/components/custom/index.js +1 -1
  36. package/dist/published/index.d.ts +3 -2
  37. package/dist/published/index.js +3 -2
  38. package/dist/published/theme/hooks.d.ts +31 -0
  39. package/dist/published/theme/hooks.js +35 -0
  40. package/dist/published/theme/index.d.ts +3 -0
  41. package/dist/published/theme/index.js +3 -0
  42. package/package.json +4 -5
@@ -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' } })))))));
@@ -402,10 +402,12 @@ const RepeatableField = (props) => {
402
402
  React.createElement(Tooltip, { title: "Delete" },
403
403
  React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } })))))))))))),
404
404
  hasCreateAction && (React.createElement(Button, { variant: "contained", sx: styles.addButton, onClick: addRow }, "Add"))),
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,
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,158 @@
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
+ describe('visibility configuration', () => {
113
+ it('shows fields based on instance data using JsonLogic', async () => {
114
+ server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
115
+ render(React.createElement(Form, { actionId: 'jsonLogicDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
116
+ id: '123',
117
+ objectId: 'specialty',
118
+ name: 'Test Specialty Object Instance',
119
+ } }));
120
+ // Validate that specialty type dropdown renders
121
+ await screen.findByRole('combobox', { name: 'Specialty Type' });
122
+ });
123
+ it('hides fields based on instance data using JsonLogic', async () => {
124
+ server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
125
+ render(React.createElement(Form, { actionId: 'jsonLogicDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
126
+ id: '123',
127
+ objectId: 'specialty',
128
+ name: 'Test Specialty Object Instance -- hidden',
129
+ } }));
130
+ // Validate that license dropdown renders
131
+ await screen.findByRole('combobox', { name: 'License' });
132
+ // Validate that specialty type dropdown does not render
133
+ expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).to.be.null;
134
+ });
135
+ it('shows fields based on instance data using simple conditions', async () => {
136
+ server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
137
+ render(React.createElement(Form, { actionId: 'simpleConditionDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
138
+ id: '123',
139
+ objectId: 'specialty',
140
+ name: 'Test Specialty Object Instance',
141
+ } }));
142
+ // Validate that specialty type dropdown renders
143
+ await screen.findByRole('combobox', { name: 'Specialty Type' });
144
+ });
145
+ it('hides fields based on instance data using simple conditions', async () => {
146
+ server.use(http.get('/data/objects/license/instances/rnLicense', () => HttpResponse.json(rnLicense)));
147
+ render(React.createElement(Form, { actionId: 'simpleConditionDisplayTest', actionType: 'update', object: specialtyObject, apiServices: apiServices, instance: {
148
+ id: '123',
149
+ objectId: 'specialty',
150
+ name: 'Test Specialty Object Instance -- hidden',
151
+ } }));
152
+ // Validate that license dropdown renders
153
+ await screen.findByRole('combobox', { name: 'License' });
154
+ // Validate that specialty type dropdown does not render
155
+ expect(screen.queryByRole('combobox', { name: 'Specialty Type' })).to.be.null;
156
+ });
157
+ });
158
+ });
@@ -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;