@olenbetong/appframe-ds 0.8.0 → 0.9.1

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 (67) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/package.json +50 -6
  3. package/scripts/copyAssets.mjs +13 -0
  4. package/src/AfLookup/LookupCombobox.tsx +3 -1
  5. package/src/filter/ChipListInput.tsx +79 -0
  6. package/src/filter/FieldFilterPanel.css +127 -0
  7. package/src/filter/FieldFilterPanel.tsx +309 -0
  8. package/src/filter/FilterBuilder.css +152 -0
  9. package/src/filter/FilterBuilder.test.tsx +153 -0
  10. package/src/filter/FilterBuilder.tsx +435 -0
  11. package/src/filter/FilterEditor.css +231 -0
  12. package/src/filter/FilterEditor.test.tsx +259 -0
  13. package/src/filter/FilterEditor.tsx +74 -0
  14. package/src/filter/FilterGroupEditor.tsx +160 -0
  15. package/src/filter/FilterNameDialog.css +22 -0
  16. package/src/filter/FilterNameDialog.tsx +81 -0
  17. package/src/filter/FilterRow.tsx +117 -0
  18. package/src/filter/FilterShareDialog.css +28 -0
  19. package/src/filter/FilterShareDialog.tsx +201 -0
  20. package/src/filter/FilterStringField.tsx +77 -0
  21. package/src/filter/FilterValueEditor.tsx +220 -0
  22. package/src/filter/SavedFilterTree.css +107 -0
  23. package/src/filter/SavedFilterTree.test.tsx +94 -0
  24. package/src/filter/SavedFilterTree.tsx +222 -0
  25. package/src/filter/fieldFilter.test.ts +158 -0
  26. package/src/filter/fieldFilter.ts +139 -0
  27. package/src/filter/index.ts +27 -0
  28. package/src/filter/types.ts +38 -0
  29. package/src/filter/useDistinctValues.test.ts +102 -0
  30. package/src/filter/useDistinctValues.ts +230 -0
  31. package/src/global.d.ts +11 -0
  32. package/src/grid/AfGridColumnsPanel.tsx +118 -0
  33. package/src/grid/AfGridContext.tsx +51 -0
  34. package/src/grid/AfGridError.tsx +46 -0
  35. package/src/grid/AfHeaderFilterCell.tsx +151 -0
  36. package/src/grid/AfHeaderFilterPanel.tsx +111 -0
  37. package/src/grid/Toolbar.tsx +233 -0
  38. package/src/grid/editing.ts +90 -0
  39. package/src/grid/filter.ts +78 -0
  40. package/src/grid/formatters.ts +48 -0
  41. package/src/grid/index.css +187 -0
  42. package/src/grid/index.tsx +376 -0
  43. package/src/grid/license.ts +48 -0
  44. package/src/grid/localization.ts +369 -0
  45. package/src/grid/slots/index.tsx +275 -0
  46. package/src/grid/slots/slots.css +53 -0
  47. package/src/grid/theme.tsx +185 -0
  48. package/src/grid/useAfColumns.tsx +313 -0
  49. package/src/grid/useAfCurrentIndex.ts +55 -0
  50. package/src/grid/useAfData.ts +20 -0
  51. package/src/grid/useAfFilter.ts +160 -0
  52. package/src/grid/useAfFilterFields.ts +55 -0
  53. package/src/grid/useAfGridApi.ts +66 -0
  54. package/src/grid/useAfKeyBindings.ts +36 -0
  55. package/src/grid/useAfPagination.ts +53 -0
  56. package/src/grid/useAfPersistedState.ts +222 -0
  57. package/src/grid/useAfRowEditModel.ts +137 -0
  58. package/src/grid/useAfSortModel.ts +72 -0
  59. package/src/index.ts +1 -0
  60. package/src/input/InputAdornments.tsx +6 -1
  61. package/src/report-downloader/ReportDownloader.css +67 -0
  62. package/src/report-downloader/components/StatusItem.tsx +86 -0
  63. package/src/report-downloader/components/StatusList.tsx +28 -0
  64. package/src/report-downloader/index.ts +41 -0
  65. package/src/report-downloader/status.tsx +92 -0
  66. package/src/test/setup.ts +8 -0
  67. package/vitest.config.ts +10 -0
