@linzjs/step-ag-grid 13.0.0 → 13.1.1

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 (33) hide show
  1. package/README.md +36 -1
  2. package/dist/GridTheme.scss +2 -2
  3. package/dist/index.css +26 -2
  4. package/dist/src/components/Grid.d.ts +2 -1
  5. package/dist/src/components/GridCell.d.ts +1 -1
  6. package/dist/src/components/gridFilter/GridFilterColumnsToggle.d.ts +5 -0
  7. package/dist/src/components/gridFilter/GridFilterHeaderIconButton.d.ts +11 -0
  8. package/dist/src/components/gridFilter/index.d.ts +3 -0
  9. package/dist/src/components/{GridFilter.d.ts → gridFilter/useGridFilter.d.ts} +2 -2
  10. package/dist/src/components/index.d.ts +0 -1
  11. package/dist/src/contexts/GridContext.d.ts +8 -3
  12. package/dist/src/contexts/GridContextProvider.d.ts +1 -1
  13. package/dist/step-ag-grid.esm.js +405 -238
  14. package/dist/step-ag-grid.esm.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/components/Grid.tsx +20 -14
  17. package/src/components/GridCell.tsx +1 -1
  18. package/src/components/gridFilter/GridFilterButtons.tsx +2 -2
  19. package/src/components/gridFilter/GridFilterColumnsToggle.tsx +141 -0
  20. package/src/components/gridFilter/GridFilterHeaderIconButton.tsx +33 -0
  21. package/src/components/gridFilter/GridFilterQuick.tsx +2 -2
  22. package/src/components/gridFilter/index.ts +3 -0
  23. package/src/components/{GridFilter.ts → gridFilter/useGridFilter.ts} +2 -2
  24. package/src/components/index.ts +0 -1
  25. package/src/contexts/GridContext.tsx +26 -5
  26. package/src/contexts/GridContextProvider.tsx +94 -29
  27. package/src/contexts/GridPopoverContextProvider.tsx +4 -3
  28. package/src/stories/grid/GridReadOnly.stories.tsx +13 -6
  29. package/src/styles/Grid.scss +18 -2
  30. package/src/styles/GridFilterColumnsToggle.scss +4 -0
  31. package/src/styles/GridFilterHeaderIconButton.scss +3 -0
  32. package/src/styles/GridTheme.scss +2 -2
  33. package/src/styles/index.scss +3 -0
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": "13.0.0",
5
+ "version": "13.1.1",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -11,7 +11,7 @@ import { GridContext } from "../contexts/GridContext";
11
11
  import { GridUpdatingContext } from "../contexts/GridUpdatingContext";
12
12
  import { fnOrVar, isNotEmpty } from "../utils/util";
13
13
  import { usePostSortRowsHook } from "./PostSortRowsHook";
14
- import { GridHeaderSelect } from "./gridHeader/GridHeaderSelect";
14
+ import { GridHeaderSelect } from "./gridHeader";
15
15
 
