adminforth 1.1.34 → 1.1.36
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 +30 -22
- package/dist/spa/index.html +2 -2
- package/dist/spa/package-lock.json +17 -4
- package/dist/spa/package.json +2 -1
- package/dist/spa/spa/src/components/ResourceListTable.vue +1 -10
- package/dist/spa/src/App.vue +125 -45
- package/dist/spa/src/assets/logo.svg +19 -1
- package/dist/spa/src/components/AcceptModal.vue +2 -9
- package/dist/spa/src/components/BreadcrumbsWithButtons.vue +1 -1
- package/dist/spa/src/components/CustomDatePicker.vue +16 -3
- package/dist/spa/src/components/CustomDateRangePicker.vue +11 -2
- package/dist/spa/src/components/Dropdown.vue +6 -1
- package/dist/spa/src/components/Filters.vue +40 -19
- package/dist/spa/src/components/ResourceForm.vue +26 -24
- package/dist/spa/src/components/ResourceListTable.vue +419 -0
- package/dist/spa/src/components/Toast.vue +66 -0
- package/dist/spa/src/components/ValueRenderer.vue +14 -8
- package/dist/spa/src/composables/useFrontendApi.ts +26 -0
- package/dist/spa/src/composables/useStores.ts +113 -0
- package/dist/spa/src/main.ts +1 -1
- package/dist/spa/src/router/index.ts +30 -20
- package/dist/spa/src/spa_types/core.ts +51 -0
- package/dist/spa/src/stores/core.ts +48 -31
- package/dist/spa/src/stores/filters.ts +22 -0
- package/dist/spa/src/stores/modal.ts +13 -3
- package/dist/spa/src/stores/toast.ts +15 -0
- package/dist/spa/src/stores/user.ts +54 -0
- package/dist/spa/src/utils.ts +62 -7
- package/dist/spa/src/views/CreateView.vue +67 -15
- package/dist/spa/src/views/EditView.vue +45 -11
- package/dist/spa/src/views/ListView.vue +82 -366
- package/dist/spa/src/views/LoginView.vue +16 -4
- package/dist/spa/src/views/ShowView.vue +105 -21
- package/dist/spa/tailwind.config.js +1 -1
- package/dist/spa/vite.config.ts +6 -0
- package/index.ts +38 -28
- package/package.json +1 -1
- package/spa/src/components/ResourceListTable.vue +1 -10
- package/types/AdminForthConfig.ts +0 -3
- package/dist/plugins/AccessControl/index.js +0 -66
- package/dist/plugins/AccessControl/types.js +0 -1
- package/dist/spa/public/favicon.ico +0 -0
- package/dist/spa/spa/public/favicon.ico +0 -0
- package/dist/types.js +0 -30
- /package/dist/spa/{spa/public → public/assets}/favicon.png +0 -0
|
@@ -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 ? '' : 'Select...'"
|
|
9
|
+
:placeholder="selectedItems.length ? '' : placeholder || 'Select...'"
|
|
10
10
|
/>
|
|
11
11
|
<div class="absolute inset-y-0 left-2 flex items-center pr-2 flex-wrap">
|
|
12
12
|
{{ }}
|
|
@@ -67,6 +67,11 @@ const props = defineProps({
|
|
|
67
67
|
type: Boolean,
|
|
68
68
|
default: false,
|
|
69
69
|
},
|
|
70
|
+
placeholder: {
|
|
71
|
+
type: String,
|
|
72
|
+
default: '',
|
|
73
|
+
|
|
74
|
+
},
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
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 right-0 z-50 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 ` }"
|
|
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="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
29
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
30
30
|
/>
|
|
31
31
|
<Dropdown
|
|
32
32
|
v-else-if="c.type === 'boolean'"
|
|
33
33
|
:options="[{ label: 'Yes', value: true }, { label: 'No', value: false }, { label: 'Unset', value: null }]"
|
|
34
34
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
35
|
-
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
35
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
36
36
|
/>
|
|
37
37
|
|
|
38
38
|
<Dropdown
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
:options="c.enum"
|
|
41
41
|
:allowCustom="c.allowCustom"
|
|
42
42
|
@update:modelValue="setFilterItem({ column: c, operator: 'in', value: $event })"
|
|
43
|
-
:modelValue="filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
43
|
+
:modelValue="filtersStore.filters.find(f => f.field === c.name && f.operator === 'in')?.value || []"
|
|
44
44
|
/>
|
|
45
45
|
|
|
46
46
|
<input
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
<CustomDateRangePicker
|
|
55
55
|
v-else-if="['datetime'].includes(c.type)"
|
|
56
56
|
:column="c"
|
|
57
|
-
:valueStart="filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
|
|
57
|
+
:valueStart="filtersStore.filters.find(f => f.field === c.name && f.operator === 'gte')?.value || undefined"
|
|
58
58
|
@update:valueStart="setFilterItem({ column: c, operator: 'gte', value: $event || undefined })"
|
|
59
|
-
:valueEnd="filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
|
|
59
|
+
:valueEnd="filtersStore.filters.find(f => f.field === c.name && f.operator === 'lte')?.value || undefined"
|
|
60
60
|
@update:valueEnd="setFilterItem({ column: c, operator: 'lte', value: $event || undefined })"
|
|
61
61
|
/>
|
|
62
62
|
|
|
@@ -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) && c.allowMinMaxQuery"
|
|
73
73
|
:min="c.min"
|
|
74
74
|
:max="c.max"
|
|
75
75
|
:valueStart="getFilterItem({ column: c, operator: 'gte' })"
|
|
@@ -77,6 +77,23 @@
|
|
|
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>
|
|
80
97
|
|
|
81
98
|
</li>
|
|
82
99
|
</ul>
|
|
@@ -84,15 +101,15 @@
|
|
|
84
101
|
|
|
85
102
|
<div class="flex justify-end gap-2">
|
|
86
103
|
<button
|
|
87
|
-
:disabled="!filters.length"
|
|
104
|
+
:disabled="!filtersStore.filters.length"
|
|
88
105
|
type="button"
|
|
89
|
-
class="flex items-center py-1 px-3
|
|
106
|
+
class="flex items-center py-1 px-3 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded border border-gray-300 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-100 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
90
107
|
@click="clear">Clear all</button>
|
|
91
108
|
|
|
92
109
|
</div>
|
|
93
110
|
</div>
|
|
94
111
|
|
|
95
|
-
<div v-if="show"
|
|
112
|
+
<div v-if="show" class="bg-gray-900/50 dark:bg-gray-900/80 fixed inset-0 z-30"
|
|
96
113
|
@click="$emit('hide')">
|
|
97
114
|
</div>
|
|
98
115
|
</template>
|
|
@@ -105,6 +122,10 @@ import { callAdminForthApi } from '@/utils';
|
|
|
105
122
|
import { useRouter } from 'vue-router';
|
|
106
123
|
import { computedAsync } from '@vueuse/core'
|
|
107
124
|
import CustomRangePicker from "@/components/CustomRangePicker.vue";
|
|
125
|
+
import { useFiltersStore } from '@/stores/filters';
|
|
126
|
+
|
|
127
|
+
const filtersStore = useFiltersStore();
|
|
128
|
+
|
|
108
129
|
|
|
109
130
|
// props: columns
|
|
110
131
|
// add support for v-model:filers
|
|
@@ -163,28 +184,28 @@ watch(() => props.show, (show) => {
|
|
|
163
184
|
|
|
164
185
|
function setFilterItem({ column, operator, value }) {
|
|
165
186
|
|
|
166
|
-
const index =
|
|
187
|
+
const index = filtersStore.filters.findIndex(f => f.field === column.name && f.operator === operator);
|
|
167
188
|
if (value === undefined) {
|
|
168
189
|
if (index !== -1) {
|
|
169
|
-
|
|
190
|
+
filtersStore.filters.splice(index, 1);
|
|
170
191
|
}
|
|
171
|
-
emits('update:filters', [...
|
|
192
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
172
193
|
return;
|
|
173
194
|
} else {
|
|
174
195
|
if (index === -1) {
|
|
175
|
-
|
|
196
|
+
filtersStore.setFilter({ field: column.name, value, operator });
|
|
176
197
|
} else {
|
|
177
|
-
|
|
198
|
+
filtersStore.setFilters([...filtersStore.filters.slice(0, index), { field: column.name, value, operator }, ...filtersStore.filters.slice(index + 1)])
|
|
178
199
|
}
|
|
179
200
|
}
|
|
180
|
-
emits('update:filters', [...
|
|
201
|
+
emits('update:filters', [...filtersStore.filters]);
|
|
181
202
|
}
|
|
182
203
|
|
|
183
204
|
function getFilterItem({ column, operator }) {
|
|
184
|
-
return
|
|
205
|
+
return filtersStore.filters.find(f => f.field === column.name && f.operator === operator)?.value || '';
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
async function clear() {
|
|
188
|
-
|
|
209
|
+
filtersStore.clearFilters();
|
|
189
210
|
}
|
|
190
211
|
</script>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div>
|
|
2
|
+
<div class="rounded-default">
|
|
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 rounded-default"
|
|
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 "> <!--align-top-->
|
|
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
|
-
|
|
48
47
|
<Dropdown
|
|
49
48
|
single
|
|
50
49
|
v-if="column.foreignResource"
|
|
51
50
|
: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
|
+
></Dropdown>
|
|
55
55
|
<Dropdown
|
|
56
56
|
single
|
|
57
57
|
v-else-if="column.enum"
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
v-else-if="['datetime'].includes(column.type)"
|
|
80
80
|
:column="column"
|
|
81
81
|
:valueStart="currentValues[column.name]"
|
|
82
|
+
auto-hide
|
|
82
83
|
@update:valueStart="setCurrentValue(column.name, $event)"
|
|
83
84
|
/>
|
|
84
85
|
<input
|
|
@@ -120,9 +121,6 @@
|
|
|
120
121
|
<IconEyeSolid class="w-6 h-6 text-gray-400" v-if="!unmasked[column.name]" />
|
|
121
122
|
<IconEyeSlashSolid class="w-6 h-6 text-gray-400" v-else />
|
|
122
123
|
</button>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
124
|
<div v-if="columnError(column) && validating" class="mt-1 text-xs text-red-500 dark:text-red-400">{{ columnError(column) }}</div>
|
|
127
125
|
|
|
128
126
|
<div v-if="column.editingNote && column.editingNote[mode]" class="mt-1 text-xs text-gray-400 dark:text-gray-500">{{ column.editingNote[mode] }}</div>
|
|
@@ -154,7 +152,7 @@ const router = useRouter();
|
|
|
154
152
|
|
|
155
153
|
const props = defineProps({
|
|
156
154
|
loading: Boolean,
|
|
157
|
-
|
|
155
|
+
resource: Object,
|
|
158
156
|
record: Object,
|
|
159
157
|
validating: Boolean,
|
|
160
158
|
customComponentsPerColumn: Object,
|
|
@@ -196,7 +194,11 @@ const columnError = (column) => {
|
|
|
196
194
|
for (let i = 0; i < validationArray.length; i++) {
|
|
197
195
|
if (validationArray[i].regExp) {
|
|
198
196
|
const regExp = new RegExp(validationArray[i].regExp);
|
|
199
|
-
|
|
197
|
+
let value = currentValues.value[column.name];
|
|
198
|
+
if (value === undefined || value === null) {
|
|
199
|
+
value = '';
|
|
200
|
+
}
|
|
201
|
+
if (!regExp.test(value)) {
|
|
200
202
|
return validationArray[i].message;
|
|
201
203
|
}
|
|
202
204
|
}
|
|
@@ -226,7 +228,7 @@ onMounted(() => {
|
|
|
226
228
|
|
|
227
229
|
const columnOptions = computedAsync(async () => {
|
|
228
230
|
return (await Promise.all(
|
|
229
|
-
Object.values(props.
|
|
231
|
+
Object.values(props.resource.columns).map(async (column) => {
|
|
230
232
|
if (column.foreignResource) {
|
|
231
233
|
const list = await callAdminForthApi({
|
|
232
234
|
method: 'POST',
|
|
@@ -249,7 +251,7 @@ const coreStore = useCoreStore();
|
|
|
249
251
|
|
|
250
252
|
const editableColumns = computed(() => {
|
|
251
253
|
const mode = props.record ? 'edit' : 'create';
|
|
252
|
-
return props.
|
|
254
|
+
return props.resource?.columns?.filter(column => column.showIn.includes(mode));
|
|
253
255
|
});
|
|
254
256
|
|
|
255
257
|
const isValid = computed(() => {
|