@opengis/cms 0.0.21 → 0.0.22
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/package.json +2 -2
- package/src/App.css +52 -0
- package/src/App.vue +62 -0
- package/src/assets/image.png +0 -0
- package/src/assets/main.css +3 -0
- package/src/assets/tailwind-3.4.17.js +113 -0
- package/src/components/LanguageSwitcher.vue +73 -0
- package/src/components/SettingsCard.vue +40 -0
- package/src/components/builder/CreateForm.vue +128 -0
- package/src/components/builder/formTypeSchema.js +145 -0
- package/src/components/builder/tabs/index.ts +9 -0
- package/src/components/builder/tabs/vs-builder-edit.vue +133 -0
- package/src/components/builder/tabs/vs-builder-monaco.vue +29 -0
- package/src/components/builder/tabs/vs-builder-preview.vue +39 -0
- package/src/components/builder/vs-builder-datatable-controls.vue +138 -0
- package/src/components/builder/vs-builder-datatable-form.vue +80 -0
- package/src/components/builder/vs-builder-datatable.vue +191 -0
- package/src/components/builder/vs-builder-list-item.vue +110 -0
- package/src/components/collections/CollectionsBreadcrumb.vue +52 -0
- package/src/components/collections/CollectionsGrid.vue +176 -0
- package/src/components/collections/ContentBlock.vue +75 -0
- package/src/components/collections/formWrapper.vue +156 -0
- package/src/components/dashboard/ContentItem.vue +82 -0
- package/src/components/dashboard/DashboardHeader.vue +33 -0
- package/src/components/dashboard/QuickActions.vue +84 -0
- package/src/components/dashboard/RecentContent.vue +54 -0
- package/src/components/dashboard/StatCard.vue +63 -0
- package/src/components/dashboard/StatsGrid.vue +28 -0
- package/src/components/form-components/MonacoEditor.vue +104 -0
- package/src/components/form-components/VsFormMeta.vue +40 -0
- package/src/components/form-components/VsFormTags.vue +150 -0
- package/src/components/form-components/custom-datatable/vs-form-custom-datatable-add.vue +84 -0
- package/src/components/form-components/custom-datatable/vs-form-custom-datatable-controls.vue +106 -0
- package/src/components/form-components/index.js +23 -0
- package/src/components/form-components/reference/vs-form-reference-add.vue +92 -0
- package/src/components/form-components/reference/vs-form-reference-controls.vue +101 -0
- package/src/components/form-components/reference-list/referenceOptionList.js +78 -0
- package/src/components/form-components/reference-list/vs-form-reference-add.vue +145 -0
- package/src/components/form-components/reference-list/vs-form-reference-choce.vue +39 -0
- package/src/components/form-components/reference-list/vs-form-reference-controls.vue +110 -0
- package/src/components/form-components/reference-skeleton/about-skeleton.vue +37 -0
- package/src/components/form-components/reference-skeleton/banner-skeleton.vue +29 -0
- package/src/components/form-components/reference-skeleton/body-skeleton.vue +56 -0
- package/src/components/form-components/reference-skeleton/cards-skeleton.vue +47 -0
- package/src/components/form-components/reference-skeleton/documents-skeleton.vue +64 -0
- package/src/components/form-components/reference-skeleton/faq-skeleton.vue +64 -0
- package/src/components/form-components/reference-skeleton/form-skeleton.vue +41 -0
- package/src/components/form-components/reference-skeleton/index.js +36 -0
- package/src/components/form-components/reference-skeleton/infoLine-skeleton.vue +37 -0
- package/src/components/form-components/reference-skeleton/news-skeleton.vue +54 -0
- package/src/components/form-components/reference-skeleton/slider-skeleton.vue +41 -0
- package/src/components/form-components/reference-skeleton/tabs-skeleton.vue +40 -0
- package/src/components/form-components/reference-skeleton/team-skeleton.vue +103 -0
- package/src/components/form-components/reference-skeleton/usefulLinks-skeleton.vue +52 -0
- package/src/components/form-components/reference-skeleton/video-skeleton.vue +36 -0
- package/src/components/form-components/testReferenceTypes.js +773 -0
- package/src/components/form-components/vs-form-color-picker.vue +29 -0
- package/src/components/form-components/vs-form-custom-datatable.vue +214 -0
- package/src/components/form-components/vs-form-integer.vue +86 -0
- package/src/components/form-components/vs-form-key-value.vue +201 -0
- package/src/components/form-components/vs-form-marcdown-md.vue +3 -0
- package/src/components/form-components/vs-form-media-select.vue +780 -0
- package/src/components/form-components/vs-form-reference-list.vue +97 -0
- package/src/components/form-components/vs-form-reference.vue +59 -0
- package/src/components/form-components/vs-form-relation.vue +30 -0
- package/src/components/form-components/vs-form-reletion-link.vue +34 -0
- package/src/components/form-components/vs-form-select-collection.vue +0 -0
- package/src/components/form-components/vs-form-slug.vue +72 -0
- package/src/components/form-components/vs-form-tiptap.vue +7 -0
- package/src/components/form-components/vs-richtext-md.vue +3 -0
- package/src/components/icons/BellIcon.vue +17 -0
- package/src/components/icons/GlobeIcon.vue +18 -0
- package/src/components/icons/KeyIcon.vue +20 -0
- package/src/components/icons/PaletteIcon.vue +22 -0
- package/src/components/icons/SettingsIcon.vue +19 -0
- package/src/components/icons/ShieldIcon.vue +18 -0
- package/src/components/icons/UsersIcon.vue +19 -0
- package/src/components/icons/icon-chevron-right.vue +16 -0
- package/src/components/icons/icon-drag.vue +20 -0
- package/src/components/icons/icon-file-text.vue +21 -0
- package/src/components/icons/icon-folder.vue +18 -0
- package/src/components/icons/icon-grid.vue +17 -0
- package/src/components/icons/icon-group.vue +19 -0
- package/src/components/icons/icon-home.vue +16 -0
- package/src/components/icons/icon-image.vue +18 -0
- package/src/components/icons/icon-list.vue +20 -0
- package/src/components/icons/icon-more.vue +17 -0
- package/src/components/icons/icon-plus.vue +17 -0
- package/src/components/icons-types/icon-array.vue +22 -0
- package/src/components/icons-types/icon-boolean.vue +18 -0
- package/src/components/icons-types/icon-datalist.vue +22 -0
- package/src/components/icons-types/icon-date.vue +20 -0
- package/src/components/icons-types/icon-datetime.vue +20 -0
- package/src/components/icons-types/icon-file.vue +21 -0
- package/src/components/icons-types/icon-gallery.vue +18 -0
- package/src/components/icons-types/icon-image.vue +19 -0
- package/src/components/icons-types/icon-integer.vue +20 -0
- package/src/components/icons-types/icon-merkdown.vue +18 -0
- package/src/components/icons-types/icon-multiselect.vue +22 -0
- package/src/components/icons-types/icon-number.vue +20 -0
- package/src/components/icons-types/icon-radio.vue +22 -0
- package/src/components/icons-types/icon-reference-list.vue +22 -0
- package/src/components/icons-types/icon-reference.vue +20 -0
- package/src/components/icons-types/icon-relation.vue +22 -0
- package/src/components/icons-types/icon-richtext.vue +18 -0
- package/src/components/icons-types/icon-select.vue +22 -0
- package/src/components/icons-types/icon-slug.vue +19 -0
- package/src/components/icons-types/icon-text.vue +19 -0
- package/src/components/icons-types/index.js +43 -0
- package/src/components/layout/Layout.vue +67 -0
- package/src/components/layout/Sidebar.vue +128 -0
- package/src/components/media/FileUploadProgress.vue +29 -0
- package/src/components/media/MediaBreadcrumb.vue +42 -0
- package/src/components/media/MediaCreateFolder.vue +59 -0
- package/src/components/media/MediaFileInfo.vue +148 -0
- package/src/components/media/MediaGrid.vue +148 -0
- package/src/components/media/MediaList.vue +148 -0
- package/src/components/media/MediaViewControls.vue +38 -0
- package/src/components/media/TypeTag.vue +23 -0
- package/src/components/menu/AddNewItemInTree.vue +75 -0
- package/src/components/menu/MenuBody.vue +149 -0
- package/src/components/menu/MenuItem.vue +73 -0
- package/src/components/menu/MenuList.vue +101 -0
- package/src/components/referencec/index.ts +7 -0
- package/src/components/referencec/vs-reference-faq.vue +61 -0
- package/src/components/referencec/vs-reference-user-card.vue +40 -0
- package/src/components/settings/NotificationSettings.vue +32 -0
- package/src/components/settings/SettingsTable.vue +50 -0
- package/src/components/settings/SettingsTitle.vue +33 -0
- package/src/components/settings/SettingsToggleItem.vue +25 -0
- package/src/components/sidebar/DropdownMenu.vue +34 -0
- package/src/components/sidebar/SettingsSidebar.vue +121 -0
- package/src/components/sidebar/SidebarFooter.vue +52 -0
- package/src/components/sidebar/SidebarHeader.vue +57 -0
- package/src/components/sidebar/SidebarMenu.vue +78 -0
- package/src/components/ui/EmptyData.vue +76 -0
- package/src/components/ui/UniversalTable.vue +310 -0
- package/src/components/ui/UniversalTableFilters.vue +0 -0
- package/src/components/ui/UniversalTablePagination.vue +118 -0
- package/src/components/ui/VsPreview.vue +75 -0
- package/src/composables/useCollectionView.ts +21 -0
- package/src/composables/useDebounce.ts +26 -0
- package/src/composables/useMonaco.ts +28 -0
- package/src/composables/useTheme.ts +40 -0
- package/src/content/test-slug/metadata.json +1 -0
- package/src/i18n.ts +75 -0
- package/src/index.css +3 -0
- package/src/locales/en.json +778 -0
- package/src/locales/uk.json +797 -0
- package/src/main.ts +41 -0
- package/src/pages/Dashboard.vue +168 -0
- package/src/pages/EmailPage.vue +183 -0
- package/src/pages/FeedbackPage.vue +232 -0
- package/src/pages/MediaPage.vue +372 -0
- package/src/pages/TagsPage.vue +207 -0
- package/src/pages/builder/BuilderPage.vue +195 -0
- package/src/pages/builder/EditCollectionPage.vue +163 -0
- package/src/pages/collections/ArticlesPage.vue +385 -0
- package/src/pages/collections/CollectionsPage.vue +146 -0
- package/src/pages/collections/SingletonsPage.vue +119 -0
- package/src/pages/collections/contentForm.vue +484 -0
- package/src/pages/collections/schema/seo.ts +27 -0
- package/src/pages/menu/MenuAddPage.vue +123 -0
- package/src/pages/menu/MenuItemPage.vue +183 -0
- package/src/pages/menu/MenuPage.vue +133 -0
- package/src/pages/settings/ApiKeys.vue +75 -0
- package/src/pages/settings/Appearance.vue +80 -0
- package/src/pages/settings/Logs.vue +260 -0
- package/src/pages/settings/PermissionsPage.vue +237 -0
- package/src/pages/settings/Settings.vue +186 -0
- package/src/pages/settings/Users.vue +109 -0
- package/src/pages/settings/general.vue +154 -0
- package/src/pages/settings/generalScheme.js +132 -0
- package/src/pages/users/AddUser.vue +106 -0
- package/src/pages/users/UsersPage.vue +98 -0
- package/src/props/builder.ts +67 -0
- package/src/props/content.ts +56 -0
- package/src/props/media.ts +63 -0
- package/src/router/index.ts +181 -0
- package/src/types/fastify-auth.d.ts +4 -0
- package/src/utils/getField.js +270 -0
- package/src/utils/translit.js +19 -0
- package/src/vite-env.d.ts +1 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="uploads?.length" class="fixed bottom-4 right-4 w-96 bg-white shadow-lg rounded-lg p-4 z-50">
|
|
3
|
+
<div class="font-bold mb-2">{{ $t("media.uploadingFiles") }}</div>
|
|
4
|
+
<div v-for="upload in uploads" :key="upload.id" class="mb-3">
|
|
5
|
+
<div class="flex justify-between items-center mb-1">
|
|
6
|
+
<span class="truncate w-2/3">{{ upload?.name }}</span>
|
|
7
|
+
<span v-if="upload.status === 'success'" class="text-green-600">✔</span>
|
|
8
|
+
<span v-else-if="upload.status === 'error'" class="text-red-600">✖</span>
|
|
9
|
+
<span v-else class="text-blue-600">{{ upload?.progress }}%</span>
|
|
10
|
+
</div>
|
|
11
|
+
<div class="w-full bg-gray-200 rounded h-2">
|
|
12
|
+
<div
|
|
13
|
+
class="bg-blue-500 h-2 rounded"
|
|
14
|
+
:style="{ width: upload?.progress + '%' }"
|
|
15
|
+
></div>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<script setup>
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
uploads: {
|
|
25
|
+
type: Array,
|
|
26
|
+
required: true,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center space-x-1">
|
|
3
|
+
<button
|
|
4
|
+
class="flex gap-x-1 items-center px-2 py-1 rounded text-sm font-medium transition-all duration-200 text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20"
|
|
5
|
+
@click="$emit('navigate', [])"
|
|
6
|
+
>
|
|
7
|
+
<IconHome class="w-3 h-3" />
|
|
8
|
+
{{ $t("media.mediaLibrary") }}
|
|
9
|
+
</button>
|
|
10
|
+
<ChevronRight
|
|
11
|
+
v-if="currentPath.length > 0"
|
|
12
|
+
class="w-3 h-3 text-slate-400 dark:text-slate-500"
|
|
13
|
+
/>
|
|
14
|
+
<button
|
|
15
|
+
v-for="(segment, index) in currentPath"
|
|
16
|
+
:key="index"
|
|
17
|
+
class="flex gap-x-1 items-center px-2 py-1 rounded text-sm font-medium transition-all duration-200 text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-900/20"
|
|
18
|
+
@click="$emit('navigate', currentPath.slice(0, index + 1))"
|
|
19
|
+
>
|
|
20
|
+
<Folder class="w-3 h-3" />
|
|
21
|
+
{{ segment }}
|
|
22
|
+
<ChevronRight
|
|
23
|
+
v-if="index < currentPath.length - 1"
|
|
24
|
+
class="w-3 h-3 text-slate-400 dark:text-slate-500"
|
|
25
|
+
/>
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup>
|
|
31
|
+
import { ChevronRight, Folder } from "lucide-vue-next";
|
|
32
|
+
import IconHome from "../icons/icon-home.vue";
|
|
33
|
+
|
|
34
|
+
defineProps({
|
|
35
|
+
currentPath: {
|
|
36
|
+
type: Array,
|
|
37
|
+
required: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
defineEmits(["navigate"]);
|
|
42
|
+
</script>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<VsModal teleport="#modal" :visible="modelValue" :title="$t('media.createFolder')" @close="$emit('update:modelValue', false)">
|
|
3
|
+
<VsForm :schema="scheme" v-model="formValues" v-model:form="form" />
|
|
4
|
+
<template #footer>
|
|
5
|
+
<div class="flex justify-end p-[20px] gap-[10px] border-t w-full">
|
|
6
|
+
<button
|
|
7
|
+
@click="$emit('update:modelValue', false)"
|
|
8
|
+
class="inline-flex items-center px-3 py-2 text-sm text-black duration-300 border border-gray-200 rounded-lg gap-x-2 whitespace-nowrap hover:bg-gray-100"
|
|
9
|
+
>
|
|
10
|
+
{{ $t("common.actions.cancel") }}
|
|
11
|
+
</button>
|
|
12
|
+
<button
|
|
13
|
+
@click="handleCreate"
|
|
14
|
+
class="py-2 px-3 inline-flex items-center gap-x-2 text-sm whitespace-nowrap text-white bg-blue-500 rounded-lg !border-gray-200 hover:bg-blue-700 duration-300"
|
|
15
|
+
>
|
|
16
|
+
{{ $t("common.actions.create") }}
|
|
17
|
+
</button>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
</VsModal>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
import { ref } from "vue";
|
|
25
|
+
import { useI18n } from "vue-i18n";
|
|
26
|
+
import VsForm from "@opengis/form";
|
|
27
|
+
import { VsModal } from "@opengis/core";
|
|
28
|
+
import { notify } from '@opengis/core';
|
|
29
|
+
const { t } = useI18n();
|
|
30
|
+
|
|
31
|
+
const modelValue = defineModel();
|
|
32
|
+
|
|
33
|
+
const emit = defineEmits(["update:modelValue", "create"]);
|
|
34
|
+
|
|
35
|
+
const formValues = ref({});
|
|
36
|
+
const form = ref({});
|
|
37
|
+
|
|
38
|
+
const scheme = {
|
|
39
|
+
name: {
|
|
40
|
+
name: "name",
|
|
41
|
+
label: t("media.newFolder"),
|
|
42
|
+
type: "Text",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleCreate = async () => {
|
|
47
|
+
let result = await form.value.validate();
|
|
48
|
+
if (result) {
|
|
49
|
+
notify({
|
|
50
|
+
type: "warning",
|
|
51
|
+
title: t("common.actions.warning"),
|
|
52
|
+
message: t("media.createFolderFailed"),
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
emit("create", { ...formValues.value });
|
|
57
|
+
formValues.value = {};
|
|
58
|
+
};
|
|
59
|
+
</script>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="h-[fit-content] w-80 flex flex-col flex-shrink-0 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-lg">
|
|
3
|
+
<div
|
|
4
|
+
v-if="!file"
|
|
5
|
+
class="flex flex-col items-center justify-center h-64 text-muted-foreground"
|
|
6
|
+
>
|
|
7
|
+
<p>{{ $t("media.selectFile") }}</p>
|
|
8
|
+
</div>
|
|
9
|
+
<template v-else>
|
|
10
|
+
<div class="p-4 border-b border-slate-200 dark:border-slate-700">
|
|
11
|
+
<h3 class="font-semibold text-slate-800 dark:text-slate-100">{{ $t("media.fileDetails") }}</h3>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="p-4 space-y-4">
|
|
14
|
+
<div class="aspect-video flex items-center justify-center bg-slate-100 dark:bg-slate-700 rounded-lg overflow-hidden relative">
|
|
15
|
+
<img
|
|
16
|
+
v-if="file?.filetype === 'image' && file?.filepath"
|
|
17
|
+
:src="file?.preview"
|
|
18
|
+
:alt="file?.alt || file?.filename"
|
|
19
|
+
class="w-full h-full object-cover"
|
|
20
|
+
>
|
|
21
|
+
<FileText v-else class="w-full h-4/5 object-cover" />
|
|
22
|
+
<div class="absolute top-3 right-3">
|
|
23
|
+
<TypeTag :type="file?.filetype" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="space-y-3">
|
|
27
|
+
<div>
|
|
28
|
+
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.name") }}</label>
|
|
29
|
+
<p class="text-slate-800 dark:text-slate-100 break-all">{{ file?.filename }}</p>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.size") }}</label>
|
|
33
|
+
<p class="text-slate-800 dark:text-slate-100">{{ formatFileSize(file?.filesize) }}</p>
|
|
34
|
+
</div>
|
|
35
|
+
<div v-if="file?.dimensions">
|
|
36
|
+
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.dimensions") }}</label>
|
|
37
|
+
<p class="text-slate-800 dark:text-slate-100">1920x1080</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.uploadDate") }}</label>
|
|
41
|
+
<p class="text-slate-800 dark:text-slate-100 flex items-center">
|
|
42
|
+
<Calendar class="w-4 h-4 mr-2" />
|
|
43
|
+
{{ file?.created_at ? formatDate(file?.created_at) : "--" }}
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
<div v-if="file?.author">
|
|
47
|
+
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.author") }}</label>
|
|
48
|
+
<p class="text-slate-800 dark:text-slate-100">{{ file?.author }}</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div v-if="file?.description">
|
|
51
|
+
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.description") }}</label>
|
|
52
|
+
<p class="text-slate-800 dark:text-slate-100">{{ file?.description }}</p>
|
|
53
|
+
</div>
|
|
54
|
+
<div v-if="file?.tags">
|
|
55
|
+
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-sm font-medium text-slate-700 dark:text-slate-300">{{ $t("media.tags") }}</label>
|
|
56
|
+
<div class="flex flex-wrap gap-1 mt-1">
|
|
57
|
+
<div class="inline-flex items-center rounded-md border px-2.5 py-0.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground text-xs">
|
|
58
|
+
banner
|
|
59
|
+
</div>
|
|
60
|
+
<div class="inline-flex items-center rounded-md border px-2.5 py-0.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground text-xs">
|
|
61
|
+
hero
|
|
62
|
+
</div>
|
|
63
|
+
<div class="inline-flex items-center rounded-md border px-2.5 py-0.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground text-xs">
|
|
64
|
+
homepage
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="space-y-2 pt-4 border-t border-slate-200 dark:border-slate-700">
|
|
70
|
+
<a :href="file?.filepath" download class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 shadow h-9 px-4 py-2 w-full bg-blue-600 hover:bg-blue-700 text-white">
|
|
71
|
+
<Download class="w-4 h-4 mr-2" />
|
|
72
|
+
{{ $t("media.download") }}
|
|
73
|
+
</a>
|
|
74
|
+
<button
|
|
75
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 border shadow-sm hover:text-accent-foreground h-9 px-4 py-2 w-full bg-slate-600 hover:bg-slate-700 text-white"
|
|
76
|
+
@click="copyFilePath"
|
|
77
|
+
>
|
|
78
|
+
<Copy class="w-4 h-4 mr-2" />
|
|
79
|
+
{{ $t("media.copyPath") }}
|
|
80
|
+
</button>
|
|
81
|
+
<button
|
|
82
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 border shadow-sm hover:text-accent-foreground h-9 px-4 py-2 w-full bg-red-500 hover:bg-red-600 text-white"
|
|
83
|
+
@click="openConfirm"
|
|
84
|
+
>
|
|
85
|
+
<Trash2 class="w-4 h-4 mr-2" />
|
|
86
|
+
{{ $t("media.delete") }}
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
</div>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<script setup>
|
|
96
|
+
import IconMore from "../icons/icon-more.vue";
|
|
97
|
+
import { Download, FileText, Share2, Pencil, Calendar, Trash2, Copy } from "lucide-vue-next";
|
|
98
|
+
import { formatFileSize, formatDate } from "../../props/media";
|
|
99
|
+
import TypeTag from "./TypeTag.vue";
|
|
100
|
+
import { confirm, notify } from '@opengis/core';
|
|
101
|
+
import { useI18n } from "vue-i18n";
|
|
102
|
+
const { t } = useI18n();
|
|
103
|
+
|
|
104
|
+
const emit = defineEmits(["delete-file"]);
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
const props = defineProps({
|
|
108
|
+
file: {
|
|
109
|
+
type: Object,
|
|
110
|
+
default: null,
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const openConfirm = () => {
|
|
115
|
+
confirm({
|
|
116
|
+
title: t("builder.deleteTitle"),
|
|
117
|
+
message: t("builder.deleteObject"),
|
|
118
|
+
type: 'error',
|
|
119
|
+
onConfirm: () => {
|
|
120
|
+
handleDeleteFile();
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleDeleteFile = () => {
|
|
126
|
+
emit("delete-file", props.file);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const copyFilePath = async () => {
|
|
130
|
+
if (props.file?.filepath) {
|
|
131
|
+
try {
|
|
132
|
+
await navigator.clipboard.writeText(props.file.filepath);
|
|
133
|
+
notify({
|
|
134
|
+
type: "success",
|
|
135
|
+
title: t("common.actions.success"),
|
|
136
|
+
message: t("toast.copied"),
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.error('Failed to copy file path:', err);
|
|
140
|
+
notify({
|
|
141
|
+
type: "error",
|
|
142
|
+
title: t("common.actions.error"),
|
|
143
|
+
message: t("media.copyPathFailed"),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
</script>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 max-h-[calc(100%-130px)] overflow-y-auto px-2 pt-2 pb-5"
|
|
4
|
+
>
|
|
5
|
+
<div
|
|
6
|
+
v-if="files?.length === 0"
|
|
7
|
+
class="flex flex-col items-center justify-center h-64 text-muted-foreground col-span-full"
|
|
8
|
+
>
|
|
9
|
+
<File class="w-12 h-12 mb-2" />
|
|
10
|
+
<p>{{ $t('noFilesInFolder') }}</p>
|
|
11
|
+
</div>
|
|
12
|
+
<template v-for="file in sortedFiles" :key="file?.url">
|
|
13
|
+
<div
|
|
14
|
+
v-if="file?.type === 'dir'"
|
|
15
|
+
class="p-6 text-center rounded-xl h-[fit-content] relative text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm hover:shadow-xl transition-all duration-200 transform hover:scale-105 cursor-pointer"
|
|
16
|
+
@click="$emit('file-click', file)"
|
|
17
|
+
>
|
|
18
|
+
<div
|
|
19
|
+
v-if="isDeleteButton"
|
|
20
|
+
@click.stop="selectedFolder = file; openConfirm"
|
|
21
|
+
class="absolute top-4 right-4 p-1 border border-gray-300 dark:border-slate-700 rounded-lg cursor-pointer hover:bg-gray-200 dark:hover:bg-slate-700 group"
|
|
22
|
+
>
|
|
23
|
+
<Trash2 class="w-4 h-4 text-gray-500 group-hover:text-red-500" />
|
|
24
|
+
</div>
|
|
25
|
+
<Folder class="w-16 h-16 mx-auto mb-4 text-blue-600" />
|
|
26
|
+
<h3 class="truncate font-medium text-slate-800 dark:text-slate-100 mb-2">{{ file?.name}}</h3>
|
|
27
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">{{ file?.count }} {{ $t('media.files') }}</p>
|
|
28
|
+
</div>
|
|
29
|
+
<div
|
|
30
|
+
v-else-if="file?.type === 'file'"
|
|
31
|
+
class="h-[fit-content] rounded-xl text-card-foreground shadow-lg border-0 bg-white dark:bg-slate-800 backdrop-blur-sm hover:shadow-xl transition-all duration-200 transform hover:scale-105 group cursor-pointer"
|
|
32
|
+
:class="{
|
|
33
|
+
'bg-accent text-accent-foreground ring-2 ring-primary':
|
|
34
|
+
selectedFile?.url === file?.url || (selectedFiles && selectedFiles.some(f => f.id === file.id)),
|
|
35
|
+
}"
|
|
36
|
+
@click="$emit('file-click', file)"
|
|
37
|
+
>
|
|
38
|
+
|
|
39
|
+
<div class="p-0">
|
|
40
|
+
<div class="aspect-video bg-slate-100 dark:bg-slate-700 rounded-t-lg overflow-hidden relative">
|
|
41
|
+
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-slate-100 to-slate-200 dark:from-slate-700 dark:to-slate-800">
|
|
42
|
+
<div
|
|
43
|
+
v-if="file?.filetype === 'image' && (file?.filepath || file?.url)"
|
|
44
|
+
class="relative w-full"
|
|
45
|
+
>
|
|
46
|
+
<img
|
|
47
|
+
:src="file?.preview"
|
|
48
|
+
:alt="file?.alt || file?.filename"
|
|
49
|
+
class="object-cover w-full h-full rounded text-xs"
|
|
50
|
+
/>
|
|
51
|
+
</div>
|
|
52
|
+
<FileText v-else class="w-12 h-12 text-gray-500" />
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="p-4">
|
|
56
|
+
<div class="flex items-center justify-between mb-2">
|
|
57
|
+
<h3 class="font-medium text-slate-800 dark:text-slate-100 truncate flex-1 mr-2">{{ file.filename }}</h3>
|
|
58
|
+
<TypeTag :type="file?.filetype" />
|
|
59
|
+
</div>
|
|
60
|
+
<div class="space-y-1 text-sm text-slate-500 dark:text-slate-400">
|
|
61
|
+
<div class="flex justify-between">
|
|
62
|
+
<span>{{ $t('media.size') }}:</span>
|
|
63
|
+
<span>{{ formatFileSize(file.filesize) }}</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="flex justify-between">
|
|
66
|
+
<span>{{ $t('media.uploaded') }}:</span>
|
|
67
|
+
<span>{{ formatDate(file.created_at) }}</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
77
|
+
<script setup>
|
|
78
|
+
import { File, Folder, FileText, Trash2 } from "lucide-vue-next";
|
|
79
|
+
import { formatFileSize } from "../../props/media";
|
|
80
|
+
import TypeTag from "./TypeTag.vue";
|
|
81
|
+
import { ref, onMounted, onUpdated, defineEmits } from "vue";
|
|
82
|
+
import { confirm } from '@opengis/core';
|
|
83
|
+
import { useI18n } from "vue-i18n";
|
|
84
|
+
const { t } = useI18n();
|
|
85
|
+
|
|
86
|
+
const emit = defineEmits(["delete-file", "file-click", "delete-folder"]);
|
|
87
|
+
|
|
88
|
+
const props = defineProps({
|
|
89
|
+
files: {
|
|
90
|
+
type: Array,
|
|
91
|
+
required: true,
|
|
92
|
+
},
|
|
93
|
+
selectedFile: {
|
|
94
|
+
type: Object,
|
|
95
|
+
default: null,
|
|
96
|
+
},
|
|
97
|
+
selectedFiles: {
|
|
98
|
+
type: Array,
|
|
99
|
+
default: () => [],
|
|
100
|
+
},
|
|
101
|
+
isDeleteButton: {
|
|
102
|
+
type: Boolean,
|
|
103
|
+
default: false,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const sortedFiles = ref([]);
|
|
108
|
+
const selectedFolder = ref(null);
|
|
109
|
+
|
|
110
|
+
const openConfirm = () => {
|
|
111
|
+
confirm({
|
|
112
|
+
title: t("builder.deleteTitle"),
|
|
113
|
+
message: t("builder.deleteObject"),
|
|
114
|
+
type: 'error',
|
|
115
|
+
onConfirm: () => {
|
|
116
|
+
handleDeleteFile();
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleDeleteFile = () => {
|
|
122
|
+
if (selectedFolder.value.type === 'file') {
|
|
123
|
+
emit("delete-file", selectedFile.value);
|
|
124
|
+
} else {
|
|
125
|
+
emit("delete-folder", selectedFolder.value);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
const formatDate = (dateStr) => {
|
|
131
|
+
if (!dateStr) return "";
|
|
132
|
+
const date = new Date(dateStr);
|
|
133
|
+
if (isNaN(date.getTime())) return '';
|
|
134
|
+
return date.toISOString().slice(0, 10);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const sortFiles = () => {
|
|
138
|
+
sortedFiles.value = props.files.sort((a, b) => {
|
|
139
|
+
if (a.type === 'dir' && b.type !== 'dir') return -1;
|
|
140
|
+
if (a.type !== 'dir' && b.type === 'dir') return 1;
|
|
141
|
+
return 0;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onMounted(() => sortFiles());
|
|
146
|
+
|
|
147
|
+
onUpdated(() => sortFiles());
|
|
148
|
+
</script>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div
|
|
4
|
+
v-if="files.length === 0"
|
|
5
|
+
class="flex flex-col items-center justify-center h-64 text-muted-foreground"
|
|
6
|
+
>
|
|
7
|
+
<File class="w-12 h-12 mb-2" />
|
|
8
|
+
<p>{{ $t("media.noFilesInFolder") }}</p>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<table v-else class="w-full text-sm caption-bottom">
|
|
12
|
+
<thead class="[&_tr]:border-b">
|
|
13
|
+
<tr
|
|
14
|
+
class="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
|
|
15
|
+
>
|
|
16
|
+
<th
|
|
17
|
+
class="h-12 px-4 text-left align-middle font-medium text-muted-foreground w-[300px]"
|
|
18
|
+
>
|
|
19
|
+
{{ $t("media.name") }}
|
|
20
|
+
</th>
|
|
21
|
+
<th
|
|
22
|
+
class="h-12 px-4 font-medium text-left align-middle text-muted-foreground"
|
|
23
|
+
>
|
|
24
|
+
{{ $t("media.type") }}
|
|
25
|
+
</th>
|
|
26
|
+
<th
|
|
27
|
+
class="h-12 px-4 font-medium text-left align-middle text-muted-foreground"
|
|
28
|
+
>
|
|
29
|
+
{{ $t("media.size") }}
|
|
30
|
+
</th>
|
|
31
|
+
<th
|
|
32
|
+
class="hidden h-12 px-4 font-medium text-left align-middle text-muted-foreground md:table-cell"
|
|
33
|
+
>
|
|
34
|
+
{{ $t("media.updatedAt") }}
|
|
35
|
+
</th>
|
|
36
|
+
<th></th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody class="[&_tr:last-child]:border-0">
|
|
40
|
+
<tr
|
|
41
|
+
v-for="file in files"
|
|
42
|
+
:key="file.id"
|
|
43
|
+
class="border-b transition-colors hover:bg-blue-200/50 data-[state=selected]:bg-muted cursor-pointer"
|
|
44
|
+
:class="{
|
|
45
|
+
'bg-blue-200': selectedFile?.id === file.id && selectedFile?.type === file.type ||
|
|
46
|
+
(selectedFiles && selectedFiles.some(f => f.id === file.id && f.type === file.type))
|
|
47
|
+
}"
|
|
48
|
+
@click="$emit('file-click', file)"
|
|
49
|
+
>
|
|
50
|
+
<td class="p-4 font-medium align-middle">
|
|
51
|
+
<div class="flex items-center gap-2">
|
|
52
|
+
<IconFolder
|
|
53
|
+
v-if="file.type === 'dir'"
|
|
54
|
+
class="w-5 h-5 text-blue-500"
|
|
55
|
+
/>
|
|
56
|
+
<div
|
|
57
|
+
v-else-if="file?.filetype === 'image' && file?.filepath"
|
|
58
|
+
class="relative w-8 h-8"
|
|
59
|
+
>
|
|
60
|
+
<img
|
|
61
|
+
:src="file?.preview"
|
|
62
|
+
:alt="file?.alt || file?.filename"
|
|
63
|
+
class="object-cover w-full h-full rounded"
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
<IconImage
|
|
67
|
+
v-else-if="file?.filetype === 'image'"
|
|
68
|
+
class="w-5 h-5 text-green-500"
|
|
69
|
+
/>
|
|
70
|
+
<File v-else class="w-5 h-5 text-gray-500" />
|
|
71
|
+
<span class="truncate max-w-[200px]">{{
|
|
72
|
+
file?.filename || file?.name
|
|
73
|
+
}}</span>
|
|
74
|
+
</div>
|
|
75
|
+
</td>
|
|
76
|
+
<td class="p-4 align-middle">
|
|
77
|
+
{{
|
|
78
|
+
file?.type === "dir"
|
|
79
|
+
? $t("media.folder")
|
|
80
|
+
: file?.filetype?.charAt(0)?.toUpperCase() +
|
|
81
|
+
file?.filetype?.slice(1)
|
|
82
|
+
}}
|
|
83
|
+
</td>
|
|
84
|
+
<td class="p-4 align-middle">
|
|
85
|
+
{{ file?.type === "dir" ? "--" : formatFileSize(file?.filesize) }}
|
|
86
|
+
</td>
|
|
87
|
+
<td class="hidden p-4 align-middle md:table-cell">
|
|
88
|
+
{{ file?.updated_at ? formatDate(file?.updated_at) : "--" }}
|
|
89
|
+
</td>
|
|
90
|
+
<td v-if="file.type === 'dir'">
|
|
91
|
+
<Trash2 class="w-4 h-4 text-red-500 cursor-pointer" @click.stop=" openConfirm" />
|
|
92
|
+
</td>
|
|
93
|
+
</tr>
|
|
94
|
+
</tbody>
|
|
95
|
+
</table>
|
|
96
|
+
</div>
|
|
97
|
+
</template>
|
|
98
|
+
|
|
99
|
+
<script setup>
|
|
100
|
+
import IconFolder from "../icons/icon-folder.vue";
|
|
101
|
+
import IconImage from "../icons/icon-image.vue";
|
|
102
|
+
import { formatFileSize, formatDate } from "../../props/media";
|
|
103
|
+
import { Trash2 } from "lucide-vue-next";
|
|
104
|
+
import { ref } from "vue";
|
|
105
|
+
import { confirm } from '@opengis/core';
|
|
106
|
+
import { useI18n } from "vue-i18n";
|
|
107
|
+
const { t } = useI18n();
|
|
108
|
+
|
|
109
|
+
const props = defineProps({
|
|
110
|
+
files: {
|
|
111
|
+
type: Array,
|
|
112
|
+
required: true,
|
|
113
|
+
},
|
|
114
|
+
selectedFile: {
|
|
115
|
+
type: Object,
|
|
116
|
+
default: null,
|
|
117
|
+
},
|
|
118
|
+
selectedFiles: {
|
|
119
|
+
type: Array,
|
|
120
|
+
default: () => [],
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const selectedFile = ref(null);
|
|
125
|
+
|
|
126
|
+
const emit = defineEmits(["file-click", "delete-folder"]);
|
|
127
|
+
|
|
128
|
+
const openConfirm = () => {
|
|
129
|
+
|
|
130
|
+
confirm({
|
|
131
|
+
title: t("builder.deleteTitle"),
|
|
132
|
+
message: t("builder.deleteObject"),
|
|
133
|
+
type: 'error',
|
|
134
|
+
onConfirm: () => {
|
|
135
|
+
handleDeleteFile();
|
|
136
|
+
},
|
|
137
|
+
})
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const handleDeleteFile = () => {
|
|
141
|
+
emit("delete-folder", selectedFile.value);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const handleSelectedFile = (file) => {
|
|
145
|
+
selectedFile.value = file;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
</script>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex items-center gap-3">
|
|
3
|
+
<button
|
|
4
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors disabled:pointer-events-none disabled:opacity-50 border shadow-sm hover:text-accent-foreground h-9 px-4 py-2 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-600"
|
|
5
|
+
@click="$emit('create-folder')"
|
|
6
|
+
>
|
|
7
|
+
<FolderPlus class="w-4 h-4 mr-2" />
|
|
8
|
+
{{ $t("media.newFolder") }}
|
|
9
|
+
</button>
|
|
10
|
+
<button
|
|
11
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white shadow-md hover:shadow-lg transition-all duration-200 transform hover:scale-105"
|
|
12
|
+
@click="$refs.fileInputRef.click()"
|
|
13
|
+
>
|
|
14
|
+
<input
|
|
15
|
+
type="file"
|
|
16
|
+
@change="$emit('file-change', $event)"
|
|
17
|
+
ref="fileInputRef"
|
|
18
|
+
class="hidden"
|
|
19
|
+
multiple
|
|
20
|
+
/>
|
|
21
|
+
<Plus class="w-4 h-4 mr-2" />
|
|
22
|
+
{{ $t("media.uploadMedia") }}
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup>
|
|
28
|
+
import { Plus, FolderPlus } from "lucide-vue-next";
|
|
29
|
+
|
|
30
|
+
defineProps({
|
|
31
|
+
modelValue: {
|
|
32
|
+
type: String,
|
|
33
|
+
required: true,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
defineEmits(["update:modelValue", "create-folder", "file-change"]);
|
|
38
|
+
</script>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors shadow-lg backdrop-blur-sm"
|
|
3
|
+
:class="typeStyles.get(type)"
|
|
4
|
+
>
|
|
5
|
+
{{ type }}
|
|
6
|
+
</div>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
const typeStyles = new Map([
|
|
11
|
+
['document', 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-700'],
|
|
12
|
+
['image', 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-700'],
|
|
13
|
+
['video', 'bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-700'],
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
type: {
|
|
19
|
+
type: String,
|
|
20
|
+
required: true,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
</script>
|