adminforth 1.2.11 → 1.2.13
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 +82 -51
- package/dist/modules/configValidator.js +4 -3
- package/dist/modules/restApi.js +11 -1
- package/index.ts +1 -0
- package/modules/codeInjector.ts +78 -39
- package/modules/configValidator.ts +2 -1
- package/modules/restApi.ts +19 -3
- package/package.json +1 -1
- package/types/AdminForthConfig.ts +37 -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,62 +397,51 @@ class CodeInjector {
|
|
|
358
397
|
const spaLockHash = hashify(spaPackageLock);
|
|
359
398
|
/* customPackageLock */
|
|
360
399
|
let usersLockHash = '';
|
|
361
|
-
let
|
|
400
|
+
let usersPackages = [];
|
|
362
401
|
if ((_m = this.adminforth.config.customization) === null || _m === void 0 ? void 0 : _m.customComponentsDir) {
|
|
363
|
-
|
|
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);
|
|
381
|
-
}
|
|
382
|
-
const iconPackagesNamesHash = hashify(iconPackageNames);
|
|
383
|
-
const fullHash = `${spaLockHash}::${iconPackagesNamesHash}::${usersLockHash}`;
|
|
384
|
-
const hashPath = path.join(CodeInjector.SPA_TMP_PATH, 'node_modules', '.adminforth_hash');
|
|
385
|
-
try {
|
|
386
|
-
const existingHash = yield fs.promises.readFile(hashPath, 'utf-8');
|
|
387
|
-
if (existingHash === fullHash) {
|
|
388
|
-
process.env.HEAVY_DEBUG && console.log(`🪲Hashes match, skipping npm ci/install, from file: ${existingHash}, actual: ${fullHash}`);
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
process.env.HEAVY_DEBUG && console.log(`🪲 Hashes do not match: from file: ${existingHash} actual: ${fullHash}, proceeding with npm ci/install`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
catch (e) {
|
|
396
|
-
// ignore
|
|
397
|
-
process.env.HEAVY_DEBUG && console.log('🪲Hash file does not exist, proceeding with npm ci/install');
|
|
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
|
+
});
|
|
398
414
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
415
|
+
}
|
|
416
|
+
// form string "pluginName:lockHash::pLugin2Name:lockHash"
|
|
417
|
+
const pluginsLockHash = pluginPackages.map(({ pluginName, lockHash }) => `${pluginName}>${lockHash}`).join('::');
|
|
418
|
+
const iconPackagesNamesHash = hashify(iconPackageNames);
|
|
419
|
+
const fullHash = `spa>${spaLockHash}::icons>${iconPackagesNamesHash}::user/custom>${usersLockHash}::${pluginsLockHash}`;
|
|
420
|
+
const hashPath = path.join(CodeInjector.SPA_TMP_PATH, 'node_modules', '.adminforth_hash');
|
|
421
|
+
try {
|
|
422
|
+
const existingHash = yield fs.promises.readFile(hashPath, 'utf-8');
|
|
423
|
+
if (existingHash === fullHash) {
|
|
424
|
+
process.env.HEAVY_DEBUG && console.log(`🪲Hashes match, skipping npm ci/install, from file: ${existingHash}, actual: ${fullHash}`);
|
|
425
|
+
return;
|
|
410
426
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
yield this.runNpmShell({ command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH });
|
|
427
|
+
else {
|
|
428
|
+
process.env.HEAVY_DEBUG && console.log(`🪲 Hashes do not match: from file: ${existingHash} actual: ${fullHash}, proceeding with npm ci/install`);
|
|
414
429
|
}
|
|
415
|
-
yield fs.promises.writeFile(hashPath, fullHash);
|
|
416
430
|
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
// ignore
|
|
433
|
+
process.env.HEAVY_DEBUG && console.log('🪲Hash file does not exist, proceeding with npm ci/install');
|
|
434
|
+
}
|
|
435
|
+
yield this.runNpmShell({ command: 'ci', cwd: CodeInjector.SPA_TMP_PATH });
|
|
436
|
+
if (iconPackageNames.length) {
|
|
437
|
+
const npmInstallCommand = `install ${[
|
|
438
|
+
...iconPackageNames,
|
|
439
|
+
...usersPackages,
|
|
440
|
+
...pluginPackages.map(({ packages }) => packages)
|
|
441
|
+
].join(' ')}`;
|
|
442
|
+
yield this.runNpmShell({ command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH });
|
|
443
|
+
}
|
|
444
|
+
yield fs.promises.writeFile(hashPath, fullHash);
|
|
417
445
|
});
|
|
418
446
|
}
|
|
419
447
|
watchForReprepare(_a) {
|
|
@@ -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
|
});
|
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,30 +449,37 @@ class CodeInjector implements ICodeInjector {
|
|
|
409
449
|
const spaLockHash = hashify(spaPackageLock);
|
|
410
450
|
|
|
411
451
|
/* customPackageLock */
|
|
412
|
-
let usersLockHash = '';
|
|
413
|
-
let
|
|
452
|
+
let usersLockHash: string = '';
|
|
453
|
+
let usersPackages: string[] = [];
|
|
454
|
+
|
|
455
|
+
|
|
414
456
|
if (this.adminforth.config.customization?.customComponentsDir) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
+
});
|
|
431
475
|
}
|
|
476
|
+
}
|
|
477
|
+
// form string "pluginName:lockHash::pLugin2Name:lockHash"
|
|
478
|
+
const pluginsLockHash = pluginPackages.map(({ pluginName, lockHash }) => `${pluginName}>${lockHash}`).join('::');
|
|
432
479
|
|
|
433
480
|
const iconPackagesNamesHash = hashify(iconPackageNames);
|
|
434
481
|
|
|
435
|
-
const fullHash =
|
|
482
|
+
const fullHash = `spa>${spaLockHash}::icons>${iconPackagesNamesHash}::user/custom>${usersLockHash}::${pluginsLockHash}`;
|
|
436
483
|
const hashPath = path.join(CodeInjector.SPA_TMP_PATH, 'node_modules', '.adminforth_hash');
|
|
437
484
|
|
|
438
485
|
try {
|
|
@@ -450,31 +497,20 @@ class CodeInjector implements ICodeInjector {
|
|
|
450
497
|
|
|
451
498
|
await this.runNpmShell({command: 'ci', cwd: CodeInjector.SPA_TMP_PATH});
|
|
452
499
|
|
|
453
|
-
|
|
454
|
-
if (usersPackage && usersLock) {
|
|
455
|
-
customPackgeNames = [
|
|
456
|
-
...Object.keys(usersPackage.dependencies || []),
|
|
457
|
-
...Object.keys(usersPackage.devDependencies || [])
|
|
458
|
-
].reduce(
|
|
459
|
-
(acc, packageName) => {
|
|
460
|
-
const version = usersLock.packages[`node_modules/${packageName}`].version;
|
|
461
|
-
acc.push(`${packageName}@${version}`);
|
|
462
|
-
return acc;
|
|
463
|
-
}, []
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
|
|
500
|
+
|
|
467
501
|
if (iconPackageNames.length) {
|
|
468
|
-
const npmInstallCommand = `install ${[
|
|
502
|
+
const npmInstallCommand = `install ${[
|
|
503
|
+
...iconPackageNames,
|
|
504
|
+
...usersPackages,
|
|
505
|
+
...pluginPackages.map(({ packages }) => packages)
|
|
506
|
+
].join(' ')}`;
|
|
469
507
|
await this.runNpmShell({command: npmInstallCommand, cwd: CodeInjector.SPA_TMP_PATH});
|
|
470
508
|
}
|
|
471
509
|
|
|
472
510
|
await fs.promises.writeFile(hashPath, fullHash);
|
|
473
|
-
|
|
474
511
|
}
|
|
475
|
-
}
|
|
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
|
}
|
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,9 +729,36 @@ 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
|
+
* defaultSort: {
|
|
744
|
+
* columnName: 'created_at',
|
|
745
|
+
* direction: AdminForthSortDirections.ASC,
|
|
746
|
+
* // or
|
|
747
|
+
* // direction: 'asc' // if you are using non-typescript
|
|
748
|
+
* }
|
|
749
|
+
* ```
|
|
750
|
+
*
|
|
751
|
+
*/
|
|
732
752
|
defaultSort?: {
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Column name which will be used to sort records.
|
|
756
|
+
*/
|
|
733
757
|
columnName: string,
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Direction of sorting. Can be 'asc' or 'desc'.
|
|
761
|
+
*/
|
|
734
762
|
direction: AdminForthSortDirections | string,
|
|
735
763
|
}
|
|
736
764
|
bulkActions?: Array<{
|
|
@@ -740,6 +768,11 @@ export type AdminForthResource = {
|
|
|
740
768
|
icon?: string,
|
|
741
769
|
action: Function,
|
|
742
770
|
confirm?: string,
|
|
771
|
+
allowed?: ({ resource, adminUser }: {
|
|
772
|
+
resource: AdminForthResource,
|
|
773
|
+
adminUser: AdminUser,
|
|
774
|
+
allowedActions: AllowedActionsResolved,
|
|
775
|
+
}) => Promise<boolean>,
|
|
743
776
|
}>,
|
|
744
777
|
allowedActions?: AllowedActions,
|
|
745
778
|
|
|
@@ -1111,6 +1144,10 @@ export type AllowedActions = {
|
|
|
1111
1144
|
all?: AllowedActionValue;
|
|
1112
1145
|
}
|
|
1113
1146
|
|
|
1147
|
+
export type AllowedActionsResolved = {
|
|
1148
|
+
[key in AllowedActionsEnum]?: boolean
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1114
1151
|
export type ValidationObject = {
|
|
1115
1152
|
/**
|
|
1116
1153
|
* Should be pure string (not RegExp string)
|