adminforth 1.1.4 → 1.1.5

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 (38) hide show
  1. package/dist/index.js +6 -0
  2. package/dist/modules/styles.js +7 -6
  3. package/dist/plugins/ForeignInlineListPlugin/index.js +32 -1
  4. package/dist/plugins/base.js +4 -0
  5. package/dist/spa/spa/src/App.vue +2 -2
  6. package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
  7. package/dist/spa/spa/src/components/Filters.vue +19 -15
  8. package/dist/spa/spa/src/components/ResourceForm.vue +12 -12
  9. package/dist/spa/spa/src/components/ResourceListTable.vue +29 -15
  10. package/dist/spa/spa/src/composables/useStores.ts +63 -3
  11. package/dist/spa/spa/src/stores/core.ts +7 -0
  12. package/dist/spa/spa/src/stores/filters.ts +22 -0
  13. package/dist/spa/spa/src/stores/modal.ts +0 -1
  14. package/dist/spa/spa/src/views/CreateView.vue +8 -3
  15. package/dist/spa/spa/src/views/EditView.vue +2 -2
  16. package/dist/spa/spa/src/views/ListView.vue +21 -21
  17. package/dist/spa/spa/src/views/ShowView.vue +3 -3
  18. package/index.ts +10 -0
  19. package/modules/styles.ts +7 -6
  20. package/package.json +1 -1
  21. package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +105 -13
  22. package/plugins/ForeignInlineListPlugin/index.ts +32 -5
  23. package/plugins/base.ts +7 -0
  24. package/spa/src/App.vue +2 -2
  25. package/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
  26. package/spa/src/components/Filters.vue +19 -15
  27. package/spa/src/components/ResourceForm.vue +12 -12
  28. package/spa/src/components/ResourceListTable.vue +29 -15
  29. package/spa/src/composables/useStores.ts +63 -3
  30. package/spa/src/stores/core.ts +7 -0
  31. package/spa/src/stores/filters.ts +22 -0
  32. package/spa/src/stores/modal.ts +0 -1
  33. package/spa/src/views/CreateView.vue +8 -3
  34. package/spa/src/views/EditView.vue +2 -2
  35. package/spa/src/views/ListView.vue +21 -21
  36. package/spa/src/views/ShowView.vue +3 -3
  37. package/types/AdminForthConfig.ts +32 -1
  38. package/types/FrontendAPI.ts +47 -0
@@ -3,7 +3,6 @@
3
3
  <Teleport to="body">
4
4
  <Filters
5
5
  :columns="coreStore.resourceColumns"
6
- v-model:filters="filters"
7
6
  :columnsMinMax="columnsMinMax" :show="filtersShow"
8
7
  @hide="filtersShow = false"
9
8
  />
@@ -23,7 +22,7 @@
23
22
  v-if="checkboxes.length"
24
23
  data-tooltip-target="tooltip-remove-all"
25
24
  data-tooltip-placement="bottom"
26
- class="flex gap-1 items-center py-1 px-3 me-2 mb-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"
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"
27
26
  >
28
27
  <IconBanOutline class="w-5 h-5 "/>
29
28
 
@@ -39,7 +38,7 @@
39
38
  v-for="(action,i) in coreStore.resource?.options?.bulkActions"
40
39
  :key="action.id"
41
40
  @click="startBulkAction(action.id)"
42
- class="flex gap-1 items-center py-1 px-3 mb-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"
41
+ 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"
43
42
  :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',
44
43
  'bg-blue-100 text-blue-800 border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
45
44
  }"
@@ -52,24 +51,24 @@
52
51
  {{ `${action.label} (${checkboxes.length})` }}
53
52
  </button>
54
53
 
55
- <RouterLink v-if="coreStore.resource?.options?.create"
54
+ <RouterLink v-if="coreStore.resource?.options?.allowedActions?.create"
56
55
  :to="{ name: 'resource-create', params: { resourceId: $route.params.resourceId } }"
57
- class="flex items-center py-1 px-3 mb-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 rounded-default"
56
+ 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"
58
57
  >
59
58
  <IconPlusOutline class="w-4 h-4 me-2"/>
