@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,484 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-6 max-w-6xl mx-auto relative bg-white">
|
|
3
|
+
<div class="p-4 sticky top-[-24px] z-10 bg-white dark:bg-slate-800">
|
|
4
|
+
<CollectionsBreadcrumb
|
|
5
|
+
:items="breadcrumbItems"
|
|
6
|
+
@navigate="handleBreadcrumbNavigate"
|
|
7
|
+
/>
|
|
8
|
+
<div class="flex items-center justify-between">
|
|
9
|
+
<div class="flex items-center gap-4">
|
|
10
|
+
<button
|
|
11
|
+
@click="router.back()"
|
|
12
|
+
class="p-2 text-gray-500 rounded-full hover:text-gray-900 dark:text-gray-400 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
13
|
+
>
|
|
14
|
+
<ArrowLeft class="w-5 h-5" />
|
|
15
|
+
</button>
|
|
16
|
+
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
17
|
+
{{ $t("builder.editCollection") }}
|
|
18
|
+
<span >"{{
|
|
19
|
+
articleData?.type === "single"
|
|
20
|
+
? articleData.title
|
|
21
|
+
: menu.value?.find((item) => item.id === (route.params.collection || route.params.id))?.title
|
|
22
|
+
}}"</span>
|
|
23
|
+
</h1>
|
|
24
|
+
<a :href="`https://cms.opengis.info/${locale}/guides/content/`" target="_blank" :title="$t('guide.content')">
|
|
25
|
+
<HelpCircle class="w-5 h-5" />
|
|
26
|
+
</a>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="flex items-center space-x-3">
|
|
29
|
+
<button
|
|
30
|
+
@click="handleSubmit"
|
|
31
|
+
type="submit"
|
|
32
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-9 px-4 py-2 bg-blue-600 text-white shadow-md transition-all duration-200 transform
|
|
33
|
+
hover:bg-blue-700 hover:shadow-lg hover:scale-105
|
|
34
|
+
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
|
35
|
+
disabled:pointer-events-none disabled:opacity-50"
|
|
36
|
+
>
|
|
37
|
+
<Save class="w-4 h-4 mr-2" />
|
|
38
|
+
{{ $t("builder.save") }}
|
|
39
|
+
</button>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="w-full mt-6 flex items-center justify-between">
|
|
43
|
+
<div
|
|
44
|
+
class="flex items-center w-full h-10 max-w-md grid-cols-3 p-1 bg-white border border-gray-200 rounded-md text-muted-foreground"
|
|
45
|
+
>
|
|
46
|
+
<button
|
|
47
|
+
v-for="tab in tabsList"
|
|
48
|
+
:key="tab.id"
|
|
49
|
+
@click="activeTab = tab.id"
|
|
50
|
+
class="inline-flex flex-1 items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm data-[state=active]:bg-blue-50 data-[state=active]:text-blue-700 data-[state=active]:border-blue-200"
|
|
51
|
+
:class="
|
|
52
|
+
tab.id === activeTab
|
|
53
|
+
? 'bg-blue-50 text-blue-700 border-blue-200'
|
|
54
|
+
: ''
|
|
55
|
+
"
|
|
56
|
+
>
|
|
57
|
+
{{ tab.name }}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="flex items-center gap-2">
|
|
61
|
+
<label for="is_pin" class="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">{{$t('collections.pinPublication')}}</label>
|
|
62
|
+
<div class="relative">
|
|
63
|
+
<input
|
|
64
|
+
type="checkbox"
|
|
65
|
+
id="is_pin"
|
|
66
|
+
v-model="isPinModel"
|
|
67
|
+
class="sr-only"
|
|
68
|
+
/>
|
|
69
|
+
<div
|
|
70
|
+
class="w-5 h-5 border-2 rounded transition-all duration-200 cursor-pointer flex items-center justify-center"
|
|
71
|
+
:class="articleData?.is_pin
|
|
72
|
+
? 'bg-sky-600 border-sky-600'
|
|
73
|
+
: 'bg-white border-gray-300 hover:border-sky-400 dark:bg-gray-700 dark:border-gray-600 dark:hover:border-sky-400'"
|
|
74
|
+
@click="togglePin"
|
|
75
|
+
>
|
|
76
|
+
<svg
|
|
77
|
+
v-if="articleData?.is_pin"
|
|
78
|
+
class="w-3 h-3 text-white"
|
|
79
|
+
fill="currentColor"
|
|
80
|
+
viewBox="0 0 20 20"
|
|
81
|
+
>
|
|
82
|
+
<path
|
|
83
|
+
fill-rule="evenodd"
|
|
84
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
85
|
+
clip-rule="evenodd"
|
|
86
|
+
/>
|
|
87
|
+
</svg>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<!-- <formWrapper
|
|
94
|
+
:columns="columns"
|
|
95
|
+
:initial-data="articleData"
|
|
96
|
+
@submit="handleSubmit"
|
|
97
|
+
@update="articleData = $event"
|
|
98
|
+
/> -->
|
|
99
|
+
<div
|
|
100
|
+
v-show="activeTab === 'content'"
|
|
101
|
+
class="container mx-auto bg-white rounded-md pb-4 px-5"
|
|
102
|
+
>
|
|
103
|
+
<VForm v-if="columns?.length" v-model="articleData" :schema="schema" v-model:form="formArticleData"/>
|
|
104
|
+
</div>
|
|
105
|
+
<div
|
|
106
|
+
v-show="activeTab === 'seo'"
|
|
107
|
+
class="container mx-auto bg-white rounded-md pb-4 px-2"
|
|
108
|
+
>
|
|
109
|
+
<VForm
|
|
110
|
+
:schema="seoSchema"
|
|
111
|
+
v-model:values="seoData"
|
|
112
|
+
v-model:form="formSeoData"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
<div
|
|
116
|
+
v-if="activeTab === 'tags'"
|
|
117
|
+
class="container mx-auto bg-white rounded-md pb-4 px-2"
|
|
118
|
+
>
|
|
119
|
+
<VsFormTags ref="tagsForm" v-model="articleData" />
|
|
120
|
+
</div>
|
|
121
|
+
<div
|
|
122
|
+
v-show="activeTab === 'preview'"
|
|
123
|
+
class="container mx-auto bg-white rounded-md pb-4 px-2"
|
|
124
|
+
>
|
|
125
|
+
<VsPreview :slug="articleData?.slug" :preview_path="preview_path"/>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</template>
|
|
129
|
+
|
|
130
|
+
<script setup lang="ts">
|
|
131
|
+
import seoSchemaBase from './schema/seo'
|
|
132
|
+
import { ref, onMounted, computed, inject, getCurrentInstance, watch, nextTick, type Ref } from "vue";
|
|
133
|
+
import { useRouter, useRoute } from "vue-router";
|
|
134
|
+
import { ArrowLeft, Save, HelpCircle } from "lucide-vue-next";
|
|
135
|
+
// @ts-ignore
|
|
136
|
+
import VsReference from '@/components/form-components/vs-form-reference-list.vue'
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
import VsTiptap from '@/components/form-components/vs-form-tiptap.vue'
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
import VsMediaSelect from '@/components/form-components/vs-form-media-select.vue'
|
|
141
|
+
// @ts-ignore
|
|
142
|
+
import VsRichtextMd from '@/components/form-components/vs-richtext-md.vue'
|
|
143
|
+
// @ts-ignore
|
|
144
|
+
import VsInteger from '@/components/form-components/vs-form-integer.vue'
|
|
145
|
+
// @ts-ignore
|
|
146
|
+
import VsDatatableForm from '@/components/form-components/vs-form-custom-datatable.vue'
|
|
147
|
+
// @ts-ignore
|
|
148
|
+
import VsRelationLink from '@/components/form-components/vs-form-reletion-link.vue'
|
|
149
|
+
import { useI18n } from "vue-i18n";
|
|
150
|
+
// import formWrapper from "../../components/collections/formWrapper.vue";
|
|
151
|
+
import { getField } from "../../utils/getField";
|
|
152
|
+
import { VForm, inputs } from '@opengis/form'
|
|
153
|
+
|
|
154
|
+
//import { VForm, inputs } from '../../../../form/src/index'
|
|
155
|
+
|
|
156
|
+
// inputs['vs-input-slug'] = inputs.VsInputText
|
|
157
|
+
// inputs['vs-input-key-value'] = inputs.VsInputText
|
|
158
|
+
inputs['vs-input-custom-datatable'] = VsDatatableForm;
|
|
159
|
+
inputs['vs-input-reference'] = VsReference;
|
|
160
|
+
inputs['vs-input-texteditor'] = VsTiptap;
|
|
161
|
+
inputs['vs-input-mediaselect'] = VsMediaSelect;
|
|
162
|
+
inputs['vs-input-file'] = VsMediaSelect;
|
|
163
|
+
inputs['vs-input-image'] = VsMediaSelect;
|
|
164
|
+
inputs['vs-input-richtext-md'] = VsRichtextMd;
|
|
165
|
+
inputs['vs-input-integer'] = VsInteger;
|
|
166
|
+
inputs['vs-input-relation-link'] = VsRelationLink;
|
|
167
|
+
|
|
168
|
+
import VsFormTags from "../../components/form-components/VsFormTags.vue";
|
|
169
|
+
import VsPreview from "../../components/ui/VsPreview.vue";
|
|
170
|
+
import CollectionsBreadcrumb from "../../components/collections/CollectionsBreadcrumb.vue";
|
|
171
|
+
import { notify } from '@opengis/core'
|
|
172
|
+
const { t, locale } = useI18n();
|
|
173
|
+
const router = useRouter();
|
|
174
|
+
const route = useRoute();
|
|
175
|
+
const menu = inject<Ref<any[]>>("menu", ref([]));
|
|
176
|
+
const columns = ref<any[]>([]);
|
|
177
|
+
const formArticleData = ref<any>({});
|
|
178
|
+
const formSeoData = ref<any>({});
|
|
179
|
+
const tagsForm = ref<any>({});
|
|
180
|
+
const preview_path = ref<string>("");
|
|
181
|
+
|
|
182
|
+
const createLocalizedColumns = (columns: any[]) => {
|
|
183
|
+
const localizedColumns: any[] = [];
|
|
184
|
+
|
|
185
|
+
columns.forEach((item) => {
|
|
186
|
+
// Add original column
|
|
187
|
+
localizedColumns.push(item);
|
|
188
|
+
|
|
189
|
+
// If column has localization, create a copy with English suffix
|
|
190
|
+
if (item.localization) {
|
|
191
|
+
const localizedItem = {
|
|
192
|
+
...item,
|
|
193
|
+
name: item.name + ":en",
|
|
194
|
+
label: item.label + " (en)",
|
|
195
|
+
};
|
|
196
|
+
localizedColumns.push(localizedItem);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return localizedColumns;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const schema = computed(() => columns.value?.map(getField));
|
|
204
|
+
|
|
205
|
+
const seoSchema = computed(() => {
|
|
206
|
+
// Додаємо locale як залежність, щоб computed був реактивним до змін локалі
|
|
207
|
+
locale.value;
|
|
208
|
+
return seoSchemaBase.flatMap((field) => {
|
|
209
|
+
const baseField = {
|
|
210
|
+
...field,
|
|
211
|
+
label: t(field.label),
|
|
212
|
+
};
|
|
213
|
+
if (field.localization) {
|
|
214
|
+
const localizedField = {
|
|
215
|
+
...field,
|
|
216
|
+
key: `${field.key}:en`,
|
|
217
|
+
label: t(field.label) + ' (en)',
|
|
218
|
+
};
|
|
219
|
+
return [baseField, localizedField];
|
|
220
|
+
}
|
|
221
|
+
return [baseField];
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const seoData = ref<any>({});
|
|
226
|
+
|
|
227
|
+
const articleData = ref<Record<string, any>>({
|
|
228
|
+
title: "",
|
|
229
|
+
status: "draft" as const,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const tabsList = computed(() => {
|
|
233
|
+
const tabs = [
|
|
234
|
+
{ id: "content", name: t("builder.content") },
|
|
235
|
+
{ id: "seo", name: t("builder.seo") },
|
|
236
|
+
{ id: "tags", name: t("builder.tags") },
|
|
237
|
+
|
|
238
|
+
];
|
|
239
|
+
if (preview_path.value) {
|
|
240
|
+
tabs.push({ id: "preview", name: t("builder.preview") });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
return tabs;
|
|
245
|
+
});
|
|
246
|
+
const activeTab = ref("content");
|
|
247
|
+
|
|
248
|
+
const collectionTitle = computed(() => {
|
|
249
|
+
const collection = (route.params?.collection || route.params?.id) as string;
|
|
250
|
+
return menu.value?.find((item: any) => item.id === collection)?.title || collection;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const articleTitle = computed(() => {
|
|
254
|
+
return articleData.value?.title || (route.params?.id ? t("builder.editCollection") : t("articles.createArticle"));
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const breadcrumbItems = computed(() => {
|
|
258
|
+
const collection = (route.params?.collection || route.params?.id) as string;
|
|
259
|
+
const id = route.params?.id as string;
|
|
260
|
+
const items = [
|
|
261
|
+
{
|
|
262
|
+
label: collectionTitle.value,
|
|
263
|
+
route: `collections/${collection}`,
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
if (id) {
|
|
268
|
+
items.push({
|
|
269
|
+
label: articleTitle.value,
|
|
270
|
+
route: `collections/${collection}/${id}`,
|
|
271
|
+
});
|
|
272
|
+
} else {
|
|
273
|
+
items.push({
|
|
274
|
+
label: t("articles.createArticle"),
|
|
275
|
+
route: `collections/${collection}/create`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return items;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const handleBreadcrumbNavigate = (routePath: string) => {
|
|
283
|
+
if (routePath === 'collections') {
|
|
284
|
+
router.push('/collections');
|
|
285
|
+
} else {
|
|
286
|
+
router.push(`/${routePath}`);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
onMounted(async () => {
|
|
291
|
+
// Визначаємо collection та id:
|
|
292
|
+
// - Якщо є route.params.collection, використовуємо його як collection
|
|
293
|
+
// - Якщо є route.params.id і немає route.params.collection, то route.params.id - це collection (стара структура)
|
|
294
|
+
// - Якщо є обидва параметри (collection та id), то id - це ідентифікатор статті
|
|
295
|
+
const collectionParam = route.params?.collection as string | undefined;
|
|
296
|
+
const idParam = route.params?.id as string | undefined;
|
|
297
|
+
|
|
298
|
+
// Якщо є collection, використовуємо його, інакше використовуємо id (якщо це не сторінка редагування)
|
|
299
|
+
const collection = collectionParam || idParam as string;
|
|
300
|
+
// id статті існує тільки якщо є обидва параметри (collection та id)
|
|
301
|
+
const id = collectionParam && idParam ? idParam : undefined;
|
|
302
|
+
|
|
303
|
+
if (!id) {
|
|
304
|
+
if (!collection) return;
|
|
305
|
+
const res = await fetch(`/api/cms/${collection}/`);
|
|
306
|
+
const data = await res.json();
|
|
307
|
+
|
|
308
|
+
columns.value = createLocalizedColumns(data.columns);
|
|
309
|
+
// let rows = data.rows;
|
|
310
|
+
// if (!Array.isArray(rows)) {
|
|
311
|
+
// rows = [rows];
|
|
312
|
+
// }
|
|
313
|
+
// articleData.value = rows[0];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
if (!id || !collection) return;
|
|
318
|
+
const res = await fetch(`/api/cms/${collection}/${id}`);
|
|
319
|
+
if (!res.ok) throw new Error("Failed to fetch article");
|
|
320
|
+
const data = await res.json();
|
|
321
|
+
|
|
322
|
+
columns.value = createLocalizedColumns(data?.columns);
|
|
323
|
+
if(data?.preview_path) preview_path.value = data?.preview_path;
|
|
324
|
+
|
|
325
|
+
if (data.rows) {
|
|
326
|
+
let rows = data.rows;
|
|
327
|
+
if (!Array.isArray(rows)) {
|
|
328
|
+
rows = [rows];
|
|
329
|
+
}
|
|
330
|
+
articleData.value = rows[0];
|
|
331
|
+
|
|
332
|
+
// Ініціалізуємо seoData з даних статті (з meta об'єкта)
|
|
333
|
+
await nextTick();
|
|
334
|
+
if (rows[0]?.meta) {
|
|
335
|
+
seoData.value = {
|
|
336
|
+
...rows[0].meta,
|
|
337
|
+
};
|
|
338
|
+
} else {
|
|
339
|
+
seoData.value = {
|
|
340
|
+
title: '',
|
|
341
|
+
description: '',
|
|
342
|
+
keywords: '',
|
|
343
|
+
meta: {},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
console.log('Initialized seoData from rows[0]:', seoData.value);
|
|
347
|
+
} else {
|
|
348
|
+
articleData.value = data;
|
|
349
|
+
|
|
350
|
+
// Ініціалізуємо seoData з даних статті (з meta об'єкта)
|
|
351
|
+
await nextTick();
|
|
352
|
+
if (data?.meta) {
|
|
353
|
+
seoData.value = {
|
|
354
|
+
title: data.meta.title || '',
|
|
355
|
+
description: data.meta.description || '',
|
|
356
|
+
keywords: data.meta.keywords || '',
|
|
357
|
+
meta: { ...data.meta },
|
|
358
|
+
};
|
|
359
|
+
// Видаляємо title, description, keywords з meta, оскільки вони окремо
|
|
360
|
+
delete seoData.value.meta.title;
|
|
361
|
+
delete seoData.value.meta.description;
|
|
362
|
+
delete seoData.value.meta.keywords;
|
|
363
|
+
} else {
|
|
364
|
+
seoData.value = {
|
|
365
|
+
title: '',
|
|
366
|
+
description: '',
|
|
367
|
+
keywords: '',
|
|
368
|
+
meta: {},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
console.log('Initialized seoData from data:', seoData.value);
|
|
372
|
+
}
|
|
373
|
+
} catch (e) {
|
|
374
|
+
console.error("Error fetching article:", e);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const isPinModel = computed({
|
|
379
|
+
get: () => articleData.value?.is_pin || false,
|
|
380
|
+
set: (value: boolean) => {
|
|
381
|
+
if (articleData.value) {
|
|
382
|
+
articleData.value.is_pin = value;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const togglePin = () => {
|
|
388
|
+
if (articleData.value) {
|
|
389
|
+
articleData.value.is_pin = !articleData.value.is_pin;
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Відстежуємо зміни в seoData для дебагу
|
|
394
|
+
watch(seoData, (newVal) => {
|
|
395
|
+
console.log('seoData changed:', newVal);
|
|
396
|
+
}, { deep: true });
|
|
397
|
+
|
|
398
|
+
const handleSubmit = async (data: any) => {
|
|
399
|
+
const collection = route.params?.collection as string;
|
|
400
|
+
const id = route.params?.id as string;
|
|
401
|
+
|
|
402
|
+
const error = await formArticleData.value.validate();
|
|
403
|
+
if (error) {
|
|
404
|
+
notify({title: t("validation"), message: JSON.stringify(error), type: "warning",});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Відновлюємо об'єкти тегів перед відправкою
|
|
409
|
+
if (tagsForm.value?.restoreTagsBeforeSubmit) {
|
|
410
|
+
await tagsForm.value.restoreTagsBeforeSubmit();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Об'єднуємо articleData з seoData перед відправкою
|
|
414
|
+
// SEO дані мають бути в meta об'єкті
|
|
415
|
+
// Беремо актуальні дані з seoData (який оновлюється формою через v-model)
|
|
416
|
+
const currentSeoData = seoData.value || {};
|
|
417
|
+
const payload = {
|
|
418
|
+
...articleData.value,
|
|
419
|
+
meta: {
|
|
420
|
+
... currentSeoData,
|
|
421
|
+
...(currentSeoData.meta || {}),
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
console.log('Payload before submit:', payload);
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const response = await fetch(`/api/cms/${collection}/${id?id:''}`, {
|
|
429
|
+
method: id? "PUT":'POST',
|
|
430
|
+
headers: {
|
|
431
|
+
"Content-Type": "application/json",
|
|
432
|
+
},
|
|
433
|
+
body: JSON.stringify(payload),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
if (!response.ok) {
|
|
437
|
+
// Пытаемся получить текст ответа
|
|
438
|
+
let errorText;
|
|
439
|
+
try {
|
|
440
|
+
const errorData = await response.json();
|
|
441
|
+
errorText = JSON.stringify(errorData, null, 2);
|
|
442
|
+
} catch {
|
|
443
|
+
errorText = await response.text();
|
|
444
|
+
}
|
|
445
|
+
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// If this was a POST request (creating new item), redirect to edit mode
|
|
449
|
+
if (!id) {
|
|
450
|
+
try {
|
|
451
|
+
const responseData = await response.json();
|
|
452
|
+
const newId = responseData.id || responseData.data?.id;
|
|
453
|
+
if (newId) {
|
|
454
|
+
// Determine the correct route based on the current route
|
|
455
|
+
if (route.name === 'createArticle') {
|
|
456
|
+
// Redirect to edit article mode
|
|
457
|
+
await router.push(`/collections/${collection}/${newId}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} catch (parseError) {
|
|
461
|
+
console.warn('Could not parse response to get ID:', parseError);
|
|
462
|
+
// Still show success notification even if we can't redirect
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
notify({
|
|
467
|
+
title: t("common.success"),
|
|
468
|
+
message: t("common.successMessage"),
|
|
469
|
+
type: "success",
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error('Submit error:', error);
|
|
474
|
+
notify({
|
|
475
|
+
title: t("common.error"),
|
|
476
|
+
message: "Помилка збереження",
|
|
477
|
+
type: "error",
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
};
|
|
484
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
|
|
2
|
+
export default [
|
|
3
|
+
{
|
|
4
|
+
key: "title",
|
|
5
|
+
label: "common.title",
|
|
6
|
+
localization: true,
|
|
7
|
+
type: "text",
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
key: "description",
|
|
11
|
+
label: "common.description",
|
|
12
|
+
localization: true,
|
|
13
|
+
type: "text",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: "keywords",
|
|
17
|
+
label: "common.keywords",
|
|
18
|
+
localization: true,
|
|
19
|
+
type: "text",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "meta",
|
|
23
|
+
label: "common.meta",
|
|
24
|
+
type: "key-value",
|
|
25
|
+
ignore: ["title", "description", "keywords"],
|
|
26
|
+
},
|
|
27
|
+
];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-6 max-w-7xl mx-auto">
|
|
3
|
+
<div class="flex items-center justify-between">
|
|
4
|
+
<div class="flex items-center gap-4">
|
|
5
|
+
<button
|
|
6
|
+
@click="router.back()"
|
|
7
|
+
class="p-2 text-gray-500 rounded-full hover:text-gray-900 dark:text-gray-400 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
8
|
+
>
|
|
9
|
+
<ArrowLeft class="w-5 h-5" />
|
|
10
|
+
</button>
|
|
11
|
+
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
12
|
+
{{ $t(route?.meta?.title as string) }}
|
|
13
|
+
</h1>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="flex items-center space-x-3">
|
|
16
|
+
<button
|
|
17
|
+
@click="handleSubmit"
|
|
18
|
+
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium h-9 px-4 py-2 bg-blue-600 text-white shadow-md transition-all duration-200 transform
|
|
19
|
+
hover:bg-blue-700 hover:shadow-lg hover:scale-105
|
|
20
|
+
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
|
21
|
+
disabled:pointer-events-none disabled:opacity-50"
|
|
22
|
+
>
|
|
23
|
+
<Save class="w-4 h-4 mr-2" />
|
|
24
|
+
{{ $t("common.actions.save") }}
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="max-w-[1000px]">
|
|
29
|
+
<VForm :schema="scheme" v-model="formValue" v-model:form="form" />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<script setup lang="ts">
|
|
35
|
+
import { ref, getCurrentInstance } from "vue";
|
|
36
|
+
import { useRouter, useRoute } from "vue-router";
|
|
37
|
+
import MonacoEditor from "../../components/form-components/MonacoEditor.vue";
|
|
38
|
+
import { ArrowLeft, Save } from "lucide-vue-next";
|
|
39
|
+
import { useI18n } from "vue-i18n";
|
|
40
|
+
import { notify } from '@opengis/core'
|
|
41
|
+
import { VForm, inputs } from '@opengis/form'
|
|
42
|
+
inputs['vs-input-monaco-editor'] = MonacoEditor
|
|
43
|
+
|
|
44
|
+
const { t } = useI18n();
|
|
45
|
+
|
|
46
|
+
const router = useRouter();
|
|
47
|
+
const route = useRoute<any>();
|
|
48
|
+
|
|
49
|
+
const scheme = ref<Record<string, any>>([
|
|
50
|
+
{
|
|
51
|
+
key: "name",
|
|
52
|
+
title: t("menu.form.name"),
|
|
53
|
+
type: "text",
|
|
54
|
+
placeholder: t("menu.form.name"),
|
|
55
|
+
required: true,
|
|
56
|
+
validators: ["required"],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "description",
|
|
60
|
+
title: t("menu.form.description"),
|
|
61
|
+
type: "text",
|
|
62
|
+
placeholder: t("menu.form.description"),
|
|
63
|
+
required: true,
|
|
64
|
+
validators: ["required"],
|
|
65
|
+
},
|
|
66
|
+
{key: "locale",
|
|
67
|
+
title: t("menu.form.language"),
|
|
68
|
+
type: "select",
|
|
69
|
+
placeholder: t("menu.form.language"),
|
|
70
|
+
options: [
|
|
71
|
+
{text: "Ukrainian", id: "uk"},
|
|
72
|
+
{text: "English", id: "en"},
|
|
73
|
+
],
|
|
74
|
+
required: true,
|
|
75
|
+
validators: ["required"],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "content",
|
|
79
|
+
title: t("menu.form.content"),
|
|
80
|
+
type: "monaco-editor",
|
|
81
|
+
height: "500px",
|
|
82
|
+
language: "yaml",
|
|
83
|
+
theme: "vs-light",
|
|
84
|
+
},
|
|
85
|
+
]);
|
|
86
|
+
const formValue = ref<Record<string, any>>({});
|
|
87
|
+
const form = ref<any>({});
|
|
88
|
+
|
|
89
|
+
const handleSubmit = async () => {
|
|
90
|
+
try {
|
|
91
|
+
let result = await form.value.validate();
|
|
92
|
+
if (result) {
|
|
93
|
+
notify({
|
|
94
|
+
type: "warning",
|
|
95
|
+
title: t("common.actions.warning"),
|
|
96
|
+
message: t("menu.createMenuFailed"),
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await fetch("/api/cms-menu", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: {
|
|
103
|
+
"Content-Type": "application/json",
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(formValue.value),
|
|
106
|
+
});
|
|
107
|
+
notify({
|
|
108
|
+
title: t("common.success"),
|
|
109
|
+
type: "success",
|
|
110
|
+
message: t("common.successMessage"),
|
|
111
|
+
});
|
|
112
|
+
formValue.value = {};
|
|
113
|
+
router.push("/menu");
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.log(error);
|
|
116
|
+
notify({
|
|
117
|
+
title: t("common.error"),
|
|
118
|
+
type: "error",
|
|
119
|
+
message: t("common.errorMessage"),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
</script>
|