adminforth 1.2.12 → 1.2.14

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/basePlugin.ts CHANGED
@@ -11,11 +11,13 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
11
11
  pluginDir: string;
12
12
  customFolderName: string = 'custom';
13
13
  pluginInstanceId: string;
14
+ customFolderPath: string;
14
15
 
15
16
  constructor(pluginOptions: any, metaUrl: string) {
16
17
  // set up plugin here
17
18
  this.pluginDir = currentFileDir(metaUrl);
18
19
  this.pluginInstanceId = uuidv4();
20
+ this.customFolderPath = path.join(this.pluginDir, this.customFolderName);
19
21
  }
20
22
 
21
23
  setupEndpoints(server: any) {
@@ -30,14 +32,12 @@ export default class AdminForthPlugin implements IAdminForthPlugin {
30
32
  const key = `@@/plugins/${this.constructor.name}/${componentFile}`;
31
33
  const componentName = getComponentNameFromPath(key);
32
34
 
33
- const absSrcFolder = path.join(this.pluginDir, this.customFolderName);
34
-
35
- if (!this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder]) {
36
- this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder] = `./plugins/${this.constructor.name}/`;
35
+ if (!this.adminforth.codeInjector.srcFoldersToSync[this.customFolderPath]) {
36
+ this.adminforth.codeInjector.srcFoldersToSync[this.customFolderPath] = `./plugins/${this.constructor.name}/`;
37
37
  }
38
38
 
39
39
  if (!this.adminforth.codeInjector.allComponentNames[key]) {
40
- const absSrcPath = path.join(absSrcFolder, componentFile);
40
+ const absSrcPath = path.join(this.customFolderPath, componentFile);
41
41
  if (!fs.existsSync(absSrcPath)) {
42
42
  throw new Error(`Plugin "${this.constructor.name}" tried to use file as component which does not exist at "${absSrcPath}"`);
43
43
  }
@@ -9,6 +9,7 @@ export default class AdminForthPlugin {
9
9
  // set up plugin here
10
10
  this.pluginDir = currentFileDir(metaUrl);
11
11
  this.pluginInstanceId = uuidv4();
12
+ this.customFolderPath = path.join(this.pluginDir, this.customFolderName);
12
13
  }
13
14
  setupEndpoints(server) {
14
15
  }
@@ -18,12 +19,11 @@ export default class AdminForthPlugin {
18
19
  componentPath(componentFile) {
19
20
  const key = `@@/plugins/${this.constructor.name}/${componentFile}`;
20
21
  const componentName = getComponentNameFromPath(key);
21
- const absSrcFolder = path.join(this.pluginDir, this.customFolderName);
22
- if (!this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder]) {
23
- this.adminforth.codeInjector.srcFoldersToSync[absSrcFolder] = `./plugins/${this.constructor.name}/`;
22
+ if (!this.adminforth.codeInjector.srcFoldersToSync[this.customFolderPath]) {
23
+ this.adminforth.codeInjector.srcFoldersToSync[this.customFolderPath] = `./plugins/${this.constructor.name}/`;
24
24
  }
25
25
  if (!this.adminforth.codeInjector.allComponentNames[key]) {
26
- const absSrcPath = path.join(absSrcFolder, componentFile);
26
+ const absSrcPath = path.join(this.customFolderPath, componentFile);
27
27
  if (!fs.existsSync(absSrcPath)) {
28
28
  throw new Error(`Plugin "${this.constructor.name}" tried to use file as component which does not exist at "${absSrcPath}"`);
29
29
  }
package/dist/index.js CHANGED
@@ -49,6 +49,7 @@ class AdminForth {
49
49
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`);
50
50
  }
51
51
  activatePlugins() {
52
+ process.env.HEAVY_DEBUG && console.log('🔌🔌🔌 Activating plugins');
52
53
  for (let resource of this.config.resources) {
53
54
  for (let pluginInstance of resource.plugins || []) {
54
55
  pluginInstance.modifyResourceConfig(this, resource);
@@ -87,6 +87,45 @@ class CodeInjector {
87
87
  }
88
88
  });
89
89
  }
90
+ packagesFromNpm(dir) {
91
+ return __awaiter(this, void 0, void 0, function* () {
92
+ const usersPackagePath = path.join(dir, 'package.json');
93
+ let packageContent = null;
94
+ let lockHash = '';
95
+ let packages = [];
96
+ try {
97
+ packageContent = JSON.parse(yield fs.promises.readFile(usersPackagePath, 'utf-8'));
98
+ }
99
+ catch (e) {
100
+ // user package.json does not exist, user does not have custom components
101
+ }
102
+ if (packageContent) {
103
+ const lockPath = path.join(dir, 'package-lock.json');
104
+ let lock = null;
105
+ try {
106
+ lock = JSON.parse(yield fs.promises.readFile(lockPath, 'utf-8'));
107
+ }
108
+ catch (e) {
109
+ throw new Error(`Custom package-lock.json does not exist in ${dir}, but package.json does.
110
+ We can't determine version of packages without package-lock.json. Please run npm install in ${dir}`);
111
+ }
112
+ lockHash = hashify(lock);
113
+ packages = [
114
+ ...Object.keys(packageContent.dependencies || []),
115
+ ...Object.keys(packageContent.devDependencies || [])
116
+ ].reduce((acc, packageName) => {
117
+ const pack = lock.packages[`node_modules/${packageName}`];
118
+ if (!pack) {
119
+ throw new Error(`Package ${packageName} is not in package-lock.json but is in package.json. Please run 'npm install' in ${dir}`);
120
+ }
121
+ const version = pack.version;
122
+ acc.push(`${packageName}@${version}`);
123
+ return acc;
124
+ }, []);
125
+ }
126
+ return [lockHash, packages];
127
+ });
128
+ }
90
129
  prepareSources(_a) {
91
130
  return __awaiter(this, arguments, void 0, function* ({ filesUpdated }) {
92
131
  var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
@@ -358,30 +397,26 @@ class CodeInjector {
358
397
  const spaLockHash = hashify(spaPackageLock);
359
398
  /* customPackageLock */
360
399
  let usersLockHash = '';
361
- let usersLock = null;
362
- let usersPackage = null;
400
+ let usersPackages = [];
363
401
  if ((_m = this.adminforth.config.customization) === null || _m === void 0 ? void 0 : _m.customComponentsDir) {
364
- const usersPackagePath = path.join(this.adminforth.config.customization.customComponentsDir, 'package.json');
365
- try {
366
- usersPackage = JSON.parse(yield fs.promises.readFile(usersPackagePath, 'utf-8'));
367
- }
368
- catch (e) {
369
- // user package.json does not exist, user does not have custom components
370
- }
371
- if (usersPackage) {
372
- const usersLockPath = path.join(this.adminforth.config.customization.customComponentsDir, 'package-lock.json');
373
- try {
374
- usersLock = JSON.parse(yield fs.promises.readFile(usersLockPath, 'utf-8'));
375
- }
376
- catch (e) {
377
- throw new Error(`Custom package-lock.json does not exist in ${this.adminforth.config.customization.customComponentsDir}, but package.json does.
378
- We can't determine version of packages without package-lock.json. Please run npm install in ${this.adminforth.config.customization.customComponentsDir}`);
379
- }
380
- usersLockHash = hashify(usersLock);
402
+ [usersLockHash, usersPackages] = yield this.packagesFromNpm(this.adminforth.config.customization.customComponentsDir);
403
+ }
404
+ const pluginPackages = [];
405
+ // for every installed plugin generate packages
406
+ for (const plugin of this.adminforth.activatedPlugins) {
407
+ const [lockHash, packages] = yield this.packagesFromNpm(plugin.customFolderPath);
408
+ if (packages.length) {
409
+ pluginPackages.push({
410
+ pluginName: plugin.constructor.name,
411
+ lockHash,
412
+ packages,
413
+ });
381
414
  }
382
415
  }
416
+ // form string "pluginName:lockHash::pLugin2Name:lockHash"
417
+ const pluginsLockHash = pluginPackages.map(({ pluginName, lockHash }) => `${pluginName}>${lockHash}`).join('::');
383
418
  const iconPackagesNamesHash = hashify(iconPackageNames);
384
- const fullHash = `${spaLockHash}::${iconPackagesNamesHash}::${usersLockHash}`;
419
+ const fullHash = `spa>${spaLockHash}::icons>${iconPackagesNamesHash}::user/custom>${usersLockHash}::${pluginsLockHash}`;
385
420
  const hashPath = path.join(CodeInjector.SPA_TMP_PATH, 'node_modules', '.adminforth_hash');
386
421
  try {
387
422
  const existingHash = yield fs.promises.readFile(hashPath, 'utf-8');
@@ -398,19 +433,12 @@ class CodeInjector {
398
433
  process.env.HEAVY_DEBUG && console.log('🪲Hash file does not exist, proceeding with npm ci/install');
399
434
  }
400
435
  yield this.runNpmShell({ command: 'ci', cwd: CodeInjector.SPA_TMP_PATH });
401
- let customPackgeNames = [];
402
- if (usersPackage && usersLock) {
403
- customPackgeNames = [
404
- ...Object.keys(usersPackage.dependencies || []),
405
- ...Object.keys(usersPackage.devDependencies || [])
406
- ].reduce((acc, packageName) => {
407
- const version = usersLock.packages[`node_modules/${packageName}`].version;
408
- acc.push(`${packageName}@${version}`);
409
- return acc;
410
- }, []);
411
- }
412
436
  if (iconPackageNames.length) {
413
- const npmInstallCommand = `install ${[...iconPackageNames, ...customPackgeNames].join(' ')}`;
437
+ const npmInstallCommand = `install ${[
438
+ ...iconPackageNames,
439
+ ...usersPackages,
440
+ ...pluginPackages.map(({ packages }) => packages)
441
+ ].join(' ')}`;
414
442
  yield this.runNpmShell({ command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH });
415
443
  }
416
444
  yield fs.promises.writeFile(hashPath, fullHash);
@@ -466,6 +494,9 @@ class CodeInjector {
466
494
  const directories = [];
467
495
  const files = [];
468
496
  const collectDirectories = (dir) => __awaiter(this, void 0, void 0, function* () {
497
+ if (['node_modules', 'dist'].includes(path.basename(dir))) {
498
+ return;
499
+ }
469
500
  directories.push(dir);
470
501
  const filesAndDirs = yield fs.promises.readdir(dir, { withFileTypes: true });
471
502
  yield Promise.all(filesAndDirs.map((file) => __awaiter(this, void 0, void 0, function* () {
@@ -134,7 +134,7 @@ export default class ConfigValidator {
134
134
  }
135
135
  if (this.config.resources) {
136
136
  this.config.resources.forEach((res) => {
137
- var _a, _b, _c;
137
+ var _a;
138
138
  if (!res.table) {
139
139
  errors.push(`Resource "${res.dataSource}" is missing table`);
140
140
  }
@@ -240,12 +240,13 @@ export default class ConfigValidator {
240
240
  errors.push(`Resource "${res.resourceId}" bulkActions must be an array`);
241
241
  bulkActions = [];
242
242
  }
243
- if (((_c = (_b = res.options) === null || _b === void 0 ? void 0 : _b.allowedActions) === null || _c === void 0 ? void 0 : _c.delete) && !bulkActions.find((action) => action.label === 'Delete checked')) {
243
+ if (!bulkActions.find((action) => action.label === 'Delete checked')) {
244
244
  bulkActions.push({
245
245
  label: `Delete checked`,
246
246
  state: 'danger',
247
247
  icon: 'flowbite:trash-bin-outline',
248
- action: (_d) => __awaiter(this, [_d], void 0, function* ({ selectedIds }) {
248
+ allowed: (_b) => __awaiter(this, [_b], void 0, function* ({ resource, adminUser, allowedActions }) { return allowedActions.delete; }),
249
+ action: (_c) => __awaiter(this, [_c], void 0, function* ({ selectedIds }) {
249
250
  const connector = this.adminforth.connectors[res.dataSource];
250
251
  yield Promise.all(selectedIds.map((recordId) => __awaiter(this, void 0, void 0, function* () {
251
252
  yield connector.deleteRecord({ resource: res, recordId });
@@ -242,9 +242,19 @@ export default class AdminForthRestAPI {
242
242
  return { error: `Resource ${resourceId} not found` };
243
243
  }
244
244
  const { allowedActions } = yield interpretResource(adminUser, resource, {}, ActionCheckSource.DisplayButtons);
245
+ const allowedBulkActions = [];
246
+ yield Promise.all(resource.options.bulkActions.map((action) => __awaiter(this, void 0, void 0, function* () {
247
+ if (action.allowed) {
248
+ const res = yield action.allowed({ adminUser, resource, allowedActions });
249
+ console.log('🪲🪲🪲🪲checking for allowedActions', allowedActions, 'res', res);
250
+ if (res) {
251
+ allowedBulkActions.push(action);
252
+ }
253
+ }
254
+ })));
245
255
  // exclude "plugins" key
246
256
  return {
247
- resource: Object.assign(Object.assign({}, resource), { plugins: undefined, options: Object.assign(Object.assign({}, resource.options), { allowedActions }) })
257
+ resource: Object.assign(Object.assign({}, resource), { plugins: undefined, options: Object.assign(Object.assign({}, resource.options), { bulkActions: allowedBulkActions, allowedActions }) })
248
258
  };
249
259
  }),
250
260
  });
@@ -598,18 +608,21 @@ export default class AdminForthRestAPI {
598
608
  method: 'POST',
599
609
  path: '/start_bulk_action',
600
610
  handler: (_11) => __awaiter(this, [_11], void 0, function* ({ body }) {
601
- const { resourceId, actionId, recordIds } = body;
611
+ const { resourceId, actionId, recordIds, adminUser } = body;
602
612
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
603
613
  if (!resource) {
604
614
  return { error: `Resource '${resourceId}' not found` };
605
615
  }
616
+ const { allowedActions } = yield interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.BulkActionRequest);
606
617
  const action = resource.options.bulkActions.find((act) => act.id == actionId);
607
618
  if (!action) {
608
619
  return { error: `Action '${actionId}' not found` };
609
620
  }
610
- else {
611
- yield action.action({ selectedIds: recordIds });
621
+ const execAllowed = yield action.allowed({ adminUser, resource, recordIds, allowedActions });
622
+ if (!execAllowed) {
623
+ return { error: `Action '${actionId}' is not allowed` };
612
624
  }
625
+ yield action.action({ selectedIds: recordIds });
613
626
  return {
614
627
  actionId,
615
628
  recordIds,
@@ -70,6 +70,7 @@ export var ActionCheckSource;
70
70
  ActionCheckSource["EditRequest"] = "editRequest";
71
71
  ActionCheckSource["CreateRequest"] = "createRequest";
72
72
  ActionCheckSource["DeleteRequest"] = "deleteRequest";
73
+ ActionCheckSource["BulkActionRequest"] = "bulkActionRequest";
73
74
  })(ActionCheckSource || (ActionCheckSource = {}));
74
75
  export var AdminForthDataTypes;
75
76
  (function (AdminForthDataTypes) {
package/index.ts CHANGED
@@ -77,6 +77,7 @@ class AdminForth implements IAdminForth {
77
77
  }
78
78
 
79
79
  activatePlugins() {
80
+ process.env.HEAVY_DEBUG && console.log('🔌🔌🔌 Activating plugins');
80
81
  for (let resource of this.config.resources) {
81
82
  for (let pluginInstance of resource.plugins || []) {
82
83
  pluginInstance.modifyResourceConfig(this, resource);
@@ -11,6 +11,7 @@ import { ADMIN_FORTH_ABSOLUTE_PATH, getComponentNameFromPath, transformObject, d
11
11
  import { styles } from './styles.js'
12
12
  import { AdminForthComponentDeclaration, ICodeInjector } from '../types/AdminForthConfig.js';
13
13
  import { StylesGenerator } from './styleGenerator.js';
14
+ import { string } from 'zod';
14
15
 
15
16
 
16
17
 
@@ -103,6 +104,47 @@ class CodeInjector implements ICodeInjector {
103
104
  }
104
105
  }
105
106
 
107
+ async packagesFromNpm(dir: string): Promise<[string, string[]]> {
108
+ const usersPackagePath = path.join(dir, 'package.json');
109
+ let packageContent: { dependencies: any, devDependencies: any } = null;
110
+ let lockHash: string = '';
111
+ let packages: string[] = [];
112
+ try {
113
+ packageContent = JSON.parse(await fs.promises.readFile(usersPackagePath, 'utf-8'));
114
+ } catch (e) {
115
+ // user package.json does not exist, user does not have custom components
116
+ }
117
+ if (packageContent) {
118
+ const lockPath = path.join(dir, 'package-lock.json');
119
+ let lock = null;
120
+ try {
121
+ lock = JSON.parse(await fs.promises.readFile(lockPath, 'utf-8'));
122
+ } catch (e) {
123
+ throw new Error(`Custom package-lock.json does not exist in ${dir}, but package.json does.
124
+ We can't determine version of packages without package-lock.json. Please run npm install in ${dir}`);
125
+ }
126
+ lockHash = hashify(lock);
127
+
128
+ packages = [
129
+ ...Object.keys(packageContent.dependencies || []),
130
+ ...Object.keys(packageContent.devDependencies || [])
131
+ ].reduce(
132
+ (acc, packageName) => {
133
+ const pack = lock.packages[`node_modules/${packageName}`];
134
+ if (!pack) {
135
+ throw new Error(`Package ${packageName} is not in package-lock.json but is in package.json. Please run 'npm install' in ${dir}`);
136
+ }
137
+ const version = pack.version;
138
+
139
+ acc.push(`${packageName}@${version}`);
140
+ return acc;
141
+ }, []
142
+ );
143
+
144
+ }
145
+ return [lockHash, packages];
146
+ }
147
+
106
148
  async prepareSources({ filesUpdated }: { filesUpdated?: string[] }) {
107
149
  // check SPA_TMP_PATH exists and create if not
108
150
  try {
@@ -251,8 +293,6 @@ class CodeInjector implements ICodeInjector {
251
293
  }
252
294
  }
253
295
 
254
-
255
-
256
296
  //collect all 'icon' fields from resources bulkActions
257
297
  this.adminforth.config.resources.forEach((resource) => {
258
298
  if (resource.options?.bulkActions) {
@@ -409,32 +449,37 @@ class CodeInjector implements ICodeInjector {
409
449
  const spaLockHash = hashify(spaPackageLock);
410
450
 
411
451
  /* customPackageLock */
412
- let usersLockHash = '';
413
- let usersLock = null;
414
- let usersPackage = null;
452
+ let usersLockHash: string = '';
453
+ let usersPackages: string[] = [];
454
+
415
455
 
416
456
  if (this.adminforth.config.customization?.customComponentsDir) {
417
- const usersPackagePath = path.join(this.adminforth.config.customization.customComponentsDir, 'package.json');
418
- try {
419
- usersPackage = JSON.parse(await fs.promises.readFile(usersPackagePath, 'utf-8'));
420
- } catch (e) {
421
- // user package.json does not exist, user does not have custom components
422
- }
423
- if (usersPackage) {
424
- const usersLockPath = path.join(this.adminforth.config.customization.customComponentsDir, 'package-lock.json');
425
- try {
426
- usersLock = JSON.parse(await fs.promises.readFile(usersLockPath, 'utf-8'));
427
- } catch (e) {
428
- throw new Error(`Custom package-lock.json does not exist in ${this.adminforth.config.customization.customComponentsDir}, but package.json does.
429
- We can't determine version of packages without package-lock.json. Please run npm install in ${this.adminforth.config.customization.customComponentsDir}`);
430
- }
431
- usersLockHash = hashify(usersLock);
457
+ [usersLockHash, usersPackages] = await this.packagesFromNpm(this.adminforth.config.customization.customComponentsDir);
458
+ }
459
+
460
+ const pluginPackages: {
461
+ pluginName: string,
462
+ lockHash: string,
463
+ packages: string[],
464
+ }[] = [];
465
+
466
+ // for every installed plugin generate packages
467
+ for (const plugin of this.adminforth.activatedPlugins) {
468
+ const [lockHash, packages] = await this.packagesFromNpm(plugin.customFolderPath);
469
+ if (packages.length) {
470
+ pluginPackages.push({
471
+ pluginName: plugin.constructor.name,
472
+ lockHash,
473
+ packages,
474
+ });
432
475
  }
433
476
  }
477
+ // form string "pluginName:lockHash::pLugin2Name:lockHash"
478
+ const pluginsLockHash = pluginPackages.map(({ pluginName, lockHash }) => `${pluginName}>${lockHash}`).join('::');
434
479
 
435
480
  const iconPackagesNamesHash = hashify(iconPackageNames);
436
481
 
437
- const fullHash = `${spaLockHash}::${iconPackagesNamesHash}::${usersLockHash}`;
482
+ const fullHash = `spa>${spaLockHash}::icons>${iconPackagesNamesHash}::user/custom>${usersLockHash}::${pluginsLockHash}`;
438
483
  const hashPath = path.join(CodeInjector.SPA_TMP_PATH, 'node_modules', '.adminforth_hash');
439
484
 
440
485
  try {
@@ -452,29 +497,20 @@ class CodeInjector implements ICodeInjector {
452
497
 
453
498
  await this.runNpmShell({command: 'ci', cwd: CodeInjector.SPA_TMP_PATH});
454
499
 
455
- let customPackgeNames = [];
456
- if (usersPackage && usersLock) {
457
- customPackgeNames = [
458
- ...Object.keys(usersPackage.dependencies || []),
459
- ...Object.keys(usersPackage.devDependencies || [])
460
- ].reduce(
461
- (acc, packageName) => {
462
- const version = usersLock.packages[`node_modules/${packageName}`].version;
463
- acc.push(`${packageName}@${version}`);
464
- return acc;
465
- }, []
466
- );
467
- }
468
-
500
+
469
501
  if (iconPackageNames.length) {
470
- const npmInstallCommand = `install ${[...iconPackageNames, ...customPackgeNames].join(' ')}`;
502
+ const npmInstallCommand = `install ${[
503
+ ...iconPackageNames,
504
+ ...usersPackages,
505
+ ...pluginPackages.map(({ packages }) => packages)
506
+ ].join(' ')}`;
471
507
  await this.runNpmShell({command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH});
472
508
  }
473
509
 
474
510
  await fs.promises.writeFile(hashPath, fullHash);
475
511
  }
476
512
 
477
- async watchForReprepare({}) {
513
+ async watchForReprepare({}) {
478
514
  const spaPath = path.join(ADMIN_FORTH_ABSOLUTE_PATH, 'spa');
479
515
  // get list of all subdirectories in spa recursively
480
516
  const directories = [];
@@ -528,6 +564,9 @@ async watchForReprepare({}) {
528
564
  const directories = [];
529
565
  const files = []
530
566
  const collectDirectories = async (dir) => {
567
+ if (['node_modules', 'dist'].includes(path.basename(dir))) {
568
+ return;
569
+ }
531
570
  directories.push(dir);
532
571
 
533
572
  const filesAndDirs = await fs.promises.readdir(dir, { withFileTypes: true });
@@ -260,11 +260,12 @@ export default class ConfigValidator implements IConfigValidator {
260
260
  bulkActions = [];
261
261
  }
262
262
 
263
- if (res.options?.allowedActions?.delete && !bulkActions.find((action) => action.label === 'Delete checked')) {
263
+ if (!bulkActions.find((action) => action.label === 'Delete checked')) {
264
264
  bulkActions.push({
265
265
  label: `Delete checked`,
266
266
  state: 'danger',
267
267
  icon: 'flowbite:trash-bin-outline',
268
+ allowed: async ({ resource, adminUser, allowedActions }) => { return allowedActions.delete },
268
269
  action: async ({ selectedIds }) => {
269
270
  const connector = this.adminforth.connectors[res.dataSource];
270
271
  await Promise.all(selectedIds.map(async (recordId) => {
@@ -12,7 +12,8 @@ import {
12
12
  BeforeSaveFunction,
13
13
  AfterDataSourceResponseFunction,
14
14
  BeforeDataSourceRequestFunction,
15
- AfterSaveFunction
15
+ AfterSaveFunction,
16
+ AllowedActionsResolved
16
17
 
17
18
  } from "../types/AdminForthConfig.js";
18
19
 
@@ -223,7 +224,7 @@ export default class AdminForthRestAPI {
223
224
  },
224
225
  });
225
226
 
226
- async function interpretResource(adminUser: AdminUser, resource: AdminForthResource, meta: any, source: ActionCheckSource): Promise<{allowedActions: AllowedActions}> {
227
+ async function interpretResource(adminUser: AdminUser, resource: AdminForthResource, meta: any, source: ActionCheckSource): Promise<{allowedActions: AllowedActionsResolved}> {
227
228
  if (process.env.HEAVY_DEBUG) {
228
229
  console.log('🪲Interpreting resource', resource.resourceId, source);
229
230
  }
@@ -274,13 +275,28 @@ export default class AdminForthRestAPI {
274
275
 
275
276
  const { allowedActions } = await interpretResource(adminUser, resource, {}, ActionCheckSource.DisplayButtons);
276
277
 
278
+
279
+ const allowedBulkActions = [];
280
+ await Promise.all(
281
+ resource.options.bulkActions.map(async (action) => {
282
+ if (action.allowed) {
283
+ const res = await action.allowed({ adminUser, resource, allowedActions });
284
+ console.log('🪲🪲🪲🪲checking for allowedActions', allowedActions, 'res', res);
285
+ if (res) {
286
+ allowedBulkActions.push(action);
287
+ }
288
+ }
289
+ })
290
+ );
291
+
277
292
  // exclude "plugins" key
278
293
  return {
279
294
  resource: {
280
- ...resource,
295
+ ...resource,
281
296
  plugins: undefined,
282
297
  options: {
283
298
  ...resource.options,
299
+ bulkActions: allowedBulkActions,
284
300
  allowedActions,
285
301
  }
286
302
  }
@@ -680,24 +696,29 @@ export default class AdminForthRestAPI {
680
696
  method: 'POST',
681
697
  path: '/start_bulk_action',
682
698
  handler: async ({ body }) => {
683
- const { resourceId, actionId, recordIds } = body;
699
+ const { resourceId, actionId, recordIds, adminUser } = body;
684
700
  const resource = this.adminforth.config.resources.find((res) => res.resourceId == resourceId);
685
701
  if (!resource) {
686
702
  return { error: `Resource '${resourceId}' not found` };
687
703
  }
704
+ const { allowedActions } = await interpretResource(adminUser, resource, { requestBody: body }, ActionCheckSource.BulkActionRequest);
705
+
688
706
  const action = resource.options.bulkActions.find((act) => act.id == actionId);
689
707
  if (!action) {
690
- return { error: `Action '${actionId}' not found` };
691
- } else{
692
- await action.action({selectedIds:recordIds})
693
-
708
+ return { error: `Action '${actionId}' not found` };
709
+ }
710
+
711
+ const execAllowed = await action.allowed({ adminUser, resource, recordIds, allowedActions });
712
+ if (!execAllowed) {
713
+ return { error: `Action '${actionId}' is not allowed` };
694
714
  }
715
+ await action.action({selectedIds:recordIds})
716
+
695
717
  return {
696
718
  actionId,
697
719
  recordIds,
698
720
  resourceId,
699
- status:'success'
700
-
721
+ status: 'success'
701
722
  }
702
723
  }
703
724
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.2.12",
3
+ "version": "1.2.14",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -271,6 +271,7 @@ export interface IAdminForthPlugin {
271
271
  pluginDir: string;
272
272
  customFolderName: string;
273
273
  pluginInstanceId: string;
274
+ customFolderPath: string;
274
275
 
275
276
  /**
276
277
  * AdminForth plugins concept is based on modification of full AdminForth configuration
@@ -728,11 +729,44 @@ export type AdminForthResource = {
728
729
  afterSave?: BeforeSaveFunction | Array<BeforeSaveFunction>,
729
730
  },
730
731
  },
732
+
733
+ /**
734
+ * General options for resource.
735
+ */
731
736
  options?: {
737
+
738
+ /**
739
+ * Default sort for list view.
740
+ * Example:
741
+ *
742
+ * ```ts
743
+ * import { AdminForthSortDirections } from 'adminforth';
744
+ *
745
+ * ...
746
+ *
747
+ * defaultSort: {
748
+ * columnName: 'created_at',
749
+ * direction: AdminForthSortDirections.ASC,
750
+ * }
751
+ * ```
752
+ *
753
+ */
732
754
  defaultSort?: {
755
+
756
+ /**
757
+ * Column name which will be used to sort records.
758
+ */
733
759
  columnName: string,
760
+
761
+ /**
762
+ * Direction of sorting. Can be 'asc' or 'desc'.
763
+ */
734
764
  direction: AdminForthSortDirections | string,
735
765
  }
766
+
767
+ /**
768
+ * Custom bulk actions list
769
+ */
736
770
  bulkActions?: Array<{
737
771
  id?: string,
738
772
  label: string,
@@ -740,7 +774,87 @@ export type AdminForthResource = {
740
774
  icon?: string,
741
775
  action: Function,
742
776
  confirm?: string,
777
+
778
+ /**
779
+ * Allowed callback called to check whether action is allowed for user.
780
+ * 1. It called first time when user goes to list view. If callback returns false, action button will be hidden on list view.
781
+ * 2. This same callback called second time when user clicks an action button. If callback returns false, action will not be executed.
782
+ * In second time recordIds will be passed to callback (because checkbox for items are selected), so you can use this to make additional
783
+ * checks ( for example to check if user has permission for certain records ).
784
+ *
785
+ * Example:
786
+ *
787
+ * ```ts
788
+ * allowed: async ({ resource, adminUser, recordIds }) => {
789
+ * if (adminUser.isRoot || adminUser.dbUser.role !== 'superadmin') {
790
+ * return false;
791
+ * }
792
+ * return true;
793
+ * }
794
+ * ```
795
+ *
796
+ */
797
+ allowed?: ({ resource, adminUser, recordIds }: {
798
+
799
+ /**
800
+ * recordIds will be passed only once user tries to perform bulk action by clicking on button
801
+ */
802
+ recordIds?: Array<any>,
803
+ resource: AdminForthResource,
804
+
805
+ /**
806
+ * Admin user object
807
+ */
808
+ adminUser: AdminUser,
809
+
810
+ /**
811
+ * Allowed standard actions for current user resolved by calling allowedActions callbacks if they are passed.
812
+ * You can use this variable to rely on standard actions permissions. E.g. if you have custom actions "Mark as read", you
813
+ * might want to allow it only for users who have "edit" action allowed:
814
+ *
815
+ * Example:
816
+ *
817
+ * ```ts
818
+ *
819
+ * options: {
820
+ * bulkActions: [
821
+ * {
822
+ * label: 'Mark as read',
823
+ * action: async ({ resource, recordIds }) => {
824
+ * await markAsRead(recordIds);
825
+ * },
826
+ * allowed: ({ allowedActions }) => allowedActions.edit,
827
+ * }
828
+ * ],
829
+ * allowedActions: {
830
+ * edit: ({ resource, adminUser, recordIds }) => {
831
+ * return adminUser.isRoot || adminUser.dbUser.role === 'superadmin';
832
+ * }
833
+ * }
834
+ * }
835
+ * ```
836
+ *
837
+ */
838
+ allowedActions: AllowedActionsResolved,
839
+ }) => Promise<boolean>,
743
840
  }>,
841
+
842
+ /**
843
+ * Allowed actions for resource.
844
+ *
845
+ * Example:
846
+ *
847
+ * ```ts
848
+ * allowedActions: {
849
+ * create: ({ resource, adminUser }) => {
850
+ * // Allow only superadmin or root user to create records
851
+ * return adminUser.isRoot || adminUser.dbUser.role === 'superadmin';
852
+ * },
853
+ * delete: false, // disable delete action for all users
854
+ * }
855
+ * ```
856
+ *
857
+ */
744
858
  allowedActions?: AllowedActions,
745
859
 
746
860
  /**
@@ -1100,6 +1214,7 @@ export enum ActionCheckSource {
1100
1214
  EditRequest = 'editRequest',
1101
1215
  CreateRequest = 'createRequest',
1102
1216
  DeleteRequest = 'deleteRequest',
1217
+ BulkActionRequest = 'bulkActionRequest',
1103
1218
  }
1104
1219
 
1105
1220
  /**
@@ -1111,6 +1226,10 @@ export type AllowedActions = {
1111
1226
  all?: AllowedActionValue;
1112
1227
  }
1113
1228
 
1229
+ export type AllowedActionsResolved = {
1230
+ [key in AllowedActionsEnum]?: boolean
1231
+ }
1232
+
1114
1233
  export type ValidationObject = {
1115
1234
  /**
1116
1235
  * Should be pure string (not RegExp string)