60
59
  Create
61
60
  </RouterLink>
62
61
 
63
62
  <button
64
- class="flex gap-1 items-center py-1 px-3 me-2 mb-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 rounded-default"
63
+ 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 rounded-default"
65
64
  @click="()=>{filtersShow = !filtersShow}"
66
65
  >
67
66
  <IconFilterOutline class="w-4 h-4 me-2"/>
68
67
  Filter
69
68
  <span
70
69
  class="bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-red-400 border border-red-400"
71
- v-if="filters.length">
72
- {{ filters.length }}
70
+ v-if="filtersStore.filters.length">
71
+ {{ filtersStore.filters.length }}
73
72
  </span>
74
73
  </button>
75
74
  </BreadcrumbsWithButtons>
@@ -90,6 +89,7 @@
90
89
  @update:checkboxes="checkboxes = $event"
91
90
  :pageSize="pageSize"
92
91
  :totalRows="totalRows"
92
+ :checkboxes="checkboxes"
93
93
  />
94
94
 
95
95
  <component
@@ -108,6 +108,7 @@ import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
108
108
  import ResourceListTable from '@/components/ResourceListTable.vue';
109
109
  import { useCoreStore } from '@/stores/core';
110
110
  import { useModalStore } from '@/stores/modal';
111
+ import { useFiltersStore } from '@/stores/filters';
111
112
  import { callAdminForthApi, getIcon } from '@/utils';
112
113
  import { initFlowbite } from 'flowbite';
113
114
  import { computed, onMounted, ref, watch } from 'vue';
@@ -116,17 +117,15 @@ import { useRoute } from 'vue-router';
116
117
  import ValueRenderer from '@/components/ValueRenderer.vue';
117
118
  import { getCustomComponent } from '@/utils';
118
119
 
119
- import {
120
- IconInboxOutline,
121
- IconPlusOutline
122
- } from '@iconify-prerendered/vue-flowbite';
123
120
 
124
121
  import {
125
- IconBanOutline,
126
- IconEyeSolid,
127
- IconFilterOutline,
128
- IconPenSolid,
129
- IconTrashBinSolid
122
+ IconBanOutline,
123
+ IconEyeSolid,
124
+ IconFilterOutline,
125
+ IconPenSolid,
126
+ IconTrashBinSolid,
127
+ IconInboxOutline,
128
+ IconPlusOutline
130
129
  } from '@iconify-prerendered/vue-flowbite';
131
130
 
132
131
  import Filters from '@/components/Filters.vue';
@@ -135,11 +134,12 @@ const filtersShow = ref(false);
135
134
 
136
135
  const coreStore = useCoreStore();
137
136
  const modalStore = useModalStore();
137
+ const filtersStore = useFiltersStore();
138
138
 
139
139
  const route = useRoute();
140
140
 
141
141
  const page = ref(1);
142
- const filters = ref([]);
142
+ const filters = filtersStore.filters;
143
143
  const columnsMinMax = ref({});
144
144
  const sort = ref([]);
145
145
 
@@ -157,7 +157,7 @@ watch([page], async () => {
157
157
  await getList();
158
158
  });
159
159
 
160
- watch([filters], async () => {
160
+ watch(()=>filtersStore.filters, async () => {
161
161
  page.value = 1;
162
162
  await getList();
163
163
  }, {deep: true});
@@ -177,7 +177,7 @@ async function getList() {
177
177
  resourceId: route.params.resourceId,
178
178
  limit: pageSize.value,
179
179
  offset: (page.value - 1) * pageSize.value,
180
- filters: filters.value,
180
+ filters: filtersStore.filters,
181
181
  sort: sort.value,
182
182
  }
183
183
  });
