@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
|
+
<input
|
|
3
|
+
type="color"
|
|
4
|
+
v-model="modelValueLocal"
|
|
5
|
+
class="w-8 h-8 p-0 border border-gray-300 rounded cursor-pointer bg-transparent"
|
|
6
|
+
@input="onInput"
|
|
7
|
+
/>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { defineProps, defineEmits, computed } from 'vue';
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
modelValue: string
|
|
15
|
+
}>();
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
(e: 'update:modelValue', value: string): void
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const modelValueLocal = computed({
|
|
21
|
+
get: () => props.modelValue,
|
|
22
|
+
set: (val: string) => emit('update:modelValue', val)
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function onInput(e: Event) {
|
|
26
|
+
const value = (e.target as HTMLInputElement).value;
|
|
27
|
+
emit('update:modelValue', value);
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="p-0 shadow w-full">
|
|
3
|
+
<div class="overflow-x-auto">
|
|
4
|
+
<div class="relative w-full overflow-auto">
|
|
5
|
+
<table class="w-full caption-bottom text-sm">
|
|
6
|
+
<thead class="[&_tr]:border-b">
|
|
7
|
+
<tr
|
|
8
|
+
class="border-b data-[state=selected]:bg-muted bg-slate-50/50 dark:bg-slate-700/50 hover:bg-slate-50/80 dark:hover:bg-slate-700/80 transition-colors"
|
|
9
|
+
>
|
|
10
|
+
<th
|
|
11
|
+
class="h-10 px-2 align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] w-12 text-center"
|
|
12
|
+
></th>
|
|
13
|
+
<th
|
|
14
|
+
v-for="field in colModel"
|
|
15
|
+
:key="field.name"
|
|
16
|
+
class="h-10 px-2 text-left align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] font-semibold text-slate-700 dark:text-slate-300 min-w-[150px]"
|
|
17
|
+
>
|
|
18
|
+
{{ field.label || field.title || field.name }}
|
|
19
|
+
</th>
|
|
20
|
+
|
|
21
|
+
<th
|
|
22
|
+
class="h-10 px-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] font-semibold text-slate-700 dark:text-slate-300 text-end"
|
|
23
|
+
>
|
|
24
|
+
{{ $t('table.actions') }}
|
|
25
|
+
</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody class="[&_tr:last-child]:border-0">
|
|
29
|
+
<tr
|
|
30
|
+
v-for="(item, index) in data"
|
|
31
|
+
:key="item?.id"
|
|
32
|
+
class="border-b data-[state=selected]:bg-muted transition-all duration-200 hover:bg-slate-50/60 dark:hover:bg-slate-700/60 cursor-move"
|
|
33
|
+
:class="{ 'opacity-50': draggedItem === item }"
|
|
34
|
+
draggable="true"
|
|
35
|
+
@dragstart="handleDragStart($event, item, index)"
|
|
36
|
+
@dragover="handleDragOver($event, index)"
|
|
37
|
+
@drop="handleDrop($event, index)"
|
|
38
|
+
@dragenter="handleDragEnter($event, index)"
|
|
39
|
+
@dragleave="handleDragLeave($event)"
|
|
40
|
+
>
|
|
41
|
+
<td
|
|
42
|
+
class="p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-center"
|
|
43
|
+
>
|
|
44
|
+
<div class="flex items-center justify-center text-slate-400">
|
|
45
|
+
<GripVertical />
|
|
46
|
+
</div>
|
|
47
|
+
</td>
|
|
48
|
+
|
|
49
|
+
<td
|
|
50
|
+
v-for="field in colModel"
|
|
51
|
+
:key="field.name"
|
|
52
|
+
class="p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] text-slate-600 dark:text-slate-400 text-sm"
|
|
53
|
+
>
|
|
54
|
+
<span v-if="field.type === 'Texteditor'" v-html="item[field.key] || '-'"></span>
|
|
55
|
+
<div v-else-if="field.type === 'File'" class="flex items-center gap-2">
|
|
56
|
+
<div v-if="Array.isArray(item[field.key]) && item[field.key].length > 0" class="flex items-center gap-1">
|
|
57
|
+
<div
|
|
58
|
+
v-for="(file, fileIndex) in getDisplayFiles(item[field.key])"
|
|
59
|
+
:key="fileIndex"
|
|
60
|
+
class="relative w-8 h-8 rounded overflow-hidden bg-gray-100 dark:bg-gray-700 flex-shrink-0"
|
|
61
|
+
>
|
|
62
|
+
<img
|
|
63
|
+
:src="file"
|
|
64
|
+
:alt="`Image ${fileIndex + 1}`"
|
|
65
|
+
class="w-full h-full object-cover"
|
|
66
|
+
@error="handleImageError($event)"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<span v-if="getRemainingFilesCount(item[field.key]) > 0" class="text-xs text-gray-500 dark:text-gray-400 ml-1">
|
|
70
|
+
+{{ getRemainingFilesCount(item[field.key]) }}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div v-else class="w-8 h-8 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
|
74
|
+
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
75
|
+
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd" />
|
|
76
|
+
</svg>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
<span v-else>{{ item[field.key] || "-" }}</span>
|
|
80
|
+
</td>
|
|
81
|
+
|
|
82
|
+
<td
|
|
83
|
+
class="p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"
|
|
84
|
+
>
|
|
85
|
+
<VsFormCustomDatatableControls
|
|
86
|
+
:item="item"
|
|
87
|
+
:colModel="colModel"
|
|
88
|
+
v-model="data"
|
|
89
|
+
/>
|
|
90
|
+
</td>
|
|
91
|
+
</tr>
|
|
92
|
+
|
|
93
|
+
<!-- Drop zone indicator -->
|
|
94
|
+
<tr
|
|
95
|
+
v-if="showDropZone"
|
|
96
|
+
class="h-2 bg-blue-200 dark:bg-blue-800 transition-all duration-200"
|
|
97
|
+
></tr>
|
|
98
|
+
</tbody>
|
|
99
|
+
</table>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<VsFormCustomDatatableAdd :colModel="colModel" v-model="data" />
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
105
|
+
|
|
106
|
+
<script setup>
|
|
107
|
+
import { GripVertical } from "lucide-vue-next";
|
|
108
|
+
import VsFormCustomDatatableControls from "./custom-datatable/vs-form-custom-datatable-controls.vue";
|
|
109
|
+
import VsFormCustomDatatableAdd from "./custom-datatable/vs-form-custom-datatable-add.vue";
|
|
110
|
+
import { onMounted, ref } from "vue";
|
|
111
|
+
|
|
112
|
+
const props = defineProps({
|
|
113
|
+
colModel: {
|
|
114
|
+
type: Array,
|
|
115
|
+
required: true,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const data = defineModel({
|
|
120
|
+
type: Array,
|
|
121
|
+
required: true,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Drag and drop state
|
|
125
|
+
const draggedItem = ref(null);
|
|
126
|
+
const draggedIndex = ref(-1);
|
|
127
|
+
const showDropZone = ref(false);
|
|
128
|
+
|
|
129
|
+
// Drag and drop handlers
|
|
130
|
+
const handleDragStart = (event, item, index) => {
|
|
131
|
+
draggedItem.value = item;
|
|
132
|
+
draggedIndex.value = index;
|
|
133
|
+
event.dataTransfer.effectAllowed = "move";
|
|
134
|
+
event.dataTransfer.setData("text/html", ""); // Required for Firefox
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const handleDragOver = (event, index) => {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
event.dataTransfer.dropEffect = "move";
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const handleDragEnter = (event, index) => {
|
|
143
|
+
if (draggedIndex.value !== index) {
|
|
144
|
+
showDropZone.value = true;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handleDragLeave = (event) => {
|
|
149
|
+
// Only hide drop zone if we're leaving the table entirely
|
|
150
|
+
if (!event.currentTarget.contains(event.relatedTarget)) {
|
|
151
|
+
showDropZone.value = false;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const handleDrop = (event, dropIndex) => {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
showDropZone.value = false;
|
|
158
|
+
|
|
159
|
+
if (draggedIndex.value === -1 || draggedIndex.value === dropIndex) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Reorder the array
|
|
164
|
+
const newData = [...data.value];
|
|
165
|
+
const [removed] = newData.splice(draggedIndex.value, 1);
|
|
166
|
+
newData.splice(dropIndex, 0, removed);
|
|
167
|
+
|
|
168
|
+
// Update the model
|
|
169
|
+
data.value = newData;
|
|
170
|
+
|
|
171
|
+
// Reset drag state
|
|
172
|
+
draggedItem.value = null;
|
|
173
|
+
draggedIndex.value = -1;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Constants for file display
|
|
177
|
+
const MAX_DISPLAY_FILES = 3;
|
|
178
|
+
|
|
179
|
+
// Get files to display (limited to MAX_DISPLAY_FILES)
|
|
180
|
+
const getDisplayFiles = (files) => {
|
|
181
|
+
if (!Array.isArray(files)) return [];
|
|
182
|
+
return files.slice(0, MAX_DISPLAY_FILES);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Get count of remaining files not displayed
|
|
186
|
+
const getRemainingFilesCount = (files) => {
|
|
187
|
+
if (!Array.isArray(files)) return 0;
|
|
188
|
+
return Math.max(0, files.length - MAX_DISPLAY_FILES);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Handle image loading errors
|
|
192
|
+
const handleImageError = (event) => {
|
|
193
|
+
const img = event.target;
|
|
194
|
+
img.style.display = 'none';
|
|
195
|
+
const placeholder = document.createElement('div');
|
|
196
|
+
placeholder.className = 'w-full h-full flex items-center justify-center bg-gray-200 dark:bg-gray-600';
|
|
197
|
+
placeholder.innerHTML = `
|
|
198
|
+
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
199
|
+
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd" />
|
|
200
|
+
</svg>
|
|
201
|
+
`;
|
|
202
|
+
img.parentNode.appendChild(placeholder);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
onMounted(() => {
|
|
206
|
+
data.value
|
|
207
|
+
?.filter((el) => typeof el === "object")
|
|
208
|
+
?.forEach((item) =>
|
|
209
|
+
item.id
|
|
210
|
+
? item.id
|
|
211
|
+
: (item.id = Math.random().toString(36).substring(2, 15))
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
</script>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="p-0">
|
|
3
|
+
<slot name="label" v-bind="{ id }"></slot>
|
|
4
|
+
<input
|
|
5
|
+
v-model="inputValue"
|
|
6
|
+
type="number"
|
|
7
|
+
@keydown="validateInput"
|
|
8
|
+
@blur="handleBlur"
|
|
9
|
+
:step="step"
|
|
10
|
+
:min="min"
|
|
11
|
+
:max="max"
|
|
12
|
+
:id="id"
|
|
13
|
+
aria-describedby="helper-text-explanation"
|
|
14
|
+
class="vs-number__input py-2 px-3 block w-full h-[38px] border border-solid border-stone-200 rounded-lg text-sm text-stone-800 placeholder:text-stone-500 focus:z-10 focus:border-1 focus:outline-none focus:border-blue-600 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-200 dark:placeholder:text-neutral-500 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-neutral-600"
|
|
15
|
+
:placeholder="placeholder"
|
|
16
|
+
:disabled="disabled"
|
|
17
|
+
:class="{ 'cursor-not-allowed': disabled }"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
<script>
|
|
22
|
+
export default {
|
|
23
|
+
props: {
|
|
24
|
+
min: { type: Number },
|
|
25
|
+
step: { type: Number, default: () => 1 },
|
|
26
|
+
max: { type: Number, default: () => Infinity },
|
|
27
|
+
modelValue: { type: Number, default: () => null },
|
|
28
|
+
label: { type: String, default: () => "" },
|
|
29
|
+
placeholder: { type: String, default: () => "0" },
|
|
30
|
+
disabled: { type: Boolean, default: () => false },
|
|
31
|
+
},
|
|
32
|
+
data() {
|
|
33
|
+
return {
|
|
34
|
+
id: `input-number-${Math.floor(Math.random() * 1000)}`,
|
|
35
|
+
inputValue: this.modelValue,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
watch: {
|
|
39
|
+
modelValue(newVal) {
|
|
40
|
+
this.inputValue = newVal;
|
|
41
|
+
},
|
|
42
|
+
inputValue(newVal) {
|
|
43
|
+
this.$emit("update:modelValue", newVal);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
methods: {
|
|
47
|
+
validateInput(event) {
|
|
48
|
+
if (
|
|
49
|
+
event.key === "+" ||
|
|
50
|
+
event.key === "," ||
|
|
51
|
+
event.key === "." ||
|
|
52
|
+
(event.key === "-" &&
|
|
53
|
+
this.inputValue &&
|
|
54
|
+
this.inputValue.toString().length > 0)
|
|
55
|
+
) {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
handleBlur() {
|
|
60
|
+
let value = parseFloat(this.inputValue);
|
|
61
|
+
|
|
62
|
+
this.$emit("blur");
|
|
63
|
+
|
|
64
|
+
if (isNaN(value)) {
|
|
65
|
+
value = this.min;
|
|
66
|
+
} else if (value > this.max) {
|
|
67
|
+
value = this.max;
|
|
68
|
+
} else if (value < this.min) {
|
|
69
|
+
value = this.min;
|
|
70
|
+
}
|
|
71
|
+
this.inputValue = value;
|
|
72
|
+
this.$emit("update:modelValue", this.inputValue);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.col-error .vs-number__input {
|
|
80
|
+
border: 1px solid red;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.vs-number__input::placeholder {
|
|
84
|
+
opacity: 0.5;
|
|
85
|
+
}
|
|
86
|
+
</style>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full">
|
|
3
|
+
<div ref="sortableContainer" class="flex flex-col gap-1">
|
|
4
|
+
<div
|
|
5
|
+
v-for="item in value"
|
|
6
|
+
:key="item?.id"
|
|
7
|
+
class="flex items-center gap-2 w-full"
|
|
8
|
+
>
|
|
9
|
+
<button class="drag-handle cursor-move">
|
|
10
|
+
<svg
|
|
11
|
+
fill="#000000"
|
|
12
|
+
width="20"
|
|
13
|
+
height="20"
|
|
14
|
+
viewBox="0 0 36 36"
|
|
15
|
+
version="1.1"
|
|
16
|
+
preserveAspectRatio="xMidYMid meet"
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
19
|
+
>
|
|
20
|
+
<title>drag-handle-line</title>
|
|
21
|
+
<circle
|
|
22
|
+
cx="15"
|
|
23
|
+
cy="12"
|
|
24
|
+
r="1.5"
|
|
25
|
+
class="clr-i-outline clr-i-outline-path-1"
|
|
26
|
+
></circle>
|
|
27
|
+
<circle
|
|
28
|
+
cx="15"
|
|
29
|
+
cy="24"
|
|
30
|
+
r="1.5"
|
|
31
|
+
class="clr-i-outline clr-i-outline-path-2"
|
|
32
|
+
></circle>
|
|
33
|
+
<circle
|
|
34
|
+
cx="21"
|
|
35
|
+
cy="12"
|
|
36
|
+
r="1.5"
|
|
37
|
+
class="clr-i-outline clr-i-outline-path-3"
|
|
38
|
+
></circle>
|
|
39
|
+
<circle
|
|
40
|
+
cx="21"
|
|
41
|
+
cy="24"
|
|
42
|
+
r="1.5"
|
|
43
|
+
class="clr-i-outline clr-i-outline-path-4"
|
|
44
|
+
></circle>
|
|
45
|
+
<circle
|
|
46
|
+
cx="21"
|
|
47
|
+
cy="18"
|
|
48
|
+
r="1.5"
|
|
49
|
+
class="clr-i-outline clr-i-outline-path-5"
|
|
50
|
+
></circle>
|
|
51
|
+
<circle
|
|
52
|
+
cx="15"
|
|
53
|
+
cy="18"
|
|
54
|
+
r="1.5"
|
|
55
|
+
class="clr-i-outline clr-i-outline-path-6"
|
|
56
|
+
></circle>
|
|
57
|
+
<rect x="0" y="0" width="36" height="36" fill-opacity="0" />
|
|
58
|
+
</svg>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<!-- Key field -->
|
|
62
|
+
<div class="w-1/3">
|
|
63
|
+
<VsText v-model="item.key" type="text" :placeholder="t('form.key')" />
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Value field -->
|
|
67
|
+
<div class="w-2/3">
|
|
68
|
+
<VsText v-model="item.value" type="text" :placeholder="t('form.value')" />
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<button
|
|
72
|
+
@click="deleteItem(item?.id)"
|
|
73
|
+
class="shrink-0 w-[30px] text-gray-600 h-[30px] rounded-full flex items-center justify-center bg-gray-100 duration-300 hover:bg-gray-200"
|
|
74
|
+
>
|
|
75
|
+
<X height="16" width="16" />
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<span v-if="!value.length" class="text-[14px] text-gray-700"
|
|
81
|
+
>{{ t('form.noDataToDisplay') }}</span
|
|
82
|
+
>
|
|
83
|
+
<div class="mt-2 w-full flex items-center">
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
@click="addItem"
|
|
87
|
+
:disabled="disabled"
|
|
88
|
+
class="py-1.5 px-2 flex items-center gap-x-1 text-xs font-medium rounded-full border border-dashed border-gray-200 bg-white text-gray-800 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-hidden focus:bg-gray-50 dark:bg-neutral-800 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:focus:bg-neutral-700"
|
|
89
|
+
>
|
|
90
|
+
<Plus height="14" width="14" /> {{ t('form.add') }}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script setup>
|
|
97
|
+
import { computed, ref, watch, onMounted, inject } from "vue";
|
|
98
|
+
import { Plus, X } from "lucide-vue-next";
|
|
99
|
+
import Sortable from "sortablejs";
|
|
100
|
+
import { useI18n } from 'vue-i18n';
|
|
101
|
+
const { t } = useI18n();
|
|
102
|
+
|
|
103
|
+
const getRandomId = (prefix) => {
|
|
104
|
+
return `${prefix}-${Math.random().toString(36).substring(2, 15)}`;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const props = defineProps({
|
|
108
|
+
colModel: { type: Object, default: () => ({ type: "Text" }) },
|
|
109
|
+
ignore: { type: Array, default: () => [] },
|
|
110
|
+
addButtonText: { type: String, default: () => "Додати" },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const metaParentValue = inject("metaParentValue");
|
|
114
|
+
|
|
115
|
+
// Convert object to array of key-value pairs, исключая игнорируемые ключи
|
|
116
|
+
const value = ref(
|
|
117
|
+
Object.entries(metaParentValue.value || {})
|
|
118
|
+
.filter(([key]) => !props.ignore.includes(key))
|
|
119
|
+
.map(([key, val]) => ({
|
|
120
|
+
id: getRandomId("key-value-"),
|
|
121
|
+
key: key,
|
|
122
|
+
value: val,
|
|
123
|
+
}))
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const sortableContainer = ref(null);
|
|
127
|
+
|
|
128
|
+
onMounted(() => {
|
|
129
|
+
if (sortableContainer.value) {
|
|
130
|
+
new Sortable(sortableContainer.value, {
|
|
131
|
+
handle: ".drag-handle",
|
|
132
|
+
animation: 150,
|
|
133
|
+
ghostClass: "sortable-ghost",
|
|
134
|
+
onEnd: ({ oldIndex, newIndex }) => {
|
|
135
|
+
if (oldIndex !== newIndex) {
|
|
136
|
+
const item = value.value.splice(oldIndex, 1)[0];
|
|
137
|
+
value.value.splice(newIndex, 0, item);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const disabled = computed(() => {
|
|
145
|
+
// Отключаем кнопку только если есть незавершенные записи
|
|
146
|
+
return value.value?.some(
|
|
147
|
+
(i) => (i?.key && !i?.value) || (!i?.key && i?.value)
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const addItem = () => {
|
|
152
|
+
value.value.push({ id: getRandomId("key-value-"), key: "", value: "" });
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const deleteItem = (id) => {
|
|
156
|
+
value.value = value.value?.filter((i) => i?.id !== id);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
watch(
|
|
160
|
+
value,
|
|
161
|
+
(n) => {
|
|
162
|
+
// Convert array back to object
|
|
163
|
+
const result = {};
|
|
164
|
+
n.forEach((item) => {
|
|
165
|
+
if (item.key && item.value !== undefined) {
|
|
166
|
+
result[item.key] = item.value;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const tempValue = {};
|
|
170
|
+
if (props.ignore.length) {
|
|
171
|
+
props.ignore.forEach((item) => {
|
|
172
|
+
tempValue[item] = metaParentValue.value[item];
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Сохраняем игнорируемые поля и добавляем новые/измененные
|
|
177
|
+
metaParentValue.value = {
|
|
178
|
+
...tempValue,
|
|
179
|
+
...result,
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
{ deep: true }
|
|
183
|
+
);
|
|
184
|
+
</script>
|
|
185
|
+
|
|
186
|
+
<style scoped>
|
|
187
|
+
.list-enter-active,
|
|
188
|
+
.list-leave-active {
|
|
189
|
+
transition: all 0.3s ease;
|
|
190
|
+
}
|
|
191
|
+
.list-enter-from,
|
|
192
|
+
.list-leave-to {
|
|
193
|
+
opacity: 0;
|
|
194
|
+
transform: translateY(10px);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.sortable-ghost {
|
|
198
|
+
opacity: 0.5;
|
|
199
|
+
background: #f0f0f0;
|
|
200
|
+
}
|
|
201
|
+
</style>
|