@linzjs/step-ag-grid 10.0.2 → 12.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/dist/index.css +10 -5
- package/dist/src/components/Grid.d.ts +0 -4
- package/dist/src/components/GridCell.d.ts +1 -1
- package/dist/src/components/GridFilter.d.ts +2 -0
- package/dist/src/components/GridWrapper.d.ts +6 -0
- package/dist/src/components/gridFilter/GridFilterQuick.d.ts +5 -0
- package/dist/src/components/gridFilter/GridFilters.d.ts +5 -0
- package/dist/src/contexts/GridContext.d.ts +5 -0
- package/dist/step-ag-grid.esm.js +51515 -53615
- package/dist/step-ag-grid.esm.js.map +1 -1
- package/package.json +3 -3
- package/src/components/Grid.tsx +24 -55
- package/src/components/GridFilter.ts +12 -0
- package/src/components/GridWrapper.tsx +12 -0
- package/src/components/gridFilter/GridFilterQuick.tsx +29 -0
- package/src/components/gridFilter/GridFilters.tsx +7 -0
- package/src/contexts/GridContext.tsx +20 -0
- package/src/contexts/GridContextProvider.tsx +51 -17
- package/src/react-menu3/components/ControlledMenu.tsx +1 -1
- package/src/stories/grid/GridPopoverEditBearing.stories.tsx +0 -1
- package/src/stories/grid/GridReadOnly.stories.tsx +46 -14
- package/src/stories/grid/gridFormInteraction/GridFormMultiSelectInteraction.stories.tsx +46 -13
- package/src/styles/Grid.scss +11 -5
- package/src/utils/textValidator.test.ts +4 -0
- package/src/utils/textValidator.ts +1 -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": "12.0.0",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"aggrid",
|
|
8
8
|
"ag-grid",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@linzjs/lui": ">=17",
|
|
37
|
-
"ag-grid-community": "
|
|
38
|
-
"ag-grid-react": "^
|
|
37
|
+
"ag-grid-community": "27.3.0",
|
|
38
|
+
"ag-grid-react": "^27.3.0",
|
|
39
39
|
"debounce-promise": "^3.1.2",
|
|
40
40
|
"lodash-es": ">=4",
|
|
41
41
|
"matcher": "^5.0.0",
|
package/src/components/Grid.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import clsx from "clsx";
|
|
2
2
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { AgGridReact } from "ag-grid-react";
|
|
4
|
-
import { CellClickedEvent, ColDef } from "ag-grid-community";
|
|
5
|
-
import { CellEvent, GridReadyEvent, SelectionChangedEvent } from "ag-grid-community/dist/lib/events";
|
|
4
|
+
import { CellClickedEvent, ColDef, ModelUpdatedEvent } from "ag-grid-community";
|
|
5
|
+
import { AgGridEvent, CellEvent, GridReadyEvent, SelectionChangedEvent } from "ag-grid-community/dist/lib/events";
|
|
6
6
|
import { GridOptions } from "ag-grid-community/dist/lib/entities/gridOptions";
|
|
7
7
|
import { difference, isEmpty, last, xorBy } from "lodash-es";
|
|
8
8
|
import { GridContext } from "../contexts/GridContext";
|
|
@@ -20,16 +20,12 @@ export interface GridProps {
|
|
|
20
20
|
readOnly?: boolean; // set all editables to false when read only, make all styles black, otherwise style is gray for not editable
|
|
21
21
|
selectable?: boolean;
|
|
22
22
|
["data-testid"]?: string;
|
|
23
|
-
quickFilter?: boolean;
|
|
24
|
-
quickFilterPlaceholder?: string;
|
|
25
|
-
quickFilterValue?: string;
|
|
26
23
|
domLayout?: GridOptions["domLayout"];
|
|
27
24
|
externalSelectedItems?: any[];
|
|
28
25
|
setExternalSelectedItems?: (items: any[]) => void;
|
|
29
26
|
defaultColDef?: GridOptions["defaultColDef"];
|
|
30
27
|
columnDefs: ColDef[];
|
|
31
28
|
rowData: GridOptions["rowData"];
|
|
32
|
-
noRowsOverlayText?: string;
|
|
33
29
|
postSortRows?: GridOptions["postSortRows"];
|
|
34
30
|
animateRows?: boolean;
|
|
35
31
|
rowClassRules?: GridOptions["rowClassRules"];
|
|
@@ -47,7 +43,6 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
47
43
|
gridReady,
|
|
48
44
|
setGridApi,
|
|
49
45
|
prePopupOps,
|
|
50
|
-
setQuickFilter,
|
|
51
46
|
ensureRowVisible,
|
|
52
47
|
selectRowsById,
|
|
53
48
|
focusByRowById,
|
|
@@ -55,10 +50,11 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
55
50
|
sizeColumnsToFit,
|
|
56
51
|
externallySelectedItemsAreInSync,
|
|
57
52
|
setExternallySelectedItemsAreInSync,
|
|
53
|
+
isExternalFilterPresent,
|
|
54
|
+
doesExternalFilterPass,
|
|
58
55
|
} = useContext(GridContext);
|
|
59
56
|
const { checkUpdating } = useContext(GridUpdatingContext);
|
|
60
57
|
|
|
61
|
-
const [internalQuickFilter, setInternalQuickFilter] = useState("");
|
|
62
58
|
const lastSelectedIds = useRef<number[]>([]);
|
|
63
59
|
const [staleGrid, setStaleGrid] = useState(false);
|
|
64
60
|
const postSortRows = usePostSortRowsHook({ setStaleGrid });
|
|
@@ -142,26 +138,6 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
142
138
|
setExternallySelectedItemsAreInSync(true);
|
|
143
139
|
}, [gridReady, params.externalSelectedItems, ensureRowVisible, selectRowsById, setExternallySelectedItemsAreInSync]);
|
|
144
140
|
|
|
145
|
-
/**
|
|
146
|
-
* Synchronise quick filter to grid
|
|
147
|
-
*/
|
|
148
|
-
const updateQuickFilter = useCallback(() => {
|
|
149
|
-
if (!gridReady) return;
|
|
150
|
-
if (params.quickFilter) {
|
|
151
|
-
setQuickFilter(internalQuickFilter);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (params.quickFilterValue == null) return;
|
|
155
|
-
setQuickFilter(params.quickFilterValue);
|
|
156
|
-
}, [gridReady, internalQuickFilter, params.quickFilter, params.quickFilterValue, setQuickFilter]);
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Synchronise quick filter to grid
|
|
160
|
-
*/
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
updateQuickFilter();
|
|
163
|
-
}, [updateQuickFilter]);
|
|
164
|
-
|
|
165
141
|
const combineEditables =
|
|
166
142
|
(...editables: (boolean | EditableCallback | undefined)[]) =>
|
|
167
143
|
(params: EditableCallbackParams): boolean => {
|
|
@@ -201,7 +177,7 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
201
177
|
maxWidth: 42,
|
|
202
178
|
suppressSizeToFit: true,
|
|
203
179
|
checkboxSelection: true,
|
|
204
|
-
headerComponent: GridHeaderSelect,
|
|
180
|
+
headerComponent: params.rowSelection === "multiple" ? GridHeaderSelect : null,
|
|
205
181
|
suppressHeaderKeyboardEvent: (e) => {
|
|
206
182
|
if ((e.event.key === "Enter" || e.event.key === " ") && !e.event.repeat) {
|
|
207
183
|
const selectedNodeCount = e.api.getSelectedRows().length;
|
|
@@ -220,27 +196,37 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
220
196
|
]
|
|
221
197
|
: adjustColDefs;
|
|
222
198
|
}, [
|
|
223
|
-
clickSelectorCheckboxWhenContainingCellClicked,
|
|
224
199
|
params.columnDefs,
|
|
225
200
|
params.selectable,
|
|
201
|
+
params.rowSelection,
|
|
226
202
|
params.readOnly,
|
|
227
|
-
params.defaultColDef,
|
|
203
|
+
params.defaultColDef?.editable,
|
|
204
|
+
clickSelectorCheckboxWhenContainingCellClicked,
|
|
228
205
|
]);
|
|
229
206
|
|
|
230
207
|
const onGridReady = useCallback(
|
|
231
208
|
(event: GridReadyEvent) => {
|
|
232
209
|
setGridApi(event.api);
|
|
233
210
|
synchroniseExternallySelectedItemsToGrid();
|
|
234
|
-
updateQuickFilter();
|
|
235
211
|
},
|
|
236
|
-
[setGridApi, synchroniseExternallySelectedItemsToGrid
|
|
212
|
+
[setGridApi, synchroniseExternallySelectedItemsToGrid],
|
|
237
213
|
);
|
|
238
214
|
|
|
239
215
|
const noRowsOverlayComponent = useCallback(
|
|
240
|
-
() =>
|
|
241
|
-
|
|
216
|
+
(event: AgGridEvent) => {
|
|
217
|
+
const hasData = (params.rowData?.length ?? 0) > 0;
|
|
218
|
+
const hasFilteredData = event.api.getDisplayedRowCount() > 0;
|
|
219
|
+
return (
|
|
220
|
+
<span>{!hasData ? "There are currently no rows" : !hasFilteredData ? "All rows have been filtered" : ""}</span>
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
[params.rowData?.length],
|
|
242
224
|
);
|
|
243
225
|
|
|
226
|
+
const onModelUpdated = useCallback((event: ModelUpdatedEvent) => {
|
|
227
|
+
event.api.getDisplayedRowCount() === 0 ? event.api.showNoRowsOverlay() : event.api.hideOverlay();
|
|
228
|
+
}, []);
|
|
229
|
+
|
|
244
230
|
const refreshSelectedRows = useCallback((event: CellEvent): void => {
|
|
245
231
|
// Force-refresh all selected rows to re-run class function, to update selection highlighting
|
|
246
232
|
event.api.refreshCells({
|
|
@@ -316,11 +302,6 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
316
302
|
}
|
|
317
303
|
}, [columnDefs?.length, sizeColumnsToFit]);
|
|
318
304
|
|
|
319
|
-
const defaultColDef = useMemo(
|
|
320
|
-
() => ({ wrapHeaderText: true, autoHeaderHeight: true, ...params.defaultColDef }),
|
|
321
|
-
[params.defaultColDef],
|
|
322
|
-
);
|
|
323
|
-
|
|
324
305
|
return (
|
|
325
306
|
<div
|
|
326
307
|
data-testid={params["data-testid"]}
|
|
@@ -331,25 +312,10 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
331
312
|
gridReady && params.rowData && "Grid-ready",
|
|
332
313
|
)}
|
|
333
314
|
>
|
|
334
|
-
{params.quickFilter && (
|
|
335
|
-
<div className="Grid-quickFilter">
|
|
336
|
-
<input
|
|
337
|
-
aria-label="Search"
|
|
338
|
-
className="lui-margin-top-xxs lui-margin-bottom-xxs Grid-quickFilterBox"
|
|
339
|
-
type="text"
|
|
340
|
-
placeholder={params.quickFilterPlaceholder ?? "Search..."}
|
|
341
|
-
value={internalQuickFilter}
|
|
342
|
-
onChange={(event): void => {
|
|
343
|
-
setInternalQuickFilter(event.target.value);
|
|
344
|
-
}}
|
|
345
|
-
/>
|
|
346
|
-
</div>
|
|
347
|
-
)}
|
|
348
315
|
<div style={{ flex: 1 }}>
|
|
349
316
|
<AgGridReact
|
|
350
317
|
animateRows={params.animateRows}
|
|
351
318
|
rowClassRules={params.rowClassRules}
|
|
352
|
-
defaultColDef={defaultColDef}
|
|
353
319
|
getRowId={(params) => `${params.data.id}`}
|
|
354
320
|
suppressRowClickSelection={true}
|
|
355
321
|
rowSelection={params.rowSelection ?? "multiple"}
|
|
@@ -366,12 +332,15 @@ export const Grid = (params: GridProps): JSX.Element => {
|
|
|
366
332
|
columnDefs={columnDefs}
|
|
367
333
|
rowData={params.rowData}
|
|
368
334
|
noRowsOverlayComponent={noRowsOverlayComponent}
|
|
335
|
+
onModelUpdated={onModelUpdated}
|
|
369
336
|
onGridReady={onGridReady}
|
|
370
337
|
onSortChanged={ensureSelectedRowIsVisible}
|
|
371
338
|
postSortRows={params.postSortRows ?? postSortRows}
|
|
372
339
|
onSelectionChanged={synchroniseExternalStateToGridSelection}
|
|
373
340
|
onColumnMoved={params.onColumnMoved}
|
|
374
341
|
alwaysShowVerticalScroll={params.alwaysShowVerticalScroll}
|
|
342
|
+
isExternalFilterPresent={isExternalFilterPresent}
|
|
343
|
+
doesExternalFilterPass={doesExternalFilterPass}
|
|
375
344
|
/>
|
|
376
345
|
</div>
|
|
377
346
|
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useContext, useEffect } from "react";
|
|
2
|
+
import { GridContext, GridFilterExternal } from "../contexts/GridContext";
|
|
3
|
+
|
|
4
|
+
export const useGridFilter = (filter: GridFilterExternal) => {
|
|
5
|
+
const { addExternalFilter, removeExternalFilter } = useContext(GridContext);
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const thisFilter = filter;
|
|
9
|
+
addExternalFilter(thisFilter);
|
|
10
|
+
return () => removeExternalFilter(thisFilter);
|
|
11
|
+
}, [addExternalFilter, filter, removeExternalFilter]);
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface GridWrapperProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
maxHeight?: number | string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const GridWrapper = ({ children, maxHeight }: GridWrapperProps) => (
|
|
9
|
+
<div className={"Grid-wrapper"} style={{ maxHeight }}>
|
|
10
|
+
{children}
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useContext, useEffect, useState } from "react";
|
|
2
|
+
import { GridContext } from "../../contexts/GridContext";
|
|
3
|
+
|
|
4
|
+
export interface GridFilterQuickProps {
|
|
5
|
+
quickFilterPlaceholder?: string;
|
|
6
|
+
defaultValue?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const GridFilterQuick = ({ quickFilterPlaceholder, defaultValue }: GridFilterQuickProps) => {
|
|
10
|
+
const { setQuickFilter } = useContext(GridContext);
|
|
11
|
+
const [quickFilterValue, setQuickFilterValue] = useState(defaultValue ?? "");
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setQuickFilter(quickFilterValue);
|
|
15
|
+
}, [quickFilterValue, setQuickFilter]);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<input
|
|
19
|
+
aria-label="Search"
|
|
20
|
+
className="lui-margin-top-xxs lui-margin-bottom-xxs Grid-quickFilterBox"
|
|
21
|
+
type="text"
|
|
22
|
+
placeholder={quickFilterPlaceholder ?? "Search..."}
|
|
23
|
+
value={quickFilterValue}
|
|
24
|
+
onChange={(event): void => {
|
|
25
|
+
setQuickFilterValue(event.target.value);
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -2,6 +2,8 @@ import { createContext } from "react";
|
|
|
2
2
|
import { GridApi, RowNode } from "ag-grid-community";
|
|
3
3
|
import { GridBaseRow } from "../components/Grid";
|
|
4
4
|
|
|
5
|
+
export type GridFilterExternal = (data: any, rowNode: RowNode) => boolean;
|
|
6
|
+
|
|
5
7
|
export interface GridContextType {
|
|
6
8
|
gridReady: boolean;
|
|
7
9
|
setGridApi: (gridApi: GridApi | undefined) => void;
|
|
@@ -31,6 +33,10 @@ export interface GridContextType {
|
|
|
31
33
|
externallySelectedItemsAreInSync: boolean;
|
|
32
34
|
setExternallySelectedItemsAreInSync: (inSync: boolean) => void;
|
|
33
35
|
waitForExternallySelectedItemsToBeInSync: () => Promise<void>;
|
|
36
|
+
addExternalFilter: (filter: GridFilterExternal) => void;
|
|
37
|
+
removeExternalFilter: (filter: GridFilterExternal) => void;
|
|
38
|
+
isExternalFilterPresent: () => boolean;
|
|
39
|
+
doesExternalFilterPass: (node: RowNode) => boolean;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export const GridContext = createContext<GridContextType>({
|
|
@@ -104,4 +110,18 @@ export const GridContext = createContext<GridContextType>({
|
|
|
104
110
|
waitForExternallySelectedItemsToBeInSync: async () => {
|
|
105
111
|
console.error("no context provider for waitForExternallySelectedItemsToBeInSync");
|
|
106
112
|
},
|
|
113
|
+
addExternalFilter: () => {
|
|
114
|
+
console.error("no context provider for addExternalFilter");
|
|
115
|
+
},
|
|
116
|
+
removeExternalFilter: () => {
|
|
117
|
+
console.error("no context provider for removeExternalFilter");
|
|
118
|
+
},
|
|
119
|
+
isExternalFilterPresent: () => {
|
|
120
|
+
console.error("no context provider for isExternalFilterPresent");
|
|
121
|
+
return false;
|
|
122
|
+
},
|
|
123
|
+
doesExternalFilterPass: () => {
|
|
124
|
+
console.error("no context provider for doesExternalFilterPass");
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
107
127
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ReactElement, ReactNode, useCallback, useContext, useRef, useState } from "react";
|
|
1
|
+
import { ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { ColDef, GridApi, RowNode } from "ag-grid-community";
|
|
3
|
-
import { GridContext } from "./GridContext";
|
|
4
|
-
import { defer, delay, difference, isEmpty, last, sortBy } from "lodash-es";
|
|
3
|
+
import { GridContext, GridFilterExternal } from "./GridContext";
|
|
4
|
+
import { debounce, defer, delay, difference, isEmpty, last, remove, sortBy } from "lodash-es";
|
|
5
5
|
import { isNotEmpty, wait } from "../utils/util";
|
|
6
6
|
import { GridUpdatingContext } from "./GridUpdatingContext";
|
|
7
7
|
import { GridBaseRow } from "../components/Grid";
|
|
@@ -20,14 +20,30 @@ export const GridContextProvider = (props: GridContextProps): ReactElement => {
|
|
|
20
20
|
const { modifyUpdating } = useContext(GridUpdatingContext);
|
|
21
21
|
const [gridApi, _setGridApi] = useState<GridApi>();
|
|
22
22
|
const [gridReady, setGridReady] = useState(false);
|
|
23
|
+
const [quickFilter, setQuickFilter] = useState("");
|
|
23
24
|
const idsBeforeUpdate = useRef<number[]>([]);
|
|
24
25
|
const prePopupFocusedCell = useRef<CellPosition>();
|
|
25
26
|
const [externallySelectedItemsAreInSync, setExternallySelectedItemsAreInSync] = useState(false);
|
|
27
|
+
const externalFilters = useRef<GridFilterExternal[]>([]);
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Set quick filter directly on grid, based on previously save quickFilter state.
|
|
31
|
+
*/
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
gridApi?.setQuickFilter(quickFilter);
|
|
34
|
+
}, [gridApi, quickFilter]);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set the grid api when the grid is ready.
|
|
38
|
+
*/
|
|
39
|
+
const setGridApi = useCallback(
|
|
40
|
+
(gridApi: GridApi | undefined) => {
|
|
41
|
+
_setGridApi(gridApi);
|
|
42
|
+
gridApi?.setQuickFilter(quickFilter);
|
|
43
|
+
setGridReady(!!gridApi);
|
|
44
|
+
},
|
|
45
|
+
[quickFilter],
|
|
46
|
+
);
|
|
31
47
|
|
|
32
48
|
/**
|
|
33
49
|
* Wraps things that require gridApi in common handling, for when gridApi not present.
|
|
@@ -52,16 +68,6 @@ export const GridContextProvider = (props: GridContextProps): ReactElement => {
|
|
|
52
68
|
prePopupFocusedCell.current = gridApi?.getFocusedCell() ?? undefined;
|
|
53
69
|
}, [gridApi]);
|
|
54
70
|
|
|
55
|
-
/**
|
|
56
|
-
* Set the quick filter value to grid.
|
|
57
|
-
*/
|
|
58
|
-
const setQuickFilter = useCallback(
|
|
59
|
-
(quickFilter: string) => {
|
|
60
|
-
gridApiOp((gridApi) => gridApi.setQuickFilter(quickFilter));
|
|
61
|
-
},
|
|
62
|
-
[gridApiOp],
|
|
63
|
-
);
|
|
64
|
-
|
|
65
71
|
/**
|
|
66
72
|
* Get all row id's in grid.
|
|
67
73
|
*/
|
|
@@ -376,6 +382,30 @@ export const GridContextProvider = (props: GridContextProps): ReactElement => {
|
|
|
376
382
|
}
|
|
377
383
|
}, [externallySelectedItemsAreInSync]);
|
|
378
384
|
|
|
385
|
+
const onFilterChanged = useMemo(
|
|
386
|
+
() =>
|
|
387
|
+
debounce(() => {
|
|
388
|
+
gridApi && gridApi.onFilterChanged();
|
|
389
|
+
}, 200),
|
|
390
|
+
[gridApi],
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const addExternalFilter = (filter: GridFilterExternal) => {
|
|
394
|
+
externalFilters.current.push(filter);
|
|
395
|
+
onFilterChanged();
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const removeExternalFilter = (filter: GridFilterExternal) => {
|
|
399
|
+
remove(externalFilters.current, (v) => v === filter);
|
|
400
|
+
onFilterChanged();
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const isExternalFilterPresent = (): boolean => externalFilters.current.length > 0;
|
|
404
|
+
|
|
405
|
+
const doesExternalFilterPass = (node: RowNode): boolean => {
|
|
406
|
+
return externalFilters.current.every((filter) => filter(node.data, node));
|
|
407
|
+
};
|
|
408
|
+
|
|
379
409
|
return (
|
|
380
410
|
<GridContext.Provider
|
|
381
411
|
value={{
|
|
@@ -402,6 +432,10 @@ export const GridContextProvider = (props: GridContextProps): ReactElement => {
|
|
|
402
432
|
externallySelectedItemsAreInSync,
|
|
403
433
|
setExternallySelectedItemsAreInSync,
|
|
404
434
|
waitForExternallySelectedItemsToBeInSync,
|
|
435
|
+
addExternalFilter,
|
|
436
|
+
removeExternalFilter,
|
|
437
|
+
isExternalFilterPresent,
|
|
438
|
+
doesExternalFilterPass,
|
|
405
439
|
}}
|
|
406
440
|
>
|
|
407
441
|
{props.children}
|
|
@@ -134,7 +134,7 @@ export const ControlledMenuFr = (
|
|
|
134
134
|
};
|
|
135
135
|
|
|
136
136
|
const allowTabToSave = activeElement.getAttribute("data-allowtabtosave") == "true";
|
|
137
|
-
if (allowTabToSave) {
|
|
137
|
+
if (allowTabToSave && ev.key === "Tab") {
|
|
138
138
|
if (isDown) {
|
|
139
139
|
ev.preventDefault();
|
|
140
140
|
ev.stopPropagation();
|
|
@@ -7,15 +7,18 @@ import { ComponentMeta, ComponentStory } from "@storybook/react/dist/ts3.9/clien
|
|
|
7
7
|
import { GridUpdatingContextProvider } from "../../contexts/GridUpdatingContextProvider";
|
|
8
8
|
import { GridContextProvider } from "../../contexts/GridContextProvider";
|
|
9
9
|
import { Grid, GridProps } from "../../components/Grid";
|
|
10
|
-
import { useMemo, useState } from "react";
|
|
10
|
+
import { useCallback, useMemo, useState } from "react";
|
|
11
11
|
import { wait } from "../../utils/util";
|
|
12
12
|
import { GridPopoverMenu } from "../../components/gridPopoverEdit/GridPopoverMenu";
|
|
13
13
|
import { ColDefT, GridCell } from "../../components/GridCell";
|
|
14
14
|
import { GridPopoverMessage } from "../../components/gridPopoverEdit/GridPopoverMessage";
|
|
15
|
-
import { MenuOption } from "../../components/gridForm/GridFormPopoverMenu";
|
|
16
15
|
import { GridFormSubComponentTextInput } from "../../components/gridForm/GridFormSubComponentTextInput";
|
|
17
16
|
import { GridFormSubComponentTextArea } from "../../components/gridForm/GridFormSubComponentTextArea";
|
|
18
17
|
import { GridIcon } from "../../components/GridIcon";
|
|
18
|
+
import { useGridFilter } from "../../components/GridFilter";
|
|
19
|
+
import { GridFilterQuick } from "../../components/gridFilter/GridFilterQuick";
|
|
20
|
+
import { GridFilters } from "../../components/gridFilter/GridFilters";
|
|
21
|
+
import { GridWrapper } from "../../components/GridWrapper";
|
|
19
22
|
|
|
20
23
|
export default {
|
|
21
24
|
title: "Components / Grids",
|
|
@@ -29,7 +32,7 @@ export default {
|
|
|
29
32
|
},
|
|
30
33
|
decorators: [
|
|
31
34
|
(Story) => (
|
|
32
|
-
<div style={{ width: 1024, height: 400 }}>
|
|
35
|
+
<div style={{ width: 1024, height: 400, display: "flex", flexDirection: "column" }}>
|
|
33
36
|
<GridUpdatingContextProvider>
|
|
34
37
|
<GridContextProvider>
|
|
35
38
|
<Story />
|
|
@@ -173,7 +176,7 @@ const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) =>
|
|
|
173
176
|
<GridFormSubComponentTextArea placeholder={"Other"} maxLength={5} required defaultValue={""} />
|
|
174
177
|
),
|
|
175
178
|
},
|
|
176
|
-
]
|
|
179
|
+
];
|
|
177
180
|
},
|
|
178
181
|
},
|
|
179
182
|
},
|
|
@@ -201,17 +204,46 @@ const GridReadOnlyTemplate: ComponentStory<typeof Grid> = (props: GridProps) =>
|
|
|
201
204
|
]);
|
|
202
205
|
|
|
203
206
|
return (
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
207
|
+
<GridWrapper maxHeight={200}>
|
|
208
|
+
<GridFilters>
|
|
209
|
+
<GridFilterQuick quickFilterPlaceholder={"Custom placeholder..."} />
|
|
210
|
+
<div>
|
|
211
|
+
Custom filter: Age less than:
|
|
212
|
+
<GridFilterLessThan field={"age"} />
|
|
213
|
+
</div>
|
|
214
|
+
</GridFilters>
|
|
215
|
+
<Grid
|
|
216
|
+
{...props}
|
|
217
|
+
selectable={true}
|
|
218
|
+
autoSelectFirstRow={true}
|
|
219
|
+
externalSelectedItems={externalSelectedItems}
|
|
220
|
+
setExternalSelectedItems={setExternalSelectedItems}
|
|
221
|
+
columnDefs={columnDefs}
|
|
222
|
+
rowData={rowData}
|
|
223
|
+
/>
|
|
224
|
+
</GridWrapper>
|
|
214
225
|
);
|
|
215
226
|
};
|
|
216
227
|
|
|
228
|
+
const GridFilterLessThan = (props: { field: keyof ITestRow }): JSX.Element => {
|
|
229
|
+
const [value, setValue] = useState<number>();
|
|
230
|
+
|
|
231
|
+
const filter = useCallback(
|
|
232
|
+
(data: ITestRow): boolean => value == null || data[props.field] < value,
|
|
233
|
+
[props.field, value],
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
useGridFilter(filter);
|
|
237
|
+
|
|
238
|
+
const updateValue = (newValue: string) => {
|
|
239
|
+
try {
|
|
240
|
+
setValue(newValue.trim() == "" ? undefined : parseInt(newValue));
|
|
241
|
+
} catch {
|
|
242
|
+
// ignore number parse exception
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return <input type={"text"} defaultValue={value} onChange={(e) => updateValue(e.target.value)} />;
|
|
247
|
+
};
|
|
248
|
+
|
|
217
249
|
export const ReadOnlySingleSelection = GridReadOnlyTemplate.bind({});
|
|
@@ -27,23 +27,26 @@ const updateValue = jest
|
|
|
27
27
|
.mockImplementation((saveFn: (selectedRows: any[]) => Promise<boolean>, _tabDirection: 1 | 0 | -1) => saveFn([]));
|
|
28
28
|
|
|
29
29
|
const onSave = jest.fn<Promise<boolean>, [GridFormMultiSelectSaveProps<any>]>().mockImplementation(async () => true);
|
|
30
|
+
const onSelectFilter = jest.fn();
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
{ label: "Zero", value: 0 },
|
|
33
|
-
{ label: "One", value: 1 },
|
|
34
|
-
{
|
|
35
|
-
label: "Sub component",
|
|
36
|
-
value: 2,
|
|
37
|
-
subComponent: () => (
|
|
38
|
-
<GridFormSubComponentTextInput placeholder={"Text input"} maxLength={5} required defaultValue={""} />
|
|
39
|
-
),
|
|
40
|
-
},
|
|
41
|
-
{ label: "Other", value: 3 },
|
|
42
|
-
]) as MultiSelectOption[];
|
|
43
|
-
|
|
32
|
+
let options: MultiSelectOption[] = [];
|
|
44
33
|
const Template: ComponentStory<typeof GridFormMultiSelect> = (props) => {
|
|
34
|
+
options = [
|
|
35
|
+
{ label: "Zero", value: 0 },
|
|
36
|
+
{ label: "One", value: 1 },
|
|
37
|
+
{
|
|
38
|
+
label: "Sub component",
|
|
39
|
+
value: 2,
|
|
40
|
+
subComponent: () => (
|
|
41
|
+
<GridFormSubComponentTextInput placeholder={"Text input"} maxLength={5} required defaultValue={""} />
|
|
42
|
+
),
|
|
43
|
+
},
|
|
44
|
+
{ label: "Other", value: 3 },
|
|
45
|
+
];
|
|
45
46
|
const config: GridFormMultiSelectProps<any> = {
|
|
46
47
|
filtered: true,
|
|
48
|
+
onSelectFilter,
|
|
49
|
+
filterHelpText: "Press enter to add free-text",
|
|
47
50
|
onSave,
|
|
48
51
|
options,
|
|
49
52
|
};
|
|
@@ -76,6 +79,10 @@ const Template: ComponentStory<typeof GridFormMultiSelect> = (props) => {
|
|
|
76
79
|
|
|
77
80
|
export const GridFormMultiSelectInteractions_ = Template.bind({});
|
|
78
81
|
GridFormMultiSelectInteractions_.play = async ({ canvasElement }) => {
|
|
82
|
+
updateValue.mockClear();
|
|
83
|
+
onSave.mockClear();
|
|
84
|
+
onSelectFilter.mockClear();
|
|
85
|
+
|
|
79
86
|
const canvas = within(canvasElement);
|
|
80
87
|
|
|
81
88
|
const getOption = (name: RegExp | string) => canvas.findByRole("menuitem", { name });
|
|
@@ -150,4 +157,30 @@ GridFormMultiSelectInteractions_.play = async ({ canvasElement }) => {
|
|
|
150
157
|
expect(canvas.queryByText("Zero")).not.toBeInTheDocument();
|
|
151
158
|
expect(canvas.queryByText("Sub component")).not.toBeInTheDocument();
|
|
152
159
|
expect(canvas.queryByText("Other")).not.toBeInTheDocument();
|
|
160
|
+
|
|
161
|
+
userEvent.type(filterText, "x");
|
|
162
|
+
expect(canvas.queryByText("One")).not.toBeInTheDocument();
|
|
163
|
+
expect(canvas.queryByText("No Options")).toBeInTheDocument();
|
|
164
|
+
|
|
165
|
+
// Check enter works to add custom free-text
|
|
166
|
+
userEvent.type(filterText, "{Enter}");
|
|
167
|
+
expect(onSelectFilter).toHaveBeenCalledWith({
|
|
168
|
+
filter: "onx",
|
|
169
|
+
options: [
|
|
170
|
+
{
|
|
171
|
+
...options[0],
|
|
172
|
+
checked: true,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
...options[1],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
...options[2],
|
|
179
|
+
checked: true,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
...options[3],
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
153
186
|
};
|
package/src/styles/Grid.scss
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
flex-direction: column;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
.Grid-wrapper {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
flex: 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
.Grid-sortIsStale {
|
|
13
19
|
span.ag-icon.ag-icon-desc::after {
|
|
14
20
|
content: "*";
|
|
@@ -19,12 +25,16 @@
|
|
|
19
25
|
}
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
.Grid-
|
|
28
|
+
.Grid-container-filters {
|
|
23
29
|
width: 100%;
|
|
24
30
|
margin-bottom: 16px;
|
|
25
31
|
flex: 0;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: row;
|
|
34
|
+
grid-column-gap: 1em;
|
|
26
35
|
}
|
|
27
36
|
|
|
37
|
+
|
|
28
38
|
.Grid-quickFilterBox {
|
|
29
39
|
width: 100%;
|
|
30
40
|
height: 48px;
|
|
@@ -71,7 +81,3 @@
|
|
|
71
81
|
.Grid-container.ag-theme-alpine .ag-cell-wrapper {
|
|
72
82
|
width: 100%;
|
|
73
83
|
}
|
|
74
|
-
|
|
75
|
-
.Grid-container.ag-theme-alpine ag-cell-label-container {
|
|
76
|
-
padding: 0;
|
|
77
|
-
}
|
|
@@ -73,6 +73,10 @@ describe("TextInputValidator", () => {
|
|
|
73
73
|
tests: [
|
|
74
74
|
["xx", null],
|
|
75
75
|
["", "Must not be empty"],
|
|
76
|
+
["\t", "Must not be empty"],
|
|
77
|
+
["\n", "Must not be empty"],
|
|
78
|
+
["\r", "Must not be empty"],
|
|
79
|
+
[" ", "Must not be empty"],
|
|
76
80
|
],
|
|
77
81
|
},
|
|
78
82
|
] as (TextInputValidatorProps<GridBaseRow> & { tests: [string, string | undefined][] })[];
|
|
@@ -28,7 +28,7 @@ export const TextInputValidator = <RowType extends GridBaseRow>(
|
|
|
28
28
|
if (typeof value !== "string") {
|
|
29
29
|
return "Value is not a string";
|
|
30
30
|
}
|
|
31
|
-
if (props.required && value == "") {
|
|
31
|
+
if (props.required && value.trim() == "") {
|
|
32
32
|
return "Must not be empty";
|
|
33
33
|
}
|
|
34
34
|
if (props.maxLength && value.length > props.maxLength) {
|