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.
Files changed (50) hide show
  1. package/dist/index.js +68 -17
  2. package/dist/plugins/AccessControl/index.js +3 -56
  3. package/dist/plugins/AuditLog/index.js +66 -0
  4. package/dist/plugins/AuditLog/types.js +1 -0
  5. package/dist/plugins/ForeignInlineListPlugin/custom/InlineList.vue +3 -9
  6. package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +248 -0
  7. package/dist/spa/index.html +2 -2
  8. package/dist/spa/package-lock.json +1 -189
  9. package/dist/spa/package.json +1 -6
  10. package/dist/spa/spa/src/components/ResourceListTable.vue +35 -1
  11. package/dist/spa/spa/src/components/ShowTableItem.vue +14 -0
  12. package/dist/spa/spa/src/composables/useFrontendApi.ts +14 -0
  13. package/dist/spa/spa/src/utils.ts +2 -0
  14. package/dist/spa/spa/src/views/CreateView.vue +3 -5
  15. package/dist/spa/spa/src/views/EditView.vue +2 -5
  16. package/dist/spa/spa/src/views/HomeView.vue +8 -0
  17. package/dist/spa/spa/src/views/ListView.vue +4 -33
  18. package/dist/spa/src/App.vue +57 -76
  19. package/dist/spa/src/components/AcceptModal.vue +1 -1
  20. package/dist/spa/src/components/CustomDatePicker.vue +3 -3
  21. package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
  22. package/dist/spa/src/components/Dropdown.vue +4 -13
  23. package/dist/spa/src/components/Filters.vue +22 -55
  24. package/dist/spa/src/components/MenuLink.vue +2 -2
  25. package/dist/spa/src/components/ResourceForm.vue +99 -157
  26. package/dist/spa/src/components/ValueRenderer.vue +1 -11
  27. package/dist/spa/src/router/index.ts +2 -3
  28. package/dist/spa/src/stores/core.ts +33 -22
  29. package/dist/spa/src/utils.ts +3 -5
  30. package/dist/spa/src/views/CreateView.vue +7 -15
  31. package/dist/spa/src/views/EditView.vue +8 -36
  32. package/dist/spa/src/views/HomeView.vue +8 -0
  33. package/dist/spa/src/views/ListView.vue +23 -35
  34. package/dist/spa/src/views/LoginView.vue +1 -1
  35. package/dist/spa/src/views/ResourceParent.vue +1 -1
  36. package/dist/spa/src/views/ShowView.vue +9 -20
  37. package/dist/spa/tailwind.config.js +2 -5
  38. package/index.ts +87 -17
  39. package/package.json +1 -1
  40. package/plugins/AccessControl/index.ts +1 -55
  41. package/plugins/AccessControl/types.ts +1 -9
  42. package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +3 -9
  43. package/spa/src/components/ResourceListTable.vue +35 -1
  44. package/spa/src/composables/useFrontendApi.ts +14 -0
  45. package/spa/src/utils.ts +2 -0
  46. package/spa/src/views/CreateView.vue +3 -5
  47. package/spa/src/views/EditView.vue +2 -5
  48. package/spa/src/views/ListView.vue +4 -33
  49. package/types/AdminForthConfig.ts +12 -1
  50. package/dist/spa/src/components/CustomRangePicker.vue +0 -148
@@ -65,7 +65,7 @@
65
65
  </BreadcrumbsWithButtons>
66
66
 
67
67
  <!-- table -->
68
- <div class="relative overflow-x-auto shadow-list-table-shadow dark:shadow-black dark:s">
68
+ <div class="relative overflow-x-auto shadow-md sm:rounded-lg dark:shadow-2xl">
69
69
 
70
70
 
71
71
  <!-- skelet loader -->
@@ -90,7 +90,7 @@
90
90
  </div>
91
91
 
92
92
  <table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
93
- <thead class="text-xs text-view-table-heading-text bg-list-view-table-heading dark:bg-gray-700 dark:text-gray-400">
93
+ <thead class="text-xs text-gray-700 bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
94
94
  <tr>
95
95
  <th scope="col" class="p-4">
96
96
  <div v-if="rows && rows.length" class="flex items-center">
@@ -127,7 +127,7 @@
127
127
  </tr>
128
128
  </thead>
129
129
  <tbody>
130
- <tr v-if="!rows" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
130
+ <tr v-if="!rows" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
131
131
  <td :colspan="coreStore.resourceColumns.length + 2">
132
132
 
133
133
  <div role="status"
@@ -171,7 +171,7 @@
171
171
  </div>
172
172
  </td>
