@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.
- package/CHANGELOG.md +80 -0
- package/package.json +50 -6
- package/scripts/copyAssets.mjs +13 -0
- package/src/AfLookup/LookupCombobox.tsx +3 -1
- package/src/filter/ChipListInput.tsx +79 -0
- package/src/filter/FieldFilterPanel.css +127 -0
- package/src/filter/FieldFilterPanel.tsx +309 -0
- package/src/filter/FilterBuilder.css +152 -0
- package/src/filter/FilterBuilder.test.tsx +153 -0
- package/src/filter/FilterBuilder.tsx +435 -0
- package/src/filter/FilterEditor.css +231 -0
- package/src/filter/FilterEditor.test.tsx +259 -0
- package/src/filter/FilterEditor.tsx +74 -0
- package/src/filter/FilterGroupEditor.tsx +160 -0
- package/src/filter/FilterNameDialog.css +22 -0
- package/src/filter/FilterNameDialog.tsx +81 -0
- package/src/filter/FilterRow.tsx +117 -0
- package/src/filter/FilterShareDialog.css +28 -0
- package/src/filter/FilterShareDialog.tsx +201 -0
- package/src/filter/FilterStringField.tsx +77 -0
- package/src/filter/FilterValueEditor.tsx +220 -0
- package/src/filter/SavedFilterTree.css +107 -0
- package/src/filter/SavedFilterTree.test.tsx +94 -0
- package/src/filter/SavedFilterTree.tsx +222 -0
- package/src/filter/fieldFilter.test.ts +158 -0
- package/src/filter/fieldFilter.ts +139 -0
- package/src/filter/index.ts +27 -0
- package/src/filter/types.ts +38 -0
- package/src/filter/useDistinctValues.test.ts +102 -0
- package/src/filter/useDistinctValues.ts +230 -0
- package/src/global.d.ts +11 -0
- package/src/grid/AfGridColumnsPanel.tsx +118 -0
- package/src/grid/AfGridContext.tsx +51 -0
- package/src/grid/AfGridError.tsx +46 -0
- package/src/grid/AfHeaderFilterCell.tsx +151 -0
- package/src/grid/AfHeaderFilterPanel.tsx +111 -0
- package/src/grid/Toolbar.tsx +233 -0
- package/src/grid/editing.ts +90 -0
- package/src/grid/filter.ts +78 -0
- package/src/grid/formatters.ts +48 -0
- package/src/grid/index.css +187 -0
- package/src/grid/index.tsx +376 -0
- package/src/grid/license.ts +48 -0
- package/src/grid/localization.ts +369 -0
- package/src/grid/slots/index.tsx +275 -0
- package/src/grid/slots/slots.css +53 -0
- package/src/grid/theme.tsx +185 -0
- package/src/grid/useAfColumns.tsx +313 -0
- package/src/grid/useAfCurrentIndex.ts +55 -0
- package/src/grid/useAfData.ts +20 -0
- package/src/grid/useAfFilter.ts +160 -0
- package/src/grid/useAfFilterFields.ts +55 -0
- package/src/grid/useAfGridApi.ts +66 -0
- package/src/grid/useAfKeyBindings.ts +36 -0
- package/src/grid/useAfPagination.ts +53 -0
- package/src/grid/useAfPersistedState.ts +222 -0
- package/src/grid/useAfRowEditModel.ts +137 -0
- package/src/grid/useAfSortModel.ts +72 -0
- package/src/index.ts +1 -0
- package/src/input/InputAdornments.tsx +6 -1
- package/src/report-downloader/ReportDownloader.css +67 -0
- package/src/report-downloader/components/StatusItem.tsx +86 -0
- package/src/report-downloader/components/StatusList.tsx +28 -0
- package/src/report-downloader/index.ts +41 -0
- package/src/report-downloader/status.tsx +92 -0
- package/src/test/setup.ts +8 -0
- 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
|
+
}
|