@linzjs/step-ag-grid 6.1.1 → 7.0.1
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 +378 -203
- 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 +4 -2
- 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 +379 -205
- 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 +1 -0
- 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.scss +10 -0
- package/src/lui/ActionButton.tsx +36 -10
- 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 +11 -0
- package/src/stories/grid/FormTest.tsx +1 -1
- 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.1",
|
|
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
|
|
|
@@ -285,6 +285,7 @@ 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) => {
|
|
@@ -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
|
+
};
|
|
@@ -19,7 +19,7 @@ export interface GridSubComponentTextAreaProps<RowType extends GridBaseRow>
|
|
|
19
19
|
export const GridFormSubComponentTextArea = <RowType extends GridBaseRow>(
|
|
20
20
|
props: GridSubComponentTextAreaProps<RowType>,
|
|
21
21
|
): JSX.Element => {
|
|
22
|
-
const { value, data, setValue, setValid } = useContext(GridSubComponentContext);
|
|
22
|
+
const { value, data, setValue, setValid, context } = useContext(GridSubComponentContext);
|
|
23
23
|
|
|
24
24
|
const helpText = props.helpText ?? "Press tab to save";
|
|
25
25
|
|
|
@@ -28,7 +28,7 @@ export const GridFormSubComponentTextArea = <RowType extends GridBaseRow>(
|
|
|
28
28
|
if (value == null) setValue(props.defaultValue);
|
|
29
29
|
}, [props.defaultValue, setValue, value]);
|
|
30
30
|
|
|
31
|
-
const invalid = useCallback(() => TextInputValidator(props, value, data), [data, props, value]);
|
|
31
|
+
const invalid = useCallback(() => TextInputValidator(props, value, data, context), [data, props, value, context]);
|
|
32
32
|
|
|
33
33
|
useEffect(() => {
|
|
34
34
|
setValid(value != null && invalid() == null);
|
|
@@ -17,7 +17,7 @@ export interface GridFormSubComponentTextInputProps<RowType extends GridBaseRow>
|
|
|
17
17
|
export const GridFormSubComponentTextInput = <RowType extends GridBaseRow>(
|
|
18
18
|
props: GridFormSubComponentTextInputProps<RowType>,
|
|
19
19
|
): JSX.Element => {
|
|
20
|
-
const { value, setValue, setValid, data } = useContext(GridSubComponentContext);
|
|
20
|
+
const { value, setValue, setValid, data, context } = useContext(GridSubComponentContext);
|
|
21
21
|
|
|
22
22
|
const helpText = props.helpText ?? "Press enter or tab to save";
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ export const GridFormSubComponentTextInput = <RowType extends GridBaseRow>(
|
|
|
26
26
|
if (value == null) setValue(props.defaultValue);
|
|
27
27
|
}, [props.defaultValue, setValue, value]);
|
|
28
28
|
|
|
29
|
-
const invalid = useCallback(() => TextInputValidator(props, value, data), [data, props, value]);
|
|
29
|
+
const invalid = useCallback(() => TextInputValidator(props, value, data, context), [context, data, props, value]);
|
|
30
30
|
|
|
31
31
|
useEffect(() => {
|
|
32
32
|
setValid(value != null && invalid() == null);
|
|
@@ -40,9 +40,7 @@ export const GridFormSubComponentTextInput = <RowType extends GridBaseRow>(
|
|
|
40
40
|
helpText={helpText}
|
|
41
41
|
autoFocus={true}
|
|
42
42
|
placeholder={props.placeholder}
|
|
43
|
-
style={{
|
|
44
|
-
width: "100%",
|
|
45
|
-
}}
|
|
43
|
+
style={{ width: "100%" }}
|
|
46
44
|
/>
|
|
47
45
|
);
|
|
48
46
|
};
|