@olenbetong/appframe-ds 0.9.0 → 1.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/CHANGELOG.md +123 -0
- package/package.json +22 -17
- package/scripts/copyAssets.mjs +3 -0
- package/src/autocomplete/Autocomplete.css +13 -0
- package/src/autocomplete/Autocomplete.tsx +3 -1
- package/src/autocomplete/Combobox.tsx +16 -3
- package/src/binding/BoundTextField.tsx +9 -1
- package/src/filter/FieldFilterPanel.css +25 -2
- package/src/grid/AfGridColumnsPanel.tsx +1 -1
- package/src/grid/AfGridContext.tsx +1 -1
- package/src/grid/AfGridError.tsx +1 -1
- package/src/grid/AfHeaderFilterCell.tsx +1 -1
- package/src/grid/Toolbar.tsx +1 -1
- package/src/grid/editing.ts +17 -7
- package/src/grid/filter.ts +1 -1
- package/src/grid/index.css +59 -1
- package/src/grid/index.tsx +126 -18
- package/src/grid/localization.ts +1 -1
- package/src/grid/slots/index.tsx +27 -2
- package/src/grid/theme.tsx +3 -50
- package/src/grid/useAfColumns.tsx +103 -45
- package/src/grid/useAfCurrentIndex.ts +13 -5
- package/src/grid/useAfGridApi.ts +6 -1
- package/src/grid/useAfKeyBindings.ts +1 -1
- package/src/grid/useAfNewItemRow.ts +258 -0
- package/src/grid/useAfPagination.ts +2 -2
- package/src/grid/useAfPersistedState.ts +9 -1
- package/src/grid/useAfRowEditModel.ts +20 -12
- package/src/grid/useAfRowGrouping.ts +47 -0
- package/src/grid/useAfServerAggregation.ts +145 -0
- package/src/grid/useAfSortModel.ts +15 -3
- package/src/layout/AppMenuDrawer.css +70 -0
- package/src/layout/AppMenuDrawer.tsx +122 -0
- package/src/layout/index.tsx +1 -0
- package/src/mdi/Mdi.css +138 -0
- package/src/mdi/Mdi.test.tsx +87 -0
- package/src/mdi/Mdi.tsx +141 -0
- package/src/mdi/MdiConfirmCloseDialog.tsx +47 -0
- package/src/mdi/MdiContext.tsx +59 -0
- package/src/mdi/MdiDocumentContext.tsx +60 -0
- package/src/mdi/MdiPanel.tsx +61 -0
- package/src/mdi/MdiRestore.test.tsx +82 -0
- package/src/mdi/MdiTab.tsx +30 -0
- package/src/mdi/controller.test.ts +382 -0
- package/src/mdi/controller.ts +656 -0
- package/src/mdi/dockviewHost.ts +132 -0
- package/src/mdi/fakeHost.ts +200 -0
- package/src/mdi/host.ts +44 -0
- package/src/mdi/ids.test.ts +20 -0
- package/src/mdi/ids.ts +18 -0
- package/src/mdi/index.ts +31 -0
- package/src/mdi/layoutRestore.test.ts +156 -0
- package/src/mdi/layoutRestore.ts +311 -0
- package/src/mdi/layoutSpec.test.ts +143 -0
- package/src/mdi/layoutSpec.ts +214 -0
- package/src/mdi/layoutStore.test.ts +93 -0
- package/src/mdi/layoutStore.ts +99 -0
- package/src/mdi/memorySettingsStore.ts +21 -0
- package/src/mdi/registry.ts +34 -0
- package/src/mdi/testTypes.ts +37 -0
- package/src/mdi/theme.ts +22 -0
- package/src/mdi/types.ts +214 -0
- package/src/page/Page.css +10 -2
- package/src/page/PageBlock.css +5 -0
- package/src/page/PageBlock.tsx +14 -0
- package/src/page/PageHeader.css +0 -60
- package/src/page/PageHeader.tsx +6 -74
- package/src/test/happy-dom-document-class.ts +24 -0
- package/src/test/setup.ts +4 -0
- package/src/theme/useDsColorScheme.ts +53 -0
package/src/grid/index.tsx
CHANGED
|
@@ -2,14 +2,17 @@ import "./index.css";
|
|
|
2
2
|
|
|
3
3
|
import { Alert } from "@digdir/designsystemet-react";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
type
|
|
5
|
+
DataGridPremium as DataGrid,
|
|
6
|
+
type DataGridPremiumProps as DataGridProps,
|
|
7
|
+
GRID_AGGREGATION_FUNCTIONS,
|
|
8
|
+
GRID_ROOT_GROUP_ID,
|
|
9
|
+
type GridAggregationFunction,
|
|
7
10
|
type GridApi,
|
|
8
11
|
type GridFilterModel,
|
|
9
12
|
useGridApiRef,
|
|
10
|
-
} from "@mui/x-data-grid-
|
|
13
|
+
} from "@mui/x-data-grid-premium";
|
|
11
14
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
12
|
-
import type
|
|
15
|
+
import { type DataObject, uid } from "@olenbetong/appframe-data";
|
|
13
16
|
import { DataObjectProvider, useLoading } from "@olenbetong/appframe-react";
|
|
14
17
|
|
|
15
18
|
import clsx from "clsx";
|
|
@@ -45,6 +48,7 @@ import { useAfFilterFields } from "./useAfFilterFields.js";
|
|
|
45
48
|
import { useAfFilter } from "./useAfFilter.js";
|
|
46
49
|
import { type AfGridApi, useAfGridApi } from "./useAfGridApi.js";
|
|
47
50
|
import { useAfKeyBindings } from "./useAfKeyBindings.js";
|
|
51
|
+
import { type AfGridNewItemRowPosition, NEW_ITEM_ROW_ID, isNewItemRow, useAfNewItemRow } from "./useAfNewItemRow.js";
|
|
48
52
|
import { useAfPagination } from "./useAfPagination.js";
|
|
49
53
|
import {
|
|
50
54
|
isPersistedStateLoaded,
|
|
@@ -53,10 +57,12 @@ import {
|
|
|
53
57
|
useAfPersistedState,
|
|
54
58
|
} from "./useAfPersistedState.js";
|
|
55
59
|
import { useAfRowEditModel } from "./useAfRowEditModel.js";
|
|
60
|
+
import { useAfRowGrouping } from "./useAfRowGrouping.js";
|
|
61
|
+
import { useAfServerAggregation } from "./useAfServerAggregation.js";
|
|
56
62
|
import { useAfSortModel } from "./useAfSortModel.js";
|
|
57
63
|
|
|
58
64
|
export * from "./formatters.js";
|
|
59
|
-
export * as MuiDataGrid from "@mui/x-data-grid-
|
|
65
|
+
export * as MuiDataGrid from "@mui/x-data-grid-premium";
|
|
60
66
|
|
|
61
67
|
registerLicense();
|
|
62
68
|
|
|
@@ -84,10 +90,21 @@ export const systemColumns = {
|
|
|
84
90
|
UpdatedBy: privSysColumns[4],
|
|
85
91
|
};
|
|
86
92
|
|
|
87
|
-
export type {
|
|
93
|
+
export type {
|
|
94
|
+
AfExcelExportOptions,
|
|
95
|
+
AfGridApi,
|
|
96
|
+
AfGridColDef,
|
|
97
|
+
AfGridColumn,
|
|
98
|
+
AfGridExcelExportOptions,
|
|
99
|
+
AfGridNewItemRowPosition,
|
|
100
|
+
};
|
|
101
|
+
export type { AfGridGrouping, AfGridGroupingProps } from "./useAfRowGrouping.js";
|
|
88
102
|
|
|
89
103
|
export {
|
|
90
104
|
AF_GRID_LOCALE_TEXT,
|
|
105
|
+
NEW_ITEM_ROW_ID,
|
|
106
|
+
isNewItemRow,
|
|
107
|
+
useAfNewItemRow,
|
|
91
108
|
AfGridColumnsPanel,
|
|
92
109
|
AfGridExcelExportMenuItem,
|
|
93
110
|
AfGridThemeProvider,
|
|
@@ -109,6 +126,8 @@ export {
|
|
|
109
126
|
useAfPagination,
|
|
110
127
|
useAfPersistedState,
|
|
111
128
|
useAfRowEditModel,
|
|
129
|
+
useAfRowGrouping,
|
|
130
|
+
useAfServerAggregation,
|
|
112
131
|
useAfSortModel,
|
|
113
132
|
};
|
|
114
133
|
|
|
@@ -162,6 +181,15 @@ export type AfGridProps = {
|
|
|
162
181
|
* saved filters are unavailable.
|
|
163
182
|
*/
|
|
164
183
|
filterViewName?: string;
|
|
184
|
+
/**
|
|
185
|
+
* Where to show the new item row: a non-data row pinned above or below the data
|
|
186
|
+
* that reads "Click here to add a new row" until the user starts editing it.
|
|
187
|
+
* Editing it moves the data object to the new record at index -1, and committing
|
|
188
|
+
* it saves the record. Only shown when the data object allows insert and
|
|
189
|
+
* `disableInsert` is not set. `none` turns it off, in which case the Add button
|
|
190
|
+
* appends an unsaved row to the data instead. Defaults to `bottom`.
|
|
191
|
+
*/
|
|
192
|
+
newItemRow?: AfGridNewItemRowPosition;
|
|
165
193
|
/**
|
|
166
194
|
* A unique ID used to persist the grid state. It is better to use the layoutVersion
|
|
167
195
|
* property if you would like to ignore any previously persisted state, as changing
|
|
@@ -174,6 +202,23 @@ export type AfGridProps = {
|
|
|
174
202
|
* is used to configure which properties should be persisted.
|
|
175
203
|
*/
|
|
176
204
|
persistenceModel?: StatePersistenceConfig;
|
|
205
|
+
/**
|
|
206
|
+
* Ask the server for the grand total instead of adding up the rows the grid is holding.
|
|
207
|
+
*
|
|
208
|
+
* MUI aggregates over the loaded rows, which on a dynamically loading data object is the
|
|
209
|
+
* current page — a sum that looks like a total and is not one. With this on, the grid runs a
|
|
210
|
+
* second retrieve under the same filters that returns only the aggregates, so the footer shows
|
|
211
|
+
* the total over the whole result. Subtotals on grouped rows stay client side, since a group
|
|
212
|
+
* only exists over the rows the grid has.
|
|
213
|
+
*
|
|
214
|
+
* `sum`, `avg`, `min` and `max` are resolved server side; `size` and any custom aggregation
|
|
215
|
+
* function stay on the client. If the view cannot be aggregated the grid falls back to the
|
|
216
|
+
* client total rather than failing.
|
|
217
|
+
*
|
|
218
|
+
* Defaults to true when the data object loads dynamically, since that is exactly the case
|
|
219
|
+
* where the client total is wrong.
|
|
220
|
+
*/
|
|
221
|
+
serverAggregation?: boolean;
|
|
177
222
|
} & Omit<
|
|
178
223
|
DataGridProps,
|
|
179
224
|
| "apiRef"
|
|
@@ -186,6 +231,10 @@ export type AfGridProps = {
|
|
|
186
231
|
| "sortingMode"
|
|
187
232
|
| "sortModel"
|
|
188
233
|
| "onSortModelChange"
|
|
234
|
+
| "rowGroupingModel"
|
|
235
|
+
| "onRowGroupingModelChange"
|
|
236
|
+
| "aggregationModel"
|
|
237
|
+
| "onAggregationModelChange"
|
|
189
238
|
| "editRowsModel"
|
|
190
239
|
| "editMode"
|
|
191
240
|
| "onRowEditCommit"
|
|
@@ -232,31 +281,57 @@ function AfGrid_(props: AfGridProps) {
|
|
|
232
281
|
getData,
|
|
233
282
|
idField = "PrimKey",
|
|
234
283
|
id,
|
|
284
|
+
// Follows the data object's `newRecordPosition` when it has one (older data
|
|
285
|
+
// packages have none and append), so the row sits where the record will land
|
|
286
|
+
newItemRow: newItemRowPosition = dataObject.options?.newRecordPosition === "start" ? "top" : "bottom",
|
|
235
287
|
persistenceModel,
|
|
236
288
|
initialState,
|
|
289
|
+
pinnedRows,
|
|
290
|
+
getRowClassName,
|
|
291
|
+
serverAggregation = dataObject.options?.dynamicLoading === true,
|
|
237
292
|
...other
|
|
238
293
|
} = props;
|
|
239
294
|
let apiRefPriv = useGridApiRef();
|
|
240
295
|
let apiRef = apiRefProp ?? apiRefPriv;
|
|
241
296
|
useAfGridApi(props, apiRef);
|
|
297
|
+
let newItemRow = useAfNewItemRow({
|
|
298
|
+
apiRef,
|
|
299
|
+
dataObject,
|
|
300
|
+
position: newItemRowPosition,
|
|
301
|
+
disableInsert,
|
|
302
|
+
pinnedRows,
|
|
303
|
+
getRowClassName,
|
|
304
|
+
});
|
|
305
|
+
let initialState_ = useAfPersistedState({
|
|
306
|
+
apiRef,
|
|
307
|
+
columns: columnsProp,
|
|
308
|
+
id,
|
|
309
|
+
layoutVersion,
|
|
310
|
+
persistenceModel,
|
|
311
|
+
initialState,
|
|
312
|
+
});
|
|
313
|
+
// Seeded from the restored layout, so a persisted grouping survives a reload.
|
|
314
|
+
let grouping = useAfRowGrouping(initialState_);
|
|
242
315
|
let loading = useLoading(dataObject);
|
|
243
316
|
let paging = useAfPagination(dataObject);
|
|
244
317
|
let sortModel = useAfSortModel({
|
|
245
318
|
dataObject,
|
|
246
319
|
columns: columnsProp,
|
|
247
320
|
disableServerSorting,
|
|
321
|
+
isGrouping: grouping.isGrouping,
|
|
248
322
|
});
|
|
249
323
|
let gridFilter = useAfFilter({ autoLoad, dataObject, id });
|
|
250
324
|
let [snackbar, setSnackbar] = useState<string | null>(null);
|
|
251
325
|
let [isFilterBuilderOpen, setFilterBuilderOpen] = useState(false);
|
|
252
326
|
let [isColumnsPanelOpen, setColumnsPanelOpen] = useState(false);
|
|
253
|
-
let editProps = useAfRowEditModel({ dataObject, disableEdit, idField, apiRef });
|
|
327
|
+
let editProps = useAfRowEditModel({ dataObject, disableEdit, idField, apiRef, newItemRow });
|
|
254
328
|
let data = useAfData(dataObject, { getData });
|
|
255
329
|
useAfCurrentIndex({
|
|
256
330
|
apiRef,
|
|
257
331
|
dataObject,
|
|
258
332
|
idField,
|
|
259
333
|
enabled: !!bindIndex,
|
|
334
|
+
newItemRowId: newItemRow.enabled ? NEW_ITEM_ROW_ID : null,
|
|
260
335
|
});
|
|
261
336
|
let columns = useAfColumns({
|
|
262
337
|
apiRef,
|
|
@@ -268,15 +343,6 @@ function AfGrid_(props: AfGridProps) {
|
|
|
268
343
|
});
|
|
269
344
|
let filterFields = useAfFilterFields(dataObject, columnsProp);
|
|
270
345
|
|
|
271
|
-
let initialState_ = useAfPersistedState({
|
|
272
|
-
apiRef,
|
|
273
|
-
columns: columnsProp,
|
|
274
|
-
id,
|
|
275
|
-
layoutVersion,
|
|
276
|
-
persistenceModel,
|
|
277
|
-
initialState,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
346
|
let keyEvents = useAfKeyBindings({ apiRef, dataObject });
|
|
281
347
|
|
|
282
348
|
useEffect(() => {
|
|
@@ -287,6 +353,39 @@ function AfGrid_(props: AfGridProps) {
|
|
|
287
353
|
return () => clearTimeout(timeout);
|
|
288
354
|
}, [snackbar]);
|
|
289
355
|
|
|
356
|
+
let serverAggregates = useAfServerAggregation(
|
|
357
|
+
dataObject,
|
|
358
|
+
grouping.gridProps.aggregationModel,
|
|
359
|
+
serverAggregation && !grouping.isGrouping,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
/*
|
|
363
|
+
* The grand total row asks for `groupId === GRID_ROOT_GROUP_ID`; every other call is a group
|
|
364
|
+
* subtotal, which only means anything over the rows the grid actually has. So the server value
|
|
365
|
+
* is substituted for the root and the built-in function is left to do the rest.
|
|
366
|
+
*/
|
|
367
|
+
let aggregationFunctions = useMemo(() => {
|
|
368
|
+
if (!serverAggregation) return undefined;
|
|
369
|
+
|
|
370
|
+
let base = other.aggregationFunctions ?? GRID_AGGREGATION_FUNCTIONS;
|
|
371
|
+
let wrapped: Record<string, GridAggregationFunction> = {};
|
|
372
|
+
|
|
373
|
+
for (let [name, fn] of Object.entries(base) as Array<[string, GridAggregationFunction]>) {
|
|
374
|
+
wrapped[name] = {
|
|
375
|
+
...fn,
|
|
376
|
+
apply: (params, api) => {
|
|
377
|
+
if (params.groupId === GRID_ROOT_GROUP_ID && params.field in serverAggregates.values) {
|
|
378
|
+
return serverAggregates.values[params.field];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return fn.apply(params, api);
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return wrapped;
|
|
387
|
+
}, [serverAggregation, other.aggregationFunctions, serverAggregates.values]);
|
|
388
|
+
|
|
290
389
|
let gridContext = useMemo<AfGridContextValue>(
|
|
291
390
|
() => ({
|
|
292
391
|
id,
|
|
@@ -302,14 +401,15 @@ function AfGrid_(props: AfGridProps) {
|
|
|
302
401
|
return (
|
|
303
402
|
<DataObjectProvider dataObject={dataObject}>
|
|
304
403
|
<AfGridContextProvider value={gridContext}>
|
|
305
|
-
<div className="AfGrid-root" {...keyEvents} tabIndex={-1}>
|
|
404
|
+
<div className="AfGrid-root" style={newItemRow.rootStyle} {...keyEvents} tabIndex={-1}>
|
|
306
405
|
<DataGrid
|
|
307
406
|
apiRef={apiRef}
|
|
308
407
|
rows={data}
|
|
309
408
|
columns={columns}
|
|
310
409
|
loading={loading}
|
|
311
|
-
getRowId={(row) => row[
|
|
410
|
+
getRowId={(row) => row[uid] ?? row[idField]}
|
|
312
411
|
{...editProps}
|
|
412
|
+
{...newItemRow.gridProps}
|
|
313
413
|
/*
|
|
314
414
|
* Our own filter tree drives the data object, so MUI never
|
|
315
415
|
* sees a filter model. The header filter cells are replaced
|
|
@@ -322,6 +422,14 @@ function AfGrid_(props: AfGridProps) {
|
|
|
322
422
|
{...paging}
|
|
323
423
|
showToolbar
|
|
324
424
|
{...other}
|
|
425
|
+
{...grouping.gridProps}
|
|
426
|
+
/*
|
|
427
|
+
* Spread rather than passed, because the grid merges props over its defaults with
|
|
428
|
+
* `Object.assign`: an explicit `undefined` here would wipe out the built-in
|
|
429
|
+
* aggregation functions rather than fall back to them, and the grid then throws on
|
|
430
|
+
* the first render.
|
|
431
|
+
*/
|
|
432
|
+
{...(aggregationFunctions ? { aggregationFunctions } : {})}
|
|
325
433
|
localeText={{ ...other.localeText, ...AF_GRID_LOCALE_TEXT }}
|
|
326
434
|
slots={{
|
|
327
435
|
...dsBaseSlots,
|
package/src/grid/localization.ts
CHANGED
package/src/grid/slots/index.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
Textfield,
|
|
14
14
|
Tooltip,
|
|
15
15
|
} from "@digdir/designsystemet-react";
|
|
16
|
-
import type { GridSlotProps, GridSlotsComponent } from "@mui/x-data-grid-
|
|
16
|
+
import type { GridSlotProps, GridSlotsComponent } from "@mui/x-data-grid-premium";
|
|
17
17
|
|
|
18
18
|
import clsx from "clsx";
|
|
19
19
|
import { type Ref, useCallback } from "react";
|
|
@@ -112,12 +112,37 @@ function BaseTooltip(props: GridSlotProps["baseTooltip"]) {
|
|
|
112
112
|
let { children, title, enterDelay: _enterDelay, disableInteractive: _disableInteractive, ...other } = props;
|
|
113
113
|
let content = tooltipContent(title);
|
|
114
114
|
|
|
115
|
+
// Designsystemet labels the tooltip host itself, and logs `Missing
|
|
116
|
+
// tabindex="0" attribute on:` whenever that host is not tabbable. In the grid
|
|
117
|
+
// it never is — the column header is the tab stop and moves focus with the
|
|
118
|
+
// arrow keys, so MUI gives the menu button `tabindex="-1"` and hangs the sort
|
|
119
|
+
// tooltip on a bare `<span>`. The tooltips still open, because Designsystemet
|
|
120
|
+
// resolves the host with `closest()` on focus and hover, so the warning is a
|
|
121
|
+
// false positive — and it is not gated on `NODE_ENV`, so it reaches
|
|
122
|
+
// production too.
|
|
123
|
+
//
|
|
124
|
+
// Setting the label ourselves is exactly what Designsystemet does a moment
|
|
125
|
+
// later (`aria-description` when the host has text, `aria-label` when it does
|
|
126
|
+
// not), and it makes Designsystemet skip that whole branch — silencing the
|
|
127
|
+
// warning without turning off `window.dsWarnings` for the entire app.
|
|
128
|
+
let ref = useCallback(
|
|
129
|
+
(element: HTMLElement | null) => {
|
|
130
|
+
if (!element || !content) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let hasText = element.getAttribute("role") !== "img" && !!element.textContent?.trim();
|
|
135
|
+
element.setAttribute(hasText ? "aria-description" : "aria-label", content);
|
|
136
|
+
},
|
|
137
|
+
[content],
|
|
138
|
+
);
|
|
139
|
+
|
|
115
140
|
if (!content) {
|
|
116
141
|
return children;
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
return (
|
|
120
|
-
<Tooltip {...(other as any)} content={content}>
|
|
145
|
+
<Tooltip {...(other as any)} ref={ref} content={content}>
|
|
121
146
|
{children}
|
|
122
147
|
</Tooltip>
|
|
123
148
|
);
|
package/src/grid/theme.tsx
CHANGED
|
@@ -2,57 +2,10 @@ import { StyledEngineProvider, type Theme, ThemeProvider } from "@mui/material";
|
|
|
2
2
|
import { createTheme as muiCreateTheme } from "@mui/material/styles";
|
|
3
3
|
import { createContext, type ReactNode, use, useEffect, useState } from "react";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
let scoped = element?.closest<HTMLElement>("[data-color-scheme]");
|
|
7
|
-
let scheme =
|
|
8
|
-
scoped?.dataset.colorScheme ??
|
|
9
|
-
document.body?.dataset.colorScheme ??
|
|
10
|
-
document.documentElement?.dataset.colorScheme ??
|
|
11
|
-
"light";
|
|
12
|
-
|
|
13
|
-
if (scheme === "auto") {
|
|
14
|
-
return globalThis.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return scheme === "dark" ? "dark" : "light";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Tracks the color scheme Designsystemet is rendering with. The article template
|
|
22
|
-
* sets `data-color-scheme` on `<body>`, but it may also be scoped to a subtree,
|
|
23
|
-
* so resolve it from `element` upwards when one is given. The OS preference is
|
|
24
|
-
* watched too, since it decides the result when the scheme is `auto`.
|
|
25
|
-
*/
|
|
26
|
-
export function useDsColorScheme(element?: Element | null) {
|
|
27
|
-
let [scheme, setScheme] = useState<"light" | "dark">(() => getColorScheme(element));
|
|
5
|
+
import { useDsColorScheme } from "../theme/useDsColorScheme.js";
|
|
28
6
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
setScheme(getColorScheme(element));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
update();
|
|
35
|
-
|
|
36
|
-
// `data-color-scheme` may sit on any ancestor, so watch the whole document
|
|
37
|
-
// for that one attribute rather than just `<html>` and `<body>`.
|
|
38
|
-
let observer = new MutationObserver(update);
|
|
39
|
-
observer.observe(document.documentElement, {
|
|
40
|
-
attributes: true,
|
|
41
|
-
attributeFilter: ["data-color-scheme"],
|
|
42
|
-
subtree: true,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
let media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
|
|
46
|
-
media?.addEventListener("change", update);
|
|
47
|
-
|
|
48
|
-
return () => {
|
|
49
|
-
observer.disconnect();
|
|
50
|
-
media?.removeEventListener("change", update);
|
|
51
|
-
};
|
|
52
|
-
}, [element]);
|
|
53
|
-
|
|
54
|
-
return scheme;
|
|
55
|
-
}
|
|
7
|
+
// Lives in `../theme/` so MUI-free areas (the `mdi` subpath) can share it.
|
|
8
|
+
export { useDsColorScheme };
|
|
56
9
|
|
|
57
10
|
function buildTheme(mode: "light" | "dark", element?: Element | null) {
|
|
58
11
|
let styles = getComputedStyle(element ?? document.body);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
GRID_AGGREGATION_ROOT_FOOTER_ROW_ID,
|
|
2
3
|
GRID_CHECKBOX_SELECTION_FIELD,
|
|
3
4
|
GRID_DETAIL_PANEL_TOGGLE_FIELD,
|
|
4
5
|
GRID_TREE_DATA_GROUPING_FIELD,
|
|
@@ -12,13 +13,14 @@ import {
|
|
|
12
13
|
type GridRowId,
|
|
13
14
|
GridRowModes,
|
|
14
15
|
type GridRowParams,
|
|
16
|
+
getAggregationFooterRowIdFromGroupId,
|
|
15
17
|
gridEditRowsStateSelector,
|
|
16
18
|
useGridApiContext,
|
|
17
19
|
useGridSelector,
|
|
18
|
-
} from "@mui/x-data-grid-
|
|
20
|
+
} from "@mui/x-data-grid-premium";
|
|
19
21
|
import { FloppydiskIcon, PencilIcon, TrashIcon, XMarkIcon } from "@navikt/aksel-icons";
|
|
20
22
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
21
|
-
import type
|
|
23
|
+
import { type DataObject, uid } from "@olenbetong/appframe-data";
|
|
22
24
|
import { usePermissions } from "@olenbetong/appframe-react";
|
|
23
25
|
|
|
24
26
|
import React, { useMemo } from "react";
|
|
@@ -27,6 +29,10 @@ import { AfHeaderFilterCell } from "./AfHeaderFilterCell.js";
|
|
|
27
29
|
import { deleteRow } from "./editing.js";
|
|
28
30
|
import { operators } from "./filter.js";
|
|
29
31
|
import { dateTimeValueFormatter, dateValueFormatter } from "./formatters.js";
|
|
32
|
+
import { isNewItemRow } from "./useAfNewItemRow.js";
|
|
33
|
+
|
|
34
|
+
/** `auto-generated-group-footer-`, without spelling it out: every group's total row shares it. */
|
|
35
|
+
const AGGREGATION_FOOTER_ID_PREFIX = String(getAggregationFooterRowIdFromGroupId(""));
|
|
30
36
|
|
|
31
37
|
export type AfGridColDef = GridColDef & {
|
|
32
38
|
sortColumn?: string;
|
|
@@ -43,6 +49,22 @@ export type AfGridColDef = GridColDef & {
|
|
|
43
49
|
calculated?: boolean;
|
|
44
50
|
};
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Whether the cell belongs to a row the grid generated to hold totals rather than to a record.
|
|
54
|
+
*
|
|
55
|
+
* The row type is not enough on its own: the grand total is pinned to the bottom, so it arrives as
|
|
56
|
+
* a `pinnedRow` — the same type as the grid's own new-item row, whose `renderCell` must still run.
|
|
57
|
+
* Its id is the reliable marker, and MUI exports the constants for it.
|
|
58
|
+
*/
|
|
59
|
+
function isAggregationRow(params: { id: GridRowId; rowNode?: { type?: string } }) {
|
|
60
|
+
if (params.rowNode?.type === "group") return true;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
typeof params.id === "string" &&
|
|
64
|
+
(params.id === GRID_AGGREGATION_ROOT_FOOTER_ROW_ID || params.id.startsWith(AGGREGATION_FOOTER_ID_PREFIX))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
export function getColumnVisibilityModel(columns: AfGridColumn[]): GridColumnVisibilityModel {
|
|
47
69
|
let visibility: GridColumnVisibilityModel = {};
|
|
48
70
|
for (let column of columns) {
|
|
@@ -155,6 +177,28 @@ export function afColumnsToMuiColumn({
|
|
|
155
177
|
col.filterOperators = (operators as any)[type];
|
|
156
178
|
}
|
|
157
179
|
|
|
180
|
+
/*
|
|
181
|
+
* A total or group row has no record behind it — the grid generates it — so a
|
|
182
|
+
* `renderCell` that reads its value off `params.row`, which most do, has nothing to
|
|
183
|
+
* read. Left alone that reads as a broken feature in two ways: turning on a sum shows
|
|
184
|
+
* a pinned row of empty cells, and every unaggregated column renders whatever its
|
|
185
|
+
* `renderCell` makes of an empty record (a status chip saying "Open", say).
|
|
186
|
+
*
|
|
187
|
+
* So the aggregated value is rendered where there is one, nothing where there is not,
|
|
188
|
+
* and `renderCell` is left to the data rows it was written for. A column that wants to
|
|
189
|
+
* shape its total formats it with `valueFormatter`, which the grid applies to
|
|
190
|
+
* aggregation cells too.
|
|
191
|
+
*/
|
|
192
|
+
if (col.renderCell) {
|
|
193
|
+
let renderDataCell = col.renderCell;
|
|
194
|
+
col.renderCell = (params) => {
|
|
195
|
+
if (params.aggregation) return params.formattedValue ?? params.value ?? "";
|
|
196
|
+
if (isAggregationRow(params)) return "";
|
|
197
|
+
|
|
198
|
+
return renderDataCell(params);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
158
202
|
// The header filter cell is fully replaced by our own, which edits the
|
|
159
203
|
// shared filter tree instead of MUI's filter model.
|
|
160
204
|
if (col.filterable !== false && col.type !== "actions") {
|
|
@@ -254,7 +298,7 @@ export function useAfColumns({
|
|
|
254
298
|
|
|
255
299
|
const row = apiRef.current?.getRow(id);
|
|
256
300
|
if (row?.isNew) {
|
|
257
|
-
apiRef.current?.updateRows([{ [
|
|
301
|
+
apiRef.current?.updateRows([{ [uid]: id, id, _action: "delete" } as any]);
|
|
258
302
|
}
|
|
259
303
|
};
|
|
260
304
|
|
|
@@ -263,48 +307,62 @@ export function useAfColumns({
|
|
|
263
307
|
field: "actions",
|
|
264
308
|
type: "actions",
|
|
265
309
|
headerName: getLocalizedString("Actions"),
|
|
266
|
-
getActions: ({ id }: GridRowParams) =>
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
getActions: ({ id }: GridRowParams) => {
|
|
311
|
+
// The new item row only exists while inserting is allowed, so its save and
|
|
312
|
+
// cancel actions do not depend on the update permission, and it has nothing
|
|
313
|
+
// to edit or delete in view mode.
|
|
314
|
+
let isNewItem = isNewItemRow(id);
|
|
315
|
+
let actions = [
|
|
316
|
+
<GridActionItem
|
|
317
|
+
key="save"
|
|
318
|
+
icon={<FloppydiskIcon aria-hidden />}
|
|
319
|
+
label={getLocalizedString("Save")}
|
|
320
|
+
onClick={handleSaveClick(id)}
|
|
321
|
+
mode={GridRowModes.Edit}
|
|
322
|
+
dataObject={dataObject}
|
|
323
|
+
rowId={id}
|
|
324
|
+
requireUpdate={!isNewItem}
|
|
325
|
+
/>,
|
|
326
|
+
<GridActionItem
|
|
327
|
+
key="revert"
|
|
328
|
+
icon={<XMarkIcon aria-hidden />}
|
|
329
|
+
label={getLocalizedString("Cancel")}
|
|
330
|
+
onClick={handleCancelClick(id)}
|
|
331
|
+
mode={GridRowModes.Edit}
|
|
332
|
+
dataObject={dataObject}
|
|
333
|
+
rowId={id}
|
|
334
|
+
requireUpdate={!isNewItem}
|
|
335
|
+
/>,
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
if (isNewItem) {
|
|
339
|
+
return actions;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return [
|
|
343
|
+
<GridActionItem
|
|
344
|
+
key="edit"
|
|
345
|
+
icon={<PencilIcon aria-hidden />}
|
|
346
|
+
label={getLocalizedString("Edit")}
|
|
347
|
+
onClick={handleEditClick(id)}
|
|
348
|
+
mode={GridRowModes.View}
|
|
349
|
+
dataObject={dataObject}
|
|
350
|
+
rowId={id}
|
|
351
|
+
requireUpdate
|
|
352
|
+
/>,
|
|
353
|
+
<GridActionItem
|
|
354
|
+
key="delete"
|
|
355
|
+
icon={<TrashIcon aria-hidden />}
|
|
356
|
+
label={getLocalizedString("Delete")}
|
|
357
|
+
onClick={handleDeleteClick(id)}
|
|
358
|
+
mode={GridRowModes.View}
|
|
359
|
+
dataObject={dataObject}
|
|
360
|
+
rowId={id}
|
|
361
|
+
requireDelete
|
|
362
|
+
/>,
|
|
363
|
+
...actions,
|
|
364
|
+
];
|
|
365
|
+
},
|
|
308
366
|
});
|
|
309
367
|
}
|
|
310
368
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { GridApi, GridCellParams } from "@mui/x-data-grid-
|
|
2
|
-
import type
|
|
1
|
+
import type { GridApi, GridCellParams, GridRowId } from "@mui/x-data-grid-premium";
|
|
2
|
+
import { type DataObject, uid } from "@olenbetong/appframe-data";
|
|
3
3
|
import { useEvent } from "@olenbetong/appframe-react";
|
|
4
4
|
|
|
5
5
|
import { useEffect, useRef } from "react";
|
|
@@ -9,14 +9,19 @@ export type useAfCurrentIndexOptions = {
|
|
|
9
9
|
dataObject: DataObject<any>;
|
|
10
10
|
enabled: boolean;
|
|
11
11
|
idField: string;
|
|
12
|
+
/**
|
|
13
|
+
* Id of the grid's new item row, if it has one. A current index of -1 then
|
|
14
|
+
* focuses that row, since the framework's new record has no row in the data.
|
|
15
|
+
*/
|
|
16
|
+
newItemRowId?: GridRowId | null;
|
|
12
17
|
};
|
|
13
18
|
|
|
14
19
|
function getRowId(row: any, idField: string) {
|
|
15
20
|
if (!row) return row;
|
|
16
|
-
return row[
|
|
21
|
+
return row[uid] ?? row[idField];
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
export function useAfCurrentIndex({ apiRef, dataObject, idField, enabled }: useAfCurrentIndexOptions) {
|
|
24
|
+
export function useAfCurrentIndex({ apiRef, dataObject, idField, enabled, newItemRowId }: useAfCurrentIndexOptions) {
|
|
20
25
|
let triggeredByFocusEvent = useRef(false);
|
|
21
26
|
let focusColumn = useRef<string>(null);
|
|
22
27
|
|
|
@@ -46,7 +51,10 @@ export function useAfCurrentIndex({ apiRef, dataObject, idField, enabled }: useA
|
|
|
46
51
|
useEvent(dataObject, "onCurrentIndexChanged", (event) => {
|
|
47
52
|
if (enabled && triggeredByFocusEvent.current === false) {
|
|
48
53
|
if (focusColumn.current && document.activeElement?.closest(".MuiDataGrid-cell")) {
|
|
49
|
-
|
|
54
|
+
let rowId = event.detail === -1 ? newItemRowId : getRowId(dataObject.getData(event.detail), idField);
|
|
55
|
+
if (rowId != null) {
|
|
56
|
+
apiRef.current?.setCellFocus(rowId, focusColumn.current);
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
} else {
|
|
52
60
|
triggeredByFocusEvent.current = false;
|
package/src/grid/useAfGridApi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GridApi } from "@mui/x-data-grid-
|
|
1
|
+
import type { GridApi } from "@mui/x-data-grid-premium";
|
|
2
2
|
import type { DataObject } from "@olenbetong/appframe-data";
|
|
3
3
|
|
|
4
4
|
import type React from "react";
|
|
@@ -11,6 +11,11 @@ import { afSortOrderToMuiSortModel } from "./useAfSortModel.js";
|
|
|
11
11
|
export interface AfGridApi extends GridApi {
|
|
12
12
|
getDataObject: () => DataObject<any> | undefined;
|
|
13
13
|
resetLayout: () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Focuses the new item row and starts editing it. Returns false when the grid
|
|
16
|
+
* has no new item row, because it is turned off or inserting is not allowed.
|
|
17
|
+
*/
|
|
18
|
+
startNewItemRow?: () => boolean;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export function useAfGridApi(props: Partial<AfGridProps>, apiRef: React.RefObject<GridApi | null>) {
|