@linzjs/step-ag-grid 14.1.2 → 14.3.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.
@@ -8,7 +8,7 @@ import { ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, u
8
8
 
9
9
  import { ColDefT, GridBaseRow } from "../components";
10
10
  import { isNotEmpty, sanitiseFileName, wait } from "../utils/util";
11
- import { GridContext, GridFilterExternal } from "./GridContext";
11
+ import { AutoSizeColumnsProps, AutoSizeColumnsResult, GridContext, GridFilterExternal } from "./GridContext";
12
12
  import { GridUpdatingContext } from "./GridUpdatingContext";
13
13
 
14
14
  interface GridContextProps {
@@ -64,14 +64,13 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
64
64
  */
65
65
  const ensureRowVisible = useCallback(
66
66
  (id: number | string): boolean => {
67
- return gridApiOp((gridApi) => {
68
- const node = gridApi.getRowNode(`${id}`);
69
- if (!node) return false;
70
- gridApi.ensureNodeVisible(node);
71
- return true;
72
- });
67
+ if (!gridApi) return false;
68
+ const node = gridApi?.getRowNode(`${id}`);
69
+ if (!node) return false;
70
+ defer(() => gridApi.ensureNodeVisible(node));
71
+ return true;
73
72
  },
74
- [gridApiOp],
73
+ [gridApi],
75
74
  );
76
75
 
77
76
  const getFirstRowId = useCallback((): number => {
@@ -211,17 +210,16 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
211
210
  );
212
211
  const firstNode = rowsThatNeedSelecting[0];
213
212
  if (firstNode) {
214
- gridApi.ensureNodeVisible(firstNode);
213
+ defer(() => gridApi.ensureNodeVisible(firstNode));
215
214
  const colDefs = gridApi.getColumnDefs();
216
- if (colDefs && colDefs.length) {
215
+ if (colDefs?.length) {
217
216
  const col = colDefs[0] as ColDef; // We don't support ColGroupDef
218
217
  const rowIndex = firstNode.rowIndex;
219
218
  if (rowIndex != null && col != null) {
220
219
  const colId = col.colId;
221
220
  // We need to make sure we aren't currently editing a cell otherwise tests will fail
222
221
  // as they will start to edit the cell before this stuff has a chance to run
223
- colId != null &&
224
- defer(() => isEmpty(gridApi.getEditingCells()) && gridApi.setFocusedCell(rowIndex, colId));
222
+ colId && defer(() => isEmpty(gridApi.getEditingCells()) && gridApi.setFocusedCell(rowIndex, colId));
225
223
  }
226
224
  }
227
225
  }
@@ -337,25 +335,29 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
337
335
  gridApiOp((gridApi) => {
338
336
  const selectedNodes = gridApi.getSelectedNodes();
339
337
  if (isEmpty(selectedNodes)) return;
340
- gridApi.ensureNodeVisible(last(selectedNodes));
338
+ defer(() => gridApi.ensureNodeVisible(last(selectedNodes)));
341
339
  });
342
340
  }, [gridApiOp]);
343
341
 
344
342
  /**
345
343
  * Resize columns to fit container
346
344
  */
347
- const autoSizeAllColumns = useCallback(
348
- ({ skipHeader }): { width: number } | null => {
349
- if (columnApi) {
350
- columnApi.autoSizeAllColumns(skipHeader);
351
- return {
352
- width: sumBy(
353
- columnApi.getColumnState().filter((col) => col.hide !== true),
354
- "width",
355
- ),
356
- };
357
- }
358
- return null;
345
+ const autoSizeColumns = useCallback(
346
+ ({ skipHeader, colIds, userSizedColIds }: AutoSizeColumnsProps = {}): AutoSizeColumnsResult => {
347
+ if (!columnApi) return null;
348
+ const colIdsSet = colIds instanceof Set ? colIds : new Set<string>(colIds ?? []);
349
+ columnApi.getColumnState().forEach((col) => {
350
+ const colId = col.colId;
351
+ if ((isEmpty(colIdsSet) || colIdsSet.has(colId)) && !userSizedColIds?.has(colId)) {
352
+ columnApi.autoSizeColumn(colId, skipHeader);
353
+ }
354
+ });
355
+ return {
356
+ width: sumBy(
357
+ columnApi.getColumnState().filter((col) => !col.hide),
358
+ "width",
359
+ ),
360
+ };
359
361
  },
360
362
  [columnApi],
361
363
  );
@@ -364,26 +366,83 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
364
366
  * Resize columns to fit container
365
367
  */
366
368
  const sizeColumnsToFit = useCallback((): void => {
367
- if (gridApi) {
368
- gridApi.sizeColumnsToFit();
369
- }
369
+ gridApi && gridApi.sizeColumnsToFit();
370
370
  }, [gridApi]);
371
371
 
372
372
  const stopEditing = useCallback((): void => {
373
+ if (!gridApi) return;
373
374
  if (prePopupFocusedCell.current) {
374
375
  gridApi?.setFocusedCell(prePopupFocusedCell.current.rowIndex, prePopupFocusedCell.current.column);
375
376
  }
376
- gridApiOp((gridApi) => gridApi.stopEditing());
377
- }, [gridApi, gridApiOp]);
377
+ gridApi.stopEditing();
378
+ }, [gridApi]);
378
379
 
379
- const selectNextCell = useCallback(
380
- (tabDirection: -1 | 0 | 1 = 0) => {
381
- gridApiOp((gridApi) => {
382
- if (tabDirection == 1) gridApi.tabToNextCell();
383
- if (tabDirection == -1) gridApi.tabToPreviousCell();
384
- });
380
+ /**
381
+ * This differs from stopEdit in that it will also invoke cellEditingCompleteCallback
382
+ */
383
+ const cancelEdit = useCallback((): void => {
384
+ stopEditing();
385
+ cellEditingCompleteCallbackRef.current && cellEditingCompleteCallbackRef.current();
386
+ }, [stopEditing]);
387
+
388
+ const cellEditingCompleteCallbackRef = useRef<() => void>();
389
+ const setOnCellEditingComplete = useCallback((cellEditingCompleteCallback: (() => void) | undefined) => {
390
+ cellEditingCompleteCallbackRef.current = cellEditingCompleteCallback;
391
+ }, []);
392
+
393
+ /**
394
+ * Returns true if an editable cell on same row was selected, else false.
395
+ */
396
+ const selectNextEditableCell = useCallback(
397
+ (tabDirection: -1 | 1): boolean => {
398
+ // Pretend it succeeded to prevent unwanted cellEditingCompleteCallback
399
+ if (!gridApi) return true;
400
+
401
+ const focusedCellIsEditable = () => {
402
+ const focusedCell = gridApi.getFocusedCell();
403
+ const nextColumn = focusedCell?.column;
404
+ const nextColDef = nextColumn?.getColDef();
405
+ const rowNode = focusedCell && gridApi.getDisplayedRowAtIndex(focusedCell?.rowIndex);
406
+ return (
407
+ !!(rowNode && nextColumn && nextColDef) &&
408
+ nextColumn.isCellEditable(rowNode) &&
409
+ !nextColDef.cellEditorParams?.preventAutoEdit &&
410
+ !nextColDef.cellRendererParams?.editAction
411
+ );
412
+ };
413
+
414
+ let foundEditableCell = false;
415
+
416
+ // Just in case I've missed something, we don't want the loop to hang everything
417
+ let maxIterations = 50;
418
+ let preRow: CellPosition | null = null;
419
+ let postRow: CellPosition | null = null;
420
+ do {
421
+ preRow = gridApi.getFocusedCell();
422
+ tabDirection === 1 ? gridApi.tabToNextCell() : gridApi.tabToPreviousCell();
423
+ postRow = gridApi.getFocusedCell();
424
+ foundEditableCell = focusedCellIsEditable();
425
+ } while (
426
+ preRow?.rowIndex === postRow?.rowIndex &&
427
+ preRow?.column !== postRow?.column &&
428
+ !foundEditableCell &&
429
+ maxIterations-- > 0
430
+ );
431
+
432
+ if (foundEditableCell) {
433
+ prePopupOps();
434
+ const focusedCell = gridApi?.getFocusedCell();
435
+ if (focusedCell) {
436
+ gridApi.startEditingCell({
437
+ rowIndex: focusedCell.rowIndex,
438
+ colKey: focusedCell.column.getColId(),
439
+ });
440
+ return false;
441
+ }
442
+ }
443
+ return true;
385
444
  },
386
- [gridApiOp],
445
+ [gridApi, prePopupOps],
387
446
  );
388
447
 
389
448
  const updatingCells = useCallback(
@@ -398,6 +457,7 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
398
457
  const selectedRows = props.selectedRows;
399
458
 
400
459
  let ok = false;
460
+
401
461
  await modifyUpdating(
402
462
  props.field ?? "",
403
463
  selectedRows.map((data) => data.id),
@@ -429,19 +489,20 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
429
489
  // Only focus next cell if user hasn't already manually changed focus
430
490
  const postPopupFocusedCell = gridApi.getFocusedCell();
431
491
  if (
432
- tabDirection &&
433
492
  prePopupFocusedCell.current &&
434
493
  postPopupFocusedCell &&
435
494
  prePopupFocusedCell.current.rowIndex == postPopupFocusedCell.rowIndex &&
436
495
  prePopupFocusedCell.current.column.getColId() == postPopupFocusedCell.column.getColId()
437
496
  ) {
438
- selectNextCell(tabDirection);
497
+ if (!tabDirection || selectNextEditableCell(tabDirection)) {
498
+ cellEditingCompleteCallbackRef.current && cellEditingCompleteCallbackRef.current();
499
+ }
439
500
  }
440
501
 
441
502
  return ok;
442
503
  });
443
504
  },
444
- [gridApiOp, modifyUpdating, selectNextCell],
505
+ [gridApiOp, modifyUpdating, selectNextEditableCell],
445
506
  );
446
507
 
447
508
  const redrawRows = useCallback(
@@ -476,12 +537,12 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
476
537
 
477
538
  const addExternalFilter = (filter: GridFilterExternal<RowType>) => {
478
539
  externalFilters.current.push(filter);
479
- onFilterChanged();
540
+ onFilterChanged().then();
480
541
  };
481
542
 
482
543
  const removeExternalFilter = (filter: GridFilterExternal<RowType>) => {
483
544
  remove(externalFilters.current, (v) => v === filter);
484
- onFilterChanged();
545
+ onFilterChanged().then();
485
546
  };
486
547
 
487
548
  const isExternalFilterPresent = (): boolean => externalFilters.current.length > 0;
@@ -557,8 +618,9 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
557
618
  ensureRowVisible,
558
619
  ensureSelectedRowIsVisible,
559
620
  sizeColumnsToFit,
560
- autoSizeAllColumns,
621
+ autoSizeColumns,
561
622
  stopEditing,
623
+ cancelEdit,
562
624
  updatingCells,
563
625
  redrawRows,
564
626
  externallySelectedItemsAreInSync,
@@ -569,6 +631,7 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
569
631
  isExternalFilterPresent,
570
632
  doesExternalFilterPass,
571
633
  downloadCsv,
634
+ setOnCellEditingComplete,
572
635
  }}
573
636
  >
574
637
  {props.children}
@@ -617,6 +680,7 @@ export const downloadCsvUseValueFormattersProcessCellCallback = (params: Process
617
680
  }
618
681
 
619
682
  const result = valueFormatter({ ...params, data: params.node?.data, colDef } as ValueFormatterParams);
683
+ // type may not be string due to casting, leave the type check in
620
684
  if (params.value != null && typeof result !== "string") {
621
685
  console.error(`downloadCsv: valueFormatter is returning non string values, colDef:", colId: ${colDef.colId}`);
622
686
  }
@@ -2,6 +2,7 @@ import { createContext } from "react";
2
2
 
3
3
  export type GridUpdatingContextType = {
4
4
  checkUpdating: (fields: string | string[], id: number | string) => boolean;
5
+ isUpdating: () => boolean;
5
6
  modifyUpdating: (
6
7
  fields: string | string[],
7
8
  ids: (number | string)[],
@@ -15,6 +16,10 @@ export const GridUpdatingContext = createContext<GridUpdatingContextType>({
15
16
  console.error("Missing GridUpdatingContext");
16
17
  return false;
17
18
  },
19
+ isUpdating: () => {
20
+ console.error("Missing GridUpdatingContext");
21
+ return false;
22
+ },
18
23
  modifyUpdating: async () => {
19
24
  console.error("Missing GridUpdatingContext");
20
25
  },
@@ -1,4 +1,4 @@
1
- import { castArray, flatten, remove } from "lodash-es";
1
+ import { castArray, flatten, isEmpty, remove } from "lodash-es";
2
2
  import { ReactNode, useRef, useState } from "react";
3
3
 
4
4
  import { GridUpdatingContext } from "./GridUpdatingContext";
@@ -21,12 +21,17 @@ export const GridUpdatingContextProvider = (props: UpdatingContextProviderProps)
21
21
  const resetUpdating = () => {
22
22
  const mergedUpdatingBlocks: GridUpdatingContextStatus = {};
23
23
  for (const key in updatingBlocks.current) {
24
- mergedUpdatingBlocks[key] = flatten(updatingBlocks.current[key]);
24
+ const arr = flatten(updatingBlocks.current[key]);
25
+ if (arr.length) {
26
+ mergedUpdatingBlocks[key] = arr;
27
+ }
25
28
  }
26
29
  updating.current = mergedUpdatingBlocks;
27
30
  setUpdatedDep((updatedDep) => updatedDep + 1);
28
31
  };
29
32
 
33
+ const isUpdating = () => !isEmpty(updating.current);
34
+
30
35
  const modifyUpdating = async (
31
36
  fields: string | string[],
32
37
  ids: (number | string)[],
@@ -50,7 +55,7 @@ export const GridUpdatingContextProvider = (props: UpdatingContextProviderProps)
50
55
  castArray(fields).some((f) => updating.current[f]?.includes(id));
51
56
 
52
57
  return (
53
- <GridUpdatingContext.Provider value={{ modifyUpdating, checkUpdating, updatedDep }}>
58
+ <GridUpdatingContext.Provider value={{ modifyUpdating, checkUpdating, isUpdating, updatedDep }}>
54
59
  {props.children}
55
60
  </GridUpdatingContext.Provider>
56
61
  );
@@ -38,3 +38,16 @@ export const useTimeoutHook = () => {
38
38
 
39
39
  return invoke;
40
40
  };
41
+
42
+ interface IntervalHookProps {
43
+ timeoutMs: number;
44
+ callback: () => void;
45
+ }
46
+ export const useIntervalHook = ({ callback, timeoutMs }: IntervalHookProps) => {
47
+ useEffect(() => {
48
+ const interval = setInterval(callback, timeoutMs);
49
+ return () => {
50
+ clearInterval(interval);
51
+ };
52
+ });
53
+ };
@@ -477,7 +477,27 @@ export const MenuList = ({
477
477
  top: menuPosition.y,
478
478
  }}
479
479
  >
480
- <div ref={focusRef} tabIndex={-1} style={{ position: "absolute", left: 0, top: 0 }} />
480
+ <div
481
+ ref={focusRef}
482
+ tabIndex={-1}
483
+ style={{ position: "absolute", left: 0, top: 0 }}
484
+ onKeyDown={(event) => {
485
+ if (event.key == "Tab") {
486
+ event.preventDefault();
487
+ event.stopPropagation();
488
+ safeCall(onClose, {
489
+ key: event.key,
490
+ shiftKey: event.shiftKey,
491
+ reason:
492
+ event.key === "Tab"
493
+ ? event.shiftKey
494
+ ? CloseReason.TAB_BACKWARD
495
+ : CloseReason.TAB_FORWARD
496
+ : CloseReason.CLICK,
497
+ });
498
+ }
499
+ }}
500
+ />
481
501
  {arrow && (
482
502
  <div
483
503
  className={_arrowClass}
@@ -192,7 +192,7 @@ const GridPopoutEditGenericTemplate: ComponentStory<typeof Grid> = (props: GridP
192
192
  setRowData([
193
193
  ...rowData,
194
194
  {
195
- id: lastRow.id + 1,
195
+ id: (lastRow?.id ?? 0) + 1,
196
196
  name: "?",
197
197
  nameType: "?",
198
198
  numba: "?",
@@ -212,6 +212,12 @@ const GridPopoutEditGenericTemplate: ComponentStory<typeof Grid> = (props: GridP
212
212
  columnDefs={columnDefs}
213
213
  rowData={rowData}
214
214
  domLayout={"autoHeight"}
215
+ defaultColDef={{ minWidth: 70 }}
216
+ sizeColumns={"auto"}
217
+ onCellEditingComplete={() => {
218
+ /* eslint-disable-next-line no-console */
219
+ console.log("Cell editing complete");
220
+ }}
215
221
  />
216
222
  <ActionButton icon={"ic_add"} name={"Add new row"} inProgressName={"Adding..."} onClick={addRowAction} />
217
223
  </>
@@ -155,6 +155,7 @@ const GridEditDropDownTemplate: ComponentStory<typeof Grid> = (props: GridProps)
155
155
  {
156
156
  field: "position3",
157
157
  headerName: "Filtered",
158
+ editable: false,
158
159
  },
159
160
  {
160
161
  multiEdit: true,
@@ -302,6 +303,10 @@ const GridEditDropDownTemplate: ComponentStory<typeof Grid> = (props: GridProps)
302
303
  columnDefs={columnDefs}
303
304
  rowData={rowData}
304
305
  domLayout={"autoHeight"}
306
+ onCellEditingComplete={() => {
307
+ /* eslint-disable-next-line no-console */
308
+ console.log("Cell editing complete");
309
+ }}
305
310
  />
306
311
  </GridWrapper>
307
312
  );
@@ -248,7 +248,9 @@ GridKeyboardInteractions.play = async ({ canvasElement }) => {
248
248
  await test(() => userEvent.keyboard("{Enter}"), "8", "2");
249
249
  await test(() => userEvent.tab(), "9", "2");
250
250
  userEvent.tab({ shift: true });
251
- await test(() => userEvent.tab({ shift: true }), "7", "2");
251
+ await test(() => userEvent.tab({ shift: true }), "6", "2");
252
+ userEvent.keyboard("{Esc}");
253
+ userEvent.tab();
252
254
 
253
255
  userEvent.keyboard("{Enter}");
254
256
  await wait(250);