@linzjs/step-ag-grid 28.5.1 → 29.0.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.
Files changed (42) hide show
  1. package/dist/index.css +11 -7
  2. package/dist/src/components/Grid.d.ts +5 -1
  3. package/dist/src/contexts/GridContext.d.ts +3 -4
  4. package/dist/src/contexts/GridPopoverContext.d.ts +1 -0
  5. package/dist/src/contexts/GridUpdatingContext.d.ts +1 -0
  6. package/dist/src/lui/reactUtils.d.ts +4 -0
  7. package/dist/step-ag-grid.cjs +263 -246
  8. package/dist/step-ag-grid.cjs.map +1 -1
  9. package/dist/step-ag-grid.esm.js +264 -247
  10. package/dist/step-ag-grid.esm.js.map +1 -1
  11. package/package.json +21 -22
  12. package/src/components/Grid.tsx +114 -112
  13. package/src/components/GridPopoverHook.tsx +25 -20
  14. package/src/components/gridFilter/useGridFilter.ts +3 -3
  15. package/src/components/gridHook/useGridContextMenu.tsx +3 -3
  16. package/src/contexts/GridContext.tsx +11 -14
  17. package/src/contexts/GridContextProvider.tsx +164 -137
  18. package/src/contexts/GridPopoverContext.tsx +2 -0
  19. package/src/contexts/GridPopoverContextProvider.tsx +9 -5
  20. package/src/contexts/GridUpdatingContext.tsx +5 -0
  21. package/src/contexts/GridUpdatingContextProvider.tsx +35 -28
  22. package/src/lui/reactUtils.tsx +24 -0
  23. package/src/react-menu3/components/MenuList.tsx +3 -0
  24. package/src/stories/grid/GridFilterButtons.stories.tsx +1 -1
  25. package/src/stories/grid/GridNonEditableRow.stories.tsx +1 -1
  26. package/src/stories/grid/GridPinnedRow.stories.tsx +1 -1
  27. package/src/stories/grid/GridPopoutContextMenu.stories.tsx +3 -3
  28. package/src/stories/grid/GridPopoutEditGenericTextArea.stories.tsx +2 -2
  29. package/src/stories/grid/GridPopoverEditDropDown.stories.tsx +8 -4
  30. package/src/stories/grid/GridPopoverEditMultiSelect.stories.tsx +1 -0
  31. package/src/stories/grid/GridReadOnly.stories.tsx +1 -1
  32. package/src/stories/grid/GridViewList.stories.tsx +1 -1
  33. package/src/stories/grid/gridFormInteraction/GridFormDropDownInteraction.stories.tsx +5 -3
  34. package/src/stories/grid/gridFormInteraction/GridFormEditBearingCorrectionInteraction.stories.tsx +3 -2
  35. package/src/stories/grid/gridFormInteraction/GridFormEditBearingInteraction.stories.tsx +3 -2
  36. package/src/stories/grid/gridFormInteraction/GridFormMultiSelectGridInteraction.stories.tsx +3 -2
  37. package/src/stories/grid/gridFormInteraction/GridFormMultiSelectInteraction.stories.tsx +3 -2
  38. package/src/stories/grid/gridFormInteraction/GridFormPopoverMenuInteraction.stories.tsx +3 -2
  39. package/src/stories/grid/gridFormInteraction/GridFormTextAreaInteraction.stories.tsx +3 -2
  40. package/src/stories/grid/gridFormInteraction/GridFormTextInputInteraction.stories.tsx +3 -2
  41. package/src/stories/grid/gridFormStatic/GridFormDropDown.stories.tsx +2 -2
  42. package/src/stories/grid/interactions/GridKeyboardInteractions.stories.tsx +2 -2
@@ -17,7 +17,7 @@ import { GridUpdatingContext } from './GridUpdatingContext';
17
17
  * Also, make sure the provider is created in a separate component, otherwise it won't be found.
18
18
  */
