adminforth 1.1.19 → 1.1.20
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 +33 -1
- package/dist/modules/codeInjector.js +1 -1
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +189 -1
- package/dist/spa/package.json +6 -1
- package/dist/spa/spa/src/components/ResourceListTable.vue +4 -2
- package/dist/spa/src/App.vue +76 -57
- package/dist/spa/src/components/AcceptModal.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +3 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +3 -3
- package/dist/spa/src/components/CustomRangePicker.vue +148 -0
- package/dist/spa/src/components/Dropdown.vue +13 -4
- package/dist/spa/src/components/Filters.vue +55 -22
- package/dist/spa/src/components/MenuLink.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +157 -99
- package/dist/spa/src/components/ValueRenderer.vue +11 -1
- package/dist/spa/src/router/index.ts +3 -2
- package/dist/spa/src/stores/core.ts +22 -33
- package/dist/spa/src/utils.ts +5 -3
- package/dist/spa/src/views/CreateView.vue +15 -7
- package/dist/spa/src/views/EditView.vue +36 -8
- package/dist/spa/src/views/ListView.vue +35 -23
- package/dist/spa/src/views/LoginView.vue +1 -1
- package/dist/spa/src/views/ResourceParent.vue +1 -1
- package/dist/spa/src/views/ShowView.vue +20 -9
- package/dist/spa/tailwind.config.js +5 -2
- package/index.ts +37 -1
- package/modules/codeInjector.ts +1 -1
- package/package.json +1 -1
- package/plugins/AccessControl/types.ts +2 -2
- package/spa/src/components/ResourceListTable.vue +4 -2
- package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +0 -248
- package/dist/spa/spa/src/components/ShowTableItem.vue +0 -14
- package/dist/spa/spa/src/views/HomeView.vue +0 -8
- package/dist/spa/src/views/HomeView.vue +0 -8
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<span v-if="column.
|
|
3
|
+
<span v-if="column.foreignResource">
|
|
4
|
+
<RouterLink v-if="row[column.name]" class="font-medium text-blue-600 dark:text-blue-500 hover:brightness-110"
|
|
5
|
+
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey: row[column.name].pk } }">
|
|
6
|
+
{{ row[column.name].label }}
|
|
7
|
+
</RouterLink>
|
|
8
|
+
<div v-else>
|
|
9
|
+
<span class="text-gray-400">-</span>
|
|
10
|
+
</div>
|
|
11
|
+
</span>
|
|
12
|
+
|
|
13
|
+
<span v-else-if="column.type === 'boolean'">
|
|
4
14
|
<span v-if="row[column.name]" class="bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-green-400 border border-green-400">Yes</span>
|
|
5
15
|
<span v-else 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">No</span>
|
|
6
16
|
</span>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
-
import HomeView from '../views/HomeView.vue'
|
|
3
2
|
import ResourceParent from '@/views/ResourceParent.vue'
|
|
4
3
|
import ListView from '@/views/ListView.vue'
|
|
5
4
|
import ShowView from '@/views/ShowView.vue'
|
|
@@ -12,7 +11,9 @@ const router = createRouter({
|
|
|
12
11
|
{
|
|
13
12
|
path: '/',
|
|
14
13
|
name: 'home',
|
|
15
|
-
|
|
14
|
+
//redirect to login
|
|
15
|
+
/* IMPORTANT:ADMINFORTH REDIRECT */
|
|
16
|
+
|
|
16
17
|
},
|
|
17
18
|
{
|
|
18
19
|
path: '/login',
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { ref, computed } from 'vue'
|
|
2
2
|
import { defineStore } from 'pinia'
|
|
3
3
|
import { callAdminForthApi } from '@/utils';
|
|
4
|
-
import router from '@/router';
|
|
5
|
-
|
|
6
|
-
async function findHomepage(menu) {
|
|
7
|
-
for (const item of menu) {
|
|
8
|
-
if (item.homepage) {
|
|
9
|
-
return item;
|
|
10
|
-
}
|
|
11
|
-
if (item.children) {
|
|
12
|
-
const res = findHomepage(item.children);
|
|
13
|
-
if (res) {
|
|
14
|
-
return res;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
4
|
|
|
21
5
|
export const useCoreStore = defineStore('core', () => {
|
|
22
6
|
const resourceById = ref([]);
|
|
@@ -40,35 +24,40 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
40
24
|
}, {});
|
|
41
25
|
config.value = resp.config;
|
|
42
26
|
user.value = resp.user;
|
|
27
|
+
console.log('🌍 AdminForth v', resp.version);
|
|
43
28
|
|
|
44
29
|
// find homepage:true in menu recuresively
|
|
45
|
-
|
|
46
|
-
return
|
|
47
|
-
} else {
|
|
48
|
-
const homepage = await findHomepage(menu.value);
|
|
49
|
-
if (homepage) {
|
|
50
|
-
if (homepage.resourceId) {
|
|
51
|
-
// redirect to homepage
|
|
52
|
-
router.push({ name: 'resource-list', params: { resourceId: homepage.resourceId } });
|
|
53
|
-
} else {
|
|
54
|
-
// redirect to path
|
|
55
|
-
router.push(homepage.path);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
30
|
+
|
|
59
31
|
}
|
|
60
32
|
|
|
61
33
|
async function fetchRecord({ resourceId, primaryKey }) {
|
|
62
34
|
record.value = null;
|
|
35
|
+
|
|
36
|
+
if (!resourceColumns.value) {
|
|
37
|
+
throw new Error('Columns not fetched yet');
|
|
38
|
+
}
|
|
63
39
|
|
|
64
|
-
|
|
65
|
-
path: '/
|
|
40
|
+
const respData = await callAdminForthApi({
|
|
41
|
+
path: '/get_resource_data',
|
|
66
42
|
method: 'POST',
|
|
67
43
|
body: {
|
|
44
|
+
source: 'show',
|
|
68
45
|
resourceId: resourceId,
|
|
69
|
-
|
|
46
|
+
filters: [
|
|
47
|
+
{
|
|
48
|
+
field: resourceColumns.value.find((col) => col.primaryKey).name,
|
|
49
|
+
operator: 'eq',
|
|
50
|
+
value: primaryKey
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
sort: [],
|
|
54
|
+
limit: 1,
|
|
55
|
+
offset: 0
|
|
70
56
|
}
|
|
71
57
|
});
|
|
58
|
+
|
|
59
|
+
console.log('📦 record', respData);
|
|
60
|
+
record.value = respData.data[0];
|
|
72
61
|
}
|
|
73
62
|
|
|
74
63
|
async function fetchColumns({ resourceId }) {
|
package/dist/spa/src/utils.ts
CHANGED
|
@@ -29,9 +29,11 @@ export async function callAdminForthApi({ path, method, body=undefined }) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
export function getCustomComponent(filePath: string) {
|
|
33
|
+
const name = filePath.replace(/@/g, '').replace(/\./g, '').replace(/\//g, '');
|
|
34
|
+
console.log('resolving name', name);
|
|
35
|
+
return resolveComponent(name);
|
|
36
|
+
}
|
|
35
37
|
|
|
36
38
|
export function getIcon(icon: string) {
|
|
37
39
|
// icon format is "feather:icon-name". We need to get IconName in pascal case
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
@update:record="onUpdateRecord"
|
|
32
32
|
@update:isValid="isValid = $event"
|
|
33
33
|
:validating="validating"
|
|
34
|
+
:customComponentsPerColumn="createComponentsPerColumn"
|
|
34
35
|
>
|
|
35
36
|
</ResourceForm>
|
|
36
37
|
|
|
@@ -40,14 +41,14 @@
|
|
|
40
41
|
|
|
41
42
|
<script setup>
|
|
42
43
|
|
|
43
|
-
import { ref, computed, onMounted } from 'vue';
|
|
44
|
-
import { useRoute, useRouter } from 'vue-router';
|
|
45
|
-
import { useCoreStore } from '@/stores/core';
|
|
46
|
-
import { callAdminForthApi } from '@/utils';
|
|
47
|
-
import ResourceForm from '@/components/ResourceForm.vue';
|
|
48
44
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
49
|
-
import
|
|
45
|
+
import ResourceForm from '@/components/ResourceForm.vue';
|
|
50
46
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
47
|
+
import { useCoreStore } from '@/stores/core';
|
|
48
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
49
|
+
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
50
|
+
import { onMounted, ref } from 'vue';
|
|
51
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
51
52
|
|
|
52
53
|
const isValid = ref(false);
|
|
53
54
|
const validating = ref(false);
|
|
@@ -59,6 +60,7 @@ const route = useRoute();
|
|
|
59
60
|
const router = useRouter();
|
|
60
61
|
|
|
61
62
|
const record = ref({});
|
|
63
|
+
let createComponentsPerColumn = {};
|
|
62
64
|
|
|
63
65
|
const coreStore = useCoreStore();
|
|
64
66
|
|
|
@@ -69,9 +71,15 @@ async function onUpdateRecord(newRecord) {
|
|
|
69
71
|
|
|
70
72
|
onMounted(async () => {
|
|
71
73
|
loading.value = true;
|
|
72
|
-
coreStore.fetchColumns({
|
|
74
|
+
await coreStore.fetchColumns({
|
|
73
75
|
resourceId: route.params.resourceId
|
|
74
76
|
});
|
|
77
|
+
createComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
78
|
+
if (column.component?.create) {
|
|
79
|
+
acc[column.name] = getCustomComponent(column.component.create);
|
|
80
|
+
}
|
|
81
|
+
return acc;
|
|
82
|
+
}, {});
|
|
75
83
|
loading.value = false;
|
|
76
84
|
});
|
|
77
85
|
|
|
@@ -23,11 +23,12 @@
|
|
|
23
23
|
|
|
24
24
|
<ResourceForm
|
|
25
25
|
v-else
|
|
26
|
-
:record="
|
|
26
|
+
:record="editableRecord"
|
|
27
27
|
:resourceColumns="coreStore.resourceColumns"
|
|
28
28
|
@update:record="onUpdateRecord"
|
|
29
29
|
@update:isValid="isValid = $event"
|
|
30
30
|
:validating="validating"
|
|
31
|
+
:customComponentsPerColumn="editComponentsPerColumn"
|
|
31
32
|
>
|
|
32
33
|
</ResourceForm>
|
|
33
34
|
</div>
|
|
@@ -36,14 +37,14 @@
|
|
|
36
37
|
|
|
37
38
|
<script setup>
|
|
38
39
|
|
|
39
|
-
import { ref, computed, onMounted } from 'vue';
|
|
40
|
-
import { useRoute, useRouter } from 'vue-router';
|
|
41
|
-
import { useCoreStore } from '@/stores/core';
|
|
42
|
-
import ResourceForm from '@/components/ResourceForm.vue';
|
|
43
40
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
44
|
-
import
|
|
41
|
+
import ResourceForm from '@/components/ResourceForm.vue';
|
|
45
42
|
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
46
|
-
import {
|
|
43
|
+
import { useCoreStore } from '@/stores/core';
|
|
44
|
+
import { callAdminForthApi, getCustomComponent } from '@/utils';
|
|
45
|
+
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
46
|
+
import { computed, onMounted, ref } from 'vue';
|
|
47
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
47
48
|
|
|
48
49
|
const coreStore = useCoreStore();
|
|
49
50
|
|
|
@@ -58,11 +59,24 @@ const loading = ref(false);
|
|
|
58
59
|
const saving = ref(false);
|
|
59
60
|
|
|
60
61
|
const record = ref({});
|
|
62
|
+
let editComponentsPerColumn = {};
|
|
61
63
|
|
|
62
64
|
async function onUpdateRecord(newRecord) {
|
|
63
65
|
record.value = newRecord;
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
const editableRecord = computed(() => {
|
|
69
|
+
const newRecord = { ...coreStore.record };
|
|
70
|
+
if (!coreStore.resourceColumns) {
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
coreStore.resourceColumns.forEach(column => {
|
|
74
|
+
if (column.foreignResource) {
|
|
75
|
+
newRecord[column.name] = newRecord[column.name]?.pk
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return newRecord;
|
|
79
|
+
})
|
|
66
80
|
|
|
67
81
|
onMounted(async () => {
|
|
68
82
|
loading.value = true;
|
|
@@ -75,6 +89,12 @@ onMounted(async () => {
|
|
|
75
89
|
primaryKey: route.params.primaryKey,
|
|
76
90
|
});
|
|
77
91
|
|
|
92
|
+
editComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
93
|
+
if (column.component?.edit) {
|
|
94
|
+
acc[column.name] = getCustomComponent(column.component.edit);
|
|
95
|
+
}
|
|
96
|
+
return acc;
|
|
97
|
+
}, {});
|
|
78
98
|
loading.value = false;
|
|
79
99
|
});
|
|
80
100
|
|
|
@@ -87,13 +107,21 @@ async function saveRecord() {
|
|
|
87
107
|
}
|
|
88
108
|
|
|
89
109
|
saving.value = true;
|
|
110
|
+
|
|
111
|
+
const updates = {};
|
|
112
|
+
for (const key in record.value) {
|
|
113
|
+
if (record.value[key] !== coreStore.record[key]) {
|
|
114
|
+
updates[key] = record.value[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
await callAdminForthApi({
|
|
91
119
|
method: 'POST',
|
|
92
120
|
path: `/update_record`,
|
|
93
121
|
body: {
|
|
94
122
|
resourceId: route.params.resourceId,
|
|
95
123
|
recordId: route.params.primaryKey,
|
|
96
|
-
record:
|
|
124
|
+
record: updates,
|
|
97
125
|
},
|
|
98
126
|
});
|
|
99
127
|
saving.value = false;
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
</BreadcrumbsWithButtons>
|
|
66
66
|
|
|
67
67
|
<!-- table -->
|
|
68
|
-
<div class="relative overflow-x-auto shadow-
|
|
68
|
+
<div class="relative overflow-x-auto shadow-list-table-shadow dark:shadow-black dark:s">
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
<!-- skelet loader -->
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
</div>
|
|
91
91
|
|
|
92
92
|
<table v-else class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
93
|
-
<thead class="text-xs text-
|
|
93
|
+
<thead class="text-xs text-view-table-heading-text bg-list-view-table-heading dark:bg-gray-700 dark:text-gray-400">
|
|
94
94
|
<tr>
|
|
95
95
|
<th scope="col" class="p-4">
|
|
96
96
|
<div v-if="rows && rows.length" class="flex items-center">
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
</tr>
|
|
128
128
|
</thead>
|
|
129
129
|
<tbody>
|
|
130
|
-
<tr v-if="!rows" class="bg-
|
|
130
|
+
<tr v-if="!rows" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
|
|
131
131
|
<td :colspan="coreStore.resourceColumns.length + 2">
|
|
132
132
|
|
|
133
133
|
<div role="status"
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
</div>
|
|
172
172
|
</td>
|
|
173
173
|
</tr>
|
|
174
|
-
<tr v-else-if="rows.length === 0" class="bg-
|
|
174
|
+
<tr v-else-if="rows.length === 0" class="bg-list-view-table-bg border-b dark:bg-gray-800 dark:border-gray-700">
|
|
175
175
|
<td :colspan="coreStore.resourceColumns.length + 2">
|
|
176
176
|
|
|
177
177
|
<div id="toast-simple"
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
</tr>
|
|
186
186
|
|
|
187
187
|
<tr v-else v-for="(row, rowI) in rows" :key="row.id"
|
|
188
|
-
class="bg-
|
|
188
|
+
class="bg-list-view-table-bg border-b dark:bg-gray-800 border-list-view-border-color dark:border-gray-700 hover:bg-list-view-table-row-hover dark:hover:bg-gray-600">
|
|
189
189
|
<td class="w-4 p-4">
|
|
190
190
|
<div class="flex items center">
|
|
191
191
|
<input
|
|
@@ -198,7 +198,12 @@
|
|
|
198
198
|
</div>
|
|
199
199
|
</td>
|
|
200
200
|
<td v-for="c in columnsListed" class="px-6 py-4">
|
|
201
|
-
|
|
201
|
+
<!-- if c.name in listComponentsPerColumn, render it. If not, render ValueRenderer -->
|
|
202
|
+
<component
|
|
203
|
+
:is="listComponentsPerColumn[c.name] || ValueRenderer"
|
|
204
|
+
:column="c"
|
|
205
|
+
:row="row"
|
|
206
|
+
/>
|
|
202
207
|
</td>
|
|
203
208
|
<td class="flex items-center px-6 py-4">
|
|
204
209
|
|
|
@@ -310,29 +315,28 @@
|
|
|
310
315
|
</template>
|
|
311
316
|
|
|
312
317
|
<script setup>
|
|
313
|
-
import {ref, onMounted, watch, computed} from 'vue';
|
|
314
|
-
import {callAdminForthApi, getIcon} from '@/utils';
|
|
315
|
-
import {useRoute} from 'vue-router';
|
|
316
|
-
import {useCoreStore} from '@/stores/core';
|
|
317
|
-
import {useModalStore} from '@/stores/modal';
|
|
318
318
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
319
|
-
import {
|
|
319
|
+
import { useCoreStore } from '@/stores/core';
|
|
320
|
+
import { useModalStore } from '@/stores/modal';
|
|
321
|
+
import { callAdminForthApi, getIcon } from '@/utils';
|
|
322
|
+
import { initFlowbite } from 'flowbite';
|
|
323
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
324
|
+
import { useRoute } from 'vue-router';
|
|
320
325
|
|
|
321
326
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
327
|
+
import { getCustomComponent } from '@/utils';
|
|
322
328
|
|
|
323
329
|
import {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
IconInboxFullSolid,
|
|
327
|
-
IconInboxOutline,
|
|
328
|
-
IconPlusOutline
|
|
330
|
+
IconInboxOutline,
|
|
331
|
+
IconPlusOutline
|
|
329
332
|
} from '@iconify-prerendered/vue-flowbite';
|
|
330
333
|
|
|
331
334
|
import {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
335
|
+
IconBanOutline,
|
|
336
|
+
IconEyeSolid,
|
|
337
|
+
IconFilterOutline,
|
|
338
|
+
IconPenSolid,
|
|
339
|
+
IconTrashBinSolid
|
|
336
340
|
} from '@iconify-prerendered/vue-flowbite';
|
|
337
341
|
|
|
338
342
|
import Filters from '@/components/Filters.vue';
|
|
@@ -357,7 +361,6 @@ const totalRows = ref(0);
|
|
|
357
361
|
const allCheckedActions = ref([]);
|
|
358
362
|
const allowedActions = ref({});
|
|
359
363
|
|
|
360
|
-
|
|
361
364
|
const DEFAULT_PAGE_SIZE = 10;
|
|
362
365
|
|
|
363
366
|
const columnsListed = computed(() => coreStore.resourceColumns?.filter(c => c.showIn.includes('list')));
|
|
@@ -376,6 +379,7 @@ async function selectAll(value) {
|
|
|
376
379
|
|
|
377
380
|
const pageSize = computed(() => coreStore.resourceById[route.params.resourceId]?.pageSize || DEFAULT_PAGE_SIZE);
|
|
378
381
|
const totalPages = computed(() => Math.ceil(totalRows.value / pageSize.value));
|
|
382
|
+
let listComponentsPerColumn = {};
|
|
379
383
|
const allFromThisPageChecked = computed(() => {
|
|
380
384
|
if (!rows.value) return false;
|
|
381
385
|
return rows.value.every((r) => checkboxes.value.includes(r.id));
|
|
@@ -419,6 +423,7 @@ async function getList() {
|
|
|
419
423
|
path: '/get_resource_data',
|
|
420
424
|
method: 'POST',
|
|
421
425
|
body: {
|
|
426
|
+
source: 'list',
|
|
422
427
|
resourceId: route.params.resourceId,
|
|
423
428
|
limit: pageSize.value,
|
|
424
429
|
offset: (page.value - 1) * pageSize.value,
|
|
@@ -426,7 +431,14 @@ async function getList() {
|
|
|
426
431
|
sort: sort.value,
|
|
427
432
|
}
|
|
428
433
|
});
|
|
429
|
-
|
|
434
|
+
listComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
435
|
+
if (column.component?.list) {
|
|
436
|
+
acc[column.name] = getCustomComponent(column.component.list);
|
|
437
|
+
}
|
|
438
|
+
return acc;
|
|
439
|
+
}, {});
|
|
440
|
+
|
|
441
|
+
fetchStatus.value.pending = false;
|
|
430
442
|
rows.value = data.data?.map(row => {
|
|
431
443
|
row._primaryKeyValue = row[coreStore.resourceColumns.find(c => c.primaryKey).name];
|
|
432
444
|
return row;
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<div id="authentication-modal" tabindex="-1" class=" overflow-y-auto overflow-x-hidden z-50 min-w-[400px] justify-center items-center md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
12
12
|
<div class="relative p-4 w-full max-w-md max-h-full">
|
|
13
13
|
<!-- Modal content -->
|
|
14
|
-
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
|
14
|
+
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700 dark:shadow-black" >
|
|
15
15
|
<!-- Modal header -->
|
|
16
16
|
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
|
|
17
17
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
</div>
|
|
28
28
|
<div
|
|
29
29
|
v-else
|
|
30
|
-
class="relative overflow-x-auto shadow-
|
|
30
|
+
class="relative overflow-x-auto shadow-resourse-form-shadow "
|
|
31
31
|
>
|
|
32
32
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
33
|
-
<thead class="text-xs text-gray-700 uppercase bg-
|
|
33
|
+
<thead class="text-xs text-gray-700 uppercase bg-form-view-heading dark:bg-gray-700 dark:text-gray-400">
|
|
34
34
|
<tr>
|
|
35
35
|
<th scope="col" class="px-6 py-3">
|
|
36
36
|
Field
|
|
@@ -42,13 +42,17 @@
|
|
|
42
42
|
</thead>
|
|
43
43
|
<tbody>
|
|
44
44
|
<tr v-for="column in coreStore.resourceColumns?.filter(c => c.showIn.includes('show'))" :key="column.name"
|
|
45
|
-
class="
|
|
45
|
+
class="bg-form-view-bg odd:dark:bg-gray-900 even:dark:bg-gray-800 border-b dark:border-gray-700"
|
|
46
46
|
>
|
|
47
47
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
48
48
|
{{ column.label }}
|
|
49
49
|
</td>
|
|
50
50
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap">
|
|
51
|
-
<
|
|
51
|
+
<component
|
|
52
|
+
:is="showComponentsPerColumn[column.name] || ValueRenderer"
|
|
53
|
+
:column="column"
|
|
54
|
+
:row="coreStore.record"
|
|
55
|
+
/>
|
|
52
56
|
</td>
|
|
53
57
|
</tr>
|
|
54
58
|
|
|
@@ -63,18 +67,19 @@
|
|
|
63
67
|
|
|
64
68
|
<script setup>
|
|
65
69
|
|
|
66
|
-
import { ref, computed, onMounted } from 'vue';
|
|
67
|
-
import { useRoute } from 'vue-router';
|
|
68
|
-
import { callAdminForthApi } from '@/utils';
|
|
69
70
|
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
70
|
-
import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
71
|
-
import { useCoreStore } from '@/stores/core';
|
|
72
71
|
import ValueRenderer from '@/components/ValueRenderer.vue';
|
|
72
|
+
import { useCoreStore } from '@/stores/core';
|
|
73
|
+
import { getCustomComponent } from '@/utils';
|
|
74
|
+
import { IconPenSolid, IconTrashBinSolid } from '@iconify-prerendered/vue-flowbite';
|
|
75
|
+
import { onMounted, ref } from 'vue';
|
|
76
|
+
import { useRoute } from 'vue-router';
|
|
73
77
|
|
|
74
78
|
|
|
75
79
|
const item = ref(null);
|
|
76
80
|
const route = useRoute();
|
|
77
81
|
const loading = ref(false);
|
|
82
|
+
let showComponentsPerColumn = {};
|
|
78
83
|
|
|
79
84
|
const coreStore = useCoreStore();
|
|
80
85
|
|
|
@@ -87,6 +92,12 @@ onMounted(async () => {
|
|
|
87
92
|
resourceId: route.params.resourceId,
|
|
88
93
|
primaryKey: route.params.primaryKey,
|
|
89
94
|
});
|
|
95
|
+
showComponentsPerColumn = coreStore.resourceColumns.reduce((acc, column) => {
|
|
96
|
+
if (column.component?.show) {
|
|
97
|
+
acc[column.name] = getCustomComponent(column.component.show);
|
|
98
|
+
}
|
|
99
|
+
return acc;
|
|
100
|
+
}, {});
|
|
90
101
|
loading.value = false;
|
|
91
102
|
});
|
|
92
103
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/** @type {import('tailwindcss').Config} */
|
|
2
2
|
export default {
|
|
3
|
-
content: ["./src/**/*.{vue, js}", "./node_modules/flowbite/**/*.js"],
|
|
3
|
+
content: ["./src/**/*.{vue, js}","./src/*.{vue, js}", "./index.html", "./node_modules/flowbite/**/*.js"],
|
|
4
4
|
theme: {
|
|
5
|
-
extend: {
|
|
5
|
+
extend: {
|
|
6
|
+
/* IMPORTANT:ADMINFORTH TAILWIND STYLES */
|
|
7
|
+
}
|
|
6
8
|
},
|
|
9
|
+
|
|
7
10
|
darkMode: 'class',
|
|
8
11
|
plugins: [
|
|
9
12
|
require('flowbite/plugin'),
|
package/index.ts
CHANGED
|
@@ -648,13 +648,49 @@ class AdminForth implements AdminForthClass {
|
|
|
648
648
|
[this.config.auth.usernameField]: username,
|
|
649
649
|
[this.config.auth.userFullNameField]: userFullName
|
|
650
650
|
};
|
|
651
|
+
const checkIsMenuItemVisible = (menuItem) => {
|
|
652
|
+
if (typeof menuItem.visible === 'function') {
|
|
653
|
+
const toReturn = menuItem.visible( adminUser );
|
|
654
|
+
if (typeof toReturn !== 'boolean') {
|
|
655
|
+
throw new Error(`'visible' function of ${menuItem.label || menuItem.type } must return boolean value`);
|
|
656
|
+
}
|
|
657
|
+
return toReturn;
|
|
658
|
+
}}
|
|
659
|
+
let newMenu = []
|
|
660
|
+
for (let menuItem of this.config.menu) {
|
|
661
|
+
let newMenuItem = {...menuItem,}
|
|
662
|
+
if (menuItem.visible){
|
|
663
|
+
if (!checkIsMenuItemVisible(menuItem)){
|
|
664
|
+
continue
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (menuItem.children){
|
|
668
|
+
let newChildren = []
|
|
669
|
+
for (let child of menuItem.children){
|
|
670
|
+
let newChild = {...child,}
|
|
671
|
+
if (child.visible){
|
|
672
|
+
if (!checkIsMenuItemVisible(child)){
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
newChildren.push(newChild)
|
|
677
|
+
}
|
|
678
|
+
newMenuItem = {...newMenuItem, children: newChildren}
|
|
679
|
+
}
|
|
680
|
+
newMenu.push(newMenuItem)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
|
|
651
687
|
return {
|
|
652
688
|
user: userData,
|
|
653
689
|
resources: this.config.resources.map((res) => ({
|
|
654
690
|
resourceId: res.resourceId,
|
|
655
691
|
label: res.label,
|
|
656
692
|
})),
|
|
657
|
-
menu:
|
|
693
|
+
menu: newMenu,
|
|
658
694
|
config: {
|
|
659
695
|
brandName: this.config.customization.brandName,
|
|
660
696
|
brandLogo: this.config.customization.brandLogo,
|
package/modules/codeInjector.ts
CHANGED
|
@@ -149,7 +149,7 @@ class CodeInjector implements CodeInjectorType {
|
|
|
149
149
|
routes += `{
|
|
150
150
|
path: '${item.path}',
|
|
151
151
|
name: '${item.path}',
|
|
152
|
-
component: () => import('${item.component}'),
|
|
152
|
+
component: () => import('${item.component}'),
|
|
153
153
|
meta: { title: '${item?.meta?.title}'
|
|
154
154
|
},\n`
|
|
155
155
|
|
package/package.json
CHANGED
|
@@ -7,9 +7,9 @@ export type PluginOptions = {
|
|
|
7
7
|
* Called to check if user has access to a page
|
|
8
8
|
* Should return `true` if user has access. If user has no access, return `false` or a string with an error message
|
|
9
9
|
* @param user - The user object
|
|
10
|
-
* @param
|
|
10
|
+
* @param page - The page to check access for
|
|
11
11
|
* @param meta - The meta object containing query for list/show or record for save/edit
|
|
12
12
|
* @returns {boolean | string} -
|
|
13
13
|
*/
|
|
14
|
-
hasAccess: (user: AdminUser,
|
|
14
|
+
hasAccess: (user: AdminUser, page: AllowedActionsEnum, meta: any) => Promise<boolean | string>;
|
|
15
15
|
}
|
|
@@ -358,9 +358,11 @@ async function selectAll(value) {
|
|
|
358
358
|
const totalPages = computed(() => Math.ceil(props.totalRows / props.pageSize));
|
|
359
359
|
|
|
360
360
|
|
|
361
|
+
|
|
362
|
+
|
|
361
363
|
const allFromThisPageChecked = computed(() => {
|
|
362
|
-
if (!props.rows
|
|
363
|
-
return props.rows.
|
|
364
|
+
if (!props.rows) return false;
|
|
365
|
+
return props.rows.every((r) => props.checkboxes.includes(r.id));
|
|
364
366
|
});
|
|
365
367
|
const ascArr = computed(() => sort.value.filter((s) => s.direction === 'asc').map((s) => s.field));
|
|
366
368
|
const descArr = computed(() => sort.value.filter((s) => s.direction === 'desc').map((s) => s.field));
|