@evoke-platform/ui-components 1.10.0-testing.9 → 1.10.1-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 (78) hide show
  1. package/dist/published/components/core/Autocomplete/Autocomplete.js +4 -2
  2. package/dist/published/components/core/Autocomplete/Autocomplete.test.js +112 -3
  3. package/dist/published/components/core/TextField/TextField.js +1 -1
  4. package/dist/published/components/core/TextField/TextField.test.js +0 -2
  5. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +24 -2
  6. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.test.js +45 -2
  7. package/dist/published/components/custom/Form/FormComponents/DocumentComponent/Document.js +2 -1
  8. package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +1 -1
  9. package/dist/published/components/custom/Form/tests/Form.test.js +0 -2
  10. package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +36 -7
  11. package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +14 -1
  12. package/dist/published/components/custom/FormField/FormField.d.ts +3 -1
  13. package/dist/published/components/custom/FormField/FormField.js +17 -5
  14. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -4
  15. package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.test.js +0 -2
  16. package/dist/published/components/custom/FormField/Select/Select.test.js +0 -2
  17. package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +14 -1
  18. package/dist/published/components/custom/FormV2/FormRenderer.d.ts +2 -1
  19. package/dist/published/components/custom/FormV2/FormRenderer.js +46 -8
  20. package/dist/published/components/custom/FormV2/FormRendererContainer.js +178 -153
  21. package/dist/published/components/custom/FormV2/components/AccordionSections.js +7 -2
  22. package/dist/published/components/custom/FormV2/components/Body.d.ts +1 -1
  23. package/dist/published/components/custom/FormV2/components/DefaultValues.d.ts +2 -2
  24. package/dist/published/components/custom/FormV2/components/DefaultValues.js +36 -28
  25. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  26. package/dist/published/components/custom/FormV2/components/Footer.d.ts +1 -0
  27. package/dist/published/components/custom/FormV2/components/Footer.js +8 -5
  28. package/dist/published/components/custom/FormV2/components/FormContext.d.ts +3 -2
  29. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.d.ts +9 -0
  30. package/dist/published/components/custom/FormV2/components/FormFieldTypes/AddressFields.js +32 -15
  31. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/ActionDialog.js +2 -2
  32. package/dist/published/components/custom/FormV2/components/FormFieldTypes/CollectionFiles/RepeatableField.js +9 -23
  33. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Criteria.js +16 -3
  34. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/Document.js +22 -4
  35. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.d.ts +2 -1
  36. package/dist/published/components/custom/FormV2/components/FormFieldTypes/DocumentFiles/DocumentList.js +16 -3
  37. package/dist/published/components/custom/FormV2/components/FormFieldTypes/Image.js +31 -5
  38. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +15 -3
  39. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +127 -92
  40. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +2 -3
  41. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +43 -20
  42. package/dist/published/components/custom/FormV2/components/Header.d.ts +5 -3
  43. package/dist/published/components/custom/FormV2/components/Header.js +47 -9
  44. package/dist/published/components/custom/FormV2/components/PropertyProtection.d.ts +16 -0
  45. package/dist/published/components/custom/FormV2/components/PropertyProtection.js +113 -0
  46. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +47 -24
  47. package/dist/published/components/custom/FormV2/components/ValidationFiles/ValidationErrors.js +1 -1
  48. package/dist/published/components/custom/FormV2/components/types.d.ts +2 -0
  49. package/dist/published/components/custom/FormV2/components/utils.d.ts +6 -4
  50. package/dist/published/components/custom/FormV2/components/utils.js +83 -13
  51. package/dist/published/components/custom/FormV2/tests/FormRenderer.test.js +411 -44
  52. package/dist/published/components/custom/FormV2/tests/FormRendererContainer.test.js +983 -16
  53. package/dist/published/components/custom/FormV2/tests/test-data.d.ts +1 -0
  54. package/dist/published/components/custom/FormV2/tests/test-data.js +138 -0
  55. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.d.ts +3 -0
  56. package/dist/published/components/custom/ViewDetailsV2/InstanceEntryRenderer.js +165 -0
  57. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.d.ts +13 -0
  58. package/dist/published/components/custom/ViewDetailsV2/ViewDetailsV2Container.js +144 -0
  59. package/dist/published/components/custom/ViewDetailsV2/index.d.ts +3 -0
  60. package/dist/published/components/custom/ViewDetailsV2/index.js +2 -0
  61. package/dist/published/components/custom/index.d.ts +2 -0
  62. package/dist/published/components/custom/index.js +1 -0
  63. package/dist/published/index.d.ts +6 -6
  64. package/dist/published/index.js +1 -1
  65. package/dist/published/stories/CriteriaBuilder.stories.js +6 -0
  66. package/dist/published/stories/FormRenderer.stories.d.ts +8 -4
  67. package/dist/published/stories/FormRendererContainer.stories.d.ts +26 -0
  68. package/dist/published/stories/FormRendererContainer.stories.js +5 -0
  69. package/dist/published/stories/FormRendererData.d.ts +12 -0
  70. package/dist/published/stories/FormRendererData.js +26 -1
  71. package/dist/published/stories/ViewDetailsV2Container.stories.d.ts +26 -0
  72. package/dist/published/stories/ViewDetailsV2Container.stories.js +37 -0
  73. package/dist/published/stories/ViewDetailsV2Data.d.ts +4 -0
  74. package/dist/published/stories/ViewDetailsV2Data.js +203 -0
  75. package/dist/published/stories/sharedMswHandlers.js +49 -10
  76. package/dist/published/theme/hooks.d.ts +4 -3
  77. package/dist/published/types.d.ts +3 -0
  78. package/package.json +10 -8
