adminforth 1.1.21 → 1.1.23
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/plugins/AccessControl/index.js +56 -3
- 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/composables/useFrontendApi.ts +12 -0
- package/dist/spa/spa/src/views/ShowView.vue +22 -27
- 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 +0 -1
- package/package.json +1 -1
- package/spa/src/composables/useFrontendApi.ts +12 -0
- package/spa/src/views/ShowView.vue +22 -27
- package/dist/plugins/AuditLog/index.js +0 -66
- package/dist/plugins/AuditLog/types.js +0 -1
- 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
- package/plugins/AccessControl/index.ts +0 -29
- package/plugins/AccessControl/types.ts +0 -7
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<div>
|
|
3
3
|
|
|
4
4
|
<div
|
|
5
|
-
class="relative shadow-
|
|
5
|
+
class="relative shadow-resourse-form-shadow sm:rounded-lg dark:shadow-2xl dark:shadow-black"
|
|
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-form-view-heading 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,97 +18,116 @@
|
|
|
18
18
|
</thead>
|
|
19
19
|
<tbody>
|
|
20
20
|
<tr v-for="column, i in editableColumns" :key="column.name"
|
|
21
|
-
class="
|
|
21
|
+
class="bg-form-view-bg dark:bg-gray-800 border-b dark:border-gray-700"
|
|
22
22
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
<!-- if column is in customComponentsPerColumn, use this component. If not, use this code -->
|
|
24
|
+
<template v-if="customComponentsPerColumn[column.name]">
|
|
25
|
+
<component
|
|
26
|
+
:is="customComponentsPerColumn[column.name]"
|
|
27
|
+
:column="column"
|
|
28
|
+
:value="currentValues[column.name]"
|
|
29
|
+
@update:value="setCurrentValue(column.name, $event)"
|
|
28
30
|
/>
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
31
|
+
</template>
|
|
32
|
+
<template v-else>
|
|
33
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
34
|
+
{{ column.label }}
|
|
35
|
+
<span :data-tooltip-target="`tooltip-show-${i}`" class="relative inline-block">
|
|
36
|
+
<IconExclamationCircleSolid v-if="column.required[mode]" class="w-4 h-4"
|
|
37
|
+
:class="(columnError(column) && validating) ? 'text-red-500 dark:text-red-400' : 'text-gray-400 dark:text-gray-500'"
|
|
38
|
+
/>
|
|
39
|
+
</span>
|
|
40
|
+
<div :id="`tooltip-show-${i}`"
|
|
41
|
+
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">
|
|
42
|
+
Required field
|
|
43
|
+
<div class="tooltip-arrow" data-popper-arrow></div>
|
|
44
|
+
</div>
|
|
45
|
+
</td>
|
|
46
|
+
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
47
|
+
|
|
48
|
+
<Dropdown
|
|
49
|
+
single
|
|
50
|
+
v-if="column.foreignResource"
|
|
51
|
+
:options="columnOptions[column.name] || []"
|
|
52
|
+
:modelValue="currentValues[column.name]"
|
|
53
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
54
|
+
/>
|
|
55
|
+
<Dropdown
|
|
56
|
+
single
|
|
57
|
+
v-else-if="column.enum"
|
|
58
|
+
:options="column.enum"
|
|
59
|
+
:modelValue="currentValues[column.name]"
|
|
60
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
61
|
+
/>
|
|
62
|
+
<Dropdown
|
|
63
|
+
single
|
|
64
|
+
v-else-if="column.type === 'boolean'"
|
|
65
|
+
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
66
|
+
:modelValue="currentValues[column.name]"
|
|
67
|
+
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
68
|
+
/>
|
|
69
|
+
<input
|
|
70
|
+
v-else-if="['integer'].includes(column.type)"
|
|
71
|
+
type="number"
|
|
72
|
+
step="1"
|
|
73
|
+
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"
|
|
74
|
+
placeholder="0"
|
|
75
|
+
:value="currentValues[column.name]"
|
|
76
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
77
|
+
>
|
|
78
|
+
<CustomDatePicker
|
|
79
|
+
v-else-if="['datetime'].includes(column.type)"
|
|
80
|
+
:column="column"
|
|
81
|
+
:valueStart="currentValues[column.name]"
|
|
82
|
+
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
83
|
+
/>
|
|
84
|
+
<input
|
|
85
|
+
v-else-if="['decimal', 'float'].includes(column.type)"
|
|
86
|
+
type="number"
|
|
87
|
+
step="0.1"
|
|
88
|
+
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"
|
|
89
|
+
placeholder="0.0"
|
|
90
|
+
:value="currentValues[column.name]"
|
|
91
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
92
|
+
/>
|
|
93
|
+
<textarea
|
|
94
|
+
v-else-if="['text'].includes(column.type)"
|
|
95
|
+
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"
|
|
96
|
+
placeholder="Text"
|
|
97
|
+
:value="currentValues[column.name]"
|
|
98
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
99
|
+
>
|
|
100
|
+
</textarea>
|
|
101
|
+
<input
|
|
102
|
+
v-else
|
|
103
|
+
:type="!column.masked || unmasked[column.name] ? 'text' : 'password'"
|
|
104
|
+
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"
|
|
105
|
+
placeholder="Text"
|
|
106
|
+
:value="currentValues[column.name]"
|
|
107
|
+
@input="setCurrentValue(column.name, $event.target.value)"
|
|
108
|
+
autocomplete="false"
|
|
109
|
+
data-lpignore="true"
|
|
110
|
+
readonly
|
|
111
|
+
onfocus="this.removeAttribute('readonly');"
|
|
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>
|
|
112
131
|
</tr>
|
|
113
132
|
|
|
114
133
|
</tbody>
|
|
@@ -121,19 +140,24 @@
|
|
|
121
140
|
|
|
122
141
|
<script setup>
|
|
123
142
|
|
|
124
|
-
import { ref, computed, onMounted, watch } from 'vue';
|
|
125
|
-
import { useCoreStore } from '@/stores/core';
|
|
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
143
|
import CustomDatePicker from "@/components/CustomDatePicker.vue";
|
|
144
|
+
import Dropdown from '@/components/Dropdown.vue';
|
|
145
|
+
import { useCoreStore } from '@/stores/core';
|
|
146
|
+
import { callAdminForthApi } from '@/utils';
|
|
147
|
+
import { IconExclamationCircleSolid, IconEyeSlashSolid, IconEyeSolid } from '@iconify-prerendered/vue-flowbite';
|
|
148
|
+
import { computedAsync } from '@vueuse/core';
|
|
149
|
+
import { initFlowbite } from 'flowbite';
|
|
150
|
+
import { computed, onMounted, ref, watch } from 'vue';
|
|
151
|
+
import { useRouter } from 'vue-router';
|
|
152
|
+
|
|
153
|
+
const router = useRouter();
|
|
131
154
|
|
|
132
155
|
const props = defineProps({
|
|
133
156
|
loading: Boolean,
|
|
134
157
|
resourceColumns: Object,
|
|
135
158
|
record: Object,
|
|
136
159
|
validating: Boolean,
|
|
160
|
+
customComponentsPerColumn: Object,
|
|
137
161
|
});
|
|
138
162
|
|
|
139
163
|
|
|
@@ -145,6 +169,7 @@ const emit = defineEmits(['update:record', 'update:isValid']);
|
|
|
145
169
|
|
|
146
170
|
const currentValues = ref({});
|
|
147
171
|
|
|
172
|
+
|
|
148
173
|
const columnError = (column) => {
|
|
149
174
|
const val = computed(() => {
|
|
150
175
|
if ( column.required[mode.value] && (currentValues.value[column.name] === undefined || currentValues.value[column.name] === null || currentValues.value[column.name] === '') ) {
|
|
@@ -166,14 +191,24 @@ const columnError = (column) => {
|
|
|
166
191
|
return `This field must be less than ${column.maxValue}`;
|
|
167
192
|
}
|
|
168
193
|
}
|
|
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
|
+
}
|
|
169
206
|
return null;
|
|
170
207
|
});
|
|
171
208
|
return val.value;
|
|
172
209
|
};
|
|
173
210
|
|
|
174
211
|
|
|
175
|
-
|
|
176
|
-
|
|
177
212
|
const setCurrentValue = (key, value) => {
|
|
178
213
|
currentValues.value[key] = value;
|
|
179
214
|
|
|
@@ -185,8 +220,31 @@ onMounted(() => {
|
|
|
185
220
|
currentValues.value[key] = props.record[key];
|
|
186
221
|
});
|
|
187
222
|
initFlowbite();
|
|
223
|
+
|
|
224
|
+
|
|
188
225
|
});
|
|
189
226
|
|
|
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
|
+
|
|
190
248
|
const coreStore = useCoreStore();
|
|
191
249
|
|
|
192
250
|
const editableColumns = computed(() => {
|
|
@@ -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;
|