@linzjs/step-ag-grid 32.1.1 → 32.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": "32.1.1",
5
+ "version": "32.2.0",
6
6
  "keywords": [
7
7
  "aggrid",
8
8
  "ag-grid",
@@ -27,7 +27,7 @@
27
27
  "node": ">=22"
28
28
  },
29
29
  "peerDependencies": {
30
- "@linzjs/lui": ">=21",
30
+ "@linzjs/lui": ">=24",
31
31
  "lodash-es": ">=4",
32
32
  "react": ">=18",
33
33
  "react-dom": ">=18"
@@ -46,7 +46,7 @@
46
46
  "build": "node --eval \"fs.mkdirSync('./dist', { recursive: true })\" && run-s clean lintall css bundle",
47
47
  "yalc": "run-s clean css bundle && yalc push",
48
48
  "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
49
- "bundle": "rollup -c",
49
+ "bundle": "tsc -p tsconfig.json && rollup -c",
50
50
  "css": "sass ./src/styles/index.scss:dist/index.css --no-source-map",
51
51
  "test": "cross-env vitest run",
52
52
  "test:watch": "vitest --watch",
@@ -59,7 +59,7 @@
59
59
  "build-storybook": "storybook build",
60
60
  "deploy-storybook": "npx --yes -p @storybook/storybook-deployer storybook-to-ghpages",
61
61
  "chromatic": "chromatic --exit-zero-on-changes",
62
- "upgrade": "npx ncu -i --format group",
62
+ "upgrade": "npx npm-check-updates -i --format group -c 1d",
63
63
  "prepare": "husky"
64
64
  },
65
65
  "browserslist": {
@@ -75,50 +75,49 @@
75
75
  ]
76
76
  },
77
77
  "devDependencies": {
78
- "@chromatic-com/storybook": "^5.2.1",
79
- "@linzjs/lui": "^24.17.0",
80
- "@linzjs/windows": "^11.2.0",
78
+ "@chromatic-com/storybook": "^5.3.0",
79
+ "@linzjs/lui": "^24.20.0",
80
+ "@linzjs/windows": "^13.0.0",
81
81
  "@rollup/plugin-commonjs": "^29.0.3",
82
82
  "@rollup/plugin-json": "^6.1.0",
83
83
  "@rollup/plugin-node-resolve": "^16.0.3",
84
- "@storybook/addon-docs": "^10.4.6",
85
- "@storybook/addon-links": "^10.4.6",
86
- "@storybook/react": "^10.4.6",
87
- "@storybook/react-vite": "^10.4.6",
84
+ "rollup-plugin-esbuild": "^6.2.1",
85
+ "@storybook/addon-docs": "^10.5.7",
86
+ "@storybook/addon-links": "^10.5.7",
87
+ "@storybook/react-vite": "^10.5.7",
88
88
  "@testing-library/dom": "^10.4.1",
89
89
  "@testing-library/react": "^16.3.2",
90
- "@testing-library/user-event": "^14.6.1",
90
+ "@testing-library/user-event": "^14.6.3",
91
91
  "@types/debounce-promise": "^3.1.9",
92
92
  "@types/jest": "^30.0.0",
93
93
  "@types/jsdom": "^28.0.3",
94
94
  "@types/lodash-es": "^4.17.12",
95
- "@types/node": "^24.13.2",
96
- "@types/react": "^18.3.27",
95
+ "@types/node": "^24.13.3",
96
+ "@types/react": "^18.3.31",
97
97
  "@types/react-dom": "^18.3.7",
98
- "@vitejs/plugin-react": "^6.0.3",
99
- "@vitest/ui": "^4.1.9",
100
- "chromatic": "^17.6.0",
98
+ "@vitejs/plugin-react": "^6.0.5",
99
+ "@vitest/ui": "^4.1.10",
100
+ "chromatic": "^17.8.0",
101
101
  "cross-env": "^10.1.0",
102
102
  "husky": "^9.1.7",
103
103
  "jsdom": "29.1.1",
104
104
  "lodash-es": "^4.18.1",
105
105
  "npm-run-all": "^4.1.5",
106
- "oxfmt": "^0.56.0",
107
- "oxlint": "^1.71.0",
108
- "postcss": "^8.5.15",
106
+ "oxfmt": "^0.62.0",
107
+ "oxlint": "^1.77.0",
108
+ "postcss": "^8.5.26",
109
109
  "postcss-scss": "^4.0.9",
110
110
  "react": ">=19",
111
- "react-dom": "^19.2.7",
112
- "rollup": "^4.62.2",
111
+ "react-dom": "^19.2.8",
112
+ "rollup": "^4.62.4",
113
113
  "rollup-plugin-copy": "^3.5.0",
114
114
  "rollup-plugin-peer-deps-external": "^2.2.4",
115
115
  "rollup-plugin-postcss": "^4.0.2",
116
- "rollup-plugin-typescript2": "^0.37.0",
117
- "sass": "^1.101.0",
118
- "storybook": "^10.4.6",
119
- "typescript": "^6.0.3",
120
- "vite": "^8.1.0",
121
- "vitest": "^4.1.9"
116
+ "sass": "^1.102.0",
117
+ "storybook": "^10.5.7",
118
+ "typescript": "^7.0.2",
119
+ "vite": "^8.2.1",
120
+ "vitest": "^4.1.10"
122
121
  },