@@ -1,5 +1,6 @@
1
1
  import { ExpandMore, InfoRounded } from '@mui/icons-material';
2
2
  import { InputLabel, Autocomplete as MUIAutocomplete } from '@mui/material';
3
+ import { omit } from 'lodash';
3
4
  import React from 'react';
4
5
  import UIThemeProvider from '../../../theme';
5
6
  import FieldError from '../FieldError';
@@ -37,6 +38,7 @@ const Autocomplete = (props) => {
37
38
  return props.instructionText;
38
39
  }
39
40
  };
41
+ const muiAutocompleteProps = omit(props, 'sortBy', 'error', 'errorMessage', 'labelPlacement', 'instructionText');
40
42
  if (!!props.label && props.labelPlacement === 'outside-top') {
41
43
  return (React.createElement(UIThemeProvider, null,
42
44
  React.createElement(InputLabel, { htmlFor: props.id ?? '', sx: { display: 'flex', paddingBottom: '0px', fontSize: '14px' } },
@@ -50,7 +52,7 @@ const Autocomplete = (props) => {
50
52
  color: (theme) => theme.palette.text.secondary,
51
53
  } })))),
52
54
  renderInstructionText(),
53
- React.createElement(MUIAutocomplete, { ...props, sx: {
55
+ React.createElement(MUIAutocomplete, { ...muiAutocompleteProps, sx: {
54
56
  '& fieldset': { borderRadius: '8px', borderColor: props.error ? 'red' : undefined },
55
57
  '& .MuiOutlinedInput-notchedOutline': {
56
58
  border: props.readOnly ? 'none' : 'auto',
@@ -67,7 +69,7 @@ const Autocomplete = (props) => {
67
69
  }
68
70
  else {
69
71
  return (React.createElement(UIThemeProvider, null,
70
- React.createElement(MUIAutocomplete, { ...props, sx: {
72
+ React.createElement(MUIAutocomplete, { ...muiAutocompleteProps, sx: {
71
73
  '& fieldset': { borderRadius: '8px', borderColor: props.error ? 'red' : undefined },
72
74
  '& .MuiOutlinedInput-notchedOutline': {
73
75
  border: props.readOnly ? 'none' : 'auto',
@@ -1,11 +1,120 @@
1
1
  import { render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
2
3
  import React from 'react';
3
4
  import { it } from 'vitest';
4
5
  import TextField from '../TextField';
5
6
  import Autocomplete from './Autocomplete';
6
- const renderInput = (params) => React.createElement(TextField, { ...params });
7
+ const renderInputFactory = (label) => (params) => React.createElement(TextField, { ...params, label: label });
7
8
  const options = [];
8
- it('renders with label outside outline', () => {
9
- render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", renderInput: renderInput, options: options }));
9
+ it('should render with label outside outline', () => {
10
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", renderInput: renderInputFactory(), options: options }));
10
11
  screen.getByRole('combobox', { name: 'Title' });
11
12
  });
13
+ it('should render label outside outline with asterisk when required', () => {
14
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", renderInput: renderInputFactory(), options: options, required: true }));
15
+ screen.getByRole('combobox', { name: 'Title *' });
16
+ });
17
+ it('should render instruction text when provided when there is an outside-top label', () => {
18
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", instructionText: "This is an instruction", renderInput: renderInputFactory(), options: options }));
19
+ screen.getByText('This is an instruction');
20
+ });
21
+ it('should not render instruction text when there is not an outside-top label', () => {
22
+ render(React.createElement(Autocomplete, { id: "testinput", instructionText: "This is an instruction", renderInput: renderInputFactory(), options: options }));
23
+ const instructionText = screen.queryByText('This is an instruction');
24
+ expect(instructionText).not.toBeInTheDocument();
25
+ });
26
+ it('should render a tooltip if one is provided and there is an outside-top label', async () => {
27
+ const user = userEvent.setup();
28
+ render(React.createElement(Autocomplete, { id: "testinput", labelPlacement: "outside-top", label: "Title", tooltip: "This is a tooltip", renderInput: renderInputFactory(), options: options }));
29
+ expect(screen.queryByText('This is a tooltip')).not.toBeInTheDocument();
30
+ await user.hover(screen.getByLabelText('This is a tooltip'));
31
+ await screen.findByText('This is a tooltip');
32
+ });
33
+ it('should sort options in ascending order if sortBy is ASC', async () => {
34
+ const user = userEvent.setup();
35
+ const unsortedOptions = [
36
+ { label: 'Banana', value: 1 },
37
+ { label: 'Apple', value: 2 },
38
+ { label: 'Cherry', value: 3 },
39
+ ];
40
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "ASC", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
41
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
42
+ await user.click(comboBox);
43
+ const options = screen.getAllByRole('option');
44
+ const labels = options.map((option) => option.textContent);
45
+ expect(labels).toEqual(['Apple', 'Banana', 'Cherry']);
46
+ });
47
+ it('should sort options in descending order if sortBy is DESC', async () => {
48
+ const user = userEvent.setup();
49
+ const unsortedOptions = [
50
+ { label: 'Banana', value: 1 },
51
+ { label: 'Apple', value: 2 },
52
+ { label: 'Cherry', value: 3 },
53
+ ];
54
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "DESC", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
55
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
56
+ await user.click(comboBox);
57
+ const options = screen.getAllByRole('option');
58
+ const labels = options.map((option) => option.textContent);
59
+ expect(labels).toEqual(['Cherry', 'Banana', 'Apple']);
60
+ });
61
+ it('should not sort options if sortBy is NONE', async () => {
62
+ const user = userEvent.setup();
63
+ const unsortedOptions = [
64
+ { label: 'Banana', value: 1 },
65
+ { label: 'Apple', value: 2 },
66
+ { label: 'Cherry', value: 3 },
67
+ ];
68
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "NONE", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
69
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
70
+ await user.click(comboBox);
71
+ const options = screen.getAllByRole('option');
72
+ const labels = options.map((option) => option.textContent);
73
+ expect(labels).toEqual(['Banana', 'Apple', 'Cherry']);
74
+ });
75
+ it('should sort options in ascending order by default', async () => {
76
+ const user = userEvent.setup();
77
+ const unsortedOptions = [
78
+ { label: 'Banana', value: 1 },
79
+ { label: 'Apple', value: 2 },
80
+ { label: 'Cherry', value: 3 },
81
+ ];
82
+ render(React.createElement(Autocomplete, { id: "testinput", renderInput: renderInputFactory('Fruits'), options: unsortedOptions }));
83
+ const comboBox = screen.getByRole('combobox', { name: 'Fruits' });
84
+ await user.click(comboBox);
85
+ const options = screen.getAllByRole('option');
86
+ const labels = options.map((option) => option.textContent);
87
+ expect(labels).toEqual(['Apple', 'Banana', 'Cherry']);
88
+ });
89
+ it('should sort string options', async () => {
90
+ const user = userEvent.setup();
91
+ const unsortedOptions = ['Banana', 'Apple', 'Cherry'];
92
+ render(React.createElement(Autocomplete, { id: "testinput", sortBy: "ASC", renderInput: renderInputFactory('Fruit Options'), options: unsortedOptions }));
93
+ const comboBox = screen.getByRole('combobox', { name: 'Fruit Options' });
94
+ await user.click(comboBox);
95
+ const options = screen.getAllByRole('option');
96
+ const labels = options.map((option) => option.textContent);
97
+ expect(labels).toEqual(['Apple', 'Banana', 'Cherry']);
98
+ });
99
+ it('should render an error when error prop is true', () => {
100
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", error: true, errorMessage: "This is an error", labelPlacement: "outside-top", renderInput: renderInputFactory(), options: options }));
101
+ screen.getByText('This is an error');
102
+ });
103
+ it('should show popupIcon when not disabled', () => {
104
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", disabled: false, renderInput: renderInputFactory(), options: options }));
105
+ screen.getByTestId('ExpandMoreIcon');
106
+ });
107
+ it('should hide popupIcon when disabled', () => {
108
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", disabled: true, renderInput: renderInputFactory(), options: options }));
109
+ const popupIcon = screen.queryByTestId('ExpandMoreIcon');
110
+ expect(popupIcon).not.toBeInTheDocument();
111
+ });
112
+ it('should hide popupIcon when readOnly', () => {
113
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", readOnly: true, renderInput: renderInputFactory(), options: options }));
114
+ const popupIcon = screen.queryByTestId('ExpandMoreIcon');
115
+ expect(popupIcon).not.toBeInTheDocument();
116
+ });
117
+ it('should show custom popupIcon when provided', () => {
118
+ render(React.createElement(Autocomplete, { id: "testinput", label: "Title", popupIcon: React.createElement("span", { "data-testid": "custom-icon" }, "^"), renderInput: renderInputFactory(), options: options }));
119
+ screen.getByTestId('custom-icon');
120
+ });
@@ -36,7 +36,7 @@ const TextField = (props) => {
36
36
  ...props.sx,
37
37
  } }),
38
38
  error && React.createElement(FieldError, { required: required, label: errorMessage }))) : (React.createElement(React.Fragment, null,
39
- React.createElement(MUITextField, { inputProps: { readOnly: readOnly, 'aria-readonly': !!readOnly, 'data-testid': 'label-inside' }, ...props, sx: readOnly
39
+ React.createElement(MUITextField, { inputProps: { readOnly: readOnly, 'aria-readonly': !!readOnly, 'data-testid': 'label-inside' }, ...muiProps, sx: readOnly
40
40
  ? { ...readOnlyStyles, ...props.sx }
41
41
  : { '& fieldset': { borderRadius: '8px' }, ...props.sx } }),
42
42
  error && React.createElement(FieldError, { required: required, label: errorMessage })))));
@@ -1,9 +1,7 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import React from 'react';
4
3
  import { expect, it } from 'vitest';
5
4
  import TextField from './index';
6
- expect.extend(matchers);
7
5
  it('render TextField and check for data-testid === label-outside when labelPlacement === outside-top && variant === outlined', () => {
8
6
  render(React.createElement(TextField, { id: "testinput", labelPlacement: "outside-top", variant: "outlined", label: "Title" }));
9
7
  const textField = screen.getByRole('textbox', { name: 'Title' });
@@ -400,9 +400,31 @@ const CriteriaBuilder = (props) => {
400
400
  const fields = useMemo(() => {
401
401
  return properties
402
402
  .filter(({ type }) => type !== 'collection')
403
- .map((property) => {
403
+ .flatMap((property) => {
404
+ if (property.type === 'object') {
405
+ const result = [
406
+ {
407
+ name: `${property.id}.id`,
408
+ label: `${property.name} ID`,
409
+ inputType: property.type,
410
+ },
411
+ {
412
+ name: `${property.id}.name`,
413
+ label: `${property.name} Name`,
414
+ inputType: property.type,
415
+ },
416
+ ];
417
+ if (!property.objectId) {
418
+ result.push({
419
+ name: `${property.id}.objectId`,
420
+ label: `${property.name} Object ID`,
421
+ inputType: property.type,
422
+ });
423
+ }
424
+ return result;
425
+ }
404
426
  return {
405
- name: property.type === 'object' ? `${property.id}.id` : property.id,
427
+ name: property.id,
406
428
  label: property.name,
407
429
  inputType: property.type,
408
430
  ...(property.enum && {
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { expect, it } from 'vitest';
6
5
  import CriteriaBuilder from './CriteriaBuilder';
7
- expect.extend(matchers);
8
6
  const mockProperties = [
9
7
  {
10
8
  id: 'name',
@@ -58,6 +56,17 @@ const mockProperties = [
58
56
  name: 'Boolean',
59
57
  type: 'boolean',
60
58
  },
59
+ {
60
+ id: 'regularRelatedObject',
61
+ name: 'Regular Related Object',
62
+ type: 'object',
63
+ objectId: 'relatedObjectId',
64
+ },
65
+ {
66
+ id: 'dynamicRelatedObject',
67
+ name: 'Dynamic Related Object',
68
+ type: 'object',
69
+ },
61
70
  ];
62
71
  describe('CriteriaBuilder', () => {
63
72
  // Mock function for setCriteria
@@ -66,6 +75,40 @@ describe('CriteriaBuilder', () => {
66
75
  // Reset the mock before each test
67
76
  setCriteriaMock.mockReset();
68
77
  });
78
+ describe('when passed regular related object fields', () => {
79
+ it('should render the field ID', () => {
80
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
81
+ 'regularRelatedObject.id': 'relatedInstanceId',
82
+ }, setCriteria: setCriteriaMock }));
83
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Regular Related Object ID');
84
+ });
85
+ it('should render the field Name', () => {
86
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
87
+ 'regularRelatedObject.name': 'relatedInstanceName',
88
+ }, setCriteria: setCriteriaMock }));
89
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Regular Related Object Name');
90
+ });
91
+ });
92
+ describe('when passed dynamic related object fields', () => {
93
+ it('should render the field ID', () => {
94
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
95
+ 'dynamicRelatedObject.id': 'relatedInstanceId',
96
+ }, setCriteria: setCriteriaMock }));
97
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Dynamic Related Object ID');
98
+ });
99
+ it('should render the field Name', () => {
100
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
101
+ 'dynamicRelatedObject.name': 'relatedInstanceName',
102
+ }, setCriteria: setCriteriaMock }));
103
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Dynamic Related Object Name');
104
+ });
105
+ it('should render the field Object ID', () => {
106
+ render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
107
+ 'dynamicRelatedObject.objectId': 'relatedInstanceObjectId',
108
+ }, setCriteria: setCriteriaMock }));
109
+ expect(screen.getByRole('combobox', { name: /select property/i })).toHaveValue('Dynamic Related Object Object ID');
110
+ });
111
+ });
69
112
  describe('when passed single-select fields', () => {
70
113
  it('should render the field name', () => {
71
114
  render(React.createElement(CriteriaBuilder, { properties: mockProperties, criteria: {
@@ -42,7 +42,8 @@ export const Document = (props) => {
42
42
  if (canUpdateProperty) {
43
43
  apiServices
44
44
  .get(getPrefixedUrl(`/objects/${objectId}/instances/${instance.id}/documents/checkAccess?action=update`))
45
- .then((accessCheck) => setHasUpdatePermission(accessCheck.result));
45
+ .then((accessCheck) => setHasUpdatePermission(accessCheck.result))
46
+ .catch(() => setHasUpdatePermission(false));
46
47
  }
47
48
  };
48
49
  const handleUpload = async (files) => {
@@ -430,7 +430,7 @@ const RepeatableField = (props) => {
430
430
  hasCreateAction && (React.createElement(Button, { variant: "contained", sx: styles.addButton, onClick: addRow }, "Add"))),
431
431
  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,
432
432
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
433
- objectInputCommonProps: { apiServices }, action: relatedObject?.actions?.find((a) => a.id ===
433
+ objectInputCommonProps: { apiServices, setSnackbarError }, action: relatedObject?.actions?.find((a) => a.id ===
434
434
  (dialogType === 'create' ? '_create' : dialogType === 'update' ? '_update' : '_delete')), instanceId: selectedRow, queryAddresses: queryAddresses, user: user, associatedObject: instance.id && property.relatedPropertyId
435
435
  ? { instanceId: instance.id, propertyId: property.relatedPropertyId }
436
436
  : undefined, richTextEditor: richTextEditor })),
@@ -1,5 +1,4 @@
1
1
  import { ApiServices } from '@evoke-platform/context';
2
- import * as matchers from '@testing-library/jest-dom/matchers';
3
2
  import { render, screen, waitFor, within } from '@testing-library/react';
4
3
  import userEvent from '@testing-library/user-event';
5
4
  import axios from 'axios';
@@ -10,7 +9,6 @@ import React from 'react';
10
9
  import { expect, it } from 'vitest';
11
10
  import Form from '../Common/Form';
12
11
  import { accessibility508Object, licenseObject, npLicense, npSpecialtyType1, npSpecialtyType2, rnLicense, rnSpecialtyType1, rnSpecialtyType2, specialtyObject, specialtyTypeObject, users, } from './test-data';
13
- expect.extend(matchers);
14
12
  const removePoppers = () => {
15
13
  const portalSelectors = ['.MuiAutocomplete-popper'];
16
14
  portalSelectors.forEach((selector) => {
@@ -1,8 +1,10 @@
1
1
  import { DateTimeFormatter } from '@js-joda/core';
2
2
  import { omit } from 'lodash';
3
- import React, { useEffect, useState } from 'react';
3
+ import React, { useCallback, useEffect, useState } from 'react';
4
+ import { useFormContext } from '../../../../theme/hooks';
4
5
  import { InvalidDate, LocalDate, nativeJs } from '../../../../util';
5
6
  import { DatePicker, LocalizationProvider, TextField } from '../../../core';
7
+ import { obfuscateValue } from '../../FormV2/components/utils';
6
8
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
7
9
  function asCalendarDate(value) {
8
10
  if (!value) {
@@ -27,21 +29,48 @@ const asMonthDayYearFormat = (date) => {
27
29
  }
28
30
  };
29
31
  const DatePickerSelect = (props) => {
30
- const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, onChange, additionalProps, } = props;
31
- const [value, setValue] = useState(asCalendarDate(defaultValue));
32
+ const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, onChange, additionalProps, endAdornment, protection, } = props;
33
+ const { onAutosave } = useFormContext();
34
+ const processValue = useCallback((val) => {
35
+ const isProtected = protection?.maskChar && val === obfuscateValue(val, { protection });
36
+ return isProtected && typeof val === 'string' ? val : asCalendarDate(val);
37
+ }, [protection]);
38
+ const [value, setValue] = useState(() => processValue(defaultValue));
32
39
  useEffect(() => {
33
- setValue(asCalendarDate(defaultValue));
34
- }, [defaultValue]);
40
+ setValue(processValue(defaultValue));
41
+ }, [defaultValue, processValue]);
35
42
  const handleChange = (date) => {
36
43
  setValue(date);
37
44
  onChange && onChange(property.id, date, property);
38
45
  };
39
- return readOnly ? (React.createElement(InputFieldComponent, { ...{ ...props, defaultValue: asMonthDayYearFormat(value) } })) : (React.createElement(LocalizationProvider, null,
40
- React.createElement(DatePicker, { value: value, onChange: handleChange, inputFormat: "MM/dd/yyyy", renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
46
+ const handleAccept = async () => {
47
+ // Trigger autosave when date is accepted (picker closes after selection)
48
+ if (onAutosave) {
49
+ try {
50
+ await onAutosave(id);
51
+ }
52
+ catch (error) {
53
+ console.error('Autosave failed:', error);
54
+ }
55
+ }
56
+ };
57
+ return readOnly ? (React.createElement(InputFieldComponent, { ...{
58
+ ...props,
59
+ defaultValue: protection?.maskChar && value === obfuscateValue(value, { protection })
60
+ ? value
61
+ : asMonthDayYearFormat(value),
62
+ endAdornment,
63
+ } })) : (React.createElement(LocalizationProvider, null,
64
+ React.createElement(DatePicker, { value: value, onChange: handleChange, onAccept: handleAccept, inputFormat: "MM/dd/yyyy", renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
41
65
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
42
66
  inputProps: {
43
67
  ...params.inputProps,
44
68
  ...(additionalProps?.inputProps ?? {}),
69
+ }, InputProps: {
70
+ ...params.InputProps,
71
+ endAdornment: (React.createElement(React.Fragment, null,
72
+ params.InputProps?.endAdornment,
73
+ endAdornment)),
45
74
  }, ...omit(additionalProps, ['inputProps']) })) })));
46
75
  };
47
76
  export default DatePickerSelect;
@@ -1,6 +1,7 @@
1
1
  import { LocalDate, LocalDateTime, LocalTime, nativeJs } from '@js-joda/core';
2
2
  import { omit } from 'lodash';
3
3
  import React, { useEffect, useState } from 'react';
4
+ import { useFormContext } from '../../../../theme/hooks';
4
5
  import { InvalidDate } from '../../../../util';
5
6
  import { DateTimePicker, LocalizationProvider, TextField } from '../../../core';
6
7
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
@@ -30,6 +31,7 @@ const formatDateTime = (date) => {
30
31
  const DateTimePickerSelect = (props) => {
31
32
  const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, additionalProps } = props;
32
33
  const [value, setValue] = useState(asCalendarDate(defaultValue));
34
+ const { onAutosave } = useFormContext();
33
35
  useEffect(() => {
34
36
  setValue(asCalendarDate(defaultValue));
35
37
  }, [defaultValue]);
@@ -43,8 +45,19 @@ const DateTimePickerSelect = (props) => {
43
45
  setValue(date);
44
46
  props.onChange && props.onChange(property.id, date, property);
45
47
  };
48
+ const handleAccept = async () => {
49
+ // Trigger autosave when date/time is accepted (picker closes after selection)
50
+ if (onAutosave) {
51
+ try {
52
+ await onAutosave(id);
53
+ }
54
+ catch (error) {
55
+ console.error('Autosave failed:', error);
56
+ }
57
+ }
58
+ };
46
59
  return readOnly ? (React.createElement(InputFieldComponent, { ...{ ...props, defaultValue: formatDateTime(value) } })) : (React.createElement(LocalizationProvider, null,
47
- React.createElement(DateTimePicker, { value: value, onChange: handleChange, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
60
+ React.createElement(DateTimePicker, { value: value, onChange: handleChange, onAccept: handleAccept, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium',
48
61
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
49
62
  inputProps: {
50
63
  ...params.inputProps,
@@ -1,4 +1,4 @@
1
- import { SelectOption } from '@evoke-platform/context';
1
+ import { PropertyProtection as PropertyProtectionType, SelectOption } from '@evoke-platform/context';
2
2
  import React, { FocusEventHandler, ReactNode } from 'react';
3
3
  import { ObjectProperty } from '../../../types';
4
4
  import { AutocompleteOption } from '../../core';
@@ -36,6 +36,8 @@ export type FormFieldProps = {
36
36
  description?: string;
37
37
  tooltip?: string;
38
38
  isCombobox?: boolean;
39
+ endAdornment?: ReactNode;
40
+ protection?: PropertyProtectionType;
39
41
  };
40
42
  declare const FormField: (props: FormFieldProps) => React.JSX.Element;
41
43
  export default FormField;
@@ -1,4 +1,5 @@
1
- import React from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
+ import PropertyProtection from '../FormV2/components/PropertyProtection';
2
3
  import AddressFieldComponent from './AddressFieldComponent/addressFieldComponent';
3
4
  import BooleanSelect from './BooleanSelect/BooleanSelect';
4
5
  import DatePickerSelect from './DatePickerSelect/DatePickerSelect';
@@ -8,8 +9,16 @@ import InputFieldComponent from './InputFieldComponent/InputFieldComponent';
8
9
  import Select from './Select/Select';
9
10
  import TimePickerSelect from './TimePickerSelect/TimePickerSelect';
10
11
  const FormField = (props) => {
11
- const { id, defaultValue, error, onChange, property, readOnly, selectOptions, required, strictlyTrue, size, placeholder, errorMessage, onBlur, mask, max, min, isMultiLineText, rows, inputMaskPlaceholderChar, queryAddresses, isOptionEqualToValue, renderOption, disableCloseOnSelect, getOptionLabel, additionalProps, displayOption, sortBy, label, description, tooltip, isCombobox, } = props;
12
- let control;
12
+ const { id, defaultValue, error, onChange, property, readOnly, selectOptions, required, strictlyTrue, size, placeholder, errorMessage, onBlur, mask, max, min, isMultiLineText, rows, inputMaskPlaceholderChar, queryAddresses, isOptionEqualToValue, renderOption, disableCloseOnSelect, getOptionLabel, additionalProps, displayOption, sortBy, label, description, tooltip, isCombobox, protection, } = props;
13
+ const [currentDisplayValue, setCurrentDisplayValue] = useState(defaultValue);
14
+ const isProtectedProperty = !!protection?.maskChar;
15
+ const [protectionMode, setProtectionMode] = useState(isProtectedProperty ? (!currentDisplayValue ? 'edit' : 'mask') : 'full');
16
+ useEffect(() => {
17
+ if (isProtectedProperty && protectionMode === 'edit') {
18
+ setCurrentDisplayValue(defaultValue);
19
+ }
20
+ }, [defaultValue]);
21
+ const protectionComponent = isProtectedProperty && !!defaultValue ? (React.createElement(PropertyProtection, { parameter: property, protection: protection, mask: mask, canEdit: !readOnly, value: defaultValue, handleChange: (value) => onChange?.(property.id, value, property), setCurrentDisplayValue: setCurrentDisplayValue, mode: protectionMode, setMode: setProtectionMode })) : null;
13
22
  const commonProps = {
14
23
  id: id ?? property.id,
15
24
  property,
@@ -17,8 +26,8 @@ const FormField = (props) => {
17
26
  onBlur,
18
27
  error,
19
28
  errorMessage,
20
- readOnly,
21
- defaultValue,
29
+ readOnly: readOnly || (!!isProtectedProperty && protectionMode !== 'edit'),
30
+ defaultValue: isProtectedProperty ? currentDisplayValue : defaultValue,
22
31
  selectOptions,
23
32
  required,
24
33
  strictlyTrue,
@@ -37,7 +46,10 @@ const FormField = (props) => {
37
46
  description,
38
47
  tooltip,
39
48
  isCombobox,
49
+ endAdornment: protectionComponent,
50
+ protection,
40
51
  };
52
+ let control;
41
53
  if (queryAddresses) {
42
54
  control = (React.createElement(AddressFieldComponent, { ...commonProps, mask: mask, inputMaskPlaceholderChar: inputMaskPlaceholderChar, isMultiLineText: isMultiLineText, rows: rows, queryAddresses: queryAddresses }));
43
55
  return control;
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
3
3
  import InputMask from 'react-input-mask';
4
4
  import NumberFormat from 'react-number-format';
5
5
  import { Autocomplete, TextField } from '../../../core';
6
+ import { obfuscateValue } from '../../FormV2/components/utils';
6
7
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
8
  export const NumericFormat = (props) => {
8
9
  const { inputRef, onChange, defaultValue, ...other } = props;
@@ -15,7 +16,7 @@ export const NumericFormat = (props) => {
15
16
  }, isNumericString: true, fixedDecimalScale: true, allowNegative: true }));
16
17
  };
17
18
  const InputFieldComponent = (props) => {
18
- const { id, property, defaultValue, error, errorMessage, onBlur, readOnly, required, size, placeholder, mask, min, max, isMultiLineText, rows, inputMaskPlaceholderChar, additionalProps, } = props;
19
+ const { id, property, defaultValue, error, errorMessage, onBlur, readOnly, required, size, placeholder, mask, min, max, isMultiLineText, rows, inputMaskPlaceholderChar, additionalProps, endAdornment, protection, } = props;
19
20
  const [value, setValue] = useState(defaultValue ?? '');
20
21
  const [inputValue, setInputValue] = useState('');
21
22
  useEffect(() => {
@@ -43,6 +44,7 @@ const InputFieldComponent = (props) => {
43
44
  : property.type === 'integer'
44
45
  ? { inputProps: { min, max, ...(additionalProps?.inputProps ?? {}) } }
45
46
  : null;
47
+ const isValueProtected = protection?.maskChar && defaultValue === obfuscateValue(defaultValue, { protection, mask });
46
48
  return property.enum && !readOnly ? (React.createElement(Autocomplete, { id: id,
47
49
  // note: this is different between widgets and builder
48
50
  // builder had select options being {label, value}
@@ -53,7 +55,7 @@ const InputFieldComponent = (props) => {
53
55
  ? [...property.enum, defaultValue]
54
56
  : property.enum, onChange: handleSelectChange, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, error: error, errorMessage: errorMessage, fullWidth: true, onBlur: onBlur, size: size ?? 'medium', placeholder: placeholder })), disableClearable: true, value: value, isOptionEqualToValue: (option, value) => {
55
57
  return option.value === value;
56
- }, error: error, required: required, inputValue: inputValue ?? '', onInputChange: handleInputValueChange, ...(additionalProps ?? {}) })) : !mask ? (React.createElement(TextField, { id: id, sx: {
58
+ }, error: error, required: required, inputValue: inputValue ?? '', onInputChange: handleInputValueChange, ...(additionalProps ?? {}) })) : !mask || isValueProtected ? (React.createElement(TextField, { id: id, sx: {
57
59
  background: 'white',
58
60
  borderRadius: '8px',
59
61
  ...(readOnly && {
@@ -65,7 +67,7 @@ const InputFieldComponent = (props) => {
65
67
  backgroundColor: '#f4f6f8',
66
68
  },
67
69
  }),
68
- }, error: error, errorMessage: errorMessage, value: value, onChange: !readOnly ? handleChange : undefined, InputProps: { ...InputProps, readOnly: readOnly }, required: required, fullWidth: true, onBlur: onBlur, placeholder: readOnly ? undefined : placeholder, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, sx: readOnly
70
+ }, error: error, errorMessage: errorMessage, value: value, onChange: !readOnly ? handleChange : undefined, InputProps: { ...InputProps, endAdornment, readOnly: readOnly }, required: required, fullWidth: true, onBlur: onBlur, placeholder: readOnly ? undefined : placeholder, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })) : (React.createElement(InputMask, { mask: mask, maskChar: inputMaskPlaceholderChar ?? '_', value: value, onChange: !readOnly ? handleChange : undefined, onBlur: onBlur, alwaysShowMask: true }, (() => (React.createElement(TextField, { id: id, sx: readOnly
69
71
  ? {
70
72
  '& .MuiOutlinedInput-notchedOutline': {
71
73
  border: 'none',
@@ -75,7 +77,7 @@ const InputFieldComponent = (props) => {
75
77
  backgroundColor: '#f4f6f8',
76
78
  },
77
79
  }
78
- : undefined, required: required, error: error, errorMessage: errorMessage, InputProps: { ...InputProps, readOnly: readOnly }, fullWidth: true, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })
80
+ : undefined, required: required, error: error, errorMessage: errorMessage, InputProps: { ...InputProps, endAdornment, readOnly: readOnly }, fullWidth: true, size: size ?? 'medium', type: property.type === 'integer' ? 'number' : 'text', multiline: property.type === 'string' && !readOnly && isMultiLineText, rows: isMultiLineText ? (rows ? rows : 3) : undefined, ...(additionalProps ?? {}) })
79
81
  // Casting to `React.ReactNode` is necessary to resolve TypeScript errors
80
82
  // due to compatibility issues with the outdated `react-input-mask` version
81
83
  // and the newer `@types/react` package.
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import { userEvent } from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { describe, expect, it, vi } from 'vitest';
6
5
  import InputField from './InputFieldComponent';
7
- expect.extend(matchers);
8
6
  describe('Free-text input', () => {
9
7
  // Right now an object property is required for this to function, but eventually this should go
10
8
  // away.
@@ -1,10 +1,8 @@
1
- import * as matchers from '@testing-library/jest-dom/matchers';
2
1
  import { render, screen } from '@testing-library/react';
3
2
  import { userEvent } from '@testing-library/user-event';
4
3
  import React from 'react';
5
4
  import { describe, expect, it, vi } from 'vitest';
6
5
  import Select from './Select';
7
- expect.extend(matchers);
8
6
  describe('Single select', () => {
9
7
  // Right now an object property is required for this to function, but eventually this should go
10
8
  // away.
@@ -3,11 +3,13 @@ import { TimePicker } from '@mui/x-date-pickers';
3
3
  import { isUndefined, omit, padStart } from 'lodash';
4
4
  import { DateTime } from 'luxon';
5
5
  import React, { useEffect, useState } from 'react';
6
+ import { useFormContext } from '../../../../theme/hooks';
6
7
  import { InvalidDate } from '../../../../util';
7
8
  import { LocalizationProvider, TextField } from '../../../core';
8
9
  import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
9
10
  const TimePickerSelect = (props) => {
10
11
  const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, placeholder, additionalProps, } = props;
12
+ const { onAutosave } = useFormContext();
11
13
  const values = defaultValue ? defaultValue.split(':') : undefined;
12
14
  const hour = values ? parseInt(values[0]) : undefined;
13
15
  const minute = values ? parseInt(values[1]) : undefined;
@@ -41,11 +43,22 @@ const TimePickerSelect = (props) => {
41
43
  props.onChange && props.onChange(property.id, date, property);
42
44
  }
43
45
  };
46
+ const handleAccept = async () => {
47
+ // Trigger autosave when time is accepted (picker closes after selection)
48
+ if (onAutosave) {
49
+ try {
50
+ await onAutosave(id);
51
+ }
52
+ catch (error) {
53
+ console.error('Autosave failed:', error);
54
+ }
55
+ }
56
+ };
44
57
  return readOnly ? (React.createElement(InputFieldComponent, { ...{
45
58
  ...props,
46
59
  defaultValue: value instanceof LocalDateTime ? DateTime.fromISO(value.toString()).toFormat('hh:mm a') : '',
47
60
  } })) : (React.createElement(LocalizationProvider, null,
48
- React.createElement(TimePicker, { value: value, onChange: handleChange, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium', placeholder: placeholder,
61
+ React.createElement(TimePicker, { value: value, onChange: handleChange, onAccept: handleAccept, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium', placeholder: placeholder,
49
62
  // merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
50
63
  inputProps: {
51
64
  ...params.inputProps,
@@ -16,7 +16,8 @@ export type FormRendererProps = BaseProps & {
16
16
  form: EvokeForm;
17
17
  title?: string | React.ReactNode;
18
18
  instance?: ObjectInstance | Document;
19
- onChange: (id: string, value: unknown) => void;
19
+ onChange: (id: string, value: unknown) => void | Promise<void>;
20
+ onAutosave?: (fieldId: string) => void | Promise<void>;
20
21
  associatedObject?: {
21
22
  instanceId?: string;
22
23
  propertyId?: string;