@linzjs/step-ag-grid 7.1.0 → 7.1.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.js +76 -20
- package/dist/index.js.map +1 -1
- package/dist/src/utils/textMatcher.d.ts +2 -2
- package/dist/step-ag-grid.esm.js +77 -21
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/gridForm/GridFormMultiSelect.tsx +83 -21
- package/src/components/gridPopoverEdit/GridPopoverEditDropDown.ts +2 -2
- package/src/stories/grid/GridPopoutEditMultiSelect.stories.tsx +2 -2
- package/src/utils/textMatcher.ts +11 -8
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "react";
|
|
15
15
|
import { GridBaseRow } from "../Grid";
|
|
16
16
|
import { ComponentLoadingWrapper } from "../ComponentLoadingWrapper";
|
|
17
|
-
import { groupBy, isEmpty, pick, toPairs } from "lodash-es";
|
|
17
|
+
import { fromPairs, groupBy, isEmpty, pick, toPairs } from "lodash-es";
|
|
18
18
|
import { LuiCheckboxInput } from "@linzjs/lui";
|
|
19
19
|
import { useGridPopoverHook } from "../GridPopoverHook";
|
|
20
20
|
import { MenuSeparatorString } from "./GridFormDropDown";
|
|
@@ -25,6 +25,8 @@ import { useGridPopoverContext } from "../../contexts/GridPopoverContext";
|
|
|
25
25
|
import { FormError } from "../../lui/FormError";
|
|
26
26
|
import { textMatch } from "../../utils/textMatcher";
|
|
27
27
|
|
|
28
|
+
type HeaderGroupType = Record<string, MultiSelectOption[]> | undefined;
|
|
29
|
+
|
|
28
30
|
export interface MultiSelectOption {
|
|
29
31
|
value: any;
|
|
30
32
|
label?: string;
|
|
@@ -112,15 +114,34 @@ export const GridFormMultiSelect = <RowType extends GridBaseRow>(props: GridForm
|
|
|
112
114
|
/**
|
|
113
115
|
* Groups options into their header groups
|
|
114
116
|
*/
|
|
115
|
-
const headerGroups = useMemo(
|
|
116
|
-
()
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
const headerGroups = useMemo(() => {
|
|
118
|
+
if (!options) return undefined;
|
|
119
|
+
const result = groupBy(
|
|
120
|
+
options.filter((o) => textMatch(o.label, filter) && o.value),
|
|
121
|
+
"filter",
|
|
122
|
+
);
|
|
123
|
+
// remove leading/trailing/duplicate dividers
|
|
124
|
+
return fromPairs(
|
|
125
|
+
toPairs(result).map(([key, arr]) => {
|
|
126
|
+
let lastWasDivider = true;
|
|
127
|
+
return [
|
|
128
|
+
key,
|
|
129
|
+
arr
|
|
130
|
+
.map((row, index) => {
|
|
131
|
+
if (row.value === MenuSeparatorString) {
|
|
132
|
+
if (lastWasDivider) return null;
|
|
133
|
+
if (index === arr.length - 1) return null;
|
|
134
|
+
lastWasDivider = true;
|
|
135
|
+
} else {
|
|
136
|
+
lastWasDivider = false;
|
|
137
|
+
}
|
|
138
|
+
return row;
|
|
139
|
+
})
|
|
140
|
+
.filter((r) => r),
|
|
141
|
+
];
|
|
142
|
+
}),
|
|
143
|
+
) as HeaderGroupType;
|
|
144
|
+
}, [filter, options]);
|
|
124
145
|
|
|
125
146
|
const headers: GridFormMultiSelectGroup[] = useMemo(() => props.headers ?? [{ header: "" }], [props.headers]);
|
|
126
147
|
|
|
@@ -136,7 +157,7 @@ export const GridFormMultiSelect = <RowType extends GridBaseRow>(props: GridForm
|
|
|
136
157
|
<>
|
|
137
158
|
{props.filtered && (
|
|
138
159
|
<FilterInput
|
|
139
|
-
{...{ headerGroups, options, setOptions, filter, setFilter }}
|
|
160
|
+
{...{ headerGroups, options, setOptions, filter, setFilter, triggerSave }}
|
|
140
161
|
filterHelpText={props.filterHelpText}
|
|
141
162
|
onSelectFilter={props.onSelectFilter}
|
|
142
163
|
filterPlaceholder={props.filterPlaceholder}
|
|
@@ -185,12 +206,25 @@ const FilterInput = (props: {
|
|
|
185
206
|
onSelectFilter?: (filter: string, options: MultiSelectOption[]) => void;
|
|
186
207
|
filter: string;
|
|
187
208
|
setFilter: Dispatch<SetStateAction<string>>;
|
|
188
|
-
headerGroups:
|
|
209
|
+
headerGroups: HeaderGroupType;
|
|
189
210
|
filterPlaceholder?: string;
|
|
190
211
|
filterHelpText?: string | ((filter: string, options: MultiSelectOption[]) => string | undefined);
|
|
212
|
+
triggerSave: () => Promise<void>;
|
|
191
213
|
}) => {
|
|
192
|
-
const {
|
|
193
|
-
|
|
214
|
+
const {
|
|
215
|
+
options,
|
|
216
|
+
setOptions,
|
|
217
|
+
onSelectFilter,
|
|
218
|
+
filter,
|
|
219
|
+
setFilter,
|
|
220
|
+
headerGroups,
|
|
221
|
+
filterPlaceholder,
|
|
222
|
+
filterHelpText,
|
|
223
|
+
triggerSave,
|
|
224
|
+
} = props;
|
|
225
|
+
|
|
226
|
+
const enterHasBeenPressed = useRef(false);
|
|
227
|
+
const lastKeyWasEnter = useRef(false);
|
|
194
228
|
|
|
195
229
|
const toggleSelectAllVisible = useCallback(() => {
|
|
196
230
|
if (!options || !headerGroups) return;
|
|
@@ -198,7 +232,9 @@ const FilterInput = (props: {
|
|
|
198
232
|
if (isEmpty(filter.trim())) {
|
|
199
233
|
// Toggle off if any items are checked otherwise on
|
|
200
234
|
const anyChecked = options.some((o) => o.checked);
|
|
201
|
-
options.forEach((o) =>
|
|
235
|
+
options.forEach((o) => {
|
|
236
|
+
if (o.label !== undefined) o.checked = !anyChecked;
|
|
237
|
+
});
|
|
202
238
|
} else {
|
|
203
239
|
// Toggle on if any filtered items are checked otherwise off
|
|
204
240
|
const anyChecked = Object.values(headerGroups).some((headerOptions) =>
|
|
@@ -206,7 +242,7 @@ const FilterInput = (props: {
|
|
|
206
242
|
);
|
|
207
243
|
Object.values(headerGroups).forEach((headerOptions) => {
|
|
208
244
|
headerOptions.forEach((o) => {
|
|
209
|
-
if (o.
|
|
245
|
+
if (o.label !== undefined) o.checked = anyChecked;
|
|
210
246
|
});
|
|
211
247
|
});
|
|
212
248
|
}
|
|
@@ -216,14 +252,26 @@ const FilterInput = (props: {
|
|
|
216
252
|
const addCustomFilterValue = useCallback(() => {
|
|
217
253
|
if (!options || !onSelectFilter) return;
|
|
218
254
|
|
|
255
|
+
const filterTrimmed = filter.trim();
|
|
256
|
+
if (isEmpty(filterTrimmed)) {
|
|
257
|
+
triggerSave().then();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
219
261
|
const preFilterOptions = JSON.stringify(options);
|
|
220
|
-
onSelectFilter(
|
|
262
|
+
onSelectFilter(filterTrimmed, options);
|
|
221
263
|
// Detect if options list changed and update
|
|
222
264
|
if (preFilterOptions === JSON.stringify(options)) return;
|
|
223
265
|
|
|
224
266
|
setOptions([...options]);
|
|
225
267
|
setFilter("");
|
|
226
|
-
}, [filter, onSelectFilter, options, setFilter, setOptions]);
|
|
268
|
+
}, [filter, onSelectFilter, options, setFilter, setOptions, triggerSave]);
|
|
269
|
+
|
|
270
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
271
|
+
if (e.key === "Enter") {
|
|
272
|
+
enterHasBeenPressed.current = true;
|
|
273
|
+
}
|
|
274
|
+
}, []);
|
|
227
275
|
|
|
228
276
|
const handleKeyUp = useCallback(
|
|
229
277
|
(e: KeyboardEvent) => {
|
|
@@ -232,10 +280,23 @@ const FilterInput = (props: {
|
|
|
232
280
|
e.preventDefault();
|
|
233
281
|
|
|
234
282
|
if (e.ctrlKey) toggleSelectAllVisible();
|
|
235
|
-
else if (
|
|
283
|
+
else if (enterHasBeenPressed.current) {
|
|
284
|
+
const filterTrimmed = filter.trim();
|
|
285
|
+
if (isEmpty(filterTrimmed)) {
|
|
286
|
+
triggerSave().then();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
onSelectFilter && addCustomFilterValue();
|
|
290
|
+
}
|
|
291
|
+
lastKeyWasEnter.current = true;
|
|
292
|
+
} else if (e.key === "Control") {
|
|
293
|
+
lastKeyWasEnter.current && setFilter("");
|
|
294
|
+
lastKeyWasEnter.current = false;
|
|
295
|
+
} else {
|
|
296
|
+
lastKeyWasEnter.current = false;
|
|
236
297
|
}
|
|
237
298
|
},
|
|
238
|
-
[addCustomFilterValue, onSelectFilter, toggleSelectAllVisible],
|
|
299
|
+
[addCustomFilterValue, filter, onSelectFilter, setFilter, toggleSelectAllVisible, triggerSave],
|
|
239
300
|
);
|
|
240
301
|
|
|
241
302
|
return (
|
|
@@ -252,6 +313,7 @@ const FilterInput = (props: {
|
|
|
252
313
|
data-disableenterautosave={true}
|
|
253
314
|
data-allowtabtosave={true}
|
|
254
315
|
onChange={(e) => setFilter(e.target.value)}
|
|
316
|
+
onKeyDown={handleKeyDown}
|
|
255
317
|
onKeyUp={handleKeyUp}
|
|
256
318
|
/>
|
|
257
319
|
{filterHelpText && (
|
|
@@ -267,7 +329,7 @@ const FilterInput = (props: {
|
|
|
267
329
|
</FocusableItem>
|
|
268
330
|
<MenuDivider key={`$$divider_filter`} />
|
|
269
331
|
{headerGroups && !toPairs(headerGroups).some(([_, options]) => !isEmpty(options)) && (
|
|
270
|
-
<div className={"szh-menu__item"}>
|
|
332
|
+
<div className={"szh-menu__item GridPopoverEditDropDown-noOptions"}>No Options</div>
|
|
271
333
|
)}
|
|
272
334
|
</>
|
|
273
335
|
);
|
|
@@ -11,8 +11,8 @@ export const GridPopoverEditDropDown = <RowType extends GridBaseRow>(
|
|
|
11
11
|
editor: GridFormDropDown,
|
|
12
12
|
...props,
|
|
13
13
|
editorParams: {
|
|
14
|
-
// Defaults to
|
|
15
|
-
className: "GridPopoverEditDropDown-
|
|
14
|
+
// Defaults to large size container
|
|
15
|
+
className: "GridPopoverEditDropDown-containerLarge",
|
|
16
16
|
...(props.editorParams as GridFormPopoutDropDownProps<RowType>),
|
|
17
17
|
},
|
|
18
18
|
});
|
|
@@ -8,7 +8,7 @@ import { GridUpdatingContextProvider } from "../../contexts/GridUpdatingContextP
|
|
|
8
8
|
import { GridContextProvider } from "../../contexts/GridContextProvider";
|
|
9
9
|
import { Grid, GridProps } from "../../components/Grid";
|
|
10
10
|
import { useMemo, useState } from "react";
|
|
11
|
-
import {
|
|
11
|
+
import { MenuSeparator } from "../../components/gridForm/GridFormDropDown";
|
|
12
12
|
import { GridFormSubComponentTextArea } from "../../components/gridForm/GridFormSubComponentTextArea";
|
|
13
13
|
import { ColDefT, GridCell } from "../../components/GridCell";
|
|
14
14
|
import { GridPopoutEditMultiSelect } from "../../components/gridPopoverEdit/GridPopoutEditMultiSelect";
|
|
@@ -85,8 +85,8 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
|
|
|
85
85
|
filtered: true,
|
|
86
86
|
filterPlaceholder: "Filter position",
|
|
87
87
|
className: "GridMultiSelect-containerUnlimited",
|
|
88
|
+
headers: [{ header: "Header item" }],
|
|
88
89
|
options: [
|
|
89
|
-
MenuHeaderItem("Header item"),
|
|
90
90
|
{ value: "lot1", label: "Lot 1" },
|
|
91
91
|
{ value: "lot2", label: "Lot 2" },
|
|
92
92
|
{ value: "lot3", label: "Lot 3" },
|
package/src/utils/textMatcher.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isEmpty } from "lodash-es";
|
|
1
|
+
import { isEmpty, partition } from "lodash-es";
|
|
2
2
|
import { isMatch } from "matcher";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -10,8 +10,8 @@ import { isMatch } from "matcher";
|
|
|
10
10
|
* "*L" => *L
|
|
11
11
|
* "A B" => A* and B*
|
|
12
12
|
* "A B, C" => (A* and B*) or C*
|
|
13
|
-
*
|
|
14
|
-
* Returns
|
|
13
|
+
* "!A" => all values must not match A
|
|
14
|
+
* Returns true if there's a text match.
|
|
15
15
|
*/
|
|
16
16
|
export const textMatch = (text: string | undefined | null, filter: string): boolean => {
|
|
17
17
|
if (text == null) return true;
|
|
@@ -20,12 +20,15 @@ export const textMatch = (text: string | undefined | null, filter: string): bool
|
|
|
20
20
|
.split(",")
|
|
21
21
|
.map((sf) => sf.trim())
|
|
22
22
|
.filter((sf) => sf);
|
|
23
|
+
const [negativeFilters, positiveFilters] = partition(superFilters, (superFilters) => superFilters.startsWith("!"));
|
|
23
24
|
const values = text.replaceAll(",", " ").trim().split(/\s+/);
|
|
24
25
|
return (
|
|
25
|
-
isEmpty(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
(isEmpty(positiveFilters) || // Not filtered
|
|
27
|
+
positiveFilters.some((superFilter) => {
|
|
28
|
+
const subFilters = superFilter.split(/\s+/).map((s) => s.replaceAll(/([!?])/g, "\\$1"));
|
|
29
|
+
return subFilters.every((subFilter) => values.some((value) => isMatch(value, subFilter)));
|
|
30
|
+
})) &&
|
|
31
|
+
(isEmpty(negativeFilters) ||
|
|
32
|
+
negativeFilters.every((superFilter) => values.every((value) => isMatch(value, superFilter))))
|
|
30
33
|
);
|
|
31
34
|
};
|