123
122
  "husky": {
124
123
  "hooks": {
@@ -1106,6 +1106,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
1106
1106
  <div style={{ flex: 1 }} ref={gridDivRef} onCopy={onCopyEvent}>
1107
1107
  <AgGridReact
1108
1108
  theme={'legacy'}
1109
+ tooltipShowDelay={500}
1109
1110
  domLayout={params.domLayout}
1110
1111
  defaultColDef={defaultColDef}
1111
1112
  columnDefs={columnDefsAdjusted}
@@ -2,6 +2,7 @@ import { CellFocusedEvent, IRowNode } from 'ag-grid-community';
2
2
  import { CustomCellEditorProps } from 'ag-grid-react';
3
3
  import clsx from 'clsx';
4
4
  import { useCallback, useEffect, useRef } from 'react';
5
+ import { isEmpty } from 'lodash-es';
5
6
 
6
7
  import { useGridContext } from '../../contexts/GridContext';
7
8
  import { fnOrVar } from '../../utils/util';
@@ -11,10 +12,11 @@ import { GenericCellColDef } from '../gridRender';
11
12
  import { ColDefT, GridBaseRow } from '../types';
12
13
  import { useSharedInterval } from '../../utils/useSharedInterval';
13
14
 
14
- const BooleanCellRenderer = (props: CustomCellEditorProps) => {
15
+ const BooleanCellRenderer = <TData extends GridBaseRow>(props: CustomCellEditorProps) => {
15
16
  const { onValueChange, value, api, node, column, colDef, data, eGridCell } = props;
16
17
  const inputRef = useRef<HTMLInputElement>(null);
17
- const { updatingCells, redrawRows, resetFocusedCellAfterCellEditing, prePopupOps } = useGridContext();
18
+ const { updatingCells, redrawRows, resetFocusedCellAfterCellEditing, getSelectedRows, prePopupOps } =
19
+ useGridContext<TData>();
18
20
 
19
21
  useEffect(() => {
20
22
  const checkFocus = (event: CellFocusedEvent) => {
@@ -38,9 +40,26 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
38
40
  return;
39
41
  }
40
42
 
43
+ const multiEdit = params.multiEdit ?? false;
44
+ let selectedRows: TData[] = [];
45
+ if (multiEdit) {
46
+ // Sort clicked row to top among all selected rows
47
+ selectedRows = getSelectedRows<TData>().sort((a, b) => {
48
+ const aa = a.id === data.id;
49
+ const bb = b.id === data.id;
50
+ if (aa && !bb) return -1;
51
+ if (!aa && bb) return 1;
52
+ return 0;
53
+ });
54
+ }
55
+ if (isEmpty(selectedRows)) {
56
+ selectedRows = [data as TData];
57
+ }
58
+ const selectedRowIds = selectedRows.map((row) => row.id);
59
+
41
60
  const nodes: IRowNode[] = [];
42
61
  api.forEachNode((n) => {
43
- if (n.data.id === data.id) {
62
+ if (selectedRowIds.includes(n.data.id)) {
44
63
  nodes.push(n);
45
64
  }
46
65
  });
@@ -50,7 +69,6 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
50
69
  const checked = !value;
51
70
  onValueChange(checked);
52
71
  const field = props.colDef?.field ?? props.colDef?.colId ?? '';
53
- const selectedRows = nodes.map((node) => node.data);
54
72
  void updatingCells({ selectedRows, field }, async () => {
55
73
  redrawRows(nodes);
56
74
  return params.onClick({ selectedRows, selectedRowIds: selectedRows.map((r) => r.id), checked });
@@ -59,12 +77,13 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
59
77
  });
60
78
  }, [
61
79
  api,
62
- data.id,
80
+ data,
63
81
  onValueChange,
64
82
  prePopupOps,
65
83
  props.colDef?.cellEditorParams,
66
84
  props.colDef?.colId,
67
85
  props.colDef?.field,
86
+ getSelectedRows,
68
87
  redrawRows,
69
88
  resetFocusedCellAfterCellEditing,
70
89
  updatingCells,
@@ -112,6 +131,7 @@ const BooleanCellRenderer = (props: CustomCellEditorProps) => {
112
131
  };
113
132
 
114
133
  export interface GridEditBooleanEditorProps<TData extends GridBaseRow> extends CellEditorCommon {
134
+ multiEdit?: boolean;
115
135
  onClick: (props: {
116
136
  selectedRows: TData[];
117
137
  selectedRowIds: TData['id'][];
@@ -0,0 +1 @@
1
+ declare module '*.scss';
@@ -1,8 +1,3 @@
1
- import '../../styles/GridTheme.scss';
2
- import '../../styles/index.scss';
3
- import '@linzjs/lui/dist/scss/base.scss';
4
- import '@linzjs/lui/dist/fonts';
5
-
6
1
  import { Meta, StoryFn } from '@storybook/react-vite';
7
2
  import { useMemo, useState } from 'react';
8
3
 
@@ -16,12 +11,17 @@ import {
16
11
  GridPopoverEditDropDown,
17
12
  GridUpdatingContextProvider,
18
13
  primitiveToSelectOption,
19
- wait,
20
14
  } from '../..';
21
15
  import { expect, userEvent, waitFor } from 'storybook/test';
22
16
 
23
17
  import { waitForGridReady } from '../../utils/__tests__/storybookTestUtil';
24
- import { IFormTestRow } from './FormTest';
18
+
19
+ interface IBoolTestRow {
20
+ id: number;
21
+ name: string;
22
+ boldSingleSelect: boolean;
23
+ boldMultiSelect: boolean;
24
+ }
25
25
 
26
26
  export default {
27
27
  title: 'Components / Grids',
@@ -41,11 +41,11 @@ export default {
41
41
 
42
42
  const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
43
43
  const [rowData, setRowData] = useState([
44
- { id: 1000, name: 'IS IS DP12345', nameType: 'IS', numba: 'IX', plan: 'DP 12345', bold: true },
45
- { id: 1001, name: 'PEG V SD523', nameType: 'PEG', numba: 'V', plan: 'SD 523', bold: false },
46
- ] as IFormTestRow[]);
44
+ { id: 1000, name: 'IS IS DP12345', boldSingleSelect: true, boldMultiSelect: false },
45
+ { id: 1001, name: 'PEG V SD523', boldSingleSelect: true, boldMultiSelect: false },
46
+ ] as IBoolTestRow[]);
47
47
 
48
- const columnDefs: ColDefT<IFormTestRow>[] = useMemo(
48
+ const columnDefs: ColDefT<IBoolTestRow>[] = useMemo(
49
49
  () => [
50
50
  GridCell({
51
51
  field: 'id',
@@ -65,16 +65,15 @@ const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
65
65
  ),
66
66
  GridEditBoolean(
67
67
  {
68
- field: 'bold',
68
+ field: 'boldSingleSelect',
69
+ headerName: 'Bold single-select',
69
70
  },
70
71
  {
72
+ // multiEdit defaults to false — only the clicked row is updated
71
73
  onClick: async ({ selectedRowIds, checked }) => {
72
- await wait(1000);
73
- setRowData((rowData) => {
74
- // eslint-disable-next-line no-console
75
- console.log('onchange', selectedRowIds, checked);
76
- return rowData.map((row) => (selectedRowIds.includes(row.id) ? { ...row, bold: checked } : row));
77
- });
74
+ setRowData((prev) =>
75
+ prev.map((row) => (selectedRowIds.includes(row.id) ? { ...row, boldSingleSelect: checked } : row)),
76
+ );
78
77
  return true;
79
78
  },
80
79
  },
@@ -97,16 +96,15 @@ const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
97
96
  ),
98
97
  GridEditBoolean(
99
98
  {
100
- colId: 'bold2',
101
- field: 'bold',
99
+ field: 'boldMultiSelect',
100
+ headerName: 'Bold multi-select',
102
101
  },
103
102
  {
103
+ multiEdit: true,
104
104
  onClick: async ({ selectedRowIds, checked }) => {
105
- setRowData((rowData) => {
106
- // eslint-disable-next-line no-console
107
- console.log('onchange', selectedRowIds, checked);
108
- return rowData.map((row) => (selectedRowIds.includes(row.id) ? { ...row, bold: checked } : row));
109
- });
105
+ setRowData((prev) =>
106
+ prev.map((row) => (selectedRowIds.includes(row.id) ? { ...row, boldMultiSelect: checked } : row)),
107
+ );
110
108
  return true;
111
109
  },
112
110
  },
@@ -121,9 +119,9 @@ const GridPopoutEditBooleanTemplate: StoryFn<typeof Grid> = () => {
121
119
  theme={'ag-theme-step-view-list-default'}
122
120
  columnDefs={columnDefs}
123
121
  rowData={rowData}
124
- selectable={false}
122
+ selectable={true}
125
123
  singleClickEdit={true}
126
- rowSelection="single"
124
+ rowSelection="multiple"
127
125
  domLayout={'autoHeight'}
128
126
  />
129
127
  </>
@@ -138,11 +136,11 @@ _EditBooleanRetainsFocusAfterToggle.play = async (context) => {
138
136
  await waitForGridReady(context);
139
137
  const { canvasElement } = context;
140
138
 
141
- // Use bold2 (no async delay) so the redraw happens immediately
139
+ // Use boldMultiSelect (no async delay) so the redraw happens immediately
142
140
  const getBoolInput = () =>
143
- canvasElement.querySelector('.ag-cell[col-id="bold2"] input.ag-checkbox-input') as HTMLElement;
141
+ canvasElement.querySelector('.ag-cell[col-id="boldMultiSelect"] input.ag-checkbox-input') as HTMLElement;
144
142
 
145
- await userEvent.click(canvasElement.querySelector('.ag-cell[col-id="bold2"]') as HTMLElement);
143
+ await userEvent.click(canvasElement.querySelector('.ag-cell[col-id="boldMultiSelect"]') as HTMLElement);
146
144
  await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 500 });
147
145
 
148
146
  // Space toggles the checkbox, which triggers redrawRows — ag-grid moves focus from the
@@ -152,3 +150,69 @@ _EditBooleanRetainsFocusAfterToggle.play = async (context) => {
152
150
  // Re-query the input each retry since redraw replaces the DOM node
153
151
  await waitFor(() => expect(getBoolInput()).toHaveFocus(), { timeout: 500 });
154
152
  };
153
+
154
+ const selectRow = async (canvasElement: HTMLElement, rowId: string) => {
155
+ await userEvent.click(
156
+ canvasElement.querySelector(`.ag-row[row-id="${rowId}"] .ag-cell[col-id="ag-Grid-SelectionColumn"]`) as HTMLElement,
157
+ );
158
+ await waitFor(() =>
159
+ expect(canvasElement.querySelector(`.ag-row[row-id="${rowId}"].ag-row-selected`)).toBeInTheDocument(),
160
+ );
161
+ };
162
+
163
+ const getBoolInput = (canvasElement: HTMLElement, rowId: string, colId: string) =>
164
+ canvasElement.querySelector(
165
+ `.ag-row[row-id="${rowId}"] .ag-cell[col-id="${colId}"] input.ag-checkbox-input`,
166
+ ) as HTMLInputElement;
167
+
168
+ export const _EditBooleanSingleSelectOnlyAffectsClickedRow = GridPopoutEditBooleanTemplate.bind({});
169
+ _EditBooleanSingleSelectOnlyAffectsClickedRow.play = async (context) => {
170
+ await waitForGridReady(context);
171
+ const { canvasElement } = context;
172
+
173
+ // Select both rows so we can prove single-select ignores the multi-row selection
174
+ await selectRow(canvasElement, '1000');
175
+ await selectRow(canvasElement, '1001');
176
+
177
+ // Both rows start with boldSingleSelect: true
178
+ await waitFor(() => {
179
+ expect(getBoolInput(canvasElement, '1000', 'boldSingleSelect').checked).toBe(true);
180
+ expect(getBoolInput(canvasElement, '1001', 'boldSingleSelect').checked).toBe(true);
181
+ });
182
+
183
+ // Click row 1000's boldSingleSelect — multiEdit defaults to false, so only row 1000 changes
184
+ await userEvent.click(
185
+ canvasElement.querySelector('.ag-row[row-id="1000"] .ag-cell[col-id="boldSingleSelect"]') as HTMLElement,
186
+ );
187
+
188
+ await waitFor(() => {
189
+ expect(getBoolInput(canvasElement, '1000', 'boldSingleSelect').checked).toBe(false);
190
+ expect(getBoolInput(canvasElement, '1001', 'boldSingleSelect').checked).toBe(true); // unchanged
191
+ });
192
+ };
193
+
194
+ export const _EditBooleanMultiSelectEditsAllSelectedRows = GridPopoutEditBooleanTemplate.bind({});
195
+ _EditBooleanMultiSelectEditsAllSelectedRows.play = async (context) => {
196
+ await waitForGridReady(context);
197
+ const { canvasElement } = context;
198
+
199
+ // Select both rows before clicking
200
+ await selectRow(canvasElement, '1000');
201
+ await selectRow(canvasElement, '1001');
202
+
203
+ // Both rows start with boldMultiSelect: false
204
+ await waitFor(() => {
205
+ expect(getBoolInput(canvasElement, '1000', 'boldMultiSelect').checked).toBe(false);
206
+ expect(getBoolInput(canvasElement, '1001', 'boldMultiSelect').checked).toBe(false);
207
+ });
208
+
209
+ // Click row 1000's boldMultiSelect — multiEdit: true, so all selected rows change
210
+ await userEvent.click(
211
+ canvasElement.querySelector('.ag-row[row-id="1000"] .ag-cell[col-id="boldMultiSelect"]') as HTMLElement,
212
+ );
213
+
214
+ await waitFor(() => {
215
+ expect(getBoolInput(canvasElement, '1000', 'boldMultiSelect').checked).toBe(true);
216
+ expect(getBoolInput(canvasElement, '1001', 'boldMultiSelect').checked).toBe(true); // also changed
217
+ });
218
+ };
package/src/utils/util.ts CHANGED
@@ -54,7 +54,7 @@ export const fnOrVar = (fn: any, param?: any) => (typeof fn === 'function' ? fn(
54
54
  export const sanitiseFileName = (filename: string): string => {
55
55
  const valid = filename
56
56
  .trim()
57
- .replaceAll(/([\/\\])+/g, '-')
57
+ .replaceAll(/([/\\])+/g, '-')
58
58
  .replaceAll(/\s+/g, '_')
59
59
  .replaceAll(/[^\w\-āēīōūĀĒĪŌŪ.]/g, '');
60
60
  const parts = valid.split('.');
@@ -1,2 +0,0 @@
1
- declare const _default: import("vite").UserConfigFnObject;
2
- export default _default;