adminforth 2.12.11 → 2.13.0-next.10
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/dataConnectors/baseConnector.js +1 -1
- package/dist/dataConnectors/baseConnector.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/configValidator.d.ts.map +1 -1
- package/dist/modules/configValidator.js +12 -5
- package/dist/modules/configValidator.js.map +1 -1
- package/dist/modules/restApi.d.ts.map +1 -1
- package/dist/modules/restApi.js +30 -3
- package/dist/modules/restApi.js.map +1 -1
- package/dist/spa/package-lock.json +1406 -749
- package/dist/spa/package.json +32 -32
- package/dist/spa/src/App.vue +87 -14
- package/dist/spa/src/adminforth.ts +2 -2
- package/dist/spa/src/afcl/AreaChart.vue +0 -1
- package/dist/spa/src/afcl/Dropzone.vue +138 -41
- package/dist/spa/src/afcl/Input.vue +5 -9
- package/dist/spa/src/afcl/Table.vue +114 -15
- package/dist/spa/src/afcl/Textarea.vue +23 -19
- package/dist/spa/src/afcl/VerticalTabs.vue +5 -0
- package/dist/spa/src/components/Filters.vue +2 -2
- package/dist/spa/src/components/GroupsTable.vue +1 -1
- package/dist/spa/src/components/MenuLink.vue +11 -6
- package/dist/spa/src/components/ResourceForm.vue +5 -0
- package/dist/spa/src/components/ResourceListTable.vue +12 -16
- package/dist/spa/src/components/ResourceListTableVirtual.vue +10 -13
- package/dist/spa/src/components/Sidebar.vue +10 -8
- package/dist/spa/src/components/Toast.vue +1 -1
- package/dist/spa/src/components/UserMenuSettingsButton.vue +2 -2
- package/dist/spa/src/components/ValueRenderer.vue +1 -1
- package/dist/spa/src/stores/core.ts +9 -0
- package/dist/spa/src/types/Back.ts +7 -1
- package/dist/spa/src/types/Common.ts +19 -1
- package/dist/spa/src/types/FrontendAPI.ts +1 -18
- package/dist/spa/src/types/adapters/StorageAdapter.ts +4 -2
- package/dist/spa/src/utils.ts +7 -3
- package/dist/spa/src/views/CreateView.vue +25 -1
- package/dist/spa/src/views/EditView.vue +26 -1
- package/dist/spa/src/views/SettingsView.vue +4 -4
- package/dist/types/Back.d.ts +6 -0
- package/dist/types/Back.d.ts.map +1 -1
- package/dist/types/Back.js.map +1 -1
- package/dist/types/Common.d.ts +18 -1
- package/dist/types/Common.d.ts.map +1 -1
- package/dist/types/Common.js.map +1 -1
- package/dist/types/FrontendAPI.d.ts +1 -15
- package/dist/types/FrontendAPI.d.ts.map +1 -1
- package/dist/types/FrontendAPI.js.map +1 -1
- package/dist/types/adapters/StorageAdapter.d.ts +2 -0
- package/dist/types/adapters/StorageAdapter.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/spa/package.json
CHANGED
|
@@ -13,47 +13,47 @@
|
|
|
13
13
|
"i18n:extract": "echo {} > i18n-empty.json && vue-i18n-extract report --vueFiles \"./src/**/*.{js,vue,ts}\" --output ./i18n-messages.json --languageFiles \"i18n-empty.json\" --add"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@iconify-prerendered/vue-flag": "^0.28.
|
|
16
|
+
"@iconify-prerendered/vue-flag": "^0.28.1754899047",
|
|
17
17
|
"@iconify-prerendered/vue-flowbite": "^0.28.1754899090",
|
|
18
|
-
"@unhead/vue": "^1.
|
|
19
|
-
"@vueuse/core": "^10.
|
|
18
|
+
"@unhead/vue": "^1.11.20",
|
|
19
|
+
"@vueuse/core": "^10.11.1",
|
|
20
20
|
"apexcharts": "^4.7.0",
|
|
21
|
-
"dayjs": "^1.11.
|
|
22
|
-
"debounce": "^2.
|
|
23
|
-
"flowbite-datepicker": "^1.2
|
|
24
|
-
"javascript-time-ago": "^2.5.
|
|
25
|
-
"pinia": "^2.1
|
|
26
|
-
"sanitize-html": "^2.
|
|
27
|
-
"unhead": "^1.
|
|
21
|
+
"dayjs": "^1.11.19",
|
|
22
|
+
"debounce": "^2.2.0",
|
|
23
|
+
"flowbite-datepicker": "^1.3.2",
|
|
24
|
+
"javascript-time-ago": "^2.5.12",
|
|
25
|
+
"pinia": "^2.3.1",
|
|
26
|
+
"sanitize-html": "^2.17.0",
|
|
27
|
+
"unhead": "^1.11.20",
|
|
28
28
|
"uuid": "^10.0.0",
|
|
29
|
-
"vue": "^3.5.
|
|
29
|
+
"vue": "^3.5.22",
|
|
30
30
|
"vue-diff": "^1.2.4",
|
|
31
|
-
"vue-i18n": "^10.0.
|
|
32
|
-
"vue-router": "^4.3
|
|
31
|
+
"vue-i18n": "^10.0.8",
|
|
32
|
+
"vue-router": "^4.6.3",
|
|
33
33
|
"vue-slider-component": "^4.1.0-beta.7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@rushstack/eslint-patch": "^1.
|
|
37
|
-
"@tsconfig/node20": "^20.1.
|
|
38
|
-
"@types/node": "^20.
|
|
39
|
-
"@vitejs/plugin-vue": "^
|
|
36
|
+
"@rushstack/eslint-patch": "^1.14.1",
|
|
37
|
+
"@tsconfig/node20": "^20.1.6",
|
|
38
|
+
"@types/node": "^20.19.24",
|
|
39
|
+
"@vitejs/plugin-vue": "^6.0.2",
|
|
40
40
|
"@vue/eslint-config-typescript": "^13.0.0",
|
|
41
|
-
"@vue/tsconfig": "^0.
|
|
42
|
-
"autoprefixer": "^10.4.
|
|
43
|
-
"eslint": "^8.57.
|
|
44
|
-
"eslint-plugin-vue": "^9.
|
|
45
|
-
"flag-icons": "^7.
|
|
41
|
+
"@vue/tsconfig": "^0.8.1",
|
|
42
|
+
"autoprefixer": "^10.4.21",
|
|
43
|
+
"eslint": "^8.57.1",
|
|
44
|
+
"eslint-plugin-vue": "^9.33.0",
|
|
45
|
+
"flag-icons": "^7.5.0",
|
|
46
46
|
"flowbite": "^3.1.2",
|
|
47
|
-
"i18n-iso-countries": "^7.
|
|
48
|
-
"npm-run-all2": "^6.
|
|
49
|
-
"portfinder": "^1.0.
|
|
50
|
-
"postcss": "^8.
|
|
51
|
-
"sass": "^1.
|
|
52
|
-
"tailwindcss": "^3.4.
|
|
53
|
-
"typescript": "~5.
|
|
54
|
-
"vite": "^
|
|
47
|
+
"i18n-iso-countries": "^7.14.0",
|
|
48
|
+
"npm-run-all2": "^6.2.6",
|
|
49
|
+
"portfinder": "^1.0.38",
|
|
50
|
+
"postcss": "^8.5.6",
|
|
51
|
+
"sass": "^1.93.3",
|
|
52
|
+
"tailwindcss": "^3.4.18",
|
|
53
|
+
"typescript": "~5.9.3",
|
|
54
|
+
"vite": "^7.2.4",
|
|
55
55
|
"vue-i18n-extract": "^2.0.7",
|
|
56
|
-
"vue-tsc": "^2.
|
|
57
|
-
"vue3-json-viewer": "^2.
|
|
56
|
+
"vue-tsc": "^2.2.12",
|
|
57
|
+
"vue3-json-viewer": "^2.4.1"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/dist/spa/src/App.vue
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
54
|
<ul class="py-1" role="none">
|
|
55
|
-
<li v-for="c in
|
|
55
|
+
<li v-for="c in userMenuComponents" class="bg-lightUserMenuItemBackground hover:bg-lightUserMenuItemBackgroundHover text-lightUserMenuItemText hover:text-lightUserMenuItemText dark:bg-darkUserMenuItemBackground dark:hover:bg-darkUserMenuItemBackgroundHover dark:text-darkUserMenuItemText dark:hover:darkUserMenuItemTextHover" >
|
|
56
56
|
<component
|
|
57
57
|
:is="getCustomComponent(c)"
|
|
58
58
|
:meta="c.meta"
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
</nav>
|
|
74
74
|
|
|
75
75
|
<Sidebar
|
|
76
|
-
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout"
|
|
76
|
+
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout && !headerOnlyLayout"
|
|
77
77
|
:sideBarOpen="sideBarOpen"
|
|
78
78
|
:forceIconOnly="route.meta?.sidebarAndHeader === 'preferIconOnly'"
|
|
79
79
|
@hideSidebar="hideSidebar"
|
|
@@ -81,12 +81,10 @@
|
|
|
81
81
|
@sidebarStateChange="handleSidebarStateChange"
|
|
82
82
|
/>
|
|
83
83
|
|
|
84
|
-
<div class="transition-all duration-300 ease-in-out max-w-[100vw]"
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
'sm:max-w-[calc(100%-4.5rem)]': isSidebarIconOnly,
|
|
89
|
-
'sm:max-w-[calc(100%-16rem)]': !isSidebarIconOnly
|
|
84
|
+
<div class="af-content-wrapper transition-all duration-300 ease-in-out max-w-[100vw]"
|
|
85
|
+
:style="{
|
|
86
|
+
marginLeft: headerOnlyLayout ? 0 : isSidebarIconOnly ? '4.5rem' : expandedWidth,
|
|
87
|
+
maxWidth: headerOnlyLayout ? '100%' : isSidebarIconOnly ? 'calc(100% - 4.5rem)' : `calc(100% - ${expandedWidth})`
|
|
90
88
|
}"
|
|
91
89
|
v-if="loggedIn && routerIsReady && loginRedirectCheckIsReady && defaultLayout">
|
|
92
90
|
<div class="p-0 dark:border-gray-700 mt-14">
|
|
@@ -147,12 +145,10 @@
|
|
|
147
145
|
@apply opacity-100;
|
|
148
146
|
}
|
|
149
147
|
|
|
150
|
-
@media (
|
|
151
|
-
.
|
|
152
|
-
margin-left:
|
|
153
|
-
|
|
154
|
-
.sm\:max-w-\[calc\(100\%-4\.5rem\)\] {
|
|
155
|
-
max-width: calc(100% - 4.5rem);
|
|
148
|
+
@media (max-width: 640px) {
|
|
149
|
+
.af-content-wrapper {
|
|
150
|
+
margin-left: 0 !important;
|
|
151
|
+
max-width: 100% !important;
|
|
156
152
|
}
|
|
157
153
|
}
|
|
158
154
|
|
|
@@ -187,6 +183,7 @@ initFrontedAPI()
|
|
|
187
183
|
createHead()
|
|
188
184
|
const sideBarOpen = ref(false);
|
|
189
185
|
const defaultLayout = ref(true);
|
|
186
|
+
const headerOnlyLayout = ref(false);
|
|
190
187
|
const route = useRoute();
|
|
191
188
|
const router = useRouter();
|
|
192
189
|
const publicConfigLoaded = ref(false);
|
|
@@ -200,8 +197,82 @@ const isSidebarIconOnly = ref(localStorage.getItem('afIconOnlySidebar') === 'tru
|
|
|
200
197
|
|
|
201
198
|
const loggedIn = computed(() => !!coreStore?.adminUser);
|
|
202
199
|
|
|
200
|
+
const expandedWidth = computed(() => coreStore.config?.iconOnlySidebar?.expandedSidebarWidth || '16.5rem');
|
|
201
|
+
|
|
203
202
|
const theme = ref('light');
|
|
204
203
|
|
|
204
|
+
const userMenuComponents = computed(() => {
|
|
205
|
+
console.log('🪲🆕 userMenuComponents recomputed', JSON.parse(JSON.stringify(coreStore?.config?.globalInjections?.userMenu)));
|
|
206
|
+
return coreStore?.config?.globalInjections?.userMenu || [];
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
watch(
|
|
210
|
+
() => coreStore.config?.globalInjections?.userMenu,
|
|
211
|
+
(newVal, oldVal) => {
|
|
212
|
+
// Only log when it becomes undefined (you can relax this if needed)
|
|
213
|
+
if (newVal === undefined) {
|
|
214
|
+
const err = new Error('🔍 userMenu changed to undefined');
|
|
215
|
+
console.groupCollapsed(
|
|
216
|
+
'%c[TRACE] userMenu changed to undefined',
|
|
217
|
+
'color: red; font-weight: bold;'
|
|
218
|
+
);
|
|
219
|
+
console.log('old value:', oldVal);
|
|
220
|
+
console.log('new value:', newVal);
|
|
221
|
+
console.log('coreStore.config.globalInjections:', coreStore.config?.globalInjections);
|
|
222
|
+
console.log('Stack trace:');
|
|
223
|
+
console.log(err.stack);
|
|
224
|
+
console.groupEnd();
|
|
225
|
+
} else {
|
|
226
|
+
// Optional: log ALL changes for debugging
|
|
227
|
+
console.groupCollapsed(
|
|
228
|
+
'%c[DEBUG] userMenu changed',
|
|
229
|
+
'color: orange; font-weight: bold;'
|
|
230
|
+
);
|
|
231
|
+
console.log('old value:', oldVal);
|
|
232
|
+
console.log('new value:', newVal);
|
|
233
|
+
console.log('coreStore.config.globalInjections:', coreStore.config?.globalInjections);
|
|
234
|
+
console.groupEnd();
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
deep: false,
|
|
239
|
+
immediate: false,
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
watch(() => coreStore.config?.globalInjections, (v) => {
|
|
244
|
+
console.log("🔧 globalInjections replaced:", v);
|
|
245
|
+
}, { deep: false });
|
|
246
|
+
|
|
247
|
+
watch(
|
|
248
|
+
() => coreStore.config?.globalInjections?.userMenu,
|
|
249
|
+
(newVal, oldVal) => {
|
|
250
|
+
if (newVal === undefined) {
|
|
251
|
+
const err = new Error('🔍 userMenu changed to undefined');
|
|
252
|
+
console.groupCollapsed(
|
|
253
|
+
'%c[TRACE] userMenu changed to undefined',
|
|
254
|
+
'color: red; font-weight: bold;'
|
|
255
|
+
);
|
|
256
|
+
console.log('old value:', oldVal);
|
|
257
|
+
console.log('new value:', newVal);
|
|
258
|
+
console.log('coreStore.config.globalInjections:', coreStore.config?.globalInjections);
|
|
259
|
+
console.log('Stack trace:');
|
|
260
|
+
console.log(err.stack);
|
|
261
|
+
console.groupEnd();
|
|
262
|
+
} else {
|
|
263
|
+
console.groupCollapsed(
|
|
264
|
+
'%c[DEBUG] userMenu changed',
|
|
265
|
+
'color: orange; font-weight: bold;'
|
|
266
|
+
);
|
|
267
|
+
console.log('old value:', oldVal);
|
|
268
|
+
console.log('new value:', newVal);
|
|
269
|
+
console.log('coreStore.config.globalInjections:', coreStore.config?.globalInjections);
|
|
270
|
+
console.groupEnd();
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{ deep: false, immediate: false }
|
|
274
|
+
);
|
|
275
|
+
|
|
205
276
|
function hideSidebar(): void {
|
|
206
277
|
sideBarOpen.value = false;
|
|
207
278
|
}
|
|
@@ -243,6 +314,8 @@ function handleCustomLayout() {
|
|
|
243
314
|
} else if (route.meta?.sidebarAndHeader === 'preferIconOnly') {
|
|
244
315
|
defaultLayout.value = true;
|
|
245
316
|
isSidebarIconOnly.value = true;
|
|
317
|
+
} else if (route.meta?.sidebarAndHeader === 'headerOnly') {
|
|
318
|
+
headerOnlyLayout.value = true;
|
|
246
319
|
} else {
|
|
247
320
|
defaultLayout.value = true;
|
|
248
321
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { AdminForthFilterOperators, AdminForthResourceColumnCommon } from '@/types/Common';
|
|
1
|
+
import type { FrontendAPIInterface, ConfirmParams, AlertParams, } from '@/types/FrontendAPI';
|
|
2
|
+
import type { AdminForthFilterOperators, AdminForthResourceColumnCommon, FilterParams } from '@/types/Common';
|
|
3
3
|
import { useToastStore } from '@/stores/toast';
|
|
4
4
|
import { useModalStore } from '@/stores/modal';
|
|
5
5
|
import { useCoreStore } from '@/stores/core';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<!-- tag form used to reset the input (method .reset() in
|
|
2
|
+
<!-- tag form used to reset the input (method .reset() in clear() function) -->
|
|
3
3
|
<form class="flex items-center justify-center w-full"
|
|
4
4
|
@dragover.prevent="dragging = true"
|
|
5
5
|
@dragleave.prevent="dragging = false"
|
|
@@ -16,42 +16,88 @@
|
|
|
16
16
|
'h-32': !props.multiple,
|
|
17
17
|
}"
|
|
18
18
|
>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
<input
|
|
20
|
+
:id="id"
|
|
21
|
+
type="file"
|
|
22
|
+
class="hidden"
|
|
23
|
+
:accept="normalizedExtensions.join(',')"
|
|
24
|
+
@change="$event.target && doEmit(($event.target as HTMLInputElement).files!)"
|
|
25
|
+
:multiple="props.multiple || false"
|
|
26
|
+
/>
|
|
27
|
+
|
|
28
|
+
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
|
29
|
+
<svg
|
|
30
|
+
v-if="!selectedFiles.length"
|
|
31
|
+
class="w-8 h-8 mb-4 text-lightDropzoneIcon dark:text-darkDropzoneIcon"
|
|
32
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
33
|
+
fill="none"
|
|
34
|
+
viewBox="0 0 20 16"
|
|
35
|
+
>
|
|
36
|
+
<path
|
|
37
|
+
stroke="currentColor"
|
|
38
|
+
stroke-linecap="round"
|
|
39
|
+
stroke-linejoin="round"
|
|
40
|
+
stroke-width="2"
|
|
41
|
+
d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5
|
|
42
|
+
5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0
|
|
43
|
+
0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"
|
|
44
|
+
/>
|
|
45
|
+
</svg>
|
|
46
|
+
|
|
47
|
+
<div
|
|
48
|
+
v-else
|
|
49
|
+
class="flex items-center justify-center py-1 flex-wrap gap-2 w-full mt-1 mb-4 px-4"
|
|
50
|
+
>
|
|
51
|
+
<template v-for="(file, index) in selectedFiles" :key="index">
|
|
52
|
+
<div
|
|
53
|
+
class="text-sm text-lightDropzoneIcon dark:text-darkDropzoneIcon bg-lightDropzoneBackgroundHover dark:bg-darkDropzoneBackgroundHover rounded-md
|
|
54
|
+
flex items-center gap-1 px-2 py-1 group"
|
|
55
|
+
>
|
|
56
|
+
<IconFileSolid class="w-4 h-4 flex-shrink-0" />
|
|
57
|
+
<span
|
|
58
|
+
class="truncate max-w-[200px]"
|
|
59
|
+
:title="file.name"
|
|
60
|
+
>
|
|
61
|
+
{{ shortenFileName(file.name) }}
|
|
62
|
+
</span>
|
|
63
|
+
<span class="text-xs">({{ humanifySize(file.size) }})</span>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
@click.prevent.stop="removeFile(index)"
|
|
67
|
+
class="text-lightDropzoneIcon dark:text-darkDropzoneIcon hover:text-red-600 dark:hover:text-red-400
|
|
68
|
+
opacity-70 hover:opacity-100 transition-all"
|
|
69
|
+
:title="$t('Remove file')"
|
|
70
|
+
>
|
|
71
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
72
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
33
75
|
</div>
|
|
34
|
-
|
|
35
|
-
<p v-if="!selectedFiles.length" class="mb-2 text-sm text-lightDropzoneText dark:text-darkDropzoneText"><span class="font-semibold">{{ $t('Click to upload') }}</span> {{ $t('or drag and drop') }}</p>
|
|
36
|
-
<p class="text-xs text-lightDropzoneText dark:text-darkDropzoneText">
|
|
37
|
-
{{ props.extensions.join(', ').toUpperCase().replace(/\./g, '') }}
|
|
38
|
-
<template v-if="props.maxSizeBytes">
|
|
39
|
-
(Max size: {{ humanifySize(props.maxSizeBytes) }})
|
|
40
|
-
</template>
|
|
41
|
-
</p>
|
|
76
|
+
</template>
|
|
42
77
|
</div>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
78
|
+
|
|
79
|
+
<p
|
|
80
|
+
v-if="!selectedFiles.length"
|
|
81
|
+
class="mb-2 text-sm text-lightDropzoneText dark:text-darkDropzoneText"
|
|
82
|
+
>
|
|
83
|
+
<span class="font-semibold">{{ $t('Click to upload') }}</span>
|
|
84
|
+
{{ $t('or drag and drop') }}
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<p class="text-xs text-lightDropzoneText dark:text-darkDropzoneText">
|
|
88
|
+
{{ normalizedExtensions.join(', ').toUpperCase().replace(/\./g, '') }}
|
|
89
|
+
<template v-if="props.maxSizeBytes">
|
|
90
|
+
(Max size: {{ humanifySize(props.maxSizeBytes) }})
|
|
91
|
+
</template>
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
48
94
|
</label>
|
|
49
|
-
</form>
|
|
95
|
+
</form>
|
|
50
96
|
</template>
|
|
51
97
|
|
|
52
98
|
<script setup lang="ts">
|
|
53
99
|
import { humanifySize } from '@/utils';
|
|
54
|
-
import { ref, type Ref } from 'vue';
|
|
100
|
+
import { ref, type Ref, computed } from 'vue';
|
|
55
101
|
import { IconFileSolid } from '@iconify-prerendered/vue-flowbite';
|
|
56
102
|
import { watch } from 'vue';
|
|
57
103
|
import adminforth from '@/adminforth';
|
|
@@ -67,25 +113,53 @@ const emit = defineEmits(['update:modelValue']);
|
|
|
67
113
|
|
|
68
114
|
const id = `afcl-dropzone-${Math.random().toString(36).substring(7)}`;
|
|
69
115
|
|
|
116
|
+
const normalizedExtensions = computed(() => {
|
|
117
|
+
return props.extensions.map(ext => {
|
|
118
|
+
const trimmed = ext.trim().toLowerCase();
|
|
119
|
+
return trimmed.startsWith('.') ? trimmed : `.${trimmed}`;
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
70
123
|
const selectedFiles: Ref<{
|
|
71
124
|
name: string,
|
|
72
125
|
size: number,
|
|
73
126
|
mime: string,
|
|
74
127
|
}[]> = ref([]);
|
|
75
128
|
|
|
129
|
+
const storedFiles: Ref<File[]> = ref([]);
|
|
130
|
+
|
|
131
|
+
function shortenFileName(name: string, maxLength = 24) {
|
|
132
|
+
if (name.length <= maxLength) return name;
|
|
133
|
+
|
|
134
|
+
const lastDotIndex = name.lastIndexOf('.');
|
|
135
|
+
const hasExtension = lastDotIndex > 0 && lastDotIndex < name.length - 1;
|
|
136
|
+
const extension = hasExtension ? name.slice(lastDotIndex + 1) : '';
|
|
137
|
+
const baseName = hasExtension ? name.slice(0, lastDotIndex) : name;
|
|
138
|
+
|
|
139
|
+
const startName = baseName.slice(0, 12);
|
|
140
|
+
const endName = baseName.split('').reverse().slice(0, 4).reverse().join('');
|
|
141
|
+
return hasExtension ? `${startName}...${endName}.${extension}` : `${startName}...${endName}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
76
144
|
watch(() => props.modelValue, (files) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
});
|
|
145
|
+
if (files && files.length > 0) {
|
|
146
|
+
selectedFiles.value = Array.from(files).map(file => ({
|
|
147
|
+
name: file.name,
|
|
148
|
+
size: file.size,
|
|
149
|
+
mime: file.type,
|
|
150
|
+
}));
|
|
151
|
+
storedFiles.value = Array.from(files);
|
|
152
|
+
} else {
|
|
153
|
+
selectedFiles.value = [];
|
|
154
|
+
storedFiles.value = [];
|
|
155
|
+
}
|
|
156
|
+
}, { immediate: true });
|
|
83
157
|
|
|
84
158
|
function doEmit(filesIn: FileList) {
|
|
85
159
|
|
|
86
160
|
const multiple = props.multiple || false;
|
|
87
161
|
const files = Array.from(filesIn);
|
|
88
|
-
const allowedExtensions =
|
|
162
|
+
const allowedExtensions = normalizedExtensions.value;
|
|
89
163
|
const maxSizeBytes = props.maxSizeBytes;
|
|
90
164
|
|
|
91
165
|
if (!files.length) return;
|
|
@@ -96,6 +170,18 @@ function doEmit(filesIn: FileList) {
|
|
|
96
170
|
const extension = file.name.split('.').pop()?.toLowerCase() || '';
|
|
97
171
|
const size = file.size;
|
|
98
172
|
|
|
173
|
+
const isDuplicate = storedFiles.value.some(
|
|
174
|
+
existingFile => existingFile.name === file.name && existingFile.size === file.size
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (isDuplicate) {
|
|
178
|
+
adminforth.alert({
|
|
179
|
+
message: `The file "${file.name}" is already selected.`,
|
|
180
|
+
variant: 'warning',
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
99
185
|
if (!allowedExtensions.includes(`.${extension}`)) {
|
|
100
186
|
adminforth.alert({
|
|
101
187
|
message: `Sorry, the file type .${extension} is not allowed. Please upload a file with one of the following extensions: ${allowedExtensions.join(', ')}`,
|
|
@@ -110,26 +196,37 @@ function doEmit(filesIn: FileList) {
|
|
|
110
196
|
});
|
|
111
197
|
return;
|
|
112
198
|
}
|
|
113
|
-
|
|
199
|
+
|
|
114
200
|
validFiles.push(file);
|
|
115
201
|
});
|
|
116
202
|
|
|
117
203
|
if (!multiple) {
|
|
118
|
-
validFiles.
|
|
204
|
+
storedFiles.value = validFiles.slice(0, 1);
|
|
205
|
+
} else {
|
|
206
|
+
storedFiles.value = [...storedFiles.value, ...validFiles];
|
|
119
207
|
}
|
|
120
|
-
|
|
208
|
+
|
|
209
|
+
selectedFiles.value = storedFiles.value.map(file => ({
|
|
121
210
|
name: file.name,
|
|
122
211
|
size: file.size,
|
|
123
212
|
mime: file.type,
|
|
124
213
|
}));
|
|
125
214
|
|
|
126
|
-
emit('update:modelValue',
|
|
215
|
+
emit('update:modelValue', storedFiles.value);
|
|
216
|
+
|
|
127
217
|
}
|
|
128
218
|
|
|
129
219
|
const dragging = ref(false);
|
|
130
220
|
|
|
221
|
+
function removeFile(index: number) {
|
|
222
|
+
storedFiles.value = storedFiles.value.filter((_, i) => i !== index);
|
|
223
|
+
selectedFiles.value = selectedFiles.value.filter((_, i) => i !== index);
|
|
224
|
+
emit('update:modelValue', storedFiles.value);
|
|
225
|
+
}
|
|
226
|
+
|
|
131
227
|
function clear() {
|
|
132
228
|
selectedFiles.value = [];
|
|
229
|
+
storedFiles.value = [];
|
|
133
230
|
emit('update:modelValue', []);
|
|
134
231
|
const form = document.getElementById(id)?.closest('form');
|
|
135
232
|
form?.reset();
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
aria-describedby="helper-text-explanation"
|
|
18
18
|
class="afcl-input inline-flex bg-lightInputBackground border border-lightInputBorder rounded-0 focus:ring-lightPrimary focus:border-lightPrimary dark:focus:ring-darkPrimary dark:focus:border-darkPrimary
|
|
19
19
|
blue-500 focus:border-blue-500 block w-20 p-2.5 dark:bg-darkInputBackground dark:border-darkInputBorder placeholder-lightInputPlaceholderText dark:placeholder-darkInputPlaceholderText dark:text-darkInputText translate-y-0"
|
|
20
|
-
:class="{'rounded-l-md': !$slots.prefix && !prefix, 'rounded-r-md': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base':
|
|
20
|
+
:class="{'rounded-l-md': !$slots.prefix && !prefix, 'rounded-r-md': !$slots.suffix && !suffix, 'w-full': fullWidth, 'text-base': isIos, 'text-sm': !isIos }"
|
|
21
21
|
:disabled="readonly"
|
|
22
22
|
>
|
|
23
23
|
|
|
@@ -36,7 +36,10 @@
|
|
|
36
36
|
<script setup lang="ts">
|
|
37
37
|
|
|
38
38
|
import { ref } from 'vue';
|
|
39
|
-
|
|
39
|
+
import { useCoreStore } from '@/stores/core';
|
|
40
|
+
|
|
41
|
+
const coreStore = useCoreStore();
|
|
42
|
+
const isIos = coreStore.isIos;
|
|
40
43
|
|
|
41
44
|
const props = defineProps<{
|
|
42
45
|
type: string,
|
|
@@ -53,12 +56,5 @@ defineExpose({
|
|
|
53
56
|
focus: () => input.value?.focus(),
|
|
54
57
|
});
|
|
55
58
|
|
|
56
|
-
function isIOS() {
|
|
57
|
-
return (
|
|
58
|
-
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
59
|
-
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
|
60
|
-
)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
</script>
|
|
64
60
|
|