@linzjs/step-ag-grid 7.1.0 → 7.2.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.js +89 -36
- package/dist/index.js.map +1 -1
- package/dist/src/components/Grid.d.ts +2 -0
- package/dist/src/components/GridCellMultiSelectClassRules.d.ts +2 -0
- package/dist/src/components/gridForm/GridFormMultiSelect.d.ts +1 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/utils/textMatcher.d.ts +2 -2
- package/dist/step-ag-grid.esm.js +90 -37
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Grid.tsx +5 -0
- package/src/components/GridCell.tsx +2 -2
- package/src/components/GridCellMultiSelectClassRules.tsx +9 -0
- package/src/components/gridForm/GridFormMultiSelect.tsx +91 -22
- package/src/components/gridPopoverEdit/GridPopoverEditDropDown.ts +2 -2
- package/src/components/gridPopoverEdit/GridPopoverMenu.tsx +1 -3
- package/src/index.ts +1 -1
- package/src/stories/grid/GridPopoutEditMultiSelect.stories.tsx +3 -3
- package/src/utils/textMatcher.ts +11 -8
- package/dist/src/components/GenericCellClass.d.ts +0 -2
- package/src/components/GenericCellClass.tsx +0 -15
package/package.json
CHANGED
package/src/components/Grid.tsx
CHANGED
|
@@ -29,6 +29,8 @@ export interface GridProps {
|
|
|
29
29
|
rowData: GridOptions["rowData"];
|
|
30
30
|
noRowsOverlayText?: string;
|
|
31
31
|
postSortRows?: GridOptions["postSortRows"];
|
|
32
|
+
animateRows?: boolean;
|
|
33
|
+
rowClassRules?: GridOptions["rowClassRules"];
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
@@ -246,6 +248,9 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
246
248
|
</div>
|
|
247
249
|
)}
|
|
248
250
|
<AgGridReact
|
|
251
|
+
animateRows={params.animateRows}
|
|
252
|
+
rowClassRules={params.rowClassRules}
|
|
253
|
+
defaultColDef={params.defaultColDef}
|
|
249
254
|
getRowId={(params) => `${params.data.id}`}
|
|
250
255
|
suppressRowClickSelection={true}
|
|
251
256
|
rowSelection={"multiple"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { forwardRef, useContext } from "react";
|
|
2
2
|
import { GridBaseRow } from "./Grid";
|
|
3
3
|
import { GridUpdatingContext } from "../contexts/GridUpdatingContext";
|
|
4
|
-
import {
|
|
4
|
+
import { GridCellMultiSelectClassRules } from "./GridCellMultiSelectClassRules";
|
|
5
5
|
import {
|
|
6
6
|
GenericCellColDef,
|
|
7
7
|
GenericCellRendererParams,
|
|
@@ -64,7 +64,7 @@ export const GridCell = <RowType extends GridBaseRow, Props extends CellEditorCo
|
|
|
64
64
|
sortable: !!(props?.field || props?.valueGetter),
|
|
65
65
|
resizable: true,
|
|
66
66
|
...(custom?.editor && {
|
|
67
|
-
|
|
67
|
+
cellClassRules: GridCellMultiSelectClassRules,
|
|
68
68
|
editable: props.editable ?? true,
|
|
69
69
|
cellEditor: GenericCellEditorComponentWrapper(custom),
|
|
70
70
|
}),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CellClassParams, CellClassRules } from "ag-grid-community/dist/lib/entities/colDef";
|
|
2
|
+
|
|
3
|
+
export const GridCellMultiSelectClassRules: CellClassRules = {
|
|
4
|
+
"ag-selected-for-edit": ({ api, node, colDef }: CellClassParams) =>
|
|
5
|
+
api
|
|
6
|
+
.getSelectedNodes()
|
|
7
|
+
.map((row) => row.id)
|
|
8
|
+
.includes(node.id) && api.getEditingCells().some((cell) => cell.column.getColDef() === colDef),
|
|
9
|
+
};
|
|
@@ -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";
|
|
@@ -24,6 +24,9 @@ import { GridSubComponentContext } from "contexts/GridSubComponentContext";
|
|
|
24
24
|
import { useGridPopoverContext } from "../../contexts/GridPopoverContext";
|
|
25
25
|
import { FormError } from "../../lui/FormError";
|
|
26
26
|
import { textMatch } from "../../utils/textMatcher";
|
|
27
|
+
import { GridIcon } from "../GridIcon";
|
|
28
|
+
|
|
29
|
+
type HeaderGroupType = Record<string, MultiSelectOption[]> | undefined;
|
|
27
30
|
|
|
28
31
|
export interface MultiSelectOption {
|
|
29
32
|
value: any;
|
|
@@ -32,6 +35,7 @@ export interface MultiSelectOption {
|
|
|
32
35
|
subValue?: any;
|
|
33
36
|
filter?: string;
|
|
34
37
|
checked?: boolean;
|
|
38
|
+
warning?: string | undefined;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export interface GridFormMultiSelectGroup {
|
|
@@ -112,15 +116,34 @@ export const GridFormMultiSelect = <RowType extends GridBaseRow>(props: GridForm
|
|
|
112
116
|
/**
|
|
113
117
|
* Groups options into their header groups
|
|
114
118
|
*/
|
|
115
|
-
const headerGroups = useMemo(
|
|
116
|
-
()
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
const headerGroups = useMemo(() => {
|
|
120
|
+
if (!options) return undefined;
|
|
121
|
+
const result = groupBy(
|
|
122
|
+
options.filter((o) => textMatch(o.label, filter) && o.value),
|
|
123
|
+
"filter",
|
|
124
|
+
);
|
|
125
|
+
// remove leading/trailing/duplicate dividers
|
|
126
|
+
return fromPairs(
|
|
127
|
+
toPairs(result).map(([key, arr]) => {
|
|
128
|
+
let lastWasDivider = true;
|
|
129
|
+
return [
|
|
130
|
+
key,
|
|
131
|
+
arr
|
|
132
|
+
.map((row, index) => {
|
|
133
|
+
if (row.value === MenuSeparatorString) {
|
|
134
|
+
if (lastWasDivider) return null;
|
|
135
|
+
if (index === arr.length - 1) return null;
|
|
136
|
+
lastWasDivider = true;
|
|
137
|
+
} else {
|
|
138
|
+
lastWasDivider = false;
|
|
139
|
+
}
|
|
140
|
+
return row;
|
|
141
|
+
})
|
|
142
|
+
.filter((r) => r),
|
|
143
|
+
];
|
|
144
|
+
}),
|
|
145
|
+
) as HeaderGroupType;
|
|
146
|
+
}, [filter, options]);
|
|
124
147
|
|
|
125
148
|
const headers: GridFormMultiSelectGroup[] = useMemo(() => props.headers ?? [{ header: "" }], [props.headers]);
|
|
126
149
|
|
|
@@ -136,7 +159,7 @@ export const GridFormMultiSelect = <RowType extends GridBaseRow>(props: GridForm
|
|
|
136
159
|
<>
|
|
137
160
|
{props.filtered && (
|
|
138
161
|
<FilterInput
|
|
139
|
-
{...{ headerGroups, options, setOptions, filter, setFilter }}
|
|
162
|
+
{...{ headerGroups, options, setOptions, filter, setFilter, triggerSave }}
|
|
140
163
|
filterHelpText={props.filterHelpText}
|
|
141
164
|
onSelectFilter={props.onSelectFilter}
|
|
142
165
|
filterPlaceholder={props.filterPlaceholder}
|
|
@@ -185,12 +208,25 @@ const FilterInput = (props: {
|
|
|
185
208
|
onSelectFilter?: (filter: string, options: MultiSelectOption[]) => void;
|
|
186
209
|
filter: string;
|
|
187
210
|
setFilter: Dispatch<SetStateAction<string>>;
|
|
188
|
-
headerGroups:
|
|
211
|
+
headerGroups: HeaderGroupType;
|
|
189
212
|
filterPlaceholder?: string;
|
|
190
213
|
filterHelpText?: string | ((filter: string, options: MultiSelectOption[]) => string | undefined);
|
|
214
|
+
triggerSave: () => Promise<void>;
|
|
191
215
|
}) => {
|
|
192
|
-
const {
|
|
193
|
-
|
|
216
|
+
const {
|
|
217
|
+
options,
|
|
218
|
+
setOptions,
|
|
219
|
+
onSelectFilter,
|
|
220
|
+
filter,
|
|
221
|
+
setFilter,
|
|
222
|
+
headerGroups,
|
|
223
|
+
filterPlaceholder,
|
|
224
|
+
filterHelpText,
|
|
225
|
+
triggerSave,
|
|
226
|
+
} = props;
|
|
227
|
+
|
|
228
|
+
const enterHasBeenPressed = useRef(false);
|
|
229
|
+
const lastKeyWasEnter = useRef(false);
|
|
194
230
|
|
|
195
231
|
const toggleSelectAllVisible = useCallback(() => {
|
|
196
232
|
if (!options || !headerGroups) return;
|
|
@@ -198,7 +234,9 @@ const FilterInput = (props: {
|
|
|
198
234
|
if (isEmpty(filter.trim())) {
|
|
199
235
|
// Toggle off if any items are checked otherwise on
|
|
200
236
|
const anyChecked = options.some((o) => o.checked);
|
|
201
|
-
options.forEach((o) =>
|
|
237
|
+
options.forEach((o) => {
|
|
238
|
+
if (o.label !== undefined) o.checked = !anyChecked;
|
|
239
|
+
});
|
|
202
240
|
} else {
|
|
203
241
|
// Toggle on if any filtered items are checked otherwise off
|
|
204
242
|
const anyChecked = Object.values(headerGroups).some((headerOptions) =>
|
|
@@ -206,7 +244,7 @@ const FilterInput = (props: {
|
|
|
206
244
|
);
|
|
207
245
|
Object.values(headerGroups).forEach((headerOptions) => {
|
|
208
246
|
headerOptions.forEach((o) => {
|
|
209
|
-
if (o.
|
|
247
|
+
if (o.label !== undefined) o.checked = anyChecked;
|
|
210
248
|
});
|
|
211
249
|
});
|
|
212
250
|
}
|
|
@@ -216,14 +254,26 @@ const FilterInput = (props: {
|
|
|
216
254
|
const addCustomFilterValue = useCallback(() => {
|
|
217
255
|
if (!options || !onSelectFilter) return;
|
|
218
256
|
|
|
257
|
+
const filterTrimmed = filter.trim();
|
|
258
|
+
if (isEmpty(filterTrimmed)) {
|
|
259
|
+
triggerSave().then();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
219
263
|
const preFilterOptions = JSON.stringify(options);
|
|
220
|
-
onSelectFilter(
|
|
264
|
+
onSelectFilter(filterTrimmed, options);
|
|
221
265
|
// Detect if options list changed and update
|
|
222
266
|
if (preFilterOptions === JSON.stringify(options)) return;
|
|
223
267
|
|
|
224
268
|
setOptions([...options]);
|
|
225
269
|
setFilter("");
|
|
226
|
-
}, [filter, onSelectFilter, options, setFilter, setOptions]);
|
|
270
|
+
}, [filter, onSelectFilter, options, setFilter, setOptions, triggerSave]);
|
|
271
|
+
|
|
272
|
+
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
|
273
|
+
if (e.key === "Enter") {
|
|
274
|
+
enterHasBeenPressed.current = true;
|
|
275
|
+
}
|
|
276
|
+
}, []);
|
|
227
277
|
|
|
228
278
|
const handleKeyUp = useCallback(
|
|
229
279
|
(e: KeyboardEvent) => {
|
|
@@ -232,10 +282,23 @@ const FilterInput = (props: {
|
|
|
232
282
|
e.preventDefault();
|
|
233
283
|
|
|
234
284
|
if (e.ctrlKey) toggleSelectAllVisible();
|
|
235
|
-
else if (
|
|
285
|
+
else if (enterHasBeenPressed.current) {
|
|
286
|
+
const filterTrimmed = filter.trim();
|
|
287
|
+
if (isEmpty(filterTrimmed)) {
|
|
288
|
+
triggerSave().then();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
onSelectFilter && addCustomFilterValue();
|
|
292
|
+
}
|
|
293
|
+
lastKeyWasEnter.current = true;
|
|
294
|
+
} else if (e.key === "Control") {
|
|
295
|
+
lastKeyWasEnter.current && setFilter("");
|
|
296
|
+
lastKeyWasEnter.current = false;
|
|
297
|
+
} else {
|
|
298
|
+
lastKeyWasEnter.current = false;
|
|
236
299
|
}
|
|
237
300
|
},
|
|
238
|
-
[addCustomFilterValue, onSelectFilter, toggleSelectAllVisible],
|
|
301
|
+
[addCustomFilterValue, filter, onSelectFilter, setFilter, toggleSelectAllVisible, triggerSave],
|
|
239
302
|
);
|
|
240
303
|
|
|
241
304
|
return (
|
|
@@ -252,6 +315,7 @@ const FilterInput = (props: {
|
|
|
252
315
|
data-disableenterautosave={true}
|
|
253
316
|
data-allowtabtosave={true}
|
|
254
317
|
onChange={(e) => setFilter(e.target.value)}
|
|
318
|
+
onKeyDown={handleKeyDown}
|
|
255
319
|
onKeyUp={handleKeyUp}
|
|
256
320
|
/>
|
|
257
321
|
{filterHelpText && (
|
|
@@ -267,7 +331,7 @@ const FilterInput = (props: {
|
|
|
267
331
|
</FocusableItem>
|
|
268
332
|
<MenuDivider key={`$$divider_filter`} />
|
|
269
333
|
{headerGroups && !toPairs(headerGroups).some(([_, options]) => !isEmpty(options)) && (
|
|
270
|
-
<div className={"szh-menu__item"}>
|
|
334
|
+
<div className={"szh-menu__item GridPopoverEditDropDown-noOptions"}>No Options</div>
|
|
271
335
|
)}
|
|
272
336
|
</>
|
|
273
337
|
);
|
|
@@ -301,7 +365,12 @@ const MenuRadioItem = (props: {
|
|
|
301
365
|
<LuiCheckboxInput
|
|
302
366
|
isChecked={item.checked ?? false}
|
|
303
367
|
value={`${item.value}`}
|
|
304
|
-
label={
|
|
368
|
+
label={
|
|
369
|
+
<>
|
|
370
|
+
{item.warning && <GridIcon icon={"ic_warning"} title={item.warning} />}
|
|
371
|
+
{item.label ?? (item.value == null ? `<${item.value}>` : `${item.value}`)}
|
|
372
|
+
</>
|
|
373
|
+
}
|
|
305
374
|
inputProps={{
|
|
306
375
|
onClick: (e) => {
|
|
307
376
|
// Click is handled by MenuItem onClick
|
|
@@ -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
|
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import "./GridPopoverMenu.scss";
|
|
2
2
|
|
|
3
|
-
import { GenericMultiEditCellClass } from "../GenericCellClass";
|
|
4
3
|
import { GridBaseRow } from "../Grid";
|
|
5
4
|
import { ColDefT, GenericCellEditorProps, GridCell } from "../GridCell";
|
|
6
5
|
import { GridFormPopoverMenu, GridFormPopoutMenuProps } from "../gridForm/GridFormPopoverMenu";
|
|
@@ -18,9 +17,8 @@ export const GridPopoverMenu = <RowType extends GridBaseRow>(
|
|
|
18
17
|
{
|
|
19
18
|
maxWidth: 40,
|
|
20
19
|
editable: colDef.editable != null ? colDef.editable : true,
|
|
21
|
-
cellStyle: { justifyContent: "
|
|
20
|
+
cellStyle: { justifyContent: "center" },
|
|
22
21
|
cellRenderer: GridRenderPopoutMenuCell,
|
|
23
|
-
cellClass: custom?.multiEdit ? GenericMultiEditCellClass : undefined,
|
|
24
22
|
...colDef,
|
|
25
23
|
cellRendererParams: {
|
|
26
24
|
// Menus open on single click, this parameter is picked up in Grid.tsx
|
package/src/index.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { Grid } from "./components/Grid";
|
|
|
14
14
|
export * from "./components/GridCell";
|
|
15
15
|
export { GridIcon } from "./components/GridIcon";
|
|
16
16
|
export { ComponentLoadingWrapper } from "./components/ComponentLoadingWrapper";
|
|
17
|
-
export {
|
|
17
|
+
export { GridCellMultiSelectClassRules } from "./components/GridCellMultiSelectClassRules";
|
|
18
18
|
export { GridLoadableCell } from "./components/GridLoadableCell";
|
|
19
19
|
export { useGridPopoverHook } from "./components/GridPopoverHook";
|
|
20
20
|
export { usePostSortRowsHook } from "./components/PostSortRowsHook";
|
|
@@ -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" },
|
|
@@ -145,7 +145,7 @@ const GridEditMultiSelectTemplate: ComponentStory<typeof Grid> = (props: GridPro
|
|
|
145
145
|
const firstRow = selectedRows[0];
|
|
146
146
|
const r: MultiSelectOption[] = [
|
|
147
147
|
{ value: "lot1", label: "Lot 1" },
|
|
148
|
-
{ value: "lot2", label: "Lot 2" },
|
|
148
|
+
{ value: "lot2", label: "Lot 2", warning: "Don't select me" },
|
|
149
149
|
{ value: "lot3", label: "Lot 3" },
|
|
150
150
|
{ value: "lot11", label: "Lot 11" },
|
|
151
151
|
{ value: "lot4", label: "Lot A 482392" },
|
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
|
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { CellClassFunc } from "ag-grid-community";
|
|
2
|
-
|
|
3
|
-
export const GenericMultiEditCellClass: CellClassFunc = (props): string => {
|
|
4
|
-
const api = props.api;
|
|
5
|
-
if (api == null) return "";
|
|
6
|
-
|
|
7
|
-
const rowSelected = api
|
|
8
|
-
.getSelectedNodes()
|
|
9
|
-
.map((row) => row.id)
|
|
10
|
-
.includes(props.node.id);
|
|
11
|
-
|
|
12
|
-
return rowSelected && api.getEditingCells().some((cell) => cell.column.getColDef() === props.colDef)
|
|
13
|
-
? "ag-selected-for-edit"
|
|
14
|
-
: "";
|
|
15
|
-
};
|