@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.
Files changed (183) hide show
  1. package/package.json +2 -2
  2. package/src/App.css +52 -0
  3. package/src/App.vue +62 -0
  4. package/src/assets/image.png +0 -0
  5. package/src/assets/main.css +3 -0
  6. package/src/assets/tailwind-3.4.17.js +113 -0
  7. package/src/components/LanguageSwitcher.vue +73 -0
  8. package/src/components/SettingsCard.vue +40 -0
  9. package/src/components/builder/CreateForm.vue +128 -0
  10. package/src/components/builder/formTypeSchema.js +145 -0
  11. package/src/components/builder/tabs/index.ts +9 -0
  12. package/src/components/builder/tabs/vs-builder-edit.vue +133 -0
  13. package/src/components/builder/tabs/vs-builder-monaco.vue +29 -0
  14. package/src/components/builder/tabs/vs-builder-preview.vue +39 -0
  15. package/src/components/builder/vs-builder-datatable-controls.vue +138 -0
  16. package/src/components/builder/vs-builder-datatable-form.vue +80 -0
  17. package/src/components/builder/vs-builder-datatable.vue +191 -0
  18. package/src/components/builder/vs-builder-list-item.vue +110 -0
  19. package/src/components/collections/CollectionsBreadcrumb.vue +52 -0
  20. package/src/components/collections/CollectionsGrid.vue +176 -0
  21. package/src/components/collections/ContentBlock.vue +75 -0
  22. package/src/components/collections/formWrapper.vue +156 -0
  23. package/src/components/dashboard/ContentItem.vue +82 -0
  24. package/src/components/dashboard/DashboardHeader.vue +33 -0
  25. package/src/components/dashboard/QuickActions.vue +84 -0
  26. package/src/components/dashboard/RecentContent.vue +54 -0
  27. package/src/components/dashboard/StatCard.vue +63 -0
  28. package/src/components/dashboard/StatsGrid.vue +28 -0
  29. package/src/components/form-components/MonacoEditor.vue +104 -0
  30. package/src/components/form-components/VsFormMeta.vue +40 -0
  31. package/src/components/form-components/VsFormTags.vue +150 -0
  32. package/src/components/form-components/custom-datatable/vs-form-custom-datatable-add.vue +84 -0
  33. package/src/components/form-components/custom-datatable/vs-form-custom-datatable-controls.vue +106 -0
  34. package/src/components/form-components/index.js +23 -0
  35. package/src/components/form-components/reference/vs-form-reference-add.vue +92 -0
  36. package/src/components/form-components/reference/vs-form-reference-controls.vue +101 -0
  37. package/src/components/form-components/reference-list/referenceOptionList.js +78 -0
  38. package/src/components/form-components/reference-list/vs-form-reference-add.vue +145 -0
  39. package/src/components/form-components/reference-list/vs-form-reference-choce.vue +39 -0
  40. package/src/components/form-components/reference-list/vs-form-reference-controls.vue +110 -0
  41. package/src/components/form-components/reference-skeleton/about-skeleton.vue +37 -0
  42. package/src/components/form-components/reference-skeleton/banner-skeleton.vue +29 -0
  43. package/src/components/form-components/reference-skeleton/body-skeleton.vue +56 -0
  44. package/src/components/form-components/reference-skeleton/cards-skeleton.vue +47 -0
  45. package/src/components/form-components/reference-skeleton/documents-skeleton.vue +64 -0
  46. package/src/components/form-components/reference-skeleton/faq-skeleton.vue +64 -0
  47. package/src/components/form-components/reference-skeleton/form-skeleton.vue +41 -0
  48. package/src/components/form-components/reference-skeleton/index.js +36 -0
  49. package/src/components/form-components/reference-skeleton/infoLine-skeleton.vue +37 -0
  50. package/src/components/form-components/reference-skeleton/news-skeleton.vue +54 -0
  51. package/src/components/form-components/reference-skeleton/slider-skeleton.vue +41 -0
  52. package/src/components/form-components/reference-skeleton/tabs-skeleton.vue +40 -0
  53. package/src/components/form-components/reference-skeleton/team-skeleton.vue +103 -0
  54. package/src/components/form-components/reference-skeleton/usefulLinks-skeleton.vue +52 -0
  55. package/src/components/form-components/reference-skeleton/video-skeleton.vue +36 -0
  56. package/src/components/form-components/testReferenceTypes.js +773 -0
  57. package/src/components/form-components/vs-form-color-picker.vue +29 -0
  58. package/src/components/form-components/vs-form-custom-datatable.vue +214 -0
  59. package/src/components/form-components/vs-form-integer.vue +86 -0
  60. package/src/components/form-components/vs-form-key-value.vue +201 -0
  61. package/src/components/form-components/vs-form-marcdown-md.vue +3 -0
  62. package/src/components/form-components/vs-form-media-select.vue +780 -0
  63. package/src/components/form-components/vs-form-reference-list.vue +97 -0
  64. package/src/components/form-components/vs-form-reference.vue +59 -0
  65. package/src/components/form-components/vs-form-relation.vue +30 -0
  66. package/src/components/form-components/vs-form-reletion-link.vue +34 -0
  67. package/src/components/form-components/vs-form-select-collection.vue +0 -0
  68. package/src/components/form-components/vs-form-slug.vue +72 -0
  69. package/src/components/form-components/vs-form-tiptap.vue +7 -0
  70. package/src/components/form-components/vs-richtext-md.vue +3 -0
  71. package/src/components/icons/BellIcon.vue +17 -0
  72. package/src/components/icons/GlobeIcon.vue +18 -0
  73. package/src/components/icons/KeyIcon.vue +20 -0
  74. package/src/components/icons/PaletteIcon.vue +22 -0
  75. package/src/components/icons/SettingsIcon.vue +19 -0
  76. package/src/components/icons/ShieldIcon.vue +18 -0
  77. package/src/components/icons/UsersIcon.vue +19 -0
  78. package/src/components/icons/icon-chevron-right.vue +16 -0
  79. package/src/components/icons/icon-drag.vue +20 -0
  80. package/src/components/icons/icon-file-text.vue +21 -0
  81. package/src/components/icons/icon-folder.vue +18 -0
  82. package/src/components/icons/icon-grid.vue +17 -0
  83. package/src/components/icons/icon-group.vue +19 -0
  84. package/src/components/icons/icon-home.vue +16 -0
  85. package/src/components/icons/icon-image.vue +18 -0
  86. package/src/components/icons/icon-list.vue +20 -0
  87. package/src/components/icons/icon-more.vue +17 -0
  88. package/src/components/icons/icon-plus.vue +17 -0
  89. package/src/components/icons-types/icon-array.vue +22 -0
  90. package/src/components/icons-types/icon-boolean.vue +18 -0
  91. package/src/components/icons-types/icon-datalist.vue +22 -0
  92. package/src/components/icons-types/icon-date.vue +20 -0
  93. package/src/components/icons-types/icon-datetime.vue +20 -0
  94. package/src/components/icons-types/icon-file.vue +21 -0
  95. package/src/components/icons-types/icon-gallery.vue +18 -0
  96. package/src/components/icons-types/icon-image.vue +19 -0
  97. package/src/components/icons-types/icon-integer.vue +20 -0
  98. package/src/components/icons-types/icon-merkdown.vue +18 -0
  99. package/src/components/icons-types/icon-multiselect.vue +22 -0
  100. package/src/components/icons-types/icon-number.vue +20 -0
  101. package/src/components/icons-types/icon-radio.vue +22 -0
  102. package/src/components/icons-types/icon-reference-list.vue +22 -0
  103. package/src/components/icons-types/icon-reference.vue +20 -0
  104. package/src/components/icons-types/icon-relation.vue +22 -0
  105. package/src/components/icons-types/icon-richtext.vue +18 -0
  106. package/src/components/icons-types/icon-select.vue +22 -0
  107. package/src/components/icons-types/icon-slug.vue +19 -0
  108. package/src/components/icons-types/icon-text.vue +19 -0
  109. package/src/components/icons-types/index.js +43 -0
  110. package/src/components/layout/Layout.vue +67 -0
  111. package/src/components/layout/Sidebar.vue +128 -0
  112. package/src/components/media/FileUploadProgress.vue +29 -0
  113. package/src/components/media/MediaBreadcrumb.vue +42 -0
  114. package/src/components/media/MediaCreateFolder.vue +59 -0
  115. package/src/components/media/MediaFileInfo.vue +148 -0
  116. package/src/components/media/MediaGrid.vue +148 -0
  117. package/src/components/media/MediaList.vue +148 -0
  118. package/src/components/media/MediaViewControls.vue +38 -0
  119. package/src/components/media/TypeTag.vue +23 -0
  120. package/src/components/menu/AddNewItemInTree.vue +75 -0
  121. package/src/components/menu/MenuBody.vue +149 -0
  122. package/src/components/menu/MenuItem.vue +73 -0
  123. package/src/components/menu/MenuList.vue +101 -0
  124. package/src/components/referencec/index.ts +7 -0
  125. package/src/components/referencec/vs-reference-faq.vue +61 -0
  126. package/src/components/referencec/vs-reference-user-card.vue +40 -0
  127. package/src/components/settings/NotificationSettings.vue +32 -0
  128. package/src/components/settings/SettingsTable.vue +50 -0
  129. package/src/components/settings/SettingsTitle.vue +33 -0
  130. package/src/components/settings/SettingsToggleItem.vue +25 -0
  131. package/src/components/sidebar/DropdownMenu.vue +34 -0
  132. package/src/components/sidebar/SettingsSidebar.vue +121 -0
  133. package/src/components/sidebar/SidebarFooter.vue +52 -0
  134. package/src/components/sidebar/SidebarHeader.vue +57 -0
  135. package/src/components/sidebar/SidebarMenu.vue +78 -0
  136. package/src/components/ui/EmptyData.vue +76 -0
  137. package/src/components/ui/UniversalTable.vue +310 -0
  138. package/src/components/ui/UniversalTableFilters.vue +0 -0
  139. package/src/components/ui/UniversalTablePagination.vue +118 -0
  140. package/src/components/ui/VsPreview.vue +75 -0
  141. package/src/composables/useCollectionView.ts +21 -0
  142. package/src/composables/useDebounce.ts +26 -0
  143. package/src/composables/useMonaco.ts +28 -0
  144. package/src/composables/useTheme.ts +40 -0
  145. package/src/content/test-slug/metadata.json +1 -0
  146. package/src/i18n.ts +75 -0
  147. package/src/index.css +3 -0
  148. package/src/locales/en.json +778 -0
  149. package/src/locales/uk.json +797 -0
  150. package/src/main.ts +41 -0
  151. package/src/pages/Dashboard.vue +168 -0
  152. package/src/pages/EmailPage.vue +183 -0
  153. package/src/pages/FeedbackPage.vue +232 -0
  154. package/src/pages/MediaPage.vue +372 -0
  155. package/src/pages/TagsPage.vue +207 -0
  156. package/src/pages/builder/BuilderPage.vue +195 -0
  157. package/src/pages/builder/EditCollectionPage.vue +163 -0
  158. package/src/pages/collections/ArticlesPage.vue +385 -0
  159. package/src/pages/collections/CollectionsPage.vue +146 -0
  160. package/src/pages/collections/SingletonsPage.vue +119 -0
  161. package/src/pages/collections/contentForm.vue +484 -0
  162. package/src/pages/collections/schema/seo.ts +27 -0
  163. package/src/pages/menu/MenuAddPage.vue +123 -0
  164. package/src/pages/menu/MenuItemPage.vue +183 -0
  165. package/src/pages/menu/MenuPage.vue +133 -0
  166. package/src/pages/settings/ApiKeys.vue +75 -0
  167. package/src/pages/settings/Appearance.vue +80 -0
  168. package/src/pages/settings/Logs.vue +260 -0
  169. package/src/pages/settings/PermissionsPage.vue +237 -0
  170. package/src/pages/settings/Settings.vue +186 -0
  171. package/src/pages/settings/Users.vue +109 -0
  172. package/src/pages/settings/general.vue +154 -0
  173. package/src/pages/settings/generalScheme.js +132 -0
  174. package/src/pages/users/AddUser.vue +106 -0
  175. package/src/pages/users/UsersPage.vue +98 -0
  176. package/src/props/builder.ts +67 -0
  177. package/src/props/content.ts +56 -0
  178. package/src/props/media.ts +63 -0
  179. package/src/router/index.ts +181 -0
  180. package/src/types/fastify-auth.d.ts +4 -0
  181. package/src/utils/getField.js +270 -0
  182. package/src/utils/translit.js +19 -0
  183. 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>