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
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}'' should be an array`);
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 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;
@@ -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>
@@ -16,8 +16,8 @@
16
16
  </script> -->
17
17
 
18
18
  </head>
19
- <body class=" ">
20
- <div id="app" class="min-h-screen bg-html-bg dark:bg-gray-800"></div>
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>