@linzjs/step-ag-grid 14.1.1 → 14.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": "14.1.1",
5
+ "version": "14.2.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -1,10 +1,15 @@
1
- import { CellClickedEvent, ColDef, ModelUpdatedEvent } from "ag-grid-community";
1
+ import { CellClickedEvent, ColDef, ColumnResizedEvent, ModelUpdatedEvent } from "ag-grid-community";
2
2
  import { CellClassParams, EditableCallback, EditableCallbackParams } from "ag-grid-community/dist/lib/entities/colDef";
3
3
  import { GridOptions } from "ag-grid-community/dist/lib/entities/gridOptions";
4
- import { CellEvent, GridReadyEvent, SelectionChangedEvent } from "ag-grid-community/dist/lib/events";
4
+ import {
5
+ CellEditingStoppedEvent,
6
+ CellEvent,
7
+ GridReadyEvent,
8
+ SelectionChangedEvent,
9
+ } from "ag-grid-community/dist/lib/events";
5
10
  import { AgGridReact } from "ag-grid-react";
6
11
  import clsx from "clsx";
7
- import { difference, isEmpty, last, xorBy } from "lodash-es";
12
+ import { difference, isEmpty, last, omit, xorBy } from "lodash-es";
8
13
  import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
9
14
 
10
15
  import { GridContext } from "../contexts/GridContext";
@@ -56,6 +61,11 @@ export interface GridProps {
56
61
  * If you want to stretch to container width if width is greater than the container add a flex column.
57
62
  */
58
63
  sizeColumns?: "fit" | "auto" | "auto-skip-headers" | "none";
64
+ /**
65
+ * When pressing tab whilst editing the grid will select and edit the next cell if available.
66
+ * Once the last cell to edit closes this callback is called.
67
+ */
68
+ onCellEditingComplete?: () => void;
59
69
  }
60
70
 
61
71
  /**
@@ -78,34 +88,59 @@ export const Grid = ({
78
88
  selectRowsById,
79
89
  focusByRowById,
80
90
  ensureSelectedRowIsVisible,
81
- autoSizeAllColumns,
91
+ autoSizeColumns,
82
92
  sizeColumnsToFit,
83
93
  externallySelectedItemsAreInSync,
84
94
  setExternallySelectedItemsAreInSync,
85
95
  isExternalFilterPresent,
86
96
  doesExternalFilterPass,
97
+ setOnCellEditingComplete,
87
98
  } = useContext(GridContext);
88
- const { checkUpdating } = useContext(GridUpdatingContext);
99
+ const { checkUpdating, updatedDep, isUpdating } = useContext(GridUpdatingContext);
89
100
 
90
101
  const lastSelectedIds = useRef<number[]>([]);
91
102
  const [staleGrid, setStaleGrid] = useState(false);
92
103
  const postSortRows = usePostSortRowsHook({ setStaleGrid });
93
104
 
105
+ /**
106
+ * onContentSize should only be called at maximum twice.
107
+ * Once when an empty grid is loaded.
108
+ * And again when the grid has content.
109
+ */
110
+ const hasSetContentSize = useRef(false);
111
+ const hasSetContentSizeEmpty = useRef(false);
112
+
94
113
  const setInitialContentSize = useCallback(() => {
95
- const skipHeaders = sizeColumns === "auto-skip-headers";
96
- if (sizeColumns === "auto" || skipHeaders) {
97
- // If we aren't skipping headers and there's no data, then don't skip headers
98
- const result = autoSizeAllColumns({ skipHeader: skipHeaders && !isEmpty(params.rowData) });
99
- params.onContentSize && result && params.onContentSize(result);
114
+ const skipHeader = sizeColumns === "auto-skip-headers";
115
+ if (sizeColumns === "auto" || skipHeader) {
116
+ const result = autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current });
117
+ if (isEmpty(params.rowData)) {
118
+ if (!hasSetContentSizeEmpty.current && !hasSetContentSize.current) {
119
+ hasSetContentSizeEmpty.current = true;
120
+ params.onContentSize && result && params.onContentSize(result);
121
+ }
122
+ } else {
123
+ if (!hasSetContentSize.current) {
124
+ hasSetContentSize.current = true;
125
+ params.onContentSize && result && params.onContentSize(result);
126
+ }
127
+ }
100
128
  }
101
129
 
102
130
  if (sizeColumns !== "none") {
103
131
  sizeColumnsToFit();
104
132
  }
105
- }, [autoSizeAllColumns, params, sizeColumns, sizeColumnsToFit]);
133
+ }, [autoSizeColumns, params, sizeColumns, sizeColumnsToFit]);
106
134
 
