adminforth 1.1.23 → 1.1.25
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 +12 -12
- package/dist/plugins/AccessControl/index.js +3 -56
- package/dist/plugins/AuditLog/index.js +13 -0
- package/dist/plugins/AuditLog/types.js +1 -0
- package/dist/plugins/plugins/ForeignInlineListPlugin/custom/InlineList.vue +248 -0
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +1 -189
- package/dist/spa/package.json +1 -6
- package/dist/spa/spa/src/components/ShowTableItem.vue +14 -0
- package/dist/spa/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/src/App.vue +57 -76
- 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/Dropdown.vue +4 -13
- package/dist/spa/src/components/Filters.vue +22 -55
- package/dist/spa/src/components/MenuLink.vue +2 -2
- package/dist/spa/src/components/ResourceForm.vue +99 -157
- package/dist/spa/src/components/ValueRenderer.vue +1 -11
- package/dist/spa/src/router/index.ts +2 -3
- package/dist/spa/src/stores/core.ts +33 -22
- package/dist/spa/src/utils.ts +3 -5
- package/dist/spa/src/views/CreateView.vue +7 -15
- package/dist/spa/src/views/EditView.vue +8 -36
- package/dist/spa/src/views/HomeView.vue +8 -0
- package/dist/spa/src/views/ListView.vue +23 -35
- 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 +9 -20
- package/dist/spa/tailwind.config.js +2 -5
- package/dist/types/AdminForthConfig.js +9 -0
- package/index.ts +16 -18
- package/package.json +1 -1
- package/plugins/AuditLog/index.ts +29 -0
- package/plugins/AuditLog/types.ts +4 -0
- package/types/AdminForthConfig.ts +23 -1
- package/dist/spa/src/components/CustomRangePicker.vue +0 -148
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<!-- drawer component -->
|
|
3
3
|
<div id="drawer-navigation"
|
|
4
4
|
|
|
5
|
-
class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl
|
|
5
|
+
class="fixed top-14 right-0 z-40 p-4 overflow-y-auto transition-transform translate-x-full bg-white w-80 dark:bg-gray-800 shadow-xl"
|
|
6
6
|
|
|
7
7
|
:class="show ? 'top-0 transform-none' : ''"
|
|
8
8
|
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
@@ -22,14 +22,8 @@
|
|
|
22
22
|
<li v-for="c in columnsWithFilter" :key="c">
|
|
23
23
|
<p class="dark:text-gray-400">{{ c.label }}</p>
|
|
24
24
|
|
|
25
|
-
<Dropdown
|
|
26
|
-
v-if="c.
|
|
27
|
-
:options="columnOptions[c.name] || []"
|
|
28
|
-
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
29
|
-
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
30
|
-
/>
|
|
31
|
-
<Dropdown
|
|
32
|
-
v-else-if="c.type === 'boolean'"
|
|
25
|
+
<Dropdown
|
|
26
|
+
v-if="c.type === 'boolean'"
|
|
33
27
|
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
34
28
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
35
29
|
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
@@ -68,15 +62,22 @@
|
|
|
68
62
|
:value="getFilterItem({ column: c, operator: 'ilike' })"
|
|
69
63
|
>
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
|
|
66
|
+
<input
|
|
67
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
68
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
69
|
+
placeholder="From"
|
|
70
|
+
@input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
|
|
71
|
+
:value="getFilterItem({ column: c, operator: 'gte' })"
|
|
72
|
+
>
|
|
73
|
+
<input
|
|
74
|
+
type="number" aria-describedby="helper-text-explanation"
|
|
75
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
76
|
+
placeholder="To"
|
|
77
|
+
@input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
|
|
78
|
+
:value="getFilterItem({ column: c, operator: 'lte' })"
|
|
79
|
+
>
|
|
80
|
+
</div>
|
|
80
81
|
|
|
81
82
|
</li>
|
|
82
83
|
</ul>
|
|
@@ -92,59 +93,25 @@
|
|
|
92
93
|
</div>
|
|
93
94
|
</div>
|
|
94
95
|
|
|
95
|
-
<div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-
|
|
96
|
+
<div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
|
|
96
97
|
@click="$emit('hide')">
|
|
97
98
|
</div>
|
|
98
99
|
</template>
|
|
99
100
|
|
|
100
101
|
<script setup>
|
|
101
|
-
import { watch, computed
|
|
102
|
+
import { watch, computed } from 'vue'
|
|
102
103
|
import Dropdown from '@/components/Dropdown.vue';
|
|
103
104
|
import CustomDateRangePicker from '@/components/CustomDateRangePicker.vue';
|
|
104
|
-
import { callAdminForthApi } from '@/utils';
|
|
105
|
-
import { useRouter } from 'vue-router';
|
|
106
|
-
import { computedAsync } from '@vueuse/core'
|
|
107
|
-
import CustomRangePicker from "@/components/CustomRangePicker.vue";
|
|
108
105
|
|
|
109
106
|
// props: columns
|
|
110
107
|
// add support for v-model:filers
|
|
111
108
|
const props = defineProps(['columns', 'filters', 'show', 'columnsMinMax']);
|
|
112
109
|
const emits = defineEmits(['update:filters', 'hide']);
|
|
113
110
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const columnsWithFilter = computed(
|
|
111
|
+
const columnsWithFilter = computed(
|
|
118
112
|
() => props.columns?.filter(column => column.showIn.includes('filter')) || []
|
|
119
113
|
);
|
|
120
114
|
|
|
121
|
-
const columnOptions = computedAsync(async () => {
|
|
122
|
-
const ret = {};
|
|
123
|
-
if (!props.columns) {
|
|
124
|
-
return ret;
|
|
125
|
-
}
|
|
126
|
-
await Promise.all(
|
|
127
|
-
Object.values(props.columns).map(async (column) => {
|
|
128
|
-
if (column.foreignResource) {
|
|
129
|
-
const list = await callAdminForthApi({
|
|
130
|
-
method: 'POST',
|
|
131
|
-
path: `/get_resource_foreign_data`,
|
|
132
|
-
body: {
|
|
133
|
-
resourceId: router.currentRoute.value.params.resourceId,
|
|
134
|
-
column: column.name,
|
|
135
|
-
limit: 1000,
|
|
136
|
-
offset: 0,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
ret[column.name] = list.items;
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
return ret;
|
|
145
|
-
}, {});
|
|
146
|
-
|
|
147
|
-
|
|
148
115
|
// sync 'body' class 'overflow-hidden' with show prop show
|
|
149
116
|
watch(() => props.show, (show) => {
|
|
150
117
|
if (show) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<RouterLink
|
|
3
3
|
:to="{name: item.resourceId ? 'resource-list' : item.path, params: item.resourceId ? { resourceId: item.resourceId }: {}}"
|
|
4
|
-
class="flex items-center py-2 text-
|
|
4
|
+
class="flex items-center py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white" role="menuitem"
|
|
5
5
|
:class="{
|
|
6
6
|
'px-4': isChild,
|
|
7
7
|
'px-2': !isChild,
|
|
8
|
-
'bg-
|
|
8
|
+
'bg-blue-100 dark:bg-gray-700': item.resourceId ?
|
|
9
9
|
($route.params.resourceId === item.resourceId && $route.name === 'resource-list') :
|
|
10
10
|
($route.name === item.path)
|
|
11
11
|
}"
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
|
|
4
4
|
<div
|
|
5
|
-
class="relative shadow-
|
|
5
|
+
class="relative shadow-md sm:rounded-lg dark:shadow-2xl"
|
|
6
6
|
>
|
|
7
7
|
<form autocomplete="off" @submit.prevent>
|
|
8
8
|
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
9
|
-
<thead class="text-xs text-gray-700 uppercase bg-
|
|
9
|
+
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
|
10
10
|
<tr>
|
|
11
11
|
<th scope="col" class="px-6 py-3">
|
|
12
12
|
Field
|
|
@@ -18,116 +18,97 @@
|
|
|
18
18
|
</thead>
|
|
19
19
|
<tbody>
|
|
20
20
|
<tr v-for="column, i in editableColumns" :key="column.name"
|
|
21
|
-
class="bg-
|
|
21
|
+
class="odd:bg-white odd:dark:bg-gray-900 even:bg-gray-50 even:dark:bg-gray-800 border-b dark:border-gray-700"
|
|
22
22
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
:value="currentValues[column.name]"
|
|
29
|
-
@update:value="setCurrentValue(column.name, $event)"
|
|
23
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
24
|
+
{{ column.label }}
|
|
25
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
|
|
26
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
27
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
30
28
|
/>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<button
|
|
115
|
-
v-if="column.masked"
|
|
116
|
-
type="button"
|
|
117
|
-
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
118
|
-
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
119
|
-
>
|
|
120
|
-
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
121
|
-
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
122
|
-
</button>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
127
|
-
|
|
128
|
-
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
129
|
-
</td>
|
|
130
|
-
</template>
|
|
29
|
+
</span>
|
|
30
|
+
<div :id="`tooltip-show-${i}`"
|
|
31
|
+
role="tooltip" 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">
|
|
32
|
+
Required field
|
|
33
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
34
|
+
</div>
|
|
35
|
+
</td>
|
|
36
|
+
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
37
|
+
<Dropdown
|
|
38
|
+
single
|
|
39
|
+
v-if="column.enum"
|
|
40
|
+
:options="column.enum"
|
|
41
|
+
:modelValue="currentValues[column.name]"
|
|
42
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
43
|
+
/>
|
|
44
|
+
<Dropdown
|
|
45
|
+
single
|
|
46
|
+
v-else-if="column.type === 'boolean'"
|
|
47
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
48
|
+
:modelValue="currentValues[column.name]"
|
|
49
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
50
|
+
/>
|
|
51
|
+
<input
|
|
52
|
+
v-else-if="['integer'].includes(column.type)"
|
|
53
|
+
type="number"
|
|
54
|
+
step="1"
|
|
55
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
56
|
+
placeholder="0"
|
|
57
|
+
:value="currentValues[column.name]"
|
|
58
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
59
|
+
>
|
|
60
|
+
<CustomDatePicker
|
|
61
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
62
|
+
:column="column"
|
|
63
|
+
:valueStart="currentValues[column.name]"
|
|
64
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
65
|
+
/>
|
|
66
|
+
<input
|
|
67
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
68
|
+
type="number"
|
|
69
|
+
step="0.1"
|
|
70
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-40 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
71
|
+
placeholder="0.0"
|
|
72
|
+
:value="currentValues[column.name]"
|
|
73
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
74
|
+
/>
|
|
75
|
+
<textarea
|
|
76
|
+
v-else-if="['text'].includes(column.type)"
|
|
77
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
78
|
+
placeholder="Text"
|
|
79
|
+
:value="currentValues[column.name]"
|
|
80
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
81
|
+
>
|
|
82
|
+
</textarea>
|
|
83
|
+
<input
|
|
84
|
+
v-else
|
|
85
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
86
|
+
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
87
|
+
placeholder="Text"
|
|
88
|
+
:value="currentValues[column.name]"
|
|
89
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
90
|
+
autocomplete="false"
|
|
91
|
+
data-lpignore="true"
|
|
92
|
+
readonly
|
|
93
|
+
onfocus="this.removeAttribute('readonly');"
|
|
94
|
+
>
|
|
95
|
+
|
|
96
|
+
<button
|
|
97
|
+
v-if="column.masked"
|
|
98
|
+
type="button"
|
|
99
|
+
@click="unmasked[column.name] = !unmasked[column.name]"
|
|
100
|
+
class="h-6 absolute inset-y-2 top-6 right-6 flex items-center pr-2 z-index-100 focus:outline-none"
|
|
101
|
+
>
|
|
102
|
+
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
103
|
+
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
104
|
+
</button>
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
109
|
+
|
|
110
|
+
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
111
|
+
</td>
|
|
131
112
|
</tr>
|
|
132
113
|
|
|
133
114
|
</tbody>
|
|
@@ -140,24 +121,19 @@
|
|
|
140
121
|
|
|
141
122
|
<script setup>
|
|
142
123
|
|
|
143
|
-
import
|
|
144
|
-
import Dropdown from '@/components/Dropdown.vue';
|
|
124
|
+
import { ref, computed, onMounted, watch } from 'vue';
|
|
145
125
|
import { useCoreStore } from '@/stores/core';
|
|
146
|
-
import
|
|
147
|
-
import { IconExclamationCircleSolid
|
|
148
|
-
import {
|
|
149
|
-
import {
|
|
150
|
-
import
|
|
151
|
-
import { useRouter } from 'vue-router';
|
|
152
|
-
|
|
153
|
-
const router = useRouter();
|
|
126
|
+
import Dropdown from '@/components/Dropdown.vue';
|
|
127
|
+
import { IconExclamationCircleSolid } from '@iconify-prerendered/vue-flowbite';
|
|
128
|
+
import { initFlowbite } from 'flowbite'
|
|
129
|
+
import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbite';
|
|
130
|
+
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
154
131
|
|
|
155
132
|
const props = defineProps({
|
|
156
133
|
loading: Boolean,
|
|
157
134
|
resourceColumns: Object,
|
|
158
135
|
record: Object,
|
|
159
136
|
validating: Boolean,
|
|
160
|
-
customComponentsPerColumn: Object,
|
|
161
137
|
});
|
|
162
138
|
|
|
163
139
|
|
|
@@ -169,7 +145,6 @@ const emit = defineEmits(['update:record', 'update:isValid']);
|
|
|
169
145
|
|
|
170
146
|
const currentValues = ref({});
|
|
171
147
|
|
|
172
|
-
|
|
173
148
|
const columnError = (column) => {
|
|
174
149
|
const val = computed(() => {
|
|
175
150
|
if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
|
|
@@ -191,24 +166,14 @@ const columnError = (column) => {
|
|
|
191
166
|
return `This field must be less than ${column.maxValue}`;
|
|
192
167
|
}
|
|
193
168
|
}
|
|
194
|
-
if ( column.validation && column.validation.length ){
|
|
195
|
-
const validationArray = column.validation;
|
|
196
|
-
for (let i = 0; i < validationArray.length; i++) {
|
|
197
|
-
if (validationArray[i].regExp) {
|
|
198
|
-
const regExp = new RegExp(validationArray[i].regExp);
|
|
199
|
-
if (!regExp.test(currentValues.value[column.name])) {
|
|
200
|
-
return validationArray[i].message;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
}
|
|
206
169
|
return null;
|
|
207
170
|
});
|
|
208
171
|
return val.value;
|
|
209
172
|
};
|
|
210
173
|
|
|
211
174
|
|
|
175
|
+
|
|
176
|
+
|
|
212
177
|
const setCurrentValue = (key, value) => {
|
|
213
178
|
currentValues.value[key] = value;
|
|
214
179
|
|
|
@@ -220,31 +185,8 @@ onMounted(() => {
|
|
|
220
185
|
currentValues.value[key] = props.record[key];
|
|
221
186
|
});
|
|
222
187
|
initFlowbite();
|
|
223
|
-
|
|
224
|
-
|
|
225
188
|
});
|
|
226
189
|
|
|
227
|
-
const columnOptions = computedAsync(async () => {
|
|
228
|
-
return (await Promise.all(
|
|
229
|
-
Object.values(props.resourceColumns).map(async (column) => {
|
|
230
|
-
if (column.foreignResource) {
|
|
231
|
-
const list = await callAdminForthApi({
|
|
232
|
-
method: 'POST',
|
|
233
|
-
path: `/get_resource_foreign_data`,
|
|
234
|
-
body: {
|
|
235
|
-
resourceId: router.currentRoute.value.params.resourceId,
|
|
236
|
-
column: column.name,
|
|
237
|
-
limit: 1000,
|
|
238
|
-
offset: 0,
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
return { [column.name]: list.items };
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
)).reduce((acc, val) => Object.assign(acc, val), {})
|
|
245
|
-
|
|
246
|
-
}, {});
|
|
247
|
-
|
|
248
190
|
const coreStore = useCoreStore();
|
|
249
191
|
|
|
250
192
|
const editableColumns = computed(() => {
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
<span v-if="column.
|
|
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'">
|
|
3
|
+
<span v-if="column.type === 'boolean'">
|
|
14
4
|
<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>
|
|
15
5
|
<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>
|
|
16
6
|
</span>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
|
+
import HomeView from '../views/HomeView.vue'
|
|
2
3
|
import ResourceParent from '@/views/ResourceParent.vue'
|
|
3
4
|
import ListView from '@/views/ListView.vue'
|
|
4
5
|
import ShowView from '@/views/ShowView.vue'
|
|
@@ -11,9 +12,7 @@ const router = createRouter({
|
|
|
11
12
|
{
|
|
12
13
|
path: '/',
|
|
13
14
|
name: 'home',
|
|
14
|
-
|
|
15
|
-
/* IMPORTANT:ADMINFORTH REDIRECT */
|
|
16
|
-
|
|
15
|
+
component: HomeView
|
|
17
16
|
},
|
|
18
17
|
{
|
|
19
18
|
path: '/login',
|
|
@@ -1,6 +1,22 @@
|
|
|
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
|
+
}
|
|
4
20
|
|
|
5
21
|
export const useCoreStore = defineStore('core', () => {
|
|
6
22
|
const resourceById = ref([]);
|
|
@@ -24,40 +40,35 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
24
40
|
}, {});
|
|
25
41
|
config.value = resp.config;
|
|
26
42
|
user.value = resp.user;
|
|
27
|
-
console.log('🌍 AdminForth v', resp.version);
|
|
28
43
|
|
|
29
44
|
// find homepage:true in menu recuresively
|
|
30
|
-
|
|
45
|
+
if (import.meta.env.DEV){
|
|
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
|
+
}
|
|
31
59
|
}
|
|
32
60
|
|
|
33
61
|
async function fetchRecord({ resourceId, primaryKey }) {
|
|
34
62
|
record.value = null;
|
|
35
|
-
|
|
36
|
-
if (!resourceColumns.value) {
|
|
37
|
-
throw new Error('Columns not fetched yet');
|
|
38
|
-
}
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
path: '/
|
|
64
|
+
record.value = await callAdminForthApi({
|
|
65
|
+
path: '/get_record',
|
|
42
66
|
method: 'POST',
|
|
43
67
|
body: {
|
|
44
|
-
source: 'show',
|
|
45
68
|
resourceId: resourceId,
|
|
46
|
-
|
|
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
|
|
69
|
+
primaryKey: primaryKey,
|
|
56
70
|
}
|
|
57
71
|
});
|
|
58
|
-
|
|
59
|
-
console.log('📦 record', respData);
|
|
60
|
-
record.value = respData.data[0];
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
async function fetchColumns({ resourceId }) {
|
package/dist/spa/src/utils.ts
CHANGED
|
@@ -29,11 +29,9 @@ export async function callAdminForthApi({ path, method, body=undefined }) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return resolveComponent(name);
|
|
36
|
-
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
37
35
|
|
|
38
36
|
export function getIcon(icon: string) {
|
|
39
37
|
// icon format is "feather:icon-name". We need to get IconName in pascal case
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
@update:record="onUpdateRecord"
|
|
32
32
|
@update:isValid="isValid = $event"
|
|
33
33
|
:validating="validating"
|
|
34
|
-
:customComponentsPerColumn="createComponentsPerColumn"
|
|
35
34
|
>
|
|
36
35
|
</ResourceForm>
|
|
37
36
|
|
|
@@ -41,14 +40,14 @@
|
|
|
41
40
|
|
|
42
41
|
<script setup>
|
|
43
42
|
|
|
44
|
-
import
|
|
45
|
-
import
|
|
46
|
-
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
43
|
+
import { ref, computed, onMounted } from 'vue';
|
|
44
|
+
import { useRoute, useRouter } from 'vue-router';
|
|
47
45
|
import { useCoreStore } from '@/stores/core';
|
|
48
|
-
import { callAdminForthApi
|
|
46
|
+
import { callAdminForthApi } from '@/utils';
|
|
47
|
+
import ResourceForm from '@/components/ResourceForm.vue';
|
|
48
|
+
import BreadcrumbsWithButtons from '@/components/BreadcrumbsWithButtons.vue';
|
|
49
49
|
import { IconFloppyDiskSolid } from '@iconify-prerendered/vue-flowbite';
|
|
50
|
-
import
|
|
51
|
-
import { useRoute, useRouter } from 'vue-router';
|
|
50
|
+
import SingleSkeletLoader from '@/components/SingleSkeletLoader.vue';
|
|
52
51
|
|
|
53
52
|
const isValid = ref(false);
|
|
54
53
|
const validating = ref(false);
|
|
@@ -60,7 +59,6 @@ const route = useRoute();
|
|
|
60
59
|
const router = useRouter();
|
|
61
60
|
|
|
62
61
|
const record = ref({});
|
|
63
|
-
let createComponentsPerColumn = {};
|
|
64
62
|
|
|
65
63
|
const coreStore = useCoreStore();
|
|
66
64
|
|
|
@@ -71,15 +69,9 @@ async function onUpdateRecord(newRecord) {
|
|
|
71
69
|
|
|
72
70
|
onMounted(async () => {
|
|
73
71
|
loading.value = true;
|
|
74
|
-
|
|
72
|
+
coreStore.fetchColumns({
|
|
75
73
|
resourceId: route.params.resourceId
|
|
76
74
|
});
|
|
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
|
-
}, {});
|
|
83
75
|
loading.value = false;
|
|
84
76
|
});
|
|
85
77
|
|