19
19
  export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithChildren): ReactElement => {
20
- const { modifyUpdating, checkUpdating } = useContext(GridUpdatingContext);
20
+ const { modifyUpdating, anyUpdating } = useContext(GridUpdatingContext);
21
21
  const [gridApi, setGridApi] = useState<GridApi>();
22
22
  const [gridReady, setGridReady] = useState(false);
23
23
  const [quickFilter, _setQuickFilter] = useState('');
@@ -41,8 +41,10 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
41
41
  /**
42
42
  * Make extra sure the GridCellFillerColId never gets added to invisibleColumnIds as it's dynamically determined
43
43
  */
44
- const setInvisibleColumnIds = (invisibleColumnIds: string[]) =>
45
- _setInvisibleColumnIds(pull(invisibleColumnIds, GridCellFillerColId));
44
+ const setInvisibleColumnIds = useCallback(
45
+ (invisibleColumnIds: string[]) => _setInvisibleColumnIds(pull(invisibleColumnIds, GridCellFillerColId)),
46
+ [],
47
+ );
46
48
 
47
49
  /**
48
50
  * Set quick filter directly on grid, based on previously save quickFilter state.
@@ -149,18 +151,6 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
149
151
  prePopupFocusedCell.current = gridApi.getFocusedCell() ?? undefined;
150
152
  }, [gridApi]);
151
153
 
152
- /**
153
- * After a popup refocus the cell.
154
- */
155
- const postPopupOps = useCallback(() => {
156
- if (!gridApi || gridApi.isDestroyed()) {
157
- return;
158
- }
159
- if (prePopupFocusedCell.current) {
160
- gridApi.setFocusedCell(prePopupFocusedCell.current.rowIndex, prePopupFocusedCell.current.column);
161
- }
162
- }, [gridApi]);
163
-
164
154
  /**
165
155
  * Get all row id's in grid.
166
156
  */
@@ -461,13 +451,16 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
461
451
  }
462
452
  }, [gridApi]);
463
453
 
464
- const stopEditing = useCallback((): void => {
465
- if (!gridApi || gridApi.isDestroyed()) {
454
+ /**
455
+ *
456
+ */
457
+ const resetFocusedCellAfterCellEditing = useCallback((): void => {
458
+ if (!gridApi || gridApi.isDestroyed() || startCellEditingInProgressRef.current) {
466
459
  return;
467
460
  }
468
- gridApi.stopEditing();
469
461
  if (prePopupFocusedCell.current) {
470
462
  gridApi.setFocusedCell(prePopupFocusedCell.current.rowIndex, prePopupFocusedCell.current.column);
463
+ prePopupFocusedCell.current = undefined;
471
464
  }
472
465
  }, [gridApi]);
473
466
 
@@ -491,59 +484,65 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
491
484
  }
492
485
  }, []);
493
486
 