@@ -0,0 +1,78 @@
1
+ import {
2
+ GridCellParams,
3
+ type GridColDef,
4
+ GridFilterInputMultipleValue,
5
+ GridFilterInputValue,
6
+ type GridFilterItem,
7
+ type GridFilterOperator,
8
+ getGridBooleanOperators,
9
+ getGridDateOperators,
10
+ getGridNumericOperators,
11
+ getGridSingleSelectOperators,
12
+ getGridStringOperators,
13
+ } from "@mui/x-data-grid-pro";
14
+ import { getLocalizedString } from "@olenbetong/appframe-core";
15
+
16
+ const stringOperators = getGridStringOperators();
17
+ const numericOperators = getGridNumericOperators();
18
+ const booleanOperators = getGridBooleanOperators();
19
+ const dateOperators = getGridDateOperators();
20
+ const singleSelectOperators = getGridSingleSelectOperators();
21
+
22
+ export const operators = {
23
+ string: stringOperators,
24
+ number: numericOperators,
25
+ boolean: booleanOperators,
26
+ date: dateOperators,
27
+ dateTime: dateOperators,
28
+ singleSelect: singleSelectOperators,
29
+ };
30
+
31
+ const likeOperator: GridFilterOperator = {
32
+ label: getLocalizedString("like"),
33
+ headerLabel: getLocalizedString("Like"),
34
+ value: "like",
35
+ InputComponent: GridFilterInputValue,
36
+ getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => {
37
+ if (!filterItem.field || !filterItem.value || !filterItem.operator) {
38
+ return null;
39
+ }
40
+
41
+ // only supported by server side filtering
42
+ return (): boolean => true;
43
+ },
44
+ };
45
+
46
+ const notLikeOperator: GridFilterOperator = {
47
+ label: getLocalizedString("not like"),
48
+ headerLabel: getLocalizedString("Not like"),
49
+ value: "notLike",
50
+ InputComponent: GridFilterInputValue,
51
+ getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => {
52
+ if (!filterItem.field || !filterItem.value || !filterItem.operator) {
53
+ return null;
54
+ }
55
+
56
+ // only supported by server side filtering
57
+ return (): boolean => true;
58
+ },
59
+ };
60
+
61
+ const notInListOperator: GridFilterOperator = {
62
+ label: getLocalizedString("is none of"),
63
+ headerLabel: getLocalizedString("Is none of"),
64
+ value: "isNoneOf",
65
+ InputComponent: GridFilterInputMultipleValue,
66
+ getApplyFilterFn: (filterItem: GridFilterItem, column: GridColDef) => {
67
+ if (!filterItem.field || !filterItem.value || !filterItem.operator) {
68
+ return null;
69
+ }
70
+
71
+ // only supported by server side filtering
72
+ return (): boolean => true;
73
+ },
74
+ };
75
+
76
+ stringOperators.push(notInListOperator);
77
+ stringOperators.push(likeOperator);
78
+ stringOperators.push(notLikeOperator);
@@ -0,0 +1,48 @@
1
+ const culture = globalThis.af?.userSession?.culture ?? "en-GB";
2
+ export const dateFormatter = new Intl.DateTimeFormat(culture, {
3
+ year: "numeric",
4
+ month: "2-digit",
5
+ day: "2-digit",
6
+ timeZone: "utc",
7
+ });
8
+
9
+ export const dateTimeFormatter = new Intl.DateTimeFormat(culture, {
10
+ year: "numeric",
11
+ month: "2-digit",
12
+ day: "2-digit",
13
+ hour: "2-digit",
14
+ minute: "2-digit",
15
+ second: "2-digit",
16
+ timeZone: "utc",
17
+ });
18
+
19
+ export const intFormatter = new Intl.NumberFormat(culture, {
20
+ maximumFractionDigits: 0,
21
+ });
22
+
23
+ export const decFormatter = new Intl.NumberFormat(culture, {
24
+ maximumFractionDigits: 2,
25
+ minimumFractionDigits: 2,
26
+ });
27
+
28
+ export const intValueFormatter = (value: any) =>
29
+ value && typeof value === "number" ? intFormatter.format(value) : value;
30
+
31
+ export const decimalValueFormatter = (value: any) =>
32
+ value && typeof value === "number" ? decFormatter.format(value) : value;
33
+
34
+ export function dateValueFormatter(value: any) {
35
+ if (value && value instanceof Date) {
36
+ return dateFormatter.format(value);
37
+ }
38
+
39
+ return value;
40
+ }
41
+
42
+ export function dateTimeValueFormatter(value: any) {
43
+ if (value && value instanceof Date) {
44
+ return dateTimeFormatter.format(value);
45
+ }
46
+
47
+ return value;
48
+ }
@@ -0,0 +1,187 @@
1
+ .AfGrid-root {
2
+ height: 100%;
3
+ }
4
+
5
+ .AfGrid-loading {
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ height: 100%;
10
+ }
11
+
12
+ .AfGrid-toolbar {
13
+ display: flex;
14
+ flex-wrap: wrap;
15
+ gap: var(--ds-size-1, 0.25rem);
16
+ align-items: center;
17
+ padding: var(--ds-size-1, 0.25rem);
18
+ }
19
+
20
+ .MuiDataGrid-headerFilterRow .MuiDataGrid-columnHeader.AfGrid-headerCell {
21
+ align-items: center;
22
+ padding-block: 0;
23
+ background-color: var(--ds-color-neutral-surface-tinted, rgb(0 0 0 / 2%));
24
+
25
+ &:focus-within {
26
+ background-color: var(--ds-color-neutral-surface-hover, rgb(0 0 0 / 4%));
27
+ }
28
+ }
29
+
30
+ .AfGrid-errorOverlay {
31
+ position: absolute;
32
+ inset-inline: 0;
33
+ top: var(--DataGrid-topContainerHeight);
34
+ z-index: 10;
35
+ padding: var(--ds-size-1, 0.25rem) var(--ds-size-2, 0.5rem);
36
+ pointer-events: none;
37
+ }
38
+
39
+ .AfGrid-errorAlert {
40
+ display: flex;
41
+ gap: var(--ds-size-2, 0.5rem);
42
+ align-items: center;
43
+ justify-content: space-between;
44
+ pointer-events: auto;
45
+ }
46
+
47
+ .AfGrid-snackbar {
48
+ position: fixed;
49
+ top: var(--ds-size-4, 1rem);
50
+ left: 50%;
51
+ z-index: 1400;
52
+ transform: translateX(-50%);
53
+ }
54
+
55
+ .AfGrid-columnsPanelSearch {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: var(--ds-size-3, 0.75rem);
59
+ }
60
+
61
+ .AfGrid-columnsPanelList {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--ds-size-1, 0.25rem);
65
+ max-height: 50vh;
66
+ margin: 0;
67
+ padding: 0;
68
+ overflow-y: auto;
69
+ list-style: none;
70
+ }
71
+
72
+ .AfGrid-columnsPanelItem {
73
+ padding: var(--ds-size-1, 0.25rem);
74
+ border-radius: var(--ds-border-radius-md, 4px);
75
+ cursor: grab;
76
+ }
77
+
78
+ .AfGrid-columnsPanelItem:hover {
79
+ background-color: var(--ds-color-neutral-surface-hover, rgb(0 0 0 / 4%));
80
+ }
81
+
82
+ .AfGrid-columnsPanelActions {
83
+ display: flex;
84
+ gap: var(--ds-size-2, 0.5rem);
85
+ justify-content: flex-end;
86
+ }
87
+
88
+ /* Header filters */
89
+
90
+ /*
91
+ * The header filter row is short, so the input drops the border radius and any
92
+ * vertical padding and fills the cell instead.
93
+ *
94
+ * The height has to be clamped to the cell. Designsystemet gives the input a
95
+ * min-height taller than the row MUI reserves, and a cell that grows past that
96
+ * stretches the row without MUI knowing — leaving the cells that hold no input
97
+ * (unfilterable columns such as `actions`) short of the row they sit in.
98
+ */
99
+ .AfGrid-headerFilterInput.ObInputAdornments-root {
100
+ --obInputAdornments-padding: 0;
101
+ --obInputAdornments-gap: 0;
102
+
103
+ block-size: 100%;
104
+ min-block-size: 0;
105
+ background-color: transparent;
106
+ border-color: transparent;
107
+ border-radius: 0;
108
+ }
109
+
110
+ .AfGrid-headerFilterInput .ObInputAdornments-field {
111
+ min-block-size: 0;
112
+ }
113
+
114
+ .AfGrid-headerFilterInput.ObInputAdornments-root:hover:not(:focus-within) {
115
+ border-color: transparent;
116
+ }
117
+
118
+ .AfGrid-headerFilterInput .ObInputAdornments-field {
119
+ padding-inline: var(--ds-size-1, 0.25rem);
120
+ text-overflow: ellipsis;
121
+ }
122
+
123
+ /* Read-only cells show a summary, so the text cursor would be misleading */
124
+ .AfGrid-headerFilterInput .ObInputAdornments-field:read-only {
125
+ cursor: pointer;
126
+ color: var(--ds-color-neutral-text-subtle, #68707c);
127
+ }
128
+
129
+ /*
130
+ * The header filter row is denser than a normal form row, so the buttons and
131
+ * their icons are stepped down from the default `sm` size.
132
+ */
133
+ .AfGrid-headerFilterInput .ds-button {
134
+ --dsc-button-size: var(--ds-size-7, 1.5rem);
135
+ --ds-icon-size: var(--ds-size-4, 1rem);
136
+
137
+ min-height: 0;
138
+ padding: 0;
139
+ }
140
+
141
+ /* The funnel is highlighted while the column has criteria, as in the toolbar */
142
+ .AfGrid-headerFilterButton[data-active] {
143
+ --dsc-button-color: var(--ds-color-accent-base-default);
144
+ --dsc-button-color--hover: var(--ds-color-accent-base-hover);
145
+ }
146
+
147
+ .AfGrid-headerFilterEmpty {
148
+ display: block;
149
+ inline-size: 100%;
150
+ block-size: 100%;
151
+ }
152
+
153
+ /*
154
+ * The panel is a native popover, so it renders in the top layer and is not
155
+ * clipped by the grid's own overflow. `display` must stay scoped to
156
+ * `:popover-open`, or it overrides the user agent rule that hides a closed
157
+ * popover and the panel is always visible.
158
+ */
159
+ .AfGrid-headerFilterPanel.ds-popover {
160
+ --dsc-popover-max-width: none;
161
+ --dsc-popover-padding: var(--ds-size-3, 0.75rem);
162
+
163
+ inline-size: 22rem;
164
+ max-inline-size: 90vw;
165
+ block-size: 26rem;
166
+ max-block-size: 80vh;
167
+ }
168
+
169
+ /*
170
+ * A column with many distinct values needs a taller list than one with a
171
+ * handful, so the panel is resizable and the panel body follows its height.
172
+ * `resize` needs a non-visible overflow to take effect.
173
+ */
174
+ .AfGrid-headerFilterPanel.ds-popover:popover-open {
175
+ display: flex;
176
+ flex-direction: column;
177
+ gap: var(--ds-size-3, 0.75rem);
178
+ min-inline-size: 18rem;
179
+ min-block-size: 16rem;
180
+ overflow: auto;
181
+ resize: both;
182
+ }
183
+
184
+ .AfGrid-headerFilterPanel .ObFieldFilterPanel-root {
185
+ flex: 1 1 auto;
186
+ min-block-size: 0;
187
+ }
@@ -0,0 +1,376 @@
1
+ import "./index.css";
2
+
3
+ import { Alert } from "@digdir/designsystemet-react";
4
+ import {
5
+ DataGridPro as DataGrid,
6
+ type DataGridProProps as DataGridProps,
7
+ type GridApi,
8
+ type GridFilterModel,
9
+ useGridApiRef,
10
+ } from "@mui/x-data-grid-pro";
11
+ import { getLocalizedString } from "@olenbetong/appframe-core";
12
+ import type { DataObject } from "@olenbetong/appframe-data";
13
+ import { DataObjectProvider, useLoading } from "@olenbetong/appframe-react";
14
+
15
+ import clsx from "clsx";
16
+ import { useEffect, useMemo, useState } from "react";
17
+ import { FilterBuilder } from "../filter/index.js";
18
+ import { AfGridColumnsPanel } from "./AfGridColumnsPanel.js";
19
+ import { type AfGridContextValue, AfGridContextProvider } from "./AfGridContext.js";
20
+ import { AfGridError } from "./AfGridError.js";
21
+ import { registerLicense } from "./license.js";
22
+ import { AF_GRID_LOCALE_TEXT } from "./localization.js";
23
+ import { dsBaseSlots } from "./slots/index.js";
24
+ import { AfGridThemeProvider } from "./theme.js";
25
+ import {
26
+ type AfExcelExportOptions,
27
+ AfGridExcelExportMenuItem,
28
+ type AfGridExcelExportOptions,
29
+ AfGridToolbar,
30
+ AfGridToolbarExcelExportButton,
31
+ GridToolbarAddButton,
32
+ GridToolbarAddIfAllowedButton,
33
+ GridToolbarColumnsButton,
34
+ GridToolbarContainer,
35
+ GridToolbarFilterButton,
36
+ GridToolbarMoreOptionsButton,
37
+ GridToolbarRefreshButton,
38
+ GridToolbarResetLayoutButton,
39
+ } from "./Toolbar.js";
40
+ import type { AfGridColumn } from "./useAfColumns.js";
41
+ import { type AfGridColDef, useAfColumns } from "./useAfColumns.js";
42
+ import { useAfCurrentIndex } from "./useAfCurrentIndex.js";
43
+ import { useAfData } from "./useAfData.js";
44
+ import { useAfFilterFields } from "./useAfFilterFields.js";
45
+ import { useAfFilter } from "./useAfFilter.js";
46
+ import { type AfGridApi, useAfGridApi } from "./useAfGridApi.js";
47
+ import { useAfKeyBindings } from "./useAfKeyBindings.js";
48
+ import { useAfPagination } from "./useAfPagination.js";
49
+ import {
50
+ isPersistedStateLoaded,
51
+ type StatePersistenceConfig,
52
+ settingsLoaded,
53
+ useAfPersistedState,
54
+ } from "./useAfPersistedState.js";
55
+ import { useAfRowEditModel } from "./useAfRowEditModel.js";
56
+ import { useAfSortModel } from "./useAfSortModel.js";
57
+
58
+ export * from "./formatters.js";
59
+ export * as MuiDataGrid from "@mui/x-data-grid-pro";
60
+
61
+ registerLicense();
62
+
63
+ /**
64
+ * The grid's criteria live in a `FilterTree` owned by `useAfFilter`, not in
65
+ * MUI's filter model, so the model is always empty. It is hoisted so the
66
+ * reference is stable and the grid does not treat it as a change.
67
+ */
68
+ const EMPTY_FILTER_MODEL: GridFilterModel = { items: [] };
69
+
70
+ const privSysColumns: AfGridColDef[] = [
71
+ { field: "PrimKey", editable: false, hide: true },
72
+ { field: "Created", editable: false, hide: true },
73
+ { field: "CreatedBy", editable: false, hide: true },
74
+ { field: "Updated", editable: false, hide: true },
75
+ { field: "UpdatedBy", editable: false, hide: true },
76
+ ];
77
+
78
+ export const systemColumns = {
79
+ [Symbol.iterator]: () => privSysColumns[Symbol.iterator](),
80
+ PrimKey: privSysColumns[0],
81
+ Created: privSysColumns[1],
82
+ CreatedBy: privSysColumns[2],
83
+ Updated: privSysColumns[3],
84
+ UpdatedBy: privSysColumns[4],
85
+ };
86
+
87
+ export type { AfExcelExportOptions, AfGridApi, AfGridColDef, AfGridColumn, AfGridExcelExportOptions };
88
+
89
+ export {
90
+ AF_GRID_LOCALE_TEXT,
91
+ AfGridColumnsPanel,
92
+ AfGridExcelExportMenuItem,
93
+ AfGridThemeProvider,
94
+ AfGridToolbar,
95
+ AfGridToolbarExcelExportButton,
96
+ GridToolbarAddButton,
97
+ GridToolbarAddIfAllowedButton,
98
+ GridToolbarColumnsButton,
99
+ GridToolbarContainer,
100
+ GridToolbarFilterButton,
101
+ GridToolbarMoreOptionsButton,
102
+ GridToolbarRefreshButton,
103
+ GridToolbarResetLayoutButton,
104
+ useAfColumns,
105
+ useAfCurrentIndex,
106
+ useAfData,
107
+ useAfFilter,
108
+ useAfFilterFields,
109
+ useAfPagination,
110
+ useAfPersistedState,
111
+ useAfRowEditModel,
112
+ useAfSortModel,
113
+ };
114
+
115
+ export type AfGridProps = {
116
+ apiRef?: React.RefObject<GridApi>;
117
+ /**
118
+ * If set to true, the grid will automatically load data if the data object has not
119
+ * been loaded previously.
120
+ */
121
+ autoLoad?: boolean;
122
+ /**
123
+ * Bind the focused/selected row to the current index in the data object.
124
+ * Useful if you want child data to load as the user navigates the grid.
125
+ */
126
+ bindIndex?: boolean;
127
+ columns: AfGridColumn[];
128
+ dataObject: DataObject<any>;
129
+ /**
130
+ * The layout version of the grid. Persisted grid state will only be restored
131
+ * if the version matches.
132
+ */
133
+ layoutVersion?: string | number;
134
+ /**
135
+ * Field that contains a unique identifier for each row. Defaults to PrimKey.
136
+ */
137
+ idField?: string;
138
+ /**
139
+ * A function that can be used to transform the data object data before it is
140
+ * passed to the grid, for example to create summary rows.
141
+ */
142
+ getData?: (data: Readonly<Readonly<any>[]>) => any[];
143
+ /**
144
+ * Override the data object permission, and remove the delete button from the default row actions
145
+ */
146
+ disableDelete?: boolean;
147
+ /**
148
+ * Override the data object permission, and remove the edit button from the default row actions
149
+ */
150
+ disableEdit?: boolean;
151
+ /**
152
+ * Override the data object permission, and remove the Add button from the default toolbar.
153
+ */
154
+ disableInsert?: boolean;
155
+ /**
156
+ * Use to disable server sorting. Required for example when using row grouping.
157
+ */
158
+ disableServerSorting?: boolean;
159
+ /**
160
+ * The view the filter builder saves and loads saved filters for, and converts
161
+ * its filter against. Without it the filter builder can still be used, but
162
+ * saved filters are unavailable.
163
+ */
164
+ filterViewName?: string;
165
+ /**
166
+ * A unique ID used to persist the grid state. It is better to use the layoutVersion
167
+ * property if you would like to ignore any previously persisted state, as changing
168
+ * this property will leave orphan records for the previous id in the Indexed DB
169
+ * grid layout store.
170
+ */
171
+ id: string;
172
+ /**
173
+ * Object almost matching the grid state, but with all values as booleans. This
174
+ * is used to configure which properties should be persisted.
175
+ */
176
+ persistenceModel?: StatePersistenceConfig;
177
+ } & Omit<
178
+ DataGridProps,
179
+ | "apiRef"
180
+ | "columns"
181
+ | "loading"
182
+ | "rows"
183
+ | "filterMode"
184
+ | "filterModel"
185
+ | "onFilterModelChange"
186
+ | "sortingMode"
187
+ | "sortModel"
188
+ | "onSortModelChange"
189
+ | "editRowsModel"
190
+ | "editMode"
191
+ | "onRowEditCommit"
192
+ | "onEditRowsModelChange"
193
+ | "pagination"
194
+ | "paginationMode"
195
+ | "pageSize"
196
+ | "rowCount"
197
+ | "rowsPerPageOptions"
198
+ | "onPageChange"
199
+ | "onPageSizeChange"
200
+ >;
201
+
202
+ // This wrapper component prevents the actual grid from rendering until the persisted state has been loaded.
203
+ // This is to prevent the grid from flickering when the persisted state is restored, plus the grid sometimes
204
+ // resets the column order if not provided in the initial state
205
+ export function AfGrid(props: AfGridProps) {
206
+ let [loaded, setLoaded] = useState(isPersistedStateLoaded());
207
+
208
+ useEffect(() => {
209
+ settingsLoaded.then(() => setLoaded(true));
210
+ }, []);
211
+
212
+ return (
213
+ <AfGridThemeProvider>
214
+ {loaded ? <AfGrid_ {...props} /> : <div className="AfGrid-loading">{getLocalizedString("Loading...")}</div>}
215
+ </AfGridThemeProvider>
216
+ );
217
+ }
218
+
219
+ function AfGrid_(props: AfGridProps) {
220
+ let {
221
+ autoLoad = false,
222
+ apiRef: apiRefProp,
223
+ bindIndex,
224
+ columns: columnsProp,
225
+ disableServerSorting,
226
+ disableInsert,
227
+ disableDelete,
228
+ disableEdit = false,
229
+ dataObject,
230
+ filterViewName,
231
+ layoutVersion = 0,
232
+ getData,
233
+ idField = "PrimKey",
234
+ id,
235
+ persistenceModel,
236
+ initialState,
237
+ ...other
238
+ } = props;
239
+ let apiRefPriv = useGridApiRef();
240
+ let apiRef = apiRefProp ?? apiRefPriv;
241
+ useAfGridApi(props, apiRef);
242
+ let loading = useLoading(dataObject);
243
+ let paging = useAfPagination(dataObject);
244
+ let sortModel = useAfSortModel({
245
+ dataObject,
246
+ columns: columnsProp,
247
+ disableServerSorting,
248
+ });
249
+ let gridFilter = useAfFilter({ autoLoad, dataObject, id });
250
+ let [snackbar, setSnackbar] = useState<string | null>(null);
251
+ let [isFilterBuilderOpen, setFilterBuilderOpen] = useState(false);
252
+ let [isColumnsPanelOpen, setColumnsPanelOpen] = useState(false);
253
+ let editProps = useAfRowEditModel({ dataObject, disableEdit, idField, apiRef });
254
+ let data = useAfData(dataObject, { getData });
255
+ useAfCurrentIndex({
256
+ apiRef,
257
+ dataObject,
258
+ idField,
259
+ enabled: !!bindIndex,
260
+ });
261
+ let columns = useAfColumns({
262
+ apiRef,
263
+ dataObject,
264
+ disableEdit,
265
+ disableDelete,
266
+ columnsProp,
267
+ idField,
268
+ });
269
+ let filterFields = useAfFilterFields(dataObject, columnsProp);
270
+
271
+ let initialState_ = useAfPersistedState({
272
+ apiRef,
273
+ columns: columnsProp,
274
+ id,
275
+ layoutVersion,
276
+ persistenceModel,
277
+ initialState,
278
+ });
279
+
280
+ let keyEvents = useAfKeyBindings({ apiRef, dataObject });
281
+
282
+ useEffect(() => {
283
+ if (!snackbar) return;
284
+
285
+ let timeout = setTimeout(() => setSnackbar(null), 6000);
286
+
287
+ return () => clearTimeout(timeout);
288
+ }, [snackbar]);
289
+
290
+ let gridContext = useMemo<AfGridContextValue>(
291
+ () => ({
292
+ id,
293
+ apiRef,
294
+ filterFields,
295
+ ...gridFilter,
296
+ openFilterBuilder: () => setFilterBuilderOpen(true),
297
+ openColumnsPanel: () => setColumnsPanelOpen(true),
298
+ }),
299
+ [id, apiRef, filterFields, gridFilter],
300
+ );
301
+
302
+ return (
303
+ <DataObjectProvider dataObject={dataObject}>
304
+ <AfGridContextProvider value={gridContext}>
305
+ <div className="AfGrid-root" {...keyEvents} tabIndex={-1}>
306
+ <DataGrid
307
+ apiRef={apiRef}
308
+ rows={data}
309
+ columns={columns}
310
+ loading={loading}
311
+ getRowId={(row) => row[af.data.uid] ?? row[idField]}
312
+ {...editProps}
313
+ /*
314
+ * Our own filter tree drives the data object, so MUI never
315
+ * sees a filter model. The header filter cells are replaced
316
+ * by `renderHeaderFilter` on every column.
317
+ */
318
+ filterMode="server"
319
+ filterModel={EMPTY_FILTER_MODEL}
320
+ {...sortModel}
321
+ headerFilters
322
+ {...paging}
323
+ showToolbar
324
+ {...other}
325
+ localeText={{ ...other.localeText, ...AF_GRID_LOCALE_TEXT }}
326
+ slots={{
327
+ ...dsBaseSlots,
328
+ toolbar: () => <AfGridToolbar disableInsert={disableInsert} />,
329
+ ...other.slots,
330
+ }}
331
+ initialState={initialState_}
332
+ slotProps={{
333
+ ...other.slotProps,
334
+ headerFilterCell: {
335
+ ...other.slotProps?.headerFilterCell,
336
+ headerClassName: clsx(other.slotProps?.headerFilterCell?.headerClassName, "AfGrid-headerCell"),
337
+ showRightBorder: true,
338
+ },
339
+ }}
340
+ onProcessRowUpdateError={(error) => {
341
+ other.onProcessRowUpdateError?.(error);
342
+ let message = "message" in (error as any) ? (error as any).message : String(error);
343
+ setSnackbar(message);
344
+ }}
345
+ />
346
+ </div>
347
+ <AfGridError apiRef={apiRef} dataObject={dataObject} />
348
+ {isColumnsPanelOpen && <AfGridColumnsPanel open onClose={() => setColumnsPanelOpen(false)} />}
349
+ <FilterBuilder
350
+ variant="dialog"
351
+ open={isFilterBuilderOpen}
352
+ onClose={() => setFilterBuilderOpen(false)}
353
+ fields={filterFields}
354
+ viewName={filterViewName}
355
+ dataObject={dataObject}
356
+ /*
357
+ * The builder edits the same named slot and the same tree as the
358
+ * header filters, so a criterion added in one shows up in the
359
+ * other. Applying writes the slot directly, which skips the
360
+ * header filters' debounce.
361
+ */
362
+ slotName={id}
363
+ value={gridFilter.filter}
364
+ onChange={gridFilter.setFilter}
365
+ />
366
+ {!!snackbar && (
367
+ <div className="AfGrid-snackbar">
368
+ <Alert data-color="danger" data-size="sm" onClick={() => setSnackbar(null)}>
369
+ {snackbar}
370
+ </Alert>
371
+ </div>
372
+ )}
373
+ </AfGridContextProvider>
374
+ </DataObjectProvider>
375
+ );
376
+ }