@olenbetong/appframe-ds 1.10.0 → 1.11.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/CHANGELOG.md +14 -0
- package/package.json +6 -6
- package/src/filter/FilterBuilder.test.tsx +18 -0
- package/src/filter/FilterBuilder.tsx +24 -1
- package/src/filter/FilterGroupEditor.tsx +1 -0
- package/src/filter/FilterRow.tsx +5 -1
- package/src/grid/AfColumnMenuColumnsItem.tsx +30 -0
- package/src/grid/AfColumnMenuFilterItem.tsx +36 -0
- package/src/grid/AfGridContext.tsx +5 -2
- package/src/grid/Toolbar.tsx +2 -2
- package/src/grid/index.tsx +33 -6
- package/src/grid/useAfNewItemRow.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @olenbetong/appframe-ds
|
|
2
2
|
|
|
3
|
+
## 1.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- aeeb072: The grid's new item row is pinned at the top by default, instead of following the data object's `newRecordPosition`. At the bottom it sat at the foot of the viewport, far below the data on a short grid. Pass `newItemRow="bottom"` to keep it there.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 4c6a4e7: `endEdit` takes `{ newRecordPosition }` to place a record created from the new record first or last for that save only, overriding the data object's option. The grid uses it so a record saved from the new item row at the top is inserted first, where it was entered, instead of jumping to the end of the data.
|
|
12
|
+
- f4a37c0: The grid's column menu opens the grid's own panels instead of MUI's: Filter opens the filter builder with a new criterion for the column, its value focused (MUI's filter panel edited a filter the grid ignores), and Manage columns opens the grid's columns panel. `FilterBuilder` takes `autoFocusPath` to focus the value of a criterion when it opens.
|
|
13
|
+
- Updated dependencies [c7750da]
|
|
14
|
+
- Updated dependencies [4c6a4e7]
|
|
15
|
+
- @olenbetong/appframe-data@1.10.0
|
|
16
|
+
|
|
3
17
|
## 1.10.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-ds",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "Components that bind Designsystemet components to Appframe data objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./es/index.d.ts",
|
|
@@ -64,15 +64,15 @@
|
|
|
64
64
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "3.0.0",
|
|
65
65
|
"@base-ui/react": "1.9.0",
|
|
66
66
|
"@olenbetong/appframe-core": "^2.16.0",
|
|
67
|
-
"@olenbetong/appframe-data": "^1.
|
|
67
|
+
"@olenbetong/appframe-data": "^1.10.0",
|
|
68
68
|
"@olenbetong/appframe-react": "^2.2.8",
|
|
69
69
|
"clsx": "^2.1.1",
|
|
70
|
-
"dockview-react": "8.
|
|
70
|
+
"dockview-react": "8.8.3",
|
|
71
71
|
"localforage": "^1.10.0",
|
|
72
72
|
"mitt": "^3.0.1",
|
|
73
73
|
"react": "19.3.1",
|
|
74
74
|
"react-dom": "19.3.1",
|
|
75
|
-
"react-window": "2.5.
|
|
75
|
+
"react-window": "2.5.3",
|
|
76
76
|
"temporal-polyfill": "1.0.6"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"@mui/material": "9.5.0",
|
|
83
83
|
"@mui/x-data-grid-premium": "8.29.5",
|
|
84
84
|
"@mui/x-license": "8.29.3",
|
|
85
|
-
"@navikt/aksel-icons": "8.
|
|
85
|
+
"@navikt/aksel-icons": "8.21.0",
|
|
86
86
|
"@testing-library/dom": "^10.4.2",
|
|
87
87
|
"@testing-library/jest-dom": "^7.0.1",
|
|
88
88
|
"@testing-library/react": "16.3.3",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"@types/react-dom": "19.3.0",
|
|
92
92
|
"happy-dom": "21.0.4",
|
|
93
93
|
"react-error-boundary": "6.1.6",
|
|
94
|
-
"react-router": "8.5.
|
|
94
|
+
"react-router": "8.5.1",
|
|
95
95
|
"typescript": "7.0.2",
|
|
96
96
|
"vitest": "5.0.5"
|
|
97
97
|
},
|
|
@@ -122,6 +122,24 @@ describe("FilterBuilder", () => {
|
|
|
122
122
|
);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
+
it("focuses the value of the criterion at autoFocusPath", () => {
|
|
126
|
+
renderBuilder({
|
|
127
|
+
autoFocusPath: [1],
|
|
128
|
+
defaultValue: {
|
|
129
|
+
type: "group",
|
|
130
|
+
mode: "and",
|
|
131
|
+
items: [
|
|
132
|
+
{ type: "expression", column: "PersonID", operator: "like", value: "a", valueType: "string" },
|
|
133
|
+
{ type: "expression", column: "Age", operator: "equals", value: "", valueType: "number" },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let values = screen.getAllByLabelText("Value");
|
|
139
|
+
|
|
140
|
+
expect(document.activeElement).toBe(values[1]);
|
|
141
|
+
});
|
|
142
|
+
|
|
125
143
|
it("reports an empty filter as null", async () => {
|
|
126
144
|
let onApply = vi.fn();
|
|
127
145
|
renderBuilder({ onApply });
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
canShareSavedFilter,
|
|
9
9
|
createEmptyFilter,
|
|
10
10
|
findSavedFilter,
|
|
11
|
+
type FilterPath,
|
|
11
12
|
type FilterTree,
|
|
12
13
|
isEmptyFilter,
|
|
13
14
|
pruneFilter,
|
|
@@ -17,7 +18,7 @@ import { type NamedFilterType, setNamedFilter, useFilterConversion, useSavedFilt
|
|
|
17
18
|
import type { SaveFilterInput } from "@olenbetong/appframe-react";
|
|
18
19
|
import type { DataObject } from "@olenbetong/appframe-data";
|
|
19
20
|
import clsx from "clsx";
|
|
20
|
-
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
|
21
|
+
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|
21
22
|
|
|
22
23
|
import { FilterEditor } from "./FilterEditor.js";
|
|
23
24
|
import { FilterNameDialog } from "./FilterNameDialog.js";
|
|
@@ -109,6 +110,11 @@ export type FilterBuilderProps = {
|
|
|
109
110
|
renderMain?: (editor: ReactNode) => ReactNode;
|
|
110
111
|
/** Hides the saved-filter list, leaving just the criteria editor. */
|
|
111
112
|
hideSavedFilters?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Path of the criterion whose value gets focus when the builder opens, for a caller that has
|
|
115
|
+
* just added it.
|
|
116
|
+
*/
|
|
117
|
+
autoFocusPath?: FilterPath | null;
|
|
112
118
|
className?: string;
|
|
113
119
|
"data-size"?: FilterEditorSize;
|
|
114
120
|
};
|
|
@@ -159,9 +165,25 @@ export function FilterBuilder({
|
|
|
159
165
|
actions,
|
|
160
166
|
renderMain,
|
|
161
167
|
hideSavedFilters,
|
|
168
|
+
autoFocusPath,
|
|
162
169
|
className,
|
|
163
170
|
"data-size": size,
|
|
164
171
|
}: FilterBuilderProps) {
|
|
172
|
+
let rootRef = useRef<HTMLDivElement>(null);
|
|
173
|
+
let autoFocusKey = autoFocusPath?.join(".");
|
|
174
|
+
|
|
175
|
+
/*
|
|
176
|
+
* Runs after the dialog's own effect has opened it, which focuses its first control. The value
|
|
177
|
+
* editor depends on the field and operator, so the first input in the row's value cell is used.
|
|
178
|
+
*/
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!open || autoFocusKey === undefined) return;
|
|
181
|
+
|
|
182
|
+
let row = rootRef.current?.querySelector(`.ObFilterEditor-row[data-path="${autoFocusKey}"]`);
|
|
183
|
+
let input = row?.querySelector<HTMLElement>(".ObFilterEditor-valueCell :is(input, select, textarea)");
|
|
184
|
+
|
|
185
|
+
input?.focus();
|
|
186
|
+
}, [open, autoFocusKey]);
|
|
165
187
|
let resolvedId = dbObjectId ?? viewName;
|
|
166
188
|
let { mine, shared, filters, loading, save, rename, remove, setCrossDomain } = useSavedFilters({
|
|
167
189
|
dbObjectId: reportId ? undefined : resolvedId,
|
|
@@ -299,6 +321,7 @@ export function FilterBuilder({
|
|
|
299
321
|
|
|
300
322
|
let content = (
|
|
301
323
|
<div
|
|
324
|
+
ref={rootRef}
|
|
302
325
|
className={clsx("ObFilterBuilder-root", hideSavedFilters && "ObFilterBuilder-root--noSavedFilters", className)}
|
|
303
326
|
data-size={size}
|
|
304
327
|
>
|
package/src/filter/FilterRow.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type FieldMetadata,
|
|
7
7
|
type FilterExpression,
|
|
8
8
|
type FilterOperator,
|
|
9
|
+
type FilterPath,
|
|
9
10
|
getOperatorLabel,
|
|
10
11
|
getOperatorsForField,
|
|
11
12
|
isRangeEnd,
|
|
@@ -34,6 +35,8 @@ export type FilterRowProps = {
|
|
|
34
35
|
fields: FieldMetadata[];
|
|
35
36
|
onChange: (expression: FilterExpression) => void;
|
|
36
37
|
onRemove: () => void;
|
|
38
|
+
/** Where the expression sits in the tree, written to `data-path` so a row can be found to focus it. */
|
|
39
|
+
path?: FilterPath;
|
|
37
40
|
getFieldOptions?: GetFieldOptions;
|
|
38
41
|
readOnly?: boolean;
|
|
39
42
|
"data-size"?: FilterEditorSize;
|
|
@@ -45,6 +48,7 @@ export function FilterRow({
|
|
|
45
48
|
fields,
|
|
46
49
|
onChange,
|
|
47
50
|
onRemove,
|
|
51
|
+
path,
|
|
48
52
|
getFieldOptions,
|
|
49
53
|
readOnly,
|
|
50
54
|
"data-size": size,
|
|
@@ -76,7 +80,7 @@ export function FilterRow({
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
return (
|
|
79
|
-
<div className="ObFilterEditor-row">
|
|
83
|
+
<div className="ObFilterEditor-row" data-path={path?.join(".")}>
|
|
80
84
|
<Combobox
|
|
81
85
|
className="ObFilterEditor-field"
|
|
82
86
|
data-size={size}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type GridColumnMenuItemProps, GridColumnMenuHideItem, useGridRootProps } from "@mui/x-data-grid-premium";
|
|
2
|
+
|
|
3
|
+
import { useOptionalAfGridContext } from "./AfGridContext.js";
|
|
4
|
+
import { getLocalizedString } from "../i18n.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The column menu's Hide and Manage columns items. MUI's Manage columns opens its own columns panel;
|
|
8
|
+
* this opens the grid's columns panel, the same one as the toolbar's Columns button.
|
|
9
|
+
*/
|
|
10
|
+
export function AfColumnMenuColumnsItem(props: GridColumnMenuItemProps) {
|
|
11
|
+
let rootProps = useGridRootProps();
|
|
12
|
+
let context = useOptionalAfGridContext();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<GridColumnMenuHideItem {...props} />
|
|
17
|
+
{context && !rootProps.disableColumnSelector && (
|
|
18
|
+
<rootProps.slots.baseMenuItem
|
|
19
|
+
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
|
20
|
+
props.onClick(event);
|
|
21
|
+
context.openColumnsPanel();
|
|
22
|
+
}}
|
|
23
|
+
iconStart={<rootProps.slots.columnMenuManageColumnsIcon fontSize="small" />}
|
|
24
|
+
>
|
|
25
|
+
{getLocalizedString("Manage columns")}
|
|
26
|
+
</rootProps.slots.baseMenuItem>
|
|
27
|
+
)}
|
|
28
|
+
</>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type GridColumnMenuItemProps, useGridRootProps } from "@mui/x-data-grid-premium";
|
|
2
|
+
|
|
3
|
+
import { useOptionalAfGridContext } from "./AfGridContext.js";
|
|
4
|
+
import { findFilterField } from "./useAfFilter.js";
|
|
5
|
+
import type { AfGridColDef } from "./useAfColumns.js";
|
|
6
|
+
import { getLocalizedString } from "../i18n.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The column menu's Filter item. MUI's opens its own filter panel, which edits a filter model the
|
|
10
|
+
* grid never reads; this opens the filter builder with a new criterion for the column instead.
|
|
11
|
+
*/
|
|
12
|
+
export function AfColumnMenuFilterItem({ colDef, onClick }: GridColumnMenuItemProps) {
|
|
13
|
+
let rootProps = useGridRootProps();
|
|
14
|
+
let context = useOptionalAfGridContext();
|
|
15
|
+
let fieldName = (colDef as AfGridColDef).filterColumn ?? colDef.field;
|
|
16
|
+
|
|
17
|
+
if (!context || rootProps.disableColumnFilter || colDef.filterable === false || colDef.type === "actions") {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!findFilterField(context.filterFields, fieldName)) return null;
|
|
22
|
+
|
|
23
|
+
let { openFilterBuilder } = context;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<rootProps.slots.baseMenuItem
|
|
27
|
+
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
|
28
|
+
onClick(event);
|
|
29
|
+
openFilterBuilder(fieldName);
|
|
30
|
+
}}
|
|
31
|
+
iconStart={<rootProps.slots.columnMenuFilterIcon fontSize="small" />}
|
|
32
|
+
>
|
|
33
|
+
{getLocalizedString("Filter")}
|
|
34
|
+
</rootProps.slots.baseMenuItem>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -8,8 +8,11 @@ export type AfGridContextValue = {
|
|
|
8
8
|
apiRef: React.RefObject<GridApi | null>;
|
|
9
9
|
/** Fields the filter builder can filter on, derived from the data object and the columns. */
|
|
10
10
|
filterFields: FieldMetadata[];
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Opens the filter builder dialog. Given a field, it adds an empty criterion for it first, unless
|
|
13
|
+
* the filter already has an unfinished one for that field.
|
|
14
|
+
*/
|
|
15
|
+
openFilterBuilder: (field?: string) => void;
|
|
13
16
|
/**
|
|
14
17
|
* The grid's filter tree. The header filters and the filter builder edit the
|
|
15
18
|
* same tree, so a criterion added in one shows up in the other.
|
package/src/grid/Toolbar.tsx
CHANGED
|
@@ -95,7 +95,7 @@ export function GridToolbarFilterButton({ iconOnly }: GridToolbarIconOnlyProps =
|
|
|
95
95
|
return (
|
|
96
96
|
<BadgePosition placement="top-right" overlap="circle">
|
|
97
97
|
{filterCount > 0 && <Badge data-color="brand" count={filterCount} aria-hidden="true" />}
|
|
98
|
-
<Button variant="tertiary" icon aria-label={name} title={name} onClick={openFilterBuilder}>
|
|
98
|
+
<Button variant="tertiary" icon aria-label={name} title={name} onClick={() => openFilterBuilder()}>
|
|
99
99
|
<FilterIcon aria-hidden />
|
|
100
100
|
</Button>
|
|
101
101
|
</BadgePosition>
|
|
@@ -103,7 +103,7 @@ export function GridToolbarFilterButton({ iconOnly }: GridToolbarIconOnlyProps =
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
return (
|
|
106
|
-
<Button variant="tertiary" onClick={openFilterBuilder}>
|
|
106
|
+
<Button variant="tertiary" onClick={() => openFilterBuilder()}>
|
|
107
107
|
<FilterIcon aria-hidden />
|
|
108
108
|
{label}
|
|
109
109
|
{filterCount > 0 ? ` (${filterCount})` : ""}
|
package/src/grid/index.tsx
CHANGED
|
@@ -16,12 +16,15 @@ import {
|
|
|
16
16
|
type GridRowParams,
|
|
17
17
|
useGridApiRef,
|
|
18
18
|
} from "@mui/x-data-grid-premium";
|
|
19
|
+
import { createExpression, type FilterPath, isCompleteExpression, isExpression } from "@olenbetong/appframe-core";
|
|
19
20
|
import { type DataObject, uid } from "@olenbetong/appframe-data";
|
|
20
21
|
import { DataObjectProvider, useLoading } from "@olenbetong/appframe-react";
|
|
21
22
|
|
|
22
23
|
import clsx from "clsx";
|
|
23
24
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
24
25
|
import { FilterBuilder } from "../filter/index.js";
|
|
26
|
+
import { AfColumnMenuColumnsItem } from "./AfColumnMenuColumnsItem.js";
|
|
27
|
+
import { AfColumnMenuFilterItem } from "./AfColumnMenuFilterItem.js";
|
|
25
28
|
import { AfGridColumnsPanel } from "./AfGridColumnsPanel.js";
|
|
26
29
|
import { AfHeaderFilterCell } from "./AfHeaderFilterCell.js";
|
|
27
30
|
import { type AfGridContextValue, AfGridContextProvider } from "./AfGridContext.js";
|
|
@@ -52,7 +55,7 @@ import { type AfGridColDef, type AfGridRowAction, useAfColumns } from "./useAfCo
|
|
|
52
55
|
import { useAfCurrentIndex } from "./useAfCurrentIndex.js";
|
|
53
56
|
import { useAfData } from "./useAfData.js";
|
|
54
57
|
import { useAfFilterFields } from "./useAfFilterFields.js";
|
|
55
|
-
import { useAfFilter } from "./useAfFilter.js";
|
|
58
|
+
import { findFilterField, useAfFilter } from "./useAfFilter.js";
|
|
56
59
|
import { type AfGridApi, useAfGridApi } from "./useAfGridApi.js";
|
|
57
60
|
import { useAfKeyBindings } from "./useAfKeyBindings.js";
|
|
58
61
|
import { type AfGridNewItemRowPosition, NEW_ITEM_ROW_ID, isNewItemRow, useAfNewItemRow } from "./useAfNewItemRow.js";
|
|
@@ -270,7 +273,7 @@ export type AfGridProps = {
|
|
|
270
273
|
* Editing it moves the data object to the new record at index -1, and committing
|
|
271
274
|
* it saves the record. Only shown when the data object allows insert and
|
|
272
275
|
* `disableInsert` is not set. `none` turns it off, in which case the Add button
|
|
273
|
-
* appends an unsaved row to the data instead. Defaults to `
|
|
276
|
+
* appends an unsaved row to the data instead. Defaults to `top`.
|
|
274
277
|
*/
|
|
275
278
|
newItemRow?: AfGridNewItemRowPosition;
|
|
276
279
|
/**
|
|
@@ -371,9 +374,8 @@ function AfGrid_(props: AfGridProps) {
|
|
|
371
374
|
getData,
|
|
372
375
|
idField = "PrimKey",
|
|
373
376
|
id,
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
newItemRow: newItemRowPosition = dataObject.options?.newRecordPosition === "start" ? "top" : "bottom",
|
|
377
|
+
// At the bottom the row is pinned to the foot of the viewport, far below the data on a short grid
|
|
378
|
+
newItemRow: newItemRowPosition = "top",
|
|
377
379
|
persistenceModel,
|
|
378
380
|
initialState,
|
|
379
381
|
pinnedRows,
|
|
@@ -420,6 +422,8 @@ function AfGrid_(props: AfGridProps) {
|
|
|
420
422
|
let gridFilter = useAfFilter({ autoLoad, dataObject, id });
|
|
421
423
|
let [snackbar, setSnackbar] = useState<string | null>(null);
|
|
422
424
|
let [isFilterBuilderOpen, setFilterBuilderOpen] = useState(false);
|
|
425
|
+
// The criterion added from a column menu, so its value has focus when the builder opens.
|
|
426
|
+
let [filterBuilderFocus, setFilterBuilderFocus] = useState<FilterPath | null>(null);
|
|
423
427
|
let [isColumnsPanelOpen, setColumnsPanelOpen] = useState(false);
|
|
424
428
|
let editProps = useAfRowEditModel({ dataObject, disableEdit, idField, apiRef, newItemRow });
|
|
425
429
|
useAfKeepFocusAfterEdit(apiRef);
|
|
@@ -493,7 +497,21 @@ function AfGrid_(props: AfGridProps) {
|
|
|
493
497
|
apiRef,
|
|
494
498
|
filterFields,
|
|
495
499
|
...gridFilter,
|
|
496
|
-
openFilterBuilder: () =>
|
|
500
|
+
openFilterBuilder: (fieldName?: string) => {
|
|
501
|
+
let field = fieldName ? findFilterField(filterFields, fieldName) : undefined;
|
|
502
|
+
let { filter, setFilter } = gridFilter;
|
|
503
|
+
let draftIndex = filter.items.findIndex(
|
|
504
|
+
(item) => isExpression(item) && item.column === field?.name && !isCompleteExpression(item),
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
if (field && draftIndex === -1) {
|
|
508
|
+
draftIndex = filter.items.length;
|
|
509
|
+
setFilter({ ...filter, items: [...filter.items, createExpression(field)] });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
setFilterBuilderFocus(field ? [draftIndex] : null);
|
|
513
|
+
setFilterBuilderOpen(true);
|
|
514
|
+
},
|
|
497
515
|
openColumnsPanel: () => setColumnsPanelOpen(true),
|
|
498
516
|
}),
|
|
499
517
|
[id, apiRef, filterFields, gridFilter],
|
|
@@ -555,6 +573,14 @@ function AfGrid_(props: AfGridProps) {
|
|
|
555
573
|
initialState={initialState_}
|
|
556
574
|
slotProps={{
|
|
557
575
|
...other.slotProps,
|
|
576
|
+
columnMenu: {
|
|
577
|
+
...other.slotProps?.columnMenu,
|
|
578
|
+
slots: {
|
|
579
|
+
columnMenuColumnsItem: AfColumnMenuColumnsItem,
|
|
580
|
+
columnMenuFilterItem: AfColumnMenuFilterItem,
|
|
581
|
+
...other.slotProps?.columnMenu?.slots,
|
|
582
|
+
},
|
|
583
|
+
},
|
|
558
584
|
headerFilterCell: {
|
|
559
585
|
...other.slotProps?.headerFilterCell,
|
|
560
586
|
headerClassName: clsx(other.slotProps?.headerFilterCell?.headerClassName, "AfGrid-headerCell"),
|
|
@@ -573,6 +599,7 @@ function AfGrid_(props: AfGridProps) {
|
|
|
573
599
|
<FilterBuilder
|
|
574
600
|
variant="dialog"
|
|
575
601
|
open={isFilterBuilderOpen}
|
|
602
|
+
autoFocusPath={filterBuilderFocus}
|
|
576
603
|
onClose={() => setFilterBuilderOpen(false)}
|
|
577
604
|
fields={filterFields}
|
|
578
605
|
viewName={filterViewName}
|
|
@@ -152,7 +152,9 @@ export function useAfNewItemRow({
|
|
|
152
152
|
let focusBefore = gridFocusCellSelector(apiRef);
|
|
153
153
|
|
|
154
154
|
try {
|
|
155
|
-
|
|
155
|
+
// A record saved from a row above the data is inserted first, so it stays where it was
|
|
156
|
+
// entered; at the bottom the data object's own `newRecordPosition` decides.
|
|
157
|
+
await dataObject.endEdit(position === "top" ? { newRecordPosition: "start" } : undefined);
|
|
156
158
|
} catch (error) {
|
|
157
159
|
// The framework keeps a failed new record in storage as an unsaved row.
|
|
158
160
|
// The grid keeps the draft in the new item row instead, so the unsaved
|
|
@@ -185,7 +187,7 @@ export function useAfNewItemRow({
|
|
|
185
187
|
|
|
186
188
|
return blankRow;
|
|
187
189
|
},
|
|
188
|
-
[apiRef, dataObject, beginNewRecord, cancelNewRecord],
|
|
190
|
+
[apiRef, dataObject, position, beginNewRecord, cancelNewRecord],
|
|
189
191
|
);
|
|
190
192
|
|
|
191
193
|
useEffect(() => {
|