@evoke-platform/ui-components 1.6.0-dev.20 → 1.6.0-dev.22

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 (17) hide show
  1. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +3 -2
  2. package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +1 -1
  3. package/dist/published/components/custom/FormField/AddressFieldComponent/AddressFieldComponent.test.js +1 -1
  4. package/dist/published/components/custom/FormField/FormField.d.ts +2 -3
  5. package/dist/published/components/custom/FormField/Select/Select.test.js +41 -16
  6. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.d.ts +15 -0
  7. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +226 -0
  8. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.d.ts +4 -0
  9. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +439 -0
  10. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +29 -0
  11. package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +74 -0
  12. package/dist/published/components/custom/FormV2/components/types.d.ts +61 -0
  13. package/dist/published/components/custom/FormV2/components/utils.d.ts +4 -0
  14. package/dist/published/components/custom/FormV2/components/utils.js +20 -0
  15. package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.js +3 -7
  16. package/dist/published/stories/FormField.stories.js +1 -2
  17. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
+ import { Property } from '@evoke-platform/context';
1
2
  import React from 'react';
2
3
  import 'react-querybuilder/dist/query-builder.css';
3
4
  import { EvokeObject } from '../../../types';
4
- import { ObjectProperty, Operator, PresetValue, TreeViewObject } from './types';
5
+ import { Operator, PresetValue, TreeViewObject } from './types';
5
6
  import { ValueEditorProps } from './ValueEditor';
6
7
  export type CriteriaInputProps = {
7
- properties: ObjectProperty[];
8
+ properties: Property[];
8
9
  setCriteria: (criteria?: Record<string, unknown> | undefined) => void;
9
10
  criteria?: Record<string, unknown>;
10
11
  originalCriteria?: Record<string, unknown>;
@@ -186,7 +186,7 @@ const customSelector = (props) => {
186
186
  return opts.find((o) => option === o.name)?.label || option;
187
187
  }
188
188
  return option.label;
189
- }, isOptionEqualToValue: (option, value) => {
189
+ }, getOptionKey: (option) => option.name, isOptionEqualToValue: (option, value) => {
190
190
  if (typeof option === 'string') {
191
191
  return option === value;
192
192
  }
@@ -8,7 +8,7 @@ import AddressFieldComponent from './addressFieldComponent';
8
8
  const addressProperty = {
9
9
  id: 'addressLine1',
10
10
  name: 'Line 1',
11
- type: 'text',
11
+ type: 'string',
12
12
  };
13
13
  it('displays matching addresses', async () => {
14
14
  const user = userEvent.setup();
@@ -1,11 +1,10 @@
1
- import { SelectOption } from '@evoke-platform/context';
1
+ import { Property, SelectOption } from '@evoke-platform/context';
2
2
  import React, { FocusEventHandler, ReactNode } from 'react';
3
- import { ObjectProperty } from '../../../types';
4
3
  import { AutocompleteOption } from '../../core';
5
4
  import { Address } from './AddressFieldComponent/addressFieldComponent';
6
5
  export type FormFieldProps = {
7
6
  id?: string;
8
- property: ObjectProperty;
7
+ property: Property;
9
8
  onChange?: Function;
10
9
  onBlur?: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
11
10
  defaultValue?: unknown;
@@ -11,23 +11,28 @@ describe('Single select', () => {
11
11
  const choiceProperty = {
12
12
  id: 'selectOptions',
13
13
  name: 'Select Options',
14
- type: 'choices',
14
+ type: 'string',
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, selectOptions: options, onChange: onChangeMock }));
20
+ render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, 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' }), choiceProperty);
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
+ });
26
31
  });
27
32
  it('displays matching options', async () => {
28
33
  const user = userEvent.setup();
29
34
  const options = ['option 1', 'option 2', 'something different'];
30
- render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: () => { } }));
35
+ render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, onChange: () => { } }));
31
36
  const input = screen.getByRole('combobox');
32
37
  await user.type(input, 'option');
33
38
  await screen.findByRole('option', { name: 'option 1' });
@@ -40,7 +45,7 @@ describe('Single select', () => {
40
45
  { sortBy: 'DESC', expectedValues: ['option 3', 'option 2', 'option 1'] },
41
46
  ])('shows options in $sortBy order as dropdown display', async ({ sortBy, expectedValues }) => {
42
47
  const options = ['option 2', 'option 1', 'option 3'];
43
- render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'dropdown', sortBy: sortBy, onChange: vi.fn() }));
48
+ render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, displayOption: 'dropdown', sortBy: sortBy, onChange: vi.fn() }));
44
49
  const user = userEvent.setup();
45
50
  const input = screen.getByRole('combobox');
46
51
  await user.click(input);
@@ -52,7 +57,7 @@ describe('Single select', () => {
52
57
  const onChangeMock = vi.fn((name, value, property) => { });
53
58
  const user = userEvent.setup();
54
59
  const options = ['option 1', 'option 2', 'option 3'];
55
- render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
60
+ render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
56
61
  const input = screen.getByRole('combobox');
57
62
  await user.click(input);
58
63
  // Verify the instruction text for a combobox displays as the sub-header of the combobox.
@@ -61,7 +66,12 @@ describe('Single select', () => {
61
66
  // Verify the option to add the custom value is displayed as an available option in the dropdown.
62
67
  const customOption = await screen.findByRole('option', { name: 'Add "custom option"' });
63
68
  await user.click(customOption);
64
- expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'Add "custom option"', value: 'custom option' }), choiceProperty);
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
+ });
65
75
  });
66
76
  });