173
173
  </tr>
174
- <tr v-else-if="rows.length === 0" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
174
+ <tr v-else-if="rows.length === 0" class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
175
175
  <td :colspan="coreStore.resourceColumns.length + 2">
176
176
 
177
177
  <div id="toast-simple"
@@ -185,7 +185,7 @@
185
185
  </tr>
186
186
 
187
187
  <tr v-else v-for="(row, rowI) in rows" :key="row.id"
188
- class="bg-list-view-table-bg border-b dark:bg-gray-800 border-list-view-border-color dark:border-gray-700 hover:bg-list-view-table-row-hover dark:hover:bg-gray-600">
188
+ class="bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600">
189
189
  <td class="w-4 p-4">
190
190
  <div class="flex items center">
191
191
  <input
@@ -198,12 +198,7 @@
198
198
  </div>
199
199
  </td>
200
200
  <td v-for="c in columnsListed" class="px-6 py-4">
201
- <!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
202
- <component
203
- :is="listComponentsPerColumn[c.name] || ValueRenderer"
204
- :column="c"
205
- :row="row"
206
- />
201
+ <ValueRenderer :column="c" :row="row"/>
207
202
  </td>
208
203
  <td class="flex items-center px-6 py-4">
209
204
 
@@ -315,28 +310,29 @@
315
310
  </template>
316
311
 
317
312
  <script setup>
313
+ import {ref, onMounted, watch, computed} from 'vue';
314
+ import {callAdminForthApi, getIcon} from '@/utils';
315
+ import {useRoute} from 'vue-router';
316
+ import {useCoreStore} from '@/stores/core';
317
+ import {useModalStore} from '@/stores/modal';
318
318
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
319
- import { useCoreStore } from '@/stores/core';
320
- import { useModalStore } from '@/stores/modal';
321
- import { callAdminForthApi, getIcon } from '@/utils';
322
- import { initFlowbite } from 'flowbite';
323
- import { computed, onMounted, ref, watch } from 'vue';
324
- import { useRoute } from 'vue-router';
319
+ import {initFlowbite} from 'flowbite'
325
320
 
326
321
  import ValueRenderer from '@/components/ValueRenderer.vue';
327
- import { getCustomComponent } from '@/utils';
328
322
 
329
323
  import {
330
- IconInboxOutline,
331
- IconPlusOutline
324
+ IconChevronDoubleLeftOutline,
325
+ IconChevronDoubleRightOutline,
326
+ IconInboxFullSolid,
327
+ IconInboxOutline,
328
+ IconPlusOutline
332
329
  } from '@iconify-prerendered/vue-flowbite';
333
330
 
334
331
  import {
335
- IconBanOutline,
336
- IconEyeSolid,
337
- IconFilterOutline,
338
- IconPenSolid,
339
- IconTrashBinSolid
332
+ IconEyeSolid,
333
+ IconTrashBinSolid,
334
+ IconPenSolid,
335
+ IconFilterOutline, IconBanOutline
340
336
  } from '@iconify-prerendered/vue-flowbite';
341
337
 
342
338
  import Filters from '@/components/Filters.vue';
@@ -361,6 +357,7 @@ const totalRows = ref(0);
361
357
  const allCheckedActions = ref([]);
362
358
  const allowedActions = ref({});
363
359
 
360
+
364
361
  const DEFAULT_PAGE_SIZE = 10;
365
362
 
366
363
  const columnsListed = computed(() => coreStore.resourceColumns?.filter(c => c.showIn.includes('list')));
