adminforth 1.3.51 → 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.
@@ -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.forEach(({ file }) => {
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 ((_e = this.adminforth.config.customization) === null || _e === void 0 ? void 0 : _e.vueUsesFile) {
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 ((_f = this.adminforth.config.customization) === null || _f === void 0 ? void 0 : _f.vueUsesFile) {
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((_g = this.adminforth.config.customization) === null || _g === void 0 ? void 0 : _g.styles);
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 */', ((_h = this.adminforth.config.customization.favicon) === null || _h === void 0 ? void 0 : _h.replace('@@/', `${this.adminforth.baseUrlSlashed}assets/`))
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 = ((_j = this.adminforth.config.menu.filter((mi) => mi.path)[0]) === null || _j === void 0 ? void 0 : _j.path) || `/resource/${(_k = this.adminforth.config.menu.filter((mi) => mi.children)[0]) === null || _k === void 0 ? void 0 : _k.resourceId}`;
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 ((_l = this.adminforth.config.customization) === null || _l === void 0 ? void 0 : _l.customComponentsDir) {
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
- if (!Array.isArray(res.options.pageInjections[key][injection])) {
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
  }
@@ -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: (_q) => __awaiter(this, [_q], void 0, function* ({ body, adminUser }) {
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: (_r) => __awaiter(this, [_r], void 0, function* ({ body, adminUser }) {
281
- var _s, _t, _u, _v;
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((_t = (_s = resource.hooks) === null || _s === void 0 ? void 0 : _s[source]) === null || _t === void 0 ? void 0 : _t.beforeDatasourceRequest)) {
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((_v = (_u = resource.hooks) === null || _u === void 0 ? void 0 : _u[source]) === null || _v === void 0 ? void 0 : _v.afterDatasourceResponse)) {
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: (_w) => __awaiter(this, [_w], void 0, function* ({ body, adminUser }) {
400
- var _x, _y, _z, _0;
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((_y = (_x = columnConfig.foreignResource.hooks) === null || _x === void 0 ? void 0 : _x.dropdownList) === null || _y === void 0 ? void 0 : _y.beforeDatasourceRequest)) {
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((_0 = (_z = columnConfig.foreignResource.hooks) === null || _z === void 0 ? void 0 : _z.dropdownList) === null || _0 === void 0 ? void 0 : _0.afterDatasourceResponse)) {
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: (_1) => __awaiter(this, [_1], void 0, function* ({ body }) {
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: (_2) => __awaiter(this, [_2], void 0, function* ({ body, adminUser }) {
495
- var _3;
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 (((_3 = column.required) === null || _3 === void 0 ? void 0 : _3.create) &&
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: (_4) => __awaiter(this, [_4], void 0, function* ({ body, adminUser }) {
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: (_5) => __awaiter(this, [_5], void 0, function* ({ body, adminUser }) {
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: (_6) => __awaiter(this, [_6], void 0, function* ({ body, adminUser }) {
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';
@@ -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.forEach(({ file }) => {
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
- if (!Array.isArray(res.options.pageInjections[key][injection])) {
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
  }
@@ -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.51",
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="min-h-screen bg-lightHtml dark:bg-darkHtml"></div>
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
- <!-- <li>
45
-
46
- </li> -->
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();
@@ -23,7 +23,7 @@
23
23
  </div>
24
24
  </th>
25
25
 
26
- <th v-for="c in columnsListed" scope="col" class="px-6 py-3">
26
+ <th v-for="c in columnsListed" scope="col" class="px-2 md:px-3 lg:px-6 py-3">
27
27
 
28
28
  <div @click="(evt) => c.sortable && onSortButtonClick(evt, c.name)"
29
29
  class="flex items-center " :class="{'cursor-pointer':c.sortable}">
@@ -94,7 +94,7 @@
94
94
  <label for="checkbox-table-search-1" class="sr-only">checkbox</label>
95
95
  </div>
96
96
  </td>
97
- <td v-for="c in columnsListed" class="px-6 py-4">
97
+ <td v-for="c in columnsListed" class="px-2 md:px-3 lg:px-6 py-4">
98
98
  <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
99
99
  <component
100
100
  :is="c?.components?.list ? getCustomComponent(c.components.list) : ValueRenderer"
@@ -105,7 +105,7 @@
105
105
  :resource="resource"
106
106
  />
107
107
  </td>
108
- <td class=" items-center px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
108
+ <td class=" items-center px-2 md:px-3 lg:px-6 py-4 cursor-default" @click="(e)=>{e.stopPropagation()}">
109
109
  <div class="flex">
110
110
  <RouterLink
111
111
  v-if="resource.options?.allowedActions.show"
@@ -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
  /**