@webtypen/webframez-core 0.3.100 → 0.3.102
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/dist/Datatable/Datatable.d.ts +17 -1
- package/dist/Datatable/Datatable.js +146 -27
- package/dist/Datatable/DatatableColumnPreferences.d.ts +40 -0
- package/dist/Datatable/DatatableColumnPreferences.js +97 -0
- package/dist/Datatable/DatatableController.d.ts +4 -1
- package/dist/Datatable/DatatableController.js +37 -20
- package/dist/Datatable/DatatablePreferencesStore.d.ts +15 -0
- package/dist/Datatable/DatatablePreferencesStore.js +59 -0
- package/dist/Datatable/DatatableSchemaColumns.d.ts +8 -0
- package/dist/Datatable/DatatableSchemaColumns.js +27 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/docs/datatables.md +128 -0
- package/package.json +3 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Request } from "../Router/Request";
|
|
2
|
+
import { DatatableColumnContext, DatatableColumnPreferencesConfig } from "./DatatableColumnPreferences";
|
|
2
3
|
export interface DatatableModelSelectorConfig {
|
|
3
4
|
value?: string;
|
|
4
5
|
label?: string;
|
|
@@ -10,6 +11,9 @@ export interface NormalizedDatatableModelSelectorConfig {
|
|
|
10
11
|
subtitle?: string;
|
|
11
12
|
}
|
|
12
13
|
export declare class Datatable {
|
|
14
|
+
static columnPreferences: DatatableColumnPreferencesConfig | false;
|
|
15
|
+
columnPreferences?: DatatableColumnPreferencesConfig | false;
|
|
16
|
+
private columnStates;
|
|
13
17
|
collection: string | Function;
|
|
14
18
|
aggregation: {
|
|
15
19
|
[key: string]: any;
|
|
@@ -49,6 +53,18 @@ export declare class Datatable {
|
|
|
49
53
|
};
|
|
50
54
|
} | null;
|
|
51
55
|
getInit(req: Request): Promise<any>;
|
|
56
|
+
private loadColumnState;
|
|
57
|
+
/** Options may be declared inline or loaded from an authenticated host callback. */
|
|
58
|
+
getColumnDisplayOptions(req: Request, key: string): Promise<any>;
|
|
59
|
+
private resolveDisplayOptions;
|
|
60
|
+
private getColumnState;
|
|
61
|
+
saveColumnPreferences(req: Request, columns: unknown): Promise<any>;
|
|
62
|
+
resetColumnPreferences(req: Request): Promise<any>;
|
|
63
|
+
/** All definitions remain available through getColumns(); this returns the user's ordered layout. */
|
|
64
|
+
getVisibleColumns(req: Request): Promise<Record<string, any>>;
|
|
65
|
+
getVisibleColumnKeys(req: Request): Promise<string[]>;
|
|
66
|
+
isColumnVisible(req: Request, key: string): Promise<boolean>;
|
|
67
|
+
getColumnContext(req: Request): Promise<DatatableColumnContext>;
|
|
52
68
|
getModelSelectorConfig(): NormalizedDatatableModelSelectorConfig | null;
|
|
53
69
|
getModelSelectorPreview(req: Request): Promise<any>;
|
|
54
70
|
getSelectableFunctionsDef(req: Request): Promise<{
|
|
@@ -62,7 +78,7 @@ export declare class Datatable {
|
|
|
62
78
|
getAggregation(req: Request): Promise<any>;
|
|
63
79
|
getSubAggregation(req: Request): Promise<any>;
|
|
64
80
|
getCollection(req: Request): Promise<any>;
|
|
65
|
-
getData(req: Request): Promise<{
|
|
81
|
+
getData(req: Request, context?: DatatableColumnContext): Promise<{
|
|
66
82
|
page: number;
|
|
67
83
|
max_entries: any;
|
|
68
84
|
total_pages: number;
|
|
@@ -27,8 +27,11 @@ exports.Datatable = void 0;
|
|
|
27
27
|
const moment_1 = __importDefault(require("moment"));
|
|
28
28
|
const DBConnection_1 = require("../Database/DBConnection");
|
|
29
29
|
const NumericFunctions_1 = require("../Functions/NumericFunctions");
|
|
30
|
+
const DatatableColumnPreferences_1 = require("./DatatableColumnPreferences");
|
|
31
|
+
const DatatablePreferencesStore_1 = require("./DatatablePreferencesStore");
|
|
30
32
|
class Datatable {
|
|
31
33
|
constructor() {
|
|
34
|
+
this.columnStates = new WeakMap();
|
|
32
35
|
this.collection = "";
|
|
33
36
|
this.aggregation = null;
|
|
34
37
|
this.subAggregation = null;
|
|
@@ -57,26 +60,19 @@ class Datatable {
|
|
|
57
60
|
if (modelSelector) {
|
|
58
61
|
out.modelSelector = modelSelector;
|
|
59
62
|
}
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
})));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
out.columns = cols;
|
|
74
|
-
resolve(true);
|
|
75
|
-
})));
|
|
76
|
-
promises.push(new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
|
|
77
|
-
out.onPressLink = yield this.getOnPressLink(req);
|
|
78
|
-
resolve(true);
|
|
79
|
-
})));
|
|
63
|
+
const [filter, state, onPressLink] = yield Promise.all([
|
|
64
|
+
this.getFilter(req), this.getColumnState(req), this.getOnPressLink(req),
|
|
65
|
+
]);
|
|
66
|
+
out.filter = filter;
|
|
67
|
+
out.columns = Object.entries(state.visible).map(([key, column]) => (Object.assign(Object.assign({}, column), { displayOptions: typeof column.displayOptions === "function" ? true : column.displayOptions, key })));
|
|
68
|
+
out.availableColumns = Object.entries(state.all).map(([key, column]) => (Object.assign(Object.assign({}, column), { displayOptions: typeof column.displayOptions === "function" ? true : column.displayOptions, key })));
|
|
69
|
+
out.columnPreferences = Boolean(state.scope) && Object.keys(state.all).length > 0;
|
|
70
|
+
out.columnSelection = out.columns.map((column) => {
|
|
71
|
+
var _a;
|
|
72
|
+
const saved = (_a = state.preferences) === null || _a === void 0 ? void 0 : _a.find((entry) => entry.key === column.key);
|
|
73
|
+
return Object.assign(Object.assign(Object.assign({ key: column.key }, ((saved === null || saved === void 0 ? void 0 : saved.width) ? { width: saved.width } : {})), ((saved === null || saved === void 0 ? void 0 : saved.title) ? { title: saved.title } : {})), ((saved === null || saved === void 0 ? void 0 : saved.displayField) && saved.displayField === column.displayField ? { displayField: saved.displayField } : {}));
|
|
74
|
+
});
|
|
75
|
+
out.onPressLink = onPressLink;
|
|
80
76
|
if (this.exports) {
|
|
81
77
|
out.export_definitions = [];
|
|
82
78
|
for (let key in this.exports) {
|
|
@@ -87,13 +83,134 @@ class Datatable {
|
|
|
87
83
|
});
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
|
-
yield Promise.all(promises);
|
|
91
86
|
if (typeof this.onInit === "function") {
|
|
92
87
|
yield this.onInit(req, out);
|
|
93
88
|
}
|
|
94
89
|
return out;
|
|
95
90
|
});
|
|
96
91
|
}
|
|
92
|
+
loadColumnState(req) {
|
|
93
|
+
var _a, _b;
|
|
94
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
95
|
+
const all = (_a = yield this.getColumns(req)) !== null && _a !== void 0 ? _a : {};
|
|
96
|
+
const config = (_b = this.columnPreferences) !== null && _b !== void 0 ? _b : Datatable.columnPreferences;
|
|
97
|
+
const store = new DatatablePreferencesStore_1.DatatablePreferencesStore(config || {});
|
|
98
|
+
const scope = config === false ? null : yield store.scope(req);
|
|
99
|
+
const preferences = scope ? yield store.load(scope) : null;
|
|
100
|
+
const savedKeys = Array.isArray(preferences) ? preferences.map(entry => entry === null || entry === void 0 ? void 0 : entry.key).filter(key => Object.prototype.hasOwnProperty.call(all, key)) : [];
|
|
101
|
+
const selected = savedKeys.length ? savedKeys : Object.keys(all).filter(key => !all[key].hidden);
|
|
102
|
+
const resolved = yield this.resolveDisplayOptions(req, all, selected);
|
|
103
|
+
return { all, visible: DatatableColumnPreferences_1.DatatableColumnPreferences.apply(resolved, preferences), preferences, store, scope };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Options may be declared inline or loaded from an authenticated host callback. */
|
|
107
|
+
getColumnDisplayOptions(req, key) {
|
|
108
|
+
var _a;
|
|
109
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
110
|
+
const all = (_a = yield this.getColumns(req)) !== null && _a !== void 0 ? _a : {};
|
|
111
|
+
if (typeof key !== "string" || !Object.prototype.hasOwnProperty.call(all, key)) {
|
|
112
|
+
throw Object.assign(new Error("Unbekannte Spalte."), { status: 400 });
|
|
113
|
+
}
|
|
114
|
+
const resolved = yield this.resolveDisplayOptions(req, all, [key]);
|
|
115
|
+
return resolved[key].displayOptions || [];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
resolveDisplayOptions(req, columns, keys) {
|
|
119
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
120
|
+
const resolved = Object.assign({}, columns);
|
|
121
|
+
yield Promise.all(Array.from(new Set(keys)).filter(key => typeof key === "string" && Object.prototype.hasOwnProperty.call(columns, key)).map((key) => __awaiter(this, void 0, void 0, function* () {
|
|
122
|
+
const column = columns[key];
|
|
123
|
+
if (typeof column.displayOptions === "function") {
|
|
124
|
+
const options = yield column.displayOptions(req, key);
|
|
125
|
+
resolved[key] = Object.assign(Object.assign({}, column), { displayOptions: Array.isArray(options) ? options : [] });
|
|
126
|
+
}
|
|
127
|
+
})));
|
|
128
|
+
return resolved;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
getColumnState(req) {
|
|
132
|
+
let state = this.columnStates.get(req);
|
|
133
|
+
if (!state) {
|
|
134
|
+
state = this.loadColumnState(req);
|
|
135
|
+
this.columnStates.set(req, state);
|
|
136
|
+
}
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
saveColumnPreferences(req, columns) {
|
|
140
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
const state = yield this.getColumnState(req);
|
|
142
|
+
if (!state.scope)
|
|
143
|
+
throw Object.assign(new Error("Spalteneinstellungen sind für diesen Benutzer nicht verfügbar."), { status: 403 });
|
|
144
|
+
let preferences;
|
|
145
|
+
try {
|
|
146
|
+
const resolved = yield this.resolveDisplayOptions(req, state.all, Array.isArray(columns) ? columns.map(entry => entry === null || entry === void 0 ? void 0 : entry.key) : []);
|
|
147
|
+
preferences = DatatableColumnPreferences_1.DatatableColumnPreferences.validate(columns, resolved);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
throw Object.assign(error, { status: 400 });
|
|
151
|
+
}
|
|
152
|
+
yield state.store.save(state.scope, preferences);
|
|
153
|
+
this.columnStates.delete(req);
|
|
154
|
+
return this.getInit(req);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
resetColumnPreferences(req) {
|
|
158
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
159
|
+
const state = yield this.getColumnState(req);
|
|
160
|
+
if (!state.scope)
|
|
161
|
+
throw Object.assign(new Error("Spalteneinstellungen sind für diesen Benutzer nicht verfügbar."), { status: 403 });
|
|
162
|
+
yield state.store.reset(state.scope);
|
|
163
|
+
this.columnStates.delete(req);
|
|
164
|
+
return this.getInit(req);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** All definitions remain available through getColumns(); this returns the user's ordered layout. */
|
|
168
|
+
getVisibleColumns(req) {
|
|
169
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
170
|
+
return (yield this.getColumnState(req)).visible;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
getVisibleColumnKeys(req) {
|
|
174
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
175
|
+
return Object.keys(yield this.getVisibleColumns(req));
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
isColumnVisible(req, key) {
|
|
179
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
180
|
+
return Object.prototype.hasOwnProperty.call(yield this.getVisibleColumns(req), key);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
getColumnContext(req) {
|
|
184
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
185
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
186
|
+
const { all, visible: columns } = yield this.getColumnState(req);
|
|
187
|
+
const columnKeys = Object.keys(columns);
|
|
188
|
+
const filters = (_a = yield this.getFilter(req)) !== null && _a !== void 0 ? _a : {};
|
|
189
|
+
const required = new Set(columnKeys);
|
|
190
|
+
for (const key of columnKeys)
|
|
191
|
+
required.add((_b = columns[key].mapping) !== null && _b !== void 0 ? _b : key);
|
|
192
|
+
for (const key of Object.keys((_d = (_c = req.body) === null || _c === void 0 ? void 0 : _c.sort) !== null && _d !== void 0 ? _d : {}))
|
|
193
|
+
required.add(key);
|
|
194
|
+
for (const key of Object.keys((_f = (_e = req.body) === null || _e === void 0 ? void 0 : _e.search) !== null && _f !== void 0 ? _f : {}))
|
|
195
|
+
required.add((_h = (_g = all[key]) === null || _g === void 0 ? void 0 : _g.mapping) !== null && _h !== void 0 ? _h : key);
|
|
196
|
+
for (const key of Object.keys((_k = (_j = req.body) === null || _j === void 0 ? void 0 : _j.filter) !== null && _k !== void 0 ? _k : {}))
|
|
197
|
+
required.add((_m = (_l = filters[key]) === null || _l === void 0 ? void 0 : _l.mapping) !== null && _m !== void 0 ? _m : key);
|
|
198
|
+
for (const [key, sum] of Object.entries((_p = (_o = req.body) === null || _o === void 0 ? void 0 : _o.sums) !== null && _p !== void 0 ? _p : {}))
|
|
199
|
+
required.add((_q = sum === null || sum === void 0 ? void 0 : sum.mapping) !== null && _q !== void 0 ? _q : key);
|
|
200
|
+
if (((_r = req.body) === null || _r === void 0 ? void 0 : _r.mode) === "model-selector-preview") {
|
|
201
|
+
const selector = this.getModelSelectorConfig();
|
|
202
|
+
for (const field of [selector === null || selector === void 0 ? void 0 : selector.value, selector === null || selector === void 0 ? void 0 : selector.label, selector === null || selector === void 0 ? void 0 : selector.subtitle])
|
|
203
|
+
if (field)
|
|
204
|
+
required.add(field);
|
|
205
|
+
}
|
|
206
|
+
const matches = (field) => Array.from(required).some(key => key === field || key.startsWith(field + ".") || field.startsWith(key + "."));
|
|
207
|
+
return {
|
|
208
|
+
columns, columnKeys,
|
|
209
|
+
isColumnVisible: key => Object.prototype.hasOwnProperty.call(columns, key),
|
|
210
|
+
isColumnRequired: key => { var _a; return matches(key) || (typeof ((_a = all[key]) === null || _a === void 0 ? void 0 : _a.mapping) === "string" && matches(all[key].mapping)); },
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
97
214
|
getModelSelectorConfig() {
|
|
98
215
|
if (!this.modelSelector) {
|
|
99
216
|
return null;
|
|
@@ -198,7 +315,7 @@ class Datatable {
|
|
|
198
315
|
if (this.defaultUnwind && this.defaultUnwind.trim() !== "") {
|
|
199
316
|
prefix.push({ $unwind: this.defaultUnwind });
|
|
200
317
|
}
|
|
201
|
-
const aggr = typeof this.aggregation === "function" ? yield this.aggregation(req) : this.aggregation;
|
|
318
|
+
const aggr = typeof this.aggregation === "function" ? yield this.aggregation(req, yield this.getColumnContext(req)) : this.aggregation;
|
|
202
319
|
if (this.autoApplyFilter && req.body.filter && typeof req.body.filter === "object") {
|
|
203
320
|
const filterMatch = yield this.getFilterMatch(req, req.body.filter);
|
|
204
321
|
if (this.autoApplyFilter === "begin") {
|
|
@@ -233,7 +350,7 @@ class Datatable {
|
|
|
233
350
|
}
|
|
234
351
|
getSubAggregation(req) {
|
|
235
352
|
return __awaiter(this, void 0, void 0, function* () {
|
|
236
|
-
return typeof this.subAggregation === "function" ? yield this.subAggregation(req) : this.subAggregation;
|
|
353
|
+
return typeof this.subAggregation === "function" ? yield this.subAggregation(req, yield this.getColumnContext(req)) : this.subAggregation;
|
|
237
354
|
});
|
|
238
355
|
}
|
|
239
356
|
getCollection(req) {
|
|
@@ -244,8 +361,9 @@ class Datatable {
|
|
|
244
361
|
return this.collection;
|
|
245
362
|
});
|
|
246
363
|
}
|
|
247
|
-
getData(req) {
|
|
364
|
+
getData(req, context) {
|
|
248
365
|
return __awaiter(this, void 0, void 0, function* () {
|
|
366
|
+
const columns = context !== null && context !== void 0 ? context : yield this.getColumnContext(req);
|
|
249
367
|
const page = req.body.page && parseInt(req.body.page) > 0 ? parseInt(req.body.page) : 0;
|
|
250
368
|
const aggregation = [];
|
|
251
369
|
const aggregationStats = [];
|
|
@@ -332,10 +450,10 @@ class Datatable {
|
|
|
332
450
|
if (this.onRow || this.onAttributes) {
|
|
333
451
|
for (let i in results) {
|
|
334
452
|
if (this.onRow) {
|
|
335
|
-
results[i] = yield this.onRow(results[i]);
|
|
453
|
+
results[i] = yield this.onRow(results[i], req, columns);
|
|
336
454
|
}
|
|
337
455
|
if (this.onAttributes) {
|
|
338
|
-
results[i].__cellattr = yield this.onAttributes(results[i]);
|
|
456
|
+
results[i].__cellattr = yield this.onAttributes(results[i], req, columns);
|
|
339
457
|
}
|
|
340
458
|
}
|
|
341
459
|
}
|
|
@@ -381,7 +499,7 @@ class Datatable {
|
|
|
381
499
|
sums: sums,
|
|
382
500
|
};
|
|
383
501
|
if (typeof this.onData === "function") {
|
|
384
|
-
yield this.onData(req, data);
|
|
502
|
+
yield this.onData(req, data, columns);
|
|
385
503
|
}
|
|
386
504
|
return data;
|
|
387
505
|
});
|
|
@@ -708,3 +826,4 @@ class Datatable {
|
|
|
708
826
|
}
|
|
709
827
|
}
|
|
710
828
|
exports.Datatable = Datatable;
|
|
829
|
+
Datatable.columnPreferences = {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DatabaseId } from "../Database/DatabaseAdapter";
|
|
2
|
+
import type { Request } from "../Router/Request";
|
|
3
|
+
export type DatatableColumnWidth = {
|
|
4
|
+
mode: "auto";
|
|
5
|
+
} | {
|
|
6
|
+
mode: "px" | "percent";
|
|
7
|
+
value: number;
|
|
8
|
+
};
|
|
9
|
+
export interface DatatableColumnPreference {
|
|
10
|
+
key: string;
|
|
11
|
+
/** Omit to retain the current backend default. */
|
|
12
|
+
width?: DatatableColumnWidth;
|
|
13
|
+
title?: string;
|
|
14
|
+
displayField?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DatatableDisplayOption {
|
|
17
|
+
value: string;
|
|
18
|
+
label: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
plain?: boolean;
|
|
21
|
+
options?: any[];
|
|
22
|
+
}
|
|
23
|
+
export interface DatatableColumnPreferencesConfig {
|
|
24
|
+
collection?: string;
|
|
25
|
+
connection?: string;
|
|
26
|
+
/** Resolve exclusively from authenticated server context. Null disables persistence for this request. */
|
|
27
|
+
owner?: (req: Request) => Record<string, DatabaseId> | null | Promise<Record<string, DatabaseId> | null>;
|
|
28
|
+
}
|
|
29
|
+
export interface DatatableColumnContext {
|
|
30
|
+
columns: Record<string, any>;
|
|
31
|
+
columnKeys: string[];
|
|
32
|
+
isColumnVisible: (key: string) => boolean;
|
|
33
|
+
/** Includes visible mappings and fields used by filters, search, sorting or sums. */
|
|
34
|
+
isColumnRequired: (key: string) => boolean;
|
|
35
|
+
}
|
|
36
|
+
export declare class DatatableColumnPreferences {
|
|
37
|
+
static validate(value: unknown, columns: Record<string, any>): DatatableColumnPreference[];
|
|
38
|
+
static display(definition: any, selected?: string): any;
|
|
39
|
+
static apply(columns: Record<string, any>, preferences: unknown): Record<string, any>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatatableColumnPreferences = void 0;
|
|
4
|
+
class DatatableColumnPreferences {
|
|
5
|
+
static validate(value, columns) {
|
|
6
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > Object.keys(columns).length) {
|
|
7
|
+
throw new Error("Bitte mindestens eine gültige Spalte auswählen.");
|
|
8
|
+
}
|
|
9
|
+
const seen = new Set();
|
|
10
|
+
return value.map(entry => {
|
|
11
|
+
if (!entry || typeof entry.key !== "string" || !Object.prototype.hasOwnProperty.call(columns, entry.key) || seen.has(entry.key)) {
|
|
12
|
+
throw new Error("Unbekannte oder doppelte Spalte.");
|
|
13
|
+
}
|
|
14
|
+
seen.add(entry.key);
|
|
15
|
+
const preference = { key: entry.key };
|
|
16
|
+
if (entry.title !== undefined) {
|
|
17
|
+
if (typeof entry.title !== "string" || entry.title.length > 160)
|
|
18
|
+
throw new Error("Ungültiger Spaltentitel (maximal 160 Zeichen).");
|
|
19
|
+
if (entry.title.trim())
|
|
20
|
+
preference.title = entry.title.trim();
|
|
21
|
+
}
|
|
22
|
+
if (entry.displayField !== undefined && entry.displayField !== "") {
|
|
23
|
+
if (typeof entry.displayField !== "string" || !Array.isArray(columns[entry.key].displayOptions)
|
|
24
|
+
|| !columns[entry.key].displayOptions.some((option) => option.value === entry.displayField)) {
|
|
25
|
+
throw new Error("Unzulässiges Anzeigefeld für diese Spalte.");
|
|
26
|
+
}
|
|
27
|
+
preference.displayField = entry.displayField;
|
|
28
|
+
}
|
|
29
|
+
if (entry.width !== undefined) {
|
|
30
|
+
const width = entry.width;
|
|
31
|
+
if ((width === null || width === void 0 ? void 0 : width.mode) === "auto") {
|
|
32
|
+
preference.width = { mode: "auto" };
|
|
33
|
+
}
|
|
34
|
+
else if (((width === null || width === void 0 ? void 0 : width.mode) === "px" || (width === null || width === void 0 ? void 0 : width.mode) === "percent") && typeof width.value === "number"
|
|
35
|
+
&& Number.isFinite(width.value) && width.value > 0 && width.value <= (width.mode === "percent" ? 100 : 10000)) {
|
|
36
|
+
preference.width = { mode: width.mode, value: width.value };
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error("Ungültige Spaltenbreite.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return preference;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
static display(definition, selected) {
|
|
46
|
+
const column = Object.assign({}, definition);
|
|
47
|
+
const options = Array.isArray(column.displayOptions) ? column.displayOptions : [];
|
|
48
|
+
const option = options.find(entry => entry.value === (selected || column.displayField)) || options[0];
|
|
49
|
+
if (option) {
|
|
50
|
+
column.displayField = option.value;
|
|
51
|
+
for (const key of ["type", "plain", "options"])
|
|
52
|
+
if (option[key] !== undefined)
|
|
53
|
+
column[key] = option[key];
|
|
54
|
+
}
|
|
55
|
+
return column;
|
|
56
|
+
}
|
|
57
|
+
static apply(columns, preferences) {
|
|
58
|
+
const defaults = () => Object.fromEntries(Object.entries(columns).filter(([, column]) => column.hidden !== true).map(([key, column]) => [key, this.display(column)]));
|
|
59
|
+
if (!Array.isArray(preferences))
|
|
60
|
+
return defaults();
|
|
61
|
+
const available = preferences.filter(entry => entry && typeof entry.key === "string" && Object.prototype.hasOwnProperty.call(columns, entry.key)).map(entry => {
|
|
62
|
+
const options = columns[entry.key].displayOptions;
|
|
63
|
+
// Removed target fields fall back independently; retain the rest of the layout.
|
|
64
|
+
return entry.displayField && (!Array.isArray(options) || !options.some((option) => option.value === entry.displayField))
|
|
65
|
+
? Object.assign(Object.assign({}, entry), { displayField: undefined }) : entry;
|
|
66
|
+
});
|
|
67
|
+
if (!available.length)
|
|
68
|
+
return defaults();
|
|
69
|
+
let validated;
|
|
70
|
+
try {
|
|
71
|
+
validated = this.validate(available, columns);
|
|
72
|
+
}
|
|
73
|
+
catch (_a) {
|
|
74
|
+
return defaults();
|
|
75
|
+
}
|
|
76
|
+
return Object.fromEntries(validated.map(preference => {
|
|
77
|
+
const column = this.display(columns[preference.key], preference.displayField);
|
|
78
|
+
if (preference.title) {
|
|
79
|
+
column.title = preference.title;
|
|
80
|
+
column.label = preference.title;
|
|
81
|
+
}
|
|
82
|
+
if (preference.width) {
|
|
83
|
+
delete column.width;
|
|
84
|
+
delete column.minWidth;
|
|
85
|
+
delete column.maxWidth;
|
|
86
|
+
column.autogrow = preference.width.mode === "auto";
|
|
87
|
+
column.grow = preference.width.mode === "auto" ? 1 : 0;
|
|
88
|
+
if (preference.width.mode === "px")
|
|
89
|
+
column.width = preference.width.value;
|
|
90
|
+
if (preference.width.mode === "percent")
|
|
91
|
+
column.width = `${preference.width.value}%`;
|
|
92
|
+
}
|
|
93
|
+
return [preference.key, column];
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.DatatableColumnPreferences = DatatableColumnPreferences;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Controller } from "../Controller/Controller";
|
|
2
2
|
import { Request } from "../Router/Request";
|
|
3
3
|
import { Response } from "../Router/Response";
|
|
4
|
+
import { Datatable } from "./Datatable";
|
|
4
5
|
export declare class DatatableController extends Controller {
|
|
5
|
-
|
|
6
|
+
protected getTableClass(req: Request): any;
|
|
7
|
+
protected configureTable(table: Datatable, req: Request): void | Promise<void>;
|
|
8
|
+
restApi(req: Request, res: Response): Promise<Response | undefined>;
|
|
6
9
|
tableExport(req: Request, res: Response): Promise<Response>;
|
|
7
10
|
}
|
|
@@ -13,36 +13,49 @@ exports.DatatableController = void 0;
|
|
|
13
13
|
const Controller_1 = require("../Controller/Controller");
|
|
14
14
|
const DatatableRegistry_1 = require("./DatatableRegistry");
|
|
15
15
|
class DatatableController extends Controller_1.Controller {
|
|
16
|
+
getTableClass(req) {
|
|
17
|
+
return DatatableRegistry_1.DatatableRegistry.getTable(req.body._table);
|
|
18
|
+
}
|
|
19
|
+
configureTable(table, req) { }
|
|
16
20
|
restApi(req, res) {
|
|
17
|
-
var _a;
|
|
21
|
+
var _a, _b, _c;
|
|
18
22
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19
|
-
if (
|
|
23
|
+
if (typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a._table) !== "string" || req.body._table.trim() === "") {
|
|
20
24
|
return res.status(404).send({
|
|
21
25
|
status: "error",
|
|
22
26
|
message: "Missing table-key ...",
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
|
-
const tableClass =
|
|
29
|
+
const tableClass = this.getTableClass(req);
|
|
26
30
|
if (!tableClass) {
|
|
27
31
|
return res.status(404).send({
|
|
28
32
|
status: "error",
|
|
29
33
|
message: "Table '" + req.body._table + "' not found ...",
|
|
30
34
|
});
|
|
31
35
|
}
|
|
32
|
-
if (req.body.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
});
|
|
36
|
+
if (req.body.export)
|
|
37
|
+
return this.tableExport(req, res);
|
|
38
|
+
const table = new tableClass();
|
|
39
|
+
yield this.configureTable(table, req);
|
|
40
|
+
if (req.body.init_request || req.body.mode === "columns-get") {
|
|
41
|
+
return res.send({ status: "success", data: yield table.getInit(req) });
|
|
38
42
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
if (["columns-save", "columns-reset", "columns-options"].includes(req.body.mode)) {
|
|
44
|
+
try {
|
|
45
|
+
const data = req.body.mode === "columns-options"
|
|
46
|
+
? yield table.getColumnDisplayOptions(req, req.body.column)
|
|
47
|
+
: req.body.mode === "columns-save"
|
|
48
|
+
? yield table.saveColumnPreferences(req, req.body.columns)
|
|
49
|
+
: yield table.resetColumnPreferences(req);
|
|
50
|
+
return res.send({ status: "success", data });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const status = error === null || error === void 0 ? void 0 : error.status;
|
|
54
|
+
if (status !== 400 && status !== 403)
|
|
55
|
+
throw error;
|
|
56
|
+
return res.status(status).send({ status: "error", message: error.message });
|
|
57
|
+
}
|
|
44
58
|
}
|
|
45
|
-
const table = new tableClass();
|
|
46
59
|
if (req.body.mode === "model-selector-preview") {
|
|
47
60
|
if (!table.getModelSelectorConfig()) {
|
|
48
61
|
return res.status(403).send({
|
|
@@ -62,7 +75,7 @@ class DatatableController extends Controller_1.Controller {
|
|
|
62
75
|
});
|
|
63
76
|
}
|
|
64
77
|
if (req.body.apiFunction && typeof req.body.apiFunction === "string" && req.body.apiFunction.trim() !== "") {
|
|
65
|
-
const func = (
|
|
78
|
+
const func = (_b = table.selectableFunctions) === null || _b === void 0 ? void 0 : _b[req.body.apiFunction];
|
|
66
79
|
if (!func || !func.handle) {
|
|
67
80
|
return res.status(404).send({
|
|
68
81
|
status: "error",
|
|
@@ -96,25 +109,28 @@ class DatatableController extends Controller_1.Controller {
|
|
|
96
109
|
message: e instanceof Error ? e.message : "Unknown error ...",
|
|
97
110
|
});
|
|
98
111
|
}
|
|
112
|
+
if (res.content !== undefined || ((_c = res.res) === null || _c === void 0 ? void 0 : _c.headersSent) || res.headersSent || res.writableEnded)
|
|
113
|
+
return;
|
|
99
114
|
return res.send({
|
|
100
115
|
status: "success",
|
|
101
116
|
});
|
|
102
117
|
}
|
|
103
118
|
return res.send({
|
|
104
119
|
status: "success",
|
|
105
|
-
data: yield table.getData(req),
|
|
120
|
+
data: yield table.getData(req, yield table.getColumnContext(req)),
|
|
106
121
|
});
|
|
107
122
|
});
|
|
108
123
|
}
|
|
109
124
|
tableExport(req, res) {
|
|
125
|
+
var _a;
|
|
110
126
|
return __awaiter(this, void 0, void 0, function* () {
|
|
111
|
-
if (
|
|
127
|
+
if (typeof ((_a = req.body) === null || _a === void 0 ? void 0 : _a._table) !== "string" || req.body._table.trim() === "") {
|
|
112
128
|
return res.status(404).send({
|
|
113
129
|
status: "error",
|
|
114
130
|
message: "Missing table-key ...",
|
|
115
131
|
});
|
|
116
132
|
}
|
|
117
|
-
const tableClass =
|
|
133
|
+
const tableClass = this.getTableClass(req);
|
|
118
134
|
if (!tableClass) {
|
|
119
135
|
return res.status(404).send({
|
|
120
136
|
status: "error",
|
|
@@ -122,6 +138,7 @@ class DatatableController extends Controller_1.Controller {
|
|
|
122
138
|
});
|
|
123
139
|
}
|
|
124
140
|
const table = new tableClass();
|
|
141
|
+
yield this.configureTable(table, req);
|
|
125
142
|
if (!req.body.export || !table.exports || (table.exports && !table.exports[req.body.export])) {
|
|
126
143
|
return res.status(404).send({
|
|
127
144
|
status: "error",
|
|
@@ -130,7 +147,7 @@ class DatatableController extends Controller_1.Controller {
|
|
|
130
147
|
}
|
|
131
148
|
const exp = table.exports[req.body.export];
|
|
132
149
|
const cols = [];
|
|
133
|
-
const colsData = yield table.
|
|
150
|
+
const colsData = yield table.getVisibleColumns(req);
|
|
134
151
|
if (colsData && Object.keys(colsData).length > 0) {
|
|
135
152
|
for (let i in colsData) {
|
|
136
153
|
cols.push(Object.assign(Object.assign({}, colsData[i]), { key: i }));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Request } from "../Router/Request";
|
|
2
|
+
import type { DatatableColumnPreference, DatatableColumnPreferencesConfig } from "./DatatableColumnPreferences";
|
|
3
|
+
/** Document-store adapter for per-owner table layouts. The deterministic ID makes first saves atomic. */
|
|
4
|
+
export declare class DatatablePreferencesStore {
|
|
5
|
+
private config;
|
|
6
|
+
constructor(config?: DatatableColumnPreferencesConfig);
|
|
7
|
+
scope(req: Request): Promise<{
|
|
8
|
+
table: any;
|
|
9
|
+
_id: string;
|
|
10
|
+
} | null>;
|
|
11
|
+
private collection;
|
|
12
|
+
load(scope: Record<string, any>): Promise<any>;
|
|
13
|
+
save(scope: Record<string, any>, columns: DatatableColumnPreference[]): Promise<void>;
|
|
14
|
+
reset(scope: Record<string, any>): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DatatablePreferencesStore = void 0;
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
const DBConnection_1 = require("../Database/DBConnection");
|
|
15
|
+
/** Document-store adapter for per-owner table layouts. The deterministic ID makes first saves atomic. */
|
|
16
|
+
class DatatablePreferencesStore {
|
|
17
|
+
constructor(config = {}) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
scope(req) {
|
|
21
|
+
var _a, _b, _c, _d;
|
|
22
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23
|
+
const user = (_b = (_a = req.auth) === null || _a === void 0 ? void 0 : _a.user) !== null && _b !== void 0 ? _b : req.user;
|
|
24
|
+
const userId = user === null || user === void 0 ? void 0 : user[(_c = user.__primaryKey) !== null && _c !== void 0 ? _c : "_id"];
|
|
25
|
+
const owner = this.config.owner ? yield this.config.owner(req) : userId !== undefined && userId !== null ? { _user: userId } : null;
|
|
26
|
+
if (!owner || !Object.keys(owner).length || typeof ((_d = req.body) === null || _d === void 0 ? void 0 : _d._table) !== "string" || !req.body._table.trim())
|
|
27
|
+
return null;
|
|
28
|
+
if (Object.entries(owner).some(([key, value]) => ["_id", "table", "columns", "updated_at"].includes(key) || value === null || value === undefined)) {
|
|
29
|
+
throw new Error("Invalid datatable preference owner.");
|
|
30
|
+
}
|
|
31
|
+
const identity = Object.keys(owner).sort().map(key => [key, typeof owner[key] === "number" ? "number" : "id", String(owner[key])]);
|
|
32
|
+
const id = (0, crypto_1.createHash)("sha256").update(JSON.stringify([req.body._table, identity])).digest("hex");
|
|
33
|
+
return Object.assign(Object.assign({}, owner), { table: req.body._table, _id: id });
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
collection() {
|
|
37
|
+
var _a;
|
|
38
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
39
|
+
return (yield DBConnection_1.DBConnection.getDocumentStore(this.config.connection)).collection((_a = this.config.collection) !== null && _a !== void 0 ? _a : "users_datatables");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
load(scope) {
|
|
43
|
+
var _a, _b;
|
|
44
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
45
|
+
return (_b = (_a = (yield (yield this.collection()).findOne(scope))) === null || _a === void 0 ? void 0 : _a.columns) !== null && _b !== void 0 ? _b : null;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
save(scope, columns) {
|
|
49
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
yield (yield this.collection()).updateOne(scope, { $set: { columns, updated_at: new Date() } }, { upsert: true });
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
reset(scope) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
yield (yield this.collection()).deleteOne(scope);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.DatatablePreferencesStore = DatatablePreferencesStore;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Converts DataBuilder/DataSchema field definitions into an extensible column catalog. */
|
|
2
|
+
export declare class DatatableSchemaColumns {
|
|
3
|
+
static fromSchema(schema: Record<string, any>, options?: {
|
|
4
|
+
prefix?: string;
|
|
5
|
+
labelPrefix?: string;
|
|
6
|
+
prepare?: (column: Record<string, any>, field: any, path: string) => Record<string, any> | null;
|
|
7
|
+
}): Record<string, any>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DatatableSchemaColumns = void 0;
|
|
4
|
+
/** Converts DataBuilder/DataSchema field definitions into an extensible column catalog. */
|
|
5
|
+
class DatatableSchemaColumns {
|
|
6
|
+
static fromSchema(schema, options = {}) {
|
|
7
|
+
const columns = {};
|
|
8
|
+
for (const [key, field] of Object.entries(schema || {})) {
|
|
9
|
+
if (!field || /[.$]/.test(key) || ["__proto__", "constructor", "prototype"].includes(key)
|
|
10
|
+
|| field.secret || field.writeOnly || field.type === "password")
|
|
11
|
+
continue;
|
|
12
|
+
const mapping = options.prefix ? `${options.prefix}.${key}` : key;
|
|
13
|
+
const label = [options.labelPrefix, field.label || key].filter(Boolean).join(" · ");
|
|
14
|
+
if (field.type === "object") {
|
|
15
|
+
Object.assign(columns, this.fromSchema(field.schema, Object.assign(Object.assign({}, options), { prefix: mapping, labelPrefix: label })));
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const types = { integer: "number", float: "number", number: "number", boolean: "boolean", date: "date", datetime: "datetime", option: "option", currency_cent: "currency", currency: "currency" };
|
|
19
|
+
const column = Object.assign(Object.assign({ label, mapping, type: types[field.type] || "string", width: 170, hidden: true, sortable: !["array", "file", "filemanager_file", "relation"].includes(field.type) && !field.multi }, (field.type === "currency" ? { plain: true } : {})), (field.options ? { options: field.options } : {}));
|
|
20
|
+
const prepared = options.prepare ? options.prepare(column, field, mapping) : column;
|
|
21
|
+
if (prepared)
|
|
22
|
+
columns[mapping] = prepared;
|
|
23
|
+
}
|
|
24
|
+
return columns;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.DatatableSchemaColumns = DatatableSchemaColumns;
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,9 @@ export * from "./Facades/Storage";
|
|
|
28
28
|
export * from "./DataBuilder/DataBuilder";
|
|
29
29
|
export * from "./DataBuilder/DataBuilderController";
|
|
30
30
|
export * from "./Datatable/Datatable";
|
|
31
|
+
export * from "./Datatable/DatatableColumnPreferences";
|
|
32
|
+
export * from "./Datatable/DatatableSchemaColumns";
|
|
33
|
+
export * from "./Datatable/DatatablePreferencesStore";
|
|
31
34
|
export * from "./Datatable/DatatableRegistry";
|
|
32
35
|
export * from "./Datatable/DatatableController";
|
|
33
36
|
export * from "./Queue/BaseQueueJob";
|
package/dist/index.js
CHANGED
|
@@ -45,6 +45,9 @@ __exportStar(require("./Facades/Storage"), exports);
|
|
|
45
45
|
__exportStar(require("./DataBuilder/DataBuilder"), exports);
|
|
46
46
|
__exportStar(require("./DataBuilder/DataBuilderController"), exports);
|
|
47
47
|
__exportStar(require("./Datatable/Datatable"), exports);
|
|
48
|
+
__exportStar(require("./Datatable/DatatableColumnPreferences"), exports);
|
|
49
|
+
__exportStar(require("./Datatable/DatatableSchemaColumns"), exports);
|
|
50
|
+
__exportStar(require("./Datatable/DatatablePreferencesStore"), exports);
|
|
48
51
|
__exportStar(require("./Datatable/DatatableRegistry"), exports);
|
|
49
52
|
__exportStar(require("./Datatable/DatatableController"), exports);
|
|
50
53
|
__exportStar(require("./Queue/BaseQueueJob"), exports);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Datatable column preferences
|
|
2
|
+
|
|
3
|
+
`DatatableController@restApi` automatically restores the authenticated user's
|
|
4
|
+
column order, visibility and widths. `getColumns(req)` still returns all columns
|
|
5
|
+
allowed by the table definition. A column with `hidden: true` starts hidden and
|
|
6
|
+
can be added in the column editor. Hiding a column is presentation state, never
|
|
7
|
+
an authorization mechanism; omit unauthorized columns from `columns(req)`.
|
|
8
|
+
|
|
9
|
+
Native Design's Datatable and DataTableScreen show **Filter zurücksetzen** and
|
|
10
|
+
**Spalten bearbeiten** in their ellipsis menu when the backend supports this
|
|
11
|
+
protocol. The editor supports drag and drop (including keyboard/native actions),
|
|
12
|
+
hiding/adding columns, backend default widths, auto grow, pixels and percentages.
|
|
13
|
+
Saving refreshes rows without resetting filters. Cancel leaves the layout intact.
|
|
14
|
+
At least one column must remain visible. `features.columnPreferences = false`
|
|
15
|
+
disables the editor; explicit client `columns` overrides also disable it.
|
|
16
|
+
|
|
17
|
+
## Storage and scope
|
|
18
|
+
|
|
19
|
+
The default document collection is `users_datatables`. The owner comes from
|
|
20
|
+
`req.auth.user` or `req.user`, respecting its `__primaryKey` (default `_id`). No
|
|
21
|
+
owner means no persistence and no edit action. Records contain `_user`, `table`,
|
|
22
|
+
`columns` and `updated_at`. A deterministic `_id` provides atomic upserts without
|
|
23
|
+
creating indexes or running schema changes during requests.
|
|
24
|
+
|
|
25
|
+
Configure all tables via `Datatable.columnPreferences`, or assign a per-table
|
|
26
|
+
`columnPreferences` configuration in a controller's `configureTable` hook. Use
|
|
27
|
+
only authenticated server context for owner fields. The client cannot select a
|
|
28
|
+
collection or another owner. Connections can be selected with `connection`.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
class EnvironmentDatatableController extends DatatableController {
|
|
32
|
+
protected configureTable(table: Datatable, req: Request) {
|
|
33
|
+
table.columnPreferences = {
|
|
34
|
+
collection: "environments_users_datatables",
|
|
35
|
+
owner: request => request.envUser?._id && request.env?._id
|
|
36
|
+
? { _environment_user: request.envUser._id, _env: request.env._id }
|
|
37
|
+
: null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Set `columnPreferences = false` to opt out. Existing definitions keep their
|
|
44
|
+
widths until explicitly overridden. Old saved keys no longer present in the
|
|
45
|
+
request's allowed definitions are removed; if none remain, defaults are used.
|
|
46
|
+
New columns become available in the editor without altering a saved layout.
|
|
47
|
+
|
|
48
|
+
## Request protocol
|
|
49
|
+
|
|
50
|
+
All operations use the same authorized POST route and `_table` plus the host's
|
|
51
|
+
normal request context:
|
|
52
|
+
|
|
53
|
+
- `init_request: true` or `mode: "columns-get"`: returns effective `columns`,
|
|
54
|
+
`availableColumns`, `columnSelection` and the boolean `columnPreferences`.
|
|
55
|
+
- `mode: "columns-save", columns: [...]`: validates and saves the ordered visible
|
|
56
|
+
columns, then returns current initialization metadata.
|
|
57
|
+
- `mode: "columns-reset"`: removes the preference and returns backend defaults.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"_table": "customers",
|
|
62
|
+
"mode": "columns-save",
|
|
63
|
+
"columns": [
|
|
64
|
+
{ "key": "email", "width": { "mode": "percent", "value": 40 } },
|
|
65
|
+
{ "key": "name", "width": { "mode": "auto" } },
|
|
66
|
+
{ "key": "created_at", "width": { "mode": "px", "value": 140 } }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Omitting `width` follows the current backend default. Percentages are greater
|
|
72
|
+
than zero and at most 100; pixels are greater than zero and at most 10,000.
|
|
73
|
+
Unknown/duplicate columns or invalid widths return HTTP 400; a missing owner or
|
|
74
|
+
disabled persistence returns HTTP 403. Normal table/middleware authorization
|
|
75
|
+
must run for every operation. Exports use the visible columns in their saved order.
|
|
76
|
+
|
|
77
|
+
## Conditional lookups and custom data methods
|
|
78
|
+
|
|
79
|
+
Use `await this.getVisibleColumns(req)`, `getVisibleColumnKeys(req)` or
|
|
80
|
+
`isColumnVisible(req, "email")` anywhere in a custom data method. Reads are cached
|
|
81
|
+
per table instance and request, never across users or environments.
|
|
82
|
+
|
|
83
|
+
`aggregation(req, context)` and `subAggregation(req, context)` receive a
|
|
84
|
+
`DatatableColumnContext`. The controller also passes it as the second argument
|
|
85
|
+
to `getData(req, context)`, including overrides. `onData(req, result, context)`,
|
|
86
|
+
`onRow(row, req, context)` and `onAttributes(row, req, context)` receive it during
|
|
87
|
+
normal paginated loading. The context exposes `columns`, `columnKeys`,
|
|
88
|
+
`isColumnVisible(key)` and `isColumnRequired(key)`.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
aggregation = async (req: Request, columns: DatatableColumnContext) => [
|
|
92
|
+
{ $match: { environment: req.env._id } },
|
|
93
|
+
...(columns.isColumnRequired("customer") ? [
|
|
94
|
+
{ $lookup: { from: "customers", localField: "customer_id", foreignField: "_id", as: "customer" } },
|
|
95
|
+
] : []),
|
|
96
|
+
];
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Prefer `isColumnRequired` for query optimization: it also considers declared
|
|
100
|
+
mappings, active filter/search/sort fields and sums, as well as model-selector
|
|
101
|
+
preview fields. This avoids breaking a sort/filter on a hidden column. Dependencies
|
|
102
|
+
inside custom expressions, custom filters, exports or other calculated columns
|
|
103
|
+
remain the table author's responsibility.
|
|
104
|
+
|
|
105
|
+
## Relation display fields and custom titles
|
|
106
|
+
|
|
107
|
+
A column can declare `displayOptions: [{ value: "name", label: "Name", type: "string" }]`
|
|
108
|
+
or an async `displayOptions(req, columnKey)` callback. The callback receives the
|
|
109
|
+
normal authenticated request. Initialization advertises lazy options with `true`;
|
|
110
|
+
`mode: "columns-options", column: "key"` resolves them when the editor needs them.
|
|
111
|
+
Only visible columns resolve options during normal loading. `displayField` selects
|
|
112
|
+
the backend default; otherwise the first option is used.
|
|
113
|
+
|
|
114
|
+
Saved preferences accept `title` (up to 160 characters) and `displayField` in
|
|
115
|
+
addition to width. Empty values restore the backend defaults. An unknown display
|
|
116
|
+
field is rejected on save; if a previously saved field disappears, loading falls
|
|
117
|
+
back to the default while preserving the other layout settings.
|
|
118
|
+
|
|
119
|
+
Core validates the selection and exposes it through `context.columns[key].displayField`;
|
|
120
|
+
the host performs the relation lookup and writes the value to the column's stable
|
|
121
|
+
`mapping`. Options can supply `type`, `plain` and `options` for typed rendering.
|
|
122
|
+
Never interpret an unchecked client field as a database projection.
|
|
123
|
+
|
|
124
|
+
`DatatableSchemaColumns.fromSchema(schema, { prefix, labelPrefix, prepare })`
|
|
125
|
+
creates an optional column catalog from nested DataBuilder/DataSchema fields.
|
|
126
|
+
The `prepare(column, field, path)` callback can attach inline or lazy display
|
|
127
|
+
options, change the mapping, or return null to omit a field. Plugin installation,
|
|
128
|
+
tenant checks and relation hydration belong in the host adapter.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webtypen/webframez-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.102",
|
|
4
4
|
"description": "The core package of the typescript based webframez (web-framework) by webtypen.de",
|
|
5
5
|
"homepage": "https://webtypen.de/",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"docs/queue-workers.md",
|
|
37
37
|
"docs/errors.md",
|
|
38
38
|
"build-cache.js",
|
|
39
|
-
"build-cache.d.ts"
|
|
39
|
+
"build-cache.d.ts",
|
|
40
|
+
"docs/datatables.md"
|
|
40
41
|
],
|
|
41
42
|
"scripts": {
|
|
42
43
|
"build": "tsc",
|