135
+ /**
136
+ * When the grid becomes ready resize it
137
+ */
138
+ const previousGridReady = useRef(gridReady);
107
139
  useEffect(() => {
108
- gridReady && setInitialContentSize();
140
+ if (!previousGridReady.current && gridReady) {
141
+ previousGridReady.current = true;
142
+ setInitialContentSize();
143
+ }
109
144
  }, [gridReady, setInitialContentSize]);
110
145
 
111
146
  /**
@@ -182,12 +217,17 @@ export const Grid = ({
182
217
 
183
218
  const selectedIds = params.externalSelectedItems.map((row) => row.id) as number[];
184
219
  const lastNewId = last(difference(selectedIds, lastSelectedIds.current));
185
- if (lastNewId != null) ensureRowVisible(lastNewId);
220
+ if (lastNewId != null) {
221
+ ensureRowVisible(lastNewId);
222
+ }
186
223
  lastSelectedIds.current = selectedIds;
187
224
  selectRowsById(selectedIds);
188
225
  setExternallySelectedItemsAreInSync(true);
189
226
  }, [gridReady, params.externalSelectedItems, ensureRowVisible, selectRowsById, setExternallySelectedItemsAreInSync]);
190
227
 
228
+ /**
229
+ * Combine grid and cell editable into one function
230
+ */
191
231
  const combineEditables =
192
232
  (...editables: (boolean | EditableCallback | undefined)[]) =>
