@olenbetong/appframe-ds 1.0.3 → 1.2.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.
Files changed (173) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/package.json +8 -7
  3. package/scripts/copyAssets.mjs +27 -55
  4. package/src/AfLookup/Lookup.css +129 -125
  5. package/src/AfLookup/Lookup.tsx +184 -162
  6. package/src/AfLookup/LookupCombobox.css +78 -78
  7. package/src/AfLookup/LookupCombobox.tsx +237 -237
  8. package/src/AfLookup/LookupEdit.tsx +130 -130
  9. package/src/AfLookup/LookupGrid.tsx +124 -124
  10. package/src/AfLookup/index.ts +5 -5
  11. package/src/autocomplete/Autocomplete.css +256 -256
  12. package/src/autocomplete/Autocomplete.tsx +320 -320
  13. package/src/autocomplete/AutocompleteFrame.tsx +168 -168
  14. package/src/autocomplete/Combobox.tsx +286 -286
  15. package/src/autocomplete/index.ts +4 -4
  16. package/src/autocomplete/types.ts +204 -204
  17. package/src/autocomplete/utils.ts +183 -183
  18. package/src/binding/BoundNumericTextField.tsx +102 -102
  19. package/src/binding/BoundTextField.tsx +49 -49
  20. package/src/binding/CancelButton.tsx +30 -30
  21. package/src/binding/CancelIconButton.tsx +30 -30
  22. package/src/binding/DataEditToolbar.tsx +92 -92
  23. package/src/binding/DeleteButton.tsx +35 -35
  24. package/src/binding/DeleteIconButton.tsx +35 -35
  25. package/src/binding/RefreshButton.tsx +31 -31
  26. package/src/binding/RefreshIconButton.tsx +33 -33
  27. package/src/binding/RefreshRowIconButton.tsx +33 -33
  28. package/src/binding/SaveButton.tsx +37 -37
  29. package/src/binding/SaveIconButton.tsx +38 -38
  30. package/src/container/Container.tsx +49 -49
  31. package/src/container/index.ts +1 -1
  32. package/src/cutoff/CutOffPicker.css +12 -0
  33. package/src/cutoff/CutOffPicker.tsx +165 -0
  34. package/src/cutoff/context.tsx +194 -0
  35. package/src/cutoff/dsCutOffDates.ts +21 -0
  36. package/src/cutoff/index.ts +4 -0
  37. package/src/cutoff/utils.test.ts +59 -0
  38. package/src/cutoff/utils.ts +276 -0
  39. package/src/filter/ChipListInput.tsx +79 -79
  40. package/src/filter/FieldFilterPanel.css +150 -150
  41. package/src/filter/FieldFilterPanel.tsx +309 -309
  42. package/src/filter/FilterBuilder.css +152 -152
  43. package/src/filter/FilterBuilder.test.tsx +153 -153
  44. package/src/filter/FilterBuilder.tsx +435 -435
  45. package/src/filter/FilterEditor.css +231 -231
  46. package/src/filter/FilterEditor.test.tsx +259 -259
  47. package/src/filter/FilterEditor.tsx +75 -74
  48. package/src/filter/FilterGroupEditor.tsx +160 -160
  49. package/src/filter/FilterNameDialog.css +22 -22
  50. package/src/filter/FilterNameDialog.tsx +81 -81
  51. package/src/filter/FilterRow.tsx +117 -117
  52. package/src/filter/FilterShareDialog.css +28 -28
  53. package/src/filter/FilterShareDialog.tsx +201 -201
  54. package/src/filter/FilterStringField.tsx +78 -77
  55. package/src/filter/FilterValueEditor.tsx +220 -220
  56. package/src/filter/SavedFilterTree.css +107 -107
  57. package/src/filter/SavedFilterTree.test.tsx +94 -94
  58. package/src/filter/SavedFilterTree.tsx +218 -222
  59. package/src/filter/fieldFilter.test.ts +158 -158
  60. package/src/filter/fieldFilter.ts +139 -139
  61. package/src/filter/index.ts +27 -27
  62. package/src/filter/types.ts +38 -38
  63. package/src/filter/useDistinctValues.test.ts +102 -102
  64. package/src/filter/useDistinctValues.ts +230 -230
  65. package/src/global.d.ts +11 -11
  66. package/src/grid/AfGridColumnsPanel.tsx +118 -118
  67. package/src/grid/AfGridContext.tsx +51 -51
  68. package/src/grid/AfGridError.tsx +46 -46
  69. package/src/grid/AfHeaderFilterCell.tsx +147 -151
  70. package/src/grid/AfHeaderFilterPanel.tsx +111 -111
  71. package/src/grid/GridEditLookup.css +12 -0
  72. package/src/grid/GridEditLookup.tsx +121 -0
  73. package/src/grid/Toolbar.tsx +233 -233
  74. package/src/grid/editing.ts +100 -100
  75. package/src/grid/filter.ts +78 -78
  76. package/src/grid/formatters.ts +48 -48
  77. package/src/grid/index.css +245 -245
  78. package/src/grid/index.tsx +489 -484
  79. package/src/grid/license.ts +48 -48
  80. package/src/grid/localization.ts +369 -369
  81. package/src/grid/slots/index.tsx +307 -307
  82. package/src/grid/slots/slots.css +64 -64
  83. package/src/grid/theme.tsx +138 -138
  84. package/src/grid/useAfColumns.tsx +371 -371
  85. package/src/grid/useAfCurrentIndex.ts +63 -63
  86. package/src/grid/useAfData.ts +20 -20
  87. package/src/grid/useAfFilter.ts +160 -160
  88. package/src/grid/useAfFilterFields.ts +55 -55
  89. package/src/grid/useAfGridApi.ts +71 -71
  90. package/src/grid/useAfKeyBindings.ts +36 -36
  91. package/src/grid/useAfNewItemRow.ts +258 -258
  92. package/src/grid/useAfPagination.ts +53 -53
  93. package/src/grid/useAfPersistedState.ts +230 -230
  94. package/src/grid/useAfRowEditModel.ts +145 -145
  95. package/src/grid/useAfRowGrouping.ts +47 -47
  96. package/src/grid/useAfServerAggregation.ts +145 -145
  97. package/src/grid/useAfSortModel.ts +84 -84
  98. package/src/i18n.ts +13 -0
  99. package/src/index.ts +14 -12
  100. package/src/input/InputAdornments.css +75 -75
  101. package/src/input/InputAdornments.tsx +102 -102
  102. package/src/input/index.ts +1 -1
  103. package/src/layout/AppMenu.tsx +74 -75
  104. package/src/layout/AppMenuDrawer.css +70 -70
  105. package/src/layout/AppMenuDrawer.tsx +122 -122
  106. package/src/layout/ErrorBoundary.tsx +162 -162
  107. package/src/layout/ResponsiveDialog.css +86 -0
  108. package/src/layout/ResponsiveDialog.tsx +55 -0
  109. package/src/layout/index.tsx +69 -68
  110. package/src/link/Link.tsx +23 -0
  111. package/src/link/index.ts +1 -0
  112. package/src/mdi/Mdi.css +138 -138
  113. package/src/mdi/Mdi.test.tsx +91 -87
  114. package/src/mdi/Mdi.tsx +141 -141
  115. package/src/mdi/MdiConfirmCloseDialog.tsx +47 -47
  116. package/src/mdi/MdiContext.tsx +59 -59
  117. package/src/mdi/MdiDocumentContext.tsx +60 -60
  118. package/src/mdi/MdiPanel.tsx +61 -61
  119. package/src/mdi/MdiRestore.test.tsx +86 -82
  120. package/src/mdi/MdiTab.tsx +30 -30
  121. package/src/mdi/controller.test.ts +382 -382
  122. package/src/mdi/controller.ts +656 -656
  123. package/src/mdi/dockviewHost.ts +132 -132
  124. package/src/mdi/fakeHost.ts +200 -200
  125. package/src/mdi/host.ts +44 -44
  126. package/src/mdi/ids.test.ts +20 -20
  127. package/src/mdi/ids.ts +18 -18
  128. package/src/mdi/index.ts +31 -31
  129. package/src/mdi/layoutRestore.test.ts +156 -156
  130. package/src/mdi/layoutRestore.ts +311 -311
  131. package/src/mdi/layoutSpec.test.ts +143 -143
  132. package/src/mdi/layoutSpec.ts +214 -214
  133. package/src/mdi/layoutStore.test.ts +93 -93
  134. package/src/mdi/layoutStore.ts +99 -99
  135. package/src/mdi/memorySettingsStore.ts +21 -21
  136. package/src/mdi/registry.ts +34 -34
  137. package/src/mdi/testTypes.ts +37 -37
  138. package/src/mdi/theme.ts +22 -22
  139. package/src/mdi/types.ts +214 -214
  140. package/src/page/Page.css +53 -53
  141. package/src/page/PageBlock.css +64 -64
  142. package/src/page/PageBlock.tsx +92 -92
  143. package/src/page/PageHeader.css +88 -88
  144. package/src/page/PageHeader.tsx +115 -116
  145. package/src/page/PageTitle.css +5 -0
  146. package/src/page/PageTitle.tsx +21 -0
  147. package/src/page/index.ts +4 -3
  148. package/src/paper/Paper.tsx +52 -52
  149. package/src/progress/LinearProgress.css +93 -93
  150. package/src/progress/LinearProgress.tsx +104 -104
  151. package/src/progress/index.ts +1 -1
  152. package/src/report-downloader/ReportDownloader.css +67 -67
  153. package/src/report-downloader/components/StatusItem.tsx +87 -86
  154. package/src/report-downloader/components/StatusList.tsx +28 -28
  155. package/src/report-downloader/index.ts +41 -41
  156. package/src/report-downloader/status.tsx +92 -92
  157. package/src/sortable/DropIndicator.css +51 -51
  158. package/src/sortable/DropIndicator.tsx +27 -27
  159. package/src/sortable/SortableItemList.css +21 -21
  160. package/src/sortable/SortableItemList.tsx +264 -264
  161. package/src/sortable/index.ts +8 -8
  162. package/src/sortable/reorderSortOrder.ts +126 -126
  163. package/src/sortable/sortOrder.ts +101 -101
  164. package/src/sortable/types.ts +9 -9
  165. package/src/sortable/useSortableContainer.ts +53 -53
  166. package/src/sortable/useSortableItem.ts +95 -95
  167. package/src/sortable/useSortableList.ts +117 -117
  168. package/src/test/happy-dom-document-class.ts +24 -24
  169. package/src/test/setup.ts +12 -12
  170. package/src/theme/useDsColorScheme.ts +53 -53
  171. package/tsconfig.build.json +9 -9
  172. package/tsconfig.json +8 -8
  173. package/vitest.config.ts +10 -10