@@ -379,7 +376,6 @@ async function selectAll(value) {
379
376
 
380
377
  const pageSize = computed(() => coreStore.resourceById[route.params.resourceId]?.pageSize || DEFAULT_PAGE_SIZE);
381
378
  const totalPages = computed(() => Math.ceil(totalRows.value / pageSize.value));
382
- let listComponentsPerColumn = {};
383
379
  const allFromThisPageChecked = computed(() => {
384
380
  if (!rows.value) return false;
385
381
  return rows.value.every((r) => checkboxes.value.includes(r.id));
@@ -423,7 +419,6 @@ async function getList() {
423
419
  path: '/get_resource_data',
424
420
  method: 'POST',
425
421
  body: {
426
- source: 'list',
427
422
  resourceId: route.params.resourceId,
428
423
  limit: pageSize.value,
429
424
  offset: (page.value - 1) * pageSize.value,
@@ -431,14 +426,7 @@ async function getList() {
431
426
  sort: sort.value,
432
427
  }
433
428
  });
434
- listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
435
- if (column.component?.list) {
436
- acc[column.name] = getCustomComponent(column.component.list);
437
- }
438
- return acc;
439
- }, {});
440
-
441
- fetchStatus.value.pending = false;
429
+ fetchStatus.value.pending = false;
442
430
  rows.value = data.data?.map(row => {
443
431
  row._primaryKeyValue = row[coreStore.resourceColumns.find(c => c.primaryKey).name];
444
432
  return row;
@@ -11,7 +11,7 @@
11
11
  <div id="authentication-modal" tabindex="-1" class=" overflow-y-auto overflow-x-hidden z-50 min-w-[400px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
12
12
  <div class="relative p-4 w-full max-w-md max-h-full">
13
13
  <!-- Modal content -->
14
- <div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
14
+ <div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
15
15
  <!-- Modal header -->
16
16
  <div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
17
17
  <h3 class="text-xl font-semibold text-gray-900 dark:text-white">
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div :key="`${$route?.params.resourceId}---${$route?.params.primaryKey}`">
2
+ <div>
3
3
  <RouterView/>
4
4
  </div>
5
5
  </template>
@@ -27,10 +27,10 @@
27
27
  </div>
28
28
  <div
29
29
  v-else
30
- class="relative overflow-x-auto shadow-resourse-form-shadow "
30
+ class="relative overflow-x-auto shadow-md sm:rounded-lg"
31
31
  >
32
32
  <table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
33
- <thead class="text-xs text-gray-700 uppercase bg-form-view-heading dark:bg-gray-700 dark:text-gray-400">
33
+ <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
34
34
  <tr>
35
35
  <th scope="col" class="px-6 py-3">
36
36
  Field
@@ -42,17 +42,13 @@
42
42
  </thead>
43
43
  <tbody>
44
44
  <tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))" :key="column.name"
45
- class="bg-form-view-bg odd:dark:bg-gray-900 even:dark:bg-gray-800 border-b dark:border-gray-700"
45
+ class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700"
46
46
  >
47
47
  <td class="px-6 py-4 whitespace-nowrap">
48
48
  {{ column.label }}
49
49
  </td>
50
50
  <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
51
- <component
52
- :is="showComponentsPerColumn[column.name] || ValueRenderer"
53
- :column="column"
54
- :row="coreStore.record"
55
- />
51
+ <ValueRenderer :column="column" :row="coreStore.record" />
56
52
  </td>
57
53
  </tr>
58
54
 
@@ -67,19 +63,18 @@
67
63
 
68
64
  <script setup>
69
65
 
66
+ import { ref, computed, onMounted } from 'vue';
67
+ import { useRoute } from 'vue-router';
68
+ import { callAdminForthApi } from '@/utils';
70
69
  import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
71
- import ValueRenderer from '@/components/ValueRenderer.vue';
72
- import { useCoreStore } from '@/stores/core';
73
- import { getCustomComponent } from '@/utils';
74
70
  import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
75
- import { onMounted, ref } from 'vue';
76
- import { useRoute } from 'vue-router';
71
+ import { useCoreStore } from '@/stores/core';
72
+ import ValueRenderer from '@/components/ValueRenderer.vue';
77
73
 
78
74
 
79
75
  const item = ref(null);
80
76
  const route = useRoute();
81
77
  const loading = ref(false);
82
- let showComponentsPerColumn = {};
83
78
 
84
79
  const coreStore = useCoreStore();
85
80
 
@@ -92,12 +87,6 @@ onMounted(async () => {
92
87
  resourceId: route.params.resourceId,
93
88
  primaryKey: route.params.primaryKey,
94
89
  });
95
- showComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
96
- if (column.component?.show) {
97
- acc[column.name] = getCustomComponent(column.component.show);
98
- }
99
- return acc;
100
- }, {});
101
90
  loading.value = false;
102
91
  });
103
92
 
@@ -1,12 +1,9 @@
1
1
  /** @type {import('tailwindcss').Config} */
2
2
  export default {
3
- content: ["./src/**/*.{vue, js}","./src/*.{vue, js}", "./index.html", "./node_modules/flowbite/**/*.js"],
3
+ content: ["./src/**/*.{vue, js}", "./node_modules/flowbite/**/*.js"],
4
4
  theme: {
5
- extend: {
6
- /* IMPORTANT:ADMINFORTH TAILWIND STYLES */
7
- }
5
+ extend: {},
8
6
  },
9
-
10
7
  darkMode: 'class',
11
8
  plugins: [
12
9
  require('flowbite/plugin'),
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) {
@@ -707,12 +725,31 @@ class AdminForth implements AdminForthClass {
707
725
  },
708
726
  });
