@linzjs/step-ag-grid 32.1.1 → 32.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.
Files changed (31) hide show
  1. package/dist/src/components/Grid.d.ts +1 -1
  2. package/dist/src/components/gridPopoverEdit/GridEditBoolean.d.ts +1 -0
  3. package/dist/src/components/gridPopoverEdit/GridPopoverEditBearing.d.ts +5 -4
  4. package/dist/src/lui/ActionButton.d.ts +1 -1
  5. package/dist/src/lui/TextInputFormatted.d.ts +1 -0
  6. package/dist/src/lui/TextInputFormatted.test.d.ts +1 -0
  7. package/dist/src/react-menu3/components/ControlledMenu.d.ts +1 -1
  8. package/dist/src/react-menu3/components/MenuRadioGroup.d.ts +1 -1
  9. package/dist/src/react-menu3/components/SubMenu.d.ts +1 -1
  10. package/dist/src/react-menu3/utils/propTypes2.d.ts +12 -12
  11. package/dist/src/react-menu3/utils/utils.d.ts +1 -1
  12. package/dist/src/utils/bearing.d.ts +1 -1
  13. package/dist/src/utils/textValidator.d.ts +1 -0
  14. package/dist/src/utils/useIsomorphicLayoutEffect.d.ts +2 -2
  15. package/dist/step-ag-grid.cjs +6789 -6382
  16. package/dist/step-ag-grid.cjs.map +1 -1
  17. package/dist/step-ag-grid.esm.js +6789 -6383
  18. package/dist/step-ag-grid.esm.js.map +1 -1
  19. package/package.json +27 -28
  20. package/src/components/Grid.tsx +1 -0
  21. package/src/components/gridForm/GridFormSubComponentTextInput.tsx +6 -1
  22. package/src/components/gridForm/GridFormTextInput.tsx +6 -1
  23. package/src/components/gridPopoverEdit/GridEditBoolean.tsx +25 -5
  24. package/src/global.d.ts +1 -0
  25. package/src/lui/TextInputFormatted.test.tsx +22 -0
  26. package/src/lui/TextInputFormatted.tsx +11 -1
  27. package/src/stories/grid/GridPopoutEditBoolean.stories.tsx +94 -30
  28. package/src/utils/textValidator.test.ts +45 -1
  29. package/src/utils/textValidator.ts +21 -0
  30. package/src/utils/util.ts +1 -1
  31. package/dist/vite.config.d.ts +0 -2
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.3.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,7 +2,11 @@ import { ReactElement, useCallback, useContext, useEffect } from 'react';
2
2
 
3
3
  import { GridSubComponentContext } from '../../contexts/GridSubComponentContext';
4
4
  import { TextInputFormatted } from '../../lui/TextInputFormatted';
5
- import { TextInputValidator, TextInputValidatorProps } from '../../utils/textValidator';
5
+ import {
6
+ TextInputValidator,
7
+ textInputValidatorPropsToSummary,
8
+ TextInputValidatorProps,
9
+ } from '../../utils/textValidator';
6
10
  import { CellEditorCommon } from '../GridCell';
7
11
  import { GridBaseRow } from '../types';
8
12
 
@@ -41,6 +45,7 @@ export const GridFormSubComponentTextInput = <TData extends GridBaseRow>(
41
45
  placeholder={props.placeholder}
42
46
  style={{ width: '100%' }}
43
47
  allowTabToSave={true}
48
+ validationSummary={textInputValidatorPropsToSummary(props)}
44
49
  />
45
50
  );
46
51
  };
@@ -2,7 +2,11 @@ import { useCallback, useMemo, useState } from 'react';
2
2
 
3
3
  import { useGridPopoverContext } from '../../contexts/GridPopoverContext';
4
4
  import { TextInputFormatted } from '../../lui/TextInputFormatted';
5
- import { TextInputValidator, TextInputValidatorProps } from '../../utils/textValidator';
5
+ import {
6
+ TextInputValidator,
7
+ textInputValidatorPropsToSummary,
8
+ TextInputValidatorProps,
9
+ } from '../../utils/textValidator';
6
10
  import { CellEditorCommon } from '../GridCell';
7
11
  import { useGridPopoverHook } from '../GridPopoverHook';
8
12
  import { GridBaseRow } from '../types';
@@ -63,6 +67,7 @@ export const GridFormTextInput = <TData extends GridBaseRow>(props: GridFormText
63
67
  style={{ width: props.width ?? 240 }}
64
68
  placeholder={props.placeholder ?? 'Type here'}
65
69
  helpText={helpText}
70
+ validationSummary={textInputValidatorPropsToSummary(props)}
66
71
  />
67
72
  </div>,
68
73
  );
@@ -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';
@@ -0,0 +1,22 @@
1
+ import { cleanup, render, screen } from '@testing-library/react';
2
+ import { afterEach, describe, expect, test } from 'vitest';
3
+
4
+ import { TextInputFormatted } from './TextInputFormatted';
5
+
6
+ describe('TextInputFormatted', () => {
7
+ afterEach(() => {
8
+ cleanup();
9
+ });
10
+
11
+ test('sets data-validationsummary attribute from validationSummary prop', () => {
12
+ render(<TextInputFormatted value={''} onChange={() => {}} validationSummary={'required,maxLength=10'} />);
13
+
14
+ expect(screen.getByRole('textbox').getAttribute('data-validationsummary')).toBe('required,maxLength=10');
15
+ });
16
+
17
+ test('omits data-validationsummary attribute when validationSummary prop is not set', () => {
18
+ render(<TextInputFormatted value={''} onChange={() => {}} />);
19
+
20
+ expect(screen.getByRole('textbox').hasAttribute('data-validationsummary')).toBe(false);
21
+ });
22
+ });
@@ -15,6 +15,7 @@ export interface LuiTextInputProps extends DetailedHTMLProps<InputHTMLAttributes
15
15
  error?: ReactElement | string | boolean | null;