193
233
  (params: EditableCallbackParams): boolean => {
@@ -201,10 +241,11 @@ export const Grid = ({
201
241
  /**
202
242
  * Synchronise externally selected items to grid on externalSelectedItems change
203
243
  */
204
- useEffect(() => {
205
- synchroniseExternallySelectedItemsToGrid();
206
- }, [synchroniseExternallySelectedItemsToGrid]);
244
+ useEffect(synchroniseExternallySelectedItemsToGrid, [synchroniseExternallySelectedItemsToGrid]);
207
245
 
246
+ /**
247
+ * Add selectable column to colDefs. Adjust column defs to block fit for auto sized columns.
248
+ */
208
249
  const columnDefs = useMemo((): ColDef[] => {
209
250
  const adjustColDefs = params.columnDefs.map((colDef) => {
210
251
  const colDefEditable = colDef.editable;
@@ -255,6 +296,9 @@ export const Grid = ({
255
296
  clickSelectorCheckboxWhenContainingCellClicked,
256
297
  ]);
257
298
 
299
+ /**
300
+ * When grid is ready set the apis to the grid context and sync selected items to grid.
301
+ */
258
302
  const onGridReady = useCallback(
259
303
  (event: GridReadyEvent) => {
260
304
  setApis(event.api, event.columnApi, dataTestId);
@@ -268,28 +312,56 @@ export const Grid = ({
268
312
  * This will resize columns when we have at least one row.
269
313
  */
270
314
  const previousRowDataLength = useRef(0);
271
- useEffect(() => {
315
+
316
+ const onRowDataChanged = useCallback(() => {
272
317
  const length = params.rowData?.length ?? 0;
273
318
  if (previousRowDataLength.current !== length) {
274
- if (previousRowDataLength.current === 0 && length > 0) {
275
- setInitialContentSize();
276
- }
319
+ setInitialContentSize();
277
320
  previousRowDataLength.current = length;
278
321
  }
279
- }, [params.rowData?.length, setInitialContentSize]);
280
322
 
323
+ if (lastUpdatedDep.current === updatedDep || isEmpty(colIdsEdited.current)) return;
324
+ lastUpdatedDep.current = updatedDep;
325
+
326
+ // Don't update while there are spinners
327
+ if (isUpdating()) return;
328
+
329
+ const skipHeader = sizeColumns === "auto-skip-headers";
330
+ autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current, colIds: colIdsEdited.current });
331
+ colIdsEdited.current.clear();
332
+ }, [autoSizeColumns, isUpdating, params.rowData?.length, setInitialContentSize, sizeColumns, updatedDep]);
333
+
334
+ /**
335
+ * Show/hide no rows overlay when model changes.
336
+ */
337
+ const isShowingNoRowsOverlay = useRef(false);
281
338
  const onModelUpdated = useCallback((event: ModelUpdatedEvent) => {
282
- event.api.getDisplayedRowCount() === 0 ? event.api.showNoRowsOverlay() : event.api.hideOverlay();
339
+ if (event.api.getDisplayedRowCount() === 0) {
340
+ if (!isShowingNoRowsOverlay.current) {
341
+ event.api.showNoRowsOverlay();
342
+ isShowingNoRowsOverlay.current = true;
343
+ }
344
+ } else {
345
+ if (isShowingNoRowsOverlay.current) {
346
+ event.api.hideOverlay();
347
+ isShowingNoRowsOverlay.current = false;
348
+ }
349
+ }
283
350
  }, []);
284
351
 
352
+ /**
353
+ * Force-refresh all selected rows to re-run class function, to update selection highlighting
354
+ */
285
355
  const refreshSelectedRows = useCallback((event: CellEvent): void => {
286
- // Force-refresh all selected rows to re-run class function, to update selection highlighting
287
356
  event.api.refreshCells({
288
357
  force: true,
289
358
  rowNodes: event.api.getSelectedNodes(),
290
359
  });
291
360
  }, []);
292
361
 
362
+ /**
363
+ * Make sure node is selected for editing and start edit
364
+ */
293
365
  const startCellEditing = useCallback(
294
366
  (event: CellEvent) => {
295
367
  prePopupOps();
@@ -311,6 +383,9 @@ export const Grid = ({
311
383
  [checkUpdating, prePopupOps],
312
384
  );
313
385
 
386
+ /**
387
+ * Handle double click edit
388
+ */
314
389
  const onCellDoubleClick = useCallback(
315
390
  (event: CellEvent) => {
316
391
  if (!invokeEditAction(event)) startCellEditing(event);
@@ -318,6 +393,9 @@ export const Grid = ({
318
393
  [startCellEditing],
319
394
  );
320
395
 
396
+ /**
397
+ * Handle single click edits
398
+ */
321
399
  const onCellClicked = useCallback(
322
400
  (event: CellEvent) => {
323
401
  if (event.colDef?.cellRendererParams?.singleClickEdit) {
@@ -327,6 +405,9 @@ export const Grid = ({
327
405
  [startCellEditing],
328
406
  );
329
407
 
408
+ /**
409
+ * If cell has an edit action invoke it (if editable)
410
+ */
330
411
  const invokeEditAction = (e: CellEvent): boolean => {
331
412
  const editAction = e.colDef?.cellRendererParams?.editAction;
332
413
  if (!editAction) return false;
@@ -341,6 +422,9 @@ export const Grid = ({
341
422
  return true;
342
423
  };
343
424
 
425
+ /**
426
+ * Start editing on pressing Enter
427
+ */
344
428
  const onCellKeyPress = useCallback(
345
429
  (e: CellEvent) => {
346
430
  if ((e.event as KeyboardEvent).key === "Enter") {
@@ -350,10 +434,6 @@ export const Grid = ({
350
434
  [startCellEditing],
351
435
  );
352
436
 
353
- const onGridSizeChanged = useCallback(() => {
354
- sizeColumns !== "none" && sizeColumnsToFit();
355
- }, [sizeColumns, sizeColumnsToFit]);
356
-
357
437
  /**
358
438
  * Once the grid has auto-sized we want to run fit to fit the grid in its container,
359
439
  * but we don't want the non-flex auto-sized columns to "fit" size, so suppressSizeToFit is set to true.
@@ -367,6 +447,80 @@ export const Grid = ({
367
447
  [columnDefs, sizeColumns],
368
448
  );
369
449
 
450
+ /**
451
+ * Set of colIds that need auto-sizing.
452
+ */
453
+ const colIdsEdited = useRef(new Set<string>());
454
+
455
+ /**
456
+ * When cell editing has completed tag the colId as needing auto-sizing
457
+ */
458
+ const onCellEditingStopped = useCallback(
459
+ (e: CellEditingStoppedEvent) => {
460
+ const skipHeader = sizeColumns === "auto-skip-headers";
461
+ if (sizeColumns === "auto" || skipHeader) {
462
+ // This may be wrong as the cell update hasn't completed?
463
+ const colId = e.colDef.colId;
464
+ if (colId && !e.colDef.flex) {
465
+ // This auto-sizes based on updatingContext completing
466
+ colIdsEdited.current.add(colId);
467
+ // This auto-sizes immediately in case it was an in place update
468
+ !isUpdating() && autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current, colIds: [colId] });
469
+ }
470
+ }
471
+ },
472
+ [autoSizeColumns, isUpdating, sizeColumns],
473
+ );
474
+
475
+ const lastUpdatedDep = useRef(updatedDep);
476
+
477
+ /**
478
+ * If columns are edited, wait for the updating context to complete then auto-size them.
479
+ */
480
+ useEffect(() => {
481
+ if (lastUpdatedDep.current === updatedDep || isEmpty(colIdsEdited.current)) return;
482
+ lastUpdatedDep.current = updatedDep;
483
+
484
+ // Don't update while there are spinners
485
+ if (isUpdating()) return;
486
+
487
+ const skipHeader = sizeColumns === "auto-skip-headers";
488
+ autoSizeColumns({ skipHeader, userSizedColIds: userSizedColIds.current, colIds: colIdsEdited.current });
489
+ colIdsEdited.current.clear();
490
+ }, [autoSizeColumns, updatedDep, sizeColumns, isUpdating]);
491
+
492
+ /**
493
+ * Resize columns to fit if required on window/container resize
494
+ */
495
+ const onGridSizeChanged = useCallback(() => {
496
+ if (sizeColumns !== "none") {
497
+ sizeColumnsToFit();
498
+ }
499
+ }, [sizeColumns, sizeColumnsToFit]);
500
+
501
+ /**
502
+ * Set of column Id's that are prevented from auto-sizing as they are user set
503
+ */
504
+ const userSizedColIds = useRef(new Set<string>());
505
+
506
+ /**
507
+ * Lock/unlock column width on user edit/reset.
508
+ */
509
+ const onColumnResized = useCallback((e: ColumnResizedEvent) => {
510
+ const colId = e.column?.getColId();
511
+ if (colId == null) return;
512
+ switch (e.source) {
513
+ case "uiColumnDragged":
514
+ userSizedColIds.current.add(colId);
515
+ break;
516
+ case "autosizeColumns":
517
+ userSizedColIds.current.delete(colId);
518
+ break;
519
+ }
520
+ }, []);
521
+
522
+ setOnCellEditingComplete(params.onCellEditingComplete);
523
+
370
524
  return (
371
525
  <div
372
526
  data-testid={dataTestId}
@@ -388,11 +542,15 @@ export const Grid = ({
388
542
  onGridSizeChanged={onGridSizeChanged}
389
543
  suppressColumnVirtualisation={suppressColumnVirtualization}
390
544
  suppressClickEdit={true}
545
+ onRowDataChanged={onRowDataChanged}
391
546
  onCellKeyPress={onCellKeyPress}
392
547
  onCellClicked={onCellClicked}
393
548
  onCellDoubleClicked={onCellDoubleClick}
394
549
  onCellEditingStarted={refreshSelectedRows}
395
550
  domLayout={params.domLayout}
551
+ onCellEditingStopped={onCellEditingStopped}
552
+ onColumnResized={onColumnResized}
553
+ defaultColDef={{ minWidth: 48, ...omit(params.defaultColDef, ["editable"]) }}
396
554
  columnDefs={columnDefsAdjusted}
397
555
  rowData={params.rowData}
398
556
  noRowsOverlayComponent={GridNoRowsOverlay}
@@ -105,6 +105,7 @@ export const GridCell = <RowType extends GridBaseRow, Props extends CellEditorCo
105
105
  props: GenericCellColDef<RowType>,
106
106
  custom?: {
107
107
  multiEdit?: boolean;
108
+ preventAutoEdit?: boolean;
108
109
  editor?: (editorProps: Props) => JSX.Element;
109
110
  editorParams?: Props;
110
111
  },
@@ -123,7 +124,6 @@ export const GridCell = <RowType extends GridBaseRow, Props extends CellEditorCo
123
124
  headerTooltip: props.headerName,
124
125
  sortable: !!(props?.field || props?.valueGetter),
125
126
  resizable: true,
126
- minWidth: props.flex ? 150 : 48,
127
127
  editable: props.editable ?? false,
128
128
  ...(custom?.editor && {
129
129
  cellClassRules: GridCellMultiSelectClassRules,
@@ -132,7 +132,11 @@ export const GridCell = <RowType extends GridBaseRow, Props extends CellEditorCo
132
132
  }),
133
133
  suppressKeyboardEvent: suppressCellKeyboardEvents,
134
134
  ...(custom?.editorParams && {
135
- cellEditorParams: { ...custom.editorParams, multiEdit: custom.multiEdit },
135
+ cellEditorParams: {
136
+ ...custom.editorParams,
137
+ multiEdit: custom.multiEdit,
138
+ preventAutoEdit: custom.preventAutoEdit ?? false,
139
+ },
136
140
  }),
137
141
  // If there's a valueFormatter and no filterValueGetter then create a filterValueGetter
138
142
  filterValueGetter,
@@ -21,7 +21,7 @@ export interface GridPopoverHookProps<RowType> {
21
21
  }
22
22
 
23
23
  export const useGridPopoverHook = <RowType extends GridBaseRow>(props: GridPopoverHookProps<RowType>) => {
24
- const { stopEditing } = useContext(GridContext);
24
+ const { stopEditing, cancelEdit } = useContext(GridContext);
25
25
  const { anchorRef, saving, updateValue } = useGridPopoverContext<RowType>();
26
26
  const saveButtonRef = useRef<HTMLButtonElement>(null);
27
27
  const [isOpen, setOpen] = useState(false);
@@ -33,7 +33,7 @@ export const useGridPopoverHook = <RowType extends GridBaseRow>(props: GridPopov
33
33
  const triggerSave = useCallback(
34
34
  async (reason?: string) => {
35
35
  if (reason == CloseReason.CANCEL) {
36
- stopEditing();
36
+ cancelEdit();
37
37
  return;
38
38
  }
39
39
  if (props.invalid && props.invalid()) {
@@ -41,7 +41,7 @@ export const useGridPopoverHook = <RowType extends GridBaseRow>(props: GridPopov
41
41
  }
42
42
 
43
43
  if (!props.save) {
44
- stopEditing();
44
+ cancelEdit();
45
45
  } else if (props.save) {
46
46
  // forms that don't provide an invalid fn must wait until they have saved to close
47
47
  if (props.invalid) stopEditing();
@@ -55,7 +55,7 @@ export const useGridPopoverHook = <RowType extends GridBaseRow>(props: GridPopov
55
55
  }
56
56
  }
57
57
  },
58
- [props, stopEditing, updateValue],
58
+ [cancelEdit, props, stopEditing, updateValue],
59
59
  );
60
60
 
61
61
  const popoverWrapper = useCallback(
@@ -115,14 +115,7 @@ export const GridFormDropDown = <RowType extends GridBaseRow>(props: GridFormDro
115
115
  : item,
116
116
  );
117
117
 
118
- if (props.filtered) {
119
- // This is needed otherwise when filter input is rendered and sets autofocus
120
- // the mouse up of the double click edit triggers the cell to cancel editing
121
- setOptions(optionsList);
122
- //delay(() => setOptions(optionsList), 100);
123
- } else {
124
- setOptions(optionsList);
125
- }
118
+ setOptions(optionsList);
126
119
  }
127
120
  })();
128
121
  }, [filter, options, props, selectedRows]);
@@ -28,6 +28,7 @@ export const GridPopoverMenu = <RowType extends GridBaseRow>(
28
28
  },
29
29
  {
30
30
  editor: GridFormPopoverMenu,
31
+ preventAutoEdit: true,
31
32
  ...custom,
32
33
  },
33
34
  );
@@ -6,6 +6,14 @@ import { ColDefT, GridBaseRow } from "../components";
6
6
 
7
7
  export type GridFilterExternal<RowType extends GridBaseRow> = (data: RowType, rowNode: RowNode) => boolean;
8
8
 
9
+ export interface AutoSizeColumnsProps {
10
+ skipHeader?: boolean;
11
+ colIds?: Set<string> | string[];
12
+ userSizedColIds?: Set<string>;
13
+ }
14
+
15
+ export type AutoSizeColumnsResult = { width: number } | null;
16
+
9
17
  export interface GridContextType<RowType extends GridBaseRow> {
10
18
  gridReady: boolean;
11
19
  setApis: (gridApi: GridApi | undefined, columnApi: ColumnApi | undefined, dataTestId?: string) => void;
@@ -26,8 +34,9 @@ export interface GridContextType<RowType extends GridBaseRow> {
26
34
  ensureRowVisible: (id: number | string) => boolean;
27
35
  ensureSelectedRowIsVisible: () => void;
28
36
  getFirstRowId: () => number;
29
- autoSizeAllColumns: (props?: { skipHeader?: boolean }) => { width: number } | null;
37
+ autoSizeColumns: (props?: AutoSizeColumnsProps) => AutoSizeColumnsResult;
30
38
  sizeColumnsToFit: () => void;
39
+ cancelEdit: () => void;
31
40
  stopEditing: () => void;
32
41
  updatingCells: (
33
42
  props: { selectedRows: GridBaseRow[]; field?: string },
@@ -47,6 +56,7 @@ export interface GridContextType<RowType extends GridBaseRow> {
47
56
  invisibleColumnIds: string[];
48
57
  setInvisibleColumnIds: (colIds: string[]) => void;
49
58
  downloadCsv: (csvExportParams?: CsvExportParams) => void;
59
+ setOnCellEditingComplete: (callback: (() => void) | undefined) => void;
50
60
  }
51
61
 
52
62
  export const GridContext = createContext<GridContextType<any>>({
@@ -117,8 +127,8 @@ export const GridContext = createContext<GridContextType<any>>({
117
127
  console.error("no context provider for getFirstRowId");
118
128
  return -1;
119
129
  },
120
- autoSizeAllColumns: () => {
121
- console.error("no context provider for autoSizeAllColumns");
130
+ autoSizeColumns: () => {
131
+ console.error("no context provider for autoSizeColumns");
122
132
  return null;
123
133
  },
124
134
  sizeColumnsToFit: () => {
@@ -129,6 +139,9 @@ export const GridContext = createContext<GridContextType<any>>({
129
139
  console.error("no context provider for editingCells");
130
140
  return false;
131
141
  },
142
+ cancelEdit: () => {
143
+ console.error("no context provider for cancelEdit");
144
+ },
132
145
  stopEditing: () => {
133
146
  console.error("no context provider for stopEditing");
134
147
  },
@@ -162,6 +175,9 @@ export const GridContext = createContext<GridContextType<any>>({
162
175
  downloadCsv: () => {
163
176
  console.error("no context provider for downloadCsv");
164
177
  },
178
+ setOnCellEditingComplete: () => {
179
+ console.error("no context provider for setOnCellEditingComplete");
180
+ },
165
181
  });
166
182
 
167
183
  export const useGridContext = <RowType extends GridBaseRow>() => useContext<GridContextType<RowType>>(GridContext);