adminforth 1.1.59 → 1.1.60
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 +68 -63
- package/dist/modules/styleGenerator.js +22 -18
- package/dist/modules/styles.js +44 -36
- package/dist/modules/utils.js +74 -0
- package/dist/plugins/AccessControl/index.js +66 -0
- package/dist/plugins/AccessControl/types.js +1 -0
- package/dist/plugins/AuditLogPlugin/custom/AuditLogView.vue +20 -0
- package/dist/plugins/AuditLogPlugin/index.js +83 -0
- package/dist/plugins/AuditLogPlugin/types.js +1 -0
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +4 -17
- package/dist/spa/package.json +1 -2
- package/dist/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/package-lock.json +111 -0
- package/dist/spa/spa/package.json +1 -0
- package/dist/spa/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/src/App.vue +4 -4
- package/dist/spa/spa/src/components/MenuLink.vue +1 -1
- package/dist/spa/spa/src/components/ResourceForm.vue +1 -1
- package/dist/spa/src/App.vue +45 -125
- package/dist/spa/src/assets/logo.svg +1 -19
- package/dist/spa/src/components/AcceptModal.vue +9 -2
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +3 -16
- package/dist/spa/src/components/CustomDateRangePicker.vue +2 -11
- package/dist/spa/src/components/Dropdown.vue +1 -6
- package/dist/spa/src/components/Filters.vue +19 -40
- package/dist/spa/src/components/ResourceForm.vue +24 -26
- package/dist/spa/src/components/ValueRenderer.vue +8 -14
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/router/index.ts +20 -30
- package/dist/spa/src/stores/core.ts +31 -48
- package/dist/spa/src/stores/modal.ts +3 -13
- package/dist/spa/src/utils.ts +7 -62
- package/dist/spa/src/views/CreateView.vue +15 -67
- package/dist/spa/src/views/EditView.vue +11 -45
- package/dist/spa/src/views/ListView.vue +366 -82
- package/dist/spa/src/views/LoginView.vue +4 -16
- package/dist/spa/src/views/ShowView.vue +21 -105
- package/dist/spa/tailwind.config.js +1 -1
- package/dist/spa/vite.config.ts +0 -6
- package/dist/types.js +30 -0
- package/index.ts +62 -61
- package/modules/styleGenerator.ts +25 -18
- package/modules/styles.ts +46 -36
- package/modules/utils.ts +94 -1
- package/package.json +2 -1
- package/plugins/AuditLogPlugin/custom/AuditLogView.vue +20 -0
- package/plugins/AuditLogPlugin/index.ts +99 -0
- package/plugins/AuditLogPlugin/types.ts +40 -0
- package/spa/package-lock.json +111 -0
- package/spa/package.json +1 -0
- package/spa/src/App.vue +4 -4
- package/spa/src/components/MenuLink.vue +1 -1
- package/spa/src/components/ResourceForm.vue +1 -1
- package/types/AdminForthConfig.ts +6 -1
- package/dist/spa/src/components/ResourceListTable.vue +0 -419
- package/dist/spa/src/components/Toast.vue +0 -66
- package/dist/spa/src/composables/useFrontendApi.ts +0 -26
- package/dist/spa/src/composables/useStores.ts +0 -113
- package/dist/spa/src/spa_types/core.ts +0 -51
- package/dist/spa/src/stores/filters.ts +0 -22
- package/dist/spa/src/stores/toast.ts +0 -15
- package/dist/spa/src/stores/user.ts +0 -54
- package/plugins/AuditLog/index.ts +0 -29
- package/plugins/AuditLog/types.ts +0 -4
- /package/dist/spa/{public/assets → spa/public}/favicon.png +0 -0
|
@@ -101,9 +101,6 @@ const endDate = ref('');
|
|
|
101
101
|
const startTime = ref('');
|
|
102
102
|
const endTime = ref('');
|
|
103
103
|
|
|
104
|
-
const datepickerStartObject = ref('')
|
|
105
|
-
const datepickerEndObject = ref('')
|
|
106
|
-
|
|
107
104
|
const start = computed(() => {
|
|
108
105
|
if (!startDate.value) {
|
|
109
106
|
return;
|
|
@@ -165,9 +162,9 @@ watch(end, () => {
|
|
|
165
162
|
|
|
166
163
|
function initDatepickers() {
|
|
167
164
|
|
|
168
|
-
|
|
165
|
+
new Datepicker(datepickerStartEl.value, {format: 'dd M yyyy'});
|
|
169
166
|
|
|
170
|
-
|
|
167
|
+
new Datepicker(datepickerEndEl.value, {format: 'dd M yyyy'});
|
|
171
168
|
addChangeDateListener();
|
|
172
169
|
}
|
|
173
170
|
|
|
@@ -181,11 +178,6 @@ function removeChangeDateListener() {
|
|
|
181
178
|
datepickerEndEl.value.removeEventListener('changeDate', setEndDate);
|
|
182
179
|
}
|
|
183
180
|
|
|
184
|
-
function destroyDatepickerElement() {
|
|
185
|
-
datepickerStartObject.value.destroy();
|
|
186
|
-
datepickerEndObject.value.destroy();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
181
|
function setStartDate(event) {
|
|
190
182
|
startDate.value = event.detail.date
|
|
191
183
|
}
|
|
@@ -213,6 +205,5 @@ onMounted(() => {
|
|
|
213
205
|
|
|
214
206
|
onBeforeUnmount(() => {
|
|
215
207
|
removeChangeDateListener();
|
|
216
|
-
destroyDatepickerElement();
|
|
217
208
|
})
|
|
218
209
|
</script>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
v-model="search"
|
|
7
7
|
@focus="showDropdown = true"
|
|
8
8
|
class="block w-full pl-3 pr-10 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:shadow-outline-blue focus:border-blue-300 sm:text-sm transition duration-150 ease-in-out 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"
|
|
9
|
-
:placeholder="selectedItems.length ? '' :
|
|
9
|
+
:placeholder="selectedItems.length ? '' : 'Select...'"
|
|
10
10
|
/>
|
|
11
11
|
<div class="absolute inset-y-0 left-2 flex items-center pr-2 flex-wrap">
|
|
12
12
|
{{ }}
|
|
@@ -67,11 +67,6 @@ const props = defineProps({
|
|
|
67
67
|
type: Boolean,
|
|
68
68
|
default: false,
|
|
69
69
|
},
|
|
70
|
-
placeholder: {
|
|
71
|
-
type: String,
|
|
72
|
-
default: '',
|
|
73
|
-
|
|
74
|
-
},
|
|
75
70
|
});
|
|
76
71
|
|
|
77
72
|
const emit = defineEmits(['update:modelValue']);
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
<!-- drawer component -->
|
|
3
3
|
<div id="drawer-navigation"
|
|
4
4
|
|
|
5
|
-
class="fixed
|
|
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 dark:shadow-black"
|
|
6
6
|
|
|
7
7
|
:class="show ? 'top-0 transform-none' : ''"
|
|
8
8
|
tabindex="-1" aria-labelledby="drawer-navigation-label"
|
|
9
|
-
:style="{ height: `calc(100vh ` }"
|
|
9
|
+
:style="{ height: `calc(100vh - 3.5rem)` }"
|
|
10
10
|
>
|
|
11
11
|
<h5 id="drawer-navigation-label" class="text-base font-semibold text-gray-500 uppercase dark:text-gray-400">
|
|
12
12
|
Filters
|
|
@@ -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="
|
|
29
|
+
:modelValue="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="
|
|
35
|
+
:modelValue="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="
|
|
43
|
+
:modelValue="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="
|
|
57
|
+
:valueStart="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="
|
|
59
|
+
:valueEnd="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
|
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
>
|
|
70
70
|
|
|
71
71
|
<CustomRangePicker
|
|
72
|
-
v-else-if="['integer', 'decimal', 'float'].includes(c.type)
|
|
72
|
+
v-else-if="['integer', 'decimal', 'float'].includes(c.type)"
|
|
73
73
|
:min="c.min"
|
|
74
74
|
:max="c.max"
|
|
75
75
|
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
|
|
@@ -77,23 +77,6 @@
|
|
|
77
77
|
:valueEnd="getFilterItem({ column: c, operator: 'lte' })"
|
|
78
78
|
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
79
79
|
/>
|
|
80
|
-
|
|
81
|
-
<div v-else-if="['integer', 'decimal', 'float'].includes(c.type)" class="flex gap-2">
|
|
82
|
-
<input
|
|
83
|
-
type="number" aria-describedby="helper-text-explanation"
|
|
84
|
-
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"
|
|
85
|
-
placeholder="From"
|
|
86
|
-
@input="setFilterItem({ column: c, operator: 'gte', value: $event.target.value || undefined })"
|
|
87
|
-
:value="getFilterItem({ column: c, operator: 'gte' })"
|
|
88
|
-
>
|
|
89
|
-
<input
|
|
90
|
-
type="number" aria-describedby="helper-text-explanation"
|
|
91
|
-
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"
|
|
92
|
-
placeholder="To"
|
|
93
|
-
@input="setFilterItem({ column: c, operator: 'lte', value: $event.target.value || undefined})"
|
|
94
|
-
:value="getFilterItem({ column: c, operator: 'lte' })"
|
|
95
|
-
>
|
|
96
|
-
</div>
|
|
97
80
|
|
|
98
81
|
</li>
|
|
99
82
|
</ul>
|
|
@@ -101,15 +84,15 @@
|
|
|
101
84
|
|
|
102
85
|
<div class="flex justify-end gap-2">
|
|
103
86
|
<button
|
|
104
|
-
:disabled="!
|
|
87
|
+
:disabled="!filters.length"
|
|
105
88
|
type="button"
|
|
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"
|
|
89
|
+
class="flex items-center py-1 px-3 mb-2 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
107
90
|
@click="clear">Clear all</button>
|
|
108
91
|
|
|
109
92
|
</div>
|
|
110
93
|
</div>
|
|
111
94
|
|
|
112
|
-
<div v-if="show"
|
|
95
|
+
<div v-if="show" drawer-backdrop="" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-50"
|
|
113
96
|
@click="$emit('hide')">
|
|
114
97
|
</div>
|
|
115
98
|
</template>
|
|
@@ -122,10 +105,6 @@ import { callAdminForthApi } from '@/utils';
|
|
|
122
105
|
import { useRouter } from 'vue-router';
|
|
123
106
|
import { computedAsync } from '@vueuse/core'
|
|
124
107
|
import CustomRangePicker from "@/components/CustomRangePicker.vue";
|
|
125
|
-
import { useFiltersStore } from '@/stores/filters';
|
|
126
|
-
|
|
127
|
-
const filtersStore = useFiltersStore();
|
|
128
|
-
|
|
129
108
|
|
|
130
109
|
// props: columns
|
|
131
110
|
// add support for v-model:filers
|
|
@@ -184,28 +163,28 @@ watch(() => props.show, (show) => {
|
|
|
184
163
|
|
|
185
164
|
function setFilterItem({ column, operator, value }) {
|
|
186
165
|
|
|
187
|
-
const index =
|
|
166
|
+
const index = props.filters.findIndex(f => f.field === column.name && f.operator === operator);
|
|
188
167
|
if (value === undefined) {
|
|
189
168
|
if (index !== -1) {
|
|
190
|
-
|
|
169
|
+
props.filters.splice(index, 1);
|
|
191
170
|
}
|
|
192
|
-
emits('update:filters', [...
|
|
171
|
+
emits('update:filters', [...props.filters]);
|
|
193
172
|
return;
|
|
194
173
|
} else {
|
|
195
174
|
if (index === -1) {
|
|
196
|
-
|
|
175
|
+
props.filters.push({ field: column.name, value, operator });
|
|
197
176
|
} else {
|
|
198
|
-
|
|
177
|
+
props.filters[index].value = value;
|
|
199
178
|
}
|
|
200
179
|
}
|
|
201
|
-
emits('update:filters', [...
|
|
180
|
+
emits('update:filters', [...props.filters]);
|
|
202
181
|
}
|
|
203
182
|
|
|
204
183
|
function getFilterItem({ column, operator }) {
|
|
205
|
-
return
|
|
184
|
+
return props.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
|
|
206
185
|
}
|
|
207
186
|
|
|
208
187
|
async function clear() {
|
|
209
|
-
|
|
188
|
+
emits('update:filters', []);
|
|
210
189
|
}
|
|
211
190
|
</script>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div>
|
|
3
3
|
|
|
4
4
|
<div
|
|
5
|
-
class="relative shadow-resourse-form-shadow sm:rounded-lg dark:shadow-2xl dark:shadow-black
|
|
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
|
-
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400
|
|
8
|
+
<table class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400">
|
|
9
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">
|
|
@@ -30,28 +30,28 @@
|
|
|
30
30
|
/>
|
|
31
31
|
</template>
|
|
32
32
|
<template v-else>
|
|
33
|
-
<td class="px-6 py-4 whitespace-nowrap
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
45
|
</td>
|
|
46
46
|
<td class="px-6 py-4 whitespace-nowrap whitespace-pre-wrap relative">
|
|
47
|
+
|
|
47
48
|
<Dropdown
|
|
48
49
|
single
|
|
49
50
|
v-if="column.foreignResource"
|
|
50
51
|
:options="columnOptions[column.name] || []"
|
|
51
|
-
:placeholder = "columnOptions[column.name]?.length || 'There are no options available'"
|
|
52
52
|
:modelValue="currentValues[column.name]"
|
|
53
53
|
@update:modelValue="setCurrentValue(column.name, $event)"
|
|
54
|
-
|
|
54
|
+
/>
|
|
55
55
|
<Dropdown
|
|
56
56
|
single
|
|
57
57
|
v-else-if="column.enum"
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
v-else-if="['datetime'].includes(column.type)"
|
|
80
80
|
:column="column"
|
|
81
81
|
:valueStart="currentValues[column.name]"
|
|
82
|
-
auto-hide
|
|
83
82
|
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
84
83
|
/>
|
|
85
84
|
<input
|
|
@@ -121,6 +120,9 @@
|
|
|
121
120
|
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
122
121
|
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
123
122
|
</button>
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
124
126
|
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
125
127
|
|
|
126
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>
|
|
@@ -152,7 +154,7 @@ const router = useRouter();
|
|
|
152
154
|
|
|
153
155
|
const props = defineProps({
|
|
154
156
|
loading: Boolean,
|
|
155
|
-
|
|
157
|
+
resourceColumns: Object,
|
|
156
158
|
record: Object,
|
|
157
159
|
validating: Boolean,
|
|
158
160
|
customComponentsPerColumn: Object,
|
|
@@ -194,11 +196,7 @@ const columnError = (column) => {
|
|
|
194
196
|
for (let i = 0; i < validationArray.length; i++) {
|
|
195
197
|
if (validationArray[i].regExp) {
|
|
196
198
|
const regExp = new RegExp(validationArray[i].regExp);
|
|
197
|
-
|
|
198
|
-
if (value === undefined || value === null) {
|
|
199
|
-
value = '';
|
|
200
|
-
}
|
|
201
|
-
if (!regExp.test(value)) {
|
|
199
|
+
if (!regExp.test(currentValues.value[column.name])) {
|
|
202
200
|
return validationArray[i].message;
|
|
203
201
|
}
|
|
204
202
|
}
|
|
@@ -228,7 +226,7 @@ onMounted(() => {
|
|
|
228
226
|
|
|
229
227
|
const columnOptions = computedAsync(async () => {
|
|
230
228
|
return (await Promise.all(
|
|
231
|
-
Object.values(props.
|
|
229
|
+
Object.values(props.resourceColumns).map(async (column) => {
|
|
232
230
|
if (column.foreignResource) {
|
|
233
231
|
const list = await callAdminForthApi({
|
|
234
232
|
method: 'POST',
|
|
@@ -251,7 +249,7 @@ const coreStore = useCoreStore();
|
|
|
251
249
|
|
|
252
250
|
const editableColumns = computed(() => {
|
|
253
251
|
const mode = props.record ? 'edit' : 'create';
|
|
254
|
-
return props.
|
|
252
|
+
return props.resourceColumns?.filter(column => column.showIn.includes(mode));
|
|
255
253
|
});
|
|
256
254
|
|
|
257
255
|
const isValid = computed(() => {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div>
|
|
3
|
-
|
|
4
3
|
<span v-if="column.foreignResource">
|
|
5
|
-
<RouterLink v-if="
|
|
6
|
-
:to="{ name: 'resource-show', params: { resourceId: column.foreignResource.resourceId, primaryKey:
|
|
7
|
-
{{
|
|
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 }}
|
|
8
7
|
</RouterLink>
|
|
9
8
|
<div v-else>
|
|
10
9
|
<span class="text-gray-400">-</span>
|
|
@@ -12,18 +11,18 @@
|
|
|
12
11
|
</span>
|
|
13
12
|
|
|
14
13
|
<span v-else-if="column.type === 'boolean'">
|
|
15
|
-
<span v-if="
|
|
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>
|
|
16
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>
|
|
17
16
|
</span>
|
|
18
17
|
<span v-else-if="column.enum">
|
|
19
|
-
{{
|
|
18
|
+
{{ column.enum.find(e => e.value === row[column.name])?.label }}
|
|
20
19
|
</span>
|
|
21
20
|
<span v-else-if="column.type === 'datetime'">
|
|
22
|
-
{{
|
|
21
|
+
{{ formatDate(row[column.name]) }}
|
|
23
22
|
|
|
24
23
|
</span>
|
|
25
24
|
<span v-else>
|
|
26
|
-
{{
|
|
25
|
+
{{ row[column.name] }}
|
|
27
26
|
</span>
|
|
28
27
|
</div>
|
|
29
28
|
</template>
|
|
@@ -34,22 +33,17 @@
|
|
|
34
33
|
import dayjs from 'dayjs';
|
|
35
34
|
import utc from 'dayjs/plugin/utc';
|
|
36
35
|
import timezone from 'dayjs/plugin/timezone';
|
|
37
|
-
import {checkEmptyValues} from '@/utils';
|
|
38
|
-
import { useRoute, useRouter } from 'vue-router';
|
|
39
|
-
|
|
40
36
|
|
|
41
37
|
import { useCoreStore } from '@/stores/core';
|
|
42
38
|
|
|
43
39
|
const coreStore = useCoreStore();
|
|
44
|
-
const route = useRoute();
|
|
45
|
-
|
|
46
40
|
|
|
47
41
|
dayjs.extend(utc);
|
|
48
42
|
dayjs.extend(timezone);
|
|
49
43
|
|
|
50
44
|
const props = defineProps({
|
|
51
45
|
column: Object,
|
|
52
|
-
|
|
46
|
+
row: Object
|
|
53
47
|
});
|
|
54
48
|
|
|
55
49
|
|
package/dist/spa/src/main.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { createRouter, createWebHistory } from 'vue-router'
|
|
2
2
|
import ResourceParent from '@/views/ResourceParent.vue'
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import ListView from '@/views/ListView.vue'
|
|
4
|
+
import ShowView from '@/views/ShowView.vue'
|
|
5
|
+
import EditView from '@/views/EditView.vue'
|
|
6
|
+
import CreateView from '@/views/CreateView.vue'
|
|
5
7
|
|
|
6
8
|
const router = createRouter({
|
|
7
9
|
history: createWebHistory(import.meta.env.BASE_URL),
|
|
8
10
|
routes: [
|
|
11
|
+
{
|
|
12
|
+
path: '/',
|
|
13
|
+
name: 'home',
|
|
14
|
+
//redirect to login
|
|
15
|
+
/* IMPORTANT:ADMINFORTH REDIRECT */
|
|
16
|
+
|
|
17
|
+
},
|
|
9
18
|
{
|
|
10
19
|
path: '/login',
|
|
11
20
|
name: 'login',
|
|
12
|
-
component: () => import('@/views/LoginView.vue')
|
|
13
|
-
meta: { title: 'login' },
|
|
14
|
-
beforeEnter: async (to, from, next) => {
|
|
15
|
-
const userStore = useUserStore()
|
|
16
|
-
if(localStorage.getItem('isAuthorized') === 'true'){
|
|
17
|
-
next({name: 'home'})
|
|
18
|
-
} else {
|
|
19
|
-
next()
|
|
20
|
-
}
|
|
21
|
-
}
|
|
21
|
+
component: () => import('@/views/LoginView.vue')
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
path: '/resource/:resourceId',
|
|
@@ -27,29 +27,23 @@ const router = createRouter({
|
|
|
27
27
|
children: [
|
|
28
28
|
{
|
|
29
29
|
path: '',
|
|
30
|
-
component:
|
|
31
|
-
name: 'resource-list'
|
|
32
|
-
meta: { title: 'list',type: 'list' }
|
|
30
|
+
component: ListView,
|
|
31
|
+
name: 'resource-list'
|
|
33
32
|
},
|
|
34
33
|
{
|
|
35
34
|
path: 'show/:primaryKey',
|
|
36
|
-
component:
|
|
37
|
-
name: 'resource-show'
|
|
38
|
-
meta: { title: 'show', type: 'show'}
|
|
39
|
-
|
|
35
|
+
component: ShowView,
|
|
36
|
+
name: 'resource-show'
|
|
40
37
|
},
|
|
41
38
|
{
|
|
42
39
|
path: 'edit/:primaryKey',
|
|
43
|
-
component:
|
|
44
|
-
name: 'resource-edit'
|
|
45
|
-
meta: { title: 'edit', type: 'edit'}
|
|
40
|
+
component: EditView,
|
|
41
|
+
name: 'resource-edit'
|
|
46
42
|
},
|
|
47
43
|
{
|
|
48
44
|
path: 'create',
|
|
49
|
-
component:
|
|
50
|
-
name: 'resource-create'
|
|
51
|
-
meta: { title: 'create', type: 'create'}
|
|
52
|
-
|
|
45
|
+
component: CreateView,
|
|
46
|
+
name: 'resource-create'
|
|
53
47
|
},
|
|
54
48
|
]
|
|
55
49
|
},
|
|
@@ -57,8 +51,4 @@ const router = createRouter({
|
|
|
57
51
|
]
|
|
58
52
|
})
|
|
59
53
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
54
|
export default router
|
|
@@ -1,50 +1,39 @@
|
|
|
1
1
|
import { ref, computed } from 'vue'
|
|
2
2
|
import { defineStore } from 'pinia'
|
|
3
3
|
import { callAdminForthApi } from '@/utils';
|
|
4
|
-
import type { AdminForthResource, AdminForthResourceColumn } from '@/types/AdminForthConfig';
|
|
5
|
-
import type { Ref } from 'vue'
|
|
6
4
|
|
|
7
5
|
export const useCoreStore = defineStore('core', () => {
|
|
8
|
-
const resourceById
|
|
6
|
+
const resourceById = ref([]);
|
|
9
7
|
const menu = ref([]);
|
|
10
8
|
const config = ref({});
|
|
11
|
-
const record
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const resourceColumnsWithFilters = computed(() => {
|
|
15
|
-
if (!resource.value) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
return resource.value.columns.filter((col: AdminForthResourceColumn) => col.showIn?.includes('filter'));
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const resourceOptions = ref(null);
|
|
9
|
+
const record = ref({});
|
|
10
|
+
const resourceColumns = ref(null);
|
|
22
11
|
const resourceColumnsError = ref('');
|
|
23
12
|
const resourceColumnsId = ref(null);
|
|
24
|
-
const
|
|
13
|
+
const user = ref(null);
|
|
25
14
|
|
|
26
15
|
async function fetchMenuAndResource() {
|
|
27
16
|
const resp = await callAdminForthApi({
|
|
28
17
|
path: '/get_base_config',
|
|
29
18
|
method: 'GET',
|
|
30
19
|
});
|
|
31
|
-
if(!resp){
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
20
|
menu.value = resp.menu;
|
|
35
|
-
resourceById.value = resp.resources.reduce((acc
|
|
21
|
+
resourceById.value = resp.resources.reduce((acc, resource) => {
|
|
36
22
|
acc[resource.resourceId] = resource;
|
|
37
23
|
return acc;
|
|
38
24
|
}, {});
|
|
39
25
|
config.value = resp.config;
|
|
40
|
-
|
|
26
|
+
user.value = resp.user;
|
|
41
27
|
console.log('🌍 AdminForth v', resp.version);
|
|
28
|
+
|
|
29
|
+
// find homepage:true in menu recuresively
|
|
30
|
+
|
|
42
31
|
}
|
|
43
32
|
|
|
44
33
|
async function fetchRecord({ resourceId, primaryKey }) {
|
|
45
34
|
record.value = null;
|
|
46
35
|
|
|
47
|
-
if (!
|
|
36
|
+
if (!resourceColumns.value) {
|
|
48
37
|
throw new Error('Columns not fetched yet');
|
|
49
38
|
}
|
|
50
39
|
|
|
@@ -56,7 +45,7 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
56
45
|
resourceId: resourceId,
|
|
57
46
|
filters: [
|
|
58
47
|
{
|
|
59
|
-
field:
|
|
48
|
+
field: resourceColumns.value.find((col) => col.primaryKey).name,
|
|
60
49
|
operator: 'eq',
|
|
61
50
|
value: primaryKey
|
|
62
51
|
}
|
|
@@ -67,39 +56,30 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
67
56
|
}
|
|
68
57
|
});
|
|
69
58
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
message: respData.error,
|
|
73
|
-
variant: 'danger',
|
|
74
|
-
timeout: 'unlimited'
|
|
75
|
-
});
|
|
76
|
-
record.value = {};
|
|
77
|
-
} else {
|
|
78
|
-
record.value = respData.data[0];
|
|
79
|
-
}
|
|
80
|
-
|
|
59
|
+
console.log('📦 record', respData);
|
|
60
|
+
record.value = respData.data[0];
|
|
81
61
|
}
|
|
82
62
|
|
|
83
|
-
async function
|
|
84
|
-
if (resourceColumnsId.value === resourceId &&
|
|
63
|
+
async function fetchColumns({ resourceId }) {
|
|
64
|
+
if (resourceColumnsId.value === resourceId && resourceColumns.value) {
|
|
85
65
|
// already fetched
|
|
86
66
|
return;
|
|
87
67
|
}
|
|
88
68
|
resourceColumnsId.value = resourceId;
|
|
69
|
+
resourceColumns.value = null;
|
|
89
70
|
resourceColumnsError.value = '';
|
|
90
71
|
const res = await callAdminForthApi({
|
|
91
|
-
path: '/
|
|
72
|
+
path: '/get_resource_columns',
|
|
92
73
|
method: 'POST',
|
|
93
74
|
body: {
|
|
94
75
|
resourceId,
|
|
95
76
|
}
|
|
96
|
-
}
|
|
77
|
+
}
|
|
78
|
+
);
|
|
97
79
|
if (res.error) {
|
|
98
80
|
resourceColumnsError.value = res.error;
|
|
99
81
|
} else {
|
|
100
|
-
|
|
101
|
-
resource.value = res.resource;
|
|
102
|
-
resourceOptions.value = res.resource.options;
|
|
82
|
+
resourceColumns.value = res.resource.columns;
|
|
103
83
|
}
|
|
104
84
|
}
|
|
105
85
|
|
|
@@ -111,16 +91,21 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
111
91
|
config.value = {...config.value, ...res};
|
|
112
92
|
}
|
|
113
93
|
|
|
114
|
-
|
|
94
|
+
async function logout() {
|
|
95
|
+
await callAdminForthApi({
|
|
96
|
+
path: '/logout',
|
|
97
|
+
method: 'POST',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
115
100
|
|
|
116
101
|
const username = computed(() => {
|
|
117
102
|
const usernameField = config.value.usernameField;
|
|
118
|
-
return
|
|
103
|
+
return user.value && user.value[usernameField];
|
|
119
104
|
});
|
|
120
105
|
|
|
121
106
|
const userFullname = computed(() => {
|
|
122
107
|
const userFullnameField = config.value.userFullnameField;
|
|
123
|
-
return
|
|
108
|
+
return user.value && user.value[userFullnameField];
|
|
124
109
|
})
|
|
125
110
|
|
|
126
111
|
|
|
@@ -134,11 +119,9 @@ export const useCoreStore = defineStore('core', () => {
|
|
|
134
119
|
fetchMenuAndResource,
|
|
135
120
|
fetchRecord,
|
|
136
121
|
record,
|
|
137
|
-
|
|
122
|
+
resourceColumns,
|
|
123
|
+
fetchColumns,
|
|
138
124
|
resourceColumnsError,
|
|
139
|
-
|
|
140
|
-
resource,
|
|
141
|
-
adminUser,
|
|
142
|
-
resourceColumnsWithFilters
|
|
125
|
+
logout
|
|
143
126
|
}
|
|
144
127
|
})
|