@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/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": "7.1.0",
5
+ "version": "7.2.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -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 { GenericMultiEditCellClass } from "./GenericCellClass";
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
- cellClass: custom?.multiEdit ? GenericMultiEditCellClass : undefined,
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
- options &&
118
- groupBy(
119
- options.filter((o) => textMatch(o.label, filter) && o.value),
120
- "filter",
121
- ),
122
- [filter, options],
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: Record<string, MultiSelectOption[]> | undefined;
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 { options, setOptions, onSelectFilter, filter, setFilter, headerGroups, filterPlaceholder, filterHelpText } =
193
- props;
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) => (o.checked = !anyChecked));
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.checked !== undefined) o.checked = anyChecked;
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(filter.trim(), options);
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 (onSelectFilter) addCustomFilterValue();
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"}>[No items match the filter]</div>
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={item.label ?? (item.value == null ? `<${item.value}>` : `${item.value}`)}
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 medium size container
15
- className: "GridPopoverEditDropDown-containerMedium",
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: "flex-end" },
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 { GenericMultiEditCellClass } from "./components/GenericCellClass";
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 { MenuHeaderItem, MenuSeparator } from "../../components/gridForm/GridFormDropDown";
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" },
@@ -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 ture if there's a text match.
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(superFilters) || // Not filtered
26
- superFilters.some((superFilter) => {
27
- const subFilters = superFilter.split(/\s+/).map((s) => s.replaceAll(/([!?])/g, "\\$1"));
28
- return subFilters.every((subFilter) => values.some((value) => isMatch(value, subFilter)));
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,2 +0,0 @@
1
- import { CellClassFunc } from "ag-grid-community";
2
- export declare const GenericMultiEditCellClass: CellClassFunc;
@@ -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
- };