709
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
+ }
710
747
 
711
748
 
712
749
  server.endpoint({
713
750
  method: 'POST',
714
751
  path: '/get_resource',
715
- handler: async ({ body }) => {
752
+ handler: async ({ body, adminUser }) => {
716
753
  const { resourceId } = body;
717
754
  if (!this.statuses.dbDiscover) {
718
755
  return { error: 'Database discovery not started' };
@@ -724,6 +761,9 @@ class AdminForth implements AdminForthClass {
724
761
  if (!resource) {
725
762
  return { error: `Resource ${resourceId} not found` };
726
763
  }
764
+
765
+ await interpretResource(adminUser, resource, {});
766
+
727
767
  // exclude "plugins" key
728
768
  return { resource: { ...resource, plugins: undefined } };
729
769
  },
@@ -747,6 +787,14 @@ class AdminForth implements AdminForthClass {
747
787
  return { error: `Resource ${resourceId} not found` };
748
788
  }
749
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
+
750
798
  for (const hook of listify(resource.hooks?.[source]?.beforeDatasourceRequest)) {
751
799
  const resp = await hook({ resource, query: body, adminUser });
752
800
  if (!resp || (!resp.ok && !resp.error)) {
@@ -759,6 +807,8 @@ class AdminForth implements AdminForthClass {
759
807
  }
760
808
  const { limit, offset, filters, sort } = body;
761
809
 
810
+
811
+
762
812
  for (const filter of (filters || [])) {
763
813
  if (!Object.values(AdminForthFilterOperators).includes(filter.operator)) {
764
814
  throw new Error(`Operator '${filter.operator}' is not allowed`);
@@ -770,7 +820,7 @@ class AdminForth implements AdminForthClass {
770
820
 
771
821
  if (filter.operator === AdminForthFilterOperators.IN || filter.operator === AdminForthFilterOperators.NIN) {
772
822
  if (!Array.isArray(filter.value)) {
773
- 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`);
774
824
  }
775
825
  }
776
826
 
@@ -958,12 +1008,17 @@ class AdminForth implements AdminForthClass {
958
1008
  method: 'POST',
959
1009
  path: '/create_record',
960
1010
  handler: async ({ body, adminUser }) => {
961
- console.log('create_record', body, this.config.resources);
962
1011
  const resource = this.config.resources.find((res) => res.resourceId == body['resourceId']);
963
1012
  if (!resource) {
964
1013
  return { error: `Resource '${body['resourceId']}' not found` };
965
1014
  }
966
-
1015
+ await interpretResource(adminUser, resource, { requestBody: body });
1016
+
1017
+ const { allowed, error } = checkAccess(AllowedActionsEnum.create, resource);
1018
+ if (!allowed) {
1019
+ return { error };
1020
+ }
1021
+
967
1022
  const record = body['record'];
968
1023
  // execute hook if needed
969
1024
  for (const hook of listify(resource.hooks?.create?.beforeSave as BeforeSaveFunction[])) {
@@ -977,6 +1032,8 @@ class AdminForth implements AdminForthClass {
977
1032
  }
978
1033
  }
979
1034
 
1035
+
1036
+
980
1037
  for (const column of resource.columns) {
981
1038
  if (column.fillOnCreate) {
982
1039
  if (body['record'][column.name] === undefined) {
@@ -1036,6 +1093,12 @@ class AdminForth implements AdminForthClass {
1036
1093
  if (!resource) {
1037
1094
  return { error: `Resource '${body['resourceId']}' not found` };
1038
1095
  }
1096
+ await interpretResource(adminUser, resource, { requestBody: body });
1097
+
1098
+ const { allowed, error } = checkAccess(AllowedActionsEnum.edit, resource);
1099
+ if (!allowed) {
1100
+ return { error };
1101
+ }
1039
1102
 
1040
1103
  const recordId = body['recordId'];
1041
1104
  const connector = this.connectors[resource.dataSource];
@@ -1110,6 +1173,13 @@ class AdminForth implements AdminForthClass {
1110
1173
  return { error: `Resource '${resource.resourceId}' does not allow delete action` };
1111
1174
  }
1112
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
+
1113
1183
  // execute hook if needed
1114
1184
  for (const hook of listify(resource.hooks?.delete?.beforeSave as BeforeSaveFunction[])) {
1115
1185
  const resp = await hook({ resource, record, adminUser });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
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 page - The page to check access for
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, page: 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
 
@@ -382,4 +385,35 @@ function onSortButtonClick(field) {
382
385
  }
383
386
  }
384
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
+
385
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
+