16
16
  export interface GridBaseRow {
17
17
  id: string | number;
@@ -27,6 +27,7 @@ export interface GridProps {
27
27
  defaultColDef?: GridOptions["defaultColDef"];
28
28
  columnDefs: ColDef[];
29
29
  rowData: GridOptions["rowData"];
30
+ noRowsOverlayText?: string;
30
31
  postSortRows?: GridOptions["postSortRows"];
31
32
  animateRows?: boolean;
32
33
  rowClassRules?: GridOptions["rowClassRules"];
@@ -39,10 +40,10 @@ export interface GridProps {
39
40
  /**
40
41
  * Wrapper for AgGrid to add commonly used functionality.
41
42
  */
42
- export const Grid = (params: GridProps): JSX.Element => {
43
+ export const Grid = ({ rowSelection = "multiple", "data-testid": dataTestId, ...params }: GridProps): JSX.Element => {
43
44
  const {
44
45
  gridReady,
45
- setGridApi,
46
+ setApis,
46
47
  prePopupOps,
47
48
  ensureRowVisible,
48
49
  selectRowsById,
@@ -79,7 +80,7 @@ export const Grid = (params: GridProps): JSX.Element => {
79
80
  externallySelectedItemsAreInSync,
80
81
  focusByRowById,
81
82
  gridReady,
82
- params,
83
+ params.externalSelectedItems,
83
84
  params.autoSelectFirstRow,
84
85
  params.rowData,
85
86
  selectRowsById,
@@ -178,11 +179,10 @@ export const Grid = (params: GridProps): JSX.Element => {
178
179
  maxWidth: 42,
179
180
  suppressSizeToFit: true,
180
181
  checkboxSelection: true,
181
- headerComponent: params.rowSelection === "multiple" ? GridHeaderSelect : null,
182
+ headerComponent: rowSelection === "multiple" ? GridHeaderSelect : null,
182
183
  suppressHeaderKeyboardEvent: (e) => {
183
184
  if ((e.event.key === "Enter" || e.event.key === " ") && !e.event.repeat) {
184
- const selectedNodeCount = e.api.getSelectedRows().length;
185
- if (selectedNodeCount == 0) {
185
+ if (isEmpty(e.api.getSelectedRows())) {
186
186
  e.api.selectAllFiltered();
187
187
  } else {
188
188
  e.api.deselectAll();
@@ -199,18 +199,18 @@ export const Grid = (params: GridProps): JSX.Element => {
199
199
  }, [
200
200
  params.columnDefs,
201
201
  params.selectable,
202
- params.rowSelection,
203
202
  params.readOnly,
204
203
  params.defaultColDef?.editable,
204
+ rowSelection,
205
205
  clickSelectorCheckboxWhenContainingCellClicked,
206
206
  ]);
207
207
 
208
208
  const onGridReady = useCallback(
209
209
  (event: GridReadyEvent) => {
210
- setGridApi(event.api);
210
+ setApis(event.api, event.columnApi, dataTestId);
211
211
  synchroniseExternallySelectedItemsToGrid();
212
212
  },
213
- [setGridApi, synchroniseExternallySelectedItemsToGrid],
213
+ [dataTestId, setApis, synchroniseExternallySelectedItemsToGrid],
214
214
  );
215
215
 
216
216
  const noRowsOverlayComponent = useCallback(
@@ -218,10 +218,16 @@ export const Grid = (params: GridProps): JSX.Element => {
218
218
  const hasData = (params.rowData?.length ?? 0) > 0;
219
219
  const hasFilteredData = event.api.getDisplayedRowCount() > 0;
220
220
  return (
221
- <span>{!hasData ? "There are currently no rows" : !hasFilteredData ? "All rows have been filtered" : ""}</span>
221
+ <span>
222
+ {!hasData
223
+ ? params.noRowsOverlayText ?? "There are currently no rows"
224
+ : !hasFilteredData
225
+ ? "All rows have been filtered"
226
+ : ""}
227
+ </span>
222
228
  );
223
229
  },
224
- [params.rowData?.length],
230
+ [params.noRowsOverlayText, params.rowData?.length],
225
231
  );
226
232
 
227
233
  const onModelUpdated = useCallback((event: ModelUpdatedEvent) => {
@@ -305,7 +311,7 @@ export const Grid = (params: GridProps): JSX.Element => {
305
311
 
306
312
  return (
307
313
  <div
308
- data-testid={params["data-testid"]}
314
+ data-testid={dataTestId}
309
315
  className={clsx(
310
316
  "Grid-container",
311
317
  "ag-theme-alpine",
@@ -319,7 +325,7 @@ export const Grid = (params: GridProps): JSX.Element => {
319
325
  rowClassRules={params.rowClassRules}
320
326
  getRowId={(params) => `${params.data.id}`}
321
327
  suppressRowClickSelection={true}
322
- rowSelection={params.rowSelection ?? "multiple"}
328
+ rowSelection={rowSelection}
323
329
  suppressBrowserResizeObserver={true}
324
330
  colResizeDefault={"shift"}
325
331
  onFirstDataRendered={sizeColumnsToFit}
@@ -14,7 +14,7 @@ import {
14
14
  GenericCellRendererParams,
15
15
  RowValueFormatterParams,
16
16
  RowValueGetterParams,
17
- } from "./gridRender/GridRenderGenericCell";
17
+ } from "./gridRender";
18
18
 
19
19
  export interface GenericCellEditorProps<E> {
20
20
  multiEdit?: boolean;
@@ -6,7 +6,7 @@ import { LuiButtonProps } from "@linzjs/lui/dist/components/LuiButton/LuiButton"
6
6
 
7
7
  import { GridFilterExternal } from "../../contexts/GridContext";
8
8
  import { GridBaseRow } from "../Grid";
9
- import { useGridFilter } from "../GridFilter";
9
+ import { useGridFilter } from "./useGridFilter";
10
10
 
11
11
  export interface GridFilterButtonsOption<RowType extends GridBaseRow> {
12
12
  defaultSelected?: boolean;
@@ -32,7 +32,7 @@ export const GridFilterButtons = <RowType extends GridBaseRow>({
32
32
  useGridFilter(filter);
33
33
 
34
34
  return (
35
- <div className={clsx("lui-margin-top-xxs lui-margin-bottom-xxs", className)}>
35
+ <div className={clsx(className, "flex-col-center")}>
36
36
  <LuiButtonGroup>
37
37
  {options.map((option, index) => (
38
38
  <LuiButton
@@ -0,0 +1,141 @@
1
+ import { isEmpty } from "lodash-es";
2
+ import { useCallback, useContext, useEffect, useMemo, useState } from "react";
3
+
4
+ import { LuiCheckboxInput, LuiIcon } from "@linzjs/lui";
5
+
6
+ import { GridContext } from "../../contexts/GridContext";
7
+ import { Menu, MenuDivider, MenuItem } from "../../react-menu3";
8
+ import { ClickEvent } from "../../react-menu3/types";
9
+ import { GridBaseRow } from "../Grid";
10
+ import { ColDefT } from "../GridCell";
11
+ import { GridFilterHeaderIconButton } from "./GridFilterHeaderIconButton";
12
+
13
+ export interface GridFilterColumnsToggleProps {
14
+ saveState?: boolean;
15
+ }
16
+
17
+ export const GridFilterColumnsToggle = ({ saveState = true }: GridFilterColumnsToggleProps): JSX.Element => {
18
+ const [loaded, setLoaded] = useState(false);
19
+ const { getColumns, invisibleColumnIds, setInvisibleColumnIds } = useContext(GridContext);
20
+
21
+ const columnStorageKey = useMemo(
22
+ () =>
23
+ isEmpty(getColumns())
24
+ ? null // Grid hasn't been initialised yet
25
+ : "stepAgGrid_invisibleColumnIds_" +
26
+ getColumns()
27
+ .map((col) => col.colId || "")
28
+ .join("_"),
29
+ [getColumns],
30
+ );
31
+
32
+ // Load state on start
33
+ useEffect(() => {
34
+ if (!columnStorageKey || loaded) return;
35
+ if (saveState) {
36
+ try {
37
+ const stored = window.localStorage.getItem(columnStorageKey);
38
+ const invisibleIds = JSON.parse(stored ?? "[]");
39
+ if (!Array.isArray(invisibleIds)) {
40
+ console.error(`stored invisible ids not an array: ${stored}`);
41
+ } else if (!invisibleIds.every((id) => typeof id === "string")) {
42
+ console.error(`stored invisible ids not strings: ${stored}`);
43
+ } else {
44
+ invisibleIds && setInvisibleColumnIds(invisibleIds);
45
+ }
46
+ } catch (ex) {
47
+ console.error(ex);
48
+ }
49
+ setLoaded(true);
50
+ }
51
+ }, [columnStorageKey, loaded, saveState, setInvisibleColumnIds]);
52
+
53
+ // Save state on column visibility change
54
+ useEffect(() => {
55
+ loaded &&
56
+ columnStorageKey &&
57
+ saveState &&
58
+ window.localStorage.setItem(columnStorageKey, JSON.stringify(invisibleColumnIds));
59
+ }, [columnStorageKey, invisibleColumnIds, loaded, saveState]);
60
+
61
+ const toggleColumn = useCallback(
62
+ (colId?: string) => {
63
+ if (!colId) return;
64
+ setInvisibleColumnIds(
65
+ invisibleColumnIds.includes(colId)
66
+ ? invisibleColumnIds.filter((id) => id !== colId)
67
+ : [...invisibleColumnIds, colId],
68
+ );
69
+ },
70
+ [invisibleColumnIds, setInvisibleColumnIds],
71
+ );
72
+
73
+ const resetColumns = () => {
74
+ setInvisibleColumnIds([]);
75
+ };
76
+
77
+ const numericRegExp = /^\d+$/;
78
+ const isNonManageableColumn = (col: ColDefT<GridBaseRow>) => {
79
+ return col.lockVisible || col.colId == null || numericRegExp.test(col.colId);
80
+ };
81
+
82
+ return (
83
+ <Menu
84
+ menuButton={<GridFilterHeaderIconButton icon={"ic_columns"} title={"Column visibility"} />}
85
+ menuClassName={"step-ag-grid-react-menu"}
86
+ portal={true}
87
+ unmountOnClose={true}
88
+ >
89
+ <div className={"GridFilterColumnsToggle-container"}>
90
+ {getColumns()
91
+ .filter((col) => !!col.headerName)
92
+ .map((col) => (
93
+ <MenuItem
94
+ key={col.colId}
95
+ disabled={col.lockVisible}
96
+ onClick={(e: ClickEvent) => {
97
+ // Global react-menu MenuItem handler handles tabs
98
+ if (e.key !== "Tab") {
99
+ e.keepOpen = true;
100
+ if (e.key !== "Enter") {
101
+ toggleColumn(col.colId);
102
+ }
103
+ }
104
+ }}
105
+ >
106
+ <LuiCheckboxInput
107
+ isChecked={!invisibleColumnIds.includes(col.colId ?? "")}
108
+ value={`${col.colId}`}
109
+ label={col.headerName ?? ""}
110
+ isDisabled={isNonManageableColumn(col)}
111
+ inputProps={{
112
+ onClick: (e) => {
113
+ // Click is handled by MenuItem onClick so keyboard events work
114
+ e.preventDefault();
115
+ e.stopPropagation();
116
+ },
117
+ }}
118
+ onChange={() => {
119
+ /*Do nothing, change handled by menuItem*/
120
+ }}
121
+ />
122
+ </MenuItem>
123
+ ))}
124
+ </div>
125
+ <MenuDivider key={`$$divider_reset_columns`} />
126
+ <MenuItem
127
+ key={"$$reset_columns"}
128
+ onClick={(e: ClickEvent) => {
129
+ // Global react-menu MenuItem handler handles tabs
130
+ if (e.key !== "Tab") {
131
+ e.keepOpen = e.key !== "Enter";
132
+ resetColumns();
133
+ }
134
+ }}
135
+ >
136
+ <LuiIcon name={"ic_regenerate"} alt={"Reset columns"} size={"md"} className={"MenuItem-icon"} />
137
+ Reset columns
138
+ </MenuItem>
139
+ </Menu>
140
+ );
141
+ };
@@ -0,0 +1,33 @@
1
+ import { ButtonHTMLAttributes, MouseEvent, forwardRef } from "react";
2
+
3
+ import { LuiButton, LuiIcon } from "@linzjs/lui";
4
+ import { IconName, IconSize } from "@linzjs/lui/dist/components/LuiIcon/LuiIcon";
5
+
6
+ export interface GridFilterHeaderIconButtonProps {
7
+ icon: IconName;
8
+ onClick?: (e: MouseEvent) => void;
9
+ buttonProps?: Partial<ButtonHTMLAttributes<HTMLButtonElement>>;
10
+ disabled?: boolean;
11
+ size?: IconSize;
12
+ title: string;
13
+ }
14
+
15
+ export const GridFilterHeaderIconButton = forwardRef<HTMLButtonElement, GridFilterHeaderIconButtonProps>(
16
+ function columnsButton({ icon, title, onClick, buttonProps, disabled = false, size = "md" }, ref) {
17
+ return (
18
+ <LuiButton
19
+ {...buttonProps}
20
+ type={"button"}
21
+ level={"tertiary"}
22
+ className={"GridFilterHeaderIconButton lui-button-icon-only"}
23
+ ref={ref}
24
+ aria-label={title}
25
+ title={title}
26
+ onClick={onClick}
27
+ disabled={disabled}
28
+ >
29
+ <LuiIcon name={icon} alt={"Menu"} size={size} />
30
+ </LuiButton>
31
+ );
32
+ },
33
+ );
@@ -18,11 +18,11 @@ export const GridFilterQuick = ({ quickFilterPlaceholder, defaultValue }: GridFi
18
18
  return (
19
19
  <input
20
20
  aria-label="Search"
21
- className="lui-margin-top-xxs lui-margin-bottom-xxs Grid-quickFilterBox"
21
+ className="Grid-quickFilterBox"
22
22
  type="text"
23
23
  placeholder={quickFilterPlaceholder ?? "Search..."}
24
24
  value={quickFilterValue}
25
- onChange={(event): void => {
25
+ onChange={(event) => {
26
26
  setQuickFilterValue(event.target.value);
27
27
  }}
28
28
  />
@@ -1,3 +1,6 @@
1
+ export * from "./GridFilterHeaderIconButton";
2
+ export * from "./GridFilterColumnsToggle";
1
3
  export * from "./GridFilterButtons";
2
4
  export * from "./GridFilterQuick";
3
5
  export * from "./GridFilters";
6
+ export * from "./useGridFilter";
@@ -1,7 +1,7 @@
1
1
  import { useContext, useEffect } from "react";
2
2
 
3
- import { GridContext, GridFilterExternal } from "../contexts/GridContext";
4
- import { GridBaseRow } from "./Grid";
3
+ import { GridContext, GridFilterExternal } from "../../contexts/GridContext";
4
+ import { GridBaseRow } from "../Grid";
5
5
 
6
6
  export const useGridFilter = <RowType extends GridBaseRow>(filter: GridFilterExternal<RowType> | undefined) => {
7
7
  const { addExternalFilter, removeExternalFilter } = useContext(GridContext);
@@ -4,7 +4,6 @@ export * from "./GridCell";
4
4
  export * from "./GridCellMultiEditor";
5
5
  export * from "./GridCellMultiSelectClassRules";
6
6
  export * from "./gridFilter";
7
- export * from "./GridFilter";
8
7
  export * from "./gridForm";
9
8
  export * from "./gridHeader";
10
9
  export * from "./GridIcon";
@@ -1,18 +1,20 @@
1
- import { GridApi, RowNode } from "ag-grid-community";
1
+ import { ColumnApi, GridApi, RowNode } from "ag-grid-community";
2
2
  import { createContext, useContext } from "react";
3
3
 
4
- import { GridBaseRow } from "../components/Grid";
4
+ import { ColDefT, GridBaseRow } from "../components";
5
5
 
6
6
  export type GridFilterExternal<RowType extends GridBaseRow> = (data: RowType, rowNode: RowNode) => boolean;
7
7
 
8
8
  export interface GridContextType<RowType extends GridBaseRow> {
9
9
  gridReady: boolean;
10
- setGridApi: (gridApi: GridApi | undefined) => void;
10
+ setApis: (gridApi: GridApi | undefined, columnApi: ColumnApi | undefined, dataTestId?: string) => void;
11
11
  prePopupOps: () => void;
12
12
  setQuickFilter: (quickFilter: string) => void;
13
13
  editingCells: () => boolean;
14
14
  getSelectedRows: <T extends GridBaseRow>() => T[];
15
+ getFilteredSelectedRows: <T extends GridBaseRow>() => T[];
15
16
  getSelectedRowIds: () => number[];
17
+ getFilteredSelectedRowIds: () => number[];
16
18
  selectRowsDiff: (updateFn: () => Promise<any>) => Promise<void>;
17
19
  selectRowsWithFlashDiff: (updateFn: () => Promise<any>) => Promise<void>;
18
20
  selectRowsById: (rowIds?: number[]) => void;
@@ -38,16 +40,27 @@ export interface GridContextType<RowType extends GridBaseRow> {
38
40
  removeExternalFilter: (filter: GridFilterExternal<RowType>) => void;
39
41
  isExternalFilterPresent: () => boolean;
40
42
  doesExternalFilterPass: (node: RowNode) => boolean;
43
+ getColumns: () => ColDefT<RowType>[];
44
+ invisibleColumnIds: string[];
45
+ setInvisibleColumnIds: (colIds: string[]) => void;
41
46
  }
42
47
 
43
48
  export const GridContext = createContext<GridContextType<any>>({
44
49
  gridReady: false,
50
+ getColumns: () => {
51
+ console.error("no context provider for getColumns");
52
+ return [];
53
+ },
54
+ invisibleColumnIds: [],
55
+ setInvisibleColumnIds: () => {
56
+ console.error("no context provider for setInvisibleColumnIds");
57
+ },
45
58
  prePopupOps: () => {
46
59
  console.error("no context provider for prePopupOps");
47
60
  },
48
61
  externallySelectedItemsAreInSync: false,
49
- setGridApi: () => {
50
- console.error("no context provider for setGridApi");
62
+ setApis: () => {
63
+ console.error("no context provider for setApis");
51
64
  },
52
65
  setQuickFilter: () => {
53
66
  console.error("no context provider for setQuickFilter");
@@ -59,10 +72,18 @@ export const GridContext = createContext<GridContextType<any>>({
59
72
  console.error("no context provider for getSelectedRows");
60
73
  return [];
61
74
  },
75
+ getFilteredSelectedRows: <T extends unknown>(): T[] => {
76
+ console.error("no context provider for getFilteredSelectedRows");
77
+ return [];
78
+ },
62
79
  getSelectedRowIds: () => {
63
80
  console.error("no context provider for getSelectedRowIds");
64
81
  return [];
65
82
  },
83
+ getFilteredSelectedRowIds: () => {
84
+ console.error("no context provider for getFilteredSelectedRowIds");
85
+ return [];
86
+ },
66
87
  selectRowsDiff: async () => {
67
88
  console.error("no context provider for selectRowsDiff");
68
89
  },
@@ -1,9 +1,9 @@
1
- import { ColDef, GridApi, RowNode } from "ag-grid-community";
1
+ import { ColDef, ColumnApi, GridApi, RowNode } from "ag-grid-community";
2
2
  import { CellPosition } from "ag-grid-community/dist/lib/entities/cellPosition";
3
- import { debounce, defer, delay, difference, isEmpty, last, remove, sortBy } from "lodash-es";
3
+ import { compact, debounce, defer, delay, difference, isEmpty, last, remove, sortBy } from "lodash-es";
4
4
  import { ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
5
 
6
- import { GridBaseRow } from "../components/Grid";
6
+ import { ColDefT, GridBaseRow } from "../components";
7
7
  import { isNotEmpty, wait } from "../utils/util";
8
8
  import { GridContext, GridFilterExternal } from "./GridContext";
9
9
  import { GridUpdatingContext } from "./GridUpdatingContext";
@@ -19,9 +19,12 @@ interface GridContextProps {
19
19
  */
20
20
  export const GridContextProvider = <RowType extends GridBaseRow>(props: GridContextProps): ReactElement => {
21
21
  const { modifyUpdating } = useContext(GridUpdatingContext);
22
- const [gridApi, _setGridApi] = useState<GridApi>();
22
+ const [gridApi, setGridApi] = useState<GridApi>();
23
+ const [columnApi, setColumnApi] = useState<ColumnApi>();
23
24
  const [gridReady, setGridReady] = useState(false);
24
25
  const [quickFilter, setQuickFilter] = useState("");
26
+ const [invisibleColumnIds, setInvisibleColumnIds] = useState<string[]>([]);
27
+ const testId = useRef<string | undefined>();
25
28
  const idsBeforeUpdate = useRef<number[]>([]);
26
29
  const prePopupFocusedCell = useRef<CellPosition>();
27
30
  const [externallySelectedItemsAreInSync, setExternallySelectedItemsAreInSync] = useState(false);
@@ -34,18 +37,6 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
34
37
  gridApi?.setQuickFilter(quickFilter);
35
38
  }, [gridApi, quickFilter]);
36
39
 
37
- /**
38
- * Set the grid api when the grid is ready.
39
- */
40
- const setGridApi = useCallback(
41
- (gridApi: GridApi | undefined) => {
42
- _setGridApi(gridApi);
43
- gridApi?.setQuickFilter(quickFilter);
44
- setGridReady(!!gridApi);
45
- },
46
- [quickFilter],
47
- );
48
-
49
40
  /**
50
41
  * Wraps things that require gridApi in common handling, for when gridApi not present.
51
42
  *
@@ -62,6 +53,54 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
62
53
  [gridApi],
63
54
  );
64
55
 
56
+ /**
57
+ * Scroll row into view by Id.
58
+ *
59
+ * @param id Id to scroll into view
60
+ * @return true if row is found, else false
61
+ */
62
+ const ensureRowVisible = useCallback(
63
+ (id: number | string): boolean => {
64
+ return gridApiOp((gridApi) => {
65
+ const node = gridApi.getRowNode(`${id}`);
66
+ if (!node) return false;
67
+ gridApi.ensureNodeVisible(node);
68
+ return true;
69
+ });
70
+ },
71
+ [gridApiOp],
72
+ );
73
+
74
+ /**
75
+ * Set the grid api when the grid is ready.
76
+ */
77
+ const setApis = useCallback(
78
+ (gridApi: GridApi | undefined, columnApi: ColumnApi | undefined, dataTestId?: string) => {
79
+ testId.current = dataTestId;
80
+ setGridApi(gridApi);
81
+ setColumnApi(columnApi);
82
+ gridApi?.setQuickFilter(quickFilter);
83
+ setGridReady(!!gridApi);
84
+ },
85
+ [quickFilter],
86
+ );
87
+
88
+ /**
89
+ * Expose scrollRowIntoView for playwright tests.
90
+ */
91
+ useEffect(() => {
92
+ const globalSupport = (window as any).__stepAgGrid || ((window as any).__stepAgGrid = { grids: {} });
93
+ if (testId.current) {
94
+ globalSupport.grids[testId.current] = {
95
+ scrollRowIntoViewById: (rowId: number | string) => {
96
+ if (!ensureRowVisible(rowId)) {
97
+ throw `scrollRowIntoView failed on grid '${testId.current}' as row with id: '${rowId}' was not found`;
98
+ }
99
+ },
100
+ };
101
+ }
102
+ }, [ensureRowVisible]);
103
+
65
104
  /**
66
105
  * Before a popup record the currently focused cell.
67
106
  */
@@ -247,11 +286,26 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
247
286
  );
248
287
  }, [gridApiOp]);
249
288
 
289
+ const getFilteredSelectedRows = useCallback(<T extends unknown>(): T[] => {
290
+ return gridApiOp((gridApi) => {
291
+ const rowData: T[] = [];
292
+ gridApi.forEachNodeAfterFilter((node) => {
293
+ if (node.isSelected()) rowData.push(node.data);
294
+ });
295
+ return rowData;
296
+ });
297
+ }, [gridApiOp]);
298
+
250
299
  const getSelectedRowIds = useCallback(
251
300
  (): number[] => getSelectedRows().map((row) => (row as any).id as number),
252
301
  [getSelectedRows],
253
302
  );
254
303
 
304
+ const getFilteredSelectedRowIds = useCallback(
305
+ (): number[] => getFilteredSelectedRows().map((row) => (row as any).id as number),
306
+ [getFilteredSelectedRows],
307
+ );
308
+
255
309
  const editingCells = useCallback((): boolean => {
256
310
  return gridApiOp(
257
311
  (gridApi) => isNotEmpty(gridApi.getEditingCells()),
@@ -259,18 +313,6 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
259
313
  );
260
314
  }, [gridApiOp]);
261
315
 
262
- const ensureRowVisible = useCallback(
263
- (id: number | string): boolean => {
264
- return gridApiOp((gridApi) => {
265
- const node = gridApi.getRowNode(`${id}`);
266
- if (!node) return false;
267
- gridApi.ensureNodeVisible(node);
268
- return true;
269
- });
270
- },
271
- [gridApiOp],
272
- );
273
-
274
316
  /**
275
317
  * Scroll last selected row into view on grid sort change
276
318
  */
@@ -407,12 +449,33 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
407
449
  return externalFilters.current.every((filter) => filter(node.data, node));
408
450
  };
409
451
 
452
+ const getColumns: () => ColDefT<RowType>[] = useCallback(() => gridApi?.getColumnDefs() ?? [], [gridApi]);
453
+
454
+ useEffect(() => {
455
+ if (columnApi) {
456
+ // show all columns that aren't invisible
457
+ columnApi.setColumnsVisible(
458
+ compact(
459
+ getColumns()
460
+ .filter((col) => !col.lockVisible && col.colId && !invisibleColumnIds.includes(col.colId))
461
+ .map((col) => col.colId),
462
+ ),
463
+ true,
464
+ );
465
+ // hide all invisible columns
466
+ columnApi.setColumnsVisible(invisibleColumnIds, false);
467
+ }
468
+ }, [invisibleColumnIds, columnApi, getColumns]);
469
+
410
470
  return (
411
471
  <GridContext.Provider
412
472
  value={{
473
+ getColumns,
474
+ invisibleColumnIds,
475
+ setInvisibleColumnIds,
413
476
  gridReady,
414
477
  prePopupOps,
415
- setGridApi,
478
+ setApis,
416
479
  setQuickFilter,
417
480
  selectRowsById,
418
481
  selectRowsDiff,
@@ -422,7 +485,9 @@ export const GridContextProvider = <RowType extends GridBaseRow>(props: GridCont
422
485
  flashRowsDiff,
423
486
  focusByRowById,
424
487
  getSelectedRows,
488
+ getFilteredSelectedRows,
425
489
  getSelectedRowIds,
490
+ getFilteredSelectedRowIds,
426
491
  editingCells,
427
492
  ensureRowVisible,
428
493
  ensureSelectedRowIsVisible,
@@ -12,7 +12,7 @@ interface GridPopoverContextProps {
12
12
  }
13
13
 
14
14
  export const GridPopoverContextProvider = ({ props, children }: GridPopoverContextProps) => {
15
- const { getSelectedRows, updatingCells } = useContext(GridContext);
15
+ const { getFilteredSelectedRows, updatingCells } = useContext(GridContext);
16
16
  const anchorRef = useRef<Element>(props.eGridCell);
17
17
 
18
18
  const hasSaved = useRef(false);
@@ -23,8 +23,9 @@ export const GridPopoverContextProvider = ({ props, children }: GridPopoverConte
23
23
  const multiEdit = cellEditorParams?.multiEdit ?? false;
24
24
  // Then item that is clicked on will always be first in the list
25
25
  const selectedRows = useMemo(
26
- () => (multiEdit ? sortBy(getSelectedRows(), (row) => row.id !== props.data.id) : [props.data as GridBaseRow]),
27
- [getSelectedRows, multiEdit, props.data],
26
+ () =>
27
+ multiEdit ? sortBy(getFilteredSelectedRows(), (row) => row.id !== props.data.id) : [props.data as GridBaseRow],
28
+ [getFilteredSelectedRows, multiEdit, props.data],
28
29
  );
29
30
  const field = props.colDef?.field ?? "";
30
31