@etsoo/materialui 1.0.55 → 1.0.57
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/__tests__/SelectEx.tsx +47 -0
- package/lib/AutocompleteExtendedProps.d.ts +1 -1
- package/lib/ListChooser.d.ts +56 -0
- package/lib/ListChooser.js +79 -0
- package/lib/OptionGroup.d.ts +4 -0
- package/lib/OptionGroup.js +3 -3
- package/lib/SelectEx.js +41 -39
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/package.json +16 -16
- package/src/AutocompleteExtendedProps.ts +1 -1
- package/src/ListChooser.tsx +204 -0
- package/src/OptionGroup.tsx +8 -3
- package/src/SelectEx.tsx +45 -38
- package/src/index.ts +1 -0
package/__tests__/SelectEx.tsx
CHANGED
|
@@ -51,3 +51,50 @@ it('Render SelectEx', async () => {
|
|
|
51
51
|
|
|
52
52
|
expect(itemChangeCallback).toBeCalledTimes(2);
|
|
53
53
|
});
|
|
54
|
+
|
|
55
|
+
it('Render multiple SelectEx', async () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
type T = { id: number; name: string };
|
|
58
|
+
const options: T[] = [
|
|
59
|
+
{ id: 1, name: 'Name 1' },
|
|
60
|
+
{ id: 2, name: 'Name 2' },
|
|
61
|
+
{ id: 3, name: 'Name 3' }
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const itemChangeCallback = jest.fn((option, userAction) => {
|
|
65
|
+
if (userAction) expect(option.id).toBe(3);
|
|
66
|
+
else expect(option.id).toBe(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Render component
|
|
70
|
+
const { baseElement } = render(
|
|
71
|
+
<SelectEx<T>
|
|
72
|
+
options={options}
|
|
73
|
+
name="test"
|
|
74
|
+
onItemChange={itemChangeCallback}
|
|
75
|
+
value={[1, 2]}
|
|
76
|
+
multiple
|
|
77
|
+
search
|
|
78
|
+
labelField="name"
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(itemChangeCallback).toBeCalled();
|
|
83
|
+
|
|
84
|
+
// Act, click to show the list
|
|
85
|
+
const button = screen.getByRole('button');
|
|
86
|
+
fireEvent.mouseDown(button); // Not click
|
|
87
|
+
|
|
88
|
+
// Get list item
|
|
89
|
+
const itemName3 = await findByText(baseElement, 'Name 3');
|
|
90
|
+
expect(itemName3.nodeName).toBe('SPAN');
|
|
91
|
+
|
|
92
|
+
// Checkbox
|
|
93
|
+
const checkbox = itemName3.closest('li')?.querySelector('input');
|
|
94
|
+
|
|
95
|
+
act(() => {
|
|
96
|
+
checkbox?.click();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(itemChangeCallback).toBeCalledTimes(2);
|
|
100
|
+
});
|
|
@@ -4,7 +4,7 @@ import { ChangeEventHandler } from 'react';
|
|
|
4
4
|
/**
|
|
5
5
|
* Autocomplete extended props
|
|
6
6
|
*/
|
|
7
|
-
export declare type AutocompleteExtendedProps<T extends object, D extends DataTypes.Keys<T>> = Omit<AutocompleteProps<T, undefined, false, false>, 'renderInput' | 'options'> & {
|
|
7
|
+
export declare type AutocompleteExtendedProps<T extends object, D extends DataTypes.Keys<T>> = Omit<AutocompleteProps<T, undefined, false, false>, 'renderInput' | 'options' | 'multiple'> & {
|
|
8
8
|
/**
|
|
9
9
|
* Id field
|
|
10
10
|
*/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DataTypes, DelayedExecutorType, IdDefaultType } from '@etsoo/shared';
|
|
2
|
+
import { ListItemButtonProps, ListProps } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
declare type QueryData = {
|
|
5
|
+
title?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* List chooser button props
|
|
9
|
+
*/
|
|
10
|
+
export interface ListChooserButtonProps<T extends object, D extends DataTypes.Keys<T>> {
|
|
11
|
+
(id: T[D]): ListItemButtonProps;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* List chooser props
|
|
15
|
+
*/
|
|
16
|
+
export declare type ListChooserProps<T extends object, D extends DataTypes.Keys<T>, Q extends object> = ListProps & {
|
|
17
|
+
/**
|
|
18
|
+
* Condition renderer
|
|
19
|
+
*/
|
|
20
|
+
conditionRenderer?: (rq: Partial<Q>, delayed: DelayedExecutorType) => React.ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* List item renderer
|
|
23
|
+
*/
|
|
24
|
+
itemRenderer?: (data: T, props: ListChooserButtonProps<T, D>) => React.ReactNode;
|
|
25
|
+
/**
|
|
26
|
+
* Label field
|
|
27
|
+
*/
|
|
28
|
+
labelField?: DataTypes.Keys<T, string> | ((data: T) => string);
|
|
29
|
+
/**
|
|
30
|
+
* Id field
|
|
31
|
+
*/
|
|
32
|
+
idField?: D;
|
|
33
|
+
/**
|
|
34
|
+
* Load data callback
|
|
35
|
+
*/
|
|
36
|
+
loadData: (rq: Partial<Q>) => PromiseLike<T[] | null | undefined>;
|
|
37
|
+
/**
|
|
38
|
+
* Multiple selected
|
|
39
|
+
*/
|
|
40
|
+
multiple?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Item onchange callback
|
|
43
|
+
*/
|
|
44
|
+
onItemChange: (items: T[], ids: T[D][]) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Title
|
|
47
|
+
*/
|
|
48
|
+
title: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* List chooser
|
|
52
|
+
* @param props Props
|
|
53
|
+
* @returns Component
|
|
54
|
+
*/
|
|
55
|
+
export declare function ListChooser<T extends object, D extends DataTypes.Keys<T> = IdDefaultType<T>, Q extends object = QueryData>(props: ListChooserProps<T, D, Q>): JSX.Element;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useDelayedExecutor } from '@etsoo/react';
|
|
2
|
+
import { List, ListItem, ListItemButton, ListItemText, TextField } from '@mui/material';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { VBox } from './FlexBox';
|
|
5
|
+
/**
|
|
6
|
+
* List chooser
|
|
7
|
+
* @param props Props
|
|
8
|
+
* @returns Component
|
|
9
|
+
*/
|
|
10
|
+
export function ListChooser(props) {
|
|
11
|
+
var _a;
|
|
12
|
+
// Selected ids state
|
|
13
|
+
const [selectedIds, setSelectedIds] = React.useState([]);
|
|
14
|
+
const selectProps = (id) => ({
|
|
15
|
+
selected: selectedIds.includes(id),
|
|
16
|
+
onClick: () => {
|
|
17
|
+
if (multiple) {
|
|
18
|
+
const index = selectedIds.indexOf(id);
|
|
19
|
+
if (index === -1)
|
|
20
|
+
selectedIds.push(id);
|
|
21
|
+
else
|
|
22
|
+
selectedIds.splice(index, 1);
|
|
23
|
+
setSelectedIds([...selectedIds]);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
setSelectedIds([id]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
// Destruct
|
|
31
|
+
const { conditionRenderer = (rq, delayed) => (React.createElement(TextField, { autoFocus: true, margin: "dense", name: "title", label: title, fullWidth: true, variant: "standard", inputProps: { maxLength: 128 }, onChange: (event) => {
|
|
32
|
+
Reflect.set(rq, 'title', event.target.value);
|
|
33
|
+
delayed.call();
|
|
34
|
+
} })), itemRenderer = (item, selectProps) => {
|
|
35
|
+
const id = item[idField];
|
|
36
|
+
const label = typeof labelField === 'function'
|
|
37
|
+
? labelField(item)
|
|
38
|
+
: Reflect.get(item, labelField);
|
|
39
|
+
return (React.createElement(ListItem, { disableGutters: true, key: `${id}` },
|
|
40
|
+
React.createElement(ListItemButton, { ...selectProps(id) },
|
|
41
|
+
React.createElement(ListItemText, { primary: label }))));
|
|
42
|
+
}, idField = 'id', labelField = 'label', loadData, multiple = false, onItemChange, title, ...rest } = props;
|
|
43
|
+
// Default minimum height
|
|
44
|
+
(_a = rest.sx) !== null && _a !== void 0 ? _a : (rest.sx = { minHeight: '220px' });
|
|
45
|
+
// State
|
|
46
|
+
const [items, setItems] = React.useState([]);
|
|
47
|
+
// Query request data
|
|
48
|
+
const mounted = React.useRef(false);
|
|
49
|
+
const rq = React.useRef({});
|
|
50
|
+
// Delayed execution
|
|
51
|
+
const delayed = useDelayedExecutor(async () => {
|
|
52
|
+
const result = await loadData(rq.current);
|
|
53
|
+
if (result == null || !mounted.current)
|
|
54
|
+
return;
|
|
55
|
+
if (!multiple &&
|
|
56
|
+
selectedIds.length > 0 &&
|
|
57
|
+
!result.some((item) => selectedIds.includes(item[idField]))) {
|
|
58
|
+
setSelectedIds([]);
|
|
59
|
+
}
|
|
60
|
+
setItems(result);
|
|
61
|
+
}, 480);
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (!mounted.current)
|
|
64
|
+
return;
|
|
65
|
+
onItemChange(items.filter((item) => selectedIds.includes(item[idField])), selectedIds);
|
|
66
|
+
}, [selectedIds]);
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
mounted.current = true;
|
|
69
|
+
delayed.call(0);
|
|
70
|
+
return () => {
|
|
71
|
+
mounted.current = false;
|
|
72
|
+
delayed.clear();
|
|
73
|
+
};
|
|
74
|
+
}, [delayed]);
|
|
75
|
+
// Layout
|
|
76
|
+
return (React.createElement(VBox, null,
|
|
77
|
+
conditionRenderer(rq.current, delayed),
|
|
78
|
+
React.createElement(List, { disablePadding: true, dense: true, ...rest }, items.map((item) => itemRenderer(item, selectProps)))));
|
|
79
|
+
}
|
package/lib/OptionGroup.d.ts
CHANGED
package/lib/OptionGroup.js
CHANGED
|
@@ -8,7 +8,7 @@ import React from 'react';
|
|
|
8
8
|
*/
|
|
9
9
|
export function OptionGroup(props) {
|
|
10
10
|
// Destruct
|
|
11
|
-
const { getOptionLabel, defaultValue, idField = 'id', label, labelField = 'label', multiple = false, mRef, name, onValueChange, options, readOnly, row,
|
|
11
|
+
const { getOptionLabel, defaultValue, idField = 'id', label, labelField = 'label', multiple = false, mRef, name, onValueChange, options, readOnly, row, itemSize, ...rest } = props;
|
|
12
12
|
// Get option value
|
|
13
13
|
// D type should be the source id type
|
|
14
14
|
const getOptionValue = (option) => {
|
|
@@ -45,7 +45,7 @@ export function OptionGroup(props) {
|
|
|
45
45
|
// Value
|
|
46
46
|
const ov = getOptionValue(option);
|
|
47
47
|
// Control
|
|
48
|
-
const control = multiple ? (React.createElement(Checkbox, { name: name, readOnly: readOnly, size:
|
|
48
|
+
const control = multiple ? (React.createElement(Checkbox, { name: name, readOnly: readOnly, size: itemSize, checked: itemChecked(option), disabled: disabledIds === null || disabledIds === void 0 ? void 0 : disabledIds.includes(ov), onChange: (event) => {
|
|
49
49
|
if (firstOptionValue == null)
|
|
50
50
|
return;
|
|
51
51
|
const typeValue = Utils.parseString(event.target.value, firstOptionValue);
|
|
@@ -64,7 +64,7 @@ export function OptionGroup(props) {
|
|
|
64
64
|
if (onValueChange)
|
|
65
65
|
onValueChange(changedValues);
|
|
66
66
|
setValues(changedValues);
|
|
67
|
-
} })) : (React.createElement(Radio, { disabled: disabledIds === null || disabledIds === void 0 ? void 0 : disabledIds.includes(ov), size:
|
|
67
|
+
} })) : (React.createElement(Radio, { disabled: disabledIds === null || disabledIds === void 0 ? void 0 : disabledIds.includes(ov), size: itemSize, readOnly: readOnly }));
|
|
68
68
|
// Label
|
|
69
69
|
const label = getOptionLabel == null
|
|
70
70
|
? `${option[labelField]}`
|
package/lib/SelectEx.js
CHANGED
|
@@ -11,26 +11,30 @@ import { ReactUtils } from '@etsoo/react';
|
|
|
11
11
|
* @returns Component
|
|
12
12
|
*/
|
|
13
13
|
export function SelectEx(props) {
|
|
14
|
-
var _a;
|
|
15
14
|
// Destruct
|
|
16
15
|
const { defaultValue, idField = 'id', error, helperText, inputRequired, itemIconRenderer, itemStyle, label, labelField = 'label', loadData, onItemChange, onItemClick, onLoadData, multiple = false, name, options, refresh, search = false, autoAddBlankItem = search, value, onChange, fullWidth, ...rest } = props;
|
|
17
16
|
// Options state
|
|
18
17
|
const [localOptions, setOptions] = React.useState([]);
|
|
19
|
-
const isMounted = React.useRef(
|
|
18
|
+
const isMounted = React.useRef(false);
|
|
20
19
|
const doItemChange = (options, value, userAction) => {
|
|
21
20
|
if (onItemChange == null)
|
|
22
21
|
return;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
let option;
|
|
23
|
+
if (multiple && Array.isArray(value)) {
|
|
24
|
+
option = options.find((option) => value.includes(option[idField]));
|
|
25
|
+
}
|
|
26
|
+
else if (value == null || value === '') {
|
|
27
|
+
option = undefined;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
option = options.find((option) => option[idField] === value);
|
|
26
31
|
}
|
|
27
|
-
const option = options.find((option) => option[idField] === value);
|
|
28
32
|
onItemChange(option, userAction);
|
|
29
33
|
};
|
|
30
34
|
const setOptionsAdd = (options) => {
|
|
31
35
|
setOptions(options);
|
|
32
|
-
if (
|
|
33
|
-
doItemChange(options,
|
|
36
|
+
if (valueSource != null)
|
|
37
|
+
doItemChange(options, valueSource, false);
|
|
34
38
|
};
|
|
35
39
|
// When options change
|
|
36
40
|
// [options] will cause infinite loop
|
|
@@ -41,28 +45,25 @@ export function SelectEx(props) {
|
|
|
41
45
|
setOptionsAdd(options);
|
|
42
46
|
}, [options, propertyWay]);
|
|
43
47
|
// Local value
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
else {
|
|
53
|
-
localValue = valueSource;
|
|
54
|
-
}
|
|
48
|
+
const v = defaultValue !== null && defaultValue !== void 0 ? defaultValue : value;
|
|
49
|
+
const valueSource = multiple
|
|
50
|
+
? v
|
|
51
|
+
? Array.isArray(v)
|
|
52
|
+
? v
|
|
53
|
+
: [v]
|
|
54
|
+
: []
|
|
55
|
+
: v !== null && v !== void 0 ? v : '';
|
|
55
56
|
// Value state
|
|
56
|
-
const [valueState, setValueStateBase] = React.useState();
|
|
57
|
+
const [valueState, setValueStateBase] = React.useState(valueSource);
|
|
57
58
|
const valueRef = React.useRef();
|
|
58
59
|
const setValueState = (newValue) => {
|
|
59
60
|
valueRef.current = newValue;
|
|
60
61
|
setValueStateBase(newValue);
|
|
61
62
|
};
|
|
62
63
|
React.useEffect(() => {
|
|
63
|
-
if (
|
|
64
|
-
setValueState(
|
|
65
|
-
}, [
|
|
64
|
+
if (valueSource != null)
|
|
65
|
+
setValueState(valueSource);
|
|
66
|
+
}, [valueSource]);
|
|
66
67
|
// Label id
|
|
67
68
|
const labelId = `selectex-label-${name}`;
|
|
68
69
|
// Item checked or not
|
|
@@ -71,27 +72,23 @@ export function SelectEx(props) {
|
|
|
71
72
|
return valueState.indexOf(id) !== -1;
|
|
72
73
|
return valueState === id;
|
|
73
74
|
};
|
|
74
|
-
// Change handler
|
|
75
|
-
const handleChange = (event) => {
|
|
76
|
-
const value = event.target.value;
|
|
77
|
-
if (multiple && !Array.isArray(value))
|
|
78
|
-
return setItemValue([value]);
|
|
79
|
-
else
|
|
80
|
-
return setItemValue(value);
|
|
81
|
-
};
|
|
82
75
|
// Set item
|
|
83
76
|
const setItemValue = (id) => {
|
|
84
77
|
var _a;
|
|
85
78
|
if (id != valueRef.current) {
|
|
79
|
+
// Difference
|
|
80
|
+
const diff = multiple
|
|
81
|
+
? Utils.arrayDifferences(id, valueRef.current)
|
|
82
|
+
: id;
|
|
86
83
|
setValueState(id);
|
|
87
84
|
const input = (_a = divRef.current) === null || _a === void 0 ? void 0 : _a.querySelector('input');
|
|
88
85
|
if (input) {
|
|
89
86
|
// Different value, trigger change event
|
|
90
87
|
ReactUtils.triggerChange(input, id, false);
|
|
91
88
|
}
|
|
92
|
-
return
|
|
89
|
+
return diff;
|
|
93
90
|
}
|
|
94
|
-
return
|
|
91
|
+
return undefined;
|
|
95
92
|
};
|
|
96
93
|
// Get option id
|
|
97
94
|
const getId = (option) => {
|
|
@@ -123,7 +120,7 @@ export function SelectEx(props) {
|
|
|
123
120
|
// When value change
|
|
124
121
|
React.useEffect(() => {
|
|
125
122
|
refreshData();
|
|
126
|
-
}, [
|
|
123
|
+
}, [valueSource]);
|
|
127
124
|
// When layout ready
|
|
128
125
|
React.useEffect(() => {
|
|
129
126
|
var _a;
|
|
@@ -134,6 +131,7 @@ export function SelectEx(props) {
|
|
|
134
131
|
setValueState(multiple ? [] : '');
|
|
135
132
|
};
|
|
136
133
|
input === null || input === void 0 ? void 0 : input.addEventListener('change', inputChange);
|
|
134
|
+
isMounted.current = true;
|
|
137
135
|
return () => {
|
|
138
136
|
isMounted.current = false;
|
|
139
137
|
input === null || input === void 0 ? void 0 : input.removeEventListener('change', inputChange);
|
|
@@ -145,17 +143,21 @@ export function SelectEx(props) {
|
|
|
145
143
|
React.createElement(InputLabel, { id: labelId, shrink: search
|
|
146
144
|
? MUGlobal.searchFieldShrink
|
|
147
145
|
: MUGlobal.inputFieldShrink }, label),
|
|
148
|
-
React.createElement(Select, { ref: divRef, value:
|
|
149
|
-
? valueState
|
|
150
|
-
:
|
|
146
|
+
React.createElement(Select, { ref: divRef, value: multiple
|
|
147
|
+
? valueState
|
|
148
|
+
: localOptions.some((o) => o[idField] === valueState)
|
|
149
|
+
? valueState
|
|
150
|
+
: '', input: React.createElement(OutlinedInput, { notched: true, label: label, required: inputRequired }), labelId: labelId, name: name, multiple: multiple, onChange: (event, child) => {
|
|
151
151
|
if (onChange) {
|
|
152
152
|
onChange(event, child);
|
|
153
153
|
// event.preventDefault() will block executing
|
|
154
154
|
if (event.defaultPrevented)
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
// Set item value
|
|
158
|
+
const diff = setItemValue(event.target.value);
|
|
159
|
+
if (diff != null) {
|
|
160
|
+
doItemChange(localOptions, diff, true);
|
|
159
161
|
}
|
|
160
162
|
}, renderValue: (selected) => {
|
|
161
163
|
// The text shows up
|
package/lib/index.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export * from './HiSelector';
|
|
|
44
44
|
export * from './IconButtonLink';
|
|
45
45
|
export * from './InputField';
|
|
46
46
|
export * from './ItemList';
|
|
47
|
+
export * from './ListChooser';
|
|
47
48
|
export * from './ListItemRightIcon';
|
|
48
49
|
export * from './ListMoreDisplay';
|
|
49
50
|
export * from './LoadingButton';
|
package/lib/index.js
CHANGED
|
@@ -44,6 +44,7 @@ export * from './HiSelector';
|
|
|
44
44
|
export * from './IconButtonLink';
|
|
45
45
|
export * from './InputField';
|
|
46
46
|
export * from './ItemList';
|
|
47
|
+
export * from './ListChooser';
|
|
47
48
|
export * from './ListItemRightIcon';
|
|
48
49
|
export * from './ListMoreDisplay';
|
|
49
50
|
export * from './LoadingButton';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@etsoo/materialui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.57",
|
|
4
4
|
"description": "TypeScript Material-UI Implementation",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -48,20 +48,20 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@dnd-kit/core": "^6.0.5",
|
|
50
50
|
"@dnd-kit/sortable": "^7.0.1",
|
|
51
|
-
"@emotion/css": "^11.10.
|
|
52
|
-
"@emotion/react": "^11.10.
|
|
53
|
-
"@emotion/styled": "^11.10.
|
|
54
|
-
"@etsoo/appscript": "^1.3.
|
|
51
|
+
"@emotion/css": "^11.10.5",
|
|
52
|
+
"@emotion/react": "^11.10.5",
|
|
53
|
+
"@emotion/styled": "^11.10.5",
|
|
54
|
+
"@etsoo/appscript": "^1.3.28",
|
|
55
55
|
"@etsoo/notificationbase": "^1.1.13",
|
|
56
|
-
"@etsoo/react": "^1.6.
|
|
57
|
-
"@etsoo/shared": "^1.1.
|
|
56
|
+
"@etsoo/react": "^1.6.23",
|
|
57
|
+
"@etsoo/shared": "^1.1.71",
|
|
58
58
|
"@mui/icons-material": "^5.10.9",
|
|
59
|
-
"@mui/material": "^5.10.
|
|
59
|
+
"@mui/material": "^5.10.12",
|
|
60
60
|
"@types/pica": "^9.0.1",
|
|
61
61
|
"@types/pulltorefreshjs": "^0.1.5",
|
|
62
|
-
"@types/react": "^18.0.
|
|
62
|
+
"@types/react": "^18.0.24",
|
|
63
63
|
"@types/react-avatar-editor": "^13.0.0",
|
|
64
|
-
"@types/react-dom": "^18.0.
|
|
64
|
+
"@types/react-dom": "^18.0.8",
|
|
65
65
|
"@types/react-input-mask": "^3.0.1",
|
|
66
66
|
"@types/react-window": "^1.8.5",
|
|
67
67
|
"pica": "^9.0.1",
|
|
@@ -71,20 +71,20 @@
|
|
|
71
71
|
"react-dom": "^18.2.0",
|
|
72
72
|
"react-draggable": "^4.4.5",
|
|
73
73
|
"react-imask": "^6.4.3",
|
|
74
|
-
"react-router-dom": "^6.4.
|
|
75
|
-
"react-window": "^1.8.
|
|
74
|
+
"react-router-dom": "^6.4.3",
|
|
75
|
+
"react-window": "^1.8.8"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@babel/cli": "^7.19.3",
|
|
79
79
|
"@babel/core": "^7.19.6",
|
|
80
80
|
"@babel/plugin-transform-runtime": "^7.19.6",
|
|
81
81
|
"@babel/preset-env": "^7.19.4",
|
|
82
|
-
"@babel/runtime-corejs3": "^7.
|
|
82
|
+
"@babel/runtime-corejs3": "^7.20.1",
|
|
83
83
|
"@testing-library/jest-dom": "^5.16.5",
|
|
84
84
|
"@testing-library/react": "^13.4.0",
|
|
85
|
-
"@types/jest": "^29.2.
|
|
86
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
87
|
-
"@typescript-eslint/parser": "^5.
|
|
85
|
+
"@types/jest": "^29.2.1",
|
|
86
|
+
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
|
87
|
+
"@typescript-eslint/parser": "^5.42.0",
|
|
88
88
|
"eslint": "^8.26.0",
|
|
89
89
|
"eslint-config-airbnb-base": "^15.0.0",
|
|
90
90
|
"eslint-plugin-import": "^2.26.0",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useDelayedExecutor } from '@etsoo/react';
|
|
2
|
+
import { DataTypes, DelayedExecutorType, IdDefaultType } from '@etsoo/shared';
|
|
3
|
+
import {
|
|
4
|
+
List,
|
|
5
|
+
ListItem,
|
|
6
|
+
ListItemButton,
|
|
7
|
+
ListItemButtonProps,
|
|
8
|
+
ListItemText,
|
|
9
|
+
ListProps,
|
|
10
|
+
TextField
|
|
11
|
+
} from '@mui/material';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { VBox } from './FlexBox';
|
|
14
|
+
|
|
15
|
+
type QueryData = {
|
|
16
|
+
title?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* List chooser button props
|
|
21
|
+
*/
|
|
22
|
+
export interface ListChooserButtonProps<
|
|
23
|
+
T extends object,
|
|
24
|
+
D extends DataTypes.Keys<T>
|
|
25
|
+
> {
|
|
26
|
+
(id: T[D]): ListItemButtonProps;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* List chooser props
|
|
31
|
+
*/
|
|
32
|
+
export type ListChooserProps<
|
|
33
|
+
T extends object,
|
|
34
|
+
D extends DataTypes.Keys<T>,
|
|
35
|
+
Q extends object
|
|
36
|
+
> = ListProps & {
|
|
37
|
+
/**
|
|
38
|
+
* Condition renderer
|
|
39
|
+
*/
|
|
40
|
+
conditionRenderer?: (
|
|
41
|
+
rq: Partial<Q>,
|
|
42
|
+
delayed: DelayedExecutorType
|
|
43
|
+
) => React.ReactNode;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List item renderer
|
|
47
|
+
*/
|
|
48
|
+
itemRenderer?: (
|
|
49
|
+
data: T,
|
|
50
|
+
props: ListChooserButtonProps<T, D>
|
|
51
|
+
) => React.ReactNode;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Label field
|
|
55
|
+
*/
|
|
56
|
+
labelField?: DataTypes.Keys<T, string> | ((data: T) => string);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Id field
|
|
60
|
+
*/
|
|
61
|
+
idField?: D;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load data callback
|
|
65
|
+
*/
|
|
66
|
+
loadData: (rq: Partial<Q>) => PromiseLike<T[] | null | undefined>;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Multiple selected
|
|
70
|
+
*/
|
|
71
|
+
multiple?: boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Item onchange callback
|
|
75
|
+
*/
|
|
76
|
+
onItemChange: (items: T[], ids: T[D][]) => void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Title
|
|
80
|
+
*/
|
|
81
|
+
title: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* List chooser
|
|
86
|
+
* @param props Props
|
|
87
|
+
* @returns Component
|
|
88
|
+
*/
|
|
89
|
+
export function ListChooser<
|
|
90
|
+
T extends object,
|
|
91
|
+
D extends DataTypes.Keys<T> = IdDefaultType<T>,
|
|
92
|
+
Q extends object = QueryData
|
|
93
|
+
>(props: ListChooserProps<T, D, Q>) {
|
|
94
|
+
// Selected ids state
|
|
95
|
+
const [selectedIds, setSelectedIds] = React.useState<T[D][]>([]);
|
|
96
|
+
|
|
97
|
+
const selectProps: ListChooserButtonProps<T, D> = (id: T[D]) => ({
|
|
98
|
+
selected: selectedIds.includes(id),
|
|
99
|
+
onClick: () => {
|
|
100
|
+
if (multiple) {
|
|
101
|
+
const index = selectedIds.indexOf(id);
|
|
102
|
+
if (index === -1) selectedIds.push(id);
|
|
103
|
+
else selectedIds.splice(index, 1);
|
|
104
|
+
setSelectedIds([...selectedIds]);
|
|
105
|
+
} else {
|
|
106
|
+
setSelectedIds([id]);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Destruct
|
|
112
|
+
const {
|
|
113
|
+
conditionRenderer = (rq: Partial<Q>, delayed: DelayedExecutorType) => (
|
|
114
|
+
<TextField
|
|
115
|
+
autoFocus
|
|
116
|
+
margin="dense"
|
|
117
|
+
name="title"
|
|
118
|
+
label={title}
|
|
119
|
+
fullWidth
|
|
120
|
+
variant="standard"
|
|
121
|
+
inputProps={{ maxLength: 128 }}
|
|
122
|
+
onChange={(event) => {
|
|
123
|
+
Reflect.set(rq, 'title', event.target.value);
|
|
124
|
+
delayed.call();
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
),
|
|
128
|
+
itemRenderer = (item, selectProps) => {
|
|
129
|
+
const id = item[idField];
|
|
130
|
+
const label =
|
|
131
|
+
typeof labelField === 'function'
|
|
132
|
+
? labelField(item)
|
|
133
|
+
: Reflect.get(item, labelField);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<ListItem disableGutters key={`${id}`}>
|
|
137
|
+
<ListItemButton {...selectProps(id)}>
|
|
138
|
+
<ListItemText primary={label} />
|
|
139
|
+
</ListItemButton>
|
|
140
|
+
</ListItem>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
idField = 'id' as D,
|
|
144
|
+
labelField = 'label',
|
|
145
|
+
loadData,
|
|
146
|
+
multiple = false,
|
|
147
|
+
onItemChange,
|
|
148
|
+
title,
|
|
149
|
+
...rest
|
|
150
|
+
} = props;
|
|
151
|
+
|
|
152
|
+
// Default minimum height
|
|
153
|
+
rest.sx ??= { minHeight: '220px' };
|
|
154
|
+
|
|
155
|
+
// State
|
|
156
|
+
const [items, setItems] = React.useState<T[]>([]);
|
|
157
|
+
|
|
158
|
+
// Query request data
|
|
159
|
+
const mounted = React.useRef<boolean>(false);
|
|
160
|
+
const rq = React.useRef<Partial<Q>>({});
|
|
161
|
+
|
|
162
|
+
// Delayed execution
|
|
163
|
+
const delayed = useDelayedExecutor(async () => {
|
|
164
|
+
const result = await loadData(rq.current);
|
|
165
|
+
if (result == null || !mounted.current) return;
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
!multiple &&
|
|
169
|
+
selectedIds.length > 0 &&
|
|
170
|
+
!result.some((item) => selectedIds.includes(item[idField]))
|
|
171
|
+
) {
|
|
172
|
+
setSelectedIds([]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setItems(result);
|
|
176
|
+
}, 480);
|
|
177
|
+
|
|
178
|
+
React.useEffect(() => {
|
|
179
|
+
if (!mounted.current) return;
|
|
180
|
+
onItemChange(
|
|
181
|
+
items.filter((item) => selectedIds.includes(item[idField])),
|
|
182
|
+
selectedIds
|
|
183
|
+
);
|
|
184
|
+
}, [selectedIds]);
|
|
185
|
+
|
|
186
|
+
React.useEffect(() => {
|
|
187
|
+
mounted.current = true;
|
|
188
|
+
delayed.call(0);
|
|
189
|
+
return () => {
|
|
190
|
+
mounted.current = false;
|
|
191
|
+
delayed.clear();
|
|
192
|
+
};
|
|
193
|
+
}, [delayed]);
|
|
194
|
+
|
|
195
|
+
// Layout
|
|
196
|
+
return (
|
|
197
|
+
<VBox>
|
|
198
|
+
{conditionRenderer(rq.current, delayed)}
|
|
199
|
+
<List disablePadding dense {...rest}>
|
|
200
|
+
{items.map((item) => itemRenderer(item, selectProps))}
|
|
201
|
+
</List>
|
|
202
|
+
</VBox>
|
|
203
|
+
);
|
|
204
|
+
}
|
package/src/OptionGroup.tsx
CHANGED
|
@@ -95,6 +95,11 @@ export type OptionGroupProps<
|
|
|
95
95
|
* Display group of elements in a compact row
|
|
96
96
|
*/
|
|
97
97
|
row?: boolean;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Item size
|
|
101
|
+
*/
|
|
102
|
+
itemSize?: 'small' | 'medium';
|
|
98
103
|
};
|
|
99
104
|
|
|
100
105
|
/**
|
|
@@ -121,7 +126,7 @@ export function OptionGroup<
|
|
|
121
126
|
options,
|
|
122
127
|
readOnly,
|
|
123
128
|
row,
|
|
124
|
-
|
|
129
|
+
itemSize,
|
|
125
130
|
...rest
|
|
126
131
|
} = props;
|
|
127
132
|
|
|
@@ -173,7 +178,7 @@ export function OptionGroup<
|
|
|
173
178
|
<Checkbox
|
|
174
179
|
name={name}
|
|
175
180
|
readOnly={readOnly}
|
|
176
|
-
size={
|
|
181
|
+
size={itemSize}
|
|
177
182
|
checked={itemChecked(option)}
|
|
178
183
|
disabled={disabledIds?.includes(ov)}
|
|
179
184
|
onChange={(event) => {
|
|
@@ -203,7 +208,7 @@ export function OptionGroup<
|
|
|
203
208
|
) : (
|
|
204
209
|
<Radio
|
|
205
210
|
disabled={disabledIds?.includes(ov)}
|
|
206
|
-
size={
|
|
211
|
+
size={itemSize}
|
|
207
212
|
readOnly={readOnly}
|
|
208
213
|
/>
|
|
209
214
|
);
|
package/src/SelectEx.tsx
CHANGED
|
@@ -143,7 +143,7 @@ export function SelectEx<
|
|
|
143
143
|
|
|
144
144
|
// Options state
|
|
145
145
|
const [localOptions, setOptions] = React.useState<readonly T[]>([]);
|
|
146
|
-
const isMounted = React.useRef(
|
|
146
|
+
const isMounted = React.useRef(false);
|
|
147
147
|
|
|
148
148
|
const doItemChange = (
|
|
149
149
|
options: readonly T[],
|
|
@@ -151,18 +151,21 @@ export function SelectEx<
|
|
|
151
151
|
userAction: boolean
|
|
152
152
|
) => {
|
|
153
153
|
if (onItemChange == null) return;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
|
|
155
|
+
let option: T | undefined;
|
|
156
|
+
if (multiple && Array.isArray(value)) {
|
|
157
|
+
option = options.find((option) => value.includes(option[idField]));
|
|
158
|
+
} else if (value == null || value === '') {
|
|
159
|
+
option = undefined;
|
|
160
|
+
} else {
|
|
161
|
+
option = options.find((option) => option[idField] === value);
|
|
157
162
|
}
|
|
158
|
-
const option = options.find((option) => option[idField] === value);
|
|
159
163
|
onItemChange(option, userAction);
|
|
160
164
|
};
|
|
161
165
|
|
|
162
166
|
const setOptionsAdd = (options: readonly T[]) => {
|
|
163
167
|
setOptions(options);
|
|
164
|
-
if (
|
|
165
|
-
doItemChange(options, localValue, false);
|
|
168
|
+
if (valueSource != null) doItemChange(options, valueSource, false);
|
|
166
169
|
};
|
|
167
170
|
|
|
168
171
|
// When options change
|
|
@@ -174,17 +177,18 @@ export function SelectEx<
|
|
|
174
177
|
}, [options, propertyWay]);
|
|
175
178
|
|
|
176
179
|
// Local value
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
const v = defaultValue ?? value;
|
|
181
|
+
const valueSource = multiple
|
|
182
|
+
? v
|
|
183
|
+
? Array.isArray(v)
|
|
184
|
+
? v
|
|
185
|
+
: [v]
|
|
186
|
+
: []
|
|
187
|
+
: v ?? '';
|
|
185
188
|
|
|
186
189
|
// Value state
|
|
187
|
-
const [valueState, setValueStateBase] =
|
|
190
|
+
const [valueState, setValueStateBase] =
|
|
191
|
+
React.useState<unknown>(valueSource);
|
|
188
192
|
const valueRef = React.useRef<unknown>();
|
|
189
193
|
const setValueState = (newValue: unknown) => {
|
|
190
194
|
valueRef.current = newValue;
|
|
@@ -192,8 +196,8 @@ export function SelectEx<
|
|
|
192
196
|
};
|
|
193
197
|
|
|
194
198
|
React.useEffect(() => {
|
|
195
|
-
if (
|
|
196
|
-
}, [
|
|
199
|
+
if (valueSource != null) setValueState(valueSource);
|
|
200
|
+
}, [valueSource]);
|
|
197
201
|
|
|
198
202
|
// Label id
|
|
199
203
|
const labelId = `selectex-label-${name}`;
|
|
@@ -204,16 +208,17 @@ export function SelectEx<
|
|
|
204
208
|
return valueState === id;
|
|
205
209
|
};
|
|
206
210
|
|
|
207
|
-
// Change handler
|
|
208
|
-
const handleChange = (event: SelectChangeEvent<unknown>) => {
|
|
209
|
-
const value = event.target.value;
|
|
210
|
-
if (multiple && !Array.isArray(value)) return setItemValue([value]);
|
|
211
|
-
else return setItemValue(value);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
211
|
// Set item
|
|
215
212
|
const setItemValue = (id: unknown) => {
|
|
216
213
|
if (id != valueRef.current) {
|
|
214
|
+
// Difference
|
|
215
|
+
const diff = multiple
|
|
216
|
+
? Utils.arrayDifferences(
|
|
217
|
+
id as T[D][],
|
|
218
|
+
valueRef.current as T[D][]
|
|
219
|
+
)
|
|
220
|
+
: id;
|
|
221
|
+
|
|
217
222
|
setValueState(id);
|
|
218
223
|
|
|
219
224
|
const input = divRef.current?.querySelector('input');
|
|
@@ -221,9 +226,9 @@ export function SelectEx<
|
|
|
221
226
|
// Different value, trigger change event
|
|
222
227
|
ReactUtils.triggerChange(input, id as string, false);
|
|
223
228
|
}
|
|
224
|
-
return
|
|
229
|
+
return diff;
|
|
225
230
|
}
|
|
226
|
-
return
|
|
231
|
+
return undefined;
|
|
227
232
|
};
|
|
228
233
|
|
|
229
234
|
// Get option id
|
|
@@ -257,7 +262,7 @@ export function SelectEx<
|
|
|
257
262
|
// When value change
|
|
258
263
|
React.useEffect(() => {
|
|
259
264
|
refreshData();
|
|
260
|
-
}, [
|
|
265
|
+
}, [valueSource]);
|
|
261
266
|
|
|
262
267
|
// When layout ready
|
|
263
268
|
React.useEffect(() => {
|
|
@@ -268,6 +273,8 @@ export function SelectEx<
|
|
|
268
273
|
};
|
|
269
274
|
input?.addEventListener('change', inputChange);
|
|
270
275
|
|
|
276
|
+
isMounted.current = true;
|
|
277
|
+
|
|
271
278
|
return () => {
|
|
272
279
|
isMounted.current = false;
|
|
273
280
|
input?.removeEventListener('change', inputChange);
|
|
@@ -297,10 +304,12 @@ export function SelectEx<
|
|
|
297
304
|
<Select
|
|
298
305
|
ref={divRef}
|
|
299
306
|
value={
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
multiple
|
|
308
|
+
? valueState
|
|
309
|
+
: localOptions.some(
|
|
310
|
+
(o) => o[idField] === valueState
|
|
311
|
+
)
|
|
312
|
+
? valueState
|
|
304
313
|
: ''
|
|
305
314
|
}
|
|
306
315
|
input={
|
|
@@ -321,12 +330,10 @@ export function SelectEx<
|
|
|
321
330
|
if (event.defaultPrevented) return;
|
|
322
331
|
}
|
|
323
332
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
true
|
|
329
|
-
);
|
|
333
|
+
// Set item value
|
|
334
|
+
const diff = setItemValue(event.target.value);
|
|
335
|
+
if (diff != null) {
|
|
336
|
+
doItemChange(localOptions, diff, true);
|
|
330
337
|
}
|
|
331
338
|
}}
|
|
332
339
|
renderValue={(selected) => {
|
package/src/index.ts
CHANGED
|
@@ -47,6 +47,7 @@ export * from './HiSelector';
|
|
|
47
47
|
export * from './IconButtonLink';
|
|
48
48
|
export * from './InputField';
|
|
49
49
|
export * from './ItemList';
|
|
50
|
+
export * from './ListChooser';
|
|
50
51
|
export * from './ListItemRightIcon';
|
|
51
52
|
export * from './ListMoreDisplay';
|
|
52
53
|
export * from './LoadingButton';
|