adminforth 1.1.20 → 1.1.22
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/dist/index.js +68 -17
- package/dist/plugins/AccessControl/index.js +3 -56
- package/dist/plugins/AuditLog/index.js +66 -0
- package/dist/plugins/AuditLog/types.js +1 -0
- package/dist/plugins/ForeignInlineListPlugin/custom/InlineList.vue +3 -9
- package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +248 -0
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +1 -189
- package/dist/spa/package.json +1 -6
- package/dist/spa/spa/src/components/ResourceListTable.vue +35 -1
- package/dist/spa/spa/src/components/ShowTableItem.vue +14 -0
- package/dist/spa/spa/src/composables/useFrontendApi.ts +14 -0
- package/dist/spa/spa/src/utils.ts +2 -0
- package/dist/spa/spa/src/views/CreateView.vue +3 -5
- package/dist/spa/spa/src/views/EditView.vue +2 -5
- package/dist/spa/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/spa/src/views/ListView.vue +4 -33
- package/dist/spa/src/App.vue +57 -76
- package/dist/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +3 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
- package/dist/spa/src/components/Dropdown.vue +4 -13
- package/dist/spa/src/components/Filters.vue +22 -55
- package/dist/spa/src/components/MenuLink.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +99 -157
- package/dist/spa/src/components/ValueRenderer.vue +1 -11
- package/dist/spa/src/router/index.ts +2 -3
- package/dist/spa/src/stores/core.ts +33 -22
- package/dist/spa/src/utils.ts +3 -5
- package/dist/spa/src/views/CreateView.vue +7 -15
- package/dist/spa/src/views/EditView.vue +8 -36
- package/dist/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/src/views/ListView.vue +23 -35
- package/dist/spa/src/views/LoginView.vue +1 -1
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +9 -20
- package/dist/spa/tailwind.config.js +2 -5
- package/index.ts +87 -17
- package/package.json +1 -1
- package/plugins/AccessControl/index.ts +1 -55
- package/plugins/AccessControl/types.ts +1 -9
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +3 -9
- package/spa/src/components/ResourceListTable.vue +35 -1
- package/spa/src/composables/useFrontendApi.ts +14 -0
- package/spa/src/utils.ts +2 -0
- package/spa/src/views/CreateView.vue +3 -5
- package/spa/src/views/EditView.vue +2 -5
- package/spa/src/views/ListView.vue +4 -33
- package/types/AdminForthConfig.ts +12 -1
- package/dist/spa/src/components/CustomRangePicker.vue +0 -148
package/dist/index.js
CHANGED
|
@@ -23,10 +23,9 @@ import ExpressServer from './servers/express.js';
|
|
|
23
23
|
import { v1 as uuid } from 'uuid';
|
|
24
24
|
import fs from 'fs';
|
|
25
25
|
import { ADMINFORTH_VERSION, listify } from './modules/utils.js';
|
|
26
|
-
import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages } from './types/AdminForthConfig.js';
|
|
26
|
+
import { AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages, AllowedActionsEnum } from './types/AdminForthConfig.js';
|
|
27
27
|
import path from 'path';
|
|
28
28
|
//get array from enum AdminForthResourcePages
|
|
29
|
-
const DEFAULT_ALLOWED_ACTIONS = { create: true, edit: true, show: true, delete: true };
|
|
30
29
|
class AdminForth {
|
|
31
30
|
constructor(config) {
|
|
32
31
|
_AdminForth_defaultConfig.set(this, {
|
|
@@ -211,6 +210,30 @@ class AdminForth {
|
|
|
211
210
|
if (!res.options) {
|
|
212
211
|
res.options = { bulkActions: [], allowedActions: {} };
|
|
213
212
|
}
|
|
213
|
+
if (!res.options.allowedActions) {
|
|
214
|
+
res.options.allowedActions = {
|
|
215
|
+
all: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (Object.keys(res.options.allowedActions).includes('all')) {
|
|
219
|
+
if (Object.keys(res.options.allowedActions).length > 1) {
|
|
220
|
+
errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
223
|
+
if (key !== 'all') {
|
|
224
|
+
res.options.allowedActions[key] = res.options.allowedActions.all;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
delete res.options.allowedActions.all;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// by default allow all actions
|
|
231
|
+
for (const key of Object.keys(AllowedActionsEnum)) {
|
|
232
|
+
if (!Object.keys(res.options.allowedActions).includes(key)) {
|
|
233
|
+
res.options.allowedActions[key] = true;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
214
237
|
//check if resource has bulkActions
|
|
215
238
|
if ((_b = res.options) === null || _b === void 0 ? void 0 : _b.bulkActions) {
|
|
216
239
|
let bulkActions = res.options.bulkActions;
|
|
@@ -256,18 +279,6 @@ class AdminForth {
|
|
|
256
279
|
});
|
|
257
280
|
}
|
|
258
281
|
}
|
|
259
|
-
//add default allowedActions to resources
|
|
260
|
-
if (res.options.allowedActions) {
|
|
261
|
-
//check if allowedActions is an object
|
|
262
|
-
if (typeof res.options.allowedActions !== 'object') {
|
|
263
|
-
errors.push(`Resource "${res.resourceId}" allowedActions must be an object`);
|
|
264
|
-
}
|
|
265
|
-
const userAllowedActions = res.options.allowedActions;
|
|
266
|
-
res.options.allowedActions = Object.assign({}, DEFAULT_ALLOWED_ACTIONS, userAllowedActions);
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
res.options.allowedActions = DEFAULT_ALLOWED_ACTIONS;
|
|
270
|
-
}
|
|
271
282
|
// transform all hooks Functions to array of functions
|
|
272
283
|
if (res.hooks) {
|
|
273
284
|
for (const value of [res.hooks.show, res.hooks.list]) {
|
|
@@ -622,10 +633,29 @@ class AdminForth {
|
|
|
622
633
|
};
|
|
623
634
|
}),
|
|
624
635
|
});
|
|
636
|
+
function interpretResource(adminUser, resource, meta) {
|
|
637
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
638
|
+
var _b;
|
|
639
|
+
yield Promise.all(Object.entries(((_b = resource.options) === null || _b === void 0 ? void 0 : _b.allowedActions) || {}).map((_c) => __awaiter(this, [_c], void 0, function* ([key, value]) {
|
|
640
|
+
// if callable then call
|
|
641
|
+
if (typeof value === 'function') {
|
|
642
|
+
resource.options.allowedActions[key] = yield value(adminUser, resource, meta);
|
|
643
|
+
}
|
|
644
|
+
})));
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
function checkAccess(action, resource) {
|
|
648
|
+
var _b;
|
|
649
|
+
const allowed = (_b = resource.options) === null || _b === void 0 ? void 0 : _b.allowedActions[action];
|
|
650
|
+
if (allowed !== true) {
|
|
651
|
+
return { error: typeof allowed === 'string' ? allowed : 'Action is not allowed', allowed: false };
|
|
652
|
+
}
|
|
653
|
+
return { allowed: true };
|
|
654
|
+
}
|
|
625
655
|
server.endpoint({
|
|
626
656
|
method: 'POST',
|
|
627
657
|
path: '/get_resource',
|
|
628
|
-
handler: (_l) => __awaiter(this, [_l], void 0, function* ({ body }) {
|
|
658
|
+
handler: (_l) => __awaiter(this, [_l], void 0, function* ({ body, adminUser }) {
|
|
629
659
|
const { resourceId } = body;
|
|
630
660
|
if (!this.statuses.dbDiscover) {
|
|
631
661
|
return { error: 'Database discovery not started' };
|
|
@@ -637,6 +667,7 @@ class AdminForth {
|
|
|
637
667
|
if (!resource) {
|
|
638
668
|
return { error: `Resource ${resourceId} not found` };
|
|
639
669
|
}
|
|
670
|
+
yield interpretResource(adminUser, resource, {});
|
|
640
671
|
// exclude "plugins" key
|
|
641
672
|
return { resource: Object.assign(Object.assign({}, resource), { plugins: undefined }) };
|
|
642
673
|
}),
|
|
@@ -660,6 +691,12 @@ class AdminForth {
|
|
|
660
691
|
if (!resource) {
|
|
661
692
|
return { error: `Resource ${resourceId} not found` };
|
|
662
693
|
}
|
|
694
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
695
|
+
const { allowed, error } = checkAccess(source, resource);
|
|
696
|
+
console.log('allowed', allowed, error);
|
|
697
|
+
if (!allowed) {
|
|
698
|
+
return { error };
|
|
699
|
+
}
|
|
663
700
|
for (const hook of listify((_p = (_o = resource.hooks) === null || _o === void 0 ? void 0 : _o[source]) === null || _p === void 0 ? void 0 : _p.beforeDatasourceRequest)) {
|
|
664
701
|
const resp = yield hook({ resource, query: body, adminUser });
|
|
665
702
|
if (!resp || (!resp.ok && !resp.error)) {
|
|
@@ -679,7 +716,7 @@ class AdminForth {
|
|
|
679
716
|
}
|
|
680
717
|
if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
|
|
681
718
|
if (!Array.isArray(filter.value)) {
|
|
682
|
-
throw new Error(`Value for operator '${filter.operator}'
|
|
719
|
+
throw new Error(`Value for operator '${filter.operator}' should be an array`);
|
|
683
720
|
}
|
|
684
721
|
}
|
|
685
722
|
if (filter.operator === AdminForthFilterOperators.IN && filter.value.length === 0) {
|
|
@@ -850,11 +887,15 @@ class AdminForth {
|
|
|
850
887
|
path: '/create_record',
|
|
851
888
|
handler: (_y) => __awaiter(this, [_y], void 0, function* ({ body, adminUser }) {
|
|
852
889
|
var _z, _0, _1, _2, _3;
|
|
853
|
-
console.log('create_record', body, this.config.resources);
|
|
854
890
|
const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
|
|
855
891
|
if (!resource) {
|
|
856
892
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
857
893
|
}
|
|
894
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
895
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.create, resource);
|
|
896
|
+
if (!allowed) {
|
|
897
|
+
return { error };
|
|
898
|
+
}
|
|
858
899
|
const record = body['record'];
|
|
859
900
|
// execute hook if needed
|
|
860
901
|
for (const hook of listify((_0 = (_z = resource.hooks) === null || _z === void 0 ? void 0 : _z.create) === null || _0 === void 0 ? void 0 : _0.beforeSave)) {
|
|
@@ -922,6 +963,11 @@ class AdminForth {
|
|
|
922
963
|
if (!resource) {
|
|
923
964
|
return { error: `Resource '${body['resourceId']}' not found` };
|
|
924
965
|
}
|
|
966
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
967
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.edit, resource);
|
|
968
|
+
if (!allowed) {
|
|
969
|
+
return { error };
|
|
970
|
+
}
|
|
925
971
|
const recordId = body['recordId'];
|
|
926
972
|
const connector = this.connectors[resource.dataSource];
|
|
927
973
|
const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
|
|
@@ -989,6 +1035,11 @@ class AdminForth {
|
|
|
989
1035
|
if (resource.options.allowedActions.delete === false) {
|
|
990
1036
|
return { error: `Resource '${resource.resourceId}' does not allow delete action` };
|
|
991
1037
|
}
|
|
1038
|
+
yield interpretResource(adminUser, resource, { requestBody: body });
|
|
1039
|
+
const { allowed, error } = checkAccess(AllowedActionsEnum.delete, resource);
|
|
1040
|
+
if (!allowed) {
|
|
1041
|
+
return { error };
|
|
1042
|
+
}
|
|
992
1043
|
// execute hook if needed
|
|
993
1044
|
for (const hook of listify((_11 = (_10 = resource.hooks) === null || _10 === void 0 ? void 0 : _10.delete) === null || _11 === void 0 ? void 0 : _11.beforeSave)) {
|
|
994
1045
|
const resp = yield hook({ resource, record, adminUser });
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
-
});
|
|
9
|
-
};
|
|
10
|
-
import { AllowedActionsEnum } from "../../types/AdminForthConfig.js";
|
|
11
1
|
import AdminForthPlugin from "../base.js";
|
|
12
|
-
class
|
|
2
|
+
class AuditLogPlugin extends AdminForthPlugin {
|
|
13
3
|
constructor(options) {
|
|
14
4
|
super(options, import.meta.url);
|
|
15
5
|
this.options = options;
|
|
@@ -17,50 +7,7 @@ class AccessControlPlugin extends AdminForthPlugin {
|
|
|
17
7
|
modifyResourceConfig(adminforth, resourceConfig) {
|
|
18
8
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
19
9
|
this.adminforth = adminforth;
|
|
20
|
-
if (!resourceConfig.hooks) {
|
|
21
|
-
resourceConfig.hooks = {};
|
|
22
|
-
}
|
|
23
|
-
const checkAccess = (adminUser, action, meta) => __awaiter(this, void 0, void 0, function* () {
|
|
24
|
-
const hasAccessOrError = yield this.options.hasAccess(adminUser, action, meta);
|
|
25
|
-
if (hasAccessOrError === true) {
|
|
26
|
-
return { ok: true };
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
return { ok: false, error: hasAccessOrError || AccessControlPlugin.defaultError };
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
const bindHookCheck = (action, hookName) => {
|
|
33
|
-
if (!resourceConfig.hooks[action]) {
|
|
34
|
-
resourceConfig.hooks[action] = {};
|
|
35
|
-
}
|
|
36
|
-
if (!resourceConfig.hooks[action][hookName]) {
|
|
37
|
-
resourceConfig.hooks[action][hookName] = [];
|
|
38
|
-
}
|
|
39
|
-
if (hookName === 'beforeDatasourceRequest') {
|
|
40
|
-
resourceConfig.hooks[action][hookName].unshift((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, query }) {
|
|
41
|
-
return checkAccess(adminUser, action, { query });
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
resourceConfig.hooks[action][hookName].unshift((_b) => __awaiter(this, [_b], void 0, function* ({ adminUser, record }) {
|
|
46
|
-
return checkAccess(adminUser, action, { record });
|
|
47
|
-
}));
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
// List check
|
|
51
|
-
bindHookCheck(AllowedActionsEnum.list, 'beforeDatasourceRequest');
|
|
52
|
-
// Show check
|
|
53
|
-
bindHookCheck(AllowedActionsEnum.show, 'beforeDatasourceRequest');
|
|
54
|
-
// Edit check
|
|
55
|
-
bindHookCheck(AllowedActionsEnum.edit, 'beforeDatasourceRequest');
|
|
56
|
-
// create check
|
|
57
|
-
bindHookCheck(AllowedActionsEnum.create, 'beforeSave');
|
|
58
|
-
// edit check
|
|
59
|
-
bindHookCheck(AllowedActionsEnum.edit, 'beforeSave');
|
|
60
|
-
// delete check
|
|
61
|
-
bindHookCheck(AllowedActionsEnum.delete, 'beforeSave');
|
|
62
|
-
console.log('resourceConfig', resourceConfig);
|
|
63
10
|
}
|
|
64
11
|
}
|
|
65
|
-
|
|
66
|
-
export default
|
|
12
|
+
AuditLogPlugin.defaultError = 'Sorry, you do not have access to this resource.';
|
|
13
|
+
export default AuditLogPlugin;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { AllowedActionsEnum } from "../../types/AdminForthConfig.js";
|
|
11
|
+
import AdminForthPlugin from "../base.js";
|
|
12
|
+
class AccessControlPlugin extends AdminForthPlugin {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
super(options, import.meta.url);
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
modifyResourceConfig(adminforth, resourceConfig) {
|
|
18
|
+
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
19
|
+
this.adminforth = adminforth;
|
|
20
|
+
if (!resourceConfig.hooks) {
|
|
21
|
+
resourceConfig.hooks = {};
|
|
22
|
+
}
|
|
23
|
+
const checkAccess = (adminUser, action, meta) => __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
const hasAccessOrError = yield this.options.hasAccess(adminUser, action, meta);
|
|
25
|
+
if (hasAccessOrError === true) {
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return { ok: false, error: hasAccessOrError || AccessControlPlugin.defaultError };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
const bindHookCheck = (action, hookName) => {
|
|
33
|
+
if (!resourceConfig.hooks[action]) {
|
|
34
|
+
resourceConfig.hooks[action] = {};
|
|
35
|
+
}
|
|
36
|
+
if (!resourceConfig.hooks[action][hookName]) {
|
|
37
|
+
resourceConfig.hooks[action][hookName] = [];
|
|
38
|
+
}
|
|
39
|
+
if (hookName === 'beforeDatasourceRequest') {
|
|
40
|
+
resourceConfig.hooks[action][hookName].unshift((_a) => __awaiter(this, [_a], void 0, function* ({ adminUser, query }) {
|
|
41
|
+
return checkAccess(adminUser, action, { query });
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
resourceConfig.hooks[action][hookName].unshift((_b) => __awaiter(this, [_b], void 0, function* ({ adminUser, record }) {
|
|
46
|
+
return checkAccess(adminUser, action, { record });
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// List check
|
|
51
|
+
bindHookCheck(AllowedActionsEnum.list, 'beforeDatasourceRequest');
|
|
52
|
+
// Show check
|
|
53
|
+
bindHookCheck(AllowedActionsEnum.show, 'beforeDatasourceRequest');
|
|
54
|
+
// Edit check
|
|
55
|
+
bindHookCheck(AllowedActionsEnum.edit, 'beforeDatasourceRequest');
|
|
56
|
+
// create check
|
|
57
|
+
bindHookCheck(AllowedActionsEnum.create, 'beforeSave');
|
|
58
|
+
// edit check
|
|
59
|
+
bindHookCheck(AllowedActionsEnum.edit, 'beforeSave');
|
|
60
|
+
// delete check
|
|
61
|
+
bindHookCheck(AllowedActionsEnum.delete, 'beforeSave');
|
|
62
|
+
console.log('resourceConfig', resourceConfig);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
AccessControlPlugin.defaultError = 'Sorry, you do not have access to this resource.';
|
|
66
|
+
export default AccessControlPlugin;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -104,6 +104,7 @@ import {
|
|
|
104
104
|
IconFilterOutline,
|
|
105
105
|
IconPlusOutline,
|
|
106
106
|
} from '@iconify-prerendered/vue-flowbite';
|
|
107
|
+
import { showErrorTost, showWarningTost, showSuccesTost} from '@/composables/useFrontendApi';
|
|
107
108
|
|
|
108
109
|
import { getIcon } from '@/utils';
|
|
109
110
|
|
|
@@ -145,10 +146,7 @@ const endFilters = computed(() => {
|
|
|
145
146
|
const primaryKeyColumn = selfPrimaryKeyColumn.value;
|
|
146
147
|
|
|
147
148
|
if (!refColumn) {
|
|
148
|
-
|
|
149
|
-
message: `Column with foreignResource.resourceId which is equal to '${props.resource.resourceId}' not found in resource which is specified as foreighResourceId '${listResource.value.resourceId}'`,
|
|
150
|
-
variant: 'danger',
|
|
151
|
-
});
|
|
149
|
+
showErrorTost(`Column with foreignResource.resourceId which is equal to '${props.resource.resourceId}' not found in resource which is specified as foreighResourceId '${listResource.value.resourceId}'`,10000);
|
|
152
150
|
return [];
|
|
153
151
|
}
|
|
154
152
|
return [
|
|
@@ -209,11 +207,7 @@ async function getList() {
|
|
|
209
207
|
});
|
|
210
208
|
|
|
211
209
|
if (data.error) {
|
|
212
|
-
|
|
213
|
-
message: data.error,
|
|
214
|
-
variant: 'danger',
|
|
215
|
-
timeout: 'unlimited',
|
|
216
|
-
});
|
|
210
|
+
showErrorTost(data.error);
|
|
217
211
|
rows.value = [];
|
|
218
212
|
totalRows.value = 0;
|
|
219
213
|
return;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<!-- todo exclude foreign column-->
|
|
4
|
+
<Filters
|
|
5
|
+
v-if="listResource"
|
|
6
|
+
:columns="listResource.columns.filter((c) => c.name !== listResourceRefColumn.name)"
|
|
7
|
+
v-model:filters="filters"
|
|
8
|
+
:columnsMinMax="columnsMinMax"
|
|
9
|
+
:show="filtersShow"
|
|
10
|
+
@hide="filtersShow = false"
|
|
11
|
+
/>
|
|
12
|
+
</Teleport>
|
|
13
|
+
|
|
14
|
+
<td colspan="2">
|
|
15
|
+
<div class="flex items-center gap-1">
|
|
16
|
+
<h4 v-if="listResource"
|
|
17
|
+
class="px-6 py-4"
|
|
18
|
+
>{{ listResource.label }} inline records</h4>
|
|
19
|
+
|
|
20
|
+
<button
|
|
21
|
+
@click="()=>{checkboxes = []}"
|
|
22
|
+
v-if="checkboxes.length"
|
|
23
|
+
data-tooltip-target="tooltip-remove-all"
|
|
24
|
+
data-tooltip-placement="bottom"
|
|
25
|
+
class="flex gap-1 items-center py-1 px-3 me-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
26
|
+
>
|
|
27
|
+
<IconBanOutline class="w-5 h-5 "/>
|
|
28
|
+
<div id="tooltip-remove-all" role="tooltip"
|
|
29
|
+
class="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700">
|
|
30
|
+
Remove selection
|
|
31
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
32
|
+
</div>
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
<button
|
|
36
|
+
v-if="checkboxes.length"
|
|
37
|
+
v-for="(action,i) in listResource?.options?.bulkActions"
|
|
38
|
+
:key="action.id"
|
|
39
|
+
@click="startBulkAction(action.id)"
|
|
40
|
+
class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
|
|
41
|
+
:class="{'bg-red-100 text-red-800 border-red-400 dark:bg-red-700 dark:text-red-400 dark:border-red-400':action.state==='danger', 'bg-green-100 text-green-800 border-green-400 dark:bg-green-700 dark:text-green-400 dark:border-green-400':action.state==='success',
|
|
42
|
+
'bg-blue-100 text-blue-800 border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
|
|
43
|
+
}"
|
|
44
|
+
>
|
|
45
|
+
<component
|
|
46
|
+
v-if="action.icon"
|
|
47
|
+
:is="getIcon(action.icon)"
|
|
48
|
+
class="w-5 h-5 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"></component>
|
|
49
|
+
|
|
50
|
+
{{ `${action.label} (${checkboxes.length})` }}
|
|
51
|
+
</button>
|
|
52
|
+
|
|
53
|
+
<RouterLink v-if="listResource?.options?.allowedActions?.create"
|
|
54
|
+
:to="{
|
|
55
|
+
name: 'resource-create',
|
|
56
|
+
params: { resourceId: listResource.resourceId },
|
|
57
|
+
query: {
|
|
58
|
+
values: encodeURIComponent(JSON.stringify({[listResourceRefColumn.name]: props.record[selfPrimaryKeyColumn.name]})),
|
|
59
|
+
returnTo: $route.fullPath,
|
|
60
|
+
},
|
|
61
|
+
}"
|
|
62
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
63
|
+
>
|
|
64
|
+
<IconPlusOutline class="w-4 h-4 me-2"/>
|
|
65
|
+
Create
|
|
66
|
+
</RouterLink>
|
|
67
|
+
|
|
68
|
+
<button
|
|
69
|
+
class="flex gap-1 items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 rounded-default"
|
|
70
|
+
@click="()=>{filtersShow = !filtersShow}"
|
|
71
|
+
>
|
|
72
|
+
<IconFilterOutline class="w-4 h-4 me-2"/>
|
|
73
|
+
Filter
|
|
74
|
+
<span
|
|
75
|
+
class="bg-red-100 text-red-800 text-xs font-medium px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
|
|
76
|
+
v-if="filters.length">
|
|
77
|
+
{{ filters.length }}
|
|
78
|
+
</span>
|
|
79
|
+
</button>
|
|
80
|
+
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<ResourceListTable
|
|
84
|
+
:resource="listResource"
|
|
85
|
+
:rows="rows"
|
|
86
|
+
@update:page="page = $event"
|
|
87
|
+
@update:sort="sort = $event"
|
|
88
|
+
@update:checkboxes="checkboxes = $event"
|
|
89
|
+
:pageSize="pageSize"
|
|
90
|
+
:totalRows="totalRows"
|
|
91
|
+
:checkboxes="checkboxes"
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
</td>
|
|
95
|
+
</template>
|
|
96
|
+
|
|
97
|
+
<script setup>
|
|
98
|
+
import { callAdminForthApi } from '@/utils';
|
|
99
|
+
import { ref, onMounted, watch, computed } from 'vue';
|
|
100
|
+
import ResourceListTable from '@/components/ResourceListTable.vue';
|
|
101
|
+
import Filters from '@/components/Filters.vue';
|
|
102
|
+
import {
|
|
103
|
+
IconBanOutline,
|
|
104
|
+
IconFilterOutline,
|
|
105
|
+
IconPlusOutline,
|
|
106
|
+
} from '@iconify-prerendered/vue-flowbite';
|
|
107
|
+
|
|
108
|
+
import { getIcon } from '@/utils';
|
|
109
|
+
|
|
110
|
+
const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
|
|
111
|
+
|
|
112
|
+
const listResource = ref(null);
|
|
113
|
+
const loading = ref(true);
|
|
114
|
+
|
|
115
|
+
const page = ref(1);
|
|
116
|
+
const sort = ref([]);
|
|
117
|
+
const checkboxes = ref([]);
|
|
118
|
+
const pageSize = computed(() => listResource.value?.options?.listPageSize || 10);
|
|
119
|
+
|
|
120
|
+
const rows = ref(null);
|
|
121
|
+
const totalRows = ref(0);
|
|
122
|
+
|
|
123
|
+
const filters = ref([]);
|
|
124
|
+
const filtersShow = ref(false);
|
|
125
|
+
const columnsMinMax = ref(null);
|
|
126
|
+
|
|
127
|
+
const listResourceRefColumn = computed(() => {
|
|
128
|
+
if (!listResource.value) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return listResource.value.columns.find(c => c.foreignResource?.resourceId === props.resource.resourceId);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const selfPrimaryKeyColumn = computed(() => {
|
|
135
|
+
return props.resource.columns.find(c => c.primaryKey);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const endFilters = computed(() => {
|
|
139
|
+
if (!listResource.value) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
// get name of the column that is foreign key
|
|
143
|
+
const refColumn = listResourceRefColumn.value;
|
|
144
|
+
|
|
145
|
+
const primaryKeyColumn = selfPrimaryKeyColumn.value;
|
|
146
|
+
|
|
147
|
+
if (!refColumn) {
|
|
148
|
+
window.adminforth.alert({
|
|
149
|
+
message: `Column with foreignResource.resourceId which is equal to '${props.resource.resourceId}' not found in resource which is specified as foreighResourceId '${listResource.value.resourceId}'`,
|
|
150
|
+
variant: 'danger',
|
|
151
|
+
});
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
return [
|
|
155
|
+
...filters.value,
|
|
156
|
+
{
|
|
157
|
+
field: refColumn.name,
|
|
158
|
+
operator: 'eq',
|
|
159
|
+
value: props.record[primaryKeyColumn.name],
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
watch([page], async () => {
|
|
165
|
+
await getList();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
watch([sort], async () => {
|
|
169
|
+
await getList();
|
|
170
|
+
}, {deep: true});
|
|
171
|
+
|
|
172
|
+
watch([filters], async () => {
|
|
173
|
+
page.value = 1;
|
|
174
|
+
checkboxes.value = [];
|
|
175
|
+
await getList();
|
|
176
|
+
}, {deep: true});
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async function startBulkAction(actionId) {
|
|
180
|
+
const data = await callAdminForthApi({
|
|
181
|
+
path: '/start_bulk_action',
|
|
182
|
+
method: 'POST',
|
|
183
|
+
body: {
|
|
184
|
+
resourceId: listResource.value.resourceId,
|
|
185
|
+
actionId: actionId,
|
|
186
|
+
recordIds: checkboxes.value
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
if (data?.status === 'success') {
|
|
190
|
+
checkboxes.value = [];
|
|
191
|
+
}
|
|
192
|
+
await getList();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function getList() {
|
|
196
|
+
rows.value = null;
|
|
197
|
+
console.log('getList', listResource.value)
|
|
198
|
+
const data = await callAdminForthApi({
|
|
199
|
+
path: '/get_resource_data',
|
|
200
|
+
method: 'POST',
|
|
201
|
+
body: {
|
|
202
|
+
source: 'list',
|
|
203
|
+
resourceId: listResource.value.resourceId,
|
|
204
|
+
limit: pageSize.value,
|
|
205
|
+
offset: (page.value - 1) * pageSize.value,
|
|
206
|
+
filters: endFilters.value,
|
|
207
|
+
sort: sort.value,
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (data.error) {
|
|
212
|
+
window.adminforth.alert({
|
|
213
|
+
message: data.error,
|
|
214
|
+
variant: 'danger',
|
|
215
|
+
timeout: 'unlimited',
|
|
216
|
+
});
|
|
217
|
+
rows.value = [];
|
|
218
|
+
totalRows.value = 0;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
rows.value = data.data?.map(row => {
|
|
223
|
+
row._primaryKeyValue = row[listResource.value.columns.find(c => c.primaryKey).name];
|
|
224
|
+
return row;
|
|
225
|
+
});
|
|
226
|
+
totalRows.value = data.total;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
onMounted( async () => {
|
|
230
|
+
loading.value = true;
|
|
231
|
+
const foreighResourceId = props.meta.foreignResourceId;
|
|
232
|
+
listResource.value = (await callAdminForthApi({
|
|
233
|
+
path: `/plugin/${props.meta.pluginInstanceId}/get_resource`,
|
|
234
|
+
method: 'POST',
|
|
235
|
+
body: {},
|
|
236
|
+
})).resource;
|
|
237
|
+
columnsMinMax.value = await callAdminForthApi({
|
|
238
|
+
path: '/get_min_max_for_columns',
|
|
239
|
+
method: 'POST',
|
|
240
|
+
body: {
|
|
241
|
+
resourceId: foreighResourceId,
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
loading.value = false;
|
|
245
|
+
await getList();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
</script>
|
package/dist/spa/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
</script> -->
|
|
17
17
|
|
|
18
18
|
</head>
|
|
19
|
-
<body
|
|
20
|
-
<div id="app" class="min-h-screen
|
|
19
|
+
<body>
|
|
20
|
+
<div id="app" class="min-h-screen dark:bg-gray-800"></div>
|
|
21
21
|
<script type="module" src="/src/main.ts"></script>
|
|
22
22
|
</body>
|
|
23
23
|
</html>
|