@evoke-platform/ui-components 1.6.0-dev.23 → 1.6.0-dev.25

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 (21) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +2 -3
  2. package/dist/published/components/custom/FormField/AddressFieldComponent/AddressFieldComponent.test.js +1 -1
  3. package/dist/published/components/custom/FormField/FormField.d.ts +3 -2
  4. package/dist/published/components/custom/FormField/Select/Select.test.js +16 -41
  5. package/dist/published/components/custom/FormV2/components/AccordionSections.d.ts +4 -0
  6. package/dist/published/components/custom/FormV2/components/AccordionSections.js +131 -0
  7. package/dist/published/components/custom/FormV2/components/FieldWrapper.d.ts +1 -1
  8. package/dist/published/components/custom/FormV2/components/FieldWrapper.js +1 -1
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.d.ts +2 -2
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/UserProperty.js +5 -1
  11. package/dist/published/components/custom/FormV2/components/FormSections.d.ts +4 -0
  12. package/dist/published/components/custom/FormV2/components/FormSections.js +104 -0
  13. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.d.ts +2 -0
  14. package/dist/published/components/custom/FormV2/components/RecursiveEntryRenderer.js +209 -0
  15. package/dist/published/components/custom/FormV2/components/TabNav.d.ts +2 -2
  16. package/dist/published/components/custom/FormV2/components/TabNav.js +2 -2
  17. package/dist/published/components/custom/FormV2/components/types.d.ts +37 -2
  18. package/dist/published/components/custom/FormV2/components/utils.d.ts +5 -2
  19. package/dist/published/components/custom/FormV2/components/utils.js +104 -0
  20. package/dist/published/stories/FormField.stories.js +2 -1
  21. package/package.json +1 -1
@@ -1,11 +1,10 @@
1
- import { Property } from '@evoke-platform/context';
2
1
  import React from 'react';
3
2
  import 'react-querybuilder/dist/query-builder.css';
4
3
  import { EvokeObject } from '../../../types';
5
- import { Operator, PresetValue, TreeViewObject } from './types';
4
+ import { ObjectProperty, Operator, PresetValue, TreeViewObject } from './types';
6
5
  import { ValueEditorProps } from './ValueEditor';
7
6
  export type CriteriaInputProps = {
8
- properties: Property[];
7
+ properties: ObjectProperty[];
9
8
  setCriteria: (criteria?: Record<string, unknown> | undefined) => void;
10
9
  criteria?: Record<string, unknown>;
11
10
  originalCriteria?: Record<string, unknown>;
@@ -8,7 +8,7 @@ import AddressFieldComponent from './addressFieldComponent';
8
8
  const addressProperty = {
9
9
  id: 'addressLine1',
10
10
  name: 'Line 1',
11
- type: 'string',
11
+ type: 'text',
12
12
  };
13
13
  it('displays matching addresses', async () => {
14
14
  const user = userEvent.setup();
@@ -1,10 +1,11 @@
1
- import { Property, SelectOption } from '@evoke-platform/context';
1
+ import { SelectOption } from '@evoke-platform/context';
2
2
  import React, { FocusEventHandler, ReactNode } from 'react';
3
+ import { ObjectProperty } from '../../../types';
3
4
  import { AutocompleteOption } from '../../core';
4
5
  import { Address } from './AddressFieldComponent/addressFieldComponent';
5
6
  export type FormFieldProps = {
6
7
  id?: string;
7
- property: Property;
8
+ property: ObjectProperty;
8
9
  onChange?: Function;
9
10
  onBlur?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
10
11
  defaultValue?: unknown;
@@ -11,28 +11,23 @@ describe('Single select', () => {
11
11
  const choiceProperty = {
12
12
  id: 'selectOptions',
13
13
  name: 'Select Options',
14
- type: 'string',
14
+ type: 'choices',
15
15
  };
16
16
  it('returns selected option', async () => {
17
17
  const onChangeMock = vi.fn((name, value, property) => { });
18
18
  const user = userEvent.setup();
19
19
  const options = ['option 1', 'option 2', 'option 3'];
20
- render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock }));
20
+ render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock }));
21
21
  const input = screen.getByRole('combobox');
22
22
  await user.click(input);
23
23
  const option2 = await screen.findByRole('option', { name: 'option 2' });
24
24
  await user.click(option2);
25
- expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'option 2', value: 'option 2' }), {
26
- id: 'selectOptions',
27
- name: 'Select Options',
28
- type: 'string',
29
- enum: ['option 1', 'option 2', 'option 3'],
30
- });
25
+ expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'option 2', value: 'option 2' }), choiceProperty);
31
26
  });
32
27
  it('displays matching options', async () => {
33
28
  const user = userEvent.setup();
34
29
  const options = ['option 1', 'option 2', 'something different'];
35
- render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, onChange: () => { } }));
30
+ render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: () => { } }));
36
31
  const input = screen.getByRole('combobox');
37
32
  await user.type(input, 'option');
38
33
  await screen.findByRole('option', { name: 'option 1' });
@@ -45,7 +40,7 @@ describe('Single select', () => {
45
40
  { sortBy: 'DESC', expectedValues: ['option 3', 'option 2', 'option 1'] },
46
41
  ])('shows options in $sortBy order as dropdown display', async ({ sortBy, expectedValues }) => {
47
42
  const options = ['option 2', 'option 1', 'option 3'];
48
- render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, displayOption: 'dropdown', sortBy: sortBy, onChange: vi.fn() }));
43
+ render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'dropdown', sortBy: sortBy, onChange: vi.fn() }));
49
44
  const user = userEvent.setup();
50
45
  const input = screen.getByRole('combobox');
51
46
  await user.click(input);
@@ -57,7 +52,7 @@ describe('Single select', () => {
57
52
  const onChangeMock = vi.fn((name, value, property) => { });
58
53
  const user = userEvent.setup();
59
54
  const options = ['option 1', 'option 2', 'option 3'];
60
- render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
55
+ render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
61
56
  const input = screen.getByRole('combobox');
62
57
  await user.click(input);
63
58
  // Verify the instruction text for a combobox displays as the sub-header of the combobox.
@@ -66,12 +61,7 @@ describe('Single select', () => {
66
61
  // Verify the option to add the custom value is displayed as an available option in the dropdown.
67
62
  const customOption = await screen.findByRole('option', { name: 'Add "custom option"' });
68
63
  await user.click(customOption);
69
- expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'Add "custom option"', value: 'custom option' }), {
70
- id: 'selectOptions',
71
- name: 'Select Options',
72
- type: 'string',
73
- enum: ['option 1', 'option 2', 'option 3'],
74
- });
64
+ expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'Add "custom option"', value: 'custom option' }), choiceProperty);
75
65
  });
76
66
  });