487
+ const startCellEditingInProgressRef = useRef(false);
494
488
  const startCellEditing = useCallback(
495
489
  async ({ rowId, colId }: { rowId: number; colId: string }) => {
496
- if (!gridApi) return;
497
-
498
- const colDef = gridApi.getColumnDef(colId);
499
- if (!colDef) {
490
+ if (!gridApi || startCellEditingInProgressRef.current) {
500
491
  return;
501
492
  }
493
+ startCellEditingInProgressRef.current = true;
502
494
 
503
- const rowNode = gridApi.getRowNode(`${rowId}`);
504
- if (!rowNode) {
505
- return;
506
- }
495
+ try {
496
+ // Edit in progress so don't edit until finished, timeout waiting after 5s
497
+ if (!(await waitForCondition(() => !anyUpdating(), 5000))) {
498
+ console.error("Could not start edit because previous edit hasn't finished after 5 seconds");
499
+ return;
500
+ }
507
501
 
508
- prePopupOps();
509
- const shouldSelectNode = !rowNode.isSelected();
510
- if (shouldSelectNode) {
511
- externallySelectedItemsAreInSyncRef.current = false;
512
- setExternallySelectedItemsAreInSync(false);
513
- rowNode.setSelected(true, true);
514
- await waitForExternallySelectedItemsToBeInSync();
515
- }
502
+ const colDef = gridApi.getColumnDef(colId);
503
+ if (!colDef) {
504
+ return;
505
+ }
516
506
 
517
- // Cell already being edited, so don't re-edit until finished
518
- if (checkUpdating([colDef.field ?? colDef.colId ?? ''], rowId)) {
519
- return;
520
- }
507
+ const rowNode = gridApi.getRowNode(`${rowId}`);
508
+ if (!rowNode) {
509
+ return;
510
+ }
521
511
 
522
- const rowIndex = rowNode.rowIndex;
523
- if (rowIndex != null) {
524
- defer(() => {
525
- !gridApi.isDestroyed() &&
526
- gridApi.startEditingCell({
527
- rowIndex,
528
- colKey: colId,
529
- });
530
- });
512
+ prePopupOps();
513
+ const shouldSelectNode = !rowNode.isSelected();
514
+ if (shouldSelectNode) {
515
+ externallySelectedItemsAreInSyncRef.current = false;
516
+ setExternallySelectedItemsAreInSync(false);
517
+ rowNode.setSelected(true, true);
518
+ await waitForExternallySelectedItemsToBeInSync();
519
+ }
520
+
521
+ const rowIndex = rowNode.rowIndex;
522
+ if (rowIndex != null) {
523
+ defer(() => {
524
+ !gridApi.isDestroyed() &&
525
+ gridApi.startEditingCell({
526
+ rowIndex,
527
+ colKey: colId,
528
+ });
529
+ });
530
+ }
531
+ } finally {
532
+ startCellEditingInProgressRef.current = false;
531
533
  }
532
534
  },
533
- [checkUpdating, gridApi, prePopupOps, waitForExternallySelectedItemsToBeInSync],
535
+ [anyUpdating, gridApi, prePopupOps, waitForExternallySelectedItemsToBeInSync],
534
536
  );
535
537
 
536
- /**
537
- * This differs from stopEdit in that it will also invoke cellEditingCompleteCallback
538
- */
539
- const cancelEdit = useCallback((): void => {
540
- stopEditing();
541
- cellEditingCompleteCallbackRef.current?.();
542
- }, [stopEditing]);
543
-
544
- const cellEditingCompleteCallbackRef = useRef<() => void>();
545
- const setOnCellEditingComplete = useCallback((cellEditingCompleteCallback: (() => void) | undefined) => {
546
- cellEditingCompleteCallbackRef.current = cellEditingCompleteCallback;
538
+ const bulkEditingCompleteCallbackRef = useRef<() => void>();
539
+ const onBulkEditingComplete = useCallback(() => {
540
+ resetFocusedCellAfterCellEditing();
541
+ bulkEditingCompleteCallbackRef.current?.();
542
+ }, [resetFocusedCellAfterCellEditing]);
543
+
544
+ const setOnBulkEditingComplete = useCallback((cellEditingCompleteCallback: (() => void) | undefined) => {
545
+ bulkEditingCompleteCallbackRef.current = cellEditingCompleteCallback;
547
546
  }, []);
548
547
 
549
548
  /**
@@ -552,7 +551,9 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
552
551
  const selectNextEditableCell = useCallback(
553
552
  async (tabDirection: -1 | 1): Promise<boolean> => {
554
553
  // Pretend it succeeded to prevent unwanted cellEditingCompleteCallback
555
- if (!gridApi) return true;
554
+ if (!gridApi) {
555
+ return true;
556
+ }
556
557
 
557
558
  const focusedCellIsEditable = () => {
558
559
  const focusedCell = gridApi.isDestroyed() ? null : gridApi.getFocusedCell();
@@ -572,17 +573,17 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
572
573
  if (gridApi.isDestroyed()) {
573
574
  return true;
574
575
  }
576
+ // Prevent resetting focus to original editing cell
577
+ prePopupFocusedCell.current = undefined;
578
+
575
579
  const preRow = gridApi.getFocusedCell();
576
580
  if (tabDirection === 1) {
581
+ gridApi.stopEditing();
577
582
  gridApi.tabToNextCell();
578
583
  } else {
579
584
  gridApi.tabToPreviousCell();
580
585
  }
581
586
 
582
- // If we don't wait the tab to next element won't work
583
- // I think this is due to the grid resizing
584
- await wait(150);
585
-
586
587
  if (gridApi.isDestroyed()) {
587
588
  return true;
588
589
  }
@@ -596,18 +597,19 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
596
597
  if (focusedCellIsEditable()) {
597
598
  const focusedCell = gridApi.getFocusedCell();
598
599
  if (focusedCell) {
599
- prePopupOps();
600
- gridApi.startEditingCell({
601
- rowIndex: focusedCell.rowIndex,
602
- colKey: focusedCell.column.getColId(),
603
- });
600
+ const rowNode = gridApi.getDisplayedRowAtIndex(focusedCell.rowIndex);
601
+ const rowId = rowNode?.data?.id;
602
+ if (rowId == null) {
603
+ return false;
604
+ }
605
+ await startCellEditing({ rowId, colId: focusedCell.column.getColId() });
604
606
  return true;
605
607
  }
606
608
  }
607
609
  }
608
610
  return false;
609
611
  },
610
- [gridApi, prePopupOps],
612
+ [gridApi, startCellEditing],
611
613
  );
612
614
 
613
615
  const updatingCells = useCallback(
@@ -617,65 +619,71 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
617
619
  setSaving?: (saving: boolean) => void,
618
620
  tabDirection?: 1 | 0 | -1,
619
621
  ): Promise<boolean> => {
620
- setSaving && setSaving(true);
621
- return await gridApiOp(async (gridApi) => {
622
- const selectedRows = props.selectedRows;
623
-
624
- let ok = false;
625
-
626
- await modifyUpdating(
627
- props.field ?? '',
628
- selectedRows.map((data) => data.id),
629
- async () => {
630
- // MATT Disabled I don't believe these are needed anymore
631
- // I've left them here just in case they are
632
- // Need to refresh to get spinners to work on all rows
633
- // gridApi.refreshCells({ rowNodes: props.selectedRows as RowNode[], force: true });
634
- ok = await fnUpdate(selectedRows).catch((ex) => {
635
- console.error('Exception during modifyUpdating', ex);
636
- return false;
637
- });
638
- },
639
- );
640
-
641
- // MATT Disabled I don't believe these are needed anymore
642
- // I've left them here just in case they are
643
- // async processes need to refresh their own rows
644
- // gridApi.refreshCells({ rowNodes: selectedRows as RowNode[], force: true });
622
+ try {
623
+ setSaving?.(true);
624
+ return await gridApiOp(async (gridApi) => {
625
+ const selectedRows = props.selectedRows;
626
+
627
+ let ok = false;
628
+
629
+ await modifyUpdating(
630
+ props.field ?? '',
631
+ selectedRows.map((data) => data.id),
632
+ async () => {
633
+ // MATT Disabled I don't believe these are needed anymore
634
+ // I've left them here just in case they are
635
+ // Need to refresh to get spinners to work on all rows
636
+ // gridApi.refreshCells({ rowNodes: props.selectedRows as RowNode[], force: true });
637
+ ok = await fnUpdate(selectedRows).catch((ex) => {
638
+ console.error('Exception during modifyUpdating', ex);
639
+ return false;
640
+ });
641
+ },
642
+ );
645
643
 
646
- if (gridApi.isDestroyed()) {
647
- return ok;
648
- }
644
+ // MATT Disabled I don't believe these are needed anymore
645
+ // I've left them here just in case they are
646
+ // async processes need to refresh their own rows
647
+ // gridApi.refreshCells({ rowNodes: selectedRows as RowNode[], force: true });
649
648
 
650
- if (ok) {
651
- const cell = gridApi.getFocusedCell();
652
- if (cell && gridApi.getFocusedCell() == null) {
653
- !gridApi.isDestroyed && gridApi.setFocusedCell(cell.rowIndex, cell.column);
649
+ if (gridApi.isDestroyed()) {
650
+ return ok;
654
651
  }
655
- // This is needed to trigger postSortRowsHook
656
- gridApi.refreshClientSideRowModel();
657
- } else {
658
- // Don't set saving if ok as the form has already closed
659
- setSaving?.(false);
660
- }
661
652
 
662
- // Only focus next cell if user hasn't already manually changed focus
663
- const postPopupFocusedCell = gridApi.getFocusedCell();
664
- if (
665
- prePopupFocusedCell.current &&
666
- postPopupFocusedCell &&
667
- prePopupFocusedCell.current.rowIndex == postPopupFocusedCell.rowIndex &&
668
- prePopupFocusedCell.current.column.getColId() == postPopupFocusedCell.column.getColId()
669
- ) {
670
- if (!tabDirection || !(await selectNextEditableCell(tabDirection))) {
671
- cellEditingCompleteCallbackRef.current && cellEditingCompleteCallbackRef.current();
653
+ if (ok) {
654
+ const cell = gridApi.getFocusedCell();
655
+ if (cell && gridApi.getFocusedCell() == null) {
656
+ !gridApi.isDestroyed && gridApi.setFocusedCell(cell.rowIndex, cell.column);
657
+ }
658
+
659
+ // This is needed to trigger postSortRowsHook
660
+ gridApi.refreshClientSideRowModel();
672
661
  }
673
- }
674
662
 
675
- return ok;
676
- });
663
+ void (async () => {
664
+ // Only focus next cell if user hasn't already manually changed focus
665
+ const postPopupFocusedCell = gridApi.getFocusedCell();
666
+ if (
667
+ prePopupFocusedCell.current &&
668
+ postPopupFocusedCell &&
669
+ prePopupFocusedCell.current.rowIndex == postPopupFocusedCell.rowIndex &&
670
+ prePopupFocusedCell.current.column.getColId() == postPopupFocusedCell.column.getColId()
671
+ ) {
672
+ if (!tabDirection || !(await selectNextEditableCell(tabDirection))) {
673
+ onBulkEditingComplete();
674
+ }
675
+ } else {
676
+ onBulkEditingComplete();
677
+ }
678
+ })();
679
+
680
+ return ok;
681
+ });
682
+ } finally {
683
+ setSaving?.(false);
684
+ }
677
685
  },
678
- [gridApiOp, modifyUpdating, selectNextEditableCell],
686
+ [gridApiOp, modifyUpdating, onBulkEditingComplete, selectNextEditableCell],
679
687
  );
680
688
 
681
689
  const redrawRows: any = useMemo(
@@ -718,20 +726,28 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
718
726
  [gridApi],
719
727
  );
720
728
 
721
- const addExternalFilter = (filter: GridFilterExternal<TData>) => {
722
- externalFilters.current.push(filter);
723
- void onFilterChanged();
724
- };
729
+ const addExternalFilter = useCallback(
730
+ (filter: GridFilterExternal<TData>) => {
731
+ externalFilters.current.push(filter);
732
+ void onFilterChanged();
733
+ },
734
+ [onFilterChanged],
735
+ );
725
736
 
726
- const removeExternalFilter = (filter: GridFilterExternal<TData>) => {
727
- remove(externalFilters.current, (v) => v === filter);
728
- void onFilterChanged();
729
- };
737
+ const removeExternalFilter = useCallback(
738
+ (filter: GridFilterExternal<TData>) => {
739
+ remove(externalFilters.current, (v) => v === filter);
740
+ void onFilterChanged();
741
+ },
742
+ [onFilterChanged],
743
+ );
730
744
 
731
- const isExternalFilterPresent = (): boolean => !isEmpty(externalFilters.current);
745
+ const isExternalFilterPresent = useCallback((): boolean => !isEmpty(externalFilters.current), []);
732
746
 
733
- const doesExternalFilterPass = (node: IRowNode): boolean =>
734
- externalFilters.current.every((filter) => filter(node.data, node));
747
+ const doesExternalFilterPass = useCallback(
748
+ (node: IRowNode): boolean => externalFilters.current.every((filter) => filter(node.data, node)),
749
+ [],
750
+ );
735
751
 
736
752
  const getColDef = useCallback(
737
753
  (colId?: string): ColDef | undefined => (!!colId && gridApi?.getColumnDef(colId)) || undefined,
@@ -804,7 +820,6 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
804
820
  setInvisibleColumnIds,
805
821
  gridReady,
806
822
  prePopupOps,
807
- postPopupOps,
808
823
  setApis,
809
824
  setQuickFilter,
810
825
  selectRowsById,
@@ -825,8 +840,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
825
840
  sizeColumnsToFit,
826
841
  autoSizeColumns,
827
842
  startCellEditing,
828
- stopEditing,
829
- cancelEdit,
843
+ resetFocusedCellAfterCellEditing,
830
844
  updatingCells,
831
845
  redrawRows,
832
846
  externallySelectedItemsAreInSync,
@@ -837,7 +851,8 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
837
851
  isExternalFilterPresent,
838
852
  doesExternalFilterPass,
839
853
  downloadCsv,
840
- setOnCellEditingComplete,
854
+ onBulkEditingComplete,
855
+ setOnBulkEditingComplete,
841
856
  showNoRowsOverlay,
842
857
  }}
843
858
  >
@@ -894,3 +909,15 @@ export const downloadCsvUseValueFormattersProcessCellCallback = (params: Process
894
909
  // We add an extra encodeToString here just in case valueFormatter is returning non string values
895
910
  return encodeToString(result);
896
911
  };
912
+
913
+ const waitForCondition = async (condition: () => boolean, timeoutMs: number): Promise<boolean> => {
914
+ const endTime = Date.now() + timeoutMs;
915
+ while (Date.now() < endTime) {
916
+ if (condition()) {
917
+ return true;
918
+ }
919
+ await wait(100);
920
+ }
921
+ console.warn('waitForCondition failed');
922
+ return false;
923
+ };
@@ -6,6 +6,7 @@ export interface GridPopoverContextType<TData extends GridBaseRow> {
6
6
  anchorRef: RefObject<Element>;
7
7
  saving: boolean;
8
8
  setSaving: (saving: boolean) => void;
9
+ stopEditing: () => void;
9
10
  colId: string;
10
11
  field: keyof TData;
11
12
  value: any;
@@ -22,6 +23,7 @@ export const GridPopoverContext = createContext<GridPopoverContextType<any>>({
22
23
  anchorRef: { current: null },
23
24
  saving: false,
24
25
  setSaving: () => {},
26
+ stopEditing: () => {},
25
27
  colId: '',
26
28
  field: '',
27
29
  value: null,
@@ -19,7 +19,7 @@ export const GridPopoverContextProvider = (props2: PropsWithChildren<GridPopover
19
19
  const hasSaved = useRef(false);
20
20
  const [saving, setSaving] = useState(false);
21
21
 
22
- const { colDef } = props;
22
+ const { colDef, stopEditing, formatValue, data, value } = props;
23
23
  const { cellEditorParams } = colDef;
24
24
  const multiEdit = cellEditorParams?.multiEdit ?? false;
25
25
  // Then item that is clicked on will always be first in the list
@@ -34,7 +34,10 @@ export const GridPopoverContextProvider = (props2: PropsWithChildren<GridPopover
34
34
 
35
35
  const updateValue = useCallback(
36
36
  async (saveFn: (selectedRows: any[]) => Promise<boolean>, tabDirection: 1 | 0 | -1): Promise<boolean> => {
37
- if (hasSaved.current) return true;
37
+ if (hasSaved.current) {
38
+ // already called save so ignore
39
+ return true;
40
+ }
38
41
  hasSaved.current = true;
39
42
  return saving ? false : await updatingCells({ selectedRows, field }, saveFn, setSaving, tabDirection);
40
43
  },
@@ -50,10 +53,11 @@ export const GridPopoverContextProvider = (props2: PropsWithChildren<GridPopover
50
53
  selectedRows,
51
54
  colId,
52
55
  field,
53
- data: props.data,
54
- value: props.value,
56
+ data,
57
+ value,
55
58
  updateValue,
56
- formatValue: props.formatValue,
59
+ formatValue,
60
+ stopEditing,
57
61
  }}
58
62
  >
59
63
  {children}
@@ -1,6 +1,7 @@
1
1
  import { createContext } from 'react';
2
2
 
3
3
  export type GridUpdatingContextType = {
4
+ anyUpdating: () => boolean;
4
5
  checkUpdating: (fields: string | string[], id: number | string) => boolean;
5
6
  modifyUpdating: (
6
7
  fields: string | string[],
@@ -12,6 +13,10 @@ export type GridUpdatingContextType = {
12
13
  };
13
14
 
14
15
  export const GridUpdatingContext = createContext<GridUpdatingContextType>({
16
+ anyUpdating: () => {
17
+ console.error('Missing GridUpdatingContext');
18
+ return false;
19
+ },
15
20
  checkUpdating: () => {
16
21
  console.error('Missing GridUpdatingContext');
17
22
  return false;
@@ -1,5 +1,5 @@
1
- import { castArray, flatten, remove } from 'lodash-es';
2
- import { PropsWithChildren, useRef, useState } from 'react';
1
+ import { castArray, flatten, isEmpty, remove } from 'lodash-es';
2
+ import { PropsWithChildren, useCallback, useRef, useState } from 'react';
3
3
 
4
4
  import { GridUpdatingContext } from './GridUpdatingContext';
5
5
 
@@ -14,7 +14,7 @@ export const GridUpdatingContextProvider = (props: PropsWithChildren) => {
14
14
  const updating = useRef<GridUpdatingContextStatus>({});
15
15
  const [updatedDep, setUpdatedDep] = useState(0);
16
16
 
17
- const resetUpdating = () => {
17
+ const resetUpdating = useCallback(() => {
18
18
  const mergedUpdatingBlocks: GridUpdatingContextStatus = {};
19
19
  for (const key in updatingBlocks.current) {
20
20
  const arr = flatten(updatingBlocks.current[key]);
@@ -24,33 +24,40 @@ export const GridUpdatingContextProvider = (props: PropsWithChildren) => {
24
24
  }
25
25
  updating.current = mergedUpdatingBlocks;
26
26
  setUpdatedDep((updatedDep) => updatedDep + 1);
27
- };
28
- const updatingCols = () => Object.keys(updating.current);
29
-
30
- const modifyUpdating = async (
31
- fields: string | string[],
32
- ids: (number | string)[],
33
- fn: () => void | Promise<void>,
34
- ) => {
35
- const idRef = [...ids];
36
- castArray(fields).forEach((field) => {
37
- const fieldUpdatingIds = updatingBlocks.current[field] ?? (updatingBlocks.current[field] = []);
38
- fieldUpdatingIds.push(idRef);
39
- });
40
- resetUpdating();
41
- await fn();
42
- castArray(fields).forEach((field) => {
43
- const fieldUpdatingIds = updatingBlocks.current[field];
44
- remove(fieldUpdatingIds, (idList) => idList === idRef);
45
- });
46
- resetUpdating();
47
- };
48
-
49
- const checkUpdating = (fields: string | string[], id: number | string): boolean =>
50
- castArray(fields).some((f) => updating.current[f]?.includes(id));
27
+ }, []);
28
+
29
+ const updatingCols = useCallback(() => Object.keys(updating.current), []);
30
+
31
+ const modifyUpdating = useCallback(
32
+ async (fields: string | string[], ids: (number | string)[], fn: () => void | Promise<void>) => {
33
+ const idRef = [...ids];
34
+ castArray(fields).forEach((field) => {
35
+ const fieldUpdatingIds = updatingBlocks.current[field] ?? (updatingBlocks.current[field] = []);
36
+ fieldUpdatingIds.push(idRef);
37
+ });
38
+ resetUpdating();
39
+ await fn();
40
+ castArray(fields).forEach((field) => {
41
+ const fieldUpdatingIds = updatingBlocks.current[field];
42
+ remove(fieldUpdatingIds, (idList) => idList === idRef);
43
+ });
44
+ resetUpdating();
45
+ },
46
+ [resetUpdating],
47
+ );
48
+
49
+ const anyUpdating = useCallback((): boolean => {
50
+ return !isEmpty(updating.current);
51
+ }, []);
52
+
53
+ const checkUpdating = useCallback(
54
+ (fields: string | string[], id: number | string): boolean =>
55
+ castArray(fields).some((f) => updating.current[f]?.includes(id)),
56
+ [],
57
+ );
51
58
 
52
59
  return (
53
- <GridUpdatingContext.Provider value={{ modifyUpdating, checkUpdating, updatingCols, updatedDep }}>
60
+ <GridUpdatingContext.Provider value={{ modifyUpdating, anyUpdating, checkUpdating, updatingCols, updatedDep }}>
54
61
  {props.children}
55
62
  </GridUpdatingContext.Provider>
56
63
  );
@@ -12,3 +12,27 @@ export const usePrevious = <T,>(value: T): T | undefined => {
12
12
  }, [value]);
13
13
  return ref.current;
14
14
  };
15
+
16
+ /**
17
+ * Dump prop differences for components between renders.
18
+ */
19
+ export const usePropMonitor = (what: string, props: NonNullable<unknown>) => {
20
+ const prev = usePrevious(props);
21
+ useEffect(() => {
22
+ if (prev) {
23
+ let first = true;
24
+ Object.keys(props).forEach((p) => {
25
+ // @ts-expect-error any type
26
+ if (props[p] !== prev[p]) {
27
+ if (first) {
28
+ // eslint-disable-next-line no-console
29
+ console.log('--- props changed -----');
30
+ first = false;
31
+ }
32
+ // eslint-disable-next-line no-console
33
+ console.log(`${what} key: ${p} changed`);
34
+ }
35
+ });
36
+ }
37
+ }, [prev, props, what]);
38
+ };
@@ -370,6 +370,9 @@ export const MenuList = ({
370
370
  }
371
371
 
372
372
  const callback = debounce(() => {
373
+ if (!menuRef.current) {
374
+ return;
375
+ }
373
376
  const { width, height } = menuRef.current.ownerDocument.body.getBoundingClientRect();
374
377
  if (width === 0 || height === 0) return;
375
378
  if (
@@ -42,7 +42,7 @@ interface ITestRow {
42
42
  desc: string;
43
43
  }
44
44
 
45
- const GridFilterButtonsTemplate: StoryFn<typeof Grid> = (props: GridProps) => {
45
+ const GridFilterButtonsTemplate: StoryFn<typeof Grid<ITestRow>> = (props: GridProps<ITestRow>) => {
46
46
  const columnDefs: ColDefT<ITestRow>[] = useMemo(
47
47
  () => [
48
48
  GridCell({
@@ -52,7 +52,7 @@ interface ITestRow {
52
52
  desc: string;
53
53
  }
54
54
 
55
- const GridNonEditableRowTemplate: StoryFn<typeof Grid> = (props: GridProps) => {
55
+ const GridNonEditableRowTemplate: StoryFn<typeof Grid<ITestRow>> = (props: GridProps<ITestRow>) => {
56
56
  const [externalSelectedItems, setExternalSelectedItems] = useState<any[]>([]);
57
57
  const columnDefs: ColDefT<ITestRow>[] = useMemo(
58
58
  () => [
@@ -57,7 +57,7 @@ interface ITestPinnedRow {
57
57
  age: number;
58
58
  }
59
59
 
60
- const PinnedRowTemplate: StoryFn<typeof Grid> = (props: GridProps) => {
60
+ const PinnedRowTemplate: StoryFn<typeof Grid<ITestRow>> = (props: GridProps<ITestRow>) => {
61
61
  const columnDefs: ColDefT<ITestRow>[] = useMemo(
62
62
  () => [
63
63
  GridCell({