adminforth 1.3.52-next.0 → 1.3.52-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/codeInjector.js +23 -15
- package/dist/modules/configValidator.js +25 -7
- package/dist/modules/restApi.js +18 -17
- package/index.ts +1 -3
- package/modules/codeInjector.ts +21 -8
- package/modules/configValidator.ts +34 -8
- package/modules/restApi.ts +1 -0
- package/package.json +2 -1
- package/spa/index.html +2 -2
- package/spa/src/App.vue +34 -4
- package/types/AdminForthConfig.ts +11 -0
|
@@ -158,7 +158,7 @@ class CodeInjector {
|
|
|
158
158
|
}
|
|
159
159
|
prepareSources(_a) {
|
|
160
160
|
return __awaiter(this, arguments, void 0, function* ({ filesUpdated }) {
|
|
161
|
-
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
161
|
+
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
162
162
|
// check SPA_TMP_PATH exists and create if not
|
|
163
163
|
try {
|
|
164
164
|
yield fs.promises.access(CodeInjector.SPA_TMP_PATH, fs.constants.F_OK);
|
|
@@ -325,6 +325,16 @@ class CodeInjector {
|
|
|
325
325
|
}).join('\n');
|
|
326
326
|
// for each custom component generate import statement
|
|
327
327
|
const customResourceComponents = [];
|
|
328
|
+
function checkInjections(filePathes) {
|
|
329
|
+
filePathes.forEach(({ file }) => {
|
|
330
|
+
if (!customResourceComponents.includes(file)) {
|
|
331
|
+
if (file === undefined) {
|
|
332
|
+
throw new Error('file is undefined');
|
|
333
|
+
}
|
|
334
|
+
customResourceComponents.push(file);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
}
|
|
328
338
|
this.adminforth.config.resources.forEach((resource) => {
|
|
329
339
|
var _a;
|
|
330
340
|
resource.columns.forEach((column) => {
|
|
@@ -341,17 +351,15 @@ class CodeInjector {
|
|
|
341
351
|
});
|
|
342
352
|
(Object.values(((_a = resource.options) === null || _a === void 0 ? void 0 : _a.pageInjections) || {})).forEach((injection) => {
|
|
343
353
|
Object.values(injection).forEach((filePathes) => {
|
|
344
|
-
filePathes
|
|
345
|
-
if (!customResourceComponents.includes(file)) {
|
|
346
|
-
if (file === undefined) {
|
|
347
|
-
throw new Error('file is undefined');
|
|
348
|
-
}
|
|
349
|
-
customResourceComponents.push(file);
|
|
350
|
-
}
|
|
351
|
-
});
|
|
354
|
+
checkInjections(filePathes);
|
|
352
355
|
});
|
|
353
356
|
});
|
|
354
357
|
});
|
|
358
|
+
if ((_e = this.adminforth.config.customization) === null || _e === void 0 ? void 0 : _e.globalInjections) {
|
|
359
|
+
Object.values(this.adminforth.config.customization.globalInjections).forEach((injection) => {
|
|
360
|
+
checkInjections(injection);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
355
363
|
customResourceComponents.forEach((filePath) => {
|
|
356
364
|
const componentName = getComponentNameFromPath(filePath);
|
|
357
365
|
this.allComponentNames[filePath] = componentName;
|
|
@@ -376,7 +384,7 @@ class CodeInjector {
|
|
|
376
384
|
}
|
|
377
385
|
let imports = iconImports + '\n';
|
|
378
386
|
imports += customComponentsImports + '\n';
|
|
379
|
-
if ((
|
|
387
|
+
if ((_f = this.adminforth.config.customization) === null || _f === void 0 ? void 0 : _f.vueUsesFile) {
|
|
380
388
|
imports += `import addCustomUses from '${this.adminforth.config.customization.vueUsesFile}';\n`;
|
|
381
389
|
}
|
|
382
390
|
// inject that code into spa_tmp/src/App.vue
|
|
@@ -384,12 +392,12 @@ class CodeInjector {
|
|
|
384
392
|
let appVueContent = yield fs.promises.readFile(appVuePath, 'utf-8');
|
|
385
393
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH IMPORTS */', imports);
|
|
386
394
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH COMPONENT REGISTRATIONS */', iconComponents + '\n' + customComponentsComponents + '\n');
|
|
387
|
-
if ((
|
|
395
|
+
if ((_g = this.adminforth.config.customization) === null || _g === void 0 ? void 0 : _g.vueUsesFile) {
|
|
388
396
|
appVueContent = appVueContent.replace('/* IMPORTANT:ADMINFORTH CUSTOM USES */', 'addCustomUses(app);');
|
|
389
397
|
}
|
|
390
398
|
yield fs.promises.writeFile(appVuePath, appVueContent);
|
|
391
399
|
// generate tailwind extend styles
|
|
392
|
-
const stylesGenerator = new StylesGenerator((
|
|
400
|
+
const stylesGenerator = new StylesGenerator((_h = this.adminforth.config.customization) === null || _h === void 0 ? void 0 : _h.styles);
|
|
393
401
|
const stylesText = JSON.stringify(stylesGenerator.mergeStyles(), null, 2).slice(1, -1);
|
|
394
402
|
let tailwindConfigPath = path.join(CodeInjector.SPA_TMP_PATH, 'tailwind.config.js');
|
|
395
403
|
let tailwindConfigContent = yield fs.promises.readFile(tailwindConfigPath, 'utf-8');
|
|
@@ -402,7 +410,7 @@ class CodeInjector {
|
|
|
402
410
|
const indexHtmlPath = path.join(CodeInjector.SPA_TMP_PATH, 'index.html');
|
|
403
411
|
let indexHtmlContent = yield fs.promises.readFile(indexHtmlPath, 'utf-8');
|
|
404
412
|
indexHtmlContent = indexHtmlContent.replace('/* IMPORTANT:ADMINFORTH TITLE */', `${this.adminforth.config.customization.title || 'AdminForth'}`);
|
|
405
|
-
indexHtmlContent = indexHtmlContent.replace('/* IMPORTANT:ADMINFORTH FAVICON */', ((
|
|
413
|
+
indexHtmlContent = indexHtmlContent.replace('/* IMPORTANT:ADMINFORTH FAVICON */', ((_j = this.adminforth.config.customization.favicon) === null || _j === void 0 ? void 0 : _j.replace('@@/', `${this.adminforth.baseUrlSlashed}assets/`))
|
|
406
414
|
||
|
|
407
415
|
`${this.adminforth.baseUrlSlashed}assets/favicon.png`);
|
|
408
416
|
yield fs.promises.writeFile(indexHtmlPath, indexHtmlContent);
|
|
@@ -417,7 +425,7 @@ class CodeInjector {
|
|
|
417
425
|
}
|
|
418
426
|
let homePagePath = homepageMenuItem.path || `/resource/${homepageMenuItem.resourceId}`;
|
|
419
427
|
if (!homePagePath) {
|
|
420
|
-
homePagePath = ((
|
|
428
|
+
homePagePath = ((_k = this.adminforth.config.menu.filter((mi) => mi.path)[0]) === null || _k === void 0 ? void 0 : _k.path) || `/resource/${(_l = this.adminforth.config.menu.filter((mi) => mi.children)[0]) === null || _l === void 0 ? void 0 : _l.resourceId}`;
|
|
421
429
|
}
|
|
422
430
|
routes += `{
|
|
423
431
|
path: '/',
|
|
@@ -434,7 +442,7 @@ class CodeInjector {
|
|
|
434
442
|
/* customPackageLock */
|
|
435
443
|
let usersLockHash = '';
|
|
436
444
|
let usersPackages = [];
|
|
437
|
-
if ((
|
|
445
|
+
if ((_m = this.adminforth.config.customization) === null || _m === void 0 ? void 0 : _m.customComponentsDir) {
|
|
438
446
|
[usersLockHash, usersPackages] = yield this.packagesFromNpm(this.adminforth.config.customization.customComponentsDir);
|
|
439
447
|
}
|
|
440
448
|
const pluginPackages = [];
|
|
@@ -19,6 +19,15 @@ export default class ConfigValidator {
|
|
|
19
19
|
this.adminforth = adminforth;
|
|
20
20
|
this.config = config;
|
|
21
21
|
}
|
|
22
|
+
validateAndListifyInjection(obj, key, errors) {
|
|
23
|
+
if (!Array.isArray(obj[key])) {
|
|
24
|
+
// not array
|
|
25
|
+
obj[key] = [obj[key]];
|
|
26
|
+
}
|
|
27
|
+
obj[key].forEach((target, i) => {
|
|
28
|
+
obj[key][i] = this.validateComponent(target, errors);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
22
31
|
checkCustomFileExists(filePath) {
|
|
23
32
|
if (filePath.startsWith('@@/')) {
|
|
24
33
|
const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
|
|
@@ -99,6 +108,9 @@ export default class ConfigValidator {
|
|
|
99
108
|
if (this.config.customization.loginPageInjections === undefined) {
|
|
100
109
|
this.config.customization.loginPageInjections = {};
|
|
101
110
|
}
|
|
111
|
+
if (this.config.customization.globalInjections === undefined) {
|
|
112
|
+
this.config.customization.globalInjections = {};
|
|
113
|
+
}
|
|
102
114
|
if (this.config.customization.loginPageInjections.underInputs === undefined) {
|
|
103
115
|
this.config.customization.loginPageInjections.underInputs = [];
|
|
104
116
|
}
|
|
@@ -297,13 +309,7 @@ export default class ConfigValidator {
|
|
|
297
309
|
}
|
|
298
310
|
Object.entries(value).map(([injection, target]) => {
|
|
299
311
|
if (possibleInjections.includes(injection)) {
|
|
300
|
-
|
|
301
|
-
// not array
|
|
302
|
-
res.options.pageInjections[key][injection] = [target];
|
|
303
|
-
}
|
|
304
|
-
res.options.pageInjections[key][injection].forEach((target, i) => {
|
|
305
|
-
res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
|
|
306
|
-
});
|
|
312
|
+
this.validateAndListifyInjection(res.options.pageInjections[key], injection, errors);
|
|
307
313
|
}
|
|
308
314
|
else {
|
|
309
315
|
const similar = suggestIfTypo(possibleInjections, injection);
|
|
@@ -351,6 +357,18 @@ export default class ConfigValidator {
|
|
|
351
357
|
}
|
|
352
358
|
}
|
|
353
359
|
});
|
|
360
|
+
if (this.config.customization.globalInjections) {
|
|
361
|
+
const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar',];
|
|
362
|
+
Object.keys(this.config.customization.globalInjections).forEach((injection) => {
|
|
363
|
+
if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
|
|
364
|
+
this.validateAndListifyInjection(this.config.customization.globalInjections, injection, errors);
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
|
|
368
|
+
errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
}
|
|
354
372
|
if (!this.config.menu) {
|
|
355
373
|
errors.push('No config.menu defined');
|
|
356
374
|
}
|
package/dist/modules/restApi.js
CHANGED
|
@@ -154,7 +154,7 @@ export default class AdminForthRestAPI {
|
|
|
154
154
|
method: 'GET',
|
|
155
155
|
path: '/get_base_config',
|
|
156
156
|
handler: (_k) => __awaiter(this, [_k], void 0, function* ({ input, adminUser, cookies }) {
|
|
157
|
-
var _l, _m, _o, _p;
|
|
157
|
+
var _l, _m, _o, _p, _q;
|
|
158
158
|
let username = '';
|
|
159
159
|
let userFullName = '';
|
|
160
160
|
const dbUser = adminUser.dbUser;
|
|
@@ -227,6 +227,7 @@ export default class AdminForthRestAPI {
|
|
|
227
227
|
title: (_o = this.adminforth.config.customization) === null || _o === void 0 ? void 0 : _o.title,
|
|
228
228
|
emptyFieldPlaceholder: (_p = this.adminforth.config.customization) === null || _p === void 0 ? void 0 : _p.emptyFieldPlaceholder,
|
|
229
229
|
announcementBadge,
|
|
230
|
+
globalInjections: (_q = this.adminforth.config.customization) === null || _q === void 0 ? void 0 : _q.globalInjections,
|
|
230
231
|
},
|
|
231
232
|
adminUser,
|
|
232
233
|
version: ADMINFORTH_VERSION,
|
|
@@ -243,7 +244,7 @@ export default class AdminForthRestAPI {
|
|
|
243
244
|
server.endpoint({
|
|
244
245
|
method: 'POST',
|
|
245
246
|
path: '/get_resource',
|
|
246
|
-
handler: (
|
|
247
|
+
handler: (_r) => __awaiter(this, [_r], void 0, function* ({ body, adminUser }) {
|
|
247
248
|
const { resourceId } = body;
|
|
248
249
|
if (!this.adminforth.statuses.dbDiscover) {
|
|
249
250
|
return { error: 'Database discovery not started' };
|
|
@@ -277,8 +278,8 @@ export default class AdminForthRestAPI {
|
|
|
277
278
|
server.endpoint({
|
|
278
279
|
method: 'POST',
|
|
279
280
|
path: '/get_resource_data',
|
|
280
|
-
handler: (
|
|
281
|
-
var
|
|
281
|
+
handler: (_s) => __awaiter(this, [_s], void 0, function* ({ body, adminUser }) {
|
|
282
|
+
var _t, _u, _v, _w;
|
|
282
283
|
const { resourceId, source } = body;
|
|
283
284
|
if (['show', 'list'].includes(source) === false) {
|
|
284
285
|
return { error: 'Invalid source, should be list or show' };
|
|
@@ -298,7 +299,7 @@ export default class AdminForthRestAPI {
|
|
|
298
299
|
if (!allowed) {
|
|
299
300
|
return { error };
|
|
300
301
|
}
|
|
301
|
-
for (const hook of listify((
|
|
302
|
+
for (const hook of listify((_u = (_t = resource.hooks) === null || _t === void 0 ? void 0 : _t[source]) === null || _u === void 0 ? void 0 : _u.beforeDatasourceRequest)) {
|
|
302
303
|
const resp = yield hook({ resource, query: body, adminUser });
|
|
303
304
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
304
305
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -381,7 +382,7 @@ export default class AdminForthRestAPI {
|
|
|
381
382
|
})));
|
|
382
383
|
}
|
|
383
384
|
// only after adminforth made all post processing, give user ability to edit it
|
|
384
|
-
for (const hook of listify((
|
|
385
|
+
for (const hook of listify((_w = (_v = resource.hooks) === null || _v === void 0 ? void 0 : _v[source]) === null || _w === void 0 ? void 0 : _w.afterDatasourceResponse)) {
|
|
385
386
|
const resp = yield hook({ resource, response: data.data, adminUser });
|
|
386
387
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
387
388
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -396,8 +397,8 @@ export default class AdminForthRestAPI {
|
|
|
396
397
|
server.endpoint({
|
|
397
398
|
method: 'POST',
|
|
398
399
|
path: '/get_resource_foreign_data',
|
|
399
|
-
handler: (
|
|
400
|
-
var
|
|
400
|
+
handler: (_x) => __awaiter(this, [_x], void 0, function* ({ body, adminUser }) {
|
|
401
|
+
var _y, _z, _0, _1;
|
|
401
402
|
const { resourceId, column } = body;
|
|
402
403
|
if (!this.adminforth.statuses.dbDiscover) {
|
|
403
404
|
return { error: 'Database discovery not started' };
|
|
@@ -418,7 +419,7 @@ export default class AdminForthRestAPI {
|
|
|
418
419
|
}
|
|
419
420
|
const targetResourceId = columnConfig.foreignResource.resourceId;
|
|
420
421
|
const targetResource = this.adminforth.config.resources.find((res) => res.resourceId == targetResourceId);
|
|
421
|
-
for (const hook of listify((
|
|
422
|
+
for (const hook of listify((_z = (_y = columnConfig.foreignResource.hooks) === null || _y === void 0 ? void 0 : _y.dropdownList) === null || _z === void 0 ? void 0 : _z.beforeDatasourceRequest)) {
|
|
422
423
|
const resp = yield hook({ query: body, adminUser, resource: targetResource });
|
|
423
424
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
424
425
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -447,7 +448,7 @@ export default class AdminForthRestAPI {
|
|
|
447
448
|
const response = {
|
|
448
449
|
items
|
|
449
450
|
};
|
|
450
|
-
for (const hook of listify((
|
|
451
|
+
for (const hook of listify((_1 = (_0 = columnConfig.foreignResource.hooks) === null || _0 === void 0 ? void 0 : _0.dropdownList) === null || _1 === void 0 ? void 0 : _1.afterDatasourceResponse)) {
|
|
451
452
|
const resp = yield hook({ response, adminUser, resource: targetResource });
|
|
452
453
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
453
454
|
throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
|
|
@@ -462,7 +463,7 @@ export default class AdminForthRestAPI {
|
|
|
462
463
|
server.endpoint({
|
|
463
464
|
method: 'POST',
|
|
464
465
|
path: '/get_min_max_for_columns',
|
|
465
|
-
handler: (
|
|
466
|
+
handler: (_2) => __awaiter(this, [_2], void 0, function* ({ body }) {
|
|
466
467
|
const { resourceId } = body;
|
|
467
468
|
if (!this.adminforth.statuses.dbDiscover) {
|
|
468
469
|
return { error: 'Database discovery not started' };
|
|
@@ -491,8 +492,8 @@ export default class AdminForthRestAPI {
|
|
|
491
492
|
server.endpoint({
|
|
492
493
|
method: 'POST',
|
|
493
494
|
path: '/create_record',
|
|
494
|
-
handler: (
|
|
495
|
-
var
|
|
495
|
+
handler: (_3) => __awaiter(this, [_3], void 0, function* ({ body, adminUser }) {
|
|
496
|
+
var _4;
|
|
496
497
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
497
498
|
if (!resource) {
|
|
498
499
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
@@ -504,7 +505,7 @@ export default class AdminForthRestAPI {
|
|
|
504
505
|
}
|
|
505
506
|
const { record } = body;
|
|
506
507
|
for (const column of resource.columns) {
|
|
507
|
-
if (((
|
|
508
|
+
if (((_4 = column.required) === null || _4 === void 0 ? void 0 : _4.create) &&
|
|
508
509
|
record[column.name] === undefined &&
|
|
509
510
|
column.showIn.includes(AdminForthResourcePages.create)) {
|
|
510
511
|
return { error: `Column '${column.name}' is required`, ok: false };
|
|
@@ -524,7 +525,7 @@ export default class AdminForthRestAPI {
|
|
|
524
525
|
server.endpoint({
|
|
525
526
|
method: 'POST',
|
|
526
527
|
path: '/update_record',
|
|
527
|
-
handler: (
|
|
528
|
+
handler: (_5) => __awaiter(this, [_5], void 0, function* ({ body, adminUser }) {
|
|
528
529
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
529
530
|
if (!resource) {
|
|
530
531
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
@@ -554,7 +555,7 @@ export default class AdminForthRestAPI {
|
|
|
554
555
|
server.endpoint({
|
|
555
556
|
method: 'POST',
|
|
556
557
|
path: '/delete_record',
|
|
557
|
-
handler: (
|
|
558
|
+
handler: (_6) => __awaiter(this, [_6], void 0, function* ({ body, adminUser }) {
|
|
558
559
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
559
560
|
const record = yield this.adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, body['primaryKey']);
|
|
560
561
|
if (!resource) {
|
|
@@ -584,7 +585,7 @@ export default class AdminForthRestAPI {
|
|
|
584
585
|
server.endpoint({
|
|
585
586
|
method: 'POST',
|
|
586
587
|
path: '/start_bulk_action',
|
|
587
|
-
handler: (
|
|
588
|
+
handler: (_7) => __awaiter(this, [_7], void 0, function* ({ body, adminUser }) {
|
|
588
589
|
const { resourceId, actionId, recordIds } = body;
|
|
589
590
|
const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
|
|
590
591
|
if (!resource) {
|
package/index.ts
CHANGED
|
@@ -12,8 +12,7 @@ import {
|
|
|
12
12
|
type IConfigValidator,
|
|
13
13
|
IOperationalResource,
|
|
14
14
|
AdminForthFilterOperators,
|
|
15
|
-
AdminForthDataTypes,
|
|
16
|
-
IHttpServer,
|
|
15
|
+
AdminForthDataTypes, IHttpServer,
|
|
17
16
|
BeforeSaveFunction,
|
|
18
17
|
AfterSaveFunction,
|
|
19
18
|
AdminUser,
|
|
@@ -25,7 +24,6 @@ import ConfigValidator from './modules/configValidator.js';
|
|
|
25
24
|
import AdminForthRestAPI, { interpretResource } from './modules/restApi.js';
|
|
26
25
|
import ClickhouseConnector from './dataConnectors/clickhouse.js';
|
|
27
26
|
import OperationalResource from './modules/operationalResource.js';
|
|
28
|
-
import { error } from 'console';
|
|
29
27
|
|
|
30
28
|
// exports
|
|
31
29
|
export * from './types/AdminForthConfig.js';
|
package/modules/codeInjector.ts
CHANGED
|
@@ -356,6 +356,18 @@ class CodeInjector implements ICodeInjector {
|
|
|
356
356
|
|
|
357
357
|
// for each custom component generate import statement
|
|
358
358
|
const customResourceComponents = [];
|
|
359
|
+
|
|
360
|
+
function checkInjections(filePathes) {
|
|
361
|
+
filePathes.forEach(({ file }) => {
|
|
362
|
+
if (!customResourceComponents.includes(file)) {
|
|
363
|
+
if (file === undefined) {
|
|
364
|
+
throw new Error('file is undefined');
|
|
365
|
+
}
|
|
366
|
+
customResourceComponents.push(file);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
359
371
|
this.adminforth.config.resources.forEach((resource) => {
|
|
360
372
|
resource.columns.forEach((column) => {
|
|
361
373
|
if (column.components) {
|
|
@@ -369,20 +381,21 @@ class CodeInjector implements ICodeInjector {
|
|
|
369
381
|
});
|
|
370
382
|
}
|
|
371
383
|
});
|
|
384
|
+
|
|
372
385
|
(Object.values(resource.options?.pageInjections || {})).forEach((injection) => {
|
|
373
386
|
Object.values(injection).forEach((filePathes: {file: string}[]) => {
|
|
374
|
-
filePathes
|
|
375
|
-
if (!customResourceComponents.includes(file)) {
|
|
376
|
-
if (file === undefined) {
|
|
377
|
-
throw new Error('file is undefined');
|
|
378
|
-
}
|
|
379
|
-
customResourceComponents.push(file);
|
|
380
|
-
}
|
|
381
|
-
});
|
|
387
|
+
checkInjections(filePathes);
|
|
382
388
|
});
|
|
383
389
|
});
|
|
384
390
|
});
|
|
385
391
|
|
|
392
|
+
if (this.adminforth.config.customization?.globalInjections) {
|
|
393
|
+
Object.values(this.adminforth.config.customization.globalInjections).forEach((injection) => {
|
|
394
|
+
checkInjections(injection);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
386
399
|
customResourceComponents.forEach((filePath) => {
|
|
387
400
|
const componentName = getComponentNameFromPath(filePath);
|
|
388
401
|
this.allComponentNames[filePath] = componentName;
|
|
@@ -15,6 +15,9 @@ import { guessLabelFromName, suggestIfTypo } from './utils.js';
|
|
|
15
15
|
|
|
16
16
|
import crypto from 'crypto';
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
18
21
|
export default class ConfigValidator implements IConfigValidator {
|
|
19
22
|
|
|
20
23
|
constructor(private adminforth: IAdminForth, private config: AdminForthConfig) {
|
|
@@ -22,6 +25,16 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
22
25
|
this.config = config;
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
validateAndListifyInjection(obj, key, errors) {
|
|
29
|
+
if (!Array.isArray(obj[key])) {
|
|
30
|
+
// not array
|
|
31
|
+
obj[key] = [obj[key]];
|
|
32
|
+
}
|
|
33
|
+
obj[key].forEach((target, i) => {
|
|
34
|
+
obj[key][i] = this.validateComponent(target, errors);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
checkCustomFileExists(filePath: string): Array<string> {
|
|
26
39
|
if (filePath.startsWith('@@/')) {
|
|
27
40
|
const checkPath = path.join(this.config.customization.customComponentsDir, filePath.replace('@@/', ''));
|
|
@@ -112,7 +125,9 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
112
125
|
if (this.config.customization.loginPageInjections === undefined) {
|
|
113
126
|
this.config.customization.loginPageInjections = {};
|
|
114
127
|
}
|
|
115
|
-
|
|
128
|
+
if (this.config.customization.globalInjections === undefined) {
|
|
129
|
+
this.config.customization.globalInjections = {};
|
|
130
|
+
}
|
|
116
131
|
if (this.config.customization.loginPageInjections.underInputs === undefined) {
|
|
117
132
|
this.config.customization.loginPageInjections.underInputs = [];
|
|
118
133
|
}
|
|
@@ -339,7 +354,11 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
339
354
|
const possibleInjections = ['beforeBreadcrumbs', 'afterBreadcrumbs', 'bottom', 'threeDotsDropdownItems', 'customActionIcons'];
|
|
340
355
|
const possiblePages = ['list', 'show', 'create', 'edit'];
|
|
341
356
|
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
|
|
342
360
|
if (res.options.pageInjections) {
|
|
361
|
+
|
|
343
362
|
Object.entries(res.options.pageInjections).map(([key, value]) => {
|
|
344
363
|
if (!possiblePages.includes(key)) {
|
|
345
364
|
const similar = suggestIfTypo(possiblePages, key);
|
|
@@ -348,13 +367,7 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
348
367
|
|
|
349
368
|
Object.entries(value).map(([injection, target]) => {
|
|
350
369
|
if (possibleInjections.includes(injection)) {
|
|
351
|
-
|
|
352
|
-
// not array
|
|
353
|
-
res.options.pageInjections[key][injection] = [target];
|
|
354
|
-
}
|
|
355
|
-
res.options.pageInjections[key][injection].forEach((target, i) => {
|
|
356
|
-
res.options.pageInjections[key][injection][i] = this.validateComponent(target, errors);
|
|
357
|
-
});
|
|
370
|
+
this.validateAndListifyInjection(res.options.pageInjections[key], injection, errors);
|
|
358
371
|
} else {
|
|
359
372
|
const similar = suggestIfTypo(possibleInjections, injection);
|
|
360
373
|
errors.push(`Resource "${res.resourceId}" has invalid pageInjection key "${injection}", Supported keys are ${possibleInjections.join(', ')} ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
@@ -362,6 +375,7 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
362
375
|
});
|
|
363
376
|
|
|
364
377
|
})
|
|
378
|
+
|
|
365
379
|
}
|
|
366
380
|
|
|
367
381
|
// transform all hooks Functions to array of functions
|
|
@@ -407,6 +421,18 @@ export default class ConfigValidator implements IConfigValidator {
|
|
|
407
421
|
}
|
|
408
422
|
});
|
|
409
423
|
|
|
424
|
+
if (this.config.customization.globalInjections) {
|
|
425
|
+
const ALLOWED_GLOBAL_INJECTIONS = ['userMenu', 'header', 'sidebar',]
|
|
426
|
+
Object.keys(this.config.customization.globalInjections).forEach((injection) => {
|
|
427
|
+
if (ALLOWED_GLOBAL_INJECTIONS.includes(injection)) {
|
|
428
|
+
this.validateAndListifyInjection(this.config.customization.globalInjections, injection, errors);
|
|
429
|
+
} else {
|
|
430
|
+
const similar = suggestIfTypo(ALLOWED_GLOBAL_INJECTIONS, injection);
|
|
431
|
+
errors.push(`Global injection key "${injection}" is not allowed. Allowed keys are ${ALLOWED_GLOBAL_INJECTIONS.join(', ')}. ${similar ? `Did you mean "${similar}"?` : ''}`);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
410
436
|
if (!this.config.menu) {
|
|
411
437
|
errors.push('No config.menu defined');
|
|
412
438
|
}
|
package/modules/restApi.ts
CHANGED
|
@@ -265,6 +265,7 @@ export default class AdminForthRestAPI {
|
|
|
265
265
|
title: this.adminforth.config.customization?.title,
|
|
266
266
|
emptyFieldPlaceholder: this.adminforth.config.customization?.emptyFieldPlaceholder,
|
|
267
267
|
announcementBadge,
|
|
268
|
+
globalInjections: this.adminforth.config.customization?.globalInjections,
|
|
268
269
|
},
|
|
269
270
|
adminUser,
|
|
270
271
|
version: ADMINFORTH_VERSION,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adminforth",
|
|
3
|
-
"version": "1.3.52-next.
|
|
3
|
+
"version": "1.3.52-next.1",
|
|
4
4
|
"description": "OpenSource Vue3 powered forth-generation admin panel",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"type": "module",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@clickhouse/client": "^1.4.0",
|
|
22
|
+
"adminforth": "^1.3.52-next.0",
|
|
22
23
|
"better-sqlite3": "^10.0.0",
|
|
23
24
|
"dayjs": "^1.11.11",
|
|
24
25
|
"express": "^4.21.0",
|
package/spa/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
</script> -->
|
|
17
17
|
|
|
18
18
|
</head>
|
|
19
|
-
<body class=" ">
|
|
20
|
-
<div id="app" class="
|
|
19
|
+
<body class="min-h-screen flex flex-column">
|
|
20
|
+
<div id="app" class="grow bg-lightHtml dark:bg-darkHtml"></div>
|
|
21
21
|
<script type="module" src="/src/main.ts"></script>
|
|
22
22
|
</body>
|
|
23
23
|
</html>
|
package/spa/src/App.vue
CHANGED
|
@@ -17,7 +17,20 @@
|
|
|
17
17
|
|
|
18
18
|
</div>
|
|
19
19
|
<div class="flex items-center">
|
|
20
|
+
|
|
21
|
+
<component
|
|
22
|
+
v-for="c in coreStore?.config?.globalInjections?.header || []"
|
|
23
|
+
:is="getCustomComponent(c)"
|
|
24
|
+
:meta="c.meta"
|
|
25
|
+
:record="coreStore.record"
|
|
26
|
+
:resource="coreStore.resource"
|
|
27
|
+
:adminUser="coreStore.adminUser"
|
|
28
|
+
/>
|
|
29
|
+
|
|
20
30
|
<div class="flex items-center ms-3 ">
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
21
34
|
<span @click="toggleTheme" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-lightHtml dark:text-darkSidebarTextHover dark:hover:bg-darkHtml dark:hover:text-darkSidebarTextActive" role="menuitem">
|
|
22
35
|
<IconMoonSolid class="w-5 h-5 text-blue-300" v-if="theme !== 'dark'" />
|
|
23
36
|
<IconSunSolid class="w-5 h-5 text-yellow-300" v-else />
|
|
@@ -41,9 +54,15 @@
|
|
|
41
54
|
</p>
|
|
42
55
|
</div>
|
|
43
56
|
<ul class="py-1" role="none">
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
<li v-for="c in coreStore?.config?.globalInjections?.userMenu || []">
|
|
58
|
+
<component
|
|
59
|
+
:is="getCustomComponent(c)"
|
|
60
|
+
:meta="c.meta"
|
|
61
|
+
:record="coreStore.record"
|
|
62
|
+
:resource="coreStore.resource"
|
|
63
|
+
:adminUser="coreStore.adminUser"
|
|
64
|
+
/>
|
|
65
|
+
</li>
|
|
47
66
|
<li>
|
|
48
67
|
<button @click="logout" class="cursor-pointer flex items-center gap-1 block px-4 py-2 text-sm text-black hover:bg-html dark:text-darkSidebarTextHover dark:hover:bg-darkSidebarItemHover dark:hover:text-darkSidebarTextActive w-full" role="menuitem">Sign out</button>
|
|
49
68
|
</li>
|
|
@@ -145,6 +164,15 @@
|
|
|
145
164
|
</p>
|
|
146
165
|
<!-- <a class="text-sm text-lightPrimary underline font-medium hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300" href="#">Turn new navigation off</a> -->
|
|
147
166
|
</div>
|
|
167
|
+
|
|
168
|
+
<component
|
|
169
|
+
v-for="c in coreStore?.config?.globalInjections?.sidebar || []"
|
|
170
|
+
:is="getCustomComponent(c)"
|
|
171
|
+
:meta="c.meta"
|
|
172
|
+
:record="coreStore.record"
|
|
173
|
+
:resource="coreStore.resource"
|
|
174
|
+
:adminUser="coreStore.adminUser"
|
|
175
|
+
/>
|
|
148
176
|
</div>
|
|
149
177
|
</aside>
|
|
150
178
|
|
|
@@ -237,7 +265,9 @@ import { createHead } from 'unhead'
|
|
|
237
265
|
import { loadFile } from '@/utils';
|
|
238
266
|
import Toast from './components/Toast.vue';
|
|
239
267
|
import {useToastStore} from '@/stores/toast';
|
|
240
|
-
import { FrontendAPI } from '@/composables/useStores'
|
|
268
|
+
import { FrontendAPI } from '@/composables/useStores';
|
|
269
|
+
import { getCustomComponent } from '@/utils';
|
|
270
|
+
|
|
241
271
|
// import { link } from 'fs';
|
|
242
272
|
const coreStore = useCoreStore();
|
|
243
273
|
const modalStore = useModalStore();
|
|
@@ -1075,6 +1075,8 @@ export type AdminForthResource = {
|
|
|
1075
1075
|
*
|
|
1076
1076
|
*/
|
|
1077
1077
|
pageInjections?: {
|
|
1078
|
+
|
|
1079
|
+
|
|
1078
1080
|
/**
|
|
1079
1081
|
* Custom components which can be injected into resource list page.
|
|
1080
1082
|
*
|
|
@@ -1405,6 +1407,15 @@ export type AdminForthConfig = {
|
|
|
1405
1407
|
loginPageInjections?: {
|
|
1406
1408
|
underInputs?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1407
1409
|
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Custom panel components or array of components which will be displayed in different parts of the admin panel.
|
|
1413
|
+
*/
|
|
1414
|
+
globalInjections?: {
|
|
1415
|
+
userMenu?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1416
|
+
header?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1417
|
+
sidebar?: AdminForthComponentDeclaration | Array<AdminForthComponentDeclaration>,
|
|
1418
|
+
}
|
|
1408
1419
|
}
|
|
1409
1420
|
|
|
1410
1421
|
/**
|