@@ -265,7 +265,7 @@ onMounted(async () => {
265
265
 
266
266
  // on route param change
267
267
  watch(() => route.params.resourceId, async () => {
268
- filters.value = [];
268
+ filtersStore.setFilters([]);
269
269
  checkboxes.value = [];
270
270
  sort.value = [];
271
271
  await init();
@@ -10,14 +10,14 @@
10
10
  />
11
11
  <BreadcrumbsWithButtons>
12
12
  <RouterLink v-if="coreStore?.resourceOptions?.allowedActions?.edit" :to="{ name: 'resource-edit', params: { resourceId: $route.params.resourceId, primaryKey: $route.params.primaryKey } }"
13
- class="flex items-center py-1 px-3 me-2 mb-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"
13
+ 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"
14
14
  >
15
15
  <IconPenSolid class="w-4 h-4" />
16
16
  Edit
17
17
  </RouterLink>
18
18
 
19
19
  <button v-if="coreStore?.resourceOptions?.allowedActions?.delete" @click="showDeleteModal"
20
- class="flex items-center py-1 px-3 mb-2 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
20
+ class="flex items-center py-1 px-3 text-sm font-medium rounded-default text-red-600 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-red-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-red-500 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700"
21
21
  >
22
22
  <IconTrashBinSolid class="w-4 h-4" />
23
23
  Delete
@@ -70,7 +70,7 @@
70
70
  :record="coreStore.record"
71
71
  />
72
72
  <template v-else>
73
- <td class="px-6 py-4 whitespace-nowrap">
73
+ <td class="px-6 py-4 whitespace-nowrap "> <!--align-top-->
74
74
  {{ column.label }}
75
75
  </td>
76
76
  <td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
package/index.ts CHANGED
@@ -13,6 +13,7 @@ import { AdminForthConfig, AdminForthClass, AdminForthComponentDeclaration,
13
13
  AdminForthFilterOperators, AdminForthDataTypes, AdminForthResourcePages } from './types/AdminForthConfig.js';
14
14
  import { getFunctionList } from './modules/utils.js';
15
15
  import path from 'path';
16
+ import AdminForthPlugin from './plugins/base.js';
16
17
 
17
18
 
18
19
  //get array from enum AdminForthResourcePages
@@ -40,6 +41,7 @@ class AdminForth implements AdminForthClass {
40
41
  connectors: any;
41
42
  connectorClasses: any;
42
43
  runningHotReload: boolean;
44
+ activatedPlugins: Array<AdminForthPlugin>;
43
45
 
44
46
  statuses: {
45
47
  dbDiscover?: 'running' | 'done',
@@ -48,6 +50,7 @@ class AdminForth implements AdminForthClass {
48
50
  constructor(config: AdminForthConfig) {
49
51
  this.config = {...this.#defaultConfig,...config};
50
52
  this.codeInjector = new CodeInjector(this);
53
+ this.activatedPlugins = [];
51
54
 
52
55
  this.validateConfig();
53
56
  this.activatePlugins();
@@ -57,6 +60,7 @@ class AdminForth implements AdminForthClass {
57
60
  this.auth = new Auth();
58
61
  this.connectors = {};
59
62
  this.statuses = {};
63
+
60
64
  console.log(`🚀 AdminForth v${ADMINFORTH_VERSION} starting up`)
61
65
  }
62
66
 
@@ -64,6 +68,7 @@ class AdminForth implements AdminForthClass {
64
68
  for (let resource of this.config.resources) {
65
69
  for (let pluginInstance of resource.plugins || []) {
66
70
  pluginInstance.modifyResourceConfig(this, resource);
71
+ this.activatedPlugins.push(pluginInstance);
67
72
  }
68
73
  };
69
74
  }
@@ -1062,6 +1067,11 @@ class AdminForth implements AdminForthClass {
1062
1067
  }
1063
1068
  }
1064
1069
  })
1070
+
1071
+ // setup endpoints for all plugins
1072
+ this.activatedPlugins.forEach((plugin) => {
1073
+ plugin.setupEndpoints(server);
1074
+ });
1065
1075
  }
1066
1076
  }
1067
1077
 
package/modules/styles.ts CHANGED
@@ -2,19 +2,20 @@ export const styles = () => `
2
2
 
3
3
  "colors": {
4
4
  "html-bg": "#FFFFFF", // Changed to white
5
- "header-bg": "#EDEDED", // Light grey
5
+ "header-bg": "#FFFFFF", // Light grey
6
6
  "header-bg-hover": "#D3D3D3", // Light grey hover
7
7
  "header-text": "#333333", // Dark grey text
8
8
  "header-text-hover": "#333333", // Dark grey text hover
9
9
  "header-icons": "#333333", // Dark grey icons
10
10
  "header-icons-hover": "#333333", // Dark grey icons hover
11
11
  "header-logo-color": "#333333", // Dark grey logo
12
- "header-border-color": "#CCCCCC", // Light grey border
12
+ "header-border-color": "rgb(229 231 235)", // Light grey border
13
13
 
14
- "nav-menu-bg": "#F5F5F5", // Very light grey
15
- "nav-menu-bg-hover": "#E0E0E0", // Light grey hover
16
- "nav-menu-bg-active": "#CCCCCC", // Light grey active
17
- "nav-menu-hover": "#E0E0E0", // Light grey hover
14
+ "nav-menu-bg": "#f9fafb", // Very light grey
15
+ "nav-menu-border": "rgb(229 231 235)",
16
+ "nav-menu-bg-hover": "rgb(243 244 246)", // Light grey hover
17
+ "nav-menu-bg-active": "rgb(233, 234, 236)", // Light grey active
18
+ "nav-menu-hover": "#rgb(209 213 219)", // Light grey hover
18
19
  "nav-menu-active": "#CCCCCC", // Light grey active
19
20
  "nav-menu-active-text": "#333333", // Dark grey active text
20
21
  "nav-menu-text": "#333333", // Dark grey text
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adminforth",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "OpenSource Vue3 powered forth-generation admin panel",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,9 +1,84 @@
1
1
  <template>
2
- <td colspan="2">
3
- <h4 v-if="listResource"
4
- class="px-6 py-4"
2
+ <Teleport to="body">
3
+ <!-- todo exclude foreign column-->
4
+ <Filters
5
+ v-if="listResource"
6
+ :columns="listResource.columns"
7
+ v-model:filters="filters"
8
+ :columnsMinMax="columnsMinMax"
9
+ :show="filtersShow"
10
+ @hide="filtersShow = false"
11
+ />
12
+ {{ listResource?.columns}}
13
+ </Teleport>
5
14
 
6
- >{{ listResource.label }} inline records</h4>
15
+ <td colspan="2">
16
+ <div class="flex items-center gap-1">
17
+ <h4 v-if="listResource"
18
+ class="px-6 py-4"
19
+ >{{ listResource.label }} inline records</h4>
20
+
21
+ <button
22
+ @click="()=>{checkboxes = []}"
23
+ v-if="checkboxes.length"
24
+ data-tooltip-target="tooltip-remove-all"
25
+ data-tooltip-placement="bottom"
26
+ 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"
27
+ >
28
+ <IconBanOutline class="w-5 h-5 "/>
29
+ <div id="tooltip-remove-all" role="tooltip"
30
+ 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">
31
+ Remove selection
32
+ <div class="tooltip-arrow" data-popper-arrow></div>
33
+ </div>
34
+ </button>
35
+
36
+ <button
37
+ v-if="checkboxes.length"
38
+ v-for="(action,i) in listResource?.options?.bulkActions"
39
+ :key="action.id"
40
+ @click="startBulkAction(action.id)"
41
+ 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"
42
+ :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',
43
+ 'bg-blue-100 text-blue-800 border-blue-400 dark:bg-blue-700 dark:text-blue-400 dark:border-blue-400':action.state==='active',
44
+ }"
45
+ >
46
+ <component
47
+ v-if="action.icon"
48
+ :is="getIcon(action.icon)"
49
+ 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>
50
+
51
+ {{ `${action.label} (${checkboxes.length})` }}
52
+ </button>
53
+
54
+ <RouterLink v-if="listResource?.options?.allowedActions?.create"
55
+ :to="{
56
+ name: 'resource-create',
57
+ params: { resourceId: listResource.resourceId },
58
+ query: {
59
+ [listResourceRefColumn.name]: props.record[selfPrimaryKeyColumn.name]
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 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 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 me-2 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>
7
82
 
8
83
  <ResourceListTable
9
84
  :resource="listResource"
@@ -22,6 +97,12 @@ import { callAdminForthApi } from '@/utils';
22
97
  import { ref, onMounted, watch, computed } from 'vue';
23
98
  import ResourceListTable from '@/components/ResourceListTable.vue';
24
99
 
100
+ import {
101
+ IconBanOutline,
102
+ IconFilterOutline,
103
+ IconPlusOutline,
104
+ } from '@iconify-prerendered/vue-flowbite';
105
+
25
106
  const props = defineProps(['column', 'record', 'meta', 'resource', 'adminUser']);
26
107
 
27
108
  const listResource = ref(null);
@@ -30,21 +111,33 @@ const loading = ref(true);
30
111
  const page = ref(1);
31
112
  const sort = ref([]);
32
113
  const checkboxes = ref([]);
33
- const pageSize = computed(() => props.meta.tablePageSize || 10);
114
+ const pageSize = computed(() => props.meta.listPageSize || 10);
34
115
 
35
116
  const rows = ref(null);
36
117
  const totalRows = ref(0);
37
118
 
119
+ const filters = ref([]);
120
+ const filtersShow = ref(false);
38
121
 
122
+ const listResourceRefColumn = computed(() => {
123
+ if (!listResource.value) {
124
+ return null;
125
+ }
126
+ return listResource.value.columns.find(c => c.foreignResource?.resourceId === props.resource.resourceId);
127
+ });
128
+
129
+ const selfPrimaryKeyColumn = computed(() => {
130
+ return props.resource.columns.find(c => c.primaryKey);
131
+ });
39
132
 
40
- const filters = computed(() => {
133
+ const endFilters = computed(() => {
41
134
  if (!listResource.value) {
42
135
  return [];
43
136
  }
44
137
  // get name of the column that is foreign key
45
- const refColumn = listResource.value.columns.find(c => c.foreignResource?.resourceId === props.resource.resourceId);
138
+ const refColumn = listResourceRefColumn.value;
46
139
 
47
- const primaryKeyColumn = listResource.value.columns.find(c => c.primaryKey);
140
+ const primaryKeyColumn = selfPrimaryKeyColumn.value;
48
141
 
49
142
  if (!refColumn) {
50
143
  window.adminforth.alert({
@@ -54,6 +147,7 @@ const filters = computed(() => {
54
147
  return [];
55
148
  }
56
149
  return [
150
+ ...filters.value,
57
151
  {
58
152
  field: refColumn.name,
59
153
  operator: 'eq',
@@ -82,7 +176,7 @@ async function getList() {
82
176
  resourceId: listResource.value.resourceId,
83
177
  limit: pageSize.value,
84
178
  offset: (page.value - 1) * pageSize.value,
85
- filters: filters.value,
179
+ filters: endFilters.value,
86
180
  sort: sort.value,
87
181
  }
88
182
  });
@@ -99,11 +193,9 @@ onMounted( async () => {
99
193
  loading.value = true;
100
194
  const foreighResourceId = props.meta.foreignResourceId;
101
195
  listResource.value = (await callAdminForthApi({
102
- path: '/get_resource',
196
+ path: `/plugin/${props.meta.pluginInstanceId}/get_resource`,
103
197
  method: 'POST',
104
- body: {
105
- resourceId: foreighResourceId,
106
- },
198
+ body: {},
107
199
  })).resource;
108
200
  loading.value = false;
109
201
  await getList();
@@ -1,24 +1,45 @@
1
- import { AdminForthResource, AdminForthResourcePages } from "../../types/AdminForthConfig.js";
1
+ import { AdminForthResource, AdminForthResourcePages, AdminForthClass, GenericHttpServer } from "../../types/AdminForthConfig.js";
2
2
  import AdminForthPlugin from "../base.js";
3
- import AdminForth from "../../index.js";
4
3
 
5
4
  type PluginOptions = {
6
5
  foreignResourceId: string;
7
- tablePageSize: number;
6
+ listPageSize?: number;
8
7
  modifyTableResourceConfig?: (resourceConfig: AdminForthResource) => void;
9
8
  }
10
9
 
11
10
  export default class ForeignInlineListPlugin extends AdminForthPlugin {
12
11
  foreignResource: AdminForthResource;
13
12
  options: PluginOptions;
13
+ adminforth: AdminForthClass;
14
14
 
15
15
  constructor(options: PluginOptions) {
16
16
  super(options, import.meta.url);
17
17
  this.options = options;
18
18
  }
19
19
 
20
- modifyResourceConfig(adminforth: AdminForth, resourceConfig: AdminForthResource) {
20
+ setupEndpoints(server: GenericHttpServer) {
21
+ server.endpoint({
22
+ method: 'POST',
23
+ path: `/plugin/${this.pluginInstanceId}/get_resource`,
24
+ handler: async ({ body }) => {
25
+ const resource = this.adminforth.config.resources.find((res) => this.options.foreignResourceId === res.resourceId);
26
+ if (!resource) {
27
+ return { error: `Resource ${this.options.foreignResourceId} not found` };
28
+ }
29
+ // exclude "plugins" key
30
+ const resourceCopy = { ...resource, plugins: undefined };
31
+ if (this.options.modifyTableResourceConfig) {
32
+ this.options.modifyTableResourceConfig(resourceCopy);
33
+ }
34
+ return { resource: resourceCopy };
35
+ },
36
+ });
37
+
38
+ }
39
+
40
+ modifyResourceConfig(adminforth: AdminForthClass, resourceConfig: AdminForthResource) {
21
41
  super.modifyResourceConfig(adminforth, resourceConfig);
42
+ this.adminforth = adminforth;
22
43
 
23
44
  // get resource with foreignResourceId
24
45
  this.foreignResource = adminforth.config.resources.find((resource) => resource.resourceId === this.options.foreignResourceId);
@@ -32,7 +53,13 @@ export default class ForeignInlineListPlugin extends AdminForthPlugin {
32
53
  virtual: true,
33
54
  showIn: [AdminForthResourcePages.show],
34
55
  components: {
35
- showRow: { file: this.componentPath('InlineList.vue'), meta: this.options }
56
+ showRow: {
57
+ file: this.componentPath('InlineList.vue'),
58
+ meta: {
59
+ ...this.options,
60
+ pluginInstanceId: this.pluginInstanceId
61
+ }
62
+ }
36
63
  },
37
64
  });
38
65
  }
package/plugins/base.ts CHANGED
@@ -3,16 +3,23 @@ import { getComponentNameFromPath } from '../modules/utils.js';
3
3
  import { currentFileDir } from '../modules/utils.js';
4
4
  import path from 'path';
5
5
  import fs from 'fs';
6
+ import { v4 as uuidv4 } from 'uuid';
6
7
 
7
8
  export default class AdminForthPlugin implements AdminForthPluginType {
8
9
 
9
10
  adminforth: AdminForthClass;
10
11
  pluginDir: string;
11
12
  customFolderName: string = 'custom';
13
+ pluginInstanceId: string;
12
14
 
13
15
  constructor(pluginOptions: any, metaUrl: string) {
14
16
  // set up plugin here
15
17
  this.pluginDir = currentFileDir(metaUrl);
18
+ this.pluginInstanceId = uuidv4();
19
+ }
20
+
21
+ setupEndpoints(server: any) {
22
+
16
23
  }
17
24
 
18
25
  modifyResourceConfig(adminforth: AdminForthClass, resourceConfig: AdminForthResource) {
package/spa/src/App.vue CHANGED
@@ -59,7 +59,7 @@
59
59
 
60
60
  v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady"
61
61
 
62
- id="logo-sidebar" class="fixed border-none top-0 left-0 z-50 w-64 h-screen transition-transform bg-nav-menu-bg border-r border- sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
62
+ id="logo-sidebar" class="fixed border-none top-0 left-0 z-20 w-64 h-screen transition-transform bg-nav-menu-bg border-r border-nav-menu-border sm:translate-x-0 dark:bg-gray-800 dark:border-gray-700"
63
63
  :class="{ '-translate-x-full': !sideBarOpen, 'transform-none': sideBarOpen }"
64
64
  aria-label="Sidebar"
65
65
  >
@@ -136,7 +136,7 @@
136
136
  <div v-if="sideBarOpen"
137
137
  @click="sideBarOpen = false"
138
138
 
139
- drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"></div>
139
+ drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-5"></div>
140
140
 
141
141
 
142
142
 
@@ -2,7 +2,7 @@
2
2
  <div>
3
3
  <div class="flex items-center justify-between mb-3 flex-wrap gap-y-2">
4
4
  <Breadcrumbs />
5
- <div class="flex items-center space-x-1">
5
+ <div class="flex items-center space-x-1 flex-wrap gap-y-1">
6
6
  <slot></slot>
7
7
  </div>
8
8
  </div>
@@ -26,13 +26,13 @@
26
26
  v-if="c.foreignResource"
27
27
  :options="columnOptions[c.name] || []"
28
28
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
29
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
29
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
30
30
  />
31
31
  <Dropdown
32
32
  v-else-if="c.type === 'boolean'"
33
33
  :options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
34
34
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
35
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
35
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
36
36
  />
37
37
 
38
38
  <Dropdown
@@ -40,7 +40,7 @@
40
40
  :options="c.enum"
41
41
  :allowCustom="c.allowCustom"
42
42
  @update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
43
- :modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
43
+ :modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
44
44
  />
45
45
 
46
46
  <input
@@ -54,9 +54,9 @@
54
54
  <CustomDateRangePicker
55
55
  v-else-if="['datetime'].includes(c.type)"
56
56
  :column="c"
57
- :valueStart="filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
57
+ :valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
58
58
  @update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
59
- :valueEnd="filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
59
+ :valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
60
60
  @update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
61
61
  />
62
62
 
@@ -101,9 +101,9 @@
101
101
 
102
102
  <div class="flex justify-end gap-2">
103
103
  <button
104
- :disabled="!filters.length"
104
+ :disabled="!filtersStore.filters.length"
105
105
  type="button"
106
- class="flex items-center py-1 px-3 mb-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 disabled:opacity-50 disabled:cursor-not-allowed"
106
+ 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 disabled:opacity-50 disabled:cursor-not-allowed"
107
107
  @click="clear">Clear all</button>
108
108
 
109
109
  </div>
@@ -122,6 +122,10 @@ import { callAdminForthApi } from '@/utils';
122
122
  import { useRouter } from 'vue-router';
123
123
  import { computedAsync } from '@vueuse/core'
124
124
  import CustomRangePicker from "@/components/CustomRangePicker.vue";
125
+ import { useFiltersStore } from '@/stores/filters';
126
+
127
+ const filtersStore = useFiltersStore();
128
+
125
129
 
126
130
  // props: columns
127
131
  // add support for v-model:filers
@@ -180,28 +184,28 @@ watch(() => props.show, (show) => {
180
184
 
181
185
  function setFilterItem({ column, operator, value }) {
182
186
 
183
- const index = props.filters.findIndex(f => f.field === column.name && f.operator === operator);
187
+ const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
184
188
  if (value === undefined) {
185
189
  if (index !== -1) {
186
- props.filters.splice(index, 1);
190
+ filtersStore.filters.splice(index, 1);
187
191
  }
188
- emits('update:filters', [...props.filters]);
192
+ emits('update:filters', [...filtersStore.filters]);
189
193
  return;
190
194
  } else {
191
195
  if (index === -1) {
192
- props.filters.push({ field: column.name, value, operator });
196
+ filtersStore.setFilter({ field: column.name, value, operator });
193
197
  } else {
194
- props.filters[index].value = value;
198
+ filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
195
199
  }
196
200
  }
197
- emits('update:filters', [...props.filters]);
201
+ emits('update:filters', [...filtersStore.filters]);
198
202
  }
199
203
 
200
204
  function getFilterItem({ column, operator }) {
201
- return props.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
205
+ return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
202
206
  }
203
207
 
204
208
  async function clear() {
205
- emits('update:filters', []);
209
+ filtersStore.clearFilters();
206
210
  }
207
211
  </script>