77
67
  describe('Multi select', () => {
@@ -86,7 +76,7 @@ describe('Multi select', () => {
86
76
  const user = userEvent.setup();
87
77
  const onChangeMock = vi.fn((name, value, property) => { });
88
78
  const options = ['option 1', 'option 2', 'option 3'];
89
- render(React.createElement(Select, { id: "testSelect", property: { ...multiChoiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock }));
79
+ render(React.createElement(Select, { id: "testSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock }));
90
80
  const input = screen.getByRole('combobox');
91
81
  await user.click(input);
92
82
  const option2 = await screen.findByRole('option', { name: 'option 2' });
@@ -96,18 +86,13 @@ describe('Multi select', () => {
96
86
  const option3 = await screen.findByRole('option', { name: 'option 3' });
97
87
  await user.click(option3);
98
88
  expect(onChangeMock).toBeCalledTimes(2);
99
- expect(onChangeMock).lastCalledWith('multiSelect', ['option 2', 'option 3'], {
100
- id: 'multiSelect',
101
- name: 'Select Multiple',
102
- type: 'array',
103
- enum: ['option 1', 'option 2', 'option 3'],
104
- });
89
+ expect(onChangeMock).lastCalledWith('multiSelect', ['option 2', 'option 3'], multiChoiceProperty);
105
90
  });
106
91
  it('allows the user to enter custom values if it is combobox component', async () => {
107
92
  const onChangeMock = vi.fn((name, value, property) => { });
108
93
  const user = userEvent.setup();
109
94
  const options = ['option 1', 'option 2', 'option 3'];
110
- render(React.createElement(Select, { id: "multiSelect", property: { ...multiChoiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
95
+ render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
111
96
  const input = screen.getByRole('combobox');
112
97
  await user.click(input);
113
98
  // Verify the instruction text for a combobox displays as the sub-header of the combobox.
@@ -119,18 +104,13 @@ describe('Multi select', () => {
119
104
  const customOption2 = await screen.findByRole('option', { name: 'Add "custom option 2"' });
120
105
  await user.click(customOption2);
121
106
  expect(onChangeMock).toBeCalledTimes(2);
122
- expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'custom option 2'], {
123
- id: 'multiSelect',
124
- name: 'Select Multiple',
125
- type: 'array',
126
- enum: ['option 1', 'option 2', 'option 3'],
127
- });
107
+ expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'custom option 2'], multiChoiceProperty);
128
108
  });
129
109
  it('allows the user to enter custom values in conjunction with the predefined options if it is combobox component', async () => {
130
110
  const onChangeMock = vi.fn((name, value, property) => { });
131
111
  const user = userEvent.setup();
132
112
  const options = ['option 1', 'option 2', 'option 3'];
133
- render(React.createElement(Select, { id: "multiSelect", property: { ...multiChoiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
113
+ render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
134
114
  const input = screen.getByRole('combobox');
135
115
  await user.click(input);
136
116
  // Verify the instruction text for a combobox displays as the sub-header of the combobox.
@@ -142,19 +122,14 @@ describe('Multi select', () => {
142
122
  const option1 = await screen.findByRole('option', { name: 'option 1' });
143
123
  await user.click(option1);
144
124
  expect(onChangeMock).toBeCalledTimes(2);
145
- expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'option 1'], {
146
- id: 'multiSelect',
147
- name: 'Select Multiple',
148
- type: 'array',
149
- enum: ['option 1', 'option 2', 'option 3'],
150
- });
125
+ expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'option 1'], multiChoiceProperty);
151
126
  });
152
127
  });
153
128
  describe('Radio Single select', () => {
154
129
  const choiceProperty = {
155
130
  id: 'selectOptions',
156
131
  name: 'Select Options',
157
- type: 'string',
132
+ type: 'choices',
158
133
  };
159
134
  it('returns selected radio option', async () => {
160
135
  const user = userEvent.setup();
@@ -171,7 +146,7 @@ describe('Radio Single select', () => {
171
146
  { sortBy: 'DESC', expectedValues: ['option 3', 'option 2', 'option 1'] },
172
147
  ])('shows options in $sortBy order as radio display', async ({ sortBy, expectedValues }) => {
173
148
  const options = ['option 2', 'option 1', 'option 3'];
174
- render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, displayOption: 'radioButton', sortBy: sortBy, onChange: vi.fn() }));
149
+ render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: sortBy, onChange: vi.fn() }));
175
150
  const radioButtons = screen.getAllByRole('radio');
176
151
  const radioValues = radioButtons.map((radioButton) => radioButton.value);
177
152
  expect(radioValues).toEqual(expectedValues);
@@ -179,7 +154,7 @@ describe('Radio Single select', () => {
179
154
  it('renders an "Other" option in the radio group if the component is configured to support a custom value', async () => {
180
155
  const onChangeMock = vi.fn((name, value, property) => { });
181
156
  const options = ['option 1', 'option 2', 'option 3'];
182
- render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true }));
157
+ render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true }));
183
158
  await screen.findByRole('radio', { name: 'Other' });
184
159
  });
185
160
  it('renders a text field for a custom option if the "Other" option is selected', async () => {
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { SectionsProps } from './types';
3
+ declare function AccordionSections(props: SectionsProps): React.JSX.Element;
4
+ export default AccordionSections;
@@ -0,0 +1,131 @@
1
+ import { ExpandMoreOutlined } from '@mui/icons-material';
2
+ import { nanoid } from 'nanoid';
3
+ import React, { useEffect } from 'react';
4
+ import { useResponsive } from '../../../../theme';
5
+ import { Accordion, AccordionDetails, AccordionSummary, Typography } from '../../../core';
6
+ import { Box } from '../../../layout';
7
+ import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
8
+ import { getErrorCountForSection } from './utils';
9
+ function AccordionSections(props) {
10
+ const { entry, handleChange, fieldHeight, richTextEditor, instance, expandedSections, setExpandedSections, expandAll, setExpandAll, errors, parameters, triggerFieldReset, showSubmitError, } = props;
11
+ const { isMd, isLg, isXl } = useResponsive();
12
+ const lastSection = entry.sections.length - 1;
13
+ const sectionsWithIds = React.useMemo(() => assignIds(entry.sections), [entry]);
14
+ function collectNestedSections(entries = []) {
15
+ const nestedSections = [];
16
+ entries.forEach((entry) => {
17
+ if (entry.type === 'sections') {
18
+ nestedSections.push(entry);
19
+ }
20
+ else if (entry.type === 'columns') {
21
+ const columnEntry = entry;
22
+ columnEntry.columns.forEach((column) => {
23
+ const columnSections = collectNestedSections(column.entries);
24
+ nestedSections.push(...columnSections);
25
+ });
26
+ }
27
+ });
28
+ return nestedSections;
29
+ }
30
+ // need to add ids to section so expanded sections can differentiate between sections with the same label
31
+ function assignIds(sections) {
32
+ const result = [];
33
+ sections.forEach((section) => {
34
+ const newSection = {
35
+ ...section,
36
+ id: nanoid(),
37
+ };
38
+ result.push(newSection);
39
+ if (section.entries) {
40
+ const nestedSections = collectNestedSections(section.entries);
41
+ nestedSections.forEach((nestedSection) => {
42
+ nestedSection.sections = assignIds(nestedSection.sections);
43
+ });
44
+ }
45
+ });
46
+ return result;
47
+ }
48
+ function getExpandedSections(sections, expandAll) {
49
+ const expandedSections = [];
50
+ const processSections = (sectionList) => {
51
+ sectionList.forEach((section, index) => {
52
+ expandedSections.push({
53
+ label: section.label,
54
+ expanded: expandAll ?? (index === 0 || isMd || isLg || isXl),
55
+ id: section?.id,
56
+ });
57
+ if (section.entries) {
58
+ const nestedSections = collectNestedSections(section.entries).flatMap((s) => s.sections);
59
+ processSections(nestedSections);
60
+ }
61
+ });
62
+ };
63
+ processSections(sections);
64
+ return expandedSections;
65
+ }
66
+ useEffect(() => {
67
+ if (expandAll !== null) {
68
+ setExpandedSections(getExpandedSections(sectionsWithIds, expandAll));
69
+ }
70
+ }, [expandAll, sectionsWithIds]);
71
+ const handleAccordionChange = (id) => {
72
+ const updatedSections = expandedSections.map((section) => section.id === id ? { ...section, expanded: !section.expanded } : section);
73
+ setExpandedSections(updatedSections);
74
+ setExpandAll(null);
75
+ };
76
+ return (React.createElement(Box, null, sectionsWithIds.map((section, sectionIndex) => {
77
+ const errorCount = getErrorCountForSection(section, errors);
78
+ return (React.createElement(Accordion, { key: section.id, expanded: expandedSections.find((expandedSection) => expandedSection.id === section.id)?.expanded ??
79
+ !!expandAll, onChange: () => handleAccordionChange(section.id), defaultExpanded: sectionIndex === 0, sx: {
80
+ border: '1px solid #dbe0e4',
81
+ boxShadow: 'none',
82
+ marginBottom: '16px',
83
+ borderRadius: '6px',
84
+ '&:before': {
85
+ display: 'none',
86
+ },
87
+ } },
88
+ React.createElement(AccordionSummary, { sx: {
89
+ '&.Mui-expanded': {
90
+ borderBottom: '1px solid #dbe0e4',
91
+ minHeight: '44px',
92
+ borderBottomLeftRadius: '0px',
93
+ borderBottomRightRadius: '0px',
94
+ },
95
+ minHeight: '44px',
96
+ maxHeight: '44px',
97
+ backgroundColor: '#F4F6F8',
98
+ borderRadius: '5px',
99
+ // MUI accordion summaries have different border radius for the first and last item
100
+ borderTopLeftRadius: sectionIndex === 0 ? '3px' : undefined,
101
+ borderTopRightRadius: sectionIndex === 0 ? '3px' : undefined,
102
+ borderBottomRightRadius: sectionIndex === lastSection ? '3px' : undefined,
103
+ borderBottomLeftRadius: sectionIndex === lastSection ? '3px' : undefined,
104
+ }, expandIcon: React.createElement(ExpandMoreOutlined, { fontSize: "medium" }) },
105
+ React.createElement(Box, { sx: {
106
+ display: 'flex',
107
+ alignItems: 'center',
108
+ width: '100%',
109
+ justifyContent: 'space-between',
110
+ } },
111
+ React.createElement(Typography, { sx: {
112
+ fontWeight: '600',
113
+ } }, section.label),
114
+ errorCount > 0 && showSubmitError && (React.createElement(Box, { sx: {
115
+ ml: 1,
116
+ bgcolor: '#FF4842',
117
+ color: 'white',
118
+ borderRadius: '50%',
119
+ minWidth: '20px',
120
+ minHeight: '20px',
121
+ display: 'flex',
122
+ justifyContent: 'center',
123
+ alignItems: 'center',
124
+ fontSize: '12px',
125
+ margin: '0px',
126
+ marginRight: '16px',
127
+ } }, errorCount)))),
128
+ React.createElement(AccordionDetails, null, section.entries?.map((sectionEntry, index) => (React.createElement(RecursiveEntryRenderer, { key: sectionEntry.type + index, entry: sectionEntry, handleChange: handleChange, errors: errors, fieldHeight: fieldHeight, richTextEditor: richTextEditor, instance: instance, expandedSections: expandedSections, setExpandedSections: setExpandedSections, expandAll: expandAll, setExpandAll: setExpandAll, parameters: parameters, triggerFieldReset: triggerFieldReset, showSubmitError: showSubmitError }))))));
129
+ })));
130
+ }
131
+ export default AccordionSections;
@@ -7,7 +7,7 @@ type FieldWrapperProps = {
7
7
  tooltip?: string;
8
8
  prefix?: string;
9
9
  suffix?: string;
10
- value?: string;
10
+ value?: unknown;
11
11
  errorMessage?: string;
12
12
  showCharCount?: boolean;
13
13
  viewOnly: boolean;
@@ -49,7 +49,7 @@ const PrefixSuffix = (props) => {
49
49
  */
50
50
  const FieldWrapper = (props) => {
51
51
  const { inputId, label, description, tooltip, prefix, suffix, value, maxLength, errorMessage, showCharCount, inputType, viewOnly, children, fieldHeight, required, } = props;
52
- const charCount = value ? value.length : 0;
52
+ const charCount = typeof value === 'string' ? value.length : 0;
53
53
  const remainingChars = maxLength ? maxLength - charCount : undefined;
54
54
  return (React.createElement(Box, null,
55
55
  React.createElement(Box, { sx: { padding: '10px 0' } },
@@ -1,8 +1,8 @@
1
+ import { UserAccount } from '@evoke-platform/context';
1
2
  import React from 'react';
2
- import { AutocompleteOption } from '../../../../core';
3
3
  export type UserPropertyProps = {
4
4
  id: string;
5
- handleChangeUserProperty: (id: string, user: AutocompleteOption) => void;
5
+ handleChange: (id: string, user: UserAccount | undefined) => void;
6
6
  error?: boolean;
7
7
  value?: {
8
8
  id: string;
@@ -5,7 +5,7 @@ import { useFormContext } from '../../../../../theme/hooks';
5
5
  import { Autocomplete, Paper, TextField, Typography } from '../../../../core';
6
6
  import { getPrefixedUrl, isOptionEqualToValue } from '../utils';
7
7
  const UserProperty = (props) => {
8
- const { id, handleChangeUserProperty, error, value, fieldHeight, readOnly, hasDescription } = props;
8
+ const { id, handleChange, error, value, fieldHeight, readOnly, hasDescription } = props;
9
9
  const { fetchedOptions, setFetchedOptions } = useFormContext();
10
10
  const [loadingOptions, setLoadingOptions] = useState(false);
11
11
  const apiServices = useApiServices();
@@ -40,6 +40,10 @@ const UserProperty = (props) => {
40
40
  });
41
41
  }
42
42
  }, [id]);
43
+ function handleChangeUserProperty(id, value) {
44
+ const updatedValue = typeof value.value === 'string' ? { name: value.label, id: value.value } : undefined;
45
+ handleChange(id, updatedValue);
46
+ }
43
47
  return (options && (React.createElement(Autocomplete, { id: id, fullWidth: true, open: openOptions, popupIcon: userValue || readOnly ? '' : React.createElement(ExpandMore, null), PaperComponent: ({ children }) => {
44
48
  return (React.createElement(Paper, { sx: {
45
49
  borderRadius: '12px',
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { SectionsProps } from './types';
3
+ declare function FormSections(props: SectionsProps): React.JSX.Element;
4
+ export default FormSections;
@@ -0,0 +1,104 @@
1
+ import RadioButtonCheckedIcon from '@mui/icons-material/RadioButtonChecked';
2
+ import { TabContext, TabPanel } from '@mui/lab';
3
+ import React, { useRef, useState } from 'react';
4
+ import { Tab, Tabs, Typography } from '../../../core';
5
+ import { Box } from '../../../layout';
6
+ import { RecursiveEntryRenderer } from './RecursiveEntryRenderer';
7
+ import TabNav from './TabNav';
8
+ import { getErrorCountForSection, scrollIntoViewWithOffset } from './utils';
9
+ function FormSections(props) {
10
+ const { entry, errors, handleChange, showSubmitError, fieldHeight, richTextEditor, instance, expandedSections, setExpandedSections, expandAll, setExpandAll, parameters, triggerFieldReset, } = props;
11
+ const tabPanelsRef = useRef(null);
12
+ const [tabValue, setTabValue] = useState(0);
13
+ const handleTabChange = (type, newValue) => {
14
+ setTabValue(Number(newValue));
15
+ if (tabPanelsRef.current && type === 'buttonNav') {
16
+ scrollIntoViewWithOffset(tabPanelsRef.current, 170);
17
+ }
18
+ };
19
+ const prevTabLabel = tabValue > 0 ? entry.sections[tabValue - 1].label : '';
20
+ const nextTabLabel = tabValue < entry.sections.length - 1 ? entry.sections[tabValue + 1].label : '';
21
+ return (React.createElement(React.Fragment, null,
22
+ React.createElement(TabContext, { value: tabValue?.toString() || '0' },
23
+ React.createElement(Box, { sx: {
24
+ display: 'flex',
25
+ minHeight: '150px',
26
+ } },
27
+ React.createElement(Tabs, { value: tabValue?.toString(), onChange: (e, value) => handleTabChange('tabNav', value), orientation: "vertical", sx: {
28
+ overflow: 'visible',
29
+ paddingTop: '10px',
30
+ flex: 1,
31
+ borderRight: '1px solid #e9ecef',
32
+ minWidth: '190px',
33
+ maxWidth: '264px',
34
+ }, TabIndicatorProps: {
35
+ sx: {
36
+ display: 'none',
37
+ },
38
+ } }, entry.sections.map((section, sectionIndex) => {
39
+ const errorCount = getErrorCountForSection(section, errors);
40
+ const isSelected = tabValue?.toString() === sectionIndex.toString();
41
+ return (React.createElement(Tab, { disableRipple: true, key: section.label + sectionIndex, label: React.createElement(Box, { sx: {
42
+ display: 'flex',
43
+ alignItems: 'center',
44
+ width: '100%',
45
+ minWidth: '168px',
46
+ justifyContent: 'space-between',
47
+ } },
48
+ React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
49
+ React.createElement(Box, { sx: { width: '28px', display: 'flex' } }, isSelected ? (React.createElement(RadioButtonCheckedIcon, { color: errorCount > 0 && showSubmitError ? 'error' : 'primary', sx: {
50
+ fontSize: 'medium',
51
+ } })) : (React.createElement(Box, { sx: {
52
+ width: '8px',
53
+ height: '8px',
54
+ borderRadius: '50%',
55
+ backgroundColor: '#dfe3e8',
56
+ marginLeft: '4.25px',
57
+ } }))),
58
+ React.createElement(Typography, { sx: {
59
+ fontSize: '16px',
60
+ fontWeight: isSelected ? 'bold' : 'normal',
61
+ } }, section.label)),
62
+ errorCount > 0 && showSubmitError && (React.createElement(Box, { sx: {
63
+ ml: 1,
64
+ bgcolor: '#FF4842',
65
+ color: 'white',
66
+ borderRadius: '50%',
67
+ minWidth: '20px',
68
+ minHeight: '20px',
69
+ display: 'flex',
70
+ justifyContent: 'center',
71
+ alignItems: 'center',
72
+ fontSize: '12px',
73
+ margin: '0px',
74
+ marginRight: '16px',
75
+ } }, errorCount))), value: sectionIndex.toString(), tabIndex: -1, sx: {
76
+ textAlign: 'left',
77
+ display: 'flex',
78
+ textTransform: 'none',
79
+ color: '#212B36',
80
+ '&.Mui-selected': {
81
+ fontWeight: 'bold',
82
+ color: '#212B36',
83
+ },
84
+ padding: '14px 10px 14px 4px',
85
+ } }));
86
+ })),
87
+ entry.sections.map((section, sectionIndex) => (React.createElement(TabPanel, { key: section.label + sectionIndex, value: sectionIndex.toString(), ref: tabPanelsRef, sx: {
88
+ padding: '10px 20px 0px 40px',
89
+ width: '100%',
90
+ flex: 4,
91
+ } },
92
+ React.createElement(Box, { sx: {
93
+ display: 'flex',
94
+ flexDirection: 'column',
95
+ justifyContent: section.entries && section?.entries.length > 0 ? 'space-between' : 'flex-end',
96
+ height: '100%',
97
+ } },
98
+ React.createElement(Box, null, section.entries &&
99
+ section.entries.map((sectionEntry, index) => {
100
+ return (React.createElement(RecursiveEntryRenderer, { key: sectionEntry.type + index, entry: sectionEntry, handleChange: handleChange, errors: errors, showSubmitError: showSubmitError, fieldHeight: fieldHeight, richTextEditor: richTextEditor, instance: instance, expandedSections: expandedSections, setExpandedSections: setExpandedSections, expandAll: expandAll, setExpandAll: setExpandAll, parameters: parameters, triggerFieldReset: triggerFieldReset }));
101
+ })),
102
+ React.createElement(TabNav, { prevTabLabel: prevTabLabel, nextTabLabel: nextTabLabel, numberOfTabs: entry.sections.length, tabValue: tabValue, handleTabChange: handleTabChange })))))))));
103
+ }
104
+ export default FormSections;
@@ -0,0 +1,2 @@
1
+ import { EntryRendererProps } from './types';
2
+ export declare function RecursiveEntryRenderer(props: EntryRendererProps): any;
@@ -0,0 +1,209 @@
1
+ import { useApiServices, useAuthenticationContext, } from '@evoke-platform/context';
2
+ import { WarningRounded } from '@mui/icons-material';
3
+ import { cloneDeep } from 'lodash';
4
+ import React, { useEffect, useMemo } from 'react';
5
+ import { useResponsive } from '../../../../theme';
6
+ import { useFormContext } from '../../../../theme/hooks';
7
+ import { TextField, Typography } from '../../../core';
8
+ import { Box } from '../../../layout';
9
+ import FormField from '../../FormField';
10
+ import AccordionSections from './AccordionSections';
11
+ import FieldWrapper from './FieldWrapper';
12
+ import AddressFields from './FormFieldTypes/AddressFields';
13
+ import DropdownRepeatableField from './FormFieldTypes/CollectionFiles/DropdownRepeatableField';
14
+ import RepeatableField from './FormFieldTypes/CollectionFiles/RepeatableField';
15
+ import Criteria from './FormFieldTypes/Criteria';
16
+ import { Document } from './FormFieldTypes/DocumentFiles/Document';
17
+ import { Image } from './FormFieldTypes/Image';
18
+ import ObjectPropertyInput from './FormFieldTypes/relatedObjectFiles/ObjectPropertyInput';
19
+ import UserProperty from './FormFieldTypes/UserProperty';
20
+ import FormSections from './FormSections';
21
+ import { entryIsVisible, fetchCollectionData, getEntryId, isAddressProperty, isOptionEqualToValue, updateCriteriaInputs, } from './utils';
22
+ function getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors, validation) {
23
+ return {
24
+ inputId: entryId,
25
+ inputType: fieldDefinition.type,
26
+ label: display?.label || fieldDefinition.name || 'default',
27
+ description: display?.description,
28
+ tooltip: display?.tooltip,
29
+ value: fieldValue,
30
+ maxLength: validation && 'maxLength' in validation ? validation.maxLength : 0,
31
+ required: entry.display?.required || false,
32
+ showCharCount: display?.charCount,
33
+ prefix: display?.prefix,
34
+ suffix: display?.suffix,
35
+ viewOnly: entry.type === 'readonlyField',
36
+ fieldHeight,
37
+ errorMessage: errors?.[entryId]?.message,
38
+ };
39
+ }
40
+ export function RecursiveEntryRenderer(props) {
41
+ const { handleChange, errors, showSubmitError, fieldHeight, instance, richTextEditor, expandedSections, setExpandedSections, expandAll, setExpandAll, parameters, entry, triggerFieldReset, } = props;
42
+ const { fetchedOptions, setFetchedOptions, object, getValues } = useFormContext();
43
+ // If the entry is hidden, clear its value and any nested values, and skip rendering
44
+ if (!entryIsVisible(entry, getValues(), instance)) {
45
+ return null;
46
+ }
47
+ const apiServices = useApiServices();
48
+ const userAccount = useAuthenticationContext()?.account;
49
+ const { smallerThan, isXs } = useResponsive();
50
+ const entryId = getEntryId(entry) || 'defaultId';
51
+ const display = 'display' in entry ? entry.display : undefined;
52
+ const fieldValue = entry.type === 'readonlyField' ? instance?.[entryId] : getValues(entryId);
53
+ const initialMiddleObjectInstances = fetchedOptions[`${entryId}InitialMiddleObjectInstances`];
54
+ const middleObject = fetchedOptions[`${entryId}MiddleObject`];
55
+ const fieldDefinition = useMemo(() => {
56
+ let def;
57
+ if (entry.type === 'input') {
58
+ def = parameters?.find((param) => param.id === entry.parameterId);
59
+ }
60
+ else if (entry.type === 'readonlyField') {
61
+ def = isAddressProperty(entry.propertyId)
62
+ ? object?.properties?.find((prop) => prop.id === entry.propertyId.split('.')[0])
63
+ : object?.properties?.find((prop) => prop.id === entry.propertyId);
64
+ }
65
+ else if (entry.type === 'inputField') {
66
+ def = entry.input;
67
+ }
68
+ else {
69
+ return undefined;
70
+ }
71
+ if (def?.enum && def.type === 'string') {
72
+ const cloned = cloneDeep(def);
73
+ // single select must be made to be type choices for label and error handling
74
+ cloned.type = 'choices';
75
+ return cloned;
76
+ }
77
+ return def;
78
+ }, [entry, parameters, object]);
79
+ const validation = fieldDefinition?.validation || {};
80
+ useEffect(() => {
81
+ if (fieldDefinition?.type === 'collection' && fieldDefinition?.manyToManyPropertyId && instance) {
82
+ fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instance.id, fetchedOptions, initialMiddleObjectInstances);
83
+ }
84
+ }, [fieldDefinition, instance]);
85
+ if (entry.type === 'content') {
86
+ return (React.createElement(Box, { dangerouslySetInnerHTML: { __html: entry.html }, sx: {
87
+ fontFamily: 'Roboto, Helvetica, Arial, sans-serif',
88
+ } }));
89
+ }
90
+ else if ((entry.type === 'input' || entry.type === 'readonlyField' || entry.type === 'inputField') &&
91
+ fieldDefinition) {
92
+ if (isAddressProperty(entryId)) {
93
+ return (React.createElement(AddressFields, { entry: entry, errors: errors, handleChange: handleChange, fieldHeight: fieldHeight, parameters: parameters, instance: instance, entryId: entryId, fieldDefinition: fieldDefinition }));
94
+ }
95
+ else if (fieldDefinition.type === 'image') {
96
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
97
+ React.createElement(Image, { id: entryId, handleChange: handleChange, canUpdateProperty: entry.type !== 'readonlyField', error: !!errors[entryId], value: fieldValue, hasDescription: !!display?.description })));
98
+ }
99
+ else if (fieldDefinition.type === 'object') {
100
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
101
+ React.createElement(ObjectPropertyInput, { instance: instance, fieldDefinition: fieldDefinition, id: entryId, handleChangeObjectProperty: handleChange, mode: display?.mode || 'default', error: !!errors[entryId], displayOption: display?.relatedObjectDisplay || 'dialogBox', initialValue: fieldValue, parameters: parameters, readOnly: entry.type === 'readonlyField', filter: validation?.criteria
102
+ ? updateCriteriaInputs(validation.criteria, getValues(), userAccount)
103
+ : undefined, sortBy: typeof display?.defaultValue === 'object' && 'sortBy' in display.defaultValue
104
+ ? display?.defaultValue.sortBy
105
+ : undefined, orderBy: typeof display?.defaultValue === 'object' && 'orderBy' in display.defaultValue
106
+ ? display?.defaultValue.orderBy
107
+ : undefined, fieldHeight: fieldHeight, richTextEditor: richTextEditor, defaultValueCriteria: typeof display?.defaultValue === 'object' && 'criteria' in display.defaultValue
108
+ ? display?.defaultValue?.criteria
109
+ : undefined, viewLayout: display?.viewLayout, hasDescription: !!display?.description })));
110
+ }
111
+ else if (fieldDefinition.type === 'user') {
112
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
113
+ React.createElement(UserProperty, { id: entryId, value: fieldValue, handleChange: handleChange, error: !!errors[entryId], readOnly: entry.type === 'readonlyField', fieldHeight: fieldHeight, hasDescription: !!display?.description })));
114
+ }
115
+ else if (fieldDefinition.type === 'collection') {
116
+ return fieldDefinition?.manyToManyPropertyId ? (middleObject && initialMiddleObjectInstances && (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
117
+ React.createElement(DropdownRepeatableField, { initialMiddleObjectInstances: fetchedOptions[`${entryId}MiddleObjectInstances`] || initialMiddleObjectInstances, fieldDefinition: fieldDefinition, id: entryId, middleObject: middleObject, instance: instance, readOnly: entry.type === 'readonlyField', fieldHeight: fieldHeight, criteria: validation?.criteria, hasDescription: !!display?.description })))) : (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
118
+ React.createElement(RepeatableField, { fieldDefinition: fieldDefinition, canUpdateProperty: entry.type !== 'readonlyField', criteria: validation?.criteria, viewLayout: display?.viewLayout, instance: instance })));
119
+ }
120
+ else if (fieldDefinition.type === 'richText') {
121
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) }, richTextEditor ? (React.createElement(richTextEditor, {
122
+ id: entryId,
123
+ value: fieldValue,
124
+ handleUpdate: (value) => handleChange(entryId, value),
125
+ format: 'rtf',
126
+ disabled: entry.type === 'readonlyField',
127
+ rows: display?.rowCount,
128
+ hasError: !!errors[entryId],
129
+ })) : (React.createElement(FormField, { id: entryId, property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, error: !!errors[entryId], errorMessage: errors[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, size: fieldHeight }))));
130
+ }
131
+ else if (fieldDefinition.type === 'document') {
132
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
133
+ React.createElement(Document, { id: entryId, handleChange: handleChange, error: !!errors[entryId], value: fieldValue, instance: instance, canUpdateProperty: !(entry.type === 'readonlyField'), hasDescription: !!display?.description })));
134
+ }
135
+ else if (fieldDefinition.type === 'criteria') {
136
+ return (React.createElement(FieldWrapper, { ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors) },
137
+ React.createElement(Criteria, { key: triggerFieldReset ? `${entryId}-reset-true` : `${entryId}-reset-false`, fieldDefinition: fieldDefinition, value: fieldValue, handleChange: handleChange, canUpdateProperty: !(entry.type === 'readonlyField'), error: !!errors[entryId] })));
138
+ }
139
+ else {
140
+ // Add `aria-describedby` to ensure screen readers read the description
141
+ // when the input is tabbed into.
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ const additionalProps = {};
144
+ if (fieldDefinition.enum && display?.description) {
145
+ additionalProps.renderInput = (params) => (React.createElement(TextField, { ...params, inputProps: {
146
+ ...params.inputProps,
147
+ 'aria-describedby': `${entryId}-description`,
148
+ } }));
149
+ }
150
+ else if (display?.description) {
151
+ additionalProps.inputProps = {
152
+ 'aria-describedby': `${entryId}-description`,
153
+ };
154
+ }
155
+ return (React.createElement(FieldWrapper
156
+ /*
157
+ * Key remounts the field if a value is changed.
158
+ * Needed to clear certain fields on discard changes.
159
+ */
160
+ , {
161
+ /*
162
+ * Key remounts the field if a value is changed.
163
+ * Needed to clear certain fields on discard changes.
164
+ */
165
+ key: (fieldDefinition.type === 'choices' ||
166
+ fieldDefinition?.type === 'time' ||
167
+ fieldDefinition?.type === 'boolean') &&
168
+ triggerFieldReset
169
+ ? `${entryId}-reset-true`
170
+ : `${entryId}-reset-false`, ...getFieldWrapperProps(fieldDefinition, entry, entryId, fieldValue, display, fieldHeight, errors), errorMessage: undefined },
171
+ React.createElement(FormField, { id: entryId,
172
+ // TODO: Ideally the FormField prop should be called parameter but can't change the name for backwards compatibility reasons
173
+ property: fieldDefinition, defaultValue: fieldValue || getValues(entryId), onChange: handleChange, readOnly: entry.type === 'readonlyField', placeholder: display?.placeholder, mask: validation?.mask, isOptionEqualToValue: isOptionEqualToValue, error: !!errors[entryId], errorMessage: errors[entryId]?.message, isMultiLineText: !!display?.rowCount, rows: display?.rowCount, required: entry.display?.required || false, getOptionLabel: (option) => {
174
+ if (typeof option === 'string') {
175
+ return (entry?.enumWithLabels?.find((e) => e.value === option)
176
+ ?.label ?? option);
177
+ }
178
+ else {
179
+ return (entry?.enumWithLabels?.find((e) => e.value === option.value)?.label ?? String(option.value));
180
+ }
181
+ }, size: fieldHeight, sortBy: display?.choicesDisplay?.sortBy && display.choicesDisplay.sortBy, displayOption: fieldDefinition.type === 'boolean'
182
+ ? display?.booleanDisplay
183
+ : display?.choicesDisplay?.type && display.choicesDisplay.type, label: display?.label, description: display?.description, tooltip: display?.tooltip, selectOptions: entry?.enumWithLabels &&
184
+ entry.enumWithLabels, additionalProps: additionalProps, isCombobox: fieldDefinition.nonStrictEnum, strictlyTrue: fieldDefinition.strictlyTrue })));
185
+ }
186
+ }
187
+ else if (entry.type === 'columns') {
188
+ return (React.createElement(Box, { sx: { display: 'flex', alignItems: 'flex-start', gap: '30px', flexDirection: isXs ? 'column' : 'row' } }, entry.columns.map((column, colIndex) => (
189
+ // calculating the width like this rather than flex={column.width} to prevent collections from being too wide
190
+ React.createElement(Box, { key: colIndex, sx: { width: isXs ? '100%' : `calc(${(column.width / 12) * 100}% - 15px)` } }, column.entries?.map((columnEntry, entryIndex) => {
191
+ return (React.createElement(RecursiveEntryRenderer, { key: entryIndex + (columnEntry?.parameterId ?? ''), entry: columnEntry, handleChange: handleChange, errors: errors, fieldHeight: fieldHeight, richTextEditor: richTextEditor, instance: instance, expandedSections: expandedSections, setExpandedSections: setExpandedSections, expandAll: expandAll, setExpandAll: setExpandAll, parameters: parameters, triggerFieldReset: triggerFieldReset }));
192
+ }))))));
193
+ }
194
+ else if (entry.type === 'sections') {
195
+ return smallerThan('md') ? (React.createElement(AccordionSections, { entry: entry, handleChange: handleChange, fieldHeight: fieldHeight, richTextEditor: richTextEditor, instance: instance, expandedSections: expandedSections, setExpandedSections: setExpandedSections, expandAll: expandAll, setExpandAll: setExpandAll, errors: errors, parameters: parameters, triggerFieldReset: triggerFieldReset })) : (React.createElement(FormSections, { entry: entry, errors: errors, handleChange: handleChange, showSubmitError: showSubmitError, fieldHeight: fieldHeight, richTextEditor: richTextEditor, instance: instance, expandedSections: expandedSections, setExpandedSections: setExpandedSections, expandAll: expandAll, setExpandAll: setExpandAll, parameters: parameters }));
196
+ }
197
+ else if (!fieldDefinition) {
198
+ return (React.createElement(Box, { sx: {
199
+ display: 'flex',
200
+ backgroundColor: '#ffc1073b',
201
+ borderRadius: '8px',
202
+ padding: '16.5px 14px',
203
+ marginTop: '6px',
204
+ } },
205
+ React.createElement(WarningRounded, { sx: { paddingRight: '8px' }, color: "warning" }),
206
+ React.createElement(Typography, { variant: "body2", color: "textSecondary" }, "This field was not configured correctly")));
207
+ }
208
+ return null;
209
+ }
@@ -1,8 +1,8 @@
1
- import React, { SyntheticEvent } from 'react';
1
+ import React from 'react';
2
2
  type TabNavProps = {
3
3
  numberOfTabs: number;
4
4
  tabValue: number;
5
- handleTabChange: (event: SyntheticEvent, newValue: number) => void;
5
+ handleTabChange: (type: 'tabNav' | 'buttonNav', newValue: number) => void;
6
6
  prevTabLabel?: string;
7
7
  nextTabLabel?: string;
8
8
  };
@@ -13,10 +13,10 @@ function TabNav(props) {
13
13
  paddingTop: '10px',
14
14
  paddingBottom: '20px',
15
15
  } },
16
- !isFirstTab && (React.createElement(Button, { id: 'navigation-prev', onClick: (event) => handleTabChange(event, tabValue - 1), sx: { color: 'black', backgroundColor: '#dfe3e8', '&:hover': { backgroundColor: '#d0d4d9' } }, "aria-label": `Navigate to previous tab ${prevTabLabel}` },
16
+ !isFirstTab && (React.createElement(Button, { onClick: () => handleTabChange('buttonNav', tabValue - 1), sx: { color: 'black', backgroundColor: '#dfe3e8', '&:hover': { backgroundColor: '#d0d4d9' } }, "aria-label": `Navigate to previous tab ${prevTabLabel}` },
17
17
  React.createElement(ArrowBackIosNewIcon, { sx: { fontSize: 'medium', marginRight: 1 } }),
18
18
  prevTabLabel ?? 'Previous tab')),
19
- !isLastTab && (React.createElement(Button, { id: 'navigation-next', onClick: (event) => handleTabChange(event, tabValue + 1), sx: { color: 'black', backgroundColor: '#dfe3e8', '&:hover': { backgroundColor: '#d0d4d9' } }, "aria-label": `Navigate to next tab ${nextTabLabel}` },
19
+ !isLastTab && (React.createElement(Button, { onClick: () => handleTabChange('buttonNav', tabValue + 1), sx: { color: 'black', backgroundColor: '#dfe3e8', '&:hover': { backgroundColor: '#d0d4d9' } }, "aria-label": `Navigate to next tab ${nextTabLabel}` },
20
20
  nextTabLabel ?? 'Next tab',
21
21
  React.createElement(ArrowForwardIosIcon, { sx: { fontSize: 'medium', marginLeft: 1 } })))));
22
22
  }
@@ -1,7 +1,7 @@
1
- import { InputParameter, ObjectInstance, Property, ViewLayoutEntityReference } from '@evoke-platform/context';
1
+ import { FormEntry, InputParameter, ObjectInstance, Property, Section, Sections, ViewLayoutEntityReference } from '@evoke-platform/context';
2
2
  import { GridSize } from '@mui/material';
3
3
  import { ComponentType } from 'react';
4
- import { FieldValues } from 'react-hook-form';
4
+ import { FieldErrors, FieldValues } from 'react-hook-form';
5
5
  export type FieldAddress = {
6
6
  line1?: string;
7
7
  line2?: string;
@@ -85,3 +85,38 @@ export type BaseProps = {
85
85
  getWidgetState?: () => any;
86
86
  saveWidgetState?: (widgetState: unknown) => void;
87
87
  };
88
+ export type ExpandedSection = Section & {
89
+ id: string;
90
+ expanded?: boolean;
91
+ };
92
+ export type EntryRendererProps = BaseProps & {
93
+ entry: FormEntry;
94
+ errors: FieldErrors;
95
+ handleChange: (name: string, value: unknown) => void;
96
+ showSubmitError?: boolean;
97
+ fieldHeight?: 'small' | 'medium';
98
+ instance?: FieldValues;
99
+ richTextEditor?: ComponentType<SimpleEditorProps>;
100
+ expandedSections: ExpandedSection[];
101
+ setExpandedSections: React.Dispatch<React.SetStateAction<ExpandedSection[]>>;
102
+ expandAll?: boolean | undefined | null;
103
+ setExpandAll: React.Dispatch<React.SetStateAction<boolean | undefined | null>>;
104
+ parameters?: InputParameter[];
105
+ readOnly?: boolean;
106
+ triggerFieldReset?: boolean;
107
+ };
108
+ export type SectionsProps = {
109
+ entry: Sections;
110
+ handleChange: (propertyId: string, value: unknown) => void;
111
+ fieldHeight?: 'small' | 'medium';
112
+ richTextEditor?: ComponentType<SimpleEditorProps>;
113
+ instance?: FieldValues;
114
+ expandAll?: boolean | undefined | null;
115
+ expandedSections: ExpandedSection[];
116
+ setExpandedSections: React.Dispatch<React.SetStateAction<ExpandedSection[]>>;
117
+ setExpandAll: React.Dispatch<React.SetStateAction<boolean | undefined | null>>;
118
+ errors: FieldErrors;
119
+ parameters?: InputParameter[];
120
+ triggerFieldReset?: boolean;
121
+ showSubmitError?: boolean;
122
+ };
@@ -1,6 +1,6 @@
1
- import { Columns, FormEntry, InputParameter, Obj, ObjectInstance, Property, Sections } from '@evoke-platform/context';
1
+ import { ApiServices, Column, Columns, FormEntry, InputParameter, Obj, ObjectInstance, Property, Section, Sections, UserAccount } from '@evoke-platform/context';
2
2
  import { LocalDateTime } from '@js-joda/core';
3
- import { FieldValues } from 'react-hook-form';
3
+ import { FieldErrors, FieldValues } from 'react-hook-form';
4
4
  import { AutocompleteOption } from '../../../core';
5
5
  export declare const scrollIntoViewWithOffset: (el: HTMLElement, offset: number, container?: HTMLElement) => void;
6
6
  export declare const normalizeDateTime: (dateTime: LocalDateTime) => string;
@@ -37,3 +37,6 @@ export declare const encodePageSlug: (slug: string) => string;
37
37
  export declare function getDefaultPages(parameters: InputParameter[], defaultPages: Record<string, string> | undefined, findDefaultPageSlugFor: (objectId: string) => Promise<string | undefined>): Promise<{
38
38
  [x: string]: string;
39
39
  }>;
40
+ export declare function updateCriteriaInputs(criteria: Record<string, unknown>, data: Record<string, unknown>, user?: UserAccount): Record<string, unknown>;
41
+ export declare function fetchCollectionData(apiServices: ApiServices, fieldDefinition: InputParameter | Property, setFetchedOptions: (newData: FieldValues) => void, instanceId?: string, fetchedOptions?: Record<string, unknown>, initialMiddleObjectInstances?: ObjectInstance[]): Promise<void>;
42
+ export declare const getErrorCountForSection: (section: Section | Column, errors: FieldErrors) => number;
@@ -2,6 +2,8 @@ import { LocalDateTime } from '@js-joda/core';
2
2
  import jsonLogic from 'json-logic-js';
3
3
  import { get, isArray, isObject, transform } from 'lodash';
4
4
  import { DateTime } from 'luxon';
5
+ import Handlebars from 'no-eval-handlebars';
6
+ import { defaultRuleProcessorMongoDB, formatQuery, parseMongoDB } from 'react-querybuilder';
5
7
  export const scrollIntoViewWithOffset = (el, offset, container) => {
6
8
  const elementRect = el.getBoundingClientRect();
7
9
  const containerRect = container ? container.getBoundingClientRect() : document.body.getBoundingClientRect();
@@ -267,3 +269,105 @@ export async function getDefaultPages(parameters, defaultPages, findDefaultPageS
267
269
  }
268
270
  return { ...defaultPages, ...foundDefaultPages };
269
271
  }
272
+ function compileQueryValues(query, data) {
273
+ if ('rules' in query && Array.isArray(query.rules)) {
274
+ const updatedRules = query.rules
275
+ .map((item) => compileQueryValues(item, data))
276
+ .filter((item) => item !== undefined);
277
+ if (updatedRules?.length) {
278
+ return {
279
+ ...query,
280
+ rules: updatedRules,
281
+ };
282
+ }
283
+ else {
284
+ return undefined;
285
+ }
286
+ }
287
+ else {
288
+ if ('value' in query && typeof query.value === 'string') {
289
+ const template = Handlebars.compileAST(query.value);
290
+ const result = template(data);
291
+ if (result !== '') {
292
+ return {
293
+ ...query,
294
+ value: result,
295
+ };
296
+ }
297
+ else {
298
+ return undefined;
299
+ }
300
+ }
301
+ return query;
302
+ }
303
+ }
304
+ export function updateCriteriaInputs(criteria, data, user) {
305
+ const dataSet = {
306
+ input: {
307
+ ...data,
308
+ },
309
+ user: user,
310
+ };
311
+ const compiledQuery = compileQueryValues(parseMongoDB(criteria), dataSet);
312
+ // The "compiledQueryValues" function filters out rules that have a value of "undefined".
313
+ // If "compiledQuery" is "undefined", that means that all rules and nested rules have a value of "undefined".
314
+ // Therefore, the resulting query is empty.
315
+ return !compiledQuery
316
+ ? {}
317
+ : JSON.parse(formatQuery(compiledQuery, {
318
+ format: 'mongodb',
319
+ ruleProcessor: (rule, options) => {
320
+ let updatedRule = rule;
321
+ if (['contains', 'beginsWith', 'endsWith'].includes(rule.operator)) {
322
+ updatedRule = { ...rule, value: escape(rule.value) };
323
+ }
324
+ return defaultRuleProcessorMongoDB(updatedRule, options);
325
+ },
326
+ }));
327
+ }
328
+ export async function fetchCollectionData(apiServices, fieldDefinition, setFetchedOptions, instanceId, fetchedOptions, initialMiddleObjectInstances) {
329
+ try {
330
+ if ((fetchedOptions && !fetchedOptions[`${fieldDefinition.id}MiddleObject`]) || !fetchedOptions) {
331
+ const fetchedMiddleObject = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), {
332
+ params: {
333
+ filter: { fields: ['properties', 'actions', 'rootObjectId'] },
334
+ },
335
+ });
336
+ setFetchedOptions({ [`${fieldDefinition.id}MiddleObject`]: fetchedMiddleObject });
337
+ }
338
+ // Fetch the initial middle object instances
339
+ const filter = instanceId ? getMiddleObjectFilter(fieldDefinition, instanceId) : {};
340
+ if (!isArray(initialMiddleObjectInstances)) {
341
+ const fetchedInitialMiddleObjectInstances = await apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances`), {
342
+ params: { filter: JSON.stringify(filter) },
343
+ });
344
+ setFetchedOptions({
345
+ [`${fieldDefinition.id}InitialMiddleObjectInstances`]: fetchedInitialMiddleObjectInstances,
346
+ });
347
+ }
348
+ }
349
+ catch (error) {
350
+ console.error('Error fetching collection data:', error);
351
+ }
352
+ }
353
+ export const getErrorCountForSection = (section, errors) => {
354
+ const entries = section.entries || [];
355
+ return isArray(section.entries)
356
+ ? entries.reduce((count, entry) => {
357
+ if (get(errors, entry.type === 'input' ? entry.parameterId : entry.type === 'inputField' ? entry.input.id : '')) {
358
+ count += 1;
359
+ }
360
+ else if (entry.type === 'sections') {
361
+ count += entry.sections.reduce((subCount, subSection) => {
362
+ return subCount + getErrorCountForSection(subSection, errors);
363
+ }, 0);
364
+ }
365
+ else if (entry.type === 'columns') {
366
+ count += entry.columns.reduce((colCount, column) => {
367
+ return colCount + getErrorCountForSection(column, errors);
368
+ }, 0);
369
+ }
370
+ return count;
371
+ }, 0)
372
+ : 0;
373
+ };
@@ -10,7 +10,8 @@ InputField.args = {
10
10
  property: {
11
11
  id: 'fullName',
12
12
  name: 'Full Name',
13
- type: 'string',
13
+ type: 'text',
14
+ // enum?: string[],
14
15
  required: true,
15
16
  },
16
17
  onChange: (id, value) => console.log('id= ', id, 'Value= ', value),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.6.0-dev.23",
3
+ "version": "1.6.0-dev.25",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",