67
77
  describe('Multi select', () => {
@@ -76,7 +86,7 @@ describe('Multi select', () => {
76
86
  const user = userEvent.setup();
77
87
  const onChangeMock = vi.fn((name, value, property) => { });
78
88
  const options = ['option 1', 'option 2', 'option 3'];
79
- render(React.createElement(Select, { id: "testSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock }));
89
+ render(React.createElement(Select, { id: "testSelect", property: { ...multiChoiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock }));
80
90
  const input = screen.getByRole('combobox');
81
91
  await user.click(input);
82
92
  const option2 = await screen.findByRole('option', { name: 'option 2' });
@@ -86,13 +96,18 @@ describe('Multi select', () => {
86
96
  const option3 = await screen.findByRole('option', { name: 'option 3' });
87
97
  await user.click(option3);
88
98
  expect(onChangeMock).toBeCalledTimes(2);
89
- expect(onChangeMock).lastCalledWith('multiSelect', ['option 2', 'option 3'], multiChoiceProperty);
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
+ });
90
105
  });
91
106
  it('allows the user to enter custom values if it is combobox component', async () => {
92
107
  const onChangeMock = vi.fn((name, value, property) => { });
93
108
  const user = userEvent.setup();
94
109
  const options = ['option 1', 'option 2', 'option 3'];
95
- render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
110
+ render(React.createElement(Select, { id: "multiSelect", property: { ...multiChoiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
96
111
  const input = screen.getByRole('combobox');
97
112
  await user.click(input);
98
113
  // Verify the instruction text for a combobox displays as the sub-header of the combobox.
@@ -104,13 +119,18 @@ describe('Multi select', () => {
104
119
  const customOption2 = await screen.findByRole('option', { name: 'Add "custom option 2"' });
105
120
  await user.click(customOption2);
106
121
  expect(onChangeMock).toBeCalledTimes(2);
107
- expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'custom option 2'], multiChoiceProperty);
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
+ });
108
128
  });
109
129
  it('allows the user to enter custom values in conjunction with the predefined options if it is combobox component', async () => {
110
130
  const onChangeMock = vi.fn((name, value, property) => { });
111
131
  const user = userEvent.setup();
112
132
  const options = ['option 1', 'option 2', 'option 3'];
113
- render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
133
+ render(React.createElement(Select, { id: "multiSelect", property: { ...multiChoiceProperty, enum: options }, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
114
134
  const input = screen.getByRole('combobox');
115
135
  await user.click(input);
116
136
  // Verify the instruction text for a combobox displays as the sub-header of the combobox.
@@ -122,14 +142,19 @@ describe('Multi select', () => {
122
142
  const option1 = await screen.findByRole('option', { name: 'option 1' });
123
143
  await user.click(option1);
124
144
  expect(onChangeMock).toBeCalledTimes(2);
125
- expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'option 1'], multiChoiceProperty);
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
+ });
126
151
  });
127
152
  });
128
153
  describe('Radio Single select', () => {
129
154
  const choiceProperty = {
130
155
  id: 'selectOptions',
131
156
  name: 'Select Options',
132
- type: 'choices',
157
+ type: 'string',
133
158
  };
134
159
  it('returns selected radio option', async () => {
135
160
  const user = userEvent.setup();
@@ -146,7 +171,7 @@ describe('Radio Single select', () => {
146
171
  { sortBy: 'DESC', expectedValues: ['option 3', 'option 2', 'option 1'] },
147
172
  ])('shows options in $sortBy order as radio display', async ({ sortBy, expectedValues }) => {
148
173
  const options = ['option 2', 'option 1', 'option 3'];
149
- render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: sortBy, onChange: vi.fn() }));
174
+ render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, displayOption: 'radioButton', sortBy: sortBy, onChange: vi.fn() }));
150
175
  const radioButtons = screen.getAllByRole('radio');
151
176
  const radioValues = radioButtons.map((radioButton) => radioButton.value);
152
177
  expect(radioValues).toEqual(expectedValues);
@@ -154,7 +179,7 @@ describe('Radio Single select', () => {
154
179
  it('renders an "Other" option in the radio group if the component is configured to support a custom value', async () => {
155
180
  const onChangeMock = vi.fn((name, value, property) => { });
156
181
  const options = ['option 1', 'option 2', 'option 3'];
157
- render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true }));
182
+ render(React.createElement(Select, { id: "testSelect", property: { ...choiceProperty, enum: options }, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true }));
158
183
  await screen.findByRole('radio', { name: 'Other' });
159
184
  });
160
185
  it('renders a text field for a custom option if the "Other" option is selected', async () => {
@@ -0,0 +1,15 @@
1
+ import { Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ import { BaseProps } from '../../types';
4
+ export type InstanceLookUpProps = BaseProps & {
5
+ object: Obj;
6
+ instanceId?: string;
7
+ setSelectedInstance: (selectedInstance: ObjectInstance) => void;
8
+ setRelationType: (relationType: 'new' | 'existing') => void;
9
+ mode: 'default' | 'existingOnly';
10
+ nestedFieldsView?: boolean;
11
+ filter?: Record<string, unknown>;
12
+ layout?: TableViewLayout;
13
+ };
14
+ declare const InstanceLookup: (props: InstanceLookUpProps) => React.JSX.Element;
15
+ export default InstanceLookup;
@@ -0,0 +1,226 @@
1
+ import { useApiServices } from '@evoke-platform/context';
2
+ import { Clear, Search } from '@mui/icons-material';
3
+ import { debounce, get, startCase } from 'lodash';
4
+ import { DateTime } from 'luxon';
5
+ import React, { useEffect, useState } from 'react';
6
+ import { Button, IconButton, InputAdornment, TextField, Typography, } from '../../../../../core';
7
+ import { Box, Grid } from '../../../../../layout';
8
+ import BuilderGrid from '../../../../BuilderGrid';
9
+ import { getPrefixedUrl } from '../../utils';
10
+ const SearchField = (props) => {
11
+ const { searchableColumns, filter, setFilter, searchString, setSearchString } = props;
12
+ const clearSearch = () => {
13
+ setSearchString('');
14
+ setFilter(undefined);
15
+ };
16
+ const handleSearch = async (e) => {
17
+ const searchProperties = searchableColumns.map((column) => {
18
+ const columnId = column.id;
19
+ return {
20
+ [columnId]: {
21
+ like: e.target.value,
22
+ options: 'i',
23
+ },
24
+ };
25
+ });
26
+ if (e.target.value.trim() === '' || !e.target.value) {
27
+ clearSearch();
28
+ }
29
+ else {
30
+ setSearchString(e.target.value);
31
+ setFilter({
32
+ ...filter,
33
+ where: {
34
+ or: searchProperties,
35
+ },
36
+ limit: 100,
37
+ });
38
+ }
39
+ };
40
+ return (React.createElement(TextField, { autoFocus: true, placeholder: "Search", value: searchString, onChange: (e) => handleSearch(e), size: "medium", fullWidth: true, sx: { marginBottom: '15px', marginTop: '15px' }, InputProps: {
41
+ endAdornment: (React.createElement(InputAdornment, { position: "end" },
42
+ React.createElement(IconButton, { sx: {
43
+ visibility: searchString.length > 0 ? 'visible' : 'hidden',
44
+ }, onClick: () => clearSearch() },
45
+ React.createElement(Clear, { sx: {
46
+ fontSize: '22px',
47
+ } })))),
48
+ startAdornment: (React.createElement(InputAdornment, { position: "start" },
49
+ React.createElement(Search, { sx: { fontSize: '22px', color: '#637381' } }))),
50
+ } }));
51
+ };
52
+ const InstanceLookup = (props) => {
53
+ const { object, setSelectedInstance, setRelationType, filter: criteriaFilter, mode, nestedFieldsView, layout, } = props;
54
+ const [rows, setRows] = useState([]);
55
+ const [loading, setLoading] = useState(false);
56
+ const [filter, setFilter] = useState();
57
+ const [searchString, setSearchString] = useState('');
58
+ const [searchableColumns] = useState(object.properties?.filter((property) => property.searchable) ?? []);
59
+ const apiServices = useApiServices();
60
+ const retrieveColumns = (tableViewLayout) => {
61
+ let columns = [];
62
+ if (tableViewLayout?.properties) {
63
+ for (const prop of tableViewLayout.properties) {
64
+ const propertyId = prop.id.split('.')[0];
65
+ const property = object.properties?.find((p) => p.id === propertyId);
66
+ if (property) {
67
+ if (property.type === 'address') {
68
+ columns.push({
69
+ field: prop.id,
70
+ headerName: `${property.name} - ${startCase(prop.id.split('.')[1])}`,
71
+ type: 'string',
72
+ flex: 1,
73
+ valueGetter: (params) => {
74
+ return get(params.row, prop.id) ?? '';
75
+ },
76
+ });
77
+ }
78
+ else if (property.type === 'object' || property.type === 'user') {
79
+ columns.push({
80
+ field: prop.id,
81
+ headerName: property.name,
82
+ type: 'string',
83
+ flex: 1,
84
+ valueGetter: (params) => {
85
+ return (get(params.row, !prop.id.includes('.') ? `${prop.id}.name` : prop.id) ?? '');
86
+ },
87
+ });
88
+ }
89
+ else if (property.type === 'document') {
90
+ columns.push({
91
+ field: prop.id,
92
+ headerName: property.name,
93
+ type: 'string',
94
+ flex: 1,
95
+ valueGetter: (params) => {
96
+ const row = params.row;
97
+ return (row[prop.id]?.map((v) => v.name).join(', ') ??
98
+ '');
99
+ },
100
+ });
101
+ }
102
+ else if (property.type === 'date') {
103
+ columns.push({
104
+ field: prop.id,
105
+ headerName: property.name,
106
+ type: 'string',
107
+ flex: 1,
108
+ valueFormatter: (params) => {
109
+ if (!params.value) {
110
+ return params.value;
111
+ }
112
+ // The value should really be typed as string | undefined so once asserted as defined
113
+ // the value must be a string.
114
+ return DateTime.fromISO(params.value).toLocaleString(DateTime.DATE_SHORT);
115
+ },
116
+ });
117
+ }
118
+ else if (property.type === 'date-time') {
119
+ columns.push({
120
+ field: prop.id,
121
+ headerName: property.name,
122
+ type: 'string',
123
+ flex: 1,
124
+ valueFormatter: (params) => {
125
+ if (!params.value) {
126
+ return params.value;
127
+ }
128
+ // The value should really be typed as string | undefined so once asserted as defined
129
+ // the value must be a string.
130
+ return DateTime.fromISO(params.value).toLocaleString(DateTime.DATETIME_SHORT);
131
+ },
132
+ });
133
+ }
134
+ else if (property.type === 'time') {
135
+ columns.push({
136
+ field: prop.id,
137
+ headerName: property.name,
138
+ type: 'string',
139
+ flex: 1,
140
+ valueFormatter: (params) => {
141
+ if (!params.value) {
142
+ return params.value;
143
+ }
144
+ // The value should really be typed as string | undefined so once asserted as defined
145
+ // the value must be a string.
146
+ return DateTime.fromISO(params.value).toLocaleString(DateTime.TIME_SIMPLE);
147
+ },
148
+ });
149
+ }
150
+ else {
151
+ columns.push({
152
+ field: prop.id,
153
+ headerName: property.name,
154
+ type: 'string',
155
+ flex: 1,
156
+ });
157
+ }
158
+ }
159
+ }
160
+ }
161
+ else {
162
+ const name = object.properties?.find((property) => property.id == 'name');
163
+ columns = [{ field: 'name', headerName: name?.name ?? 'Name', type: 'string', flex: 1 }];
164
+ }
165
+ return columns;
166
+ };
167
+ function fetchObjectInstance() {
168
+ setLoading(true);
169
+ const combinedFilter = {
170
+ ...criteriaFilter,
171
+ ...filter,
172
+ where: criteriaFilter?.where
173
+ ? {
174
+ and: [criteriaFilter.where, filter?.where],
175
+ }
176
+ : filter?.where,
177
+ };
178
+ apiServices.get(getPrefixedUrl(`/objects/${object.id}/instances`), { params: { filter: JSON.stringify(combinedFilter) } }, (error, objectInstances) => {
179
+ if (error) {
180
+ console.error(error);
181
+ }
182
+ else {
183
+ !!objectInstances && setRows(objectInstances);
184
+ }
185
+ setLoading(false);
186
+ });
187
+ }
188
+ useEffect(() => {
189
+ if (filter || searchString.length) {
190
+ debounce(() => fetchObjectInstance(), 500)();
191
+ }
192
+ else if (searchString === '') {
193
+ setRows([]);
194
+ }
195
+ }, [filter, searchString.length]);
196
+ return (React.createElement(Grid, { container: true, sx: { paddingBottom: '30px' } },
197
+ React.createElement(Grid, { item: true, xs: 12 }, searchableColumns.length ? (React.createElement(SearchField, { searchString: searchString, setSearchString: setSearchString, filter: filter, setFilter: setFilter, searchableColumns: searchableColumns })) : (React.createElement(Typography, { sx: { fontSize: '16px', fontWeight: '700' } }, "There are no searchable properties configured for this object"))),
198
+ React.createElement(BuilderGrid, { item: 'instances', rows: rows, columns: retrieveColumns(layout), onRowClick: (params) => setSelectedInstance(params.row), initialSort: {
199
+ field: object.viewLayout?.table?.sort?.colId ?? 'name',
200
+ sort: object.viewLayout?.table?.sort?.sort ?? 'asc',
201
+ }, sx: {
202
+ height: '360px',
203
+ width: '100%',
204
+ }, hideToolbar: true, loading: loading, hideEmptyContent: true, localeText: {
205
+ noRowsLabel: searchString.length > 0
206
+ ? `No Results Found. Refine your search ${mode !== 'existingOnly' ? 'or create a new record.' : '.'}`
207
+ : 'Search to view results',
208
+ }, noRowsOverlay: React.createElement(Box, { sx: { height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center' } }, searchString.length > 0 ? (React.createElement(React.Fragment, null,
209
+ React.createElement(Box, null,
210
+ React.createElement(Typography, { sx: {
211
+ fontSize: '14px',
212
+ fontWeight: 700,
213
+ textAlign: 'center',
214
+ } }, "No Results Found"),
215
+ React.createElement(Typography, { sx: {
216
+ fontSize: '14px',
217
+ textAlign: 'center',
218
+ } },
219
+ "Refine your search",
220
+ !nestedFieldsView && mode !== 'existingOnly' && (React.createElement(React.Fragment, null,
221
+ ' or ',
222
+ React.createElement(Button, { variant: "text", onClick: () => setRelationType('new'), sx: {
223
+ padding: '3px 5px',
224
+ } }, "enter a new record"))))))) : (React.createElement(Typography, { sx: { fontSize: '16px' } }, "Search to view results"))) })));
225
+ };
226
+ export default InstanceLookup;
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import { ObjectPropertyInputProps } from '../../types';
3
+ declare const ObjectPropertyInput: (props: ObjectPropertyInputProps) => React.JSX.Element;
4
+ export default ObjectPropertyInput;
@@ -0,0 +1,439 @@
1
+ import { useApiServices, useApp, useNavigate, } from '@evoke-platform/context';
2
+ import cleanDeep from 'clean-deep';
3
+ import { cloneDeep, debounce, isEmpty, isNil } from 'lodash';
4
+ import Handlebars from 'no-eval-handlebars';
5
+ import React, { useCallback, useEffect, useState } from 'react';
6
+ import { Close } from '../../../../../../icons';
7
+ import { useFormContext } from '../../../../../../theme/hooks';
8
+ import { Autocomplete, Button, Dialog, IconButton, Link, Paper, Snackbar, TextField, Tooltip, Typography, } from '../../../../../core';
9
+ import { Box } from '../../../../../layout';
10
+ import { encodePageSlug, getDefaultPages, getPrefixedUrl, transformToWhere } from '../../utils';
11
+ import RelatedObjectInstance from './RelatedObjectInstance';
12
+ const ObjectPropertyInput = (props) => {
13
+ const { id, instance, fieldDefinition, handleChangeObjectProperty, nestedFieldsView, readOnly, error, mode, parameters, displayOption, filter, defaultValueCriteria, sortBy, orderBy, isModal, initialValue, fieldHeight, richTextEditor, viewLayout, hasDescription, } = props;
14
+ const { fetchedOptions, setFetchedOptions } = useFormContext();
15
+ const { defaultPages, findDefaultPageSlugFor } = useApp();
16
+ const [selectedInstance, setSelectedInstance] = useState(initialValue || undefined);
17
+ const [openCreateDialog, setOpenCreateDialog] = useState(false);
18
+ const [allDefaultPages, setAllDefaultPages] = useState(defaultPages ?? {});
19
+ const [loadingOptions, setLoadingOptions] = useState(false);
20
+ const [navigationSlug, setNavigationSlug] = useState(fetchedOptions[`${id}NavigationSlug`]);
21
+ const [relatedObject, setRelatedObject] = useState(fetchedOptions[`${id}RelatedObject`]);
22
+ const [dropdownInput, setDropdownInput] = useState();
23
+ const [openOptions, setOpenOptions] = useState(false);
24
+ const [hasFetched, setHasFetched] = useState(fetchedOptions[`${id}OptionsHaveFetched`] || false);
25
+ const [options, setOptions] = useState(fetchedOptions[`${id}Options`] || []);
26
+ const [layout, setLayout] = useState();
27
+ const [appId, setAppId] = useState(fetchedOptions[`${id}AppId`]);
28
+ const [form, setForm] = useState();
29
+ const [snackbarError, setSnackbarError] = useState({
30
+ showAlert: false,
31
+ isError: true,
32
+ });
33
+ const DEFAULT_CREATE_ACTION = '_create';
34
+ const action = relatedObject?.actions?.find((action) => action.id === DEFAULT_CREATE_ACTION);
35
+ const apiServices = useApiServices();
36
+ const navigateTo = useNavigate();
37
+ const updatedCriteria = filter
38
+ ? {
39
+ where: transformToWhere(filter),
40
+ }
41
+ : undefined;
42
+ useEffect(() => {
43
+ if (relatedObject) {
44
+ let defaultViewLayout;
45
+ if (relatedObject?.viewLayout?.dropdown && displayOption === 'dropdown') {
46
+ defaultViewLayout = {
47
+ id: 'default',
48
+ name: 'Default',
49
+ objectId: relatedObject.id,
50
+ ...relatedObject.viewLayout.dropdown,
51
+ };
52
+ }
53
+ else if (relatedObject?.viewLayout?.table && displayOption === 'dialogBox') {
54
+ defaultViewLayout = {
55
+ id: 'default',
56
+ name: 'Default',
57
+ objectId: relatedObject.id,
58
+ ...relatedObject.viewLayout.table,
59
+ };
60
+ }
61
+ if (viewLayout) {
62
+ apiServices
63
+ .get(getPrefixedUrl(`/objects/${viewLayout.objectId}/${displayOption === 'dropdown' ? 'dropdown' : 'table'}Layouts/${viewLayout.id}`))
64
+ .then(setLayout)
65
+ .catch((err) => setLayout(defaultViewLayout));
66
+ }
67
+ else {
68
+ setLayout(defaultViewLayout);
69
+ }
70
+ }
71
+ }, [displayOption, relatedObject, viewLayout]);
72
+ useEffect(() => {
73
+ // setting the default value when there is default criteria
74
+ if (!isEmpty(defaultValueCriteria) && !selectedInstance && (!instance || !instance[id])) {
75
+ const updatedFilter = cleanDeep({
76
+ where: transformToWhere({ $and: [defaultValueCriteria, updatedCriteria?.where ?? {}] }),
77
+ order: orderBy && sortBy ? encodeURIComponent(sortBy + ' ' + orderBy) : undefined,
78
+ limit: 1,
79
+ });
80
+ if (updatedFilter.where) {
81
+ setLoadingOptions(true);
82
+ apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${encodeURIComponent(JSON.stringify(updatedFilter))}`), (error, instances) => {
83
+ if (error) {
84
+ console.error(error);
85
+ setLoadingOptions(false);
86
+ }
87
+ if (instances && instances.length > 0) {
88
+ setSelectedInstance(instances[0]);
89
+ handleChangeObjectProperty(id, instances[0]);
90
+ }
91
+ setLoadingOptions(false);
92
+ });
93
+ }
94
+ }
95
+ }, [fieldDefinition, defaultValueCriteria, sortBy, orderBy]);
96
+ const getDropdownOptions = useCallback((name) => {
97
+ if ((!fetchedOptions[`${id}Options`] ||
98
+ fetchedOptions[`${id}Options`].length === 0) &&
99
+ !hasFetched) {
100
+ setLoadingOptions(true);
101
+ const updatedFilter = cloneDeep(updatedCriteria) || {};
102
+ updatedFilter.limit = 100;
103
+ const { propertyId, direction } = layout?.sort ?? {
104
+ propertyId: 'name',
105
+ direction: 'asc',
106
+ };
107
+ updatedFilter.order = `${propertyId} ${direction}`;
108
+ const where = name
109
+ ? transformToWhere({
110
+ name: {
111
+ like: name,
112
+ options: 'i',
113
+ },
114
+ })
115
+ : {};
116
+ updatedFilter.where = updatedFilter.where
117
+ ? {
118
+ and: [updatedFilter.where, where],
119
+ }
120
+ : where;
121
+ apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/instances?filter=${JSON.stringify(updatedFilter)}`), (error, instances) => {
122
+ if (error) {
123
+ console.error(error);
124
+ setLoadingOptions(false);
125
+ }
126
+ if (instances) {
127
+ setOptions(instances);
128
+ setLoadingOptions(false);
129
+ // so if you go off a section too quickly and it doesn't fetch it re-fetches but doesn't cause an infinite loop
130
+ setHasFetched(true);
131
+ }
132
+ });
133
+ }
134
+ }, [relatedObject, setLoadingOptions, setOptions, fieldDefinition, updatedCriteria, layout]);
135
+ useEffect(() => {
136
+ if (displayOption === 'dropdown') {
137
+ getDropdownOptions();
138
+ }
139
+ }, [fieldDefinition, updatedCriteria, displayOption]);
140
+ useEffect(() => {
141
+ setSelectedInstance(initialValue);
142
+ }, [initialValue]);
143
+ const debouncedGetDropdownOptions = useCallback(debounce(getDropdownOptions, 200), [updatedCriteria]);
144
+ useEffect(() => {
145
+ debouncedGetDropdownOptions(dropdownInput);
146
+ return () => debouncedGetDropdownOptions.cancel();
147
+ }, [dropdownInput]);
148
+ useEffect(() => {
149
+ if (action?.defaultFormId) {
150
+ apiServices
151
+ .get(getPrefixedUrl(`data/forms/${action.defaultFormId}`))
152
+ .then((evokeForm) => {
153
+ setForm(evokeForm);
154
+ })
155
+ .catch((error) => {
156
+ console.error('Error fetching form:', error);
157
+ });
158
+ }
159
+ }, [action]);
160
+ useEffect(() => {
161
+ if (!fetchedOptions[`${id}RelatedObject`]) {
162
+ apiServices.get(getPrefixedUrl(`/objects/${fieldDefinition.objectId}/effective?sanitizedVersion=true`), (error, object) => {
163
+ if (error) {
164
+ console.error(error);
165
+ }
166
+ else {
167
+ setRelatedObject(object);
168
+ }
169
+ });
170
+ }
171
+ }, [fieldDefinition]);
172
+ useEffect(() => {
173
+ const fetchDefaultPages = async () => {
174
+ if (parameters) {
175
+ const pages = await getDefaultPages(parameters, defaultPages, findDefaultPageSlugFor);
176
+ setAllDefaultPages(pages);
177
+ }
178
+ };
179
+ fetchDefaultPages();
180
+ }, []);
181
+ useEffect(() => {
182
+ if (fieldDefinition.objectId &&
183
+ allDefaultPages &&
184
+ allDefaultPages[fieldDefinition.objectId] &&
185
+ (!fetchedOptions?.[`${id}NavigationSlug`] || !fetchedOptions[`${id}AppId`])) {
186
+ apiServices.get(getPrefixedUrl(`/apps/${allDefaultPages[fieldDefinition.objectId].split('/').slice(1, 2)}/pages/${encodePageSlug(allDefaultPages[fieldDefinition.objectId].split('/').slice(2).join('/'))}`), (error, page) => {
187
+ if (error) {
188
+ console.error(error);
189
+ }
190
+ else {
191
+ setAppId(page?.appId);
192
+ setNavigationSlug(page?.slug);
193
+ }
194
+ });
195
+ }
196
+ }, []);
197
+ const handleClose = () => {
198
+ setOpenCreateDialog(false);
199
+ };
200
+ const compileExpression = (expression, instance) => {
201
+ const template = Handlebars.compileAST(expression);
202
+ return instance ? template(instance) : undefined;
203
+ };
204
+ useEffect(() => {
205
+ if (relatedObject && !fetchedOptions[`${id}RelatedObject`]) {
206
+ setFetchedOptions({
207
+ [`${id}RelatedObject`]: relatedObject,
208
+ });
209
+ }
210
+ if (options &&
211
+ (!fetchedOptions[`${id}Options`] || fetchedOptions[`${id}Options`].length === 0) &&
212
+ hasFetched &&
213
+ !fetchedOptions[`${id}OptionsHaveFetched`]) {
214
+ setFetchedOptions({
215
+ [`${id}Options`]: options,
216
+ [`${id}OptionsHaveFetched`]: hasFetched,
217
+ });
218
+ }
219
+ if (navigationSlug && !fetchedOptions[`${id}NavigationSlug`]) {
220
+ setFetchedOptions({
221
+ [`${id}NavigationSlug`]: navigationSlug,
222
+ });
223
+ }
224
+ }, [relatedObject, options, hasFetched]);
225
+ return (React.createElement(React.Fragment, null,
226
+ displayOption === 'dropdown' ? (React.createElement(React.Fragment, null,
227
+ React.createElement(Autocomplete, { id: id, fullWidth: true, sortBy: "NONE", open: openOptions, size: fieldHeight, componentsProps: {
228
+ popper: {
229
+ modifiers: [
230
+ {
231
+ name: 'flip',
232
+ enabled: false,
233
+ },
234
+ ],
235
+ },
236
+ }, PaperComponent: ({ children }) => {
237
+ return (React.createElement(Paper, { sx: {
238
+ borderRadius: '12px',
239
+ boxShadow: '0px 24px 48px 0px rgba(145, 158, 171, 0.2)',
240
+ '& .MuiAutocomplete-listbox': {
241
+ maxHeight: '25vh',
242
+ },
243
+ '& .MuiAutocomplete-noOptions': {
244
+ fontFamily: 'sans-serif',
245
+ fontSize: '14px',
246
+ paddingLeft: '24px',
247
+ color: 'rgba(145, 158, 171, 1)',
248
+ },
249
+ '& .MuiAutocomplete-loading': {
250
+ fontFamily: 'sans-serif',
251
+ fontSize: '14px',
252
+ paddingLeft: '24px',
253
+ color: 'rgba(145, 158, 171, 1)',
254
+ },
255
+ } },
256
+ mode !== 'newOnly' && children,
257
+ mode !== 'existingOnly' && form && (React.createElement(Button, { fullWidth: true, sx: {
258
+ justifyContent: 'flex-start',
259
+ pl: 2,
260
+ minHeight: '48px',
261
+ borderTop: '1px solid rgba(145, 158, 171, 0.24)',
262
+ borderRadius: '0p 0pc 6px 6px',
263
+ paddingLeft: '22px',
264
+ fontWeight: 400,
265
+ }, onMouseDown: (e) => {
266
+ setOpenCreateDialog(true);
267
+ }, color: 'inherit' }, "+ Add New"))));
268
+ }, sx: {
269
+ '& button.MuiButtonBase-root': {
270
+ ...(!loadingOptions &&
271
+ (instance?.[id]?.id || selectedInstance?.id)
272
+ ? {
273
+ visibility: 'visible',
274
+ }
275
+ : {}),
276
+ },
277
+ backgroundColor: 'white', // prevents the field flickering gray when loading in the value and options when it's a read only
278
+ }, noOptionsText: 'No options available', renderOption: (props, option) => {
279
+ return (React.createElement("li", { ...props, key: option.id },
280
+ React.createElement(Box, null,
281
+ React.createElement(Typography, { sx: { marginLeft: '8px', fontSize: '14px' } }, option.label),
282
+ layout?.secondaryTextExpression ? (React.createElement(Typography, { sx: { marginLeft: '8px', fontSize: '14px', color: '#637381' } }, compileExpression(layout?.secondaryTextExpression, options.find((o) => o.id === option.value)))) : null)));
283
+ }, onOpen: () => {
284
+ if (instance?.[id]?.id || selectedInstance?.id) {
285
+ setOpenOptions(false);
286
+ }
287
+ else {
288
+ setOpenOptions(true);
289
+ }
290
+ }, onClose: () => setOpenOptions(false), value: selectedInstance?.id
291
+ ? {
292
+ value: selectedInstance?.id ?? '',
293
+ label: selectedInstance?.name ?? '',
294
+ }
295
+ : null, isOptionEqualToValue: (option, value) => {
296
+ if (typeof value === 'string') {
297
+ return option.value === value;
298
+ }
299
+ return option.value === value?.value;
300
+ }, options: options.map((o) => ({ label: o.name, value: o.id })), getOptionLabel: (option) => {
301
+ return typeof option === 'string'
302
+ ? (options.find((o) => o.id === option)?.name ?? '')
303
+ : option.label;
304
+ }, onKeyDownCapture: (e) => {
305
+ // prevents keyboard trap
306
+ if (e.key === 'Tab') {
307
+ return;
308
+ }
309
+ if (selectedInstance?.id) {
310
+ e.preventDefault();
311
+ }
312
+ }, loading: loadingOptions, onChange: (event, value) => {
313
+ if (isNil(value)) {
314
+ setDropdownInput(undefined);
315
+ setSelectedInstance(undefined);
316
+ handleChangeObjectProperty(id, null);
317
+ }
318
+ else {
319
+ const selectedInstance = options.find((o) => o.id === value?.value);
320
+ setSelectedInstance(selectedInstance);
321
+ handleChangeObjectProperty(id, selectedInstance);
322
+ }
323
+ }, selectOnFocus: false, onBlur: () => setDropdownInput(undefined), renderInput: (params) => (React.createElement(TextField, { ...params, placeholder: selectedInstance?.id || readOnly ? '' : 'Select', readOnly: !loadingOptions && !selectedInstance?.id && readOnly, onChange: (event) => setDropdownInput(event.target.value), onClick: (e) => {
324
+ if (openOptions &&
325
+ e.target?.nodeName === 'svg') {
326
+ setOpenOptions(false);
327
+ }
328
+ else {
329
+ setOptions(options);
330
+ setOpenOptions(true);
331
+ }
332
+ }, sx: {
333
+ ...(!loadingOptions && selectedInstance?.id
334
+ ? {
335
+ '.MuiOutlinedInput-root': {
336
+ background: 'white',
337
+ border: 'auto',
338
+ '.MuiOutlinedInput-input': {
339
+ ...(navigationSlug
340
+ ? {
341
+ color: 'rgb(1, 78, 123)',
342
+ textDecoration: 'underline',
343
+ cursor: 'pointer',
344
+ }
345
+ : {}),
346
+ },
347
+ },
348
+ '& fieldset': {
349
+ border: readOnly ? 'none' : 'auto',
350
+ borderColor: 'auto',
351
+ },
352
+ '&:hover .MuiOutlinedInput-notchedOutline': {
353
+ border: 'auto',
354
+ },
355
+ '& svg': {
356
+ display: readOnly ? 'none' : 'block',
357
+ },
358
+ caretColor: 'white',
359
+ }
360
+ : {}),
361
+ }, InputProps: {
362
+ ...params.InputProps,
363
+ startAdornment: selectedInstance?.id ? (React.createElement(Typography, { onClick: (e) => {
364
+ if (navigationSlug && selectedInstance?.id) {
365
+ navigateTo(`/${appId}/${navigationSlug.replace(':instanceId', selectedInstance.id)}`);
366
+ }
367
+ }, sx: {
368
+ cursor: navigationSlug ? 'pointer' : 'default',
369
+ color: navigationSlug ? 'rgb(1, 78, 123)' : 'inherit',
370
+ textDecoration: navigationSlug ? 'underline' : 'none',
371
+ paddingLeft: '5px',
372
+ } }, selectedInstance?.name || '')) : null,
373
+ }, inputProps: {
374
+ ...params.inputProps,
375
+ value: selectedInstance?.id ? '' : params.inputProps.value,
376
+ ...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined),
377
+ } })), readOnly: readOnly, error: error }))) : (React.createElement(Box, { sx: {
378
+ padding: selectedInstance?.name ? '16.5px 14px' : '10.5px 0',
379
+ } },
380
+ (selectedInstance?.name || readOnly) && (React.createElement(Link, { sx: {
381
+ textDecoration: 'none',
382
+ marginRight: 1,
383
+ textDecorationLine: navigationSlug && instance?.[id] && !isModal ? 'underline' : 'none',
384
+ cursor: navigationSlug && instance?.[id] && !isModal ? 'pointer' : 'auto',
385
+ fontSize: '16px',
386
+ fontWeight: 400,
387
+ lineHeight: '24px',
388
+ textAlign: 'left',
389
+ color: navigationSlug && instance?.[id] && !isModal
390
+ ? '#0075A7'
391
+ : !selectedInstance?.id
392
+ ? '#999'
393
+ : '#212B36',
394
+ }, variant: "body2", href: navigationSlug && !isModal
395
+ ? `${'/app'}/${appId}/${navigationSlug.replace(':instanceId', selectedInstance?.id ?? '')}`
396
+ : undefined, "aria-label": selectedInstance?.name }, selectedInstance?.name ? selectedInstance?.name : readOnly && 'None')),
397
+ !readOnly && (selectedInstance || (nestedFieldsView && instance?.id)) ? (React.createElement(Tooltip, { title: `Unlink` },
398
+ React.createElement("span", null,
399
+ React.createElement(IconButton, { onClick: (event) => {
400
+ event.stopPropagation();
401
+ handleChangeObjectProperty(id, null);
402
+ setSelectedInstance(undefined);
403
+ }, sx: { p: 0, marginBottom: '4px' }, "aria-label": `Unlink` },
404
+ React.createElement(Close, { sx: { width: '20px', height: '20px' } }))))) : (React.createElement(Button, { sx: {
405
+ backgroundColor: '#DFE3E8',
406
+ color: '#212B36',
407
+ fontWeight: '700',
408
+ textTransform: 'capitalize',
409
+ '&:hover': {
410
+ backgroundColor: '#e6eaf0',
411
+ boxShadow: 'none',
412
+ },
413
+ '&:disabled': {
414
+ cursor: 'not-allowed',
415
+ pointerEvents: 'all',
416
+ },
417
+ boxShadow: 'none',
418
+ lineHeight: '2.75',
419
+ padding: '0 23px',
420
+ display: readOnly ? 'none' : 'block',
421
+ }, size: "small", variant: "contained", disableElevation: true, onClick: (event) => {
422
+ event.stopPropagation();
423
+ setOpenCreateDialog(true);
424
+ }, "aria-label": `Add` }, "Add")))),
425
+ openCreateDialog && (React.createElement(React.Fragment, null, nestedFieldsView ? (React.createElement(RelatedObjectInstance, { id: id, handleClose: handleClose, handleChangeObjectProperty: handleChangeObjectProperty, setSelectedInstance: setSelectedInstance, relatedObject: relatedObject, nestedFieldsView: nestedFieldsView, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, fieldHeight: fieldHeight, richTextEditor: richTextEditor, form: form, action: action, setSnackbarError: setSnackbarError, selectedInstance: selectedInstance })) : (React.createElement(Dialog, { fullWidth: true, maxWidth: "md", open: openCreateDialog, onClose: (e, reason) => reason !== 'backdropClick' && handleClose },
426
+ React.createElement(Typography, { sx: {
427
+ marginTop: '28px',
428
+ fontSize: '22px',
429
+ fontWeight: 700,
430
+ marginLeft: '24px',
431
+ marginBottom: '10px',
432
+ } }, `Add ${fieldDefinition.name}`),
433
+ React.createElement(RelatedObjectInstance, { handleClose: handleClose, handleChangeObjectProperty: handleChangeObjectProperty, setSelectedInstance: setSelectedInstance, nestedFieldsView: nestedFieldsView, relatedObject: relatedObject, id: id, mode: mode, displayOption: displayOption, setOptions: setOptions, options: options, filter: updatedCriteria, layout: layout, fieldHeight: fieldHeight, richTextEditor: richTextEditor, form: form, action: action, setSnackbarError: setSnackbarError, selectedInstance: selectedInstance }))))),
434
+ React.createElement(Snackbar, { open: snackbarError.showAlert, handleClose: () => setSnackbarError({
435
+ isError: snackbarError.isError,
436
+ showAlert: false,
437
+ }), message: snackbarError.message, error: snackbarError.isError })));
438
+ };
439
+ export default ObjectPropertyInput;
@@ -0,0 +1,29 @@
1
+ import { Action, EvokeForm, Obj, ObjectInstance, TableViewLayout } from '@evoke-platform/context';
2
+ import React, { ComponentType } from 'react';
3
+ import { BaseProps, SimpleEditorProps } from '../../types';
4
+ export type RelatedObjectInstanceProps = BaseProps & {
5
+ relatedObject: Obj | undefined;
6
+ id: string;
7
+ setSelectedInstance: (selectedInstance: ObjectInstance) => void;
8
+ handleChangeObjectProperty: (propertyId: string, instance?: any) => void;
9
+ handleClose: () => void;
10
+ mode: 'default' | 'existingOnly' | 'newOnly';
11
+ setSnackbarError: React.Dispatch<React.SetStateAction<{
12
+ showAlert: boolean;
13
+ message?: string;
14
+ isError: boolean;
15
+ }>>;
16
+ nestedFieldsView?: boolean;
17
+ displayOption?: 'dropdown' | 'dialogBox';
18
+ setOptions: (options: ObjectInstance[]) => void;
19
+ options: ObjectInstance[];
20
+ filter?: Record<string, unknown>;
21
+ layout?: TableViewLayout;
22
+ fieldHeight?: 'small' | 'medium';
23
+ richTextEditor?: ComponentType<SimpleEditorProps>;
24
+ form?: EvokeForm;
25
+ action?: Action;
26
+ selectedInstance?: ObjectInstance;
27
+ };
28
+ declare const RelatedObjectInstance: (props: RelatedObjectInstanceProps) => React.JSX.Element;
29
+ export default RelatedObjectInstance;
@@ -0,0 +1,74 @@
1
+ import { InfoRounded } from '@mui/icons-material';
2
+ import React, { useState } from 'react';
3
+ import { Alert, Button, FormControlLabel, Radio, RadioGroup } from '../../../../../core';
4
+ import { Box, Grid } from '../../../../../layout';
5
+ import InstanceLookup from './InstanceLookup';
6
+ const styles = {
7
+ actionButtons: {
8
+ padding: '8px 8px 24px 8px',
9
+ marginRight: '18px',
10
+ display: 'flex',
11
+ justifyContent: 'flex-end',
12
+ alignItems: 'center',
13
+ },
14
+ };
15
+ const RelatedObjectInstance = (props) => {
16
+ const { relatedObject, id, setSelectedInstance, handleChangeObjectProperty, handleClose, nestedFieldsView, mode, displayOption, filter, layout, form, } = props;
17
+ const [errors, setErrors] = useState([]);
18
+ const [selectedRow, setSelectedRow] = useState();
19
+ const [relationType, setRelationType] = useState(displayOption === 'dropdown' ? 'new' : 'existing');
20
+ const linkExistingInstance = async () => {
21
+ if (selectedRow) {
22
+ setSelectedInstance(selectedRow);
23
+ handleChangeObjectProperty(id, selectedRow);
24
+ }
25
+ onClose();
26
+ };
27
+ const onClose = () => {
28
+ handleClose();
29
+ setErrors([]);
30
+ };
31
+ return (React.createElement(Box, { sx: {
32
+ background: nestedFieldsView ? '#F4F6F8' : 'none',
33
+ borderRadius: '8px',
34
+ } },
35
+ React.createElement(Box, { sx: {
36
+ padding: '8px 24px 0px',
37
+ '.MuiInputBase-root': { background: '#FFFF', borderRadius: '8px' },
38
+ } },
39
+ !nestedFieldsView &&
40
+ displayOption !== 'dropdown' &&
41
+ mode !== 'existingOnly' &&
42
+ mode !== 'newOnly' &&
43
+ form && (React.createElement(Grid, { container: true },
44
+ React.createElement(Grid, { container: true, item: true },
45
+ React.createElement(RadioGroup, { row: true, "aria-labelledby": "related-object-link-type", onChange: (event) => {
46
+ event.target.value === 'existing' && setErrors([]);
47
+ setRelationType(event.target.value);
48
+ }, value: relationType },
49
+ React.createElement(FormControlLabel, { value: "existing", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "Existing" }),
50
+ React.createElement(FormControlLabel, { value: "new", control: React.createElement(Radio, { sx: { '&.Mui-checked': { color: 'primary' } } }), label: "New" }))))),
51
+ errors?.length ? (React.createElement(Alert, { severity: "error", sx: { margin: '10px 0', borderRadius: '8px' }, icon: React.createElement(InfoRounded, { sx: { color: '#A22723' } }) }, errors.length === 1 ? (React.createElement(React.Fragment, null,
52
+ "There is ",
53
+ React.createElement("strong", null, "1"),
54
+ " error")) : (React.createElement(React.Fragment, null,
55
+ "There are ",
56
+ React.createElement("strong", null, errors.length),
57
+ " errors")))) : undefined,
58
+ (relationType === 'new' || mode === 'newOnly') && form ? (React.createElement(Box, { id: 'related-object-wrapper', sx: { width: '100%' } })) : (relatedObject &&
59
+ mode !== 'newOnly' && (React.createElement(React.Fragment, null,
60
+ React.createElement(InstanceLookup, { colspan: 12, nestedFieldsView: nestedFieldsView, setRelationType: setRelationType, object: relatedObject, setSelectedInstance: setSelectedRow, mode: mode, filter: filter, layout: layout }))))),
61
+ relationType !== 'new' && mode !== 'newOnly' && (React.createElement(Box, { sx: styles.actionButtons },
62
+ React.createElement(Button, { onClick: onClose, color: 'inherit', sx: {
63
+ border: '1px solid #ced4da',
64
+ width: '75px',
65
+ marginRight: '0px',
66
+ color: 'black',
67
+ } }, "Cancel"),
68
+ React.createElement(Button, { sx: {
69
+ marginLeft: '8px',
70
+ width: '85px',
71
+ '&:hover': { boxShadow: 'none' },
72
+ }, onClick: linkExistingInstance, variant: "contained", disabled: !selectedRow, "aria-label": `Add` }, "Add")))));
73
+ };
74
+ export default RelatedObjectInstance;
@@ -1,3 +1,7 @@
1
+ import { InputParameter, ObjectInstance, Property, ViewLayoutEntityReference } from '@evoke-platform/context';
2
+ import { GridSize } from '@mui/material';
3
+ import { ComponentType } from 'react';
4
+ import { FieldValues } from 'react-hook-form';
1
5
  export type FieldAddress = {
2
6
  line1?: string;
3
7
  line2?: string;
@@ -24,3 +28,60 @@ export type Document = {
24
28
  };
25
29
  versionId?: string;
26
30
  };
31
+ export type SimpleEditorProps = {
32
+ id: string;
33
+ value: string;
34
+ handleUpdate?: (value: string) => void;
35
+ format: 'rtf' | 'plain' | 'openxml';
36
+ disabled?: boolean;
37
+ rows?: number;
38
+ hasError?: boolean;
39
+ viewHistory?: boolean;
40
+ };
41
+ export type ObjectPropertyInputProps = {
42
+ id: string;
43
+ instance?: FieldValues;
44
+ fieldDefinition: InputParameter;
45
+ handleChangeObjectProperty: (propertyId: string, instance?: ObjectInstance | null) => void;
46
+ mode: 'default' | 'existingOnly' | 'newOnly';
47
+ nestedFieldsView?: boolean;
48
+ readOnly?: boolean;
49
+ error?: boolean;
50
+ displayOption?: 'dropdown' | 'dialogBox';
51
+ filter?: Record<string, unknown>;
52
+ defaultValueCriteria?: Record<string, unknown>;
53
+ sortBy?: string;
54
+ orderBy?: string;
55
+ isModal?: boolean;
56
+ label?: string;
57
+ initialValue?: ObjectInstance | null;
58
+ fieldHeight?: 'small' | 'medium';
59
+ richTextEditor?: ComponentType<SimpleEditorProps>;
60
+ viewLayout?: ViewLayoutEntityReference;
61
+ hasDescription?: boolean;
62
+ parameters?: InputParameter[];
63
+ };
64
+ export type Page = {
65
+ id: string;
66
+ name: string;
67
+ slug?: string;
68
+ pk?: string;
69
+ appId?: string;
70
+ pathParameters?: {
71
+ id: string;
72
+ }[];
73
+ children?: unknown[];
74
+ };
75
+ export type SearchFieldProps = {
76
+ searchableColumns: Property[];
77
+ filter: Record<string, unknown> | undefined;
78
+ setFilter: (filter: Record<string, unknown> | undefined) => void;
79
+ searchString: string;
80
+ setSearchString: (searchString: string) => void;
81
+ };
82
+ export type BaseProps = {
83
+ colspan?: GridSize;
84
+ baseUrl?: string;
85
+ getWidgetState?: () => any;
86
+ saveWidgetState?: (widgetState: unknown) => void;
87
+ };
@@ -33,3 +33,7 @@ export declare const getMiddleObjectFilter: (fieldDefinition: InputParameter | P
33
33
  [x: string]: string;
34
34
  };
35
35
  };
36
+ export declare const encodePageSlug: (slug: string) => string;
37
+ export declare function getDefaultPages(parameters: InputParameter[], defaultPages: Record<string, string> | undefined, findDefaultPageSlugFor: (objectId: string) => Promise<string | undefined>): Promise<{
38
+ [x: string]: string;
39
+ }>;
@@ -247,3 +247,23 @@ export const getMiddleObjectFilter = (fieldDefinition, instanceId) => {
247
247
  const filter = { where: { [filterProperty]: instanceId } };
248
248
  return filter;
249
249
  };
250
+ export const encodePageSlug = (slug) => {
251
+ return encodeURIComponent(encodeURIComponent(slug));
252
+ };
253
+ export async function getDefaultPages(parameters, defaultPages, findDefaultPageSlugFor) {
254
+ let foundDefaultPages = defaultPages;
255
+ const relatedObjectProperties = parameters?.filter((param) => param.type === 'object');
256
+ if (relatedObjectProperties) {
257
+ foundDefaultPages = await relatedObjectProperties.reduce(async (acc, parameter) => {
258
+ const result = await acc;
259
+ if (parameter.objectId && defaultPages && defaultPages[parameter.objectId]) {
260
+ const slug = await findDefaultPageSlugFor(parameter.objectId);
261
+ if (slug) {
262
+ return { ...result, [parameter.objectId]: slug };
263
+ }
264
+ }
265
+ return result;
266
+ }, Promise.resolve({}));
267
+ }
268
+ return { ...defaultPages, ...foundDefaultPages };
269
+ }
@@ -60,21 +60,17 @@ export const ResponsiveOverflow = (props) => {
60
60
  }
61
61
  });
62
62
  const availableWidth = containerRef.current.getBoundingClientRect().width;
63
+ const moreButtonWidth = moreButtonBoxRef.current?.getBoundingClientRect().width ?? 0;
63
64
  let usedWidth = 0;
64
65
  let maxFit = children.length;
65
- // Determine which items fit and which overflow
66
+ // Determine which items fit, accounting for overflow button width
66
67
  for (let i = 0; i < itemRefs.current.length; i++) {
67
68
  const item = itemRefs.current[i];
68
69
  if (!item) {
69
70
  continue;
70
71
  }
71
72
  const itemWidth = item.getBoundingClientRect().width;
72
- // Calculate if we need the "More" button
73
- const remainingItems = itemRefs.current.length - i - 1;
74
- const moreButtonWidth = remainingItems > 0 && moreButtonBoxRef.current
75
- ? moreButtonBoxRef.current.getBoundingClientRect().width
76
- : 0;
77
- if (usedWidth + itemWidth + (remainingItems > 0 ? moreButtonWidth : 0) > availableWidth) {
73
+ if (usedWidth + itemWidth + moreButtonWidth > availableWidth) {
78
74
  maxFit = i;
79
75
  break;
80
76
  }
@@ -10,8 +10,7 @@ InputField.args = {
10
10
  property: {
11
11
  id: 'fullName',
12
12
  name: 'Full Name',
13
- type: 'text',
14
- // enum?: string[],
13
+ type: 'string',
15
14
  required: true,
16
15
  },
17
16
  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.20",
3
+ "version": "1.6.0-dev.22",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",