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 +5 -5
- package/dist/basePlugin.js +4 -4
- package/dist/index.js +1 -0
- package/dist/modules/codeInjector.js +63 -32
- package/dist/modules/configValidator.js +4 -3
- package/dist/modules/restApi.js +17 -4
- package/dist/types/AdminForthConfig.js +1 -0
- package/index.ts +1 -0
- package/modules/codeInjector.ts +76 -37
- package/modules/configValidator.ts +2 -1
- package/modules/restApi.ts +31 -10
- package/package.json +1 -1
- package/types/AdminForthConfig.ts +119 -0
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
|
-
|
|
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(
|
|
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
|
}
|
package/dist/basePlugin.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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(
|
|
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
|
|
362
|
-
let usersPackage = null;
|
|
400
|
+
let usersPackages = [];
|
|
363
401
|
if ((_m = this.adminforth.config.customization) === null || _m === void 0 ? void 0 : _m.customComponentsDir) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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 =
|
|
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 ${[
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 });
|
package/dist/modules/restApi.js
CHANGED
|
@@ -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
|
-
|
|
611
|
-
|
|
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);
|
package/modules/codeInjector.ts
CHANGED
|
@@ -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
|
|
414
|
-
|
|
452
|
+
let usersLockHash: string = '';
|
|
453
|
+
let usersPackages: string[] = [];
|
|
454
|
+
|
|
415
455
|
|
|
416
456
|
if (this.adminforth.config.customization?.customComponentsDir) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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 =
|
|
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
|
-
|
|
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 ${[
|
|
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 (
|
|
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) => {
|
package/modules/restApi.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
691
|
-
}
|
|
692
|
-
|
|
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
|
@@ -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)
|