16
16
  formatted?: string;
17
17
  allowTabToSave?: boolean;
18
+ validationSummary?: string;
18
19
  }
19
20
 
20
21
  export const TextInputFormatted = (props: LuiTextInputProps): ReactElement => {
@@ -32,13 +33,22 @@ export const TextInputFormatted = (props: LuiTextInputProps): ReactElement => {
32
33
  type={'text'}
33
34
  spellCheck={true}
34
35
  defaultValue={props.value}
35
- {...omit(props, ['error', 'value', 'helpText', 'formatted', 'className', 'allowTabToSave'])}
36
+ {...omit(props, [
37
+ 'error',
38
+ 'value',
39
+ 'helpText',
40
+ 'formatted',
41
+ 'className',
42
+ 'allowTabToSave',
43
+ 'validationSummary',
44
+ ])}
36
45
  className={'LuiTextInput-input'}
37
46
  onMouseEnter={(e) => {
38
47
  e.currentTarget.focus();
39
48
  props.onMouseEnter?.(e);
40
49
  }}
41
50
  data-allowtabtosave={props.allowTabToSave}
51
+ data-validationsummary={props.validationSummary}
42
52
  />
43
53
  <span className={'LuiTextInput-formatted'}>{props.formatted}</span>
44
54
  </span>
@@ -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
+ };
@@ -1,7 +1,7 @@
1
1
  import { describe, expect, test, vi } from 'vitest';
2
2
 
3
3
  import { GridBaseRow } from '../components/types';
4
- import { TextInputValidator, TextInputValidatorProps } from './textValidator';
4
+ import { textInputValidatorPropsToSummary, TextInputValidator, TextInputValidatorProps } from './textValidator';
5
5
 
6
6
  describe('TextInputValidator', () => {
7
7
  test('number format', () => {
@@ -115,3 +115,47 @@ describe('TextInputValidator', () => {
115
115
  expect(TextInputValidator({ maxBytes: 2 }, 'a–', { id: 0 }, {})).toBe('Must be no longer than 2 bytes');
116
116
  });
117
117
  });
118
+
119
+ describe('textInputValidatorPropsToSummary', () => {
120
+ test('empty props gives empty summary', () => {
121
+ expect(textInputValidatorPropsToSummary({})).toBe('');
122
+ });
123
+
124
+ test('required', () => {
125
+ expect(textInputValidatorPropsToSummary({ required: true })).toBe('required');
126
+ expect(textInputValidatorPropsToSummary({ required: false })).toBe('');
127
+ });
128
+
129
+ test('maxLength and maxBytes', () => {
130
+ expect(textInputValidatorPropsToSummary({ maxLength: 10 })).toBe('maxLength=10');
131
+ expect(textInputValidatorPropsToSummary({ maxBytes: 20 })).toBe('maxBytes=20');
132
+ });
133
+
134
+ test('invalid callback', () => {
135
+ expect(textInputValidatorPropsToSummary({ invalid: () => null })).toBe('invalid');
136
+ });
137
+
138
+ test('numberFormat fields', () => {
139
+ expect(
140
+ textInputValidatorPropsToSummary({
141
+ numberFormat: { precision: 3, scale: 2, gtMin: 0, geMin: 1, ltMax: 100, leMax: 99, notZero: true },
142
+ }),
143
+ ).toBe('precision=3,scale=2,gtMin=0,geMin=1,ltMax=100,leMax=99,notZero');
144
+ });
145
+
146
+ test('numberFormat with no set fields contributes nothing', () => {
147
+ expect(textInputValidatorPropsToSummary({ numberFormat: {} })).toBe('');
148
+ });
149
+
150
+ test('combines multiple props in order', () => {
151
+ expect(
152
+ textInputValidatorPropsToSummary({
153
+ required: true,
154
+ maxLength: 10,
155
+ maxBytes: 20,
156
+ invalid: () => null,
157
+ numberFormat: { scale: 2 },
158
+ }),
159
+ ).toBe('required,maxLength=10,maxBytes=20,invalid,scale=2');
160
+ });
161
+ });
@@ -81,3 +81,24 @@ export const TextInputValidator = <TData extends GridBaseRow>(
81
81
  }
82
82
  return null;
83
83
  };
84
+
85
+ export const textInputValidatorPropsToSummary = <TData extends GridBaseRow>(
86
+ props: TextInputValidatorProps<TData>,
87
+ ): string => {
88
+ const parts: string[] = [];
89
+ if (props.required) parts.push('required');
90
+ if (props.maxLength != null) parts.push(`maxLength=${props.maxLength}`);
91
+ if (props.maxBytes != null) parts.push(`maxBytes=${props.maxBytes}`);
92
+ if (props.invalid) parts.push('invalid');
93
+ const nf = props.numberFormat;
94
+ if (nf) {
95
+ if (nf.precision != null) parts.push(`precision=${nf.precision}`);
96
+ if (nf.scale != null) parts.push(`scale=${nf.scale}`);
97
+ if (nf.gtMin != null) parts.push(`gtMin=${nf.gtMin}`);
98
+ if (nf.geMin != null) parts.push(`geMin=${nf.geMin}`);
99
+ if (nf.ltMax != null) parts.push(`ltMax=${nf.ltMax}`);
100
+ if (nf.leMax != null) parts.push(`leMax=${nf.leMax}`);
101
+ if (nf.notZero) parts.push('notZero');
102
+ }
103
+ return parts.join(',');
104
+ };
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;