@olenbetong/appframe-ds 0.9.1 → 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 +113 -0
- package/package.json +21 -16
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
# @olenbetong/appframe-ds
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 3c78efe: Migrate the grid from MUI X Pro to MUI X Premium.
|
|
8
|
+
|
|
9
|
+
**Breaking:** the `@mui/x-data-grid-pro` peer dependency is replaced by
|
|
10
|
+
`@mui/x-data-grid-premium`, and the MUI X license key must be a Premium key
|
|
11
|
+
(`S=premium`). A Pro key makes every grid fail with "license key plan mismatch".
|
|
12
|
+
|
|
13
|
+
`AfGrid` still renders and behaves as before — `DataGridPremium` is a superset of
|
|
14
|
+
`DataGridPro` — but the re-exported namespaces now come from the Premium package:
|
|
15
|
+
- `@olenbetong/appframe-ds/grid` → `MuiDataGrid`
|
|
16
|
+
- `@olenbetong/appframe-mui` → `DataGrid`
|
|
17
|
+
|
|
18
|
+
Consumers importing `DataGridPro`, `DataGridProProps` or `GridApiPro` through those
|
|
19
|
+
namespaces must rename them to `DataGridPremium`, `DataGridPremiumProps` and
|
|
20
|
+
`GridApiPremium`.
|
|
21
|
+
|
|
22
|
+
Row grouping and aggregation are now wired up:
|
|
23
|
+
- The column menu gains "Group by …" and "Aggregation", from the Premium package.
|
|
24
|
+
- Both models are persisted with the rest of the grid layout, and restored on
|
|
25
|
+
reload. Turn either off with `persistenceModel.rowGrouping` /
|
|
26
|
+
`persistenceModel.aggregation`.
|
|
27
|
+
- While a grouping is active the grid falls back to client side sorting, since
|
|
28
|
+
grouped rows are built and ordered on the client. `disableServerSorting` no
|
|
29
|
+
longer has to be set by hand for a grouped grid.
|
|
30
|
+
- The new `useAfRowGrouping` hook is exported for grids that compose the grid
|
|
31
|
+
props themselves.
|
|
32
|
+
|
|
33
|
+
Note that grouping and aggregation are computed over the rows the grid holds. For
|
|
34
|
+
a dynamically loading data object that is the current page only, so a grouped grid
|
|
35
|
+
wants `maxRecords` large enough to cover the result set.
|
|
36
|
+
|
|
37
|
+
### Minor Changes
|
|
38
|
+
|
|
39
|
+
- c6ee375: `AfGrid` can resolve the grand total on the server. MUI aggregates over the rows it has been given,
|
|
40
|
+
which on a dynamically loading data object is the current page — a sum that looks like a total and
|
|
41
|
+
is not one. The grid now runs a second retrieve under the same filters asking only for the
|
|
42
|
+
aggregates, and substitutes that into the footer. On by default when the data object loads
|
|
43
|
+
dynamically; `serverAggregation={false}` opts out. `sum`, `avg`, `min` and `max` are resolved server
|
|
44
|
+
side, group subtotals stay on the client, and a view that cannot be aggregated falls back to the
|
|
45
|
+
client total rather than failing.
|
|
46
|
+
|
|
47
|
+
Aggregation rows also no longer run a column's `renderCell`. A total row has no record behind it, so
|
|
48
|
+
a `renderCell` reading `params.row` rendered nothing there — turning on a sum produced a pinned row
|
|
49
|
+
of empty cells, and unaggregated columns rendered whatever their `renderCell` made of an empty
|
|
50
|
+
record. The grid renders the aggregated value instead, formatted by the column's `valueFormatter`.
|
|
51
|
+
|
|
52
|
+
Links inside a grid cell now take the brand colour and an underline instead of inheriting the cell's
|
|
53
|
+
text colour, which left them indistinguishable from ordinary text until hovered.
|
|
54
|
+
|
|
55
|
+
- 61afab0: Add `AppMenuDrawer` and `useAppMenuDrawer`, so an app can have an app menu without an appbar.
|
|
56
|
+
|
|
57
|
+
Apps on the `ob.2026.application` template get a toolbar from the template. Until now the app menu
|
|
58
|
+
was only reachable through `Page.Header`'s `appbar` variant, which draws a bar of its own — so
|
|
59
|
+
adding navigation meant a second, stacked toolbar. The drawer is now a component in its own right:
|
|
60
|
+
docked beside the content on desktop, an overlay on smaller screens, exactly as before.
|
|
61
|
+
|
|
62
|
+
`Page.Header` is refactored onto the same component and hook, so there is one implementation and
|
|
63
|
+
its behaviour is unchanged.
|
|
64
|
+
|
|
65
|
+
The drawer is now positioned against the page rather than the viewport (`absolute` inside
|
|
66
|
+
`Page`, which becomes `position: relative`), so it stays clear of any site chrome rendered above
|
|
67
|
+
the app. Raising its `z-index` would instead put the menu over that chrome and hide it — and
|
|
68
|
+
`#root` frequently creates its own stacking context, so it would not reliably work anyway. A
|
|
69
|
+
drawer therefore has to be inside `Page`, and `layout="app"` so the page has a height.
|
|
70
|
+
|
|
71
|
+
`Page` with `layout="app"` is now `height: 100%` rather than `100vh`. It fills the element the app
|
|
72
|
+
is mounted in; `100vh` was taller than the space available whenever a template rendered chrome
|
|
73
|
+
above `#root`, cutting off the bottom of the page.
|
|
74
|
+
|
|
75
|
+
- d64c1b1: Add the `Mdi` multi-document workspace under the new `@olenbetong/appframe-ds/mdi` subpath: tiled groups of tabbed documents on dockview, with a document-type registry (type → React component + params), open-or-activate by key, unclosable panes, a dirty-close prompt, popout windows, and per-user layout persistence in IndexedDB including the open documents. Adds the `dockview-react` dependency; it is only bundled by apps that import the subpath.
|
|
76
|
+
- 2e2ef82: Add a DevExpress-style new item row to `AfGrid`: a single non-data row pinned above or below the data (`newItemRow="top" | "bottom" | "none"`, default `bottom`) that reads "Click here to add a new row" until edited. Editing it moves the data object's current index to -1, the framework's new record, so bound forms and child data objects follow; committing it saves the record through `endEdit()`, and Escape discards it. The toolbar Add button and Alt+N start it, and `apiRef.current.startNewItemRow()` starts it from code. When `newItemRow` is not given, the position follows the data object's `newRecordPosition` option, so the row sits where the saved record will land; older data packages without the option get the row at the bottom. The previous approach of injecting an unsaved row into the grid is only used when the new item row is turned off.
|
|
77
|
+
|
|
78
|
+
Row ids are now read through the `uid` symbol exported by `@olenbetong/appframe-data` instead of the global `af.data.uid`. They are the same object in applications; the change matters where the data package is bundled separately, such as Storybook.
|
|
79
|
+
|
|
80
|
+
- e022401: `Page.Block` takes an `align` prop. `start` keeps a width-constrained block against the leading edge
|
|
81
|
+
instead of centring it in whatever space is left — what the old `PageContainer` did, and what an app
|
|
82
|
+
with a docked navigation drawer usually wants, since centring leaves the content floating away from
|
|
83
|
+
the menu it belongs to. Defaults to `center`, so nothing changes for existing blocks.
|
|
84
|
+
|
|
85
|
+
### Patch Changes
|
|
86
|
+
|
|
87
|
+
- 6fa571f: `Combobox` and `Autocomplete` lay a custom option out as a row. `renderOption` content sat in a
|
|
88
|
+
bare inline `span`, so an option built from an avatar or icon beside a block of text — which is
|
|
89
|
+
what most of them are — dropped the text onto its own line and stacked the two. The wrapper is now
|
|
90
|
+
a flex row with the popup's own gap, and carries a class (`ObAutocomplete-itemContent`) so it can
|
|
91
|
+
be styled.
|
|
92
|
+
- 22cd5f4: `BoundTextField` now accepts the textarea-only props. `TextfieldProps` is a union of the input and
|
|
93
|
+
the textarea variant, and the non-distributive `Omit` over it kept only what both branches share —
|
|
94
|
+
so `multiline` type-checked but `rows` and `cols` did not.
|
|
95
|
+
- 22cd5f4: `Combobox` now reports the selected option's label through `onInputChange` with the reason
|
|
96
|
+
`"reset"`. It already wrote that label into its own input, but the write is a no-op when the caller
|
|
97
|
+
controls `inputValue`, so a controlled input stayed empty next to a value that was set from the
|
|
98
|
+
outside.
|
|
99
|
+
- 21c1977: Fix the distinct value list overlapping the Apply and Clear buttons in the grid's header filter panel.
|
|
100
|
+
|
|
101
|
+
The distinct section was free to shrink to nothing while the list inside it held a minimum height, so once the panel was shorter than its content the list spilled out — the section's overflow is visible — and painted over the buttons and the truncation hint below it. It now has an explicit floor covering the search toolbar, the hint and the list's own minimum, so a panel too short for that scrolls instead of overlapping. The floor cannot be `auto`: that resolves to min-content, and the list is the scroll container, so its min-content height is the entire unscrolled list.
|
|
102
|
+
|
|
103
|
+
The header filter panel's default height also goes from `26rem` to `30rem`, which is what the criteria, the list at its minimum and the hint actually need. The panel is still resizable and still capped at `80vh`.
|
|
104
|
+
|
|
105
|
+
- 96600c7: Stop the data grid from logging `Designsystemet: Missing tabindex="0" attribute on:` for every column header.
|
|
106
|
+
|
|
107
|
+
Designsystemet's tooltip labels its host element and warns when that host is not tabbable. Inside the grid it never is — the column header owns the tab stop and moves focus with the arrow keys, so MUI gives the column menu button `tabindex="-1"` and hangs the sort tooltip on a bare `<span>`. The tooltips work regardless, because Designsystemet resolves the host with `closest()` on hover and focus, so the warning was a false positive — and it is not gated on `NODE_ENV`, so it reached production builds too.
|
|
108
|
+
|
|
109
|
+
The `baseTooltip` slot now applies the accessible name itself, using the same rule Designsystemet does (`aria-description` when the host has text content, `aria-label` when it does not), which makes Designsystemet skip the branch that warns. Markup and behaviour are unchanged, and `window.dsWarnings` stays on for the rest of the app.
|
|
110
|
+
|
|
111
|
+
- Updated dependencies [690f764]
|
|
112
|
+
- Updated dependencies [2e2ef82]
|
|
113
|
+
- @olenbetong/appframe-react@2.1.3
|
|
114
|
+
- @olenbetong/appframe-data@1.7.0
|
|
115
|
+
|
|
3
116
|
## 0.9.1
|
|
4
117
|
|
|
5
118
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-ds",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Components that bind Designsystemet components to Appframe data objects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./es/index.d.ts",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"types": "./es/grid/index.d.ts",
|
|
14
14
|
"import": "./es/grid/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./mdi": {
|
|
17
|
+
"types": "./es/mdi/index.d.ts",
|
|
18
|
+
"import": "./es/mdi/index.js"
|
|
19
|
+
},
|
|
16
20
|
"./report-downloader": {
|
|
17
21
|
"types": "./es/report-downloader/index.d.ts",
|
|
18
22
|
"import": "./es/report-downloader/index.js"
|
|
@@ -28,7 +32,7 @@
|
|
|
28
32
|
"@emotion/react": ">=11.14.0",
|
|
29
33
|
"@emotion/styled": ">=11.14.1",
|
|
30
34
|
"@mui/material": ">=9.0.0",
|
|
31
|
-
"@mui/x-data-grid-
|
|
35
|
+
"@mui/x-data-grid-premium": ">=8.0.0 <9.0.0",
|
|
32
36
|
"@mui/x-license": ">=8.0.0 <9.0.0",
|
|
33
37
|
"@navikt/aksel-icons": ">=8.16.1",
|
|
34
38
|
"react": ">=19.2.8",
|
|
@@ -46,7 +50,7 @@
|
|
|
46
50
|
"@mui/material": {
|
|
47
51
|
"optional": true
|
|
48
52
|
},
|
|
49
|
-
"@mui/x-data-grid-
|
|
53
|
+
"@mui/x-data-grid-premium": {
|
|
50
54
|
"optional": true
|
|
51
55
|
},
|
|
52
56
|
"@mui/x-license": {
|
|
@@ -55,37 +59,38 @@
|
|
|
55
59
|
},
|
|
56
60
|
"dependencies": {
|
|
57
61
|
"@atlaskit/pragmatic-drag-and-drop": "2.0.2",
|
|
58
|
-
"@atlaskit/pragmatic-drag-and-drop-hitbox": "2.
|
|
62
|
+
"@atlaskit/pragmatic-drag-and-drop-hitbox": "2.2.0",
|
|
59
63
|
"@base-ui/react": "1.7.0",
|
|
60
64
|
"clsx": "^2.1.1",
|
|
65
|
+
"dockview-react": "8.2.0",
|
|
61
66
|
"localforage": "^1.10.0",
|
|
62
67
|
"mitt": "^3.0.1",
|
|
63
68
|
"react-virtualized-auto-sizer": "^1.0.26",
|
|
64
69
|
"react-window": "^1.8.11",
|
|
65
70
|
"@olenbetong/appframe-core": "2.13.1",
|
|
66
|
-
"@olenbetong/appframe-data": "1.
|
|
67
|
-
"@olenbetong/appframe-react": "2.1.
|
|
71
|
+
"@olenbetong/appframe-data": "1.7.0",
|
|
72
|
+
"@olenbetong/appframe-react": "2.1.3"
|
|
68
73
|
},
|
|
69
74
|
"devDependencies": {
|
|
70
|
-
"@digdir/designsystemet-react": "1.
|
|
75
|
+
"@digdir/designsystemet-react": "1.21.0",
|
|
71
76
|
"@emotion/react": "^11.14.0",
|
|
72
77
|
"@emotion/styled": "^11.14.1",
|
|
73
|
-
"@mui/material": "9.
|
|
74
|
-
"@mui/x-data-grid-
|
|
75
|
-
"@mui/x-license": "8.29.
|
|
78
|
+
"@mui/material": "9.4.0",
|
|
79
|
+
"@mui/x-data-grid-premium": "8.29.3",
|
|
80
|
+
"@mui/x-license": "8.29.3",
|
|
76
81
|
"@navikt/aksel-icons": "8.16.1",
|
|
77
82
|
"@testing-library/dom": "^10.4.1",
|
|
78
83
|
"@testing-library/jest-dom": "^7.0.1",
|
|
79
|
-
"@testing-library/react": "16.3.
|
|
80
|
-
"@testing-library/user-event": "^14.6.
|
|
84
|
+
"@testing-library/react": "16.3.3",
|
|
85
|
+
"@testing-library/user-event": "^14.6.6",
|
|
81
86
|
"@types/react": "19.2.18",
|
|
82
|
-
"@types/react-dom": "19.2.
|
|
87
|
+
"@types/react-dom": "19.2.5",
|
|
83
88
|
"@types/react-window": "^1.8.8",
|
|
84
|
-
"happy-dom": "^20.
|
|
89
|
+
"happy-dom": "^20.12.0",
|
|
85
90
|
"react": "19.2.8",
|
|
86
91
|
"react-dom": "19.2.8",
|
|
87
|
-
"react-error-boundary": "6.1.
|
|
88
|
-
"react-router": "8.3.
|
|
92
|
+
"react-error-boundary": "6.1.4",
|
|
93
|
+
"react-router": "8.3.1",
|
|
89
94
|
"typescript": "7.0.2"
|
|
90
95
|
},
|
|
91
96
|
"scripts": {
|
package/scripts/copyAssets.mjs
CHANGED
|
@@ -5,6 +5,7 @@ const files = [
|
|
|
5
5
|
["src/AfLookup/LookupGrid.css", "es/AfLookup/LookupGrid.css"],
|
|
6
6
|
["src/AfLookup/LookupCombobox.css", "es/AfLookup/LookupCombobox.css"],
|
|
7
7
|
["src/layout/AppMenu.css", "es/layout/AppMenu.css"],
|
|
8
|
+
["src/layout/AppMenuDrawer.css", "es/layout/AppMenuDrawer.css"],
|
|
8
9
|
["src/layout/AppMenuItem.css", "es/layout/AppMenuItem.css"],
|
|
9
10
|
["src/layout/AppMenuSubheader.css", "es/layout/AppMenuSubheader.css"],
|
|
10
11
|
["src/layout/AppLayoutDesktop.css", "es/layout/AppLayoutDesktop.css"],
|
|
@@ -30,6 +31,7 @@ const files = [
|
|
|
30
31
|
["src/filter/SavedFilterTree.css", "es/filter/SavedFilterTree.css"],
|
|
31
32
|
["src/grid/index.css", "es/grid/index.css"],
|
|
32
33
|
["src/grid/slots/slots.css", "es/grid/slots/slots.css"],
|
|
34
|
+
["src/mdi/Mdi.css", "es/mdi/Mdi.css"],
|
|
33
35
|
["src/report-downloader/ReportDownloader.css", "es/report-downloader/ReportDownloader.css"],
|
|
34
36
|
];
|
|
35
37
|
|
|
@@ -44,6 +46,7 @@ mkdirSync("es/autocomplete", { recursive: true });
|
|
|
44
46
|
mkdirSync("es/filter", { recursive: true });
|
|
45
47
|
mkdirSync("es/grid", { recursive: true });
|
|
46
48
|
mkdirSync("es/grid/slots", { recursive: true });
|
|
49
|
+
mkdirSync("es/mdi", { recursive: true });
|
|
47
50
|
mkdirSync("es/report-downloader", { recursive: true });
|
|
48
51
|
|
|
49
52
|
for (let [src, dest] of files) {
|
|
@@ -223,6 +223,19 @@
|
|
|
223
223
|
opacity: var(--ds-opacity-disabled);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
+
/*
|
|
227
|
+
* The option's content. It is a flex row rather than the inline box it looks like, because
|
|
228
|
+
* `renderOption` is normally an icon or an avatar beside a block of text — and a block child in an
|
|
229
|
+
* inline parent drops onto its own line, stacking the two instead of setting them side by side.
|
|
230
|
+
*/
|
|
231
|
+
.ObAutocomplete-itemContent {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: var(--ds-size-2);
|
|
235
|
+
flex: 1;
|
|
236
|
+
min-inline-size: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
226
239
|
.ObAutocomplete-itemIndicator {
|
|
227
240
|
display: inline-flex;
|
|
228
241
|
flex: none;
|
|
@@ -228,7 +228,9 @@ export function Autocomplete<
|
|
|
228
228
|
data-selected={selected || undefined}
|
|
229
229
|
onClick={(event) => handleSelectOption(option, event.nativeEvent)}
|
|
230
230
|
>
|
|
231
|
-
<span
|
|
231
|
+
<span className="ObAutocomplete-itemContent">
|
|
232
|
+
{renderOption?.(option, { index, inputValue, selected }) ?? getOptionLabel(option)}
|
|
233
|
+
</span>
|
|
232
234
|
</BaseAutocomplete.Item>
|
|
233
235
|
);
|
|
234
236
|
}
|
|
@@ -121,14 +121,25 @@ export function Combobox<Option, Multiple extends boolean | undefined = false>(p
|
|
|
121
121
|
// the user interacts, but not when the selection is set from the outside.
|
|
122
122
|
let selectedLabel = multiple || value == null ? "" : getOptionLabel(value as Option);
|
|
123
123
|
let [inputValue, setInputValue] = useControlled(inputValueProp, selectedLabel);
|
|
124
|
-
|
|
124
|
+
// `null` until the first sync, so a value that is already set on mount is synced too.
|
|
125
|
+
let previousLabel = useRef<string | null>(null);
|
|
125
126
|
|
|
126
127
|
useEffect(() => {
|
|
127
128
|
if (previousLabel.current === selectedLabel) return;
|
|
128
129
|
|
|
130
|
+
let firstSync = previousLabel.current === null;
|
|
129
131
|
previousLabel.current = selectedLabel;
|
|
132
|
+
|
|
133
|
+
// Nothing to correct on mount when no option is selected, and blanking the input would
|
|
134
|
+
// throw away an initial `inputValue` the caller set deliberately.
|
|
135
|
+
if (firstSync && selectedLabel === "") return;
|
|
136
|
+
|
|
130
137
|
setInputValue(selectedLabel);
|
|
131
|
-
|
|
138
|
+
// `setInputValue` is a no-op when the caller controls `inputValue`, so the label has to
|
|
139
|
+
// be reported instead — otherwise a controlled input stays empty next to a selected
|
|
140
|
+
// value. Mirrors MUI, which reports the same thing with the reason `"reset"`.
|
|
141
|
+
onInputChange?.(undefined, selectedLabel, "reset");
|
|
142
|
+
}, [selectedLabel, setInputValue, onInputChange]);
|
|
132
143
|
|
|
133
144
|
let filteredOptions = useMemo(() => {
|
|
134
145
|
let filter = (filterOptions ?? defaultFilterOptions) as (
|
|
@@ -190,7 +201,9 @@ export function Combobox<Option, Multiple extends boolean | undefined = false>(p
|
|
|
190
201
|
{multiple && (
|
|
191
202
|
<BaseCombobox.ItemIndicator className="ObAutocomplete-itemIndicator">✓</BaseCombobox.ItemIndicator>
|
|
192
203
|
)}
|
|
193
|
-
<span
|
|
204
|
+
<span className="ObAutocomplete-itemContent">
|
|
205
|
+
{renderOption?.(option, { index, inputValue, selected }) ?? getOptionLabel(option)}
|
|
206
|
+
</span>
|
|
194
207
|
</BaseCombobox.Item>
|
|
195
208
|
);
|
|
196
209
|
}
|
|
@@ -2,7 +2,15 @@ import { Textfield, type TextfieldProps } from "@digdir/designsystemet-react";
|
|
|
2
2
|
import { useField } from "@olenbetong/appframe-react";
|
|
3
3
|
import { forwardRef } from "react";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/*
|
|
6
|
+
* `TextfieldProps` is a union of the input and the textarea variant. A plain `Omit` over it keeps
|
|
7
|
+
* only the props both branches share, which silently drops the textarea-only ones (`rows`,
|
|
8
|
+
* `cols`) — so `multiline` was accepted but `rows` was not. Distributing the `Omit` over the union
|
|
9
|
+
* keeps each branch intact.
|
|
10
|
+
*/
|
|
11
|
+
type OmitBoundProps<T> = T extends unknown ? Omit<T, "value" | "onChange" | "onKeyDown"> : never;
|
|
12
|
+
|
|
13
|
+
export type BoundTextFieldProps = OmitBoundProps<TextfieldProps> & {
|
|
6
14
|
field: string;
|
|
7
15
|
};
|
|
8
16
|
|
|
@@ -41,13 +41,36 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.ObFieldFilterPanel-distinct {
|
|
44
|
+
/*
|
|
45
|
+
* The parts the section cannot usefully shrink past, named so the floor below
|
|
46
|
+
* and the list's own minimum cannot drift apart.
|
|
47
|
+
*/
|
|
48
|
+
--obFieldFilterPanel-list-min-size: 8rem;
|
|
49
|
+
--obFieldFilterPanel-toolbar-size: 2.75rem; /* a `sm` search field */
|
|
50
|
+
--obFieldFilterPanel-hint-size: 1.25rem; /* one line of truncation hint */
|
|
51
|
+
|
|
44
52
|
display: flex;
|
|
45
53
|
flex-direction: column;
|
|
46
54
|
flex: 1 1 auto;
|
|
47
55
|
gap: var(--ds-size-2, 0.5rem);
|
|
48
|
-
min-block-size: 0;
|
|
49
56
|
padding-block-start: var(--ds-size-2, 0.5rem);
|
|
50
57
|
border-block-start: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-subtle, #e7e7e7);
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* The section follows the panel's height, but only down to the toolbar, the
|
|
61
|
+
* hint and the list's own minimum. Without a floor it kept shrinking while
|
|
62
|
+
* the list held its minimum, and since the overflow here is visible the list
|
|
63
|
+
* spilled out and painted over the buttons the panel puts after it.
|
|
64
|
+
*
|
|
65
|
+
* The floor has to be an explicit length. `auto` resolves to min-content, and
|
|
66
|
+
* the list is the scroll container, so its min-content height is the whole
|
|
67
|
+
* unscrolled list — that would stretch the section to fit every distinct
|
|
68
|
+
* value. Once the panel is too short for this, it scrolls instead.
|
|
69
|
+
*/
|
|
70
|
+
min-block-size: calc(
|
|
71
|
+
var(--obFieldFilterPanel-list-min-size) + var(--obFieldFilterPanel-toolbar-size) +
|
|
72
|
+
var(--obFieldFilterPanel-hint-size) + 2 * var(--ds-size-2, 0.5rem)
|
|
73
|
+
);
|
|
51
74
|
}
|
|
52
75
|
|
|
53
76
|
.ObFieldFilterPanel-distinctToolbar {
|
|
@@ -80,7 +103,7 @@
|
|
|
80
103
|
*/
|
|
81
104
|
.ObFieldFilterPanel-valuesBox {
|
|
82
105
|
flex: 1 1 auto;
|
|
83
|
-
min-block-size: 8rem;
|
|
106
|
+
min-block-size: var(--obFieldFilterPanel-list-min-size, 8rem);
|
|
84
107
|
border: var(--ds-border-width-default, 1px) solid var(--ds-color-neutral-border-subtle, #e7e7e7);
|
|
85
108
|
border-radius: var(--ds-border-radius-md, 0.25rem);
|
|
86
109
|
background: var(--ds-color-neutral-surface-tinted, rgb(0 0 0 / 2%));
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
gridColumnDefinitionsSelector,
|
|
5
5
|
gridColumnVisibilityModelSelector,
|
|
6
6
|
useGridSelector,
|
|
7
|
-
} from "@mui/x-data-grid-
|
|
7
|
+
} from "@mui/x-data-grid-premium";
|
|
8
8
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
9
9
|
|
|
10
10
|
import { useState } from "react";
|
package/src/grid/AfGridError.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Alert, Button } from "@digdir/designsystemet-react";
|
|
2
|
-
import type { GridApi } from "@mui/x-data-grid-
|
|
2
|
+
import type { GridApi } from "@mui/x-data-grid-premium";
|
|
3
3
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
4
4
|
import type { DataObject } from "@olenbetong/appframe-data";
|
|
5
5
|
import { useError } from "@olenbetong/appframe-react";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button, Popover } from "@digdir/designsystemet-react";
|
|
2
|
-
import type { GridRenderHeaderFilterProps } from "@mui/x-data-grid-
|
|
2
|
+
import type { GridRenderHeaderFilterProps } from "@mui/x-data-grid-premium";
|
|
3
3
|
import { FunnelFillIcon, FunnelIcon, XMarkIcon } from "@navikt/aksel-icons";
|
|
4
4
|
import {
|
|
5
5
|
createExpression,
|
package/src/grid/Toolbar.tsx
CHANGED
package/src/grid/editing.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { type GridApi, gridFocusCellSelector, gridVisibleColumnDefinitionsSelector } from "@mui/x-data-grid-
|
|
1
|
+
import { type GridApi, gridFocusCellSelector, gridVisibleColumnDefinitionsSelector } from "@mui/x-data-grid-premium";
|
|
2
2
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
3
|
-
import type
|
|
3
|
+
import { type DataObject, uid } from "@olenbetong/appframe-data";
|
|
4
4
|
|
|
5
5
|
import type React from "react";
|
|
6
6
|
|
|
7
|
+
import type { AfGridApi } from "./useAfGridApi.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Starts a new row. With a new item row on the grid that row is focused and put
|
|
11
|
+
* in edit mode; otherwise an unsaved row is appended to the data and edited there.
|
|
12
|
+
*/
|
|
7
13
|
export function addRow<T extends { PrimKey: string }>({
|
|
8
14
|
apiRef,
|
|
9
15
|
dataObject,
|
|
@@ -11,9 +17,13 @@ export function addRow<T extends { PrimKey: string }>({
|
|
|
11
17
|
apiRef: React.RefObject<GridApi | null>;
|
|
12
18
|
dataObject: DataObject<T>;
|
|
13
19
|
}) {
|
|
20
|
+
if ((apiRef.current as AfGridApi | null)?.startNewItemRow?.()) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
let newRow = dataObject.newRow();
|
|
15
|
-
let id = (newRow as any)[
|
|
16
|
-
apiRef.current?.updateRows([{ [
|
|
25
|
+
let id = (newRow as any)[uid];
|
|
26
|
+
apiRef.current?.updateRows([{ [uid]: id, id, isNew: true } as any]);
|
|
17
27
|
apiRef.current?.startRowEditMode({ id });
|
|
18
28
|
|
|
19
29
|
// Wait for the grid to render with the new row
|
|
@@ -48,7 +58,7 @@ export async function deleteRow<T extends { PrimKey: string }>({
|
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
let focusBefore = gridFocusCellSelector(apiRef);
|
|
51
|
-
let index = dataObject.findIndex((r) => (r as any)[idField] === id || (r as any)[
|
|
61
|
+
let index = dataObject.findIndex((r) => (r as any)[idField] === id || (r as any)[uid] === id);
|
|
52
62
|
|
|
53
63
|
if (index < 0) {
|
|
54
64
|
alert(getLocalizedString("Row not found"));
|
|
@@ -60,12 +70,12 @@ export async function deleteRow<T extends { PrimKey: string }>({
|
|
|
60
70
|
let focusAfter = gridFocusCellSelector(apiRef);
|
|
61
71
|
|
|
62
72
|
// Need to run this operation to prevent MUI errors
|
|
63
|
-
apiRef.current?.updateRows([{ [
|
|
73
|
+
apiRef.current?.updateRows([{ [uid]: id, id, _action: "delete" }]);
|
|
64
74
|
|
|
65
75
|
if (!focusAfter || focusAfter.id === id) {
|
|
66
76
|
if (dataObject.getDataLength() > 0) {
|
|
67
77
|
let newIndex = Math.max(0, Math.min(dataObject.getDataLength() - 1, index - 1));
|
|
68
|
-
let newId = (dataObject.getData(newIndex) as any)[
|
|
78
|
+
let newId = (dataObject.getData(newIndex) as any)[uid];
|
|
69
79
|
let newField = focusBefore?.field ?? gridVisibleColumnDefinitionsSelector(apiRef)[0]?.field;
|
|
70
80
|
|
|
71
81
|
if (newId && newField) {
|
package/src/grid/filter.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
getGridNumericOperators,
|
|
11
11
|
getGridSingleSelectOperators,
|
|
12
12
|
getGridStringOperators,
|
|
13
|
-
} from "@mui/x-data-grid-
|
|
13
|
+
} from "@mui/x-data-grid-premium";
|
|
14
14
|
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
15
15
|
|
|
16
16
|
const stringOperators = getGridStringOperators();
|
package/src/grid/index.css
CHANGED
|
@@ -162,8 +162,14 @@
|
|
|
162
162
|
|
|
163
163
|
inline-size: 22rem;
|
|
164
164
|
max-inline-size: 90vw;
|
|
165
|
-
block-size: 26rem;
|
|
166
165
|
max-block-size: 80vh;
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* Tall enough for the criteria, the distinct list at its minimum and the
|
|
169
|
+
* truncation hint, without the panel having to scroll. At 26rem the hint and
|
|
170
|
+
* the buttons below it did not fit.
|
|
171
|
+
*/
|
|
172
|
+
block-size: 30rem;
|
|
167
173
|
}
|
|
168
174
|
|
|
169
175
|
/*
|
|
@@ -185,3 +191,55 @@
|
|
|
185
191
|
flex: 1 1 auto;
|
|
186
192
|
min-block-size: 0;
|
|
187
193
|
}
|
|
194
|
+
|
|
195
|
+
/* New item row */
|
|
196
|
+
|
|
197
|
+
/*
|
|
198
|
+
* The new item row is a pinned row that shows an overlay text until it is
|
|
199
|
+
* edited. The text comes from the `--AfGrid-newItemRowText` custom property the
|
|
200
|
+
* grid root sets, so it is localized without an extra DOM node. In view mode the
|
|
201
|
+
* cell contents are hidden rather than removed, so keyboard focus still lands
|
|
202
|
+
* on a cell and a double-click or keypress starts editing as on any row.
|
|
203
|
+
*/
|
|
204
|
+
.MuiDataGrid-row.AfGrid-newItemRow {
|
|
205
|
+
position: relative;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.MuiDataGrid-row.AfGrid-newItemRow:not(.MuiDataGrid-row--editing) .MuiDataGrid-cell > * {
|
|
209
|
+
visibility: hidden;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.MuiDataGrid-row.AfGrid-newItemRow:not(.MuiDataGrid-row--editing)::after {
|
|
213
|
+
content: var(--AfGrid-newItemRowText, "");
|
|
214
|
+
position: absolute;
|
|
215
|
+
inset: 0;
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
padding-inline: var(--ds-size-2, 0.5rem);
|
|
219
|
+
color: var(--ds-color-neutral-text-subtle, #68707c);
|
|
220
|
+
font-style: italic;
|
|
221
|
+
pointer-events: none;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/*
|
|
225
|
+
* Links in a cell.
|
|
226
|
+
*
|
|
227
|
+
* The grid sets its own text colour on the cell, and an anchor inherits it, so a link to another
|
|
228
|
+
* record renders as ordinary text and is only discoverable by hovering it. These give it the
|
|
229
|
+
* brand colour and an underline the way a link anywhere else on the site has, without the app
|
|
230
|
+
* having to style every cell that holds one.
|
|
231
|
+
*/
|
|
232
|
+
.AfGrid-root .MuiDataGrid-cell a {
|
|
233
|
+
color: var(--ds-color-brand-text-default, #0062ba);
|
|
234
|
+
text-decoration: underline;
|
|
235
|
+
text-underline-offset: 0.15em;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.AfGrid-root .MuiDataGrid-cell a:hover {
|
|
239
|
+
color: var(--ds-color-brand-text-subtle, #00417d);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/* Action buttons render as anchors when a column links a row somewhere; they style themselves. */
|
|
243
|
+
.AfGrid-root .MuiDataGrid-cell a.ds-button {
|
|
244
|
+
text-decoration: none;
|
|
245
|
+
}
|