@linzjs/step-ag-grid 6.1.0 → 7.0.0
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/index.css +12 -2
- package/dist/index.js +389 -213
- package/dist/index.js.map +1 -1
- package/dist/src/components/GridPopoverHook.d.ts +1 -1
- package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +17 -12
- package/dist/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.d.ts +1 -1
- package/dist/src/contexts/GridSubComponentContext.d.ts +1 -0
- package/dist/src/lui/ActionButton.d.ts +2 -1
- package/dist/src/lui/FormError.d.ts +2 -1
- package/dist/src/lui/TextAreaInput.d.ts +1 -1
- package/dist/src/lui/TextInputFormatted.d.ts +1 -1
- package/dist/src/react-menu3/utils/utils.d.ts +1 -0
- package/dist/src/utils/textMatcher.d.ts +13 -0
- package/dist/src/utils/textValidator.d.ts +3 -2
- package/dist/src/utils/util.d.ts +2 -1
- package/dist/step-ag-grid.esm.js +390 -215
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +9 -8
- package/src/components/GridPopoverHook.tsx +7 -1
- package/src/components/gridForm/GridFormDropDown.tsx +15 -10
- package/src/components/gridForm/GridFormMultiSelect.tsx +290 -188
- package/src/components/gridForm/GridFormPopoverMenu.tsx +1 -0
- package/src/components/gridForm/GridFormSubComponentTextArea.tsx +2 -2
- package/src/components/gridForm/GridFormSubComponentTextInput.tsx +3 -5
- package/src/components/gridForm/GridFormTextArea.tsx +13 -13
- package/src/components/gridForm/GridFormTextInput.tsx +3 -8
- package/src/components/gridPopoverEdit/GridPopoutEditMultiSelect.ts +3 -3
- package/src/contexts/GridSubComponentContext.ts +2 -0
- package/src/lui/ActionButton.tsx +26 -8
- package/src/lui/FormError.scss +7 -0
- package/src/lui/FormError.tsx +4 -13
- package/src/lui/TextAreaInput.tsx +1 -1
- package/src/lui/TextInputFormatted.tsx +1 -1
- package/src/react-menu3/components/ControlledMenu.tsx +3 -2
- package/src/react-menu3/components/MenuList.tsx +2 -12
- package/src/react-menu3/hooks/useItems.ts +5 -2
- package/src/react-menu3/utils/utils.ts +15 -0
- package/src/stories/components/ActionButton.stories.tsx +9 -0
- package/src/stories/grid/GridPopoutEditMultiSelect.stories.tsx +93 -17
- package/src/styles/Grid.scss +12 -0
- package/src/styles/lui-overrides.scss +0 -2
- package/src/utils/textMatcher.ts +31 -0
- package/src/utils/textValidator.ts +6 -3
- package/src/utils/util.ts +6 -7
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@linzjs/step-ag-grid",
|
|
3
3
|
"repository": "github:linz/step-ag-grid.git",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "7.0.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"aggrid",
|
|
8
8
|
"ag-grid",
|
|
@@ -35,12 +35,13 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@linzjs/lui": ">=17",
|
|
38
|
-
"ag-grid-community": "
|
|
39
|
-
"ag-grid-react": "
|
|
38
|
+
"ag-grid-community": ">=27",
|
|
39
|
+
"ag-grid-react": ">=27",
|
|
40
40
|
"debounce-promise": "^3.1.2",
|
|
41
|
-
"lodash-es": "
|
|
42
|
-
"
|
|
43
|
-
"react
|
|
41
|
+
"lodash-es": ">=4",
|
|
42
|
+
"matcher": "^5.0.0",
|
|
43
|
+
"react": ">=17",
|
|
44
|
+
"react-dom": ">=17",
|
|
44
45
|
"react-transition-state": "^1.1.5",
|
|
45
46
|
"uuid": "^9.0.0"
|
|
46
47
|
},
|
|
@@ -122,8 +123,8 @@
|
|
|
122
123
|
"mkdirp": "^1.0.4",
|
|
123
124
|
"npm-run-all": "^4.1.5",
|
|
124
125
|
"postcss": "^8.4.18",
|
|
125
|
-
"postcss-scss": "^4.0.5",
|
|
126
126
|
"postcss-loader": "^7.0.1",
|
|
127
|
+
"postcss-scss": "^4.0.5",
|
|
127
128
|
"prettier": "^2.7.1",
|
|
128
129
|
"react-scripts": "^5.0.1",
|
|
129
130
|
"rollup": "^3.2.5",
|
|
@@ -138,8 +139,8 @@
|
|
|
138
139
|
"style-loader": "2.0.0",
|
|
139
140
|
"stylelint": "^14.14.1",
|
|
140
141
|
"stylelint-config-prettier": "^9.0.3",
|
|
141
|
-
"stylelint-config-standard": "^29.0.0",
|
|
142
142
|
"stylelint-config-recommended-scss": "^8.0.0",
|
|
143
|
+
"stylelint-config-standard": "^29.0.0",
|
|
143
144
|
"stylelint-prettier": "^2.0.0",
|
|
144
145
|
"stylelint-scss": "^4.3.0",
|
|
145
146
|
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
|
@@ -8,7 +8,13 @@ import { CloseReason } from "../react-menu3/utils";
|
|
|
8
8
|
|
|
9
9
|
export interface GridPopoverHookProps<RowType> {
|
|
10
10
|
className: string | undefined;
|
|
11
|
-
invalid?: () =>
|
|
11
|
+
invalid?: () =>
|
|
12
|
+
| Promise<JSX.Element | boolean | string | null | undefined>
|
|
13
|
+
| JSX.Element
|
|
14
|
+
| boolean
|
|
15
|
+
| string
|
|
16
|
+
| null
|
|
17
|
+
| undefined;
|
|
12
18
|
save?: (selectedRows: RowType[]) => Promise<boolean>;
|
|
13
19
|
}
|
|
14
20
|
|
|
@@ -68,12 +68,15 @@ export const GridFormDropDown = <RowType extends GridBaseRow>(props: GridFormPop
|
|
|
68
68
|
const optionsInitialising = useRef(false);
|
|
69
69
|
const [options, setOptions] = useState<FinalSelectOption[] | null>(null);
|
|
70
70
|
const subComponentIsValid = useRef(false);
|
|
71
|
-
const
|
|
71
|
+
const subComponentInitialValue = useRef<string | null>(null);
|
|
72
|
+
const [subSelectedValue, setSubSelectedValue] = useState<any>(null);
|
|
72
73
|
const [selectedSubComponent, setSelectedSubComponent] = useState<FinalSelectOption | null>(null);
|
|
73
74
|
|
|
74
75
|
const selectItemHandler = useCallback(
|
|
75
76
|
async (value: any, subComponentValue?: any): Promise<boolean> => {
|
|
76
|
-
const hasChanged =
|
|
77
|
+
const hasChanged =
|
|
78
|
+
selectedRows.some((row) => row[field as keyof RowType] !== value) ||
|
|
79
|
+
(subComponentValue !== undefined && subComponentInitialValue.current !== JSON.stringify(subComponentValue));
|
|
77
80
|
if (hasChanged) {
|
|
78
81
|
if (props.onSelectedItem) {
|
|
79
82
|
await props.onSelectedItem({ selectedRows, value, subComponentValue });
|
|
@@ -182,6 +185,7 @@ export const GridFormDropDown = <RowType extends GridBaseRow>(props: GridFormPop
|
|
|
182
185
|
*/
|
|
183
186
|
const save = useCallback(async () => {
|
|
184
187
|
if (!options) return true;
|
|
188
|
+
|
|
185
189
|
const activeOptions = options.filter((option) => !filteredValues.includes(option.value));
|
|
186
190
|
if (activeOptions.length === 1) {
|
|
187
191
|
await selectItemHandler(activeOptions[0].value);
|
|
@@ -254,14 +258,10 @@ export const GridFormDropDown = <RowType extends GridBaseRow>(props: GridFormPop
|
|
|
254
258
|
value={item.value}
|
|
255
259
|
onClick={(e: ClickEvent) => {
|
|
256
260
|
if (item.subComponent) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
} else {
|
|
262
|
-
// toggle selection on
|
|
263
|
-
setSelectedSubComponent(item);
|
|
264
|
-
}
|
|
261
|
+
// toggle selection
|
|
262
|
+
setSelectedSubComponent(selectedSubComponent === item ? null : item);
|
|
263
|
+
subComponentIsValid.current = true;
|
|
264
|
+
subComponentInitialValue.current = null;
|
|
265
265
|
e.keepOpen = true;
|
|
266
266
|
} else {
|
|
267
267
|
clickItemHandler(
|
|
@@ -285,10 +285,15 @@ export const GridFormDropDown = <RowType extends GridBaseRow>(props: GridFormPop
|
|
|
285
285
|
{(ref: MenuInstance) => (
|
|
286
286
|
<GridSubComponentContext.Provider
|
|
287
287
|
value={{
|
|
288
|
+
context: { options },
|
|
288
289
|
data,
|
|
289
290
|
value: subSelectedValue,
|
|
290
291
|
setValue: (value: any) => {
|
|
291
292
|
setSubSelectedValue(value);
|
|
293
|
+
if (subComponentInitialValue.current === null) {
|
|
294
|
+
// copy the default value of the sub-component so we can change detect on save
|
|
295
|
+
subComponentInitialValue.current = JSON.stringify(value);
|
|
296
|
+
}
|
|
292
297
|
},
|
|
293
298
|
setValid: (valid: boolean) => {
|
|
294
299
|
subComponentIsValid.current = valid;
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import "../../styles/GridFormMultiSelect.scss";
|
|
2
2
|
|
|
3
|
-
import { FocusableItem, MenuDivider, MenuItem } from "../../react-menu3";
|
|
4
|
-
import {
|
|
3
|
+
import { FocusableItem, MenuDivider, MenuHeader, MenuItem } from "../../react-menu3";
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
KeyboardEvent,
|
|
11
|
+
SetStateAction,
|
|
12
|
+
Dispatch,
|
|
13
|
+
Fragment,
|
|
14
|
+
} from "react";
|
|
5
15
|
import { GridBaseRow } from "../Grid";
|
|
6
16
|
import { ComponentLoadingWrapper } from "../ComponentLoadingWrapper";
|
|
7
|
-
import {
|
|
17
|
+
import { groupBy, isEmpty, pick, toPairs } from "lodash-es";
|
|
8
18
|
import { LuiCheckboxInput } from "@linzjs/lui";
|
|
9
19
|
import { useGridPopoverHook } from "../GridPopoverHook";
|
|
10
20
|
import { MenuSeparatorString } from "./GridFormDropDown";
|
|
@@ -12,20 +22,24 @@ import { CellEditorCommon } from "../GridCell";
|
|
|
12
22
|
import { ClickEvent } from "../../react-menu3/types";
|
|
13
23
|
import { GridSubComponentContext } from "contexts/GridSubComponentContext";
|
|
14
24
|
import { useGridPopoverContext } from "../../contexts/GridPopoverContext";
|
|
25
|
+
import { FormError } from "../../lui/FormError";
|
|
26
|
+
import { textMatch } from "../../utils/textMatcher";
|
|
15
27
|
|
|
16
|
-
interface
|
|
17
|
-
value:
|
|
18
|
-
label?:
|
|
28
|
+
export interface MultiSelectOption {
|
|
29
|
+
value: any;
|
|
30
|
+
label?: string;
|
|
19
31
|
subComponent?: (props: any) => JSX.Element;
|
|
32
|
+
subValue?: any;
|
|
33
|
+
filter?: string;
|
|
34
|
+
checked?: boolean;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
subValue?: any;
|
|
37
|
+
export interface GridFormMultiSelectGroup {
|
|
38
|
+
header: string;
|
|
39
|
+
filter?: string;
|
|
26
40
|
}
|
|
27
41
|
|
|
28
|
-
export interface GridFormMultiSelectProps<RowType extends GridBaseRow
|
|
42
|
+
export interface GridFormMultiSelectProps<RowType extends GridBaseRow> extends CellEditorCommon {
|
|
29
43
|
className?:
|
|
30
44
|
| "GridMultiSelect-containerSmall"
|
|
31
45
|
| "GridMultiSelect-containerMedium"
|
|
@@ -35,218 +49,306 @@ export interface GridFormMultiSelectProps<RowType extends GridBaseRow, ValueType
|
|
|
35
49
|
| undefined;
|
|
36
50
|
filtered?: boolean;
|
|
37
51
|
filterPlaceholder?: string;
|
|
38
|
-
|
|
39
|
-
options:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
filterHelpText?: string | ((filter: string, options: MultiSelectOption[]) => string | undefined);
|
|
53
|
+
onSelectFilter?: (filter: string, options: MultiSelectOption[]) => void;
|
|
54
|
+
onSave?: (selectedRows: RowType[], selectedOptions: MultiSelectOption[]) => Promise<boolean>;
|
|
55
|
+
headers?: GridFormMultiSelectGroup[];
|
|
56
|
+
options: MultiSelectOption[] | ((selectedRows: RowType[]) => Promise<MultiSelectOption[]> | MultiSelectOption[]);
|
|
57
|
+
invalid?: (selectedRows: RowType[], selectedOptions: MultiSelectOption[]) => boolean;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
export const GridFormMultiSelect = <RowType extends GridBaseRow
|
|
46
|
-
props: GridFormMultiSelectProps<RowType, ValueType>,
|
|
47
|
-
) => {
|
|
60
|
+
export const GridFormMultiSelect = <RowType extends GridBaseRow>(props: GridFormMultiSelectProps<RowType>) => {
|
|
48
61
|
const { selectedRows, data } = useGridPopoverContext<RowType>();
|
|
49
62
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
// convert array of strings to object<value,null>
|
|
53
|
-
return Array.isArray(r) ? fromPairs(r.map((v) => [v, null])) : r;
|
|
54
|
-
}, [props, selectedRows]);
|
|
55
|
-
|
|
56
|
-
const subComponentIsValid = useRef<Record<string, boolean>>({});
|
|
57
|
-
const [selectedValues, setSelectedValues] = useState<Record<string, null | any>>(() => initialiseValues ?? {});
|
|
63
|
+
const subComponentIsValidRef = useRef<Record<string, boolean>>({});
|
|
64
|
+
const optionsInitialising = useRef(false);
|
|
58
65
|
|
|
59
66
|
const [filter, setFilter] = useState("");
|
|
60
|
-
const [
|
|
61
|
-
const
|
|
62
|
-
const [options, setOptions] = useState<MultiFinalSelectOption<ValueType>[]>();
|
|
67
|
+
const [initialValues, setInitialValues] = useState("");
|
|
68
|
+
const [options, setOptions] = useState<MultiSelectOption[]>();
|
|
63
69
|
|
|
64
70
|
const invalid = useCallback(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
if (!options) return true;
|
|
72
|
+
const selectedValues = options.filter((o) => o.checked).map((o) => o.value);
|
|
73
|
+
const subValidations = pick(subComponentIsValidRef.current, selectedValues);
|
|
74
|
+
if (Object.values(subValidations).some((v) => !v)) return true;
|
|
75
|
+
return (
|
|
76
|
+
props.invalid &&
|
|
77
|
+
props.invalid(
|
|
78
|
+
selectedRows,
|
|
79
|
+
options.filter((o) => o.checked),
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
}, [options, props, selectedRows]);
|
|
68
83
|
|
|
69
84
|
const save = useCallback(
|
|
70
85
|
async (selectedRows: RowType[]): Promise<boolean> => {
|
|
71
|
-
if (props.onSave)
|
|
72
|
-
if (!options) {
|
|
73
|
-
console.error("options not initialised");
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
86
|
+
if (!options || !props.onSave) return true;
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (notValid) return false;
|
|
88
|
+
// Any changes to save?
|
|
89
|
+
if (initialValues === JSON.stringify(options)) return true;
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
o.subValue = subValue;
|
|
86
|
-
return o;
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
if (isEqual(initialiseValues, selectedValues)) {
|
|
90
|
-
// No changes to save
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return await props.onSave(selectedRows, menuOptionSubValueResult);
|
|
95
|
-
}
|
|
96
|
-
return true;
|
|
91
|
+
return props.onSave(
|
|
92
|
+
selectedRows,
|
|
93
|
+
options.filter((o) => o.checked),
|
|
94
|
+
);
|
|
97
95
|
},
|
|
98
|
-
[
|
|
96
|
+
[initialValues, options, props],
|
|
99
97
|
);
|
|
100
98
|
|
|
101
99
|
// Load up options list if it's async function
|
|
102
100
|
useEffect(() => {
|
|
103
101
|
if (options || optionsInitialising.current) return;
|
|
104
102
|
optionsInitialising.current = true;
|
|
105
|
-
let optionsConf = props.options ?? [];
|
|
106
103
|
|
|
107
104
|
(async () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const optionsList = optionsConf?.map((item) => {
|
|
113
|
-
if (item == null || typeof item === "string" || typeof item === "number") {
|
|
114
|
-
item = { value: item as ValueType, label: item } as MultiFinalSelectOption<ValueType>;
|
|
115
|
-
}
|
|
116
|
-
return item;
|
|
117
|
-
}) as any as MultiFinalSelectOption<ValueType>[];
|
|
118
|
-
|
|
119
|
-
if (props.filtered) {
|
|
120
|
-
// This is needed otherwise when filter input is rendered and sets autofocus
|
|
121
|
-
// the mouse up of the double click edit triggers the cell to cancel editing
|
|
122
|
-
delay(() => setOptions(optionsList), 100);
|
|
123
|
-
} else {
|
|
124
|
-
setOptions(optionsList);
|
|
125
|
-
}
|
|
105
|
+
const optionsList = typeof props.options === "function" ? await props.options(selectedRows) : props.options;
|
|
106
|
+
setInitialValues(JSON.stringify(optionsList));
|
|
107
|
+
setOptions(optionsList);
|
|
126
108
|
optionsInitialising.current = false;
|
|
127
109
|
})();
|
|
128
|
-
}, [
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (!props.filtered || options == null) return;
|
|
132
|
-
setFilteredValues(
|
|
133
|
-
options
|
|
134
|
-
.map((option) => {
|
|
135
|
-
if (option.label != null && typeof option.label !== "string") {
|
|
136
|
-
console.error("Cannot filter non-string labels", option);
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
const str = (option.label as string) || "";
|
|
140
|
-
return str.toLowerCase().indexOf(filter.trim()) === -1 ? option.value : undefined;
|
|
141
|
-
})
|
|
142
|
-
.filter((r) => r !== undefined),
|
|
143
|
-
);
|
|
144
|
-
}, [props.filtered, filter, options]);
|
|
110
|
+
}, [options, props, selectedRows]);
|
|
145
111
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Groups options into their header groups
|
|
114
|
+
*/
|
|
115
|
+
const headerGroups = useMemo(
|
|
116
|
+
() =>
|
|
117
|
+
options &&
|
|
118
|
+
groupBy(
|
|
119
|
+
options.filter((o) => textMatch(o.label, filter) && o.value),
|
|
120
|
+
"filter",
|
|
121
|
+
),
|
|
122
|
+
[filter, options],
|
|
155
123
|
);
|
|
156
124
|
|
|
125
|
+
const headers: GridFormMultiSelectGroup[] = useMemo(() => props.headers ?? [{ header: "" }], [props.headers]);
|
|
126
|
+
|
|
157
127
|
const { popoverWrapper, triggerSave } = useGridPopoverHook({
|
|
158
128
|
className: props.className,
|
|
159
129
|
invalid,
|
|
160
130
|
save,
|
|
161
131
|
});
|
|
132
|
+
|
|
162
133
|
return popoverWrapper(
|
|
163
134
|
<ComponentLoadingWrapper loading={!options} className={"GridFormMultiSelect-container"}>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
style={{ border: "0px" }}
|
|
174
|
-
ref={ref}
|
|
175
|
-
type="text"
|
|
176
|
-
placeholder={props.filterPlaceholder ?? "Placeholder"}
|
|
177
|
-
data-testid={"filteredMenu-free-text-input"}
|
|
178
|
-
defaultValue={""}
|
|
179
|
-
onChange={(e) => setFilter(e.target.value.toLowerCase())}
|
|
180
|
-
/>
|
|
181
|
-
</div>
|
|
182
|
-
)}
|
|
183
|
-
</FocusableItem>
|
|
184
|
-
<MenuDivider key={`$$divider_filter`} />
|
|
185
|
-
</>
|
|
186
|
-
)}
|
|
187
|
-
<div className={"GridFormMultiSelect-options"}>
|
|
188
|
-
{options?.map((item, index) =>
|
|
189
|
-
item.value === MenuSeparatorString ? (
|
|
190
|
-
<MenuDivider key={`$$divider_${index}`} />
|
|
191
|
-
) : filteredValues.includes(item.value) ? null : (
|
|
192
|
-
<div key={`${index}`}>
|
|
193
|
-
<MenuItem
|
|
194
|
-
onClick={(e: ClickEvent) => {
|
|
195
|
-
// Global react-menu MenuItem handler handles tabs
|
|
196
|
-
if (e.key !== "Tab") {
|
|
197
|
-
e.keepOpen = true;
|
|
198
|
-
toggleValue(item);
|
|
199
|
-
}
|
|
200
|
-
}}
|
|
201
|
-
>
|
|
202
|
-
<LuiCheckboxInput
|
|
203
|
-
isChecked={`${item.value}` in selectedValues}
|
|
204
|
-
value={`${item.value}`}
|
|
205
|
-
label={item.label ?? (item.value == null ? `<${item.value}>` : `${item.value}`)}
|
|
206
|
-
inputProps={{
|
|
207
|
-
onClick: (e) => {
|
|
208
|
-
e.preventDefault();
|
|
209
|
-
e.stopPropagation();
|
|
210
|
-
return false;
|
|
211
|
-
},
|
|
212
|
-
}}
|
|
213
|
-
onChange={() => {
|
|
214
|
-
/*Do nothing, change handled by menuItem*/
|
|
215
|
-
}}
|
|
216
|
-
/>
|
|
217
|
-
</MenuItem>
|
|
218
|
-
|
|
219
|
-
{`${item.value}` in selectedValues && item.subComponent && (
|
|
220
|
-
<FocusableItem className={"LuiDeprecatedForms"} key={`${item.value}_subcomponent`}>
|
|
221
|
-
{(_: any) =>
|
|
222
|
-
item.subComponent && (
|
|
223
|
-
<GridSubComponentContext.Provider
|
|
224
|
-
value={{
|
|
225
|
-
data,
|
|
226
|
-
value: selectedValues[`${item.value}`],
|
|
227
|
-
setValue: (value: any) => {
|
|
228
|
-
setSelectedValues({
|
|
229
|
-
...selectedValues,
|
|
230
|
-
[`${item.value}`]: value,
|
|
231
|
-
});
|
|
232
|
-
},
|
|
233
|
-
setValid: (valid: boolean) => {
|
|
234
|
-
subComponentIsValid.current[`${item.value}`] = valid;
|
|
235
|
-
},
|
|
236
|
-
triggerSave,
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
<item.subComponent />
|
|
240
|
-
</GridSubComponentContext.Provider>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
</FocusableItem>
|
|
244
|
-
)}
|
|
245
|
-
</div>
|
|
246
|
-
),
|
|
135
|
+
{options && (
|
|
136
|
+
<>
|
|
137
|
+
{props.filtered && (
|
|
138
|
+
<FilterInput
|
|
139
|
+
{...{ headerGroups, options, setOptions, filter, setFilter }}
|
|
140
|
+
filterHelpText={props.filterHelpText}
|
|
141
|
+
onSelectFilter={props.onSelectFilter}
|
|
142
|
+
filterPlaceholder={props.filterPlaceholder}
|
|
143
|
+
/>
|
|
247
144
|
)}
|
|
248
|
-
|
|
249
|
-
|
|
145
|
+
|
|
146
|
+
{headerGroups && (
|
|
147
|
+
<div className={"GridFormMultiSelect-options"}>
|
|
148
|
+
{headers.map((header) => {
|
|
149
|
+
const subOptions = headerGroups[`${header.filter}`];
|
|
150
|
+
return (
|
|
151
|
+
!isEmpty(subOptions) && (
|
|
152
|
+
<>
|
|
153
|
+
{header.header && <MenuHeader>{header.header}</MenuHeader>}
|
|
154
|
+
{subOptions.map((item, index) =>
|
|
155
|
+
item.value === MenuSeparatorString ? (
|
|
156
|
+
<MenuDivider key={`div_${index}`} />
|
|
157
|
+
) : (
|
|
158
|
+
<Fragment key={`val_${item.value}`}>
|
|
159
|
+
<MenuRadioItem item={item} options={options} setOptions={setOptions} />
|
|
160
|
+
|
|
161
|
+
{item.checked && item.subComponent && (
|
|
162
|
+
<MenuSubComponent
|
|
163
|
+
{...{ item, options, setOptions, data, triggerSave }}
|
|
164
|
+
subComponentIsValid={subComponentIsValidRef.current}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
167
|
+
</Fragment>
|
|
168
|
+
),
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
)
|
|
172
|
+
);
|
|
173
|
+
})}
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</>
|
|
177
|
+
)}
|
|
250
178
|
</ComponentLoadingWrapper>,
|
|
251
179
|
);
|
|
252
180
|
};
|
|
181
|
+
|
|
182
|
+
const FilterInput = (props: {
|
|
183
|
+
options: MultiSelectOption[];
|
|
184
|
+
setOptions: (options: MultiSelectOption[]) => void;
|
|
185
|
+
onSelectFilter?: (filter: string, options: MultiSelectOption[]) => void;
|
|
186
|
+
filter: string;
|
|
187
|
+
setFilter: Dispatch<SetStateAction<string>>;
|
|
188
|
+
headerGroups: Record<string, MultiSelectOption[]> | undefined;
|
|
189
|
+
filterPlaceholder?: string;
|
|
190
|
+
filterHelpText?: string | ((filter: string, options: MultiSelectOption[]) => string | undefined);
|
|
191
|
+
}) => {
|
|
192
|
+
const { options, setOptions, onSelectFilter, filter, setFilter, headerGroups, filterPlaceholder, filterHelpText } =
|
|
193
|
+
props;
|
|
194
|
+
|
|
195
|
+
const toggleSelectAllVisible = useCallback(() => {
|
|
196
|
+
if (!options || !headerGroups) return;
|
|
197
|
+
|
|
198
|
+
if (isEmpty(filter.trim())) {
|
|
199
|
+
// Toggle off if any items are checked otherwise on
|
|
200
|
+
const anyChecked = options.some((o) => o.checked);
|
|
201
|
+
options.forEach((o) => (o.checked = !anyChecked));
|
|
202
|
+
} else {
|
|
203
|
+
// Toggle on if any filtered items are checked otherwise off
|
|
204
|
+
const anyChecked = Object.values(headerGroups).some((headerOptions) =>
|
|
205
|
+
headerOptions.some((o) => o.checked === false),
|
|
206
|
+
);
|
|
207
|
+
Object.values(headerGroups).forEach((headerOptions) => {
|
|
208
|
+
headerOptions.forEach((o) => {
|
|
209
|
+
if (o.checked !== undefined) o.checked = anyChecked;
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
setOptions([...options]);
|
|
214
|
+
}, [filter, headerGroups, options, setOptions]);
|
|
215
|
+
|
|
216
|
+
const addCustomFilterValue = useCallback(() => {
|
|
217
|
+
if (!options || !onSelectFilter) return;
|
|
218
|
+
|
|
219
|
+
const preFilterOptions = JSON.stringify(options);
|
|
220
|
+
onSelectFilter(filter.trim(), options);
|
|
221
|
+
// Detect if options list changed and update
|
|
222
|
+
if (preFilterOptions === JSON.stringify(options)) return;
|
|
223
|
+
|
|
224
|
+
setOptions([...options]);
|
|
225
|
+
setFilter("");
|
|
226
|
+
}, [filter, onSelectFilter, options, setFilter, setOptions]);
|
|
227
|
+
|
|
228
|
+
const handleKeyUp = useCallback(
|
|
229
|
+
(e: KeyboardEvent) => {
|
|
230
|
+
if (e.key === "Enter") {
|
|
231
|
+
e.stopPropagation();
|
|
232
|
+
e.preventDefault();
|
|
233
|
+
|
|
234
|
+
if (e.ctrlKey) toggleSelectAllVisible();
|
|
235
|
+
else if (onSelectFilter) addCustomFilterValue();
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
[addCustomFilterValue, onSelectFilter, toggleSelectAllVisible],
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<>
|
|
243
|
+
<FocusableItem className={"filter-item"} key={"filter"}>
|
|
244
|
+
{(_: any) => (
|
|
245
|
+
<div style={{ width: "100%" }} className={"GridFormMultiSelect-filter"}>
|
|
246
|
+
<input
|
|
247
|
+
className={"LuiTextInput-input"}
|
|
248
|
+
type="text"
|
|
249
|
+
placeholder={filterPlaceholder ?? "Placeholder"}
|
|
250
|
+
data-testid={"filteredMenu-free-text-input"}
|
|
251
|
+
value={filter}
|
|
252
|
+
data-disableenterautosave={true}
|
|
253
|
+
data-allowtabtoSave={true}
|
|
254
|
+
onChange={(e) => setFilter(e.target.value)}
|
|
255
|
+
onKeyUp={handleKeyUp}
|
|
256
|
+
/>
|
|
257
|
+
{filterHelpText && (
|
|
258
|
+
<FormError
|
|
259
|
+
error={null}
|
|
260
|
+
helpText={
|
|
261
|
+
typeof filterHelpText === "function" ? filterHelpText(filter.trim(), options) : filterHelpText
|
|
262
|
+
}
|
|
263
|
+
/>
|
|
264
|
+
)}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</FocusableItem>
|
|
268
|
+
<MenuDivider key={`$$divider_filter`} />
|
|
269
|
+
{headerGroups && !toPairs(headerGroups).some(([_, options]) => !isEmpty(options)) && (
|
|
270
|
+
<div className={"szh-menu__item"}>[No items match the filter]</div>
|
|
271
|
+
)}
|
|
272
|
+
</>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const MenuRadioItem = (props: {
|
|
277
|
+
item: MultiSelectOption;
|
|
278
|
+
options: MultiSelectOption[];
|
|
279
|
+
setOptions: (options: MultiSelectOption[]) => void;
|
|
280
|
+
}) => {
|
|
281
|
+
const { item, options, setOptions } = props;
|
|
282
|
+
|
|
283
|
+
const toggleValue = useCallback(
|
|
284
|
+
(item: MultiSelectOption) => {
|
|
285
|
+
item.checked = !item.checked;
|
|
286
|
+
setOptions([...options]);
|
|
287
|
+
},
|
|
288
|
+
[options, setOptions],
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<MenuItem
|
|
293
|
+
onClick={(e: ClickEvent) => {
|
|
294
|
+
// Global react-menu MenuItem handler handles tabs
|
|
295
|
+
if (e.key !== "Tab") {
|
|
296
|
+
e.keepOpen = true;
|
|
297
|
+
toggleValue(item);
|
|
298
|
+
}
|
|
299
|
+
}}
|
|
300
|
+
>
|
|
301
|
+
<LuiCheckboxInput
|
|
302
|
+
isChecked={item.checked ?? false}
|
|
303
|
+
value={`${item.value}`}
|
|
304
|
+
label={item.label ?? (item.value == null ? `<${item.value}>` : `${item.value}`)}
|
|
305
|
+
inputProps={{
|
|
306
|
+
onClick: (e) => {
|
|
307
|
+
// Click is handled by MenuItem onClick
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
e.stopPropagation();
|
|
310
|
+
},
|
|
311
|
+
}}
|
|
312
|
+
onChange={() => {
|
|
313
|
+
/*Do nothing, change handled by menuItem*/
|
|
314
|
+
}}
|
|
315
|
+
/>
|
|
316
|
+
</MenuItem>
|
|
317
|
+
);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const MenuSubComponent = (props: {
|
|
321
|
+
data: any;
|
|
322
|
+
item: MultiSelectOption;
|
|
323
|
+
options: MultiSelectOption[];
|
|
324
|
+
setOptions: (options: MultiSelectOption[]) => void;
|
|
325
|
+
subComponentIsValid: Record<string, boolean>;
|
|
326
|
+
triggerSave: () => Promise<void>;
|
|
327
|
+
}) => {
|
|
328
|
+
const { data, item, options, setOptions, subComponentIsValid, triggerSave } = props;
|
|
329
|
+
return (
|
|
330
|
+
<FocusableItem className={"LuiDeprecatedForms"} key={`${item.value}_subcomponent`}>
|
|
331
|
+
{(_: any) =>
|
|
332
|
+
item.subComponent && (
|
|
333
|
+
<GridSubComponentContext.Provider
|
|
334
|
+
value={{
|
|
335
|
+
context: { options },
|
|
336
|
+
data,
|
|
337
|
+
value: item.subValue,
|
|
338
|
+
setValue: (value: any) => {
|
|
339
|
+
item.subValue = value;
|
|
340
|
+
setOptions([...options]);
|
|
341
|
+
},
|
|
342
|
+
setValid: (valid: boolean) => {
|
|
343
|
+
subComponentIsValid[`${item.value}`] = valid;
|
|
344
|
+
},
|
|
345
|
+
triggerSave,
|
|
346
|
+
}}
|
|
347
|
+
>
|
|
348
|
+
<item.subComponent />
|
|
349
|
+
</GridSubComponentContext.Provider>
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
</FocusableItem>
|
|
353
|
+
);
|
|
354
|
+
};
|