@@ -1,484 +1,489 @@
1
- import "./index.css";
2
-
3
- import { Alert } from "@digdir/designsystemet-react";
4
- import {
5
- DataGridPremium as DataGrid,
6
- type DataGridPremiumProps as DataGridProps,
7
- GRID_AGGREGATION_FUNCTIONS,
8
- GRID_ROOT_GROUP_ID,
9
- type GridAggregationFunction,
10
- type GridApi,
11
- type GridFilterModel,
12
- useGridApiRef,
13
- } from "@mui/x-data-grid-premium";
14
- import { getLocalizedString } from "@olenbetong/appframe-core";
15
- import { type DataObject, uid } from "@olenbetong/appframe-data";
16
- import { DataObjectProvider, useLoading } from "@olenbetong/appframe-react";
17
-
18
- import clsx from "clsx";
19
- import { useEffect, useMemo, useState } from "react";
20
- import { FilterBuilder } from "../filter/index.js";
21
- import { AfGridColumnsPanel } from "./AfGridColumnsPanel.js";
22
- import { type AfGridContextValue, AfGridContextProvider } from "./AfGridContext.js";
23
- import { AfGridError } from "./AfGridError.js";
24
- import { registerLicense } from "./license.js";
25
- import { AF_GRID_LOCALE_TEXT } from "./localization.js";
26
- import { dsBaseSlots } from "./slots/index.js";
27
- import { AfGridThemeProvider } from "./theme.js";
28
- import {
29
- type AfExcelExportOptions,
30
- AfGridExcelExportMenuItem,
31
- type AfGridExcelExportOptions,
32
- AfGridToolbar,
33
- AfGridToolbarExcelExportButton,
34
- GridToolbarAddButton,
35
- GridToolbarAddIfAllowedButton,
36
- GridToolbarColumnsButton,
37
- GridToolbarContainer,
38
- GridToolbarFilterButton,
39
- GridToolbarMoreOptionsButton,
40
- GridToolbarRefreshButton,
41
- GridToolbarResetLayoutButton,
42
- } from "./Toolbar.js";
43
- import type { AfGridColumn } from "./useAfColumns.js";
44
- import { type AfGridColDef, useAfColumns } from "./useAfColumns.js";
45
- import { useAfCurrentIndex } from "./useAfCurrentIndex.js";
46
- import { useAfData } from "./useAfData.js";
47
- import { useAfFilterFields } from "./useAfFilterFields.js";
48
- import { useAfFilter } from "./useAfFilter.js";
49
- import { type AfGridApi, useAfGridApi } from "./useAfGridApi.js";
50
- import { useAfKeyBindings } from "./useAfKeyBindings.js";
51
- import { type AfGridNewItemRowPosition, NEW_ITEM_ROW_ID, isNewItemRow, useAfNewItemRow } from "./useAfNewItemRow.js";
52
- import { useAfPagination } from "./useAfPagination.js";
53
- import {
54
- isPersistedStateLoaded,
55
- type StatePersistenceConfig,
56
- settingsLoaded,
57
- useAfPersistedState,
58
- } from "./useAfPersistedState.js";
59
- import { useAfRowEditModel } from "./useAfRowEditModel.js";
60
- import { useAfRowGrouping } from "./useAfRowGrouping.js";
61
- import { useAfServerAggregation } from "./useAfServerAggregation.js";
62
- import { useAfSortModel } from "./useAfSortModel.js";
63
-
64
- export * from "./formatters.js";
65
- export * as MuiDataGrid from "@mui/x-data-grid-premium";
66
-
67
- registerLicense();
68
-
69
- /**
70
- * The grid's criteria live in a `FilterTree` owned by `useAfFilter`, not in
71
- * MUI's filter model, so the model is always empty. It is hoisted so the
72
- * reference is stable and the grid does not treat it as a change.
73
- */
74
- const EMPTY_FILTER_MODEL: GridFilterModel = { items: [] };
75
-
76
- const privSysColumns: AfGridColDef[] = [
77
- { field: "PrimKey", editable: false, hide: true },
78
- { field: "Created", editable: false, hide: true },
79
- { field: "CreatedBy", editable: false, hide: true },
80
- { field: "Updated", editable: false, hide: true },
81
- { field: "UpdatedBy", editable: false, hide: true },
82
- ];
83
-
84
- export const systemColumns = {
85
- [Symbol.iterator]: () => privSysColumns[Symbol.iterator](),
86
- PrimKey: privSysColumns[0],
87
- Created: privSysColumns[1],
88
- CreatedBy: privSysColumns[2],
89
- Updated: privSysColumns[3],
90
- UpdatedBy: privSysColumns[4],
91
- };
92
-
93
- export type {
94
- AfExcelExportOptions,
95
- AfGridApi,
96
- AfGridColDef,
97
- AfGridColumn,
98
- AfGridExcelExportOptions,
99
- AfGridNewItemRowPosition,
100
- };
101
- export type { AfGridGrouping, AfGridGroupingProps } from "./useAfRowGrouping.js";
102
-
103
- export {
104
- AF_GRID_LOCALE_TEXT,
105
- NEW_ITEM_ROW_ID,
106
- isNewItemRow,
107
- useAfNewItemRow,
108
- AfGridColumnsPanel,
109
- AfGridExcelExportMenuItem,
110
- AfGridThemeProvider,
111
- AfGridToolbar,
112
- AfGridToolbarExcelExportButton,
113
- GridToolbarAddButton,
114
- GridToolbarAddIfAllowedButton,
115
- GridToolbarColumnsButton,
116
- GridToolbarContainer,
117
- GridToolbarFilterButton,
118
- GridToolbarMoreOptionsButton,
119
- GridToolbarRefreshButton,
120
- GridToolbarResetLayoutButton,
121
- useAfColumns,
122
- useAfCurrentIndex,
123
- useAfData,
124
- useAfFilter,
125
- useAfFilterFields,
126
- useAfPagination,
127
- useAfPersistedState,
128
- useAfRowEditModel,
129
- useAfRowGrouping,
130
- useAfServerAggregation,
131
- useAfSortModel,
132
- };
133
-
134
- export type AfGridProps = {
135
- apiRef?: React.RefObject<GridApi>;
136
- /**
137
- * If set to true, the grid will automatically load data if the data object has not
138
- * been loaded previously.
139
- */
140
- autoLoad?: boolean;
141
- /**
142
- * Bind the focused/selected row to the current index in the data object.
143
- * Useful if you want child data to load as the user navigates the grid.
144
- */
145
- bindIndex?: boolean;
146
- columns: AfGridColumn[];
147
- dataObject: DataObject<any>;
148
- /**
149
- * The layout version of the grid. Persisted grid state will only be restored
150
- * if the version matches.
151
- */
152
- layoutVersion?: string | number;
153
- /**
154
- * Field that contains a unique identifier for each row. Defaults to PrimKey.
155
- */
156
- idField?: string;
157
- /**
158
- * A function that can be used to transform the data object data before it is
159
- * passed to the grid, for example to create summary rows.
160
- */
161
- getData?: (data: Readonly<Readonly<any>[]>) => any[];
162
- /**
163
- * Override the data object permission, and remove the delete button from the default row actions
164
- */
165
- disableDelete?: boolean;
166
- /**
167
- * Override the data object permission, and remove the edit button from the default row actions
168
- */
169
- disableEdit?: boolean;
170
- /**
171
- * Override the data object permission, and remove the Add button from the default toolbar.
172
- */
173
- disableInsert?: boolean;
174
- /**
175
- * Use to disable server sorting. Required for example when using row grouping.
176
- */
177
- disableServerSorting?: boolean;
178
- /**
179
- * The view the filter builder saves and loads saved filters for, and converts
180
- * its filter against. Without it the filter builder can still be used, but
181
- * saved filters are unavailable.
182
- */
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;
193
- /**
194
- * A unique ID used to persist the grid state. It is better to use the layoutVersion
195
- * property if you would like to ignore any previously persisted state, as changing
196
- * this property will leave orphan records for the previous id in the Indexed DB
197
- * grid layout store.
198
- */
199
- id: string;
200
- /**
201
- * Object almost matching the grid state, but with all values as booleans. This
202
- * is used to configure which properties should be persisted.
203
- */
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;
222
- } & Omit<
223
- DataGridProps,
224
- | "apiRef"
225
- | "columns"
226
- | "loading"
227
- | "rows"
228
- | "filterMode"
229
- | "filterModel"
230
- | "onFilterModelChange"
231
- | "sortingMode"
232
- | "sortModel"
233
- | "onSortModelChange"
234
- | "rowGroupingModel"
235
- | "onRowGroupingModelChange"
236
- | "aggregationModel"
237
- | "onAggregationModelChange"
238
- | "editRowsModel"
239
- | "editMode"
240
- | "onRowEditCommit"
241
- | "onEditRowsModelChange"
242
- | "pagination"
243
- | "paginationMode"
244
- | "pageSize"
245
- | "rowCount"
246
- | "rowsPerPageOptions"
247
- | "onPageChange"
248
- | "onPageSizeChange"
249
- >;
250
-
251
- // This wrapper component prevents the actual grid from rendering until the persisted state has been loaded.
252
- // This is to prevent the grid from flickering when the persisted state is restored, plus the grid sometimes
253
- // resets the column order if not provided in the initial state
254
- export function AfGrid(props: AfGridProps) {
255
- let [loaded, setLoaded] = useState(isPersistedStateLoaded());
256
-
257
- useEffect(() => {
258
- settingsLoaded.then(() => setLoaded(true));
259
- }, []);
260
-
261
- return (
262
- <AfGridThemeProvider>
263
- {loaded ? <AfGrid_ {...props} /> : <div className="AfGrid-loading">{getLocalizedString("Loading...")}</div>}
264
- </AfGridThemeProvider>
265
- );
266
- }
267
-
268
- function AfGrid_(props: AfGridProps) {
269
- let {
270
- autoLoad = false,
271
- apiRef: apiRefProp,
272
- bindIndex,
273
- columns: columnsProp,
274
- disableServerSorting,
275
- disableInsert,
276
- disableDelete,
277
- disableEdit = false,
278
- dataObject,
279
- filterViewName,
280
- layoutVersion = 0,
281
- getData,
282
- idField = "PrimKey",
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",
287
- persistenceModel,
288
- initialState,
289
- pinnedRows,
290
- getRowClassName,
291
- serverAggregation = dataObject.options?.dynamicLoading === true,
292
- ...other
293
- } = props;
294
- let apiRefPriv = useGridApiRef();
295
- let apiRef = apiRefProp ?? apiRefPriv;
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_);
315
- let loading = useLoading(dataObject);
316
- let paging = useAfPagination(dataObject);
317
- let sortModel = useAfSortModel({
318
- dataObject,
319
- columns: columnsProp,
320
- disableServerSorting,
321
- isGrouping: grouping.isGrouping,
322
- });
323
- let gridFilter = useAfFilter({ autoLoad, dataObject, id });
324
- let [snackbar, setSnackbar] = useState<string | null>(null);
325
- let [isFilterBuilderOpen, setFilterBuilderOpen] = useState(false);
326
- let [isColumnsPanelOpen, setColumnsPanelOpen] = useState(false);
327
- let editProps = useAfRowEditModel({ dataObject, disableEdit, idField, apiRef, newItemRow });
328
- let data = useAfData(dataObject, { getData });
329
- useAfCurrentIndex({
330
- apiRef,
331
- dataObject,
332
- idField,
333
- enabled: !!bindIndex,
334
- newItemRowId: newItemRow.enabled ? NEW_ITEM_ROW_ID : null,
335
- });
336
- let columns = useAfColumns({
337
- apiRef,
338
- dataObject,
339
- disableEdit,
340
- disableDelete,
341
- columnsProp,
342
- idField,
343
- });
344
- let filterFields = useAfFilterFields(dataObject, columnsProp);
345
-
346
- let keyEvents = useAfKeyBindings({ apiRef, dataObject });
347
-
348
- useEffect(() => {
349
- if (!snackbar) return;
350
-
351
- let timeout = setTimeout(() => setSnackbar(null), 6000);
352
-
353
- return () => clearTimeout(timeout);
354
- }, [snackbar]);
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
-
389
- let gridContext = useMemo<AfGridContextValue>(
390
- () => ({
391
- id,
392
- apiRef,
393
- filterFields,
394
- ...gridFilter,
395
- openFilterBuilder: () => setFilterBuilderOpen(true),
396
- openColumnsPanel: () => setColumnsPanelOpen(true),
397
- }),
398
- [id, apiRef, filterFields, gridFilter],
399
- );
400
-
401
- return (
402
- <DataObjectProvider dataObject={dataObject}>
403
- <AfGridContextProvider value={gridContext}>
404
- <div className="AfGrid-root" style={newItemRow.rootStyle} {...keyEvents} tabIndex={-1}>
405
- <DataGrid
406
- apiRef={apiRef}
407
- rows={data}
408
- columns={columns}
409
- loading={loading}
410
- getRowId={(row) => row[uid] ?? row[idField]}
411
- {...editProps}
412
- {...newItemRow.gridProps}
413
- /*
414
- * Our own filter tree drives the data object, so MUI never
415
- * sees a filter model. The header filter cells are replaced
416
- * by `renderHeaderFilter` on every column.
417
- */
418
- filterMode="server"
419
- filterModel={EMPTY_FILTER_MODEL}
420
- {...sortModel}
421
- headerFilters
422
- {...paging}
423
- showToolbar
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 } : {})}
433
- localeText={{ ...other.localeText, ...AF_GRID_LOCALE_TEXT }}
434
- slots={{
435
- ...dsBaseSlots,
436
- toolbar: () => <AfGridToolbar disableInsert={disableInsert} />,
437
- ...other.slots,
438
- }}
439
- initialState={initialState_}
440
- slotProps={{
441
- ...other.slotProps,
442
- headerFilterCell: {
443
- ...other.slotProps?.headerFilterCell,
444
- headerClassName: clsx(other.slotProps?.headerFilterCell?.headerClassName, "AfGrid-headerCell"),
445
- showRightBorder: true,
446
- },
447
- }}
448
- onProcessRowUpdateError={(error) => {
449
- other.onProcessRowUpdateError?.(error);
450
- let message = "message" in (error as any) ? (error as any).message : String(error);
451
- setSnackbar(message);
452
- }}
453
- />
454
- </div>
455
- <AfGridError apiRef={apiRef} dataObject={dataObject} />
456
- {isColumnsPanelOpen && <AfGridColumnsPanel open onClose={() => setColumnsPanelOpen(false)} />}
457
- <FilterBuilder
458
- variant="dialog"
459
- open={isFilterBuilderOpen}
460
- onClose={() => setFilterBuilderOpen(false)}
461
- fields={filterFields}
462
- viewName={filterViewName}
463
- dataObject={dataObject}
464
- /*
465
- * The builder edits the same named slot and the same tree as the
466
- * header filters, so a criterion added in one shows up in the
467
- * other. Applying writes the slot directly, which skips the
468
- * header filters' debounce.
469
- */
470
- slotName={id}
471
- value={gridFilter.filter}
472
- onChange={gridFilter.setFilter}
473
- />
474
- {!!snackbar && (
475
- <div className="AfGrid-snackbar">
476
- <Alert data-color="danger" data-size="sm" onClick={() => setSnackbar(null)}>
477
- {snackbar}
478
- </Alert>
479
- </div>
480
- )}
481
- </AfGridContextProvider>
482
- </DataObjectProvider>
483
- );
484
- }
1
+ import "./index.css";
2
+
3
+ import { Alert } from "@digdir/designsystemet-react";
4
+ import {
5
+ DataGridPremium as DataGrid,
6
+ type DataGridPremiumProps as DataGridProps,
7
+ GRID_AGGREGATION_FUNCTIONS,
8
+ GRID_ROOT_GROUP_ID,
9
+ type GridAggregationFunction,
10
+ type GridApi,
11
+ type GridFilterModel,
12
+ useGridApiRef,
13
+ } from "@mui/x-data-grid-premium";
14
+ import { type DataObject, uid } from "@olenbetong/appframe-data";
15
+ import { DataObjectProvider, useLoading } from "@olenbetong/appframe-react";
16
+
17
+ import clsx from "clsx";
18
+ import { useEffect, useMemo, useState } from "react";
19
+ import { FilterBuilder } from "../filter/index.js";
20
+ import { AfGridColumnsPanel } from "./AfGridColumnsPanel.js";
21
+ import { type AfGridContextValue, AfGridContextProvider } from "./AfGridContext.js";
22
+ import { AfGridError } from "./AfGridError.js";
23
+ import { registerLicense } from "./license.js";
24
+ import { AF_GRID_LOCALE_TEXT } from "./localization.js";
25
+ import { dsBaseSlots } from "./slots/index.js";
26
+ import { AfGridThemeProvider } from "./theme.js";
27
+ import {
28
+ type AfExcelExportOptions,
29
+ AfGridExcelExportMenuItem,
30
+ type AfGridExcelExportOptions,
31
+ AfGridToolbar,
32
+ AfGridToolbarExcelExportButton,
33
+ GridToolbarAddButton,
34
+ GridToolbarAddIfAllowedButton,
35
+ GridToolbarColumnsButton,
36
+ GridToolbarContainer,
37
+ GridToolbarFilterButton,
38
+ GridToolbarMoreOptionsButton,
39
+ GridToolbarRefreshButton,
40
+ GridToolbarResetLayoutButton,
41
+ } from "./Toolbar.js";
42
+ import type { AfGridColumn } from "./useAfColumns.js";
43
+ import { type AfGridColDef, useAfColumns } from "./useAfColumns.js";
44
+ import { useAfCurrentIndex } from "./useAfCurrentIndex.js";
45
+ import { useAfData } from "./useAfData.js";
46
+ import { useAfFilterFields } from "./useAfFilterFields.js";
47
+ import { useAfFilter } from "./useAfFilter.js";
48
+ import { type AfGridApi, useAfGridApi } from "./useAfGridApi.js";
49
+ import { useAfKeyBindings } from "./useAfKeyBindings.js";
50
+ import { type AfGridNewItemRowPosition, NEW_ITEM_ROW_ID, isNewItemRow, useAfNewItemRow } from "./useAfNewItemRow.js";
51
+ import { useAfPagination } from "./useAfPagination.js";
52
+ import {
53
+ isPersistedStateLoaded,
54
+ type StatePersistenceConfig,
55
+ settingsLoaded,
56
+ useAfPersistedState,
57
+ } from "./useAfPersistedState.js";
58
+ import { useAfRowEditModel } from "./useAfRowEditModel.js";
59
+ import { useAfRowGrouping } from "./useAfRowGrouping.js";
60
+ import { useAfServerAggregation } from "./useAfServerAggregation.js";
61
+ import { useAfSortModel } from "./useAfSortModel.js";
62
+ import { getLocalizedString } from "../i18n.js";
63
+
64
+ export * from "./formatters.js";
65
+ export * from "./GridEditLookup.js";
66
+ // MUI X as a namespace (`DataGrid.DataGridPremium`, `DataGrid.GridColDef`, `DataGrid.useGridApiRef`,
67
+ // ...), for grids that are not bound to a data object - from here rather than the app, so a
68
+ // platform app does not bundle a second MUI X. Same shape as appframe-mui exported it.
69
+ export * as DataGrid from "@mui/x-data-grid-premium";
70
+ export * as MuiDataGrid from "@mui/x-data-grid-premium";
71
+
72
+ registerLicense();
73
+
74
+ /**
75
+ * The grid's criteria live in a `FilterTree` owned by `useAfFilter`, not in
76
+ * MUI's filter model, so the model is always empty. It is hoisted so the
77
+ * reference is stable and the grid does not treat it as a change.
78
+ */
79
+ const EMPTY_FILTER_MODEL: GridFilterModel = { items: [] };
80
+
81
+ const privSysColumns: AfGridColDef[] = [
82
+ { field: "PrimKey", editable: false, hide: true },
83
+ { field: "Created", editable: false, hide: true },
84
+ { field: "CreatedBy", editable: false, hide: true },
85
+ { field: "Updated", editable: false, hide: true },
86
+ { field: "UpdatedBy", editable: false, hide: true },
87
+ ];
88
+
89
+ export const systemColumns = {
90
+ [Symbol.iterator]: () => privSysColumns[Symbol.iterator](),
91
+ PrimKey: privSysColumns[0],
92
+ Created: privSysColumns[1],
93
+ CreatedBy: privSysColumns[2],
94
+ Updated: privSysColumns[3],
95
+ UpdatedBy: privSysColumns[4],
96
+ };
97
+
98
+ export type {
99
+ AfExcelExportOptions,
100
+ AfGridApi,
101
+ AfGridColDef,
102
+ AfGridColumn,
103
+ AfGridExcelExportOptions,
104
+ AfGridNewItemRowPosition,
105
+ };
106
+ export type { AfGridGrouping, AfGridGroupingProps } from "./useAfRowGrouping.js";
107
+
108
+ export {
109
+ AF_GRID_LOCALE_TEXT,
110
+ NEW_ITEM_ROW_ID,
111
+ isNewItemRow,
112
+ useAfNewItemRow,
113
+ AfGridColumnsPanel,
114
+ AfGridExcelExportMenuItem,
115
+ AfGridThemeProvider,
116
+ AfGridToolbar,
117
+ AfGridToolbarExcelExportButton,
118
+ GridToolbarAddButton,
119
+ GridToolbarAddIfAllowedButton,
120
+ GridToolbarColumnsButton,
121
+ GridToolbarContainer,
122
+ GridToolbarFilterButton,
123
+ GridToolbarMoreOptionsButton,
124
+ GridToolbarRefreshButton,
125
+ GridToolbarResetLayoutButton,
126
+ useAfColumns,
127
+ useAfCurrentIndex,
128
+ useAfData,
129
+ useAfFilter,
130
+ useAfFilterFields,
131
+ useAfPagination,
132
+ useAfPersistedState,
133
+ useAfRowEditModel,
134
+ useAfRowGrouping,
135
+ useAfServerAggregation,
136
+ useAfSortModel,
137
+ };
138
+
139
+ export type AfGridProps = {
140
+ apiRef?: React.RefObject<GridApi>;
141
+ /**
142
+ * If set to true, the grid will automatically load data if the data object has not
143
+ * been loaded previously.
144
+ */
145
+ autoLoad?: boolean;
146
+ /**
147
+ * Bind the focused/selected row to the current index in the data object.
148
+ * Useful if you want child data to load as the user navigates the grid.
149
+ */
150
+ bindIndex?: boolean;
151
+ columns: AfGridColumn[];
152
+ dataObject: DataObject<any>;
153
+ /**
154
+ * The layout version of the grid. Persisted grid state will only be restored
155
+ * if the version matches.
156
+ */
157
+ layoutVersion?: string | number;
158
+ /**
159
+ * Field that contains a unique identifier for each row. Defaults to PrimKey.
160
+ */
161
+ idField?: string;
162
+ /**
163
+ * A function that can be used to transform the data object data before it is
164
+ * passed to the grid, for example to create summary rows.
165
+ */
166
+ getData?: (data: Readonly<Readonly<any>[]>) => any[];
167
+ /**
168
+ * Override the data object permission, and remove the delete button from the default row actions
169
+ */
170
+ disableDelete?: boolean;
171
+ /**
172
+ * Override the data object permission, and remove the edit button from the default row actions
173
+ */
174
+ disableEdit?: boolean;
175
+ /**
176
+ * Override the data object permission, and remove the Add button from the default toolbar.
177
+ */
178
+ disableInsert?: boolean;
179
+ /**
180
+ * Use to disable server sorting. Required for example when using row grouping.
181
+ */
182
+ disableServerSorting?: boolean;
183
+ /**
184
+ * The view the filter builder saves and loads saved filters for, and converts
185
+ * its filter against. Without it the filter builder can still be used, but
186
+ * saved filters are unavailable.
187
+ */
188
+ filterViewName?: string;
189
+ /**
190
+ * Where to show the new item row: a non-data row pinned above or below the data
191
+ * that reads "Click here to add a new row" until the user starts editing it.
192
+ * Editing it moves the data object to the new record at index -1, and committing
193
+ * it saves the record. Only shown when the data object allows insert and
194
+ * `disableInsert` is not set. `none` turns it off, in which case the Add button
195
+ * appends an unsaved row to the data instead. Defaults to `bottom`.
196
+ */
197
+ newItemRow?: AfGridNewItemRowPosition;
198
+ /**
199
+ * A unique ID used to persist the grid state. It is better to use the layoutVersion
200
+ * property if you would like to ignore any previously persisted state, as changing
201
+ * this property will leave orphan records for the previous id in the Indexed DB
202
+ * grid layout store.
203
+ */
204
+ id: string;
205
+ /**
206
+ * Object almost matching the grid state, but with all values as booleans. This
207
+ * is used to configure which properties should be persisted.
208
+ */
209
+ persistenceModel?: StatePersistenceConfig;
210
+ /**
211
+ * Ask the server for the grand total instead of adding up the rows the grid is holding.
212
+ *
213
+ * MUI aggregates over the loaded rows, which on a dynamically loading data object is the
214
+ * current page — a sum that looks like a total and is not one. With this on, the grid runs a
215
+ * second retrieve under the same filters that returns only the aggregates, so the footer shows
216
+ * the total over the whole result. Subtotals on grouped rows stay client side, since a group
217
+ * only exists over the rows the grid has.
218
+ *
219
+ * `sum`, `avg`, `min` and `max` are resolved server side; `size` and any custom aggregation
220
+ * function stay on the client. If the view cannot be aggregated the grid falls back to the
221
+ * client total rather than failing.
222
+ *
223
+ * Defaults to true when the data object loads dynamically, since that is exactly the case
224
+ * where the client total is wrong.
225
+ */
226
+ serverAggregation?: boolean;
227
+ } & Omit<
228
+ DataGridProps,
229
+ | "apiRef"
230
+ | "columns"
231
+ | "loading"
232
+ | "rows"
233
+ | "filterMode"
234
+ | "filterModel"
235
+ | "onFilterModelChange"
236
+ | "sortingMode"
237
+ | "sortModel"
238
+ | "onSortModelChange"
239
+ | "rowGroupingModel"
240
+ | "onRowGroupingModelChange"
241
+ | "aggregationModel"
242
+ | "onAggregationModelChange"
243
+ | "editRowsModel"
244
+ | "editMode"
245
+ | "onRowEditCommit"
246
+ | "onEditRowsModelChange"
247
+ | "pagination"
248
+ | "paginationMode"
249
+ | "pageSize"
250
+ | "rowCount"
251
+ | "rowsPerPageOptions"
252
+ | "onPageChange"
253
+ | "onPageSizeChange"
254
+ >;
255
+
256
+ // This wrapper component prevents the actual grid from rendering until the persisted state has been loaded.
257
+ // This is to prevent the grid from flickering when the persisted state is restored, plus the grid sometimes
258
+ // resets the column order if not provided in the initial state
259
+ export function AfGrid(props: AfGridProps) {
260
+ let [loaded, setLoaded] = useState(isPersistedStateLoaded());
261
+
262
+ useEffect(() => {
263
+ settingsLoaded.then(() => setLoaded(true));
264
+ }, []);
265
+
266
+ return (
267
+ <AfGridThemeProvider>
268
+ {loaded ? <AfGrid_ {...props} /> : <div className="AfGrid-loading">{getLocalizedString("Loading...")}</div>}
269
+ </AfGridThemeProvider>
270
+ );
271
+ }
272
+
273
+ function AfGrid_(props: AfGridProps) {
274
+ let {
275
+ autoLoad = false,
276
+ apiRef: apiRefProp,
277
+ bindIndex,
278
+ columns: columnsProp,
279
+ disableServerSorting,
280
+ disableInsert,
281
+ disableDelete,
282
+ disableEdit = false,
283
+ dataObject,
284
+ filterViewName,
285
+ layoutVersion = 0,
286
+ getData,
287
+ idField = "PrimKey",
288
+ id,
289
+ // Follows the data object's `newRecordPosition` when it has one (older data
290
+ // packages have none and append), so the row sits where the record will land
291
+ newItemRow: newItemRowPosition = dataObject.options?.newRecordPosition === "start" ? "top" : "bottom",
292
+ persistenceModel,
293
+ initialState,
294
+ pinnedRows,
295
+ getRowClassName,
296
+ serverAggregation = dataObject.options?.dynamicLoading === true,
297
+ ...other
298
+ } = props;
299
+ let apiRefPriv = useGridApiRef();
300
+ let apiRef = apiRefProp ?? apiRefPriv;
301
+ useAfGridApi(props, apiRef);
302
+ let newItemRow = useAfNewItemRow({
303
+ apiRef,
304
+ dataObject,
305
+ position: newItemRowPosition,
306
+ disableInsert,
307
+ pinnedRows,
308
+ getRowClassName,
309
+ });
310
+ let initialState_ = useAfPersistedState({
311
+ apiRef,
312
+ columns: columnsProp,
313
+ id,
314
+ layoutVersion,
315
+ persistenceModel,
316
+ initialState,
317
+ });
318
+ // Seeded from the restored layout, so a persisted grouping survives a reload.
319
+ let grouping = useAfRowGrouping(initialState_);
320
+ let loading = useLoading(dataObject);
321
+ let paging = useAfPagination(dataObject);
322
+ let sortModel = useAfSortModel({
323
+ dataObject,
324
+ columns: columnsProp,
325
+ disableServerSorting,
326
+ isGrouping: grouping.isGrouping,
327
+ });
328
+ let gridFilter = useAfFilter({ autoLoad, dataObject, id });
329
+ let [snackbar, setSnackbar] = useState<string | null>(null);
330
+ let [isFilterBuilderOpen, setFilterBuilderOpen] = useState(false);
331
+ let [isColumnsPanelOpen, setColumnsPanelOpen] = useState(false);
332
+ let editProps = useAfRowEditModel({ dataObject, disableEdit, idField, apiRef, newItemRow });
333
+ let data = useAfData(dataObject, { getData });
334
+ useAfCurrentIndex({
335
+ apiRef,
336
+ dataObject,
337
+ idField,
338
+ enabled: !!bindIndex,
339
+ newItemRowId: newItemRow.enabled ? NEW_ITEM_ROW_ID : null,
340
+ });
341
+ let columns = useAfColumns({
342
+ apiRef,
343
+ dataObject,
344
+ disableEdit,
345
+ disableDelete,
346
+ columnsProp,
347
+ idField,
348
+ });
349
+ let filterFields = useAfFilterFields(dataObject, columnsProp);
350
+
351
+ let keyEvents = useAfKeyBindings({ apiRef, dataObject });
352
+
353
+ useEffect(() => {
354
+ if (!snackbar) return;
355
+
356
+ let timeout = setTimeout(() => setSnackbar(null), 6000);
357
+
358
+ return () => clearTimeout(timeout);
359
+ }, [snackbar]);
360
+
361
+ let serverAggregates = useAfServerAggregation(
362
+ dataObject,
363
+ grouping.gridProps.aggregationModel,
364
+ serverAggregation && !grouping.isGrouping,
365
+ );
366
+
367
+ /*
368
+ * The grand total row asks for `groupId === GRID_ROOT_GROUP_ID`; every other call is a group
369
+ * subtotal, which only means anything over the rows the grid actually has. So the server value
370
+ * is substituted for the root and the built-in function is left to do the rest.
371
+ */
372
+ let aggregationFunctions = useMemo(() => {
373
+ if (!serverAggregation) return undefined;
374
+
375
+ let base = other.aggregationFunctions ?? GRID_AGGREGATION_FUNCTIONS;
376
+ let wrapped: Record<string, GridAggregationFunction> = {};
377
+
378
+ for (let [name, fn] of Object.entries(base) as Array<[string, GridAggregationFunction]>) {
379
+ wrapped[name] = {
380
+ ...fn,
381
+ apply: (params, api) => {
382
+ if (params.groupId === GRID_ROOT_GROUP_ID && params.field in serverAggregates.values) {
383
+ return serverAggregates.values[params.field];
384
+ }
385
+
386
+ return fn.apply(params, api);
387
+ },
388
+ };
389
+ }
390
+
391
+ return wrapped;
392
+ }, [serverAggregation, other.aggregationFunctions, serverAggregates.values]);
393
+
394
+ let gridContext = useMemo<AfGridContextValue>(
395
+ () => ({
396
+ id,
397
+ apiRef,
398
+ filterFields,
399
+ ...gridFilter,
400
+ openFilterBuilder: () => setFilterBuilderOpen(true),
401
+ openColumnsPanel: () => setColumnsPanelOpen(true),
402
+ }),
403
+ [id, apiRef, filterFields, gridFilter],
404
+ );
405
+
406
+ return (
407
+ <DataObjectProvider dataObject={dataObject}>
408
+ <AfGridContextProvider value={gridContext}>
409
+ <div className="AfGrid-root" style={newItemRow.rootStyle} {...keyEvents} tabIndex={-1}>
410
+ <DataGrid
411
+ apiRef={apiRef}
412
+ rows={data}
413
+ columns={columns}
414
+ loading={loading}
415
+ getRowId={(row) => row[uid] ?? row[idField]}
416
+ {...editProps}
417
+ {...newItemRow.gridProps}
418
+ /*
419
+ * Our own filter tree drives the data object, so MUI never
420
+ * sees a filter model. The header filter cells are replaced
421
+ * by `renderHeaderFilter` on every column.
422
+ */
423
+ filterMode="server"
424
+ filterModel={EMPTY_FILTER_MODEL}
425
+ {...sortModel}
426
+ headerFilters
427
+ {...paging}
428
+ showToolbar
429
+ {...other}
430
+ {...grouping.gridProps}
431
+ /*
432
+ * Spread rather than passed, because the grid merges props over its defaults with
433
+ * `Object.assign`: an explicit `undefined` here would wipe out the built-in
434
+ * aggregation functions rather than fall back to them, and the grid then throws on
435
+ * the first render.
436
+ */
437
+ {...(aggregationFunctions ? { aggregationFunctions } : {})}
438
+ localeText={{ ...other.localeText, ...AF_GRID_LOCALE_TEXT }}
439
+ slots={{
440
+ ...dsBaseSlots,
441
+ toolbar: () => <AfGridToolbar disableInsert={disableInsert} />,
442
+ ...other.slots,
443
+ }}
444
+ initialState={initialState_}
445
+ slotProps={{
446
+ ...other.slotProps,
447
+ headerFilterCell: {
448
+ ...other.slotProps?.headerFilterCell,
449
+ headerClassName: clsx(other.slotProps?.headerFilterCell?.headerClassName, "AfGrid-headerCell"),
450
+ showRightBorder: true,
451
+ },
452
+ }}
453
+ onProcessRowUpdateError={(error) => {
454
+ other.onProcessRowUpdateError?.(error);
455
+ let message = "message" in (error as any) ? (error as any).message : String(error);
456
+ setSnackbar(message);
457
+ }}
458
+ />
459
+ </div>
460
+ <AfGridError apiRef={apiRef} dataObject={dataObject} />
461
+ {isColumnsPanelOpen && <AfGridColumnsPanel open onClose={() => setColumnsPanelOpen(false)} />}
462
+ <FilterBuilder
463
+ variant="dialog"
464
+ open={isFilterBuilderOpen}
465
+ onClose={() => setFilterBuilderOpen(false)}
466
+ fields={filterFields}
467
+ viewName={filterViewName}
468
+ dataObject={dataObject}
469
+ /*
470
+ * The builder edits the same named slot and the same tree as the
471
+ * header filters, so a criterion added in one shows up in the
472
+ * other. Applying writes the slot directly, which skips the
473
+ * header filters' debounce.
474
+ */
475
+ slotName={id}
476
+ value={gridFilter.filter}
477
+ onChange={gridFilter.setFilter}
478
+ />
479
+ {!!snackbar && (
480
+ <div className="AfGrid-snackbar">
481
+ <Alert data-color="danger" data-size="sm" onClick={() => setSnackbar(null)}>
482
+ {snackbar}
483
+ </Alert>
484
+ </div>
485
+ )}
486
+ </AfGridContextProvider>
487
+ </DataObjectProvider>
488
+ );
489
+ }