@evoke-platform/ui-components 1.5.1-testing.1 → 1.6.0-dev.10
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/core/SwipeableDrawer/SwipeableDrawer.d.ts +4 -0
- package/dist/published/components/core/SwipeableDrawer/SwipeableDrawer.js +8 -0
- package/dist/published/components/core/SwipeableDrawer/index.d.ts +3 -0
- package/dist/published/components/core/SwipeableDrawer/index.js +3 -0
- package/dist/published/components/core/index.d.ts +2 -1
- package/dist/published/components/core/index.js +2 -1
- package/dist/published/components/custom/BuilderGrid/BuilderGrid.js +27 -27
- package/dist/published/components/custom/Form/Common/FormComponentWrapper.js +2 -13
- package/dist/published/components/custom/Form/FormComponents/FormFieldComponent.d.ts +1 -0
- package/dist/published/components/custom/Form/FormComponents/FormFieldComponent.js +1 -1
- package/dist/published/components/custom/Form/FormComponents/RepeatableFieldComponent/RepeatableField.js +87 -57
- package/dist/published/components/custom/Form/tests/Form.test.js +1 -1
- package/dist/published/components/custom/Form/utils.js +1 -0
- package/dist/published/components/custom/FormField/AddressFieldComponent/addressFieldComponent.js +1 -1
- package/dist/published/components/custom/FormField/BooleanSelect/BooleanSelect.js +3 -3
- package/dist/published/components/custom/FormField/DatePickerSelect/DatePickerSelect.js +7 -1
- package/dist/published/components/custom/FormField/DateTimePickerSelect/DateTimePickerSelect.js +7 -1
- package/dist/published/components/custom/FormField/FormField.d.ts +1 -0
- package/dist/published/components/custom/FormField/FormField.js +2 -1
- package/dist/published/components/custom/FormField/InputFieldComponent/InputFieldComponent.js +6 -3
- package/dist/published/components/custom/FormField/Select/Select.js +132 -20
- package/dist/published/components/custom/FormField/Select/Select.test.js +72 -3
- package/dist/published/components/custom/FormField/TimePickerSelect/TimePickerSelect.js +13 -3
- package/dist/published/components/custom/HistoryLog/HistoryData.d.ts +1 -0
- package/dist/published/components/custom/HistoryLog/HistoryData.js +14 -6
- package/dist/published/components/custom/HistoryLog/HistoryLoading.d.ts +4 -1
- package/dist/published/components/custom/HistoryLog/HistoryLoading.js +14 -8
- package/dist/published/components/custom/HistoryLog/index.d.ts +2 -0
- package/dist/published/components/custom/HistoryLog/index.js +4 -4
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.d.ts +33 -0
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.js +143 -0
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.test.d.ts +1 -0
- package/dist/published/components/custom/ResponsiveOverflow/ResponsiveOverflow.test.js +100 -0
- package/dist/published/components/custom/ResponsiveOverflow/index.d.ts +4 -0
- package/dist/published/components/custom/ResponsiveOverflow/index.js +3 -0
- package/dist/published/components/custom/index.d.ts +1 -0
- package/dist/published/components/custom/index.js +1 -0
- package/dist/published/index.d.ts +2 -2
- package/dist/published/index.js +2 -2
- package/dist/published/stories/ResponsiveOverflow.stories.d.ts +8 -0
- package/dist/published/stories/ResponsiveOverflow.stories.js +91 -0
- package/dist/published/theme/hooks.d.ts +78 -0
- package/dist/published/theme/hooks.js +65 -0
- package/dist/published/theme/hooks.test.d.ts +1 -0
- package/dist/published/theme/hooks.test.js +187 -0
- package/dist/published/theme/index.d.ts +2 -2
- package/dist/published/theme/index.js +2 -2
- package/package.json +2 -1
|
@@ -1,23 +1,60 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { createFilterOptions, List, ListSubheader } from '@mui/material';
|
|
2
|
+
import { uniq } from 'lodash';
|
|
3
|
+
import React, { forwardRef, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Clear } from '../../../../icons';
|
|
5
|
+
import { Autocomplete, FormControl, FormControlLabel, IconButton, Radio, RadioGroup, TextField, Typography, } from '../../../core';
|
|
6
|
+
import { Box } from '../../../layout';
|
|
3
7
|
import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
|
|
8
|
+
const filter = createFilterOptions();
|
|
4
9
|
const Select = (props) => {
|
|
5
|
-
const { id, property, defaultValue, error, errorMessage, onBlur, readOnly, selectOptions, required, size, isOptionEqualToValue, renderOption, getOptionLabel, disableCloseOnSelect, additionalProps, displayOption, sortBy, } = props;
|
|
10
|
+
const { id, property, defaultValue, error, errorMessage, onBlur, onChange, readOnly, isCombobox, selectOptions, required, size, isOptionEqualToValue, renderOption, getOptionLabel, disableCloseOnSelect, additionalProps, displayOption, sortBy, } = props;
|
|
11
|
+
const otherInputRef = useRef(null);
|
|
12
|
+
const [isOther, setIsOther] = useState(!!isCombobox &&
|
|
13
|
+
!!defaultValue &&
|
|
14
|
+
!selectOptions?.some((option) => (typeof option === 'string' && option === defaultValue) ||
|
|
15
|
+
option.value === defaultValue));
|
|
16
|
+
const [isOtherFocused, setIsOtherFocused] = useState(false);
|
|
6
17
|
const [value, setValue] = useState(defaultValue);
|
|
7
|
-
const [inputValue, setInputValue] = useState('')
|
|
18
|
+
const [inputValue, setInputValue] = useState(!selectOptions?.some((option) => (typeof option === 'string' && option === defaultValue) ||
|
|
19
|
+
option.value === defaultValue) && property.type !== 'array'
|
|
20
|
+
? defaultValue
|
|
21
|
+
: '');
|
|
22
|
+
const [errorState, setErrorState] = useState();
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setErrorState(isCombobox ? error && !isOtherFocused : error);
|
|
25
|
+
}, [error, isOtherFocused]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (isOther && otherInputRef.current && value === '') {
|
|
28
|
+
otherInputRef.current.focus();
|
|
29
|
+
}
|
|
30
|
+
}, [isOther, value]);
|
|
8
31
|
useEffect(() => {
|
|
9
32
|
setValue(defaultValue);
|
|
10
33
|
}, [defaultValue]);
|
|
11
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
35
|
const handleChange = (event, selected) => {
|
|
13
36
|
if (Array.isArray(selected)) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
37
|
+
const newValues = selected.map((option) => option.value ?? option);
|
|
38
|
+
setValue(uniq(newValues));
|
|
39
|
+
onChange && onChange(property.id, newValues, property);
|
|
17
40
|
}
|
|
18
41
|
else {
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
if (isCombobox && displayOption === 'radioButton') {
|
|
43
|
+
if (selected === '__other__') {
|
|
44
|
+
setIsOther(true);
|
|
45
|
+
setValue('');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
setIsOther(false);
|
|
49
|
+
setValue(selected);
|
|
50
|
+
setInputValue('');
|
|
51
|
+
onChange && onChange(property.id, selected, property);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
setValue(selected);
|
|
56
|
+
onChange && onChange(property.id, selected, property);
|
|
57
|
+
}
|
|
21
58
|
}
|
|
22
59
|
};
|
|
23
60
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -29,6 +66,10 @@ const Select = (props) => {
|
|
|
29
66
|
setInputValue(selectValue);
|
|
30
67
|
}
|
|
31
68
|
};
|
|
69
|
+
const ListboxComponent = forwardRef(function ListboxComponent(props, ref) {
|
|
70
|
+
const { children, ...other } = props;
|
|
71
|
+
return (React.createElement(List, { component: "ul", ...other, ref: ref, subheader: isCombobox ? (React.createElement(ListSubheader, { component: 'div', id: `${id}-instructions`, role: "note", sx: { lineHeight: '36px', borderBottom: '1px solid #919EAB3D' } }, "Select below or type and click Enter to add a new item.")) : undefined }, children));
|
|
72
|
+
});
|
|
32
73
|
const sortedOptions = (() => {
|
|
33
74
|
if (!selectOptions)
|
|
34
75
|
return [];
|
|
@@ -42,21 +83,92 @@ const Select = (props) => {
|
|
|
42
83
|
return options.sort((a, b) => typeof a === 'string' ? a.localeCompare(b) : a.label.localeCompare(b.label));
|
|
43
84
|
}
|
|
44
85
|
})();
|
|
45
|
-
return readOnly ? (React.createElement(InputFieldComponent, { ...props })) : displayOption === 'radioButton' ? (React.createElement(
|
|
46
|
-
React.createElement(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
return readOnly ? (React.createElement(InputFieldComponent, { ...props })) : displayOption === 'radioButton' ? (React.createElement(React.Fragment, null,
|
|
87
|
+
React.createElement(FormControl, { error: error, required: required },
|
|
88
|
+
React.createElement(RadioGroup, { name: `radioGroup-${id}`, value: isOther ? '__other__' : value, onChange: handleChange },
|
|
89
|
+
sortedOptions.map((option, index) => (React.createElement(FormControlLabel, { key: index, sx: { margin: '3px 0' }, value: typeof option === 'string' ? option : option.value, control: React.createElement(Radio, { size: "small", sx: {
|
|
90
|
+
padding: '3px',
|
|
91
|
+
...(index === 0 && {
|
|
92
|
+
paddingTop: 0,
|
|
93
|
+
}),
|
|
94
|
+
...(!!errorState && {
|
|
95
|
+
color: '#FF4842',
|
|
96
|
+
'&.Mui-checked': { color: '#FF4842' },
|
|
97
|
+
}),
|
|
98
|
+
} }), label: typeof option === 'string' ? option : option.label }))),
|
|
99
|
+
isCombobox && (React.createElement(React.Fragment, null,
|
|
100
|
+
React.createElement(FormControlLabel, { value: '__other__', sx: { margin: '3px 0' }, control: React.createElement(Radio, { size: "small", sx: {
|
|
101
|
+
padding: '3px',
|
|
102
|
+
...(!!errorState && {
|
|
103
|
+
color: '#FF4842',
|
|
104
|
+
'&.Mui-checked': { color: '#FF4842' },
|
|
105
|
+
}),
|
|
106
|
+
} }), label: React.createElement("span", { id: `other-radio-button-${id}` }, "Other") }),
|
|
107
|
+
isOther && (React.createElement(TextField, { id: `other-${id}`, inputProps: {
|
|
108
|
+
'aria-labelledby': `other-radio-button-${id}`,
|
|
109
|
+
}, value: inputValue, inputRef: otherInputRef, error: !!errorState, sx: { marginLeft: '4px' }, onChange: (e) => {
|
|
110
|
+
setValue(e.target.value);
|
|
111
|
+
setInputValue(e.target.value);
|
|
112
|
+
onChange && onChange(property.id, e.target.value, property);
|
|
113
|
+
}, size: 'small', onFocus: () => setIsOtherFocused(true), onBlur: (e) => {
|
|
114
|
+
// If another radio button is focused, change the value to that radio button's value.
|
|
115
|
+
const targetElement = e.nativeEvent.relatedTarget;
|
|
116
|
+
if (targetElement?.defaultValue && targetElement.name === `radioGroup-${id}`) {
|
|
117
|
+
onChange && onChange(property.id, targetElement, property);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
onChange && onChange(property.id, e.target.value, property);
|
|
121
|
+
}
|
|
122
|
+
setIsOtherFocused(false);
|
|
123
|
+
} })))))),
|
|
124
|
+
displayOption === 'radioButton' && onChange && !readOnly && value && (React.createElement(Box, { sx: {
|
|
125
|
+
':hover': { cursor: 'pointer' },
|
|
126
|
+
marginTop: '4px',
|
|
127
|
+
display: 'flex',
|
|
128
|
+
alignItems: 'center',
|
|
129
|
+
} },
|
|
130
|
+
React.createElement(IconButton, { "aria-label": `Clear`, onClick: () => {
|
|
131
|
+
setValue('');
|
|
132
|
+
property && onChange(property.id, '');
|
|
133
|
+
setIsOther(false);
|
|
134
|
+
}, sx: { padding: '3px', marginRight: '6px' } },
|
|
135
|
+
React.createElement(Clear, { sx: {
|
|
136
|
+
color: '#637381',
|
|
137
|
+
fontSize: '1.2rem',
|
|
138
|
+
} })),
|
|
139
|
+
React.createElement(Typography, { variant: "caption" }, "Clear Selection"))))) : (React.createElement(Autocomplete, { multiple: property?.type === 'array' ? true : false, id: id, sortBy: sortBy, renderInput: (params) => (React.createElement(TextField, { ...params, value: value, fullWidth: true, onBlur: onBlur, inputProps: {
|
|
140
|
+
...params.inputProps,
|
|
141
|
+
'aria-describedby': isCombobox ? `${id}-instructions` : undefined,
|
|
142
|
+
} })), value: value ?? (property?.type === 'array' ? [] : undefined), onChange: handleChange, options: selectOptions ?? property?.enum ?? [], inputValue: inputValue ?? '', error: error, errorMessage: errorMessage, required: required, onInputChange: handleInputValueChange, size: size, filterOptions: (options, params) => {
|
|
143
|
+
const filtered = filter(options, params);
|
|
144
|
+
const { inputValue } = params;
|
|
145
|
+
// Suggest to the user to add a new value.
|
|
146
|
+
const found = options.some((option) => inputValue === option.label);
|
|
147
|
+
if (inputValue !== '' && !found && isCombobox) {
|
|
148
|
+
filtered.push({
|
|
149
|
+
value: inputValue,
|
|
150
|
+
label: `Add "${inputValue}"`,
|
|
151
|
+
isCustomValue: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return filtered;
|
|
155
|
+
}, isOptionEqualToValue: isOptionEqualToValue
|
|
53
156
|
? (option, value) => isOptionEqualToValue(option, value)
|
|
54
|
-
: undefined, getOptionLabel: getOptionLabel
|
|
157
|
+
: undefined, getOptionLabel: getOptionLabel && !isCombobox
|
|
158
|
+
? (option) => getOptionLabel(option)
|
|
159
|
+
: (option) => {
|
|
160
|
+
if (typeof option === 'string')
|
|
161
|
+
return option;
|
|
162
|
+
// If the option is a custom value, return the value without the prepended "Add" text.
|
|
163
|
+
if (option.isCustomValue)
|
|
164
|
+
return option.value;
|
|
165
|
+
return option.label ?? '';
|
|
166
|
+
}, renderOption: renderOption
|
|
55
167
|
? (props, option, state) => renderOption(props, option, state)
|
|
56
|
-
:
|
|
168
|
+
: (props, option) => React.createElement("li", { ...props }, option.label ?? option), ListboxComponent: ListboxComponent, disableCloseOnSelect: disableCloseOnSelect, sx: {
|
|
57
169
|
'& button.MuiButtonBase-root': {
|
|
58
170
|
visibility: 'visible',
|
|
59
171
|
},
|
|
60
|
-
}, ...(additionalProps ?? {}) }));
|
|
172
|
+
}, forcePopupIcon: true, ...(isCombobox ? { selectOnFocus: true, handleHomeEndKeys: true, freeSolo: true } : {}), ...(additionalProps ?? {}) }));
|
|
61
173
|
};
|
|
62
174
|
export default Select;
|
|
@@ -14,8 +14,8 @@ describe('Single select', () => {
|
|
|
14
14
|
type: 'choices',
|
|
15
15
|
};
|
|
16
16
|
it('returns selected option', async () => {
|
|
17
|
-
const user = userEvent.setup();
|
|
18
17
|
const onChangeMock = vi.fn((name, value, property) => { });
|
|
18
|
+
const user = userEvent.setup();
|
|
19
19
|
const options = ['option 1', 'option 2', 'option 3'];
|
|
20
20
|
render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock }));
|
|
21
21
|
const input = screen.getByRole('combobox');
|
|
@@ -48,6 +48,21 @@ describe('Single select', () => {
|
|
|
48
48
|
const optionLabels = allOptions.map((option) => option.textContent);
|
|
49
49
|
expect(optionLabels).toEqual(expectedValues);
|
|
50
50
|
});
|
|
51
|
+
it('allows the user to enter a custom value if it is combobox component', async () => {
|
|
52
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
53
|
+
const user = userEvent.setup();
|
|
54
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
55
|
+
render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
|
|
56
|
+
const input = screen.getByRole('combobox');
|
|
57
|
+
await user.click(input);
|
|
58
|
+
// Verify the instruction text for a combobox displays as the sub-header of the combobox.
|
|
59
|
+
screen.getByText('Select below or type and click Enter to add a new item.');
|
|
60
|
+
await user.type(input, 'custom option');
|
|
61
|
+
// Verify the option to add the custom value is displayed as an available option in the dropdown.
|
|
62
|
+
const customOption = await screen.findByRole('option', { name: 'Add "custom option"' });
|
|
63
|
+
await user.click(customOption);
|
|
64
|
+
expect(onChangeMock).toBeCalledWith('selectOptions', expect.objectContaining({ label: 'Add "custom option"', value: 'custom option' }), choiceProperty);
|
|
65
|
+
});
|
|
51
66
|
});
|
|
52
67
|
describe('Multi select', () => {
|
|
53
68
|
// Right now an object property is required for this to function, but eventually this should go
|
|
@@ -66,12 +81,49 @@ describe('Multi select', () => {
|
|
|
66
81
|
await user.click(input);
|
|
67
82
|
const option2 = await screen.findByRole('option', { name: 'option 2' });
|
|
68
83
|
await user.click(option2);
|
|
69
|
-
|
|
84
|
+
// selecting option closes the dropdown, re-open the options
|
|
85
|
+
await user.click(input);
|
|
70
86
|
const option3 = await screen.findByRole('option', { name: 'option 3' });
|
|
71
87
|
await user.click(option3);
|
|
72
88
|
expect(onChangeMock).toBeCalledTimes(2);
|
|
73
89
|
expect(onChangeMock).lastCalledWith('multiSelect', ['option 2', 'option 3'], multiChoiceProperty);
|
|
74
90
|
});
|
|
91
|
+
it('allows the user to enter custom values if it is combobox component', async () => {
|
|
92
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
93
|
+
const user = userEvent.setup();
|
|
94
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
95
|
+
render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
|
|
96
|
+
const input = screen.getByRole('combobox');
|
|
97
|
+
await user.click(input);
|
|
98
|
+
// Verify the instruction text for a combobox displays as the sub-header of the combobox.
|
|
99
|
+
screen.getByText('Select below or type and click Enter to add a new item.');
|
|
100
|
+
await user.type(input, 'custom option 1');
|
|
101
|
+
const customOption1 = await screen.findByRole('option', { name: 'Add "custom option 1"' });
|
|
102
|
+
await user.click(customOption1);
|
|
103
|
+
await user.type(input, 'custom option 2');
|
|
104
|
+
const customOption2 = await screen.findByRole('option', { name: 'Add "custom option 2"' });
|
|
105
|
+
await user.click(customOption2);
|
|
106
|
+
expect(onChangeMock).toBeCalledTimes(2);
|
|
107
|
+
expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'custom option 2'], multiChoiceProperty);
|
|
108
|
+
});
|
|
109
|
+
it('allows the user to enter custom values in conjunction with the predefined options if it is combobox component', async () => {
|
|
110
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
111
|
+
const user = userEvent.setup();
|
|
112
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
113
|
+
render(React.createElement(Select, { id: "multiSelect", property: multiChoiceProperty, selectOptions: options, onChange: onChangeMock, isCombobox: true }));
|
|
114
|
+
const input = screen.getByRole('combobox');
|
|
115
|
+
await user.click(input);
|
|
116
|
+
// Verify the instruction text for a combobox displays as the sub-header of the combobox.
|
|
117
|
+
screen.getByText('Select below or type and click Enter to add a new item.');
|
|
118
|
+
await user.type(input, 'custom option 1');
|
|
119
|
+
const customOption1 = await screen.findByRole('option', { name: 'Add "custom option 1"' });
|
|
120
|
+
await user.click(customOption1);
|
|
121
|
+
await user.click(input);
|
|
122
|
+
const option1 = await screen.findByRole('option', { name: 'option 1' });
|
|
123
|
+
await user.click(option1);
|
|
124
|
+
expect(onChangeMock).toBeCalledTimes(2);
|
|
125
|
+
expect(onChangeMock).lastCalledWith('multiSelect', ['custom option 1', 'option 1'], multiChoiceProperty);
|
|
126
|
+
});
|
|
75
127
|
});
|
|
76
128
|
describe('Radio Single select', () => {
|
|
77
129
|
const choiceProperty = {
|
|
@@ -79,7 +131,7 @@ describe('Radio Single select', () => {
|
|
|
79
131
|
name: 'Select Options',
|
|
80
132
|
type: 'choices',
|
|
81
133
|
};
|
|
82
|
-
|
|
134
|
+
it('returns selected radio option', async () => {
|
|
83
135
|
const user = userEvent.setup();
|
|
84
136
|
const onChangeMock = vi.fn((name, value, property) => { });
|
|
85
137
|
const options = ['option 1', 'option 2', 'option 3'];
|
|
@@ -99,4 +151,21 @@ describe('Radio Single select', () => {
|
|
|
99
151
|
const radioValues = radioButtons.map((radioButton) => radioButton.value);
|
|
100
152
|
expect(radioValues).toEqual(expectedValues);
|
|
101
153
|
});
|
|
154
|
+
it('renders an "Other" option in the radio group if the component is configured to support a custom value', async () => {
|
|
155
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
156
|
+
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 }));
|
|
158
|
+
await screen.findByRole('radio', { name: 'Other' });
|
|
159
|
+
});
|
|
160
|
+
it('renders a text field for a custom option if the "Other" option is selected', async () => {
|
|
161
|
+
const user = userEvent.setup();
|
|
162
|
+
const onChangeMock = vi.fn((name, value, property) => { });
|
|
163
|
+
const options = ['option 1', 'option 2', 'option 3'];
|
|
164
|
+
render(React.createElement(Select, { id: "testSelect", property: choiceProperty, selectOptions: options, displayOption: 'radioButton', sortBy: 'ASC', onChange: onChangeMock, isCombobox: true }));
|
|
165
|
+
await screen.findByRole('radio', { name: 'Other' });
|
|
166
|
+
await user.click(screen.getByRole('radio', { name: 'Other' }));
|
|
167
|
+
const otherTextField = await screen.findByRole('textbox', { name: `Other` });
|
|
168
|
+
await user.type(otherTextField, 'custom option');
|
|
169
|
+
expect(onChangeMock).toBeCalledWith('selectOptions', expect.stringContaining('custom option'), choiceProperty);
|
|
170
|
+
});
|
|
102
171
|
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { LocalDateTime } from '@js-joda/core';
|
|
2
2
|
import { TimePicker } from '@mui/x-date-pickers';
|
|
3
|
-
import { isUndefined, padStart } from 'lodash';
|
|
3
|
+
import { isUndefined, omit, padStart } from 'lodash';
|
|
4
|
+
import { DateTime } from 'luxon';
|
|
4
5
|
import React, { useEffect, useState } from 'react';
|
|
5
6
|
import { InvalidDate } from '../../../../util';
|
|
6
7
|
import { LocalizationProvider, TextField } from '../../../core';
|
|
8
|
+
import InputFieldComponent from '../InputFieldComponent/InputFieldComponent';
|
|
7
9
|
const TimePickerSelect = (props) => {
|
|
8
10
|
const { id, property, defaultValue, error, errorMessage, readOnly, required, size, onBlur, placeholder, additionalProps, } = props;
|
|
9
11
|
const values = defaultValue ? defaultValue.split(':') : undefined;
|
|
@@ -39,7 +41,15 @@ const TimePickerSelect = (props) => {
|
|
|
39
41
|
props.onChange && props.onChange(property.id, date, property);
|
|
40
42
|
}
|
|
41
43
|
};
|
|
42
|
-
return (React.createElement(
|
|
43
|
-
|
|
44
|
+
return readOnly ? (React.createElement(InputFieldComponent, { ...{
|
|
45
|
+
...props,
|
|
46
|
+
defaultValue: value instanceof LocalDateTime ? DateTime.fromISO(value.toString()).toFormat('hh:mm a') : '',
|
|
47
|
+
} })) : (React.createElement(LocalizationProvider, null,
|
|
48
|
+
React.createElement(TimePicker, { value: value, onChange: handleChange, renderInput: (params) => (React.createElement(TextField, { ...params, id: id, error: error, errorMessage: errorMessage, onBlur: onBlur, fullWidth: true, required: required, sx: { background: 'white', borderRadius: '8px' }, size: size ?? 'medium', placeholder: placeholder,
|
|
49
|
+
// merges MUI inputProps with additionalProps.inputProps in a way that still shows the value
|
|
50
|
+
inputProps: {
|
|
51
|
+
...params.inputProps,
|
|
52
|
+
...(additionalProps?.inputProps ?? {}),
|
|
53
|
+
}, ...omit(additionalProps, ['inputProps']) })), readOnly: readOnly })));
|
|
44
54
|
};
|
|
45
55
|
export default TimePickerSelect;
|
|
@@ -19,8 +19,7 @@ const styles = {
|
|
|
19
19
|
color: '#637381',
|
|
20
20
|
},
|
|
21
21
|
timelineConnector: {
|
|
22
|
-
padding: '1px 0px 24px
|
|
23
|
-
borderLeft: '2px rgba(145, 158, 171, 0.24) solid',
|
|
22
|
+
padding: '1px 0px 24px 0px',
|
|
24
23
|
marginLeft: '5px',
|
|
25
24
|
},
|
|
26
25
|
historyData: {
|
|
@@ -30,15 +29,24 @@ const styles = {
|
|
|
30
29
|
},
|
|
31
30
|
};
|
|
32
31
|
const HistoricalData = (props) => {
|
|
33
|
-
const { records, documentHistory, object, referencedObjects } = props;
|
|
32
|
+
const { records, documentHistory, object, referencedObjects, hideTimeline } = props;
|
|
34
33
|
const getPastDocumentVersion = (history) => {
|
|
35
34
|
const documentVersions = documentHistory?.[history.subject?.id ?? 'unknown'] ?? [];
|
|
36
35
|
const currentVersion = documentVersions?.map((v) => v.timestamp).indexOf(history.timestamp);
|
|
37
36
|
return currentVersion ? documentVersions[currentVersion - 1] : undefined;
|
|
38
37
|
};
|
|
39
|
-
return (React.createElement(Box, { sx:
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
return (React.createElement(Box, { sx: {
|
|
39
|
+
...styles.timelineConnector,
|
|
40
|
+
borderLeft: !hideTimeline ? '2px rgba(145, 158, 171, 0.24) solid' : undefined,
|
|
41
|
+
} }, records.map((r) => (React.createElement(Box, { margin: '16px 0px', key: `${r.timestamp}-${nanoid()}` },
|
|
42
|
+
React.createElement(Box, { sx: {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
justifyContent: 'space-between',
|
|
45
|
+
flexDirection: { xs: 'column', sm: 'row' },
|
|
46
|
+
alignItems: { xs: 'flex-start', sm: 'center' },
|
|
47
|
+
gap: 0,
|
|
48
|
+
} },
|
|
49
|
+
React.createElement(Box, { sx: { display: 'flex', maxWidth: { sx: '100%', sm: '60%' }, alignContent: 'flex-start' } },
|
|
42
50
|
React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } },
|
|
43
51
|
React.createElement(Typography, { component: 'span', sx: { fontWeight: 600, fontSize: '12px' } }, r.user.name ?? 'Unknown User'),
|
|
44
52
|
"\u00A0",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
+
import { Circle } from '@mui/icons-material';
|
|
1
2
|
import React from 'react';
|
|
2
|
-
import { Box } from '../../layout';
|
|
3
3
|
import { Skeleton } from '../../core';
|
|
4
|
-
import {
|
|
4
|
+
import { Box } from '../../layout';
|
|
5
5
|
const styles = {
|
|
6
6
|
timelineConnector: {
|
|
7
7
|
padding: '1px 0px 5px 22px',
|
|
8
|
-
borderLeft: '2px rgba(145, 158, 171, 0.24) solid',
|
|
9
8
|
marginLeft: '5px',
|
|
10
9
|
},
|
|
11
10
|
headerSkeleton: {
|
|
@@ -18,20 +17,27 @@ const styles = {
|
|
|
18
17
|
borderRadius: '7px',
|
|
19
18
|
},
|
|
20
19
|
};
|
|
21
|
-
const HistoryLoading = () => {
|
|
20
|
+
const HistoryLoading = (props) => {
|
|
21
|
+
const { hideTimeline } = props;
|
|
22
22
|
return (React.createElement(React.Fragment, { key: 'history-log-loading' },
|
|
23
23
|
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
24
|
-
React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
24
|
+
!hideTimeline && React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
25
25
|
React.createElement(Skeleton, { variant: "text", sx: styles.headerSkeleton })),
|
|
26
|
-
React.createElement(Box, { sx:
|
|
26
|
+
React.createElement(Box, { sx: {
|
|
27
|
+
...styles.timelineConnector,
|
|
28
|
+
borderLeft: !hideTimeline ? '2px rgba(145, 158, 171, 0.24) solid' : undefined,
|
|
29
|
+
} },
|
|
27
30
|
React.createElement(Box, { margin: '4px 0px 24px 0px' },
|
|
28
31
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '120px' } }),
|
|
29
32
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '65%' } }),
|
|
30
33
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '75%' } }))),
|
|
31
34
|
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
32
|
-
React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
35
|
+
!hideTimeline && React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
33
36
|
React.createElement(Skeleton, { variant: "text", sx: styles.headerSkeleton })),
|
|
34
|
-
React.createElement(Box, { sx:
|
|
37
|
+
React.createElement(Box, { sx: {
|
|
38
|
+
...styles.timelineConnector,
|
|
39
|
+
borderLeft: !hideTimeline ? '2px rgba(145, 158, 171, 0.24) solid' : undefined,
|
|
40
|
+
} },
|
|
35
41
|
React.createElement(Box, { margin: '4px 0px 20px 0px' },
|
|
36
42
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '120px' } }),
|
|
37
43
|
React.createElement(Skeleton, { variant: "text", sx: { ...styles.dataSkeleton, width: '55%' } })))));
|
|
@@ -10,6 +10,8 @@ export type HistoryLogProps = {
|
|
|
10
10
|
loading?: boolean;
|
|
11
11
|
/** The title displayed above the timeline */
|
|
12
12
|
title?: string;
|
|
13
|
+
/** Whether to hide the timeline in the history log */
|
|
14
|
+
hideTimeline?: boolean;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
17
|
* Renders a timeline of the instance's history log.
|
|
@@ -20,7 +20,7 @@ const { format } = require('small-date');
|
|
|
20
20
|
* @returns {JSX.Element} A timeline view representing the instance's history.
|
|
21
21
|
*/
|
|
22
22
|
export const HistoryLog = (props) => {
|
|
23
|
-
const { object, history, loading, title } = props;
|
|
23
|
+
const { object, history, loading, title, hideTimeline } = props;
|
|
24
24
|
const [historyMap, setHistoryMap] = useState({});
|
|
25
25
|
const [documentHistory, setDocumentHistory] = useState({});
|
|
26
26
|
const [referencedObjects, setReferencedObjects] = useState([]);
|
|
@@ -91,7 +91,7 @@ export const HistoryLog = (props) => {
|
|
|
91
91
|
if (records.length) {
|
|
92
92
|
return (React.createElement(React.Fragment, { key: date },
|
|
93
93
|
React.createElement(Box, { sx: { display: 'flex', alignItems: 'center' } },
|
|
94
|
-
React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } }),
|
|
94
|
+
!hideTimeline && (React.createElement(Circle, { color: "primary", sx: { fontSize: '12px', marginRight: '12px' } })),
|
|
95
95
|
React.createElement(Typography, { sx: { fontWeight: 600, fontSize: '16px' } },
|
|
96
96
|
format(new Date(), 'yyyy-MM-dd') === date
|
|
97
97
|
? 'Today'
|
|
@@ -99,14 +99,14 @@ export const HistoryLog = (props) => {
|
|
|
99
99
|
' ',
|
|
100
100
|
"\u00A0"),
|
|
101
101
|
React.createElement(Typography, { sx: { fontWeight: 600, fontSize: '16px', color: '#637381' } }, format(new Date(date + ' 00:00:000'), 'MMM dd, yyyy'))),
|
|
102
|
-
React.createElement(HistoricalData, { object: object, records: records, documentHistory: documentHistory, referencedObjects: referencedObjects })));
|
|
102
|
+
React.createElement(HistoricalData, { object: object, records: records, documentHistory: documentHistory, referencedObjects: referencedObjects, hideTimeline: hideTimeline })));
|
|
103
103
|
}
|
|
104
104
|
return null;
|
|
105
105
|
}),
|
|
106
106
|
!loading && filteredHistory && Object.values(filteredHistory).every((v) => !v.length) && (React.createElement(Box, { width: '100%', display: 'grid', justifyContent: 'center', marginTop: '60px' },
|
|
107
107
|
React.createElement(Typography, { fontSize: '20px', fontWeight: 700 }, "You Have No History"),
|
|
108
108
|
React.createElement(Typography, { fontSize: '14px', fontWeight: 400 }, "Try modifying the history type."))),
|
|
109
|
-
loading && React.createElement(HistoryLoading,
|
|
109
|
+
loading && React.createElement(HistoryLoading, { hideTimeline: hideTimeline }),
|
|
110
110
|
React.createElement(Snackbar, { open: showSnackbar, handleClose: () => setShowSnackbar(false), message: 'Error occurred when loading referenced objects', error: true })));
|
|
111
111
|
};
|
|
112
112
|
export default HistoryLog;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface ResponsiveOverflowProps {
|
|
3
|
+
/**
|
|
4
|
+
* Children elements to be displayed horizontally
|
|
5
|
+
*/
|
|
6
|
+
children: React.ReactNode[];
|
|
7
|
+
/**
|
|
8
|
+
* Text for the overflow button. Default is "More"
|
|
9
|
+
*/
|
|
10
|
+
moreButtonText?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Custom button element to replace the default "More" button
|
|
13
|
+
*/
|
|
14
|
+
customMoreButton?: React.ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum number of children to be displayed before overflowing
|
|
17
|
+
*/
|
|
18
|
+
maxVisible?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Function to render each overflow menu item. Receives the overflowed child and its index.
|
|
21
|
+
*/
|
|
22
|
+
renderOverflowMenuItem?: (item: React.ReactNode) => React.ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* If true, the container will take full width
|
|
25
|
+
*/
|
|
26
|
+
fullWidth?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* ResponsiveOverflow is a container component that displays children horizontally,
|
|
30
|
+
* automatically moving overflow items into a dropdown menu when there isn't enough space.
|
|
31
|
+
*/
|
|
32
|
+
export declare const ResponsiveOverflow: (props: ResponsiveOverflowProps) => React.JSX.Element;
|
|
33
|
+
export default ResponsiveOverflow;
|