@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.
- package/CHANGELOG.md +13 -0
- package/package.json +8 -7
- package/scripts/copyAssets.mjs +27 -55
- package/src/AfLookup/Lookup.css +129 -125
- package/src/AfLookup/Lookup.tsx +184 -162
- package/src/AfLookup/LookupCombobox.css +78 -78
- package/src/AfLookup/LookupCombobox.tsx +237 -237
- package/src/AfLookup/LookupEdit.tsx +130 -130
- package/src/AfLookup/LookupGrid.tsx +124 -124
- package/src/AfLookup/index.ts +5 -5
- package/src/autocomplete/Autocomplete.css +256 -256
- package/src/autocomplete/Autocomplete.tsx +320 -320
- package/src/autocomplete/AutocompleteFrame.tsx +168 -168
- package/src/autocomplete/Combobox.tsx +286 -286
- package/src/autocomplete/index.ts +4 -4
- package/src/autocomplete/types.ts +204 -204
- package/src/autocomplete/utils.ts +183 -183
- package/src/binding/BoundNumericTextField.tsx +102 -102
- package/src/binding/BoundTextField.tsx +49 -49
- package/src/binding/CancelButton.tsx +30 -30
- package/src/binding/CancelIconButton.tsx +30 -30
- package/src/binding/DataEditToolbar.tsx +92 -92
- package/src/binding/DeleteButton.tsx +35 -35
- package/src/binding/DeleteIconButton.tsx +35 -35
- package/src/binding/RefreshButton.tsx +31 -31
- package/src/binding/RefreshIconButton.tsx +33 -33
- package/src/binding/RefreshRowIconButton.tsx +33 -33
- package/src/binding/SaveButton.tsx +37 -37
- package/src/binding/SaveIconButton.tsx +38 -38
- package/src/container/Container.tsx +49 -49
- package/src/container/index.ts +1 -1
- package/src/cutoff/CutOffPicker.css +12 -0
- package/src/cutoff/CutOffPicker.tsx +165 -0
- package/src/cutoff/context.tsx +194 -0
- package/src/cutoff/dsCutOffDates.ts +21 -0
- package/src/cutoff/index.ts +4 -0
- package/src/cutoff/utils.test.ts +59 -0
- package/src/cutoff/utils.ts +276 -0
- package/src/filter/ChipListInput.tsx +79 -79
- package/src/filter/FieldFilterPanel.css +150 -150
- package/src/filter/FieldFilterPanel.tsx +309 -309
- package/src/filter/FilterBuilder.css +152 -152
- package/src/filter/FilterBuilder.test.tsx +153 -153
- package/src/filter/FilterBuilder.tsx +435 -435
- package/src/filter/FilterEditor.css +231 -231
- package/src/filter/FilterEditor.test.tsx +259 -259
- package/src/filter/FilterEditor.tsx +75 -74
- package/src/filter/FilterGroupEditor.tsx +160 -160
- package/src/filter/FilterNameDialog.css +22 -22
- package/src/filter/FilterNameDialog.tsx +81 -81
- package/src/filter/FilterRow.tsx +117 -117
- package/src/filter/FilterShareDialog.css +28 -28
- package/src/filter/FilterShareDialog.tsx +201 -201
- package/src/filter/FilterStringField.tsx +78 -77
- package/src/filter/FilterValueEditor.tsx +220 -220
- package/src/filter/SavedFilterTree.css +107 -107
- package/src/filter/SavedFilterTree.test.tsx +94 -94
- package/src/filter/SavedFilterTree.tsx +218 -222
- package/src/filter/fieldFilter.test.ts +158 -158
- package/src/filter/fieldFilter.ts +139 -139
- package/src/filter/index.ts +27 -27
- package/src/filter/types.ts +38 -38
- package/src/filter/useDistinctValues.test.ts +102 -102
- package/src/filter/useDistinctValues.ts +230 -230
- package/src/global.d.ts +11 -11
- package/src/grid/AfGridColumnsPanel.tsx +118 -118
- package/src/grid/AfGridContext.tsx +51 -51
- package/src/grid/AfGridError.tsx +46 -46
- package/src/grid/AfHeaderFilterCell.tsx +147 -151
- package/src/grid/AfHeaderFilterPanel.tsx +111 -111
- package/src/grid/GridEditLookup.css +12 -0
- package/src/grid/GridEditLookup.tsx +121 -0
- package/src/grid/Toolbar.tsx +233 -233
- package/src/grid/editing.ts +100 -100
- package/src/grid/filter.ts +78 -78
- package/src/grid/formatters.ts +48 -48
- package/src/grid/index.css +245 -245
- package/src/grid/index.tsx +489 -484
- package/src/grid/license.ts +48 -48
- package/src/grid/localization.ts +369 -369
- package/src/grid/slots/index.tsx +307 -307
- package/src/grid/slots/slots.css +64 -64
- package/src/grid/theme.tsx +138 -138
- package/src/grid/useAfColumns.tsx +371 -371
- package/src/grid/useAfCurrentIndex.ts +63 -63
- package/src/grid/useAfData.ts +20 -20
- package/src/grid/useAfFilter.ts +160 -160
- package/src/grid/useAfFilterFields.ts +55 -55
- package/src/grid/useAfGridApi.ts +71 -71
- package/src/grid/useAfKeyBindings.ts +36 -36
- package/src/grid/useAfNewItemRow.ts +258 -258
- package/src/grid/useAfPagination.ts +53 -53
- package/src/grid/useAfPersistedState.ts +230 -230
- package/src/grid/useAfRowEditModel.ts +145 -145
- package/src/grid/useAfRowGrouping.ts +47 -47
- package/src/grid/useAfServerAggregation.ts +145 -145
- package/src/grid/useAfSortModel.ts +84 -84
- package/src/i18n.ts +13 -0
- package/src/index.ts +14 -12
- package/src/input/InputAdornments.css +75 -75
- package/src/input/InputAdornments.tsx +102 -102
- package/src/input/index.ts +1 -1
- package/src/layout/AppMenu.tsx +74 -75
- package/src/layout/AppMenuDrawer.css +70 -70
- package/src/layout/AppMenuDrawer.tsx +122 -122
- package/src/layout/ErrorBoundary.tsx +162 -162
- package/src/layout/ResponsiveDialog.css +86 -0
- package/src/layout/ResponsiveDialog.tsx +55 -0
- package/src/layout/index.tsx +69 -68
- package/src/link/Link.tsx +23 -0
- package/src/link/index.ts +1 -0
- package/src/mdi/Mdi.css +138 -138
- package/src/mdi/Mdi.test.tsx +91 -87
- package/src/mdi/Mdi.tsx +141 -141
- package/src/mdi/MdiConfirmCloseDialog.tsx +47 -47
- package/src/mdi/MdiContext.tsx +59 -59
- package/src/mdi/MdiDocumentContext.tsx +60 -60
- package/src/mdi/MdiPanel.tsx +61 -61
- package/src/mdi/MdiRestore.test.tsx +86 -82
- package/src/mdi/MdiTab.tsx +30 -30
- package/src/mdi/controller.test.ts +382 -382
- package/src/mdi/controller.ts +656 -656
- package/src/mdi/dockviewHost.ts +132 -132
- package/src/mdi/fakeHost.ts +200 -200
- package/src/mdi/host.ts +44 -44
- package/src/mdi/ids.test.ts +20 -20
- package/src/mdi/ids.ts +18 -18
- package/src/mdi/index.ts +31 -31
- package/src/mdi/layoutRestore.test.ts +156 -156
- package/src/mdi/layoutRestore.ts +311 -311
- package/src/mdi/layoutSpec.test.ts +143 -143
- package/src/mdi/layoutSpec.ts +214 -214
- package/src/mdi/layoutStore.test.ts +93 -93
- package/src/mdi/layoutStore.ts +99 -99
- package/src/mdi/memorySettingsStore.ts +21 -21
- package/src/mdi/registry.ts +34 -34
- package/src/mdi/testTypes.ts +37 -37
- package/src/mdi/theme.ts +22 -22
- package/src/mdi/types.ts +214 -214
- package/src/page/Page.css +53 -53
- package/src/page/PageBlock.css +64 -64
- package/src/page/PageBlock.tsx +92 -92
- package/src/page/PageHeader.css +88 -88
- package/src/page/PageHeader.tsx +115 -116
- package/src/page/PageTitle.css +5 -0
- package/src/page/PageTitle.tsx +21 -0
- package/src/page/index.ts +4 -3
- package/src/paper/Paper.tsx +52 -52
- package/src/progress/LinearProgress.css +93 -93
- package/src/progress/LinearProgress.tsx +104 -104
- package/src/progress/index.ts +1 -1
- package/src/report-downloader/ReportDownloader.css +67 -67
- package/src/report-downloader/components/StatusItem.tsx +87 -86
- package/src/report-downloader/components/StatusList.tsx +28 -28
- package/src/report-downloader/index.ts +41 -41
- package/src/report-downloader/status.tsx +92 -92
- package/src/sortable/DropIndicator.css +51 -51
- package/src/sortable/DropIndicator.tsx +27 -27
- package/src/sortable/SortableItemList.css +21 -21
- package/src/sortable/SortableItemList.tsx +264 -264
- package/src/sortable/index.ts +8 -8
- package/src/sortable/reorderSortOrder.ts +126 -126
- package/src/sortable/sortOrder.ts +101 -101
- package/src/sortable/types.ts +9 -9
- package/src/sortable/useSortableContainer.ts +53 -53
- package/src/sortable/useSortableItem.ts +95 -95
- package/src/sortable/useSortableList.ts +117 -117
- package/src/test/happy-dom-document-class.ts +24 -24
- package/src/test/setup.ts +12 -12
- package/src/theme/useDsColorScheme.ts +53 -53
- package/tsconfig.build.json +9 -9
- package/tsconfig.json +8 -8
- package/vitest.config.ts +10 -10
|
@@ -1,371 +1,371 @@
|
|
|
1
|
-
import {
|
|
2
|
-
GRID_AGGREGATION_ROOT_FOOTER_ROW_ID,
|
|
3
|
-
GRID_CHECKBOX_SELECTION_FIELD,
|
|
4
|
-
GRID_DETAIL_PANEL_TOGGLE_FIELD,
|
|
5
|
-
GRID_TREE_DATA_GROUPING_FIELD,
|
|
6
|
-
GridActionsCellItem,
|
|
7
|
-
type GridActionsCellItemProps,
|
|
8
|
-
type GridAlignment,
|
|
9
|
-
type GridApi,
|
|
10
|
-
type GridColDef,
|
|
11
|
-
type GridColType,
|
|
12
|
-
type GridColumnVisibilityModel,
|
|
13
|
-
type GridRowId,
|
|
14
|
-
GridRowModes,
|
|
15
|
-
type GridRowParams,
|
|
16
|
-
getAggregationFooterRowIdFromGroupId,
|
|
17
|
-
gridEditRowsStateSelector,
|
|
18
|
-
useGridApiContext,
|
|
19
|
-
useGridSelector,
|
|
20
|
-
} from "@mui/x-data-grid-premium";
|
|
21
|
-
import { FloppydiskIcon, PencilIcon, TrashIcon, XMarkIcon } from "@navikt/aksel-icons";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
34
|
-
/** `auto-generated-group-footer-`, without spelling it out: every group's total row shares it. */
|
|
35
|
-
const AGGREGATION_FOOTER_ID_PREFIX = String(getAggregationFooterRowIdFromGroupId(""));
|
|
36
|
-
|
|
37
|
-
export type AfGridColDef = GridColDef & {
|
|
38
|
-
sortColumn?: string;
|
|
39
|
-
filterColumn?: string;
|
|
40
|
-
/**
|
|
41
|
-
* Whether the column should be hidden by default
|
|
42
|
-
*/
|
|
43
|
-
hide?: boolean;
|
|
44
|
-
/**
|
|
45
|
-
* Mark this column as calculated/virtual — it does not correspond to a field in
|
|
46
|
-
* the data object. Calculated columns are always non-editable and are excluded
|
|
47
|
-
* from server-side sort and filter operations.
|
|
48
|
-
*/
|
|
49
|
-
calculated?: boolean;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Whether the cell belongs to a row the grid generated to hold totals rather than to a record.
|
|
54
|
-
*
|
|
55
|
-
* The row type is not enough on its own: the grand total is pinned to the bottom, so it arrives as
|
|
56
|
-
* a `pinnedRow` — the same type as the grid's own new-item row, whose `renderCell` must still run.
|
|
57
|
-
* Its id is the reliable marker, and MUI exports the constants for it.
|
|
58
|
-
*/
|
|
59
|
-
function isAggregationRow(params: { id: GridRowId; rowNode?: { type?: string } }) {
|
|
60
|
-
if (params.rowNode?.type === "group") return true;
|
|
61
|
-
|
|
62
|
-
return (
|
|
63
|
-
typeof params.id === "string" &&
|
|
64
|
-
(params.id === GRID_AGGREGATION_ROOT_FOOTER_ROW_ID || params.id.startsWith(AGGREGATION_FOOTER_ID_PREFIX))
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function getColumnVisibilityModel(columns: AfGridColumn[]): GridColumnVisibilityModel {
|
|
69
|
-
let visibility: GridColumnVisibilityModel = {};
|
|
70
|
-
for (let column of columns) {
|
|
71
|
-
if (typeof column !== "string" && column.hide) {
|
|
72
|
-
visibility[column.field] = false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return visibility;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function afColumnsToMuiColumn({
|
|
79
|
-
apiRef,
|
|
80
|
-
allowUpdate,
|
|
81
|
-
columnsProp,
|
|
82
|
-
dataObject,
|
|
83
|
-
}: useAfColumnsOptions & { allowDelete: boolean; allowUpdate: boolean }) {
|
|
84
|
-
let columns = columnsProp;
|
|
85
|
-
if (columns.length === 0) {
|
|
86
|
-
columns = dataObject.getFields().map((field) => field.name);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let sysColumns = [GRID_CHECKBOX_SELECTION_FIELD, GRID_DETAIL_PANEL_TOGGLE_FIELD, GRID_TREE_DATA_GROUPING_FIELD];
|
|
90
|
-
let muiColumns: GridColDef[] = [];
|
|
91
|
-
let currentColumns = apiRef.current?.getAllColumns?.();
|
|
92
|
-
|
|
93
|
-
for (let column of columnsProp) {
|
|
94
|
-
let fieldName = typeof column === "string" ? column : column.field;
|
|
95
|
-
|
|
96
|
-
if (sysColumns.includes(fieldName)) {
|
|
97
|
-
muiColumns.push(typeof column === "string" ? { field: column } : column);
|
|
98
|
-
} else {
|
|
99
|
-
let fieldDef = dataObject.getFields(fieldName);
|
|
100
|
-
let type = "string";
|
|
101
|
-
if (typeof column !== "string" && column.type) {
|
|
102
|
-
type = column.type;
|
|
103
|
-
} else if (fieldDef?.type) {
|
|
104
|
-
type = fieldDef.type;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (type === "datetime") {
|
|
108
|
-
type = "dateTime";
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (type === "bigint") {
|
|
112
|
-
type = "number";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (!muiColumnTypes.includes(type)) {
|
|
116
|
-
type = "string";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let align: GridAlignment;
|
|
120
|
-
switch (type) {
|
|
121
|
-
case "number":
|
|
122
|
-
align = "right";
|
|
123
|
-
break;
|
|
124
|
-
case "date":
|
|
125
|
-
case "dateTime":
|
|
126
|
-
align = "center";
|
|
127
|
-
break;
|
|
128
|
-
default:
|
|
129
|
-
align = "left";
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let valueFormatter: (value: any) => string | number | boolean | object | null | undefined = (value) => value;
|
|
134
|
-
|
|
135
|
-
if (type === "date") {
|
|
136
|
-
valueFormatter = dateValueFormatter;
|
|
137
|
-
} else if (type === "dateTime") {
|
|
138
|
-
valueFormatter = dateTimeValueFormatter;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
let editable = allowUpdate && !fieldDef?.ignored && !fieldDef?.computed && !fieldDef?.joined;
|
|
142
|
-
|
|
143
|
-
// Calculated columns are never editable regardless of data object permissions
|
|
144
|
-
if (typeof column !== "string" && column.calculated) {
|
|
145
|
-
editable = false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let currentWidth = currentColumns?.find((c) => c.field === fieldName)?.width;
|
|
149
|
-
|
|
150
|
-
let col: GridColDef;
|
|
151
|
-
if (typeof column === "string") {
|
|
152
|
-
col = {
|
|
153
|
-
field: column,
|
|
154
|
-
type: type as GridColType,
|
|
155
|
-
headerName: column,
|
|
156
|
-
valueFormatter,
|
|
157
|
-
editable,
|
|
158
|
-
align,
|
|
159
|
-
headerAlign: align,
|
|
160
|
-
width: currentWidth ?? 150,
|
|
161
|
-
};
|
|
162
|
-
} else {
|
|
163
|
-
let { hide, calculated, ...rest } = column;
|
|
164
|
-
col = {
|
|
165
|
-
headerName: fieldDef?.caption ?? fieldName,
|
|
166
|
-
editable,
|
|
167
|
-
type: type as GridColType,
|
|
168
|
-
valueFormatter,
|
|
169
|
-
align,
|
|
170
|
-
headerAlign: align,
|
|
171
|
-
...rest,
|
|
172
|
-
width: currentWidth ?? rest.width ?? 150,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if ((operators as any)[type]) {
|
|
177
|
-
col.filterOperators = (operators as any)[type];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/*
|
|
181
|
-
* A total or group row has no record behind it — the grid generates it — so a
|
|
182
|
-
* `renderCell` that reads its value off `params.row`, which most do, has nothing to
|
|
183
|
-
* read. Left alone that reads as a broken feature in two ways: turning on a sum shows
|
|
184
|
-
* a pinned row of empty cells, and every unaggregated column renders whatever its
|
|
185
|
-
* `renderCell` makes of an empty record (a status chip saying "Open", say).
|
|
186
|
-
*
|
|
187
|
-
* So the aggregated value is rendered where there is one, nothing where there is not,
|
|
188
|
-
* and `renderCell` is left to the data rows it was written for. A column that wants to
|
|
189
|
-
* shape its total formats it with `valueFormatter`, which the grid applies to
|
|
190
|
-
* aggregation cells too.
|
|
191
|
-
*/
|
|
192
|
-
if (col.renderCell) {
|
|
193
|
-
let renderDataCell = col.renderCell;
|
|
194
|
-
col.renderCell = (params) => {
|
|
195
|
-
if (params.aggregation) return params.formattedValue ?? params.value ?? "";
|
|
196
|
-
if (isAggregationRow(params)) return "";
|
|
197
|
-
|
|
198
|
-
return renderDataCell(params);
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// The header filter cell is fully replaced by our own, which edits the
|
|
203
|
-
// shared filter tree instead of MUI's filter model.
|
|
204
|
-
if (col.filterable !== false && col.type !== "actions") {
|
|
205
|
-
col.renderHeaderFilter = (props) => <AfHeaderFilterCell {...props} dataObject={dataObject} />;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
muiColumns.push(col);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return muiColumns;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const GridActionItem = React.forwardRef<
|
|
216
|
-
HTMLButtonElement,
|
|
217
|
-
GridActionsCellItemProps & {
|
|
218
|
-
rowId: GridRowId;
|
|
219
|
-
mode: GridRowModes;
|
|
220
|
-
dataObject: DataObject<any>;
|
|
221
|
-
requireUpdate?: boolean;
|
|
222
|
-
requireDelete?: boolean;
|
|
223
|
-
}
|
|
224
|
-
>(function GridActionItem({ dataObject, mode, rowId, requireDelete = false, requireUpdate = false, ...props }, ref) {
|
|
225
|
-
let apiRef = useGridApiContext();
|
|
226
|
-
let rowModesModel = useGridSelector(apiRef, gridEditRowsStateSelector);
|
|
227
|
-
let { allowUpdate, allowDelete } = usePermissions(dataObject);
|
|
228
|
-
|
|
229
|
-
let currentMode = typeof rowModesModel[rowId] === "undefined" ? GridRowModes.View : GridRowModes.Edit;
|
|
230
|
-
|
|
231
|
-
if (currentMode !== mode) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (requireDelete && !allowDelete) {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (requireUpdate && !allowUpdate) {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return <GridActionsCellItem ref={ref} {...props} />;
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
export type AfGridColumn = string | AfGridColDef;
|
|
247
|
-
const muiColumnTypes = ["string", "number", "date", "dateTime", "boolean", "singleSelect", "actions"];
|
|
248
|
-
|
|
249
|
-
export type useAfColumnsOptions = {
|
|
250
|
-
apiRef: React.RefObject<GridApi | null>;
|
|
251
|
-
disableDelete?: boolean;
|
|
252
|
-
disableEdit?: boolean;
|
|
253
|
-
dataObject: DataObject<any>;
|
|
254
|
-
columnsProp: AfGridColumn[];
|
|
255
|
-
idField: string;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
export function useAfColumns({
|
|
259
|
-
apiRef,
|
|
260
|
-
dataObject,
|
|
261
|
-
disableEdit,
|
|
262
|
-
disableDelete,
|
|
263
|
-
columnsProp,
|
|
264
|
-
idField,
|
|
265
|
-
}: useAfColumnsOptions): GridColDef[] {
|
|
266
|
-
let { allowUpdate, allowDelete } = usePermissions(dataObject);
|
|
267
|
-
|
|
268
|
-
return useMemo(() => {
|
|
269
|
-
let columns = afColumnsToMuiColumn({
|
|
270
|
-
allowDelete,
|
|
271
|
-
allowUpdate,
|
|
272
|
-
apiRef,
|
|
273
|
-
dataObject,
|
|
274
|
-
disableEdit,
|
|
275
|
-
disableDelete,
|
|
276
|
-
columnsProp,
|
|
277
|
-
idField,
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
const handleEditClick = (id: GridRowId) => (event: React.MouseEvent<any>) => {
|
|
281
|
-
event.stopPropagation();
|
|
282
|
-
apiRef.current?.startRowEditMode({ id });
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
const handleSaveClick = (id: GridRowId) => async (event: React.MouseEvent<any>) => {
|
|
286
|
-
event.stopPropagation();
|
|
287
|
-
apiRef.current?.stopRowEditMode({ id });
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const handleDeleteClick = (id: GridRowId) => async (event: React.MouseEvent<any>) => {
|
|
291
|
-
event.stopPropagation();
|
|
292
|
-
await deleteRow({ apiRef, dataObject, id, idField });
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
const handleCancelClick = (id: GridRowId) => (event: React.MouseEvent<any>) => {
|
|
296
|
-
event.stopPropagation();
|
|
297
|
-
apiRef.current?.stopRowEditMode({ id, ignoreModifications: true });
|
|
298
|
-
|
|
299
|
-
const row = apiRef.current?.getRow(id);
|
|
300
|
-
if (row?.isNew) {
|
|
301
|
-
apiRef.current?.updateRows([{ [uid]: id, id, _action: "delete" } as any]);
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
if (!columns.find((c) => c.type === "actions") && (allowDelete || allowUpdate)) {
|
|
306
|
-
columns.push({
|
|
307
|
-
field: "actions",
|
|
308
|
-
type: "actions",
|
|
309
|
-
headerName: getLocalizedString("Actions"),
|
|
310
|
-
getActions: ({ id }: GridRowParams) => {
|
|
311
|
-
// The new item row only exists while inserting is allowed, so its save and
|
|
312
|
-
// cancel actions do not depend on the update permission, and it has nothing
|
|
313
|
-
// to edit or delete in view mode.
|
|
314
|
-
let isNewItem = isNewItemRow(id);
|
|
315
|
-
let actions = [
|
|
316
|
-
<GridActionItem
|
|
317
|
-
key="save"
|
|
318
|
-
icon={<FloppydiskIcon aria-hidden />}
|
|
319
|
-
label={getLocalizedString("Save")}
|
|
320
|
-
onClick={handleSaveClick(id)}
|
|
321
|
-
mode={GridRowModes.Edit}
|
|
322
|
-
dataObject={dataObject}
|
|
323
|
-
rowId={id}
|
|
324
|
-
requireUpdate={!isNewItem}
|
|
325
|
-
/>,
|
|
326
|
-
<GridActionItem
|
|
327
|
-
key="revert"
|
|
328
|
-
icon={<XMarkIcon aria-hidden />}
|
|
329
|
-
label={getLocalizedString("Cancel")}
|
|
330
|
-
onClick={handleCancelClick(id)}
|
|
331
|
-
mode={GridRowModes.Edit}
|
|
332
|
-
dataObject={dataObject}
|
|
333
|
-
rowId={id}
|
|
334
|
-
requireUpdate={!isNewItem}
|
|
335
|
-
/>,
|
|
336
|
-
];
|
|
337
|
-
|
|
338
|
-
if (isNewItem) {
|
|
339
|
-
return actions;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return [
|
|
343
|
-
<GridActionItem
|
|
344
|
-
key="edit"
|
|
345
|
-
icon={<PencilIcon aria-hidden />}
|
|
346
|
-
label={getLocalizedString("Edit")}
|
|
347
|
-
onClick={handleEditClick(id)}
|
|
348
|
-
mode={GridRowModes.View}
|
|
349
|
-
dataObject={dataObject}
|
|
350
|
-
rowId={id}
|
|
351
|
-
requireUpdate
|
|
352
|
-
/>,
|
|
353
|
-
<GridActionItem
|
|
354
|
-
key="delete"
|
|
355
|
-
icon={<TrashIcon aria-hidden />}
|
|
356
|
-
label={getLocalizedString("Delete")}
|
|
357
|
-
onClick={handleDeleteClick(id)}
|
|
358
|
-
mode={GridRowModes.View}
|
|
359
|
-
dataObject={dataObject}
|
|
360
|
-
rowId={id}
|
|
361
|
-
requireDelete
|
|
362
|
-
/>,
|
|
363
|
-
...actions,
|
|
364
|
-
];
|
|
365
|
-
},
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
return columns;
|
|
370
|
-
}, [columnsProp, disableDelete, disableEdit, allowUpdate, allowDelete, idField, dataObject, apiRef]);
|
|
371
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
GRID_AGGREGATION_ROOT_FOOTER_ROW_ID,
|
|
3
|
+
GRID_CHECKBOX_SELECTION_FIELD,
|
|
4
|
+
GRID_DETAIL_PANEL_TOGGLE_FIELD,
|
|
5
|
+
GRID_TREE_DATA_GROUPING_FIELD,
|
|
6
|
+
GridActionsCellItem,
|
|
7
|
+
type GridActionsCellItemProps,
|
|
8
|
+
type GridAlignment,
|
|
9
|
+
type GridApi,
|
|
10
|
+
type GridColDef,
|
|
11
|
+
type GridColType,
|
|
12
|
+
type GridColumnVisibilityModel,
|
|
13
|
+
type GridRowId,
|
|
14
|
+
GridRowModes,
|
|
15
|
+
type GridRowParams,
|
|
16
|
+
getAggregationFooterRowIdFromGroupId,
|
|
17
|
+
gridEditRowsStateSelector,
|
|
18
|
+
useGridApiContext,
|
|
19
|
+
useGridSelector,
|
|
20
|
+
} from "@mui/x-data-grid-premium";
|
|
21
|
+
import { FloppydiskIcon, PencilIcon, TrashIcon, XMarkIcon } from "@navikt/aksel-icons";
|
|
22
|
+
import { type DataObject, uid } from "@olenbetong/appframe-data";
|
|
23
|
+
import { usePermissions } from "@olenbetong/appframe-react";
|
|
24
|
+
|
|
25
|
+
import React, { useMemo } from "react";
|
|
26
|
+
|
|
27
|
+
import { AfHeaderFilterCell } from "./AfHeaderFilterCell.js";
|
|
28
|
+
import { deleteRow } from "./editing.js";
|
|
29
|
+
import { operators } from "./filter.js";
|
|
30
|
+
import { dateTimeValueFormatter, dateValueFormatter } from "./formatters.js";
|
|
31
|
+
import { isNewItemRow } from "./useAfNewItemRow.js";
|
|
32
|
+
import { getLocalizedString } from "../i18n.js";
|
|
33
|
+
|
|
34
|
+
/** `auto-generated-group-footer-`, without spelling it out: every group's total row shares it. */
|
|
35
|
+
const AGGREGATION_FOOTER_ID_PREFIX = String(getAggregationFooterRowIdFromGroupId(""));
|
|
36
|
+
|
|
37
|
+
export type AfGridColDef = GridColDef & {
|
|
38
|
+
sortColumn?: string;
|
|
39
|
+
filterColumn?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the column should be hidden by default
|
|
42
|
+
*/
|
|
43
|
+
hide?: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Mark this column as calculated/virtual — it does not correspond to a field in
|
|
46
|
+
* the data object. Calculated columns are always non-editable and are excluded
|
|
47
|
+
* from server-side sort and filter operations.
|
|
48
|
+
*/
|
|
49
|
+
calculated?: boolean;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Whether the cell belongs to a row the grid generated to hold totals rather than to a record.
|
|
54
|
+
*
|
|
55
|
+
* The row type is not enough on its own: the grand total is pinned to the bottom, so it arrives as
|
|
56
|
+
* a `pinnedRow` — the same type as the grid's own new-item row, whose `renderCell` must still run.
|
|
57
|
+
* Its id is the reliable marker, and MUI exports the constants for it.
|
|
58
|
+
*/
|
|
59
|
+
function isAggregationRow(params: { id: GridRowId; rowNode?: { type?: string } }) {
|
|
60
|
+
if (params.rowNode?.type === "group") return true;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
typeof params.id === "string" &&
|
|
64
|
+
(params.id === GRID_AGGREGATION_ROOT_FOOTER_ROW_ID || params.id.startsWith(AGGREGATION_FOOTER_ID_PREFIX))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getColumnVisibilityModel(columns: AfGridColumn[]): GridColumnVisibilityModel {
|
|
69
|
+
let visibility: GridColumnVisibilityModel = {};
|
|
70
|
+
for (let column of columns) {
|
|
71
|
+
if (typeof column !== "string" && column.hide) {
|
|
72
|
+
visibility[column.field] = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return visibility;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function afColumnsToMuiColumn({
|
|
79
|
+
apiRef,
|
|
80
|
+
allowUpdate,
|
|
81
|
+
columnsProp,
|
|
82
|
+
dataObject,
|
|
83
|
+
}: useAfColumnsOptions & { allowDelete: boolean; allowUpdate: boolean }) {
|
|
84
|
+
let columns = columnsProp;
|
|
85
|
+
if (columns.length === 0) {
|
|
86
|
+
columns = dataObject.getFields().map((field) => field.name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let sysColumns = [GRID_CHECKBOX_SELECTION_FIELD, GRID_DETAIL_PANEL_TOGGLE_FIELD, GRID_TREE_DATA_GROUPING_FIELD];
|
|
90
|
+
let muiColumns: GridColDef[] = [];
|
|
91
|
+
let currentColumns = apiRef.current?.getAllColumns?.();
|
|
92
|
+
|
|
93
|
+
for (let column of columnsProp) {
|
|
94
|
+
let fieldName = typeof column === "string" ? column : column.field;
|
|
95
|
+
|
|
96
|
+
if (sysColumns.includes(fieldName)) {
|
|
97
|
+
muiColumns.push(typeof column === "string" ? { field: column } : column);
|
|
98
|
+
} else {
|
|
99
|
+
let fieldDef = dataObject.getFields(fieldName);
|
|
100
|
+
let type = "string";
|
|
101
|
+
if (typeof column !== "string" && column.type) {
|
|
102
|
+
type = column.type;
|
|
103
|
+
} else if (fieldDef?.type) {
|
|
104
|
+
type = fieldDef.type;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (type === "datetime") {
|
|
108
|
+
type = "dateTime";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (type === "bigint") {
|
|
112
|
+
type = "number";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!muiColumnTypes.includes(type)) {
|
|
116
|
+
type = "string";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let align: GridAlignment;
|
|
120
|
+
switch (type) {
|
|
121
|
+
case "number":
|
|
122
|
+
align = "right";
|
|
123
|
+
break;
|
|
124
|
+
case "date":
|
|
125
|
+
case "dateTime":
|
|
126
|
+
align = "center";
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
align = "left";
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let valueFormatter: (value: any) => string | number | boolean | object | null | undefined = (value) => value;
|
|
134
|
+
|
|
135
|
+
if (type === "date") {
|
|
136
|
+
valueFormatter = dateValueFormatter;
|
|
137
|
+
} else if (type === "dateTime") {
|
|
138
|
+
valueFormatter = dateTimeValueFormatter;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let editable = allowUpdate && !fieldDef?.ignored && !fieldDef?.computed && !fieldDef?.joined;
|
|
142
|
+
|
|
143
|
+
// Calculated columns are never editable regardless of data object permissions
|
|
144
|
+
if (typeof column !== "string" && column.calculated) {
|
|
145
|
+
editable = false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let currentWidth = currentColumns?.find((c) => c.field === fieldName)?.width;
|
|
149
|
+
|
|
150
|
+
let col: GridColDef;
|
|
151
|
+
if (typeof column === "string") {
|
|
152
|
+
col = {
|
|
153
|
+
field: column,
|
|
154
|
+
type: type as GridColType,
|
|
155
|
+
headerName: column,
|
|
156
|
+
valueFormatter,
|
|
157
|
+
editable,
|
|
158
|
+
align,
|
|
159
|
+
headerAlign: align,
|
|
160
|
+
width: currentWidth ?? 150,
|
|
161
|
+
};
|
|
162
|
+
} else {
|
|
163
|
+
let { hide, calculated, ...rest } = column;
|
|
164
|
+
col = {
|
|
165
|
+
headerName: fieldDef?.caption ?? fieldName,
|
|
166
|
+
editable,
|
|
167
|
+
type: type as GridColType,
|
|
168
|
+
valueFormatter,
|
|
169
|
+
align,
|
|
170
|
+
headerAlign: align,
|
|
171
|
+
...rest,
|
|
172
|
+
width: currentWidth ?? rest.width ?? 150,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if ((operators as any)[type]) {
|
|
177
|
+
col.filterOperators = (operators as any)[type];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
* A total or group row has no record behind it — the grid generates it — so a
|
|
182
|
+
* `renderCell` that reads its value off `params.row`, which most do, has nothing to
|
|
183
|
+
* read. Left alone that reads as a broken feature in two ways: turning on a sum shows
|
|
184
|
+
* a pinned row of empty cells, and every unaggregated column renders whatever its
|
|
185
|
+
* `renderCell` makes of an empty record (a status chip saying "Open", say).
|
|
186
|
+
*
|
|
187
|
+
* So the aggregated value is rendered where there is one, nothing where there is not,
|
|
188
|
+
* and `renderCell` is left to the data rows it was written for. A column that wants to
|
|
189
|
+
* shape its total formats it with `valueFormatter`, which the grid applies to
|
|
190
|
+
* aggregation cells too.
|
|
191
|
+
*/
|
|
192
|
+
if (col.renderCell) {
|
|
193
|
+
let renderDataCell = col.renderCell;
|
|
194
|
+
col.renderCell = (params) => {
|
|
195
|
+
if (params.aggregation) return params.formattedValue ?? params.value ?? "";
|
|
196
|
+
if (isAggregationRow(params)) return "";
|
|
197
|
+
|
|
198
|
+
return renderDataCell(params);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The header filter cell is fully replaced by our own, which edits the
|
|
203
|
+
// shared filter tree instead of MUI's filter model.
|
|
204
|
+
if (col.filterable !== false && col.type !== "actions") {
|
|
205
|
+
col.renderHeaderFilter = (props) => <AfHeaderFilterCell {...props} dataObject={dataObject} />;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
muiColumns.push(col);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return muiColumns;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const GridActionItem = React.forwardRef<
|
|
216
|
+
HTMLButtonElement,
|
|
217
|
+
GridActionsCellItemProps & {
|
|
218
|
+
rowId: GridRowId;
|
|
219
|
+
mode: GridRowModes;
|
|
220
|
+
dataObject: DataObject<any>;
|
|
221
|
+
requireUpdate?: boolean;
|
|
222
|
+
requireDelete?: boolean;
|
|
223
|
+
}
|
|
224
|
+
>(function GridActionItem({ dataObject, mode, rowId, requireDelete = false, requireUpdate = false, ...props }, ref) {
|
|
225
|
+
let apiRef = useGridApiContext();
|
|
226
|
+
let rowModesModel = useGridSelector(apiRef, gridEditRowsStateSelector);
|
|
227
|
+
let { allowUpdate, allowDelete } = usePermissions(dataObject);
|
|
228
|
+
|
|
229
|
+
let currentMode = typeof rowModesModel[rowId] === "undefined" ? GridRowModes.View : GridRowModes.Edit;
|
|
230
|
+
|
|
231
|
+
if (currentMode !== mode) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (requireDelete && !allowDelete) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (requireUpdate && !allowUpdate) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return <GridActionsCellItem ref={ref} {...props} />;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
export type AfGridColumn = string | AfGridColDef;
|
|
247
|
+
const muiColumnTypes = ["string", "number", "date", "dateTime", "boolean", "singleSelect", "actions"];
|
|
248
|
+
|
|
249
|
+
export type useAfColumnsOptions = {
|
|
250
|
+
apiRef: React.RefObject<GridApi | null>;
|
|
251
|
+
disableDelete?: boolean;
|
|
252
|
+
disableEdit?: boolean;
|
|
253
|
+
dataObject: DataObject<any>;
|
|
254
|
+
columnsProp: AfGridColumn[];
|
|
255
|
+
idField: string;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export function useAfColumns({
|
|
259
|
+
apiRef,
|
|
260
|
+
dataObject,
|
|
261
|
+
disableEdit,
|
|
262
|
+
disableDelete,
|
|
263
|
+
columnsProp,
|
|
264
|
+
idField,
|
|
265
|
+
}: useAfColumnsOptions): GridColDef[] {
|
|
266
|
+
let { allowUpdate, allowDelete } = usePermissions(dataObject);
|
|
267
|
+
|
|
268
|
+
return useMemo(() => {
|
|
269
|
+
let columns = afColumnsToMuiColumn({
|
|
270
|
+
allowDelete,
|
|
271
|
+
allowUpdate,
|
|
272
|
+
apiRef,
|
|
273
|
+
dataObject,
|
|
274
|
+
disableEdit,
|
|
275
|
+
disableDelete,
|
|
276
|
+
columnsProp,
|
|
277
|
+
idField,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const handleEditClick = (id: GridRowId) => (event: React.MouseEvent<any>) => {
|
|
281
|
+
event.stopPropagation();
|
|
282
|
+
apiRef.current?.startRowEditMode({ id });
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const handleSaveClick = (id: GridRowId) => async (event: React.MouseEvent<any>) => {
|
|
286
|
+
event.stopPropagation();
|
|
287
|
+
apiRef.current?.stopRowEditMode({ id });
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const handleDeleteClick = (id: GridRowId) => async (event: React.MouseEvent<any>) => {
|
|
291
|
+
event.stopPropagation();
|
|
292
|
+
await deleteRow({ apiRef, dataObject, id, idField });
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const handleCancelClick = (id: GridRowId) => (event: React.MouseEvent<any>) => {
|
|
296
|
+
event.stopPropagation();
|
|
297
|
+
apiRef.current?.stopRowEditMode({ id, ignoreModifications: true });
|
|
298
|
+
|
|
299
|
+
const row = apiRef.current?.getRow(id);
|
|
300
|
+
if (row?.isNew) {
|
|
301
|
+
apiRef.current?.updateRows([{ [uid]: id, id, _action: "delete" } as any]);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
if (!columns.find((c) => c.type === "actions") && (allowDelete || allowUpdate)) {
|
|
306
|
+
columns.push({
|
|
307
|
+
field: "actions",
|
|
308
|
+
type: "actions",
|
|
309
|
+
headerName: getLocalizedString("Actions"),
|
|
310
|
+
getActions: ({ id }: GridRowParams) => {
|
|
311
|
+
// The new item row only exists while inserting is allowed, so its save and
|
|
312
|
+
// cancel actions do not depend on the update permission, and it has nothing
|
|
313
|
+
// to edit or delete in view mode.
|
|
314
|
+
let isNewItem = isNewItemRow(id);
|
|
315
|
+
let actions = [
|
|
316
|
+
<GridActionItem
|
|
317
|
+
key="save"
|
|
318
|
+
icon={<FloppydiskIcon aria-hidden />}
|
|
319
|
+
label={getLocalizedString("Save")}
|
|
320
|
+
onClick={handleSaveClick(id)}
|
|
321
|
+
mode={GridRowModes.Edit}
|
|
322
|
+
dataObject={dataObject}
|
|
323
|
+
rowId={id}
|
|
324
|
+
requireUpdate={!isNewItem}
|
|
325
|
+
/>,
|
|
326
|
+
<GridActionItem
|
|
327
|
+
key="revert"
|
|
328
|
+
icon={<XMarkIcon aria-hidden />}
|
|
329
|
+
label={getLocalizedString("Cancel")}
|
|
330
|
+
onClick={handleCancelClick(id)}
|
|
331
|
+
mode={GridRowModes.Edit}
|
|
332
|
+
dataObject={dataObject}
|
|
333
|
+
rowId={id}
|
|
334
|
+
requireUpdate={!isNewItem}
|
|
335
|
+
/>,
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
if (isNewItem) {
|
|
339
|
+
return actions;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return [
|
|
343
|
+
<GridActionItem
|
|
344
|
+
key="edit"
|
|
345
|
+
icon={<PencilIcon aria-hidden />}
|
|
346
|
+
label={getLocalizedString("Edit")}
|
|
347
|
+
onClick={handleEditClick(id)}
|
|
348
|
+
mode={GridRowModes.View}
|
|
349
|
+
dataObject={dataObject}
|
|
350
|
+
rowId={id}
|
|
351
|
+
requireUpdate
|
|
352
|
+
/>,
|
|
353
|
+
<GridActionItem
|
|
354
|
+
key="delete"
|
|
355
|
+
icon={<TrashIcon aria-hidden />}
|
|
356
|
+
label={getLocalizedString("Delete")}
|
|
357
|
+
onClick={handleDeleteClick(id)}
|
|
358
|
+
mode={GridRowModes.View}
|
|
359
|
+
dataObject={dataObject}
|
|
360
|
+
rowId={id}
|
|
361
|
+
requireDelete
|
|
362
|
+
/>,
|
|
363
|
+
...actions,
|
|
364
|
+
];
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return columns;
|
|
370
|
+
}, [columnsProp, disableDelete, disableEdit, allowUpdate, allowDelete, idField, dataObject, apiRef]);
|
|
371
|
+
}
|