@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.
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.d.ts +3 -2
- package/dist/published/components/custom/CriteriaBuilder/CriteriaBuilder.js +1 -1
- package/dist/published/components/custom/FormField/AddressFieldComponent/AddressFieldComponent.test.js +1 -1
- package/dist/published/components/custom/FormField/FormField.d.ts +2 -3
- package/dist/published/components/custom/FormField/Select/Select.test.js +41 -16
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.d.ts +15 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/InstanceLookup.js +226 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.d.ts +4 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/ObjectPropertyInput.js +439 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.d.ts +29 -0
- package/dist/published/components/custom/FormV2/components/FormFieldTypes/relatedObjectFiles/RelatedObjectInstance.js +74 -0
- package/dist/published/components/custom/FormV2/components/types.d.ts +61 -0
- package/dist/published/components/custom/FormV2/components/utils.d.ts +4 -0
- package/dist/published/components/custom/FormV2/components/utils.js +20 -0
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.js +3 -7
- package/dist/published/stories/FormField.stories.js +1 -2
- 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 {
|
|
5
|
+
import { Operator, PresetValue, TreeViewObject } from './types';
|
|
5
6
|
import { ValueEditorProps } from './ValueEditor';
|
|
6
7
|
export type CriteriaInputProps = {
|
|
7
|
-
properties:
|
|
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
|
}
|
|
@@ -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:
|
|
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: '
|
|
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' }),
|
|
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' }),
|
|
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'],
|
|
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'],
|
|
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'],
|
|
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: '
|
|
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,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
|
|
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
|
-
|
|
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
|
}
|