adminforth 1.5.4-next.8 → 1.5.4
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/auth.js +4 -4
- package/dist/auth.js.map +1 -1
- package/dist/dataConnectors/clickhouse.d.ts.map +1 -1
- package/dist/dataConnectors/clickhouse.js +0 -1
- package/dist/dataConnectors/clickhouse.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -13
- package/dist/index.js.map +1 -1
- package/dist/modules/codeInjector.d.ts.map +1 -1
- package/dist/modules/codeInjector.js +7 -5
- package/dist/modules/codeInjector.js.map +1 -1
- package/dist/modules/configValidator.d.ts +11 -5
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +387 -379
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +32 -13
- package/dist/modules/restApi.js.map +1 -1
- package/dist/modules/socketBroker.d.ts.map +1 -1
- package/dist/modules/socketBroker.js +0 -1
- package/dist/modules/socketBroker.js.map +1 -1
- package/dist/servers/express.d.ts.map +1 -1
- package/dist/servers/express.js +2 -3
- package/dist/servers/express.js.map +1 -1
- package/dist/spa/src/App.vue +8 -6
- package/dist/spa/src/afcl/Input.vue +1 -1
- package/dist/spa/src/afcl/Link.vue +9 -1
- package/dist/spa/src/afcl/LinkButton.vue +5 -3
- package/dist/spa/src/afcl/VerticalTabs.vue +1 -1
- package/dist/spa/src/components/AcceptModal.vue +1 -2
- package/dist/spa/src/components/ResourceListTable.vue +4 -3
- package/dist/spa/src/components/SkeleteLoader.vue +5 -10
- package/dist/spa/src/components/ValueRenderer.vue +13 -14
- package/dist/spa/src/spa_types/core.ts +2 -4
- package/dist/spa/src/stores/core.ts +15 -16
- package/dist/spa/src/stores/user.ts +0 -1
- package/dist/spa/src/types/Back.ts +310 -327
- package/dist/spa/src/types/Common.ts +220 -10
- package/dist/spa/src/types/FrontendAPI.ts +5 -4
- package/dist/spa/src/utils.ts +5 -1
- package/dist/spa/src/views/LoginView.vue +6 -1
- package/dist/spa/src/websocket.ts +3 -2
- package/dist/types/Back.d.ts +262 -287
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js +1 -46
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +194 -9
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js +47 -0
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +4 -4
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/package.json +4 -4
|
@@ -3,11 +3,11 @@ import path from 'path';
|
|
|
3
3
|
import { guessLabelFromName, md5hash, suggestIfTypo } from './utils.js';
|
|
4
4
|
import { AdminForthSortDirections, AllowedActionsEnum, AdminForthResourcePages, } from "../types/Common.js";
|
|
5
5
|
export default class ConfigValidator {
|
|
6
|
-
constructor(adminforth,
|
|
6
|
+
constructor(adminforth, inputConfig) {
|
|
7
7
|
this.adminforth = adminforth;
|
|
8
|
-
this.
|
|
8
|
+
this.inputConfig = inputConfig;
|
|
9
9
|
this.adminforth = adminforth;
|
|
10
|
-
this.
|
|
10
|
+
this.inputConfig = inputConfig;
|
|
11
11
|
}
|
|
12
12
|
validateAndListifyInjection(obj, key, errors) {
|
|
13
13
|
if (!Array.isArray(obj[key])) {
|
|
@@ -18,18 +18,29 @@ export default class ConfigValidator {
|
|
|
18
18
|
obj[key][i] = this.validateComponent(target, errors);
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
+
validateAndListifyInjectionNew(obj, key, errors) {
|
|
22
|
+
let injections = obj[key];
|
|
23
|
+
if (!Array.isArray(injections)) {
|
|
24
|
+
// not array
|
|
25
|
+
injections = [injections];
|
|
26
|
+
}
|
|
27
|
+
injections.forEach((target, i) => {
|
|
28
|
+
injections[i] = this.validateComponent(target, errors);
|
|
29
|
+
});
|
|
30
|
+
return injections;
|
|
31
|
+
}
|
|
21
32
|
checkCustomFileExists(filePath) {
|
|
22
33
|
if (filePath.startsWith('@@/')) {
|
|
23
|
-
const checkPath = path.join(this.
|
|
34
|
+
const checkPath = path.join(this.customComponentsDir, filePath.replace('@@/', ''));
|
|
24
35
|
if (!fs.existsSync(checkPath)) {
|
|
25
|
-
return [`File file ${filePath} does not exist in ${this.
|
|
36
|
+
return [`File file ${filePath} does not exist in ${this.customComponentsDir}`];
|
|
26
37
|
}
|
|
27
38
|
}
|
|
28
39
|
return [];
|
|
29
40
|
}
|
|
30
41
|
validateComponent(component, errors) {
|
|
31
42
|
if (!component) {
|
|
32
|
-
|
|
43
|
+
throw new Error('Component is missing during validation');
|
|
33
44
|
}
|
|
34
45
|
let obj;
|
|
35
46
|
if (typeof component === 'string') {
|
|
@@ -49,422 +60,428 @@ export default class ConfigValidator {
|
|
|
49
60
|
}
|
|
50
61
|
return obj;
|
|
51
62
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
try {
|
|
58
|
-
// check customComponentsDir exists
|
|
59
|
-
fs.accessSync(this.config.customization.customComponentsDir, fs.constants.R_OK);
|
|
60
|
-
}
|
|
61
|
-
catch (e) {
|
|
62
|
-
this.config.customization.customComponentsDir = undefined;
|
|
63
|
-
}
|
|
64
|
-
if (!this.config.customization) {
|
|
65
|
-
this.config.customization = {};
|
|
66
|
-
}
|
|
67
|
-
if (!this.config.customization.customComponentsDir) {
|
|
68
|
-
this.config.customization.customComponentsDir = './custom';
|
|
63
|
+
validateAndNormalizeCustomization(errors) {
|
|
64
|
+
var _a, _b, _c;
|
|
65
|
+
this.customComponentsDir = (_a = this.inputConfig.customization) === null || _a === void 0 ? void 0 : _a.customComponentsDir;
|
|
66
|
+
if (!this.customComponentsDir) {
|
|
67
|
+
this.customComponentsDir = './custom';
|
|
69
68
|
}
|
|
70
69
|
try {
|
|
71
70
|
// check customComponentsDir exists
|
|
72
|
-
fs.accessSync(this.
|
|
71
|
+
fs.accessSync(this.customComponentsDir, fs.constants.R_OK);
|
|
73
72
|
}
|
|
74
73
|
catch (e) {
|
|
75
|
-
this.
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
this.customComponentsDir = undefined;
|
|
75
|
+
}
|
|
76
|
+
const loginPageInjections = {
|
|
77
|
+
underInputs: [],
|
|
78
|
+
};
|
|
79
|
+
if ((_b = this.inputConfig.customization) === null || _b === void 0 ? void 0 : _b.loginPageInjections) {
|
|
80
|
+
const ALLOWED_LOGIN_INJECTIONS = ['underInputs'];
|
|
81
|
+
Object.keys(this.inputConfig.customization.loginPageInjections).forEach((injection) => {
|
|
82
|
+
if (ALLOWED_LOGIN_INJECTIONS.includes(injection)) {
|
|
83
|
+
loginPageInjections[injection] = this.validateAndListifyInjectionNew(this.inputConfig.customization.loginPageInjections, injection, errors);
|
|
84
|
+
console.log('🐛🐛 loginPageInjections', JSON.stringify(loginPageInjections, null, 2));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const similar = suggestIfTypo(ALLOWED_LOGIN_INJECTIONS, injection);
|
|
88
|
+
errors.push(`Login page injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_LOGIN_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
89
|
+
}
|
|
80
90
|
});
|
|
81
91
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.
|
|
92
|
+
const globalInjections = {
|
|
93
|
+
userMenu: [],
|
|
94
|
+
header: [],
|
|
95
|
+
sidebar: [],
|
|
96
|
+
};
|
|
97
|
+
if ((_c = this.inputConfig.customization) === null || _c === void 0 ? void 0 : _c.globalInjections) {
|
|
98
|
+
const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar'];
|
|
99
|
+
Object.keys(this.inputConfig.customization.globalInjections).forEach((injection) => {
|
|
100
|
+
if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
|
|
101
|
+
globalInjections[injection] = this.validateAndListifyInjectionNew(this.inputConfig.customization.globalInjections, injection, errors);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
|
|
105
|
+
errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
90
108
|
}
|
|
91
|
-
|
|
92
|
-
|
|
109
|
+
const customization = Object.assign(Object.assign({}, (this.inputConfig.customization || {})), { customComponentsDir: this.customComponentsDir, loginPageInjections,
|
|
110
|
+
globalInjections });
|
|
111
|
+
if (!customization.customPages) {
|
|
112
|
+
customization.customPages = [];
|
|
93
113
|
}
|
|
94
|
-
|
|
95
|
-
this.
|
|
114
|
+
customization.customPages.forEach((page, i) => {
|
|
115
|
+
this.validateComponent(page.component, errors);
|
|
116
|
+
});
|
|
117
|
+
if (!customization.brandName) { //} === undefined) {
|
|
118
|
+
customization.brandName = 'AdminForth';
|
|
96
119
|
}
|
|
97
120
|
// slug should have only lowercase letters, dashes and numbers
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
this.
|
|
101
|
-
}
|
|
102
|
-
if (this.config.customization.globalInjections === undefined) {
|
|
103
|
-
this.config.customization.globalInjections = {};
|
|
121
|
+
customization.brandNameSlug = customization.brandName.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
122
|
+
if (customization.brandLogo) {
|
|
123
|
+
errors.push(...this.checkCustomFileExists(customization.brandLogo));
|
|
104
124
|
}
|
|
105
|
-
if (
|
|
106
|
-
|
|
125
|
+
if (customization.showBrandNameInSidebar === undefined) {
|
|
126
|
+
customization.showBrandNameInSidebar = true;
|
|
107
127
|
}
|
|
108
|
-
if (
|
|
109
|
-
errors.push(...this.checkCustomFileExists(
|
|
128
|
+
if (customization.favicon) {
|
|
129
|
+
errors.push(...this.checkCustomFileExists(customization.favicon));
|
|
110
130
|
}
|
|
111
|
-
if (
|
|
112
|
-
|
|
131
|
+
if (!customization.datesFormat) {
|
|
132
|
+
customization.datesFormat = 'MMM D, YYYY';
|
|
113
133
|
}
|
|
114
|
-
if (
|
|
115
|
-
|
|
134
|
+
if (!customization.timeFormat) {
|
|
135
|
+
customization.timeFormat = 'HH:mm:ss';
|
|
116
136
|
}
|
|
117
|
-
|
|
118
|
-
|
|
137
|
+
return customization;
|
|
138
|
+
}
|
|
139
|
+
validateAndNormalizeAllowedActions(resInput, errors) {
|
|
140
|
+
var _a;
|
|
141
|
+
const allowedActions = ((_a = resInput.options) === null || _a === void 0 ? void 0 : _a.allowedActions) || { all: true };
|
|
142
|
+
if (Object.keys(allowedActions).includes('all')) {
|
|
143
|
+
if (Object.keys(allowedActions).length > 1) {
|
|
144
|
+
errors.push(`Resource "${resInput.resourceId || resInput.table}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(allowedActions).join(', ')}`);
|
|
145
|
+
}
|
|
146
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
147
|
+
if (key !== 'all') {
|
|
148
|
+
allowedActions[key] = allowedActions.all;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
delete allowedActions.all;
|
|
119
152
|
}
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
else {
|
|
154
|
+
// by default allow all actions
|
|
155
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
156
|
+
if (!Object.keys(allowedActions).includes(key)) {
|
|
157
|
+
allowedActions[key] = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
122
160
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (col.showIn && !Array.isArray(col.showIn)) {
|
|
156
|
-
errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
|
|
157
|
-
}
|
|
158
|
-
// check col.required is string or object
|
|
159
|
-
if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
|
|
160
|
-
errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
|
|
161
|
-
}
|
|
162
|
-
// if it is object check the keys are one of ['create', 'edit']
|
|
163
|
-
if (typeof col.required === 'object') {
|
|
164
|
-
const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
|
|
165
|
-
if (wrongRequiredOn) {
|
|
166
|
-
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
|
|
161
|
+
return allowedActions;
|
|
162
|
+
}
|
|
163
|
+
validateAndNormalizeBulkActions(resInput, res, errors) {
|
|
164
|
+
var _a;
|
|
165
|
+
//check if resource has bulkActions
|
|
166
|
+
let bulkActions = ((_a = resInput === null || resInput === void 0 ? void 0 : resInput.options) === null || _a === void 0 ? void 0 : _a.bulkActions) || [];
|
|
167
|
+
if (!Array.isArray(bulkActions)) {
|
|
168
|
+
errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
|
|
169
|
+
bulkActions = [];
|
|
170
|
+
}
|
|
171
|
+
bulkActions.push({
|
|
172
|
+
label: `Delete checked`,
|
|
173
|
+
state: 'danger',
|
|
174
|
+
icon: 'flowbite:trash-bin-outline',
|
|
175
|
+
confirm: 'Are you sure you want to delete selected items?',
|
|
176
|
+
allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete; },
|
|
177
|
+
action: async ({ selectedIds, adminUser }) => {
|
|
178
|
+
const connector = this.adminforth.connectors[res.dataSource];
|
|
179
|
+
// for now if at least one error, stop and return error
|
|
180
|
+
let error = null;
|
|
181
|
+
await Promise.all(selectedIds.map(async (recordId) => {
|
|
182
|
+
const record = await connector.getRecordByPrimaryKey(res, recordId);
|
|
183
|
+
await Promise.all(res.hooks.delete.beforeSave.map(async (hook) => {
|
|
184
|
+
const resp = await hook({
|
|
185
|
+
recordId: recordId,
|
|
186
|
+
resource: res,
|
|
187
|
+
record,
|
|
188
|
+
adminUser,
|
|
189
|
+
adminforth: this.adminforth
|
|
190
|
+
});
|
|
191
|
+
if (!error && resp.error) {
|
|
192
|
+
error = resp.error;
|
|
167
193
|
}
|
|
194
|
+
}));
|
|
195
|
+
if (error) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await connector.deleteRecord({ resource: res, recordId });
|
|
199
|
+
// call afterDelete hook
|
|
200
|
+
await Promise.all(res.hooks.delete.afterSave.map(async (hook) => {
|
|
201
|
+
await hook({
|
|
202
|
+
resource: res,
|
|
203
|
+
record,
|
|
204
|
+
adminUser,
|
|
205
|
+
recordId: recordId,
|
|
206
|
+
adminforth: this.adminforth,
|
|
207
|
+
});
|
|
208
|
+
}));
|
|
209
|
+
}));
|
|
210
|
+
if (error) {
|
|
211
|
+
return { error, ok: false };
|
|
212
|
+
}
|
|
213
|
+
return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
bulkActions.map((action) => {
|
|
217
|
+
if (!action.id) {
|
|
218
|
+
action.id = md5hash(action.label);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return bulkActions;
|
|
222
|
+
}
|
|
223
|
+
validateAndNormalizeResources(errors) {
|
|
224
|
+
if (!this.inputConfig.resources) {
|
|
225
|
+
errors.push('No resources defined, at least one resource must be defined');
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
return this.inputConfig.resources.map((resInput) => {
|
|
229
|
+
const res = Object.assign(Object.assign({}, resInput), { options: undefined });
|
|
230
|
+
if (!res.table) {
|
|
231
|
+
errors.push(`Resource in "${res.dataSource}" is missing table`);
|
|
232
|
+
}
|
|
233
|
+
res.resourceId = res.resourceId || res.table;
|
|
234
|
+
// if recordLabel is not callable, throw error
|
|
235
|
+
if (res.recordLabel && typeof res.recordLabel !== 'function') {
|
|
236
|
+
errors.push(`Resource "${res.resourceId}" recordLabel is not a function`);
|
|
237
|
+
}
|
|
238
|
+
if (!res.recordLabel) {
|
|
239
|
+
res.recordLabel = (item) => {
|
|
240
|
+
const pkVal = item[res.columns.find((col) => col.primaryKey).name];
|
|
241
|
+
return `${res.label} ${pkVal}`;
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// as fallback value, capitalize and then replace _ with space
|
|
245
|
+
res.label = res.label || res.resourceId.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
|
|
246
|
+
if (!res.dataSource) {
|
|
247
|
+
errors.push(`Resource "${res.resourceId}" is missing dataSource`);
|
|
248
|
+
}
|
|
249
|
+
if (!res.columns) {
|
|
250
|
+
res.columns = [];
|
|
251
|
+
}
|
|
252
|
+
res.columns = res.columns.map((inCol) => {
|
|
253
|
+
var _a, _b, _c, _d;
|
|
254
|
+
const col = Object.assign({}, inCol);
|
|
255
|
+
col.label = col.label || guessLabelFromName(col.name);
|
|
256
|
+
//define default sortable
|
|
257
|
+
if (!Object.keys(col).includes('sortable')) {
|
|
258
|
+
col.sortable = true;
|
|
259
|
+
}
|
|
260
|
+
if (col.showIn && !Array.isArray(col.showIn)) {
|
|
261
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" showIn must be an array`);
|
|
262
|
+
}
|
|
263
|
+
// check col.required is string or object
|
|
264
|
+
if (col.required && !((typeof col.required === 'boolean') || (typeof col.required === 'object'))) {
|
|
265
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" required must be a string or object`);
|
|
266
|
+
}
|
|
267
|
+
// if it is object check the keys are one of ['create', 'edit']
|
|
268
|
+
if (typeof col.required === 'object') {
|
|
269
|
+
const wrongRequiredOn = Object.keys(col.required).find((c) => !['create', 'edit'].includes(c));
|
|
270
|
+
if (wrongRequiredOn) {
|
|
271
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid required value "${wrongRequiredOn}", allowed keys are 'create', 'edit']`);
|
|
168
272
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
273
|
+
}
|
|
274
|
+
// same for editingNote
|
|
275
|
+
if (col.editingNote && !((typeof col.editingNote === 'string') || (typeof col.editingNote === 'object'))) {
|
|
276
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" editingNote must be a string or object`);
|
|
277
|
+
}
|
|
278
|
+
if (typeof col.editingNote === 'object') {
|
|
279
|
+
const wrongEditingNoteOn = Object.keys(col.editingNote).find((c) => !['create', 'edit'].includes(c));
|
|
280
|
+
if (wrongEditingNoteOn) {
|
|
281
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid editingNote value "${wrongEditingNoteOn}", allowed keys are 'create', 'edit']`);
|
|
172
282
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
283
|
+
}
|
|
284
|
+
const wrongShowIn = col.showIn && col.showIn.find((c) => AdminForthResourcePages[c] === undefined);
|
|
285
|
+
if (wrongShowIn) {
|
|
286
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has invalid showIn value "${wrongShowIn}", allowed values are ${Object.keys(AdminForthResourcePages).join(', ')}`);
|
|
287
|
+
}
|
|
288
|
+
col.showIn = col.showIn || Object.values(AdminForthResourcePages);
|
|
289
|
+
if (col.foreignResource) {
|
|
290
|
+
if (!col.foreignResource.resourceId) {
|
|
291
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
|
|
292
|
+
}
|
|
293
|
+
// we do || here because 'resourceId' might yet not be assigned from 'table'
|
|
294
|
+
const resource = this.inputConfig.resources.find((r) => r.resourceId === col.foreignResource.resourceId || r.table === col.foreignResource.resourceId);
|
|
295
|
+
if (!resource) {
|
|
296
|
+
const similar = suggestIfTypo(this.inputConfig.resources.map((r) => r.resourceId || r.table), col.foreignResource.resourceId);
|
|
297
|
+
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
|
|
298
|
+
${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
|
|
299
|
+
}
|
|
300
|
+
const befHook = (_b = (_a = col.foreignResource.hooks) === null || _a === void 0 ? void 0 : _a.dropdownList) === null || _b === void 0 ? void 0 : _b.beforeDatasourceRequest;
|
|
301
|
+
if (befHook) {
|
|
302
|
+
if (!Array.isArray(befHook)) {
|
|
303
|
+
col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
|
|
177
304
|
}
|
|
178
305
|
}
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
col.showIn = col.showIn || Object.values(AdminForthResourcePages);
|
|
184
|
-
if (col.foreignResource) {
|
|
185
|
-
if (!col.foreignResource.resourceId) {
|
|
186
|
-
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource without resourceId`);
|
|
187
|
-
}
|
|
188
|
-
const resource = this.config.resources.find((r) => r.resourceId === col.foreignResource.resourceId);
|
|
189
|
-
if (!resource) {
|
|
190
|
-
const similar = suggestIfTypo(this.config.resources.map((r) => r.resourceId), col.foreignResource.resourceId);
|
|
191
|
-
errors.push(`Resource "${res.resourceId}" column "${col.name}" has foreignResource resourceId which is not in resources: "${col.foreignResource.resourceId}".
|
|
192
|
-
${similar ? `Did you mean "${similar}" instead of "${col.foreignResource.resourceId}"?` : ''}`);
|
|
193
|
-
}
|
|
194
|
-
const befHook = (_b = (_a = col.foreignResource.hooks) === null || _a === void 0 ? void 0 : _a.dropdownList) === null || _b === void 0 ? void 0 : _b.beforeDatasourceRequest;
|
|
195
|
-
if (befHook) {
|
|
196
|
-
if (!Array.isArray(befHook)) {
|
|
197
|
-
col.foreignResource.hooks.dropdownList.beforeDatasourceRequest = [befHook];
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const aftHook = (_d = (_c = col.foreignResource.hooks) === null || _c === void 0 ? void 0 : _c.dropdownList) === null || _d === void 0 ? void 0 : _d.afterDatasourceResponse;
|
|
201
|
-
if (aftHook) {
|
|
202
|
-
if (!Array.isArray(aftHook)) {
|
|
203
|
-
col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
|
|
204
|
-
}
|
|
306
|
+
const aftHook = (_d = (_c = col.foreignResource.hooks) === null || _c === void 0 ? void 0 : _c.dropdownList) === null || _d === void 0 ? void 0 : _d.afterDatasourceResponse;
|
|
307
|
+
if (aftHook) {
|
|
308
|
+
if (!Array.isArray(aftHook)) {
|
|
309
|
+
col.foreignResource.hooks.dropdownList.afterDatasourceResponse = [aftHook];
|
|
205
310
|
}
|
|
206
311
|
}
|
|
207
|
-
});
|
|
208
|
-
if (!res.options) {
|
|
209
|
-
res.options = { bulkActions: [], allowedActions: {} };
|
|
210
|
-
}
|
|
211
|
-
if (!res.options.allowedActions) {
|
|
212
|
-
res.options.allowedActions = {
|
|
213
|
-
all: true,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
if (res.options.defaultSort) {
|
|
217
|
-
const colName = res.options.defaultSort.columnName;
|
|
218
|
-
const col = res.columns.find((c) => c.name === colName);
|
|
219
|
-
if (!col) {
|
|
220
|
-
const similar = suggestIfTypo(res.columns.map((c) => c.name), colName);
|
|
221
|
-
errors.push(`Resource "${res.resourceId}" defaultSort.columnName column "${colName}" not found in columns. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
222
|
-
}
|
|
223
|
-
const dir = res.options.defaultSort.direction;
|
|
224
|
-
if (!dir) {
|
|
225
|
-
errors.push(`Resource "${res.resourceId}" defaultSort.direction is missing`);
|
|
226
|
-
}
|
|
227
|
-
// AdminForthSortDirections is enum
|
|
228
|
-
if (!Object.values(AdminForthSortDirections).includes(dir)) {
|
|
229
|
-
errors.push(`Resource "${res.resourceId}" defaultSort.direction "${dir}" is invalid, allowed values are ${Object.values(AdminForthSortDirections).join(', ')}`);
|
|
230
|
-
}
|
|
231
312
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
313
|
+
// check is all custom components files exists
|
|
314
|
+
if (col.components) {
|
|
315
|
+
for (const [key, comp] of Object.entries(col.components)) {
|
|
316
|
+
col.components[key] = this.validateComponent(comp, errors);
|
|
235
317
|
}
|
|
236
|
-
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
237
|
-
if (key !== 'all') {
|
|
238
|
-
res.options.allowedActions[key] = res.options.allowedActions.all;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
delete res.options.allowedActions.all;
|
|
242
318
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
319
|
+
return col;
|
|
320
|
+
});
|
|
321
|
+
const options = Object.assign(Object.assign({}, resInput.options), { bulkActions: undefined, allowedActions: undefined });
|
|
322
|
+
options.allowedActions = this.validateAndNormalizeAllowedActions(resInput, errors);
|
|
323
|
+
if (options.defaultSort) {
|
|
324
|
+
const colName = options.defaultSort.columnName;
|
|
325
|
+
const col = res.columns.find((c) => c.name === colName);
|
|
326
|
+
if (!col) {
|
|
327
|
+
const similar = suggestIfTypo(res.columns.map((c) => c.name), colName);
|
|
328
|
+
errors.push(`Resource "${res.resourceId}" defaultSort.columnName column "${colName}" not found in columns. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
250
329
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
|
|
255
|
-
bulkActions = [];
|
|
256
|
-
}
|
|
257
|
-
if (!bulkActions.find((action) => action.label === 'Delete checked')) {
|
|
258
|
-
bulkActions.push({
|
|
259
|
-
label: `Delete checked`,
|
|
260
|
-
state: 'danger',
|
|
261
|
-
icon: 'flowbite:trash-bin-outline',
|
|
262
|
-
confirm: 'Are you sure you want to delete selected items?',
|
|
263
|
-
allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete; },
|
|
264
|
-
action: async ({ selectedIds, adminUser }) => {
|
|
265
|
-
const connector = this.adminforth.connectors[res.dataSource];
|
|
266
|
-
// for now if at least one error, stop and return error
|
|
267
|
-
let error = null;
|
|
268
|
-
await Promise.all(selectedIds.map(async (recordId) => {
|
|
269
|
-
const record = await connector.getRecordByPrimaryKey(res, recordId);
|
|
270
|
-
await Promise.all(res.hooks.delete.beforeSave.map(async (hook) => {
|
|
271
|
-
const resp = await hook({
|
|
272
|
-
recordId: recordId,
|
|
273
|
-
resource: res,
|
|
274
|
-
record,
|
|
275
|
-
adminUser,
|
|
276
|
-
adminforth: this.adminforth
|
|
277
|
-
});
|
|
278
|
-
if (!error && resp.error) {
|
|
279
|
-
error = resp.error;
|
|
280
|
-
}
|
|
281
|
-
}));
|
|
282
|
-
if (error) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
await connector.deleteRecord({ resource: res, recordId });
|
|
286
|
-
// call afterDelete hook
|
|
287
|
-
await Promise.all(res.hooks.delete.afterSave.map(async (hook) => {
|
|
288
|
-
await hook({
|
|
289
|
-
resource: res,
|
|
290
|
-
record,
|
|
291
|
-
adminUser,
|
|
292
|
-
recordId: recordId,
|
|
293
|
-
adminforth: this.adminforth,
|
|
294
|
-
});
|
|
295
|
-
}));
|
|
296
|
-
}));
|
|
297
|
-
if (error) {
|
|
298
|
-
return { error, ok: false };
|
|
299
|
-
}
|
|
300
|
-
return { ok: true, successMessage: `${selectedIds.length} item${selectedIds.length > 1 ? 's' : ''} deleted` };
|
|
301
|
-
}
|
|
302
|
-
});
|
|
330
|
+
const dir = options.defaultSort.direction;
|
|
331
|
+
if (!dir) {
|
|
332
|
+
errors.push(`Resource "${res.resourceId}" defaultSort.direction is missing`);
|
|
303
333
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
334
|
+
// AdminForthSortDirections is enum
|
|
335
|
+
if (!Object.values(AdminForthSortDirections).includes(dir)) {
|
|
336
|
+
errors.push(`Resource "${res.resourceId}" defaultSort.direction "${dir}" is invalid, allowed values are ${Object.values(AdminForthSortDirections).join(', ')}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
options.bulkActions = this.validateAndNormalizeBulkActions(resInput, res, errors);
|
|
340
|
+
// if pageInjection is a string, make array with one element. Also check file exists
|
|
341
|
+
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];
|
|
342
|
+
const possiblePages = ['list', 'show', 'create', 'edit'];
|
|
343
|
+
if (options.pageInjections) {
|
|
344
|
+
Object.entries(options.pageInjections).map(([key, value]) => {
|
|
345
|
+
if (!possiblePages.includes(key)) {
|
|
346
|
+
const similar = suggestIfTypo(possiblePages, key);
|
|
347
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${key}", allowed keys are ${possiblePages.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
348
|
+
}
|
|
349
|
+
Object.entries(value).map(([injection, target]) => {
|
|
350
|
+
if (possibleInjections.includes(injection)) {
|
|
351
|
+
this.validateAndListifyInjection(options.pageInjections[key], injection, errors);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const similar = suggestIfTypo(possibleInjections, injection);
|
|
355
|
+
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
318
356
|
}
|
|
319
|
-
Object.entries(value).map(([injection, target]) => {
|
|
320
|
-
if (possibleInjections.includes(injection)) {
|
|
321
|
-
this.validateAndListifyInjection(res.options.pageInjections[key], injection, errors);
|
|
322
|
-
}
|
|
323
|
-
else {
|
|
324
|
-
const similar = suggestIfTypo(possibleInjections, injection);
|
|
325
|
-
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
357
|
});
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
res.options = options;
|
|
361
|
+
// transform all hooks Functions to array of functions
|
|
362
|
+
if (!res.hooks) {
|
|
363
|
+
res.hooks = {};
|
|
364
|
+
}
|
|
365
|
+
for (const hookName of ['show', 'list']) {
|
|
366
|
+
if (!res.hooks[hookName]) {
|
|
367
|
+
res.hooks[hookName] = {};
|
|
329
368
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
res.hooks = {};
|
|
369
|
+
if (!res.hooks[hookName].beforeDatasourceRequest) {
|
|
370
|
+
res.hooks[hookName].beforeDatasourceRequest = [];
|
|
333
371
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
res.hooks[hookName] = {};
|
|
337
|
-
}
|
|
338
|
-
if (!res.hooks[hookName].beforeDatasourceRequest) {
|
|
339
|
-
res.hooks[hookName].beforeDatasourceRequest = [];
|
|
340
|
-
}
|
|
341
|
-
if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
|
|
342
|
-
res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
|
|
343
|
-
}
|
|
344
|
-
if (!res.hooks[hookName].afterDatasourceResponse) {
|
|
345
|
-
res.hooks[hookName].afterDatasourceResponse = [];
|
|
346
|
-
}
|
|
347
|
-
if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
|
|
348
|
-
res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
|
|
349
|
-
}
|
|
372
|
+
if (!Array.isArray(res.hooks[hookName].beforeDatasourceRequest)) {
|
|
373
|
+
res.hooks[hookName].beforeDatasourceRequest = [res.hooks[hookName].beforeDatasourceRequest];
|
|
350
374
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
res.hooks[hookName].beforeSave = [];
|
|
357
|
-
}
|
|
358
|
-
if (!Array.isArray(res.hooks[hookName].beforeSave)) {
|
|
359
|
-
res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
|
|
360
|
-
}
|
|
361
|
-
if (!res.hooks[hookName].afterSave) {
|
|
362
|
-
res.hooks[hookName].afterSave = [];
|
|
363
|
-
}
|
|
364
|
-
if (!Array.isArray(res.hooks[hookName].afterSave)) {
|
|
365
|
-
res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
|
|
366
|
-
}
|
|
375
|
+
if (!res.hooks[hookName].afterDatasourceResponse) {
|
|
376
|
+
res.hooks[hookName].afterDatasourceResponse = [];
|
|
377
|
+
}
|
|
378
|
+
if (!Array.isArray(res.hooks[hookName].afterDatasourceResponse)) {
|
|
379
|
+
res.hooks[hookName].afterDatasourceResponse = [res.hooks[hookName].afterDatasourceResponse];
|
|
367
380
|
}
|
|
368
|
-
});
|
|
369
|
-
if (this.config.customization.globalInjections) {
|
|
370
|
-
const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar',];
|
|
371
|
-
Object.keys(this.config.customization.globalInjections).forEach((injection) => {
|
|
372
|
-
if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
|
|
373
|
-
this.validateAndListifyInjection(this.config.customization.globalInjections, injection, errors);
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
376
|
-
const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
|
|
377
|
-
errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
if (this.config.customization.loginPageInjections) {
|
|
382
|
-
const ALLOWED_GLOBAL_INJECTIONS = ['underInputs'];
|
|
383
|
-
Object.keys(this.config.customization.loginPageInjections).forEach((injection) => {
|
|
384
|
-
if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
|
|
385
|
-
this.validateAndListifyInjection(this.config.customization.loginPageInjections, injection, errors);
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
|
|
389
|
-
errors.push(`Login page injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
381
|
}
|
|
393
|
-
|
|
394
|
-
|
|
382
|
+
for (const hookName of ['create', 'edit', 'delete']) {
|
|
383
|
+
if (!res.hooks[hookName]) {
|
|
384
|
+
res.hooks[hookName] = {};
|
|
385
|
+
}
|
|
386
|
+
if (!res.hooks[hookName].beforeSave) {
|
|
387
|
+
res.hooks[hookName].beforeSave = [];
|
|
388
|
+
}
|
|
389
|
+
if (!Array.isArray(res.hooks[hookName].beforeSave)) {
|
|
390
|
+
res.hooks[hookName].beforeSave = [res.hooks[hookName].beforeSave];
|
|
391
|
+
}
|
|
392
|
+
if (!res.hooks[hookName].afterSave) {
|
|
393
|
+
res.hooks[hookName].afterSave = [];
|
|
394
|
+
}
|
|
395
|
+
if (!Array.isArray(res.hooks[hookName].afterSave)) {
|
|
396
|
+
res.hooks[hookName].afterSave = [res.hooks[hookName].afterSave];
|
|
397
|
+
}
|
|
395
398
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
399
|
+
return res;
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
validateConfig() {
|
|
403
|
+
const errors = [];
|
|
404
|
+
const newConfig = Object.assign(Object.assign({}, this.inputConfig), { customization: this.validateAndNormalizeCustomization(errors), resources: this.validateAndNormalizeResources(errors) });
|
|
405
|
+
if (!newConfig.baseUrl) {
|
|
406
|
+
newConfig.baseUrl = '';
|
|
407
|
+
}
|
|
408
|
+
if (!newConfig.baseUrl.endsWith('/')) {
|
|
409
|
+
newConfig.baseUrlSlashed = `${newConfig.baseUrl}/`;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
newConfig.baseUrlSlashed = newConfig.baseUrl;
|
|
413
|
+
}
|
|
414
|
+
if (!newConfig.menu) {
|
|
415
|
+
errors.push('No config.menu defined');
|
|
416
|
+
}
|
|
417
|
+
// check if there is only one homepage: true in menu, recursivly
|
|
418
|
+
let homepages = 0;
|
|
419
|
+
const browseMenu = (menu) => {
|
|
420
|
+
menu.forEach((item) => {
|
|
421
|
+
if (item.component && item.resourceId) {
|
|
422
|
+
errors.push(`Menu item cannot have both component and resourceId: ${JSON.stringify(item)}`);
|
|
423
|
+
}
|
|
424
|
+
if (item.component && !item.path) {
|
|
425
|
+
errors.push(`Menu item with component must have path : ${JSON.stringify(item)}`);
|
|
426
|
+
}
|
|
427
|
+
if (item.type === 'resource' && !item.resourceId) {
|
|
428
|
+
errors.push(`Menu item with type 'resource' must have resourceId : ${JSON.stringify(item)}`);
|
|
429
|
+
}
|
|
430
|
+
if (item.resourceId && !newConfig.resources.find((res) => res.resourceId === item.resourceId)) {
|
|
431
|
+
const similar = suggestIfTypo(newConfig.resources.map((res) => res.resourceId), item.resourceId);
|
|
432
|
+
errors.push(`Menu item with type 'resourceId' has resourceId which is not in resources: "${JSON.stringify(item)}".
|
|
433
|
+
${similar ? `Did you mean "${similar}" instead of "${item.resourceId}"?` : ''}`);
|
|
434
|
+
}
|
|
435
|
+
if (item.type === 'page' && !item.component) {
|
|
436
|
+
errors.push(`Menu item with type 'component' must have component : ${JSON.stringify(item)}`);
|
|
437
|
+
}
|
|
438
|
+
// make sure component starts with @@
|
|
439
|
+
if (item.component) {
|
|
440
|
+
if (!item.component.startsWith('@@')) {
|
|
441
|
+
errors.push(`Menu item component must start with @@ : ${JSON.stringify(item)}`);
|
|
426
442
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
|
|
431
|
-
}
|
|
443
|
+
const path = item.component.replace('@@', newConfig.customization.customComponentsDir);
|
|
444
|
+
if (!fs.existsSync(path)) {
|
|
445
|
+
errors.push(`Menu item component "${item.component.replace('@@', '')}" does not exist in "${newConfig.customization.customComponentsDir}"`);
|
|
432
446
|
}
|
|
433
|
-
|
|
434
|
-
|
|
447
|
+
}
|
|
448
|
+
if (item.homepage) {
|
|
449
|
+
homepages++;
|
|
450
|
+
if (homepages > 1) {
|
|
451
|
+
errors.push('There must be only one homepage: true in menu, found second one in ' + JSON.stringify(item));
|
|
435
452
|
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (!
|
|
453
|
+
}
|
|
454
|
+
if (item.children) {
|
|
455
|
+
browseMenu(item.children);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
browseMenu(newConfig.menu);
|
|
460
|
+
// AUTH checks
|
|
461
|
+
if (newConfig.auth) {
|
|
462
|
+
if (!newConfig.auth.usersResourceId) {
|
|
446
463
|
throw new Error('No config.auth.usersResourceId defined');
|
|
447
464
|
}
|
|
448
|
-
if (!
|
|
465
|
+
if (!newConfig.auth.passwordHashField) {
|
|
449
466
|
throw new Error('No config.auth.passwordHashField defined');
|
|
450
467
|
}
|
|
451
|
-
if (!
|
|
468
|
+
if (!newConfig.auth.usernameField) {
|
|
452
469
|
throw new Error('No config.auth.usernameField defined');
|
|
453
470
|
}
|
|
454
|
-
if (
|
|
455
|
-
errors.push(...this.checkCustomFileExists(
|
|
471
|
+
if (newConfig.auth.loginBackgroundImage) {
|
|
472
|
+
errors.push(...this.checkCustomFileExists(newConfig.auth.loginBackgroundImage));
|
|
456
473
|
}
|
|
457
|
-
const userResource =
|
|
474
|
+
const userResource = newConfig.resources.find((res) => res.resourceId === newConfig.auth.usersResourceId);
|
|
458
475
|
if (!userResource) {
|
|
459
|
-
const similar = suggestIfTypo(
|
|
460
|
-
throw new Error(`Resource with id "${
|
|
476
|
+
const similar = suggestIfTypo(newConfig.resources.map((res) => res.resourceId), newConfig.auth.usersResourceId);
|
|
477
|
+
throw new Error(`Resource with id "${newConfig.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
461
478
|
}
|
|
462
|
-
if (!
|
|
463
|
-
|
|
479
|
+
if (!newConfig.auth.beforeLoginConfirmation) {
|
|
480
|
+
newConfig.auth.beforeLoginConfirmation = [];
|
|
464
481
|
}
|
|
465
482
|
}
|
|
466
483
|
// check for duplicate resourceIds and show which ones are duplicated
|
|
467
|
-
const resourceIds =
|
|
484
|
+
const resourceIds = newConfig.resources.map((res) => res.resourceId);
|
|
468
485
|
const uniqueResourceIds = new Set(resourceIds);
|
|
469
486
|
if (uniqueResourceIds.size != resourceIds.length) {
|
|
470
487
|
const duplicates = resourceIds.filter((item, index) => resourceIds.indexOf(item) != index);
|
|
@@ -474,16 +491,7 @@ export default class ConfigValidator {
|
|
|
474
491
|
if (errors.length > 0) {
|
|
475
492
|
throw new Error(`Invalid AdminForth config: ${errors.join(', ')}`);
|
|
476
493
|
}
|
|
477
|
-
|
|
478
|
-
for (const resource of this.config.resources) {
|
|
479
|
-
for (const column of resource.columns) {
|
|
480
|
-
if (column.components) {
|
|
481
|
-
for (const [key, comp] of Object.entries(column.components)) {
|
|
482
|
-
column.components[key] = this.validateComponent(comp, errors);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
494
|
+
this.adminforth.config = newConfig;
|
|
487
495
|
}
|
|
488
496
|
postProcessAfterDiscover(resource) {
|
|
489
497
|
resource.columns.forEach((column) => {
|