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.
- package/dist/index.js +6 -0
- package/dist/modules/styles.js +7 -6
- package/dist/plugins/ForeignInlineListPlugin/index.js +32 -1
- package/dist/plugins/base.js +4 -0
- package/dist/spa/spa/src/App.vue +2 -2
- package/dist/spa/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/spa/src/components/Filters.vue +19 -15
- package/dist/spa/spa/src/components/ResourceForm.vue +12 -12
- package/dist/spa/spa/src/components/ResourceListTable.vue +29 -15
- package/dist/spa/spa/src/composables/useStores.ts +63 -3
- package/dist/spa/spa/src/stores/core.ts +7 -0
- package/dist/spa/spa/src/stores/filters.ts +22 -0
- package/dist/spa/spa/src/stores/modal.ts +0 -1
- package/dist/spa/spa/src/views/CreateView.vue +8 -3
- package/dist/spa/spa/src/views/EditView.vue +2 -2
- package/dist/spa/spa/src/views/ListView.vue +21 -21
- package/dist/spa/spa/src/views/ShowView.vue +3 -3
- package/index.ts +10 -0
- package/modules/styles.ts +7 -6
- package/package.json +1 -1
- package/plugins/ForeignInlineListPlugin/custom/InlineList.vue +105 -13
- package/plugins/ForeignInlineListPlugin/index.ts +32 -5
- package/plugins/base.ts +7 -0
- package/spa/src/App.vue +2 -2
- package/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/spa/src/components/Filters.vue +19 -15
- package/spa/src/components/ResourceForm.vue +12 -12
- package/spa/src/components/ResourceListTable.vue +29 -15
- package/spa/src/composables/useStores.ts +63 -3
- package/spa/src/stores/core.ts +7 -0
- package/spa/src/stores/filters.ts +22 -0
- package/spa/src/stores/modal.ts +0 -1
- package/spa/src/views/CreateView.vue +8 -3
- package/spa/src/views/EditView.vue +2 -2
- package/spa/src/views/ListView.vue +21 -21
- package/spa/src/views/ShowView.vue +3 -3
- package/types/AdminForthConfig.ts +32 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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": "#
|
|
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": "
|
|
12
|
+
"header-border-color": "rgb(229 231 235)", // Light grey border
|
|
13
13
|
|
|
14
|
-
"nav-menu-bg": "#
|
|
15
|
-
"nav-menu-
|
|
16
|
-
"nav-menu-bg-
|
|
17
|
-
"nav-menu-
|
|
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,9 +1,84 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
138
|
+
const refColumn = listResourceRefColumn.value;
|
|
46
139
|
|
|
47
|
-
const primaryKeyColumn =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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-
|
|
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-
|
|
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
|
|
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
190
|
+
filtersStore.filters.splice(index, 1);
|
|
187
191
|
}
|
|
188
|
-
emits('update:filters', [...
|
|
192
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
189
193
|
return;
|
|
190
194
|
} else {
|
|
191
195
|
if (index === -1) {
|
|
192
|
-
|
|
196
|
+
filtersStore.setFilter({ field: column.name, value, operator });
|
|
193
197
|
} else {
|
|
194
|
-
|
|
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', [...
|
|
201
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
function getFilterItem({ column, operator }) {
|
|
201
|
-
return
|
|
205
|
+
return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
|
|
202
206
|
}
|
|
203
207
|
|
|
204
208
|
async function clear() {
|
|
205
|
-
|
|
209
|
+
filtersStore.clearFilters();
|
|
206
210
|
}
|
|
207
211
|
</script>
|