@linzjs/step-ag-grid 29.16.0 → 30.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.
- package/README.md +1 -1
- package/dist/index.css +27 -0
- package/dist/src/components/Grid.d.ts +3 -2
- package/dist/src/components/gridForm/GridFormInlineTextInput.d.ts +15 -0
- package/dist/src/components/gridForm/index.d.ts +1 -0
- package/dist/src/components/gridHook/useGridRangeSelection.d.ts +1 -1
- package/dist/src/components/gridPopoverEdit/GridInlineTextInput.d.ts +5 -0
- package/dist/src/components/gridPopoverEdit/index.d.ts +1 -0
- package/dist/src/contexts/GridContext.d.ts +1 -1
- package/dist/src/utils/__tests__/storybookTestUtil.ts +5 -1
- package/dist/step-ag-grid.cjs +58441 -505
- package/dist/step-ag-grid.cjs.map +1 -1
- package/dist/step-ag-grid.esm.js +58025 -91
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +13 -13
- package/src/components/Grid.tsx +33 -10
- package/src/components/GridPopoverHook.tsx +5 -2
- package/src/components/gridForm/GridFormInlineTextInput.tsx +119 -0
- package/src/components/gridForm/index.ts +1 -0
- package/src/components/gridHook/useGridContextMenu.tsx +3 -1
- package/src/components/gridHook/useGridCopy.ts +5 -2
- package/src/components/gridHook/useGridRangeSelection.ts +25 -4
- package/src/components/gridPopoverEdit/GridInlineTextInput.ts +13 -0
- package/src/components/gridPopoverEdit/index.ts +1 -0
- package/src/contexts/GridContext.tsx +6 -1
- package/src/contexts/GridContextProvider.tsx +30 -13
- package/src/stories/grid/GridFilterColumnsMultiSelect.stories.tsx +43 -2
- package/src/stories/grid/GridInlineText.stories.tsx +185 -0
- package/src/stories/grid/interactions/GridKeyboardInteractions.stories.tsx +3 -3
- package/src/styles/Grid.scss +5 -0
- package/src/styles/GridFormInlineTextInput.scss +23 -0
- package/src/styles/index.scss +1 -1
- package/src/utils/__tests__/storybookTestUtil.ts +5 -1
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": "
|
|
5
|
+
"version": "30.0.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"aggrid",
|
|
8
8
|
"ag-grid",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@linzjs/lui": ">=21",
|
|
31
|
-
"ag-grid-community": "~34.2.0",
|
|
32
|
-
"ag-grid-react": "~34.2.0",
|
|
33
31
|
"lodash-es": ">=4",
|
|
34
32
|
"react": ">=18",
|
|
35
33
|
"react-dom": ">=18"
|
|
36
34
|
},
|
|
37
35
|
"dependencies": {
|
|
36
|
+
"ag-grid-community": "34.3.1",
|
|
37
|
+
"ag-grid-react": "34.3.1",
|
|
38
38
|
"clsx": "^2.1.1",
|
|
39
39
|
"debounce-promise": "^3.1.2",
|
|
40
40
|
"matcher": "^5.0.0",
|
|
@@ -59,7 +59,8 @@
|
|
|
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 ncu -i --format group",
|
|
63
|
+
"prepare": "husky"
|
|
63
64
|
},
|
|
64
65
|
"eslintConfig": {
|
|
65
66
|
"extends": [
|
|
@@ -86,14 +87,14 @@
|
|
|
86
87
|
"@chromatic-com/storybook": "^4.1.3",
|
|
87
88
|
"@linzjs/lui": "^24.3.1",
|
|
88
89
|
"@linzjs/style": "^5.4.0",
|
|
89
|
-
"@linzjs/windows": "^8.3
|
|
90
|
+
"@linzjs/windows": "^8.6.3",
|
|
90
91
|
"@rollup/plugin-commonjs": "^28.0.9",
|
|
91
92
|
"@rollup/plugin-json": "^6.1.0",
|
|
92
93
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
93
|
-
"@storybook/addon-docs": "^9.1.
|
|
94
|
-
"@storybook/addon-links": "^9.1.
|
|
95
|
-
"@storybook/react": "^9.1.
|
|
96
|
-
"@storybook/react-vite": "^9.1.
|
|
94
|
+
"@storybook/addon-docs": "^9.1.17",
|
|
95
|
+
"@storybook/addon-links": "^9.1.17",
|
|
96
|
+
"@storybook/react": "^9.1.17",
|
|
97
|
+
"@storybook/react-vite": "^9.1.17",
|
|
97
98
|
"@testing-library/dom": "^10.4.1",
|
|
98
99
|
"@testing-library/react": "^16.3.0",
|
|
99
100
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -106,15 +107,14 @@
|
|
|
106
107
|
"@types/uuid": "^11.0.0",
|
|
107
108
|
"@vitejs/plugin-react-swc": "^3.11.0",
|
|
108
109
|
"@vitest/ui": "^3.2.4",
|
|
109
|
-
"ag-grid-community": "34.2.0",
|
|
110
|
-
"ag-grid-react": "34.2.0",
|
|
111
110
|
"babel-preset-react-app": "^10.1.0",
|
|
112
111
|
"canvas": "^3.2.0",
|
|
113
112
|
"chromatic": "^13.3.4",
|
|
114
113
|
"cross-env": "^10.1.0",
|
|
115
114
|
"css-loader": "^7.1.2",
|
|
116
115
|
"eslint-plugin-react": "^7.37.5",
|
|
117
|
-
"eslint-plugin-storybook": "^9.1.
|
|
116
|
+
"eslint-plugin-storybook": "^9.1.17",
|
|
117
|
+
"husky": "^9.1.7",
|
|
118
118
|
"jsdom": "^26.1.0",
|
|
119
119
|
"lodash-es": "^4.17.21",
|
|
120
120
|
"mkdirp": "^3.0.1",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"rollup-plugin-typescript2": "^0.36.0",
|
|
134
134
|
"sass": "^1.96.0",
|
|
135
135
|
"sass-loader": "^16.0.6",
|
|
136
|
-
"storybook": "^9.1.
|
|
136
|
+
"storybook": "^9.1.17",
|
|
137
137
|
"storybook-css-modules-preset": "^1.1.1",
|
|
138
138
|
"style-loader": "^4.0.0",
|
|
139
139
|
"typescript": "^5.9.3",
|
package/src/components/Grid.tsx
CHANGED
|
@@ -74,9 +74,11 @@ export interface GridProps<TData extends GridBaseRow = GridBaseRow> {
|
|
|
74
74
|
enableClickSelection?: boolean;
|
|
75
75
|
enableRangeSelection?: boolean;
|
|
76
76
|
enableSelectionWithoutKeys?: boolean;
|
|
77
|
+
enableMultilineBulkEdit?: boolean;
|
|
77
78
|
externalSelectedIds?: TData['id'][];
|
|
78
79
|
externalSelectedItems?: TData[];
|
|
79
80
|
hideSelectColumn?: boolean;
|
|
81
|
+
hideSelectedRow?: boolean;
|
|
80
82
|
selectColumnPinned?: ColDef['pinned'];
|
|
81
83
|
selectable?: boolean;
|
|
82
84
|
setExternalSelectedIds?: (ids: TData['id'][]) => void;
|
|
@@ -85,7 +87,6 @@ export interface GridProps<TData extends GridBaseRow = GridBaseRow> {
|
|
|
85
87
|
// ─── Editing ──────────────────────────────────────────────────
|
|
86
88
|
onBulkEditingComplete?: () => Promise<void> | void;
|
|
87
89
|
singleClickEdit?: boolean;
|
|
88
|
-
stopEditingWhenCellsLoseFocus?: boolean;
|
|
89
90
|
|
|
90
91
|
// ─── Context Menu ─────────────────────────────────────────────
|
|
91
92
|
contextMenu?: GridContextMenuComponent<TData>;
|
|
@@ -150,7 +151,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
|
|
|
150
151
|
|
|
151
152
|
// ─── Editing ──────────────────────────────────
|
|
152
153
|
singleClickEdit = false,
|
|
153
|
-
|
|
154
|
+
enableMultilineBulkEdit = false,
|
|
154
155
|
|
|
155
156
|
// ─── Context Menu ─────────────────────────────
|
|
156
157
|
contextMenuSelectRow = false,
|
|
@@ -500,11 +501,17 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
|
|
|
500
501
|
*/
|
|
501
502
|
const onGridReady = useCallback(
|
|
502
503
|
(event: GridReadyEvent) => {
|
|
503
|
-
setApis(event.api, hasExternallySelectedItems, dataTestId);
|
|
504
|
+
setApis(event.api, hasExternallySelectedItems, enableMultilineBulkEdit, dataTestId);
|
|
504
505
|
event.api.showNoRowsOverlay();
|
|
505
506
|
synchroniseExternallySelectedItemsToGrid();
|
|
506
507
|
},
|
|
507
|
-
[
|
|
508
|
+
[
|
|
509
|
+
dataTestId,
|
|
510
|
+
enableMultilineBulkEdit,
|
|
511
|
+
hasExternallySelectedItems,
|
|
512
|
+
setApis,
|
|
513
|
+
synchroniseExternallySelectedItemsToGrid,
|
|
514
|
+
],
|
|
508
515
|
);
|
|
509
516
|
|
|
510
517
|
/**
|
|
@@ -556,6 +563,11 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
|
|
|
556
563
|
*/
|
|
557
564
|
const onCellDoubleClick = useCallback(
|
|
558
565
|
(event: CellDoubleClickedEvent) => {
|
|
566
|
+
const cellEditing = event.api.getEditingCells()?.[0];
|
|
567
|
+
if (cellEditing && cellEditing.colId === event.column.getColId() && cellEditing.rowIndex === event.rowIndex) {
|
|
568
|
+
// Ignore double click in already editing cell
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
559
571
|
const editable = fnOrVar(event.colDef?.editable, event);
|
|
560
572
|
if (editable && !invokeEditAction(event)) {
|
|
561
573
|
void startCellEditing({ rowId: event.data.id, colId: event.column.getColId() });
|
|
@@ -657,19 +669,30 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
|
|
|
657
669
|
*/
|
|
658
670
|
const onCellClicked = useCallback(
|
|
659
671
|
(event: CellClickedEvent) => {
|
|
660
|
-
if (rangeStartRef.current && rangeEndRef.current) {
|
|
672
|
+
if (enableRangeSelection && rangeStartRef.current && rangeEndRef.current) {
|
|
661
673
|
// This is to detect difference between a single click and a click drag return to cell.
|
|
662
|
-
|
|
663
|
-
|
|
674
|
+
const maxDistanceBetweenC = Math.max(
|
|
675
|
+
Math.abs(rangeEndRef.current.clickLocation[0] - rangeStartRef.current.clickLocation[0]),
|
|
676
|
+
Math.abs(rangeEndRef.current.clickLocation[1] - rangeStartRef.current.clickLocation[1]),
|
|
677
|
+
);
|
|
678
|
+
if (maxDistanceBetweenC > 8) {
|
|
679
|
+
return;
|
|
664
680
|
}
|
|
665
|
-
|
|
681
|
+
clearRangeSelection();
|
|
666
682
|
}
|
|
683
|
+
|
|
667
684
|
const editable = fnOrVar(event.colDef?.editable, event);
|
|
668
685
|
if ((editable && event.colDef.singleClickEdit) ?? singleClickEdit) {
|
|
686
|
+
const editingCell = event.api.getEditingCells()[0];
|
|
687
|
+
// If we are editing and single click we don't want the editing to stop
|
|
688
|
+
if (editingCell && editingCell.colId === event.column.getColId() && editingCell.rowIndex === event.rowIndex) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
event.api.setFocusedCell(event.rowIndex ?? 0, event.column.getColId(), event.rowPinned);
|
|
669
692
|
void startCellEditing({ rowId: event.data.id, colId: event.column.getColId() });
|
|
670
693
|
}
|
|
671
694
|
},
|
|
672
|
-
[clearRangeSelection, singleClickEdit, startCellEditing],
|
|
695
|
+
[clearRangeSelection, enableRangeSelection, singleClickEdit, startCellEditing],
|
|
673
696
|
);
|
|
674
697
|
|
|
675
698
|
/**
|
|
@@ -1020,6 +1043,7 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
|
|
|
1020
1043
|
'theme-specific',
|
|
1021
1044
|
staleGrid && 'Grid-sortIsStale',
|
|
1022
1045
|
gridReady && rowData && autoSized && 'Grid-ready',
|
|
1046
|
+
params.hideSelectedRow && 'Grid-hideSelectedRow',
|
|
1023
1047
|
)}
|
|
1024
1048
|
>
|
|
1025
1049
|
{contextMenuComponent}
|
|
@@ -1081,7 +1105,6 @@ export const Grid = <TData extends GridBaseRow = GridBaseRow>({
|
|
|
1081
1105
|
}
|
|
1082
1106
|
: undefined
|
|
1083
1107
|
}
|
|
1084
|
-
stopEditingWhenCellsLoseFocus={stopEditingWhenCellsLoseFocus}
|
|
1085
1108
|
selectionColumnDef={selectionColumnDef}
|
|
1086
1109
|
suppressCellFocus={params.suppressCellFocus}
|
|
1087
1110
|
suppressClickEdit={true}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ReactElement, useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
|
+
import { useGridContext } from '../contexts/GridContext';
|
|
3
4
|
import { useGridPopoverContext } from '../contexts/GridPopoverContext';
|
|
4
5
|
import { ControlledMenu } from '../react-menu3';
|
|
5
6
|
import { MenuCloseEvent } from '../react-menu3/types';
|
|
@@ -27,6 +28,7 @@ export const useGridPopoverHook = <TData extends GridBaseRow>({
|
|
|
27
28
|
invalid,
|
|
28
29
|
dontSaveOnExternalClick,
|
|
29
30
|
}: GridPopoverHookProps<TData>) => {
|
|
31
|
+
const { resetFocusedCellAfterCellEditing } = useGridContext<TData>();
|
|
30
32
|
const { anchorRef, saving, updateValue, stopEditing } = useGridPopoverContext<TData>();
|
|
31
33
|
const saveButtonRef = useRef<HTMLButtonElement>(null);
|
|
32
34
|
const [isOpen, setOpen] = useState(false);
|
|
@@ -40,7 +42,7 @@ export const useGridPopoverHook = <TData extends GridBaseRow>({
|
|
|
40
42
|
if (reason == CloseReason.CANCEL) {
|
|
41
43
|
await updateValue(CancelPromise, 0);
|
|
42
44
|
stopEditing();
|
|
43
|
-
|
|
45
|
+
resetFocusedCellAfterCellEditing();
|
|
44
46
|
return;
|
|
45
47
|
}
|
|
46
48
|
if (invalid?.()) {
|
|
@@ -55,9 +57,10 @@ export const useGridPopoverHook = <TData extends GridBaseRow>({
|
|
|
55
57
|
)
|
|
56
58
|
) {
|
|
57
59
|
stopEditing();
|
|
60
|
+
resetFocusedCellAfterCellEditing();
|
|
58
61
|
}
|
|
59
62
|
},
|
|
60
|
-
[invalid, save, stopEditing, updateValue],
|
|
63
|
+
[invalid, resetFocusedCellAfterCellEditing, save, stopEditing, updateValue],
|
|
61
64
|
);
|
|
62
65
|
|
|
63
66
|
const popoverWrapper = useCallback(
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import clsx from 'clsx';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useGridContext } from '../../contexts/GridContext';
|
|
5
|
+
import { useGridPopoverContext } from '../../contexts/GridPopoverContext';
|
|
6
|
+
import { CloseReason } from '../../react-menu3/utils';
|
|
7
|
+
import { TextInputValidator, TextInputValidatorProps } from '../../utils/textValidator';
|
|
8
|
+
import { MaybePromise } from '../../utils/util';
|
|
9
|
+
import { CellEditorCommon } from '../GridCell';
|
|
10
|
+
import { useGridPopoverHook } from '../GridPopoverHook';
|
|
11
|
+
import { GridBaseRow } from '../types';
|
|
12
|
+
|
|
13
|
+
export interface GridFormInlineTextInput<TData extends GridBaseRow>
|
|
14
|
+
extends TextInputValidatorProps<TData>,
|
|
15
|
+
CellEditorCommon {
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
units?: string;
|
|
18
|
+
onSave?: (props: { selectedRows: TData[]; selectedRowIds: TData['id'][]; value: string }) => MaybePromise<boolean>;
|
|
19
|
+
helpText?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const GridFormInlineTextInput = <TData extends GridBaseRow>(props: GridFormInlineTextInput<TData>) => {
|
|
23
|
+
const { onSave } = props;
|
|
24
|
+
|
|
25
|
+
const { getSelectedRows, prePopupOps } = useGridContext<TData>();
|
|
26
|
+
const { field, value: initialVale, data } = useGridPopoverContext<TData>();
|
|
27
|
+
|
|
28
|
+
const initValue = useMemo(() => (initialVale == null ? '' : `${initialVale}`), [initialVale]);
|
|
29
|
+
const [value, setValue] = useState(initValue);
|
|
30
|
+
|
|
31
|
+
const invalid = useCallback(() => TextInputValidator<TData>(props, value, data, {}), [data, props, value]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
prePopupOps();
|
|
35
|
+
}, [prePopupOps]);
|
|
36
|
+
|
|
37
|
+
const save = useCallback(async (): Promise<boolean> => {
|
|
38
|
+
if (invalid()) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const trimmedValue = value.trim();
|
|
43
|
+
// No change, so don't save
|
|
44
|
+
if (initValue === trimmedValue) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!onSave) {
|
|
49
|
+
console.error(`No onSave handler for GridFormInlineTextInput "${String(field)}`);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const selectedRows = getSelectedRows<TData>();
|
|
54
|
+
const selectedRowIds = selectedRows.map((data) => data.id);
|
|
55
|
+
|
|
56
|
+
return onSave({
|
|
57
|
+
selectedRowIds,
|
|
58
|
+
selectedRows,
|
|
59
|
+
value: trimmedValue,
|
|
60
|
+
});
|
|
61
|
+
}, [invalid, value, initValue, onSave, getSelectedRows, field]);
|
|
62
|
+
|
|
63
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
inputRef.current?.focus();
|
|
66
|
+
inputRef.current?.select();
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const { triggerSave } = useGridPopoverHook({
|
|
70
|
+
className: props.className,
|
|
71
|
+
invalid,
|
|
72
|
+
save,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const tabDirectionRef = useRef<string>(CloseReason.BLUR);
|
|
76
|
+
|
|
77
|
+
const tabRecorder = useCallback(
|
|
78
|
+
(e: KeyboardEvent) => {
|
|
79
|
+
if (e.key === 'Tab') {
|
|
80
|
+
tabDirectionRef.current = e.shiftKey ? CloseReason.TAB_BACKWARD : CloseReason.TAB_FORWARD;
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
e.stopPropagation();
|
|
83
|
+
void triggerSave(tabDirectionRef.current);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
[triggerSave],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const input = inputRef.current;
|
|
91
|
+
input?.addEventListener('keydown', tabRecorder, { capture: true });
|
|
92
|
+
return () => {
|
|
93
|
+
input?.removeEventListener('keydown', tabRecorder, { capture: true });
|
|
94
|
+
};
|
|
95
|
+
}, [tabRecorder]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<div
|
|
99
|
+
className={clsx('GridFormInlineTextInput', invalid() && 'GridFormInlineTextInput-error')}
|
|
100
|
+
title={String(invalid() ?? '')}
|
|
101
|
+
>
|
|
102
|
+
<input
|
|
103
|
+
ref={inputRef}
|
|
104
|
+
type={'text'}
|
|
105
|
+
value={value}
|
|
106
|
+
onChange={(e) => setValue(e.target.value)}
|
|
107
|
+
placeholder={props.placeholder ?? 'Type here'}
|
|
108
|
+
onBlur={() => {
|
|
109
|
+
void triggerSave(CloseReason.BLUR);
|
|
110
|
+
}}
|
|
111
|
+
onKeyUp={(e) => {
|
|
112
|
+
if (e.key === 'Enter') {
|
|
113
|
+
void triggerSave(CloseReason.BLUR);
|
|
114
|
+
}
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -45,7 +45,9 @@ export const useGridContextMenu = <TData extends GridBaseRow, Context extends ob
|
|
|
45
45
|
|
|
46
46
|
const openMenu = useCallback(
|
|
47
47
|
(e: PointerEvent | null | undefined) => {
|
|
48
|
-
if (!e)
|
|
48
|
+
if (!e) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
49
51
|
prePopupOps();
|
|
50
52
|
setAnchorPoint({ x: e.clientX, y: e.clientY });
|
|
51
53
|
setOpen(true);
|
|
@@ -250,8 +250,11 @@ export const useGridCopy = <TData extends GridBaseRow>({
|
|
|
250
250
|
});
|
|
251
251
|
|
|
252
252
|
const rangeSelectInterceptContextMenu = useCallback(
|
|
253
|
-
(event: CellContextMenuEvent<TData>) =>
|
|
254
|
-
rangeStartRef.current
|
|
253
|
+
(event: CellContextMenuEvent<TData>) => {
|
|
254
|
+
return hasSelectedMoreThanOneCellRef.current && rangeStartRef.current !== null
|
|
255
|
+
? rangeSelectContextMenu(event)
|
|
256
|
+
: cellContextMenu(event);
|
|
257
|
+
},
|
|
255
258
|
[cellContextMenu, rangeSelectContextMenu],
|
|
256
259
|
);
|
|
257
260
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CellMouseDownEvent, CellMouseOverEvent, IRowNode } from 'ag-grid-community';
|
|
2
|
-
import { MutableRefObject, RefObject, useCallback, useState } from 'react';
|
|
2
|
+
import { MutableRefObject, RefObject, useCallback, useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { useInterval } from 'usehooks-ts';
|
|
4
4
|
|
|
5
5
|
import { GridBaseRow } from '../types';
|
|
@@ -7,7 +7,7 @@ import { GridBaseRow } from '../types';
|
|
|
7
7
|
export interface CellLocation {
|
|
8
8
|
rowId: string;
|
|
9
9
|
colId: string;
|
|
10
|
-
|
|
10
|
+
clickLocation: [number, number];
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface GridRanges<TData extends GridBaseRow> {
|
|
@@ -180,6 +180,26 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
|
|
|
180
180
|
updateRangeSelectionCellClasses();
|
|
181
181
|
}, [updateRangeSelectionCellClasses]);
|
|
182
182
|
|
|
183
|
+
const mouseDownRef = useRef<[number, number]>([0, 0]);
|
|
184
|
+
const trackMouseDown = useCallback((e: MouseEvent) => {
|
|
185
|
+
mouseDownRef.current = [e.screenX, e.screenY];
|
|
186
|
+
}, []);
|
|
187
|
+
|
|
188
|
+
const trackMouseUp = useCallback((e: MouseEvent) => {
|
|
189
|
+
if (rangeEndRef.current) {
|
|
190
|
+
rangeEndRef.current.clickLocation = [e.screenX, e.screenY];
|
|
191
|
+
}
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
document.addEventListener('mousedown', trackMouseDown, { capture: true });
|
|
196
|
+
document.addEventListener('mouseup', trackMouseUp, { capture: true });
|
|
197
|
+
return () => {
|
|
198
|
+
document.removeEventListener('mousemove', trackMouseDown, { capture: true });
|
|
199
|
+
document.removeEventListener('mouseup', trackMouseUp, { capture: true });
|
|
200
|
+
};
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
183
203
|
const onCellMouseOver = useCallback(
|
|
184
204
|
(e: CellMouseOverEvent, mouseDown?: boolean) => {
|
|
185
205
|
if (!enableRangeSelection) {
|
|
@@ -194,7 +214,7 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
|
|
|
194
214
|
rangeEndRef.current = {
|
|
195
215
|
rowId: e.node.data.id,
|
|
196
216
|
colId: e.column.getColId(),
|
|
197
|
-
|
|
217
|
+
clickLocation: [0, 0],
|
|
198
218
|
};
|
|
199
219
|
|
|
200
220
|
if (mouseDown) {
|
|
@@ -202,7 +222,7 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
|
|
|
202
222
|
e.api.forEachNodeAfterFilterAndSort((node: IRowNode<TData>) => sortedNodes.push(node));
|
|
203
223
|
rangeSortedNodesRef.current = sortedNodes;
|
|
204
224
|
setRefreshIntervalEnabled(true);
|
|
205
|
-
rangeStartRef.current = { ...rangeEndRef.current };
|
|
225
|
+
rangeStartRef.current = { ...rangeEndRef.current, clickLocation: mouseDownRef.current };
|
|
206
226
|
}
|
|
207
227
|
|
|
208
228
|
if (
|
|
@@ -211,6 +231,7 @@ export const useGridRangeSelection = <TData extends GridBaseRow>({
|
|
|
211
231
|
(rangeStartRef.current.rowId !== rangeEndRef.current.rowId ||
|
|
212
232
|
rangeStartRef.current.colId !== rangeEndRef.current.colId)
|
|
213
233
|
) {
|
|
234
|
+
e.api.stopEditing();
|
|
214
235
|
hasSelectedMoreThanOneCellRef.current = true;
|
|
215
236
|
window.getSelection()?.removeAllRanges();
|
|
216
237
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { GenericCellEditorProps, GridCell } from '../GridCell';
|
|
2
|
+
import { GridFormInlineTextInput } from '../gridForm/GridFormInlineTextInput';
|
|
3
|
+
import { GenericCellColDef } from '../gridRender/GridRenderGenericCell';
|
|
4
|
+
import { ColDefT, GridBaseRow } from '../types';
|
|
5
|
+
|
|
6
|
+
export const GridInlineTextInput = <TData extends GridBaseRow, TValue = any>(
|
|
7
|
+
colDef: GenericCellColDef<TData, TValue>,
|
|
8
|
+
params: GenericCellEditorProps<GridFormInlineTextInput<TData>>,
|
|
9
|
+
): ColDefT<TData, TValue> =>
|
|
10
|
+
GridCell<TData, TValue, GridFormInlineTextInput<TData>>(colDef, {
|
|
11
|
+
editor: GridFormInlineTextInput,
|
|
12
|
+
...params,
|
|
13
|
+
});
|
|
@@ -27,7 +27,12 @@ export interface GridContextType<TData extends GridBaseRow> {
|
|
|
27
27
|
filter?: keyof ColDef | ((r: ColDef) => boolean | undefined | null | number | string),
|
|
28
28
|
) => ColDefT<TData, any>[];
|
|
29
29
|
getColumnIds: (filter?: keyof ColDef | ((r: ColDef) => boolean | undefined | null | number | string)) => string[];
|
|
30
|
-
setApis: (
|
|
30
|
+
setApis: (
|
|
31
|
+
gridApi: GridApi | undefined,
|
|
32
|
+
hasExternallySelectedItems: boolean,
|
|
33
|
+
enableMultilineBulkEdit: boolean,
|
|
34
|
+
dataTestId?: string,
|
|
35
|
+
) => void;
|
|
31
36
|
prePopupOps: () => void;
|
|
32
37
|
setQuickFilter: (quickFilter: string) => void;
|
|
33
38
|
editingCells: () => boolean;
|
|
@@ -38,6 +38,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
38
38
|
const [invisibleColumnIds, _setInvisibleColumnIds] = useState<string[]>();
|
|
39
39
|
const testId = useRef<string | undefined>();
|
|
40
40
|
const hasExternallySelectedItemsRef = useRef(false);
|
|
41
|
+
const enableMultilineBulkEditRef = useRef(false);
|
|
41
42
|
const idsBeforeUpdate = useRef<TData['id'][]>([]);
|
|
42
43
|
const prePopupFocusedCell = useRef<CellPosition>();
|
|
43
44
|
const [externallySelectedItemsAreInSync, setExternallySelectedItemsAreInSync] = useState(false);
|
|
@@ -120,8 +121,14 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
120
121
|
* Set the grid api when the grid is ready.
|
|
121
122
|
*/
|
|
122
123
|
const setApis = useCallback(
|
|
123
|
-
(
|
|
124
|
+
(
|
|
125
|
+
gridApi: GridApi | undefined,
|
|
126
|
+
hasExternallySelectedItems: boolean,
|
|
127
|
+
enableMultilineBulkEdit: boolean,
|
|
128
|
+
dataTestId?: string,
|
|
129
|
+
) => {
|
|
124
130
|
hasExternallySelectedItemsRef.current = hasExternallySelectedItems;
|
|
131
|
+
enableMultilineBulkEditRef.current = enableMultilineBulkEdit;
|
|
125
132
|
testId.current = dataTestId;
|
|
126
133
|
setGridApi(gridApi);
|
|
127
134
|
gridApi?.setGridOption('quickFilterText', quickFilter);
|
|
@@ -164,6 +171,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
164
171
|
if (!gridApi || gridApi.isDestroyed()) {
|
|
165
172
|
return;
|
|
166
173
|
}
|
|
174
|
+
|
|
167
175
|
prePopupFocusedCell.current = gridApi.getFocusedCell() ?? undefined;
|
|
168
176
|
}, [gridApi]);
|
|
169
177
|
|
|
@@ -323,7 +331,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
323
331
|
if (flash) {
|
|
324
332
|
delay(() => {
|
|
325
333
|
try {
|
|
326
|
-
!gridApi.isDestroyed && gridApi.flashCells({ rowNodes });
|
|
334
|
+
!gridApi.isDestroyed() && gridApi.flashCells({ rowNodes });
|
|
327
335
|
} catch {
|
|
328
336
|
// ignore, flash cells sometimes throws errors as nodes have gone out of scope
|
|
329
337
|
}
|
|
@@ -437,7 +445,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
437
445
|
*/
|
|
438
446
|
const autoSizeColumns = useCallback(
|
|
439
447
|
async ({ skipHeader, colIds, userSizedColIds }: AutoSizeColumnsProps = {}): Promise<AutoSizeColumnsResult> => {
|
|
440
|
-
if (!gridApi || !gridApi.getColumnState()) {
|
|
448
|
+
if (!gridApi || gridApi.isDestroyed() || !gridApi.getColumnState()) {
|
|
441
449
|
return null;
|
|
442
450
|
}
|
|
443
451
|
|
|
@@ -445,7 +453,7 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
445
453
|
const colIdsSet = colIds instanceof Set ? colIds : new Set(colIds);
|
|
446
454
|
|
|
447
455
|
const getVisibleColStates = () => {
|
|
448
|
-
const colStates = gridApi.getColumnState();
|
|
456
|
+
const colStates = gridApi.getColumnState() ?? [];
|
|
449
457
|
return colStates.filter((colState) => {
|
|
450
458
|
const colId = colState.colId;
|
|
451
459
|
return (isEmpty(colIdsSet) || colIdsSet.has(colId)) && !userSizedColIds?.has(colId);
|
|
@@ -522,9 +530,12 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
522
530
|
if (!gridApi || gridApi.isDestroyed() || startCellEditingInProgressRef.current) {
|
|
523
531
|
return;
|
|
524
532
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
533
|
+
|
|
534
|
+
const cellToFocus = prePopupFocusedCell.current;
|
|
535
|
+
if (cellToFocus) {
|
|
536
|
+
defer(() => {
|
|
537
|
+
gridApi.setFocusedCell(cellToFocus.rowIndex, cellToFocus.column);
|
|
538
|
+
});
|
|
528
539
|
}
|
|
529
540
|
}, [gridApi]);
|
|
530
541
|
|
|
@@ -607,9 +618,8 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
607
618
|
|
|
608
619
|
const bulkEditingCompleteCallbackRef = useRef<() => Promise<void> | void>();
|
|
609
620
|
const onBulkEditingComplete = useCallback(async () => {
|
|
610
|
-
resetFocusedCellAfterCellEditing();
|
|
611
621
|
await bulkEditingCompleteCallbackRef.current?.();
|
|
612
|
-
}, [
|
|
622
|
+
}, []);
|
|
613
623
|
|
|
614
624
|
const setOnBulkEditingComplete = useCallback(
|
|
615
625
|
(cellEditingCompleteCallback: (() => Promise<void> | void) | undefined) => {
|
|
@@ -661,10 +671,17 @@ export const GridContextProvider = <TData extends GridBaseRow>(props: PropsWithC
|
|
|
661
671
|
return true;
|
|
662
672
|
}
|
|
663
673
|
const postRow = gridApi.getFocusedCell();
|
|
664
|
-
if (
|
|
665
|
-
//
|
|
666
|
-
|
|
667
|
-
|
|
674
|
+
if (enableMultilineBulkEditRef.current) {
|
|
675
|
+
// Last cell in grid, nowhere to tab
|
|
676
|
+
if (preRow?.rowIndex === postRow?.rowIndex && preRow?.column === postRow?.column) {
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
} else {
|
|
680
|
+
if (preRow?.rowIndex !== postRow?.rowIndex || preRow?.column === postRow?.column) {
|
|
681
|
+
// We didn't find an editable cell in the same row, or the cell column didn't change
|
|
682
|
+
// implying it was start/end of grid
|
|
683
|
+
break;
|
|
684
|
+
}
|
|
668
685
|
}
|
|
669
686
|
|
|
670
687
|
if (focusedCellIsEditable()) {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
GridFilterColumnsMultiSelect,
|
|
10
10
|
} from 'components/gridFilter/GridFilterColumnsMultiSelect';
|
|
11
11
|
import { useMemo, useState } from 'react';
|
|
12
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
12
13
|
|
|
13
14
|
import {
|
|
14
15
|
ColDefT,
|
|
@@ -21,8 +22,15 @@ import {
|
|
|
21
22
|
GridProps,
|
|
22
23
|
GridUpdatingContextProvider,
|
|
23
24
|
GridWrapper,
|
|
25
|
+
wait,
|
|
24
26
|
} from '../..';
|
|
25
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
getNumberOfGridRows,
|
|
29
|
+
gridColumnValues,
|
|
30
|
+
waitForGridReady,
|
|
31
|
+
waitForGridRows,
|
|
32
|
+
} from '../../utils/__tests__/storybookTestUtil';
|
|
33
|
+
import { findQuick } from '../../utils/__tests__/testQuick';
|
|
26
34
|
|
|
27
35
|
export default {
|
|
28
36
|
title: 'Components / Grids',
|
|
@@ -124,4 +132,37 @@ const GridFilterColumnsMultiSelectTemplate: StoryFn<typeof Grid<ITestRow>> = (pr
|
|
|
124
132
|
};
|
|
125
133
|
|
|
126
134
|
export const _FilterColumnsMultiSelectExample = GridFilterColumnsMultiSelectTemplate.bind({});
|
|
127
|
-
_FilterColumnsMultiSelectExample.play =
|
|
135
|
+
_FilterColumnsMultiSelectExample.play = async (context) => {
|
|
136
|
+
const { canvasElement } = context;
|
|
137
|
+
await waitForGridReady(context);
|
|
138
|
+
await waitForGridRows({ grid: canvasElement });
|
|
139
|
+
|
|
140
|
+
const setGridFilterAndCheckResultingRows = async (filterString: string, expected: string[] | null) => {
|
|
141
|
+
await filterGridUsingColumnFilter(filterString);
|
|
142
|
+
await wait(500);
|
|
143
|
+
if (expected !== null) {
|
|
144
|
+
expect(gridColumnValues('id', canvasElement)).toEqual(expected);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
await waitForGridReady(context);
|
|
148
|
+
expect(getNumberOfGridRows()).toEqual(0);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
await setGridFilterAndCheckResultingRows('Tester', ['1001', '1002', '1004', '1005', '1006']);
|
|
152
|
+
await setGridFilterAndCheckResultingRows('Select All', ['1000', '1001', '1002', '1003', '1004', '1005', '1006']);
|
|
153
|
+
await setGridFilterAndCheckResultingRows('Select All', null); // Check that deselecting Select All filters out all rows
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const filterGridUsingColumnFilter = async (filterString: string): Promise<void> => {
|
|
157
|
+
const user = userEvent.setup({
|
|
158
|
+
delay: 100,
|
|
159
|
+
});
|
|
160
|
+
const headerCellFilterButton = await findQuick({ classes: `.ag-header-cell-filter-button` });
|
|
161
|
+
await expect(headerCellFilterButton).toBeInTheDocument();
|
|
162
|
+
await user.click(headerCellFilterButton);
|
|
163
|
+
const canvas = within(document.body);
|
|
164
|
+
const regex = new RegExp(filterString, 'i'); // case‑insensitive
|
|
165
|
+
const filterCheckbox = await canvas.findByRole('checkbox', { name: regex });
|
|
166
|
+
await expect(filterCheckbox).toBeInTheDocument();
|
|
167
|
+
await user.click(filterCheckbox);
|
|
168
|
+
};
|