adminforth 1.1.19 → 1.1.21

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 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]) {
@@ -568,13 +579,45 @@ class AdminForth {
568
579
  [this.config.auth.usernameField]: username,
569
580
  [this.config.auth.userFullNameField]: userFullName
570
581
  };
582
+ const checkIsMenuItemVisible = (menuItem) => {
583
+ if (typeof menuItem.visible === 'function') {
584
+ const toReturn = menuItem.visible(adminUser);
585
+ if (typeof toReturn !== 'boolean') {
586
+ throw new Error(`'visible' function of ${menuItem.label || menuItem.type} must return boolean value`);
587
+ }
588
+ return toReturn;
589
+ }
590
+ };
591
+ let newMenu = [];
592
+ for (let menuItem of this.config.menu) {
593
+ let newMenuItem = Object.assign({}, menuItem);
594
+ if (menuItem.visible) {
595
+ if (!checkIsMenuItemVisible(menuItem)) {
596
+ continue;
597
+ }
598
+ }
599
+ if (menuItem.children) {
600
+ let newChildren = [];
601
+ for (let child of menuItem.children) {
602
+ let newChild = Object.assign({}, child);
603
+ if (child.visible) {
604
+ if (!checkIsMenuItemVisible(child)) {
605
+ continue;
606
+ }
607
+ }
608
+ newChildren.push(newChild);
609
+ }
610
+ newMenuItem = Object.assign(Object.assign({}, newMenuItem), { children: newChildren });
611
+ }
612
+ newMenu.push(newMenuItem);
613
+ }
571
614
  return {
572
615
  user: userData,
573
616
  resources: this.config.resources.map((res) => ({
574
617
  resourceId: res.resourceId,
575
618
  label: res.label,
576
619
  })),
577
- menu: this.config.menu,
620
+ menu: newMenu,
578
621
  config: {
579
622
  brandName: this.config.customization.brandName,
580
623
  brandLogo: this.config.customization.brandLogo,
@@ -590,10 +633,29 @@ class AdminForth {
590
633
  };
591
634
  }),
592
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
+ }
593
655
  server.endpoint({
594
656
  method: 'POST',
595
657
  path: '/get_resource',
596
- handler: (_l) => __awaiter(this, [_l], void 0, function* ({ body }) {
658
+ handler: (_l) => __awaiter(this, [_l], void 0, function* ({ body, adminUser }) {
597
659
  const { resourceId } = body;
598
660
  if (!this.statuses.dbDiscover) {
599
661
  return { error: 'Database discovery not started' };
@@ -605,6 +667,7 @@ class AdminForth {
605
667
  if (!resource) {
606
668
  return { error: `Resource ${resourceId} not found` };
607
669
  }
670
+ yield interpretResource(adminUser, resource, {});
608
671
  // exclude "plugins" key
609
672
  return { resource: Object.assign(Object.assign({}, resource), { plugins: undefined }) };
610
673
  }),
@@ -628,6 +691,12 @@ class AdminForth {
628
691
  if (!resource) {
629
692
  return { error: `Resource ${resourceId} not found` };
630
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
+ }
631
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)) {
632
701
  const resp = yield hook({ resource, query: body, adminUser });
633
702
  if (!resp || (!resp.ok && !resp.error)) {
@@ -647,7 +716,7 @@ class AdminForth {
647
716
  }
648
717
  if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
649
718
  if (!Array.isArray(filter.value)) {
650
- throw new Error(`Value for operator '${filter.operator}'' should be an array`);
719
+ throw new Error(`Value for operator '${filter.operator}' should be an array`);
651
720
  }
652
721
  }
653
722
  if (filter.operator === AdminForthFilterOperators.IN && filter.value.length === 0) {
@@ -818,11 +887,15 @@ class AdminForth {
818
887
  path: '/create_record',
819
888
  handler: (_y) => __awaiter(this, [_y], void 0, function* ({ body, adminUser }) {
820
889
  var _z, _0, _1, _2, _3;
821
- console.log('create_record', body, this.config.resources);
822
890
  const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
823
891
  if (!resource) {
824
892
  return { error: `Resource '${body['resourceId']}' not found` };
825
893
  }
894
+ yield interpretResource(adminUser, resource, { requestBody: body });
895
+ const { allowed, error } = checkAccess(AllowedActionsEnum.create, resource);
896
+ if (!allowed) {
897
+ return { error };
898
+ }
826
899
  const record = body['record'];
827
900
  // execute hook if needed
828
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)) {
@@ -890,6 +963,11 @@ class AdminForth {
890
963
  if (!resource) {
891
964
  return { error: `Resource '${body['resourceId']}' not found` };
892
965
  }
966
+ yield interpretResource(adminUser, resource, { requestBody: body });
967
+ const { allowed, error } = checkAccess(AllowedActionsEnum.edit, resource);
968
+ if (!allowed) {
969
+ return { error };
970
+ }
893
971
  const recordId = body['recordId'];
894
972
  const connector = this.connectors[resource.dataSource];
895
973
  const oldRecord = yield connector.getRecordByPrimaryKey(resource, recordId);
@@ -957,6 +1035,11 @@ class AdminForth {
957
1035
  if (resource.options.allowedActions.delete === false) {
958
1036
  return { error: `Resource '${resource.resourceId}' does not allow delete action` };
959
1037
  }
1038
+ yield interpretResource(adminUser, resource, { requestBody: body });
1039
+ const { allowed, error } = checkAccess(AllowedActionsEnum.delete, resource);
1040
+ if (!allowed) {
1041
+ return { error };
1042
+ }
960
1043
  // execute hook if needed
961
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)) {
962
1045
  const resp = yield hook({ resource, record, adminUser });
@@ -140,7 +140,7 @@ class CodeInjector {
140
140
  routes += `{
141
141
  path: '${item.path}',
142
142
  name: '${item.path}',
143
- component: () => import('${item.component}'),}
143
+ component: () => import('${item.component}'),
144
144
  meta: { title: '${(_d = item === null || item === void 0 ? void 0 : item.meta) === null || _d === void 0 ? void 0 : _d.title}'
145
145
  },\n`;
146
146
  }
@@ -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 AccessControlPlugin extends AdminForthPlugin {
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
- AccessControlPlugin.defaultError = 'Sorry, you do not have access to this resource.';
66
- export default AccessControlPlugin;
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
- 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
- });
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
- window.adminforth.alert({
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;
@@ -190,7 +190,7 @@
190
190
  <button v-if="resource.options?.allowedActions.delete"
191
191
  class="font-medium text-red-600 dark:text-red-500 hover:underline ms-3"
192
192
  :data-tooltip-target="`tooltip-delete-${rowI}`"
193
- @click="showDeleteModal(row)"
193
+ @click="deleteRecord(row)"
194
194
  >
195
195
  <IconTrashBinSolid class="w-5 h-5 me-2"/>
196
196
  </button>
@@ -272,10 +272,12 @@
272
272
  import { initFlowbite } from 'flowbite';
273
273
  import { computed, onMounted, ref, watch } from 'vue';
274
274
  import { useRoute } from 'vue-router';
275
+ import { callAdminForthApi, getIcon } from '@/utils';
275
276
 
276
277
  import ValueRenderer from '@/components/ValueRenderer.vue';
277
278
  import { getCustomComponent } from '@/utils';
278
279
  import { useCoreStore } from '@/stores/core';
280
+ import { showSuccesTost } from '@/composables/useFrontendApi';
279
281
 
280
282
  import {
281
283
  IconInboxOutline,
@@ -305,6 +307,7 @@ const emits = defineEmits([
305
307
  'update:page',
306
308
  'update:sort',
307
309
  'update:checkboxes',
310
+ 'update:records'
308
311
 
309
312
  ]);
310
313
 
@@ -358,9 +361,11 @@ async function selectAll(value) {
358
361
  const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
359
362
 
360
363
 
364
+
365
+
361
366
  const allFromThisPageChecked = computed(() => {
362
- if (!props.rows.value) return false;
363
- return props.rows.value.every((r) => checkboxes.value.includes(r.id));
367
+ if (!props.rows) return false;
368
+ return props.rows.every((r) => props.checkboxes.includes(r.id));
364
369
  });
365
370
  const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
366
371
  const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
@@ -380,4 +385,35 @@ function onSortButtonClick(field) {
380
385
  }
381
386
  }
382
387
 
388
+ async function deleteRecord(row) {
389
+ const data = await window.adminforth.confirm({
390
+ message: 'Are you sure you want to delete this item?',
391
+ yes: 'Delete',
392
+ no: 'Cancel',
393
+ });
394
+ if (data) {
395
+ try {
396
+ const res = await callAdminForthApi({
397
+ path: '/delete_record',
398
+ method: 'POST',
399
+ body: {
400
+ resourceId: props.resource.resourceId,
401
+ primaryKey: row._primaryKeyValue,
402
+ }});
403
+ if (!res.error){
404
+ emits('update:records', true)
405
+ showSuccesTost('Record deleted successfully')
406
+ } else {
407
+ console.error(res.error)
408
+ }
409
+
410
+ } catch (e) {
411
+ console.error(e);
412
+ };
413
+ }
414
+
415
+
416
+ }
417
+
418
+
383
419
  </script>
@@ -0,0 +1,14 @@
1
+ export function showSuccesTost(message: string) {
2
+ window.adminforth.alert({ message, variant: 'success' });
3
+ return message;
4
+ }
5
+
6
+ export function showWarningTost(message: string) {
7
+ window.adminforth.alert({ message, variant: 'warning' });
8
+ return message;
9
+ }
10
+
11
+ export function showErrorTost(message: string, timeout?: number) {
12
+ window.adminforth.alert({ message, variant: 'danger', timeout: timeout || 'unlimited'});
13
+ return message;
14
+ }
@@ -97,3 +97,5 @@ export function checkAcessByAllowedActions(allowedActions:any, action:any ) {
97
97
  router.back();
98
98
  }
99
99
  }
100
+
101
+
@@ -78,6 +78,7 @@ import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
78
78
  import { onMounted, ref, watch } from 'vue';
79
79
  import { useRoute, useRouter } from 'vue-router';
80
80
  import { computed } from 'vue';
81
+ import { showErrorTost } from '@/composables/useFrontendApi';
81
82
 
82
83
 
83
84
  const isValid = ref(false);
@@ -141,11 +142,8 @@ async function saveRecord() {
141
142
  },
142
143
  });
143
144
  if (response.error) {
144
- window.adminforth.alert({
145
- message: response.error,
146
- variant: 'danger',
147
- timeout: 'unlimited',
148
- })
145
+ showErrorTost(response.error);
146
+
149
147
  }
150
148
  saving.value = false;
151
149
  if (route.query.returnTo) {
@@ -73,6 +73,7 @@ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from
73
73
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
74
74
  import { computed, onMounted, ref } from 'vue';
75
75
  import { useRoute, useRouter } from 'vue-router';
76
+ import { showErrorTost } from '@/composables/useFrontendApi';
76
77
 
77
78
  const coreStore = useCoreStore();
78
79
 
@@ -155,11 +156,7 @@ async function saveRecord() {
155
156
  },
156
157
  });
157
158
  if (resp.error) {
158
- window.adminforth.alert({
159
- message: resp.error,
160
- variant: 'danger',
161
- timeout: 'unlimited',
162
- })
159
+ showErrorTost(resp.error);
163
160
  }
164
161
  saving.value = false;
165
162
  router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
@@ -87,6 +87,7 @@
87
87
  @update:page="page = $event"
88
88
  @update:sort="sort = $event"
89
89
  @update:checkboxes="checkboxes = $event"
90
+ @update:records="getList"
90
91
  :pageSize="pageSize"
91
92
  :totalRows="totalRows"
92
93
  :checkboxes="checkboxes"
@@ -113,6 +114,7 @@ import { callAdminForthApi, getIcon } from '@/utils';
113
114
  import { initFlowbite } from 'flowbite';
114
115
  import { computed, onMounted, ref, watch } from 'vue';
115
116
  import { useRoute } from 'vue-router';
117
+ import { showErrorTost } from '@/composables/useFrontendApi'
116
118
 
117
119
  import ValueRenderer from '@/components/ValueRenderer.vue';
118
120
  import { getCustomComponent } from '@/utils';
@@ -184,11 +186,7 @@ async function getList() {
184
186
  }
185
187
  });
186
188
  if (data.error) {
187
- window.adminforth.alert({
188
- message: data.error,
189
- variant: 'danger',
190
- timeout: 'unlimited',
191
- });
189
+ showErrorTost(data.error);
192
190
  rows.value = [];
193
191
  totalRows.value = 0;
194
192
  return;
@@ -201,36 +199,9 @@ async function getList() {
201
199
  }
202
200
 
203
201
 
204
- function showDeleteModal(row) {
205
- if (!coreStore.config?.deleteConfirmation) {
206
- return deleteRecord(row);
207
- }
208
- modalStore.setModalContent({
209
- content: 'Are you sure you want to delete this item?',
210
- acceptText: 'Delete',
211
- cancelText: 'Cancel',
212
- });
213
- modalStore.setOnAcceptFunction(() => {
214
- return deleteRecord(row)
215
- })
216
- console.log('row', row);
217
- modalStore.togleModal();
218
202
 
219
- }
220
203
 
221
- async function deleteRecord(row) {
222
- await callAdminForthApi({
223
- path: '/delete_record',
224
- method: 'POST',
225
- body: {
226
- resourceId: route.params.resourceId,
227
- primaryKey: row._primaryKeyValue,
228
- recordId: row.id
229
- }
230
- });
231
- await getList();
232
- modalStore.resetmodalState()
233
- }
204
+
234
205
 
235
206
  async function startBulkAction(actionId) {
236
207
  const data = await callAdminForthApi({
package/index.ts CHANGED
@@ -14,14 +14,17 @@ import { AdminForthConfig, AdminForthClass, AdminForthComponentDeclaration, Admi
14
14
  BeforeSaveFunction,
15
15
  AfterSaveFunction,
16
16
  BeforeDataSourceRequestFunction,
17
- AfterDataSourceResponseFunction} from './types/AdminForthConfig.js';
17
+ AfterDataSourceResponseFunction, AllowedActionValue, AdminUser,
18
+ AdminForthResource,
19
+ AllowedActionsEnum
20
+ } from './types/AdminForthConfig.js';
18
21
  import path from 'path';
19
22
  import AdminForthPlugin from './plugins/base.js';
23
+ import { tr } from '@faker-js/faker';
20
24
 
21
25
 
22
26
  //get array from enum AdminForthResourcePages
23
27
 
24
- const DEFAULT_ALLOWED_ACTIONS = {create: true, edit: true, show: true, delete: true};
25
28
 
26
29
 
27
30
  class AdminForth implements AdminForthClass {
@@ -250,6 +253,32 @@ class AdminForth implements AdminForthClass {
250
253
  res.options = {bulkActions: [], allowedActions: {}};
251
254
  }
252
255
 
256
+ if (!res.options.allowedActions) {
257
+ res.options.allowedActions = {
258
+ all: true,
259
+ };
260
+ }
261
+
262
+
263
+ if (Object.keys(res.options.allowedActions).includes('all')) {
264
+ if (Object.keys(res.options.allowedActions).length > 1) {
265
+ errors.push(`Resource "${res.resourceId}" allowedActions cannot have "all" and other keys at same time: ${Object.keys(res.options.allowedActions).join(', ')}`);
266
+ }
267
+ for (const key of Object.keys(AllowedActionsEnum)) {
268
+ if (key !== 'all') {
269
+ res.options.allowedActions[key] = res.options.allowedActions.all;
270
+ }
271
+ }
272
+ delete res.options.allowedActions.all;
273
+ } else {
274
+ // by default allow all actions
275
+ for (const key of Object.keys(AllowedActionsEnum)) {
276
+ if (!Object.keys(res.options.allowedActions).includes(key)) {
277
+ res.options.allowedActions[key] = true;
278
+ }
279
+ }
280
+ }
281
+
253
282
 
254
283
  //check if resource has bulkActions
255
284
  if(res.options?.bulkActions){
@@ -301,17 +330,6 @@ class AdminForth implements AdminForthClass {
301
330
 
302
331
  }
303
332
 
304
- //add default allowedActions to resources
305
- if(res.options.allowedActions){
306
- //check if allowedActions is an object
307
- if(typeof res.options.allowedActions !== 'object'){
308
- errors.push(`Resource "${res.resourceId}" allowedActions must be an object`);
309
- }
310
- const userAllowedActions = res.options.allowedActions
311
- res.options.allowedActions = Object.assign({}, DEFAULT_ALLOWED_ACTIONS, userAllowedActions);
312
- } else {
313
- res.options.allowedActions = DEFAULT_ALLOWED_ACTIONS;
314
- }
315
333
 
316
334
  // transform all hooks Functions to array of functions
317
335
  if (res.hooks) {
@@ -648,13 +666,49 @@ class AdminForth implements AdminForthClass {
648
666
  [this.config.auth.usernameField]: username,
649
667
  [this.config.auth.userFullNameField]: userFullName
650
668
  };
669
+ const checkIsMenuItemVisible = (menuItem) => {
670
+ if (typeof menuItem.visible === 'function') {
671
+ const toReturn = menuItem.visible( adminUser );
672
+ if (typeof toReturn !== 'boolean') {
673
+ throw new Error(`'visible' function of ${menuItem.label || menuItem.type } must return boolean value`);
674
+ }
675
+ return toReturn;
676
+ }}
677
+ let newMenu = []
678
+ for (let menuItem of this.config.menu) {
679
+ let newMenuItem = {...menuItem,}
680
+ if (menuItem.visible){
681
+ if (!checkIsMenuItemVisible(menuItem)){
682
+ continue
683
+ }
684
+ }
685
+ if (menuItem.children){
686
+ let newChildren = []
687
+ for (let child of menuItem.children){
688
+ let newChild = {...child,}
689
+ if (child.visible){
690
+ if (!checkIsMenuItemVisible(child)){
691
+ continue
692
+ }
693
+ }
694
+ newChildren.push(newChild)
695
+ }
696
+ newMenuItem = {...newMenuItem, children: newChildren}
697
+ }
698
+ newMenu.push(newMenuItem)
699
+ }
700
+
701
+
702
+
703
+
704
+
651
705
  return {
652
706
  user: userData,
653
707
  resources: this.config.resources.map((res) => ({
654
708
  resourceId: res.resourceId,
655
709
  label: res.label,
656
710
  })),
657
- menu: this.config.menu,
711
+ menu: newMenu,
658
712
  config: {
659
713
  brandName: this.config.customization.brandName,
660
714
  brandLogo: this.config.customization.brandLogo,
@@ -671,12 +725,31 @@ class AdminForth implements AdminForthClass {
671
725
  },
672
726
  });
673
727
 
728
+ async function interpretResource(adminUser: AdminUser, resource: AdminForthResource, meta: any) {
729
+ await Promise.all(
730
+ Object.entries(resource.options?.allowedActions || {}).map(
731
+ async ([key, value]: [string, AllowedActionValue]) => {
732
+ // if callable then call
733
+ if (typeof value === 'function') {
734
+ resource.options.allowedActions[key] = await value( adminUser, resource, meta );
735
+ }
736
+ })
737
+ );
738
+ }
739
+
740
+ function checkAccess(action: AllowedActionsEnum, resource: AdminForthResource): { allowed: boolean, error?: string } {
741
+ const allowed = (resource.options?.allowedActions[action] as boolean | string | undefined);
742
+ if (allowed !== true) {
743
+ return { error: typeof allowed === 'string' ? allowed : 'Action is not allowed', allowed: false };
744
+ }
745
+ return { allowed: true };
746
+ }
674
747
 
675
748
 
676
749
  server.endpoint({
677
750
  method: 'POST',
678
751
  path: '/get_resource',
679
- handler: async ({ body }) => {
752
+ handler: async ({ body, adminUser }) => {
680
753
  const { resourceId } = body;
681
754
  if (!this.statuses.dbDiscover) {
682
755
  return { error: 'Database discovery not started' };
@@ -688,6 +761,9 @@ class AdminForth implements AdminForthClass {
688
761
  if (!resource) {
689
762
  return { error: `Resource ${resourceId} not found` };
690
763
  }
764
+
765
+ await interpretResource(adminUser, resource, {});
766
+
691
767
  // exclude "plugins" key
692
768
  return { resource: { ...resource, plugins: undefined } };
693
769
  },
@@ -711,6 +787,14 @@ class AdminForth implements AdminForthClass {
711
787
  return { error: `Resource ${resourceId} not found` };
712
788
  }
713
789
 
790
+ await interpretResource(adminUser, resource, { requestBody: body });
791
+
792
+ const { allowed, error } = checkAccess(source as AllowedActionsEnum, resource);
793
+ console.log('allowed', allowed, error);
794
+ if (!allowed) {
795
+ return { error };
796
+ }
797
+
714
798
  for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
715
799
  const resp = await hook({ resource, query: body, adminUser });
716
800
  if (!resp || (!resp.ok && !resp.error)) {
@@ -723,6 +807,8 @@ class AdminForth implements AdminForthClass {
723
807
  }
724
808
  const { limit, offset, filters, sort } = body;
725
809
 
810
+
811
+
726
812
  for (const filter of (filters || [])) {
727
813
  if (!Object.values(AdminForthFilterOperators).includes(filter.operator)) {
728
814
  throw new Error(`Operator '${filter.operator}' is not allowed`);
@@ -734,7 +820,7 @@ class AdminForth implements AdminForthClass {
734
820
 
735
821
  if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
736
822
  if (!Array.isArray(filter.value)) {
737
- throw new Error(`Value for operator '${filter.operator}'' should be an array`);
823
+ throw new Error(`Value for operator '${filter.operator}' should be an array`);
738
824
  }
739
825
  }
740
826
 
@@ -922,12 +1008,17 @@ class AdminForth implements AdminForthClass {
922
1008
  method: 'POST',
923
1009
  path: '/create_record',
924
1010
  handler: async ({ body, adminUser }) => {
925
- console.log('create_record', body, this.config.resources);
926
1011
  const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
927
1012
  if (!resource) {
928
1013
  return { error: `Resource '${body['resourceId']}' not found` };
929
1014
  }
930
-
1015
+ await interpretResource(adminUser, resource, { requestBody: body });
1016
+
1017
+ const { allowed, error } = checkAccess(AllowedActionsEnum.create, resource);
1018
+ if (!allowed) {
1019
+ return { error };
1020
+ }
1021
+
931
1022
  const record = body['record'];
932
1023
  // execute hook if needed
933
1024
  for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
@@ -941,6 +1032,8 @@ class AdminForth implements AdminForthClass {
941
1032
  }
942
1033
  }
943
1034
 
1035
+
1036
+
944
1037
  for (const column of resource.columns) {
945
1038
  if (column.fillOnCreate) {
946
1039
  if (body['record'][column.name] === undefined) {
@@ -1000,6 +1093,12 @@ class AdminForth implements AdminForthClass {
1000
1093
  if (!resource) {
1001
1094
  return { error: `Resource '${body['resourceId']}' not found` };
1002
1095
  }
1096
+ await interpretResource(adminUser, resource, { requestBody: body });
1097
+
1098
+ const { allowed, error } = checkAccess(AllowedActionsEnum.edit, resource);
1099
+ if (!allowed) {
1100
+ return { error };
1101
+ }
1003
1102
 
1004
1103
  const recordId = body['recordId'];
1005
1104
  const connector = this.connectors[resource.dataSource];
@@ -1074,6 +1173,13 @@ class AdminForth implements AdminForthClass {
1074
1173
  return { error: `Resource '${resource.resourceId}' does not allow delete action` };
1075
1174
  }
1076
1175
 
1176
+ await interpretResource(adminUser, resource, { requestBody: body });
1177
+
1178
+ const { allowed, error } = checkAccess(AllowedActionsEnum.delete, resource);
1179
+ if (!allowed) {
1180
+ return { error };
1181
+ }
1182
+
1077
1183
  // execute hook if needed
1078
1184
  for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
1079
1185
  const resp = await hook({ resource, record, adminUser });
@@ -149,7 +149,7 @@ class CodeInjector implements CodeInjectorType {
149
149
  routes += `{
150
150
  path: '${item.path}',
151
151
  name: '${item.path}',
152
- component: () => import('${item.component}'),}
152
+ component: () => import('${item.component}'),
153
153
  meta: { title: '${item?.meta?.title}'
154
154
  },\n`
155
155
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.1.19",
3
+ "version": "1.1.21",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,7 @@ import { PluginOptions } from "./types.js";
8
8
 
9
9
 
10
10
 
11
- export default class AccessControlPlugin extends AdminForthPlugin {
11
+ export default class AuditLogPlugin extends AdminForthPlugin {
12
12
 
13
13
  options: PluginOptions;
14
14
  adminforth: AdminForthClass;
@@ -25,59 +25,5 @@ export default class AccessControlPlugin extends AdminForthPlugin {
25
25
  super.modifyResourceConfig(adminforth, resourceConfig);
26
26
  this.adminforth = adminforth;
27
27
 
28
- if (!resourceConfig.hooks) {
29
- resourceConfig.hooks = {};
30
- }
31
-
32
- const checkAccess = async (adminUser: any, action: AllowedActionsEnum, meta: any) => {
33
- const hasAccessOrError = await this.options.hasAccess(adminUser, action, meta);
34
- if (hasAccessOrError === true) {
35
- return { ok: true };
36
- } else {
37
- return { ok: false, error: hasAccessOrError || AccessControlPlugin.defaultError };
38
- }
39
- }
40
-
41
- const bindHookCheck = (action: AllowedActionsEnum, hookName: string) => {
42
- if (!resourceConfig.hooks[action]) {
43
- resourceConfig.hooks[action] = {};
44
- }
45
- if (!resourceConfig.hooks[action][hookName]) {
46
- resourceConfig.hooks[action][hookName] = [];
47
- }
48
- if (hookName === 'beforeDatasourceRequest') {
49
- (resourceConfig.hooks[action][hookName] as Array<BeforeDataSourceRequestFunction>).unshift(
50
- async ({adminUser, query}: {resource: AdminForthResource, adminUser: any, query: any}) => {
51
- return checkAccess(adminUser, action, { query });
52
- }
53
- );
54
- } else {
55
- (resourceConfig.hooks[action][hookName] as Array<BeforeSaveFunction>).unshift(
56
- async ({adminUser, record}: {resource: AdminForthResource, adminUser: any, record: any}) => {
57
- return checkAccess(adminUser, action, { record });
58
- }
59
- );
60
- }
61
- }
62
-
63
- // List check
64
- bindHookCheck(AllowedActionsEnum.list, 'beforeDatasourceRequest');
65
-
66
- // Show check
67
- bindHookCheck(AllowedActionsEnum.show, 'beforeDatasourceRequest');
68
-
69
- // Edit check
70
- bindHookCheck(AllowedActionsEnum.edit, 'beforeDatasourceRequest');
71
-
72
- // create check
73
- bindHookCheck(AllowedActionsEnum.create, 'beforeSave');
74
-
75
- // edit check
76
- bindHookCheck(AllowedActionsEnum.edit, 'beforeSave');
77
-
78
- // delete check
79
- bindHookCheck(AllowedActionsEnum.delete, 'beforeSave');
80
-
81
- console.log('resourceConfig', resourceConfig);
82
28
  }
83
29
  }
@@ -3,13 +3,5 @@ import { AllowedActionsEnum, AdminUser } from '../../types/AdminForthConfig.js';
3
3
 
4
4
 
5
5
  export type PluginOptions = {
6
- /**
7
- * Called to check if user has access to a page
8
- * Should return `true` if user has access. If user has no access, return `false` or a string with an error message
9
- * @param user - The user object
10
- * @param action - The {@link AllowedActionsEnum} action
11
- * @param meta - The meta object containing query for list/show or record for save/edit
12
- * @returns {boolean | string} -
13
- */
14
- hasAccess: (user: AdminUser, action: AllowedActionsEnum, meta: any) => Promise<boolean | string>;
6
+
15
7
  }
@@ -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
- 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
- });
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
- window.adminforth.alert({
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;
@@ -190,7 +190,7 @@
190
190
  <button v-if="resource.options?.allowedActions.delete"
191
191
  class="font-medium text-red-600 dark:text-red-500 hover:underline ms-3"
192
192
  :data-tooltip-target="`tooltip-delete-${rowI}`"
193
- @click="showDeleteModal(row)"
193
+ @click="deleteRecord(row)"
194
194
  >
195
195
  <IconTrashBinSolid class="w-5 h-5 me-2"/>
196
196
  </button>
@@ -272,10 +272,12 @@
272
272
  import { initFlowbite } from 'flowbite';
273
273
  import { computed, onMounted, ref, watch } from 'vue';
274
274
  import { useRoute } from 'vue-router';
275
+ import { callAdminForthApi, getIcon } from '@/utils';
275
276
 
276
277
  import ValueRenderer from '@/components/ValueRenderer.vue';
277
278
  import { getCustomComponent } from '@/utils';
278
279
  import { useCoreStore } from '@/stores/core';
280
+ import { showSuccesTost } from '@/composables/useFrontendApi';
279
281
 
280
282
  import {
281
283
  IconInboxOutline,
@@ -305,6 +307,7 @@ const emits = defineEmits([
305
307
  'update:page',
306
308
  'update:sort',
307
309
  'update:checkboxes',
310
+ 'update:records'
308
311
 
309
312
  ]);
310
313
 
@@ -358,9 +361,11 @@ async function selectAll(value) {
358
361
  const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
359
362
 
360
363
 
364
+
365
+
361
366
  const allFromThisPageChecked = computed(() => {
362
- if (!props.rows.value) return false;
363
- return props.rows.value.every((r) => checkboxes.value.includes(r.id));
367
+ if (!props.rows) return false;
368
+ return props.rows.every((r) => props.checkboxes.includes(r.id));
364
369
  });
365
370
  const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
366
371
  const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
@@ -380,4 +385,35 @@ function onSortButtonClick(field) {
380
385
  }
381
386
  }
382
387
 
388
+ async function deleteRecord(row) {
389
+ const data = await window.adminforth.confirm({
390
+ message: 'Are you sure you want to delete this item?',
391
+ yes: 'Delete',
392
+ no: 'Cancel',
393
+ });
394
+ if (data) {
395
+ try {
396
+ const res = await callAdminForthApi({
397
+ path: '/delete_record',
398
+ method: 'POST',
399
+ body: {
400
+ resourceId: props.resource.resourceId,
401
+ primaryKey: row._primaryKeyValue,
402
+ }});
403
+ if (!res.error){
404
+ emits('update:records', true)
405
+ showSuccesTost('Record deleted successfully')
406
+ } else {
407
+ console.error(res.error)
408
+ }
409
+
410
+ } catch (e) {
411
+ console.error(e);
412
+ };
413
+ }
414
+
415
+
416
+ }
417
+
418
+
383
419
  </script>
@@ -0,0 +1,14 @@
1
+ export function showSuccesTost(message: string) {
2
+ window.adminforth.alert({ message, variant: 'success' });
3
+ return message;
4
+ }
5
+
6
+ export function showWarningTost(message: string) {
7
+ window.adminforth.alert({ message, variant: 'warning' });
8
+ return message;
9
+ }
10
+
11
+ export function showErrorTost(message: string, timeout?: number) {
12
+ window.adminforth.alert({ message, variant: 'danger', timeout: timeout || 'unlimited'});
13
+ return message;
14
+ }
package/spa/src/utils.ts CHANGED
@@ -97,3 +97,5 @@ export function checkAcessByAllowedActions(allowedActions:any, action:any ) {
97
97
  router.back();
98
98
  }
99
99
  }
100
+
101
+
@@ -78,6 +78,7 @@ import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
78
78
  import { onMounted, ref, watch } from 'vue';
79
79
  import { useRoute, useRouter } from 'vue-router';
80
80
  import { computed } from 'vue';
81
+ import { showErrorTost } from '@/composables/useFrontendApi';
81
82
 
82
83
 
83
84
  const isValid = ref(false);
@@ -141,11 +142,8 @@ async function saveRecord() {
141
142
  },
142
143
  });
143
144
  if (response.error) {
144
- window.adminforth.alert({
145
- message: response.error,
146
- variant: 'danger',
147
- timeout: 'unlimited',
148
- })
145
+ showErrorTost(response.error);
146
+
149
147
  }
150
148
  saving.value = false;
151
149
  if (route.query.returnTo) {
@@ -73,6 +73,7 @@ import { callAdminForthApi, getCustomComponent,checkAcessByAllowedActions } from
73
73
  import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
74
74
  import { computed, onMounted, ref } from 'vue';
75
75
  import { useRoute, useRouter } from 'vue-router';
76
+ import { showErrorTost } from '@/composables/useFrontendApi';
76
77
 
77
78
  const coreStore = useCoreStore();
78
79
 
@@ -155,11 +156,7 @@ async function saveRecord() {
155
156
  },
156
157
  });
157
158
  if (resp.error) {
158
- window.adminforth.alert({
159
- message: resp.error,
160
- variant: 'danger',
161
- timeout: 'unlimited',
162
- })
159
+ showErrorTost(resp.error);
163
160
  }
164
161
  saving.value = false;
165
162
  router.push({ name: 'resource-show', params: { resourceId: route.params.resourceId, primaryKey: coreStore.record[coreStore.primaryKey] } });
@@ -87,6 +87,7 @@
87
87
  @update:page="page = $event"
88
88
  @update:sort="sort = $event"
89
89
  @update:checkboxes="checkboxes = $event"
90
+ @update:records="getList"
90
91
  :pageSize="pageSize"
91
92
  :totalRows="totalRows"
92
93
  :checkboxes="checkboxes"
@@ -113,6 +114,7 @@ import { callAdminForthApi, getIcon } from '@/utils';
113
114
  import { initFlowbite } from 'flowbite';
114
115
  import { computed, onMounted, ref, watch } from 'vue';
115
116
  import { useRoute } from 'vue-router';
117
+ import { showErrorTost } from '@/composables/useFrontendApi'
116
118
 
117
119
  import ValueRenderer from '@/components/ValueRenderer.vue';
118
120
  import { getCustomComponent } from '@/utils';
@@ -184,11 +186,7 @@ async function getList() {
184
186
  }
185
187
  });
186
188
  if (data.error) {
187
- window.adminforth.alert({
188
- message: data.error,
189
- variant: 'danger',
190
- timeout: 'unlimited',
191
- });
189
+ showErrorTost(data.error);
192
190
  rows.value = [];
193
191
  totalRows.value = 0;
194
192
  return;
@@ -201,36 +199,9 @@ async function getList() {
201
199
  }
202
200
 
203
201
 
204
- function showDeleteModal(row) {
205
- if (!coreStore.config?.deleteConfirmation) {
206
- return deleteRecord(row);
207
- }
208
- modalStore.setModalContent({
209
- content: 'Are you sure you want to delete this item?',
210
- acceptText: 'Delete',
211
- cancelText: 'Cancel',
212
- });
213
- modalStore.setOnAcceptFunction(() => {
214
- return deleteRecord(row)
215
- })
216
- console.log('row', row);
217
- modalStore.togleModal();
218
202
 
219
- }
220
203
 
221
- async function deleteRecord(row) {
222
- await callAdminForthApi({
223
- path: '/delete_record',
224
- method: 'POST',
225
- body: {
226
- resourceId: route.params.resourceId,
227
- primaryKey: row._primaryKeyValue,
228
- recordId: row.id
229
- }
230
- });
231
- await getList();
232
- modalStore.resetmodalState()
233
- }
204
+
234
205
 
235
206
  async function startBulkAction(actionId) {
236
207
  const data = await callAdminForthApi({
@@ -833,9 +833,20 @@ export enum AllowedActionsEnum {
833
833
  filter = 'filter',
834
834
  }
835
835
 
836
+ /**
837
+ * Defines whether user has access to an action, can statically be Boolean
838
+ * or function which returns Boolean or string with error message
839
+ *
840
+ */
841
+ export type AllowedActionValue = boolean | ((adminUser: AdminUser, resource: AdminForthResource, meta: any) => Promise<boolean | string>);
836
842
 
843
+ /**
844
+ * Object which describes allowed actions for user.
845
+ */
837
846
  export type AllowedActions = {
838
- [key in AllowedActionsEnum]?: boolean
847
+ [key in AllowedActionsEnum]?: AllowedActionValue
848
+ } & {
849
+ all?: AllowedActionValue;
839
850
  }
840
851
 
841
852
  export type ValidationObject = {