adminforth 1.3.52-next.0 → 1.3.52-next.2

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,15 +154,18 @@ 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;
161
161
  username = dbUser[this.adminforth.config.auth.usernameField];
162
162
  userFullName = dbUser[this.adminforth.config.auth.userFullNameField];
163
+ const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
164
+ const userPk = dbUser[userResource.columns.find((col) => col.primaryKey).name];
163
165
  const userData = {
164
166
  [this.adminforth.config.auth.usernameField]: username,
165
- [this.adminforth.config.auth.userFullNameField]: userFullName
167
+ [this.adminforth.config.auth.userFullNameField]: userFullName,
168
+ pk: userPk,
166
169
  };
167
170
  const checkIsMenuItemVisible = (menuItem) => {
168
171
  if (typeof menuItem.visible === 'function') {
@@ -227,6 +230,7 @@ export default class AdminForthRestAPI {
227
230
  title: (_o = this.adminforth.config.customization) === null || _o === void 0 ? void 0 : _o.title,
228
231
  emptyFieldPlaceholder: (_p = this.adminforth.config.customization) === null || _p === void 0 ? void 0 : _p.emptyFieldPlaceholder,
229
232
  announcementBadge,
233
+ globalInjections: (_q = this.adminforth.config.customization) === null || _q === void 0 ? void 0 : _q.globalInjections,
230
234
  },
231
235
  adminUser,
232
236
  version: ADMINFORTH_VERSION,
@@ -243,7 +247,7 @@ export default class AdminForthRestAPI {
243
247
  server.endpoint({
244
248
  method: 'POST',
245
249
  path: '/get_resource',
246
- handler: (_q) => __awaiter(this, [_q], void 0, function* ({ body, adminUser }) {
250
+ handler: (_r) => __awaiter(this, [_r], void 0, function* ({ body, adminUser }) {
247
251
  const { resourceId } = body;
248
252
  if (!this.adminforth.statuses.dbDiscover) {
249
253
  return { error: 'Database discovery not started' };
@@ -277,8 +281,8 @@ export default class AdminForthRestAPI {
277
281
  server.endpoint({
278
282
  method: 'POST',
279
283
  path: '/get_resource_data',
280
- handler: (_r) => __awaiter(this, [_r], void 0, function* ({ body, adminUser }) {
281
- var _s, _t, _u, _v;
284
+ handler: (_s) => __awaiter(this, [_s], void 0, function* ({ body, adminUser }) {
285
+ var _t, _u, _v, _w;
282
286
  const { resourceId, source } = body;
283
287
  if (['show', 'list'].includes(source) === false) {
284
288
  return { error: 'Invalid source, should be list or show' };
@@ -298,7 +302,7 @@ export default class AdminForthRestAPI {
298
302
  if (!allowed) {
299
303
  return { error };
300
304
  }
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)) {
305
+ 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
306
  const resp = yield hook({ resource, query: body, adminUser });
303
307
  if (!resp || (!resp.ok && !resp.error)) {
304
308
  throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
@@ -381,7 +385,7 @@ export default class AdminForthRestAPI {
381
385
  })));
382
386
  }
383
387
  // 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)) {
388
+ 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
389
  const resp = yield hook({ resource, response: data.data, adminUser });
386
390
  if (!resp || (!resp.ok && !resp.error)) {
387
391
  throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
@@ -396,8 +400,8 @@ export default class AdminForthRestAPI {
396
400
  server.endpoint({
397
401
  method: 'POST',
398
402
  path: '/get_resource_foreign_data',
399
- handler: (_w) => __awaiter(this, [_w], void 0, function* ({ body, adminUser }) {
400
- var _x, _y, _z, _0;
403
+ handler: (_x) => __awaiter(this, [_x], void 0, function* ({ body, adminUser }) {
404
+ var _y, _z, _0, _1;
401
405
  const { resourceId, column } = body;
402
406
  if (!this.adminforth.statuses.dbDiscover) {
403
407
  return { error: 'Database discovery not started' };
@@ -418,7 +422,7 @@ export default class AdminForthRestAPI {
418
422
  }
419
423
  const targetResourceId = columnConfig.foreignResource.resourceId;
420
424
  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)) {
425
+ 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
426
  const resp = yield hook({ query: body, adminUser, resource: targetResource });
423
427
  if (!resp || (!resp.ok && !resp.error)) {
424
428
  throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
@@ -447,7 +451,7 @@ export default class AdminForthRestAPI {
447
451
  const response = {
448
452
  items
449
453
  };
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)) {
454
+ 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
455
  const resp = yield hook({ response, adminUser, resource: targetResource });
452
456
  if (!resp || (!resp.ok && !resp.error)) {
453
457
  throw new Error(`Hook must return object with {ok: true} or { error: 'Error' } `);
@@ -462,7 +466,7 @@ export default class AdminForthRestAPI {
462
466
  server.endpoint({
463
467
  method: 'POST',
464
468
  path: '/get_min_max_for_columns',
465
- handler: (_1) => __awaiter(this, [_1], void 0, function* ({ body }) {
469
+ handler: (_2) => __awaiter(this, [_2], void 0, function* ({ body }) {
466
470
  const { resourceId } = body;
467
471
  if (!this.adminforth.statuses.dbDiscover) {
468
472
  return { error: 'Database discovery not started' };
@@ -491,8 +495,8 @@ export default class AdminForthRestAPI {
491
495
  server.endpoint({
492
496
  method: 'POST',
493
497
  path: '/create_record',
494
- handler: (_2) => __awaiter(this, [_2], void 0, function* ({ body, adminUser }) {
495
- var _3;
498
+ handler: (_3) => __awaiter(this, [_3], void 0, function* ({ body, adminUser }) {
499
+ var _4;
496
500
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
497
501
  if (!resource) {
498
502
  return { error: `Resource '${body['resourceId']}' not found` };
@@ -504,7 +508,7 @@ export default class AdminForthRestAPI {
504
508
  }
505
509
  const { record } = body;
506
510
  for (const column of resource.columns) {
507
- if (((_3 = column.required) === null || _3 === void 0 ? void 0 : _3.create) &&
511
+ if (((_4 = column.required) === null || _4 === void 0 ? void 0 : _4.create) &&
508
512
  record[column.name] === undefined &&
509
513
  column.showIn.includes(AdminForthResourcePages.create)) {
510
514
  return { error: `Column '${column.name}' is required`, ok: false };
@@ -524,7 +528,7 @@ export default class AdminForthRestAPI {
524
528
  server.endpoint({
525
529
  method: 'POST',
526
530
  path: '/update_record',
527
- handler: (_4) => __awaiter(this, [_4], void 0, function* ({ body, adminUser }) {
531
+ handler: (_5) => __awaiter(this, [_5], void 0, function* ({ body, adminUser }) {
528
532
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
529
533
  if (!resource) {
530
534
  return { error: `Resource '${body['resourceId']}' not found` };
@@ -554,7 +558,7 @@ export default class AdminForthRestAPI {
554
558
  server.endpoint({
555
559
  method: 'POST',
556
560
  path: '/delete_record',
557
- handler: (_5) => __awaiter(this, [_5], void 0, function* ({ body, adminUser }) {
561
+ handler: (_6) => __awaiter(this, [_6], void 0, function* ({ body, adminUser }) {
558
562
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == body['resourceId']);
559
563
  const record = yield this.adminforth.connectors[resource.dataSource].getRecordByPrimaryKey(resource, body['primaryKey']);
560
564
  if (!resource) {
@@ -584,7 +588,7 @@ export default class AdminForthRestAPI {
584
588
  server.endpoint({
585
589
  method: 'POST',
586
590
  path: '/start_bulk_action',
587
- handler: (_6) => __awaiter(this, [_6], void 0, function* ({ body, adminUser }) {
591
+ handler: (_7) => __awaiter(this, [_7], void 0, function* ({ body, adminUser }) {
588
592
  const { resourceId, actionId, recordIds } = body;
589
593
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
590
594
  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
  }
@@ -193,11 +193,15 @@ export default class AdminForthRestAPI {
193
193
 
194
194
  const dbUser = adminUser.dbUser;
195
195
  username = dbUser[this.adminforth.config.auth.usernameField];
196
- userFullName =dbUser[this.adminforth.config.auth.userFullNameField];
196
+ userFullName = dbUser[this.adminforth.config.auth.userFullNameField];
197
+ const userResource = this.adminforth.config.resources.find((res) => res.resourceId === this.adminforth.config.auth.usersResourceId);
198
+
199
+ const userPk = dbUser[userResource.columns.find((col) => col.primaryKey).name];
197
200
 
198
201
  const userData = {
199
202
  [this.adminforth.config.auth.usernameField]: username,
200
- [this.adminforth.config.auth.userFullNameField]: userFullName
203
+ [this.adminforth.config.auth.userFullNameField]: userFullName,
204
+ pk: userPk,
201
205
  };
202
206
  const checkIsMenuItemVisible = (menuItem) => {
203
207
  if (typeof menuItem.visible === 'function') {
@@ -265,6 +269,7 @@ export default class AdminForthRestAPI {
265
269
  title: this.adminforth.config.customization?.title,
266
270
  emptyFieldPlaceholder: this.adminforth.config.customization?.emptyFieldPlaceholder,
267
271
  announcementBadge,
272
+ globalInjections: this.adminforth.config.customization?.globalInjections,
268
273
  },
269
274
  adminUser,
270
275
  version: ADMINFORTH_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.3.52-next.0",
3
+ "version": "1.3.52-next.2",
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,18 @@
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
+ :adminUser="coreStore.adminUser"
26
+ />
27
+
20
28
  <div class="flex items-center ms-3 ">
29
+
30
+
31
+
21
32
  <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
33
  <IconMoonSolid class="w-5 h-5 text-blue-300" v-if="theme !== 'dark'" />
23
34
  <IconSunSolid class="w-5 h-5 text-yellow-300" v-else />
@@ -41,9 +52,13 @@
41
52
  </p>
42
53
  </div>
43
54
  <ul class="py-1" role="none">
44
- <!-- <li>
45
-
46
- </li> -->
55
+ <li v-for="c in coreStore?.config?.globalInjections?.userMenu || []">
56
+ <component
57
+ :is="getCustomComponent(c)"
58
+ :meta="c.meta"
59
+ :adminUser="coreStore.adminUser"
60
+ />
61
+ </li>
47
62
  <li>
48
63
  <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
64
  </li>
@@ -145,6 +160,13 @@
145
160
  </p>
146
161
  <!-- <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
162
  </div>
163
+
164
+ <component
165
+ v-for="c in coreStore?.config?.globalInjections?.sidebar || []"
166
+ :is="getCustomComponent(c)"
167
+ :meta="c.meta"
168
+ :adminUser="coreStore.adminUser"
169
+ />
148
170
  </div>
149
171
  </aside>
150
172
 
@@ -237,7 +259,9 @@ import { createHead } from 'unhead'
237
259
  import { loadFile } from '@/utils';
238
260
  import Toast from './components/Toast.vue';
239
261
  import {useToastStore} from '@/stores/toast';
240
- import { FrontendAPI } from '@/composables/useStores'
262
+ import { FrontendAPI } from '@/composables/useStores';
263
+ import { getCustomComponent } from '@/utils';
264
+
241
265
  // import { link } from 'fs';
242
266
  const coreStore = useCoreStore();
243
267
  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
  /**