@linzjs/step-ag-grid 14.1.2 → 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.2",
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,47 +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
+ */
94
110
  const hasSetContentSize = useRef(false);
95
111
  const hasSetContentSizeEmpty = useRef(false);
96
112
 
97
113
  const setInitialContentSize = useCallback(() => {
98
- if (
99
- (!isEmpty(params.rowData) && !hasSetContentSize.current) ||
100
- (isEmpty(params.rowData) && !hasSetContentSizeEmpty.current)
101
- ) {
102
- const skipHeaders = sizeColumns === "auto-skip-headers";
103
- if (sizeColumns === "auto" || skipHeaders) {
104
- // If we aren't skipping headers and there's no data, then don't skip headers
105
- const result = autoSizeAllColumns({ skipHeader: skipHeaders && !isEmpty(params.rowData) });
106
- if (isEmpty(params.rowData)) {
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) {
107
119
  hasSetContentSizeEmpty.current = true;
108
- } else {
120
+ params.onContentSize && result && params.onContentSize(result);
121
+ }
122
+ } else {
123
+ if (!hasSetContentSize.current) {
109
124
  hasSetContentSize.current = true;
125
+ params.onContentSize && result && params.onContentSize(result);
110
126
  }
111
- params.onContentSize && result && params.onContentSize(result);
112
127
  }
113
128
  }
114
129
 
115
130
  if (sizeColumns !== "none") {
116
131
  sizeColumnsToFit();
117
132
  }
118
- }, [autoSizeAllColumns, params, sizeColumns, sizeColumnsToFit]);
133
+ }, [autoSizeColumns, params, sizeColumns, sizeColumnsToFit]);
119
134
 
135
+ /**
136
+ * When the grid becomes ready resize it
137
+ */
138
+ const previousGridReady = useRef(gridReady);
120
139
  useEffect(() => {
121
- gridReady && setInitialContentSize();
140
+ if (!previousGridReady.current && gridReady) {
141
+ previousGridReady.current = true;
142
+ setInitialContentSize();
143
+ }
122
144
  }, [gridReady, setInitialContentSize]);
123
145
 
124
146
  /**
@@ -195,12 +217,17 @@ export const Grid = ({
195
217
 
196
218
  const selectedIds = params.externalSelectedItems.map((row) => row.id) as number[];
197
219
  const lastNewId = last(difference(selectedIds, lastSelectedIds.current));
198
- if (lastNewId != null) ensureRowVisible(lastNewId);
220
+ if (lastNewId != null) {
221
+ ensureRowVisible(lastNewId);
222
+ }
199
223
  lastSelectedIds.current = selectedIds;
200
224
  selectRowsById(selectedIds);
201
225
  setExternallySelectedItemsAreInSync(true);
202
226
  }, [gridReady, params.externalSelectedItems, ensureRowVisible, selectRowsById, setExternallySelectedItemsAreInSync]);
203
227
 
228
+ /**
229
+ * Combine grid and cell editable into one function
230
+ */
204
231
  const combineEditables =
205
232
  (...editables: (boolean | EditableCallback | undefined)[]) =>
206
233
  (params: EditableCallbackParams): boolean => {
@@ -214,10 +241,11 @@ export const Grid = ({
214
241
  /**
215
242
  * Synchronise externally selected items to grid on externalSelectedItems change
216
243
  */
217
- useEffect(() => {
218
- synchroniseExternallySelectedItemsToGrid();
219
- }, [synchroniseExternallySelectedItemsToGrid]);
244
+ useEffect(synchroniseExternallySelectedItemsToGrid, [synchroniseExternallySelectedItemsToGrid]);
220
245
 
246
+ /**
247
+ * Add selectable column to colDefs. Adjust column defs to block fit for auto sized columns.
248
+ */
221
249
  const columnDefs = useMemo((): ColDef[] => {
222
250
  const adjustColDefs = params.columnDefs.map((colDef) => {
223
251
  const colDefEditable = colDef.editable;
@@ -268,6 +296,9 @@ export const Grid = ({
268
296
  clickSelectorCheckboxWhenContainingCellClicked,
269
297
  ]);
270
298
 
299
+ /**
300
+ * When grid is ready set the apis to the grid context and sync selected items to grid.
301
+ */
271
302
  const onGridReady = useCallback(
272
303
  (event: GridReadyEvent) => {
273
304
  setApis(event.api, event.columnApi, dataTestId);
@@ -281,30 +312,56 @@ export const Grid = ({
281
312
  * This will resize columns when we have at least one row.
282
313
  */
283
314
  const previousRowDataLength = useRef(0);
284
- useEffect(() => {
285
- if (!gridReady) return;
286
315
 
316
+ const onRowDataChanged = useCallback(() => {
287
317
  const length = params.rowData?.length ?? 0;
288
318
  if (previousRowDataLength.current !== length) {
289
- if (previousRowDataLength.current === 0 && length > 0) {
290
- setInitialContentSize();
291
- }
319
+ setInitialContentSize();
292
320
  previousRowDataLength.current = length;
293
321
  }
294
- }, [gridReady, params.rowData?.length, setInitialContentSize]);
295
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);
296
338
  const onModelUpdated = useCallback((event: ModelUpdatedEvent) => {
297
- 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
+ }
298
350
  }, []);
299
351
 
352
+ /**
353
+ * Force-refresh all selected rows to re-run class function, to update selection highlighting
354
+ */
300
355
  const refreshSelectedRows = useCallback((event: CellEvent): void => {
301
- // Force-refresh all selected rows to re-run class function, to update selection highlighting
302
356
  event.api.refreshCells({
303
357
  force: true,
304
358
  rowNodes: event.api.getSelectedNodes(),
305
359
  });
306
360
  }, []);
307
361
 
362
+ /**
363
+ * Make sure node is selected for editing and start edit
364
+ */
308
365
  const startCellEditing = useCallback(
309
366
  (event: CellEvent) => {
310
367
  prePopupOps();
@@ -326,6 +383,9 @@ export const Grid = ({
326
383
  [checkUpdating, prePopupOps],
327
384
  );
328
385
 
386
+ /**
387
+ * Handle double click edit
388
+ */
329
389
  const onCellDoubleClick = useCallback(
330
390
  (event: CellEvent) => {
331
391
  if (!invokeEditAction(event)) startCellEditing(event);
@@ -333,6 +393,9 @@ export const Grid = ({
333
393
  [startCellEditing],
334
394
  );
335
395
 
396
+ /**
397
+ * Handle single click edits
398
+ */
336
399
  const onCellClicked = useCallback(
337
400
  (event: CellEvent) => {
338
401
  if (event.colDef?.cellRendererParams?.singleClickEdit) {
@@ -342,6 +405,9 @@ export const Grid = ({
342
405
  [startCellEditing],
343
406
  );
344
407
 
408
+ /**
409
+ * If cell has an edit action invoke it (if editable)
410
+ */
345
411
  const invokeEditAction = (e: CellEvent): boolean => {
346
412
  const editAction = e.colDef?.cellRendererParams?.editAction;
347
413
  if (!editAction) return false;
@@ -356,6 +422,9 @@ export const Grid = ({
356
422
  return true;
357
423
  };
358
424
 
425
+ /**
426
+ * Start editing on pressing Enter
427
+ */
359
428
  const onCellKeyPress = useCallback(
360
429
  (e: CellEvent) => {
361
430
  if ((e.event as KeyboardEvent).key === "Enter") {
@@ -378,6 +447,80 @@ export const Grid = ({
378
447
  [columnDefs, sizeColumns],
379
448
  );
380
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
+
381
524
  return (
382
525
  <div
383
526
  data-testid={dataTestId}
@@ -396,14 +539,18 @@ export const Grid = ({
396
539
  suppressRowClickSelection={true}
397
540
  rowSelection={rowSelection}
398
541
  suppressBrowserResizeObserver={true}
399
- onGridSizeChanged={setInitialContentSize}
542
+ onGridSizeChanged={onGridSizeChanged}
400
543
  suppressColumnVirtualisation={suppressColumnVirtualization}
401
544
  suppressClickEdit={true}
545
+ onRowDataChanged={onRowDataChanged}
402
546
  onCellKeyPress={onCellKeyPress}
403
547
  onCellClicked={onCellClicked}
404
548
  onCellDoubleClicked={onCellDoubleClick}
405
549
  onCellEditingStarted={refreshSelectedRows}
406
550
  domLayout={params.domLayout}
551
+ onCellEditingStopped={onCellEditingStopped}
552
+ onColumnResized={onColumnResized}
553
+ defaultColDef={{ minWidth: 48, ...omit(params.defaultColDef, ["editable"]) }}
407
554
  columnDefs={columnDefsAdjusted}
408
555
  rowData={params.rowData}
409
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);