@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,385 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, onMounted, watch, inject, computed, nextTick } from "vue";
|
|
3
|
+
import { useRouter, useRoute } from "vue-router";
|
|
4
|
+
import { Plus, FileText, Calendar, Globe, User, ChevronDown, HelpCircle } from "lucide-vue-next";
|
|
5
|
+
import UniversalTable from "../../components/ui/UniversalTable.vue";
|
|
6
|
+
import UniversalTablePagination from "../../components/ui/UniversalTablePagination.vue";
|
|
7
|
+
import { VsInputCheckbox } from "@opengis/form";
|
|
8
|
+
import { useHead } from "@vueuse/head";
|
|
9
|
+
import VsFilter from '@opengis/filter';
|
|
10
|
+
import { confirm } from '@opengis/core';
|
|
11
|
+
|
|
12
|
+
import { useI18n } from "vue-i18n";
|
|
13
|
+
import EmptyData from "../../components/ui/EmptyData.vue";
|
|
14
|
+
import CollectionsBreadcrumb from "../../components/collections/CollectionsBreadcrumb.vue";
|
|
15
|
+
interface Article {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
author: string;
|
|
20
|
+
category: string;
|
|
21
|
+
status: "published" | "draft" | "archived";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const route = useRoute();
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
|
|
27
|
+
const tableData = ref<any>(null);
|
|
28
|
+
const searchQuery = ref("");
|
|
29
|
+
const { t, locale } = useI18n();
|
|
30
|
+
const articles = ref<any[]>([]);
|
|
31
|
+
const menu = inject("menu") || <any>[];
|
|
32
|
+
const filter = ref<string>('');
|
|
33
|
+
const page = ref(1);
|
|
34
|
+
const showColumns = ref(false);
|
|
35
|
+
const baseColumns = ref(["title","slug","author","publish_at","published_at","status"])
|
|
36
|
+
const columns = ref([]);
|
|
37
|
+
const options = ref<any[]>([]);
|
|
38
|
+
const scheme = ref([]);
|
|
39
|
+
const initialFilters = ref<Record<string, any>>({});
|
|
40
|
+
const filterKey = ref(0); // Key для примусового перерендеру VsFilter
|
|
41
|
+
const isLoading = ref(false); // Флаг для запобігання подвійних викликів
|
|
42
|
+
|
|
43
|
+
interface FilterData {
|
|
44
|
+
data: Record<string, any>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rewritingFilters = () => {
|
|
48
|
+
scheme.value = tableData.value?.filters.map((filter: any) => {
|
|
49
|
+
return {
|
|
50
|
+
...filter,
|
|
51
|
+
label: filter.name,
|
|
52
|
+
id: filter.name,
|
|
53
|
+
inline: true,
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const updateColumns = () => {
|
|
59
|
+
let selectedColumns = tableData.value?.columns.filter((col: any) => baseColumns?.value?.includes(col.name));
|
|
60
|
+
|
|
61
|
+
columns.value = selectedColumns.map((col: any) => {
|
|
62
|
+
switch (col.name) {
|
|
63
|
+
case "title":
|
|
64
|
+
return {
|
|
65
|
+
...col,
|
|
66
|
+
title: col.name ? col.name.charAt(0).toUpperCase() + col.name.slice(1) : '--',
|
|
67
|
+
icon: FileText,
|
|
68
|
+
};
|
|
69
|
+
case "slug":
|
|
70
|
+
return {
|
|
71
|
+
...col,
|
|
72
|
+
title: col.name ? col.name.charAt(0).toUpperCase() + col.name.slice(1) : '-',
|
|
73
|
+
icon: Globe,
|
|
74
|
+
};
|
|
75
|
+
case "author":
|
|
76
|
+
return {
|
|
77
|
+
...col,
|
|
78
|
+
title: col.name ? col.name.charAt(0).toUpperCase() + col.name.slice(1) : '-',
|
|
79
|
+
icon: User,
|
|
80
|
+
};
|
|
81
|
+
case "publish_at":
|
|
82
|
+
return {
|
|
83
|
+
...col,
|
|
84
|
+
title: col.name ? col.name.charAt(0).toUpperCase() + col.name.slice(1) : '-',
|
|
85
|
+
icon: Calendar,
|
|
86
|
+
};
|
|
87
|
+
case "published_at":
|
|
88
|
+
return {
|
|
89
|
+
...col,
|
|
90
|
+
title: col.name ? col.name.charAt(0).toUpperCase() + col.name.slice(1) : '-',
|
|
91
|
+
icon: Calendar,
|
|
92
|
+
};
|
|
93
|
+
default:
|
|
94
|
+
return {
|
|
95
|
+
...col,
|
|
96
|
+
title: col.name ? col.name.charAt(0).toUpperCase() + col.name.slice(1) : '-',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
showColumns.value = false;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const titleOfCollection = computed(
|
|
105
|
+
() => {
|
|
106
|
+
return menu.value.find((item: any) => item.id === route.params.id)?.title
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const breadcrumbItems = computed(() => {
|
|
111
|
+
const collectionTitle = titleOfCollection.value || route.params.id;
|
|
112
|
+
return [
|
|
113
|
+
{
|
|
114
|
+
label: collectionTitle,
|
|
115
|
+
route: `collections/${route.params.id}`,
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const handleBreadcrumbNavigate = (routePath: string) => {
|
|
121
|
+
if (routePath === 'collections') {
|
|
122
|
+
router.push('/collections');
|
|
123
|
+
} else {
|
|
124
|
+
router.push(`/${routePath}`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const handleCreateArticle = () => {
|
|
129
|
+
router.push(`/collections/${route.params.id}/create`);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handleRemoveArticle = async (article: Article) => {
|
|
133
|
+
console.log('handleRemoveArticle');
|
|
134
|
+
confirm({
|
|
135
|
+
title: t("builder.deleteTitle"),
|
|
136
|
+
message: t("builder.deleteObject"),
|
|
137
|
+
type:'error',
|
|
138
|
+
onConfirm: async () => {
|
|
139
|
+
await fetch(`/api/cms/${route.params.id}/${article.id}`, { method: 'DELETE' });
|
|
140
|
+
fetchArticles();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleEditArticle = (article: Article) => {
|
|
146
|
+
router.push(`/collections/${route.params.id}/${article.id}`);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const fetchArticles = async () => {
|
|
150
|
+
if (isLoading.value) return; // Запобігаємо подвійним викликам
|
|
151
|
+
|
|
152
|
+
isLoading.value = true;
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(
|
|
155
|
+
`/api/cms/${route.params.id}?page=${page.value}&filter=${filter.value}`
|
|
156
|
+
);
|
|
157
|
+
const data = await res.json();
|
|
158
|
+
|
|
159
|
+
articles.value = data.rows || [];
|
|
160
|
+
tableData.value = data;
|
|
161
|
+
|
|
162
|
+
// Clean filter schema to only include valid IFilterItem properties
|
|
163
|
+
const cleanFilters = (data.filters || []).map((filter: any) => {
|
|
164
|
+
const { extra, title, ...cleanFilter } = filter;
|
|
165
|
+
return cleanFilter;
|
|
166
|
+
});
|
|
167
|
+
scheme.value = cleanFilters;
|
|
168
|
+
|
|
169
|
+
if(route.params.id === 'pages') {
|
|
170
|
+
baseColumns.value.push('type');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
setOptions();
|
|
174
|
+
updateColumns();
|
|
175
|
+
// rewritingFilters();
|
|
176
|
+
} finally {
|
|
177
|
+
isLoading.value = false;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const setOptions = () => {
|
|
182
|
+
if (!tableData.value?.columns) return;
|
|
183
|
+
|
|
184
|
+
const rawOptions = tableData.value.columns.map((col, index) => ({
|
|
185
|
+
text: col.label,
|
|
186
|
+
id: col.name,
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
options.value = [
|
|
191
|
+
...baseColumns.value
|
|
192
|
+
.map(col => rawOptions.find(opt => opt.id === col))
|
|
193
|
+
.filter(Boolean),
|
|
194
|
+
...rawOptions.filter(opt => !baseColumns.value.includes(opt.id))
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const filterFn = (row: any, query: string) => {
|
|
199
|
+
if (!query) return true;
|
|
200
|
+
const q = query.toLowerCase();
|
|
201
|
+
return columns.value.some((col: any) => {
|
|
202
|
+
const val = row[col.name];
|
|
203
|
+
return val && val.toString().toLowerCase().includes(q);
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const filterChange = (filters: FilterData) => {
|
|
208
|
+
const filterStr = Object.entries(filters?.data)
|
|
209
|
+
.filter(([, v]) => v != null)
|
|
210
|
+
.map(([key, val]) => `${key}=${val}`)
|
|
211
|
+
.join('|');
|
|
212
|
+
|
|
213
|
+
filter.value = filterStr;
|
|
214
|
+
|
|
215
|
+
// Оновлюємо URL з параметрами фільтрів
|
|
216
|
+
const query = { ...route.query };
|
|
217
|
+
if (filterStr) {
|
|
218
|
+
query.filter = filterStr;
|
|
219
|
+
} else {
|
|
220
|
+
delete query.filter;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
router.replace({ query });
|
|
224
|
+
// fetchArticles() буде викликано через watch(() => route.query, ...)
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Функція для ініціалізації фільтрів з URL
|
|
228
|
+
const initializeFiltersFromUrl = () => {
|
|
229
|
+
if (route.query.filter) {
|
|
230
|
+
filter.value = route.query.filter as string;
|
|
231
|
+
|
|
232
|
+
// Парсимо фільтри з URL для початкових значень
|
|
233
|
+
const filterParams = (route.query.filter as string).split('|');
|
|
234
|
+
const parsedFilters: Record<string, any> = {};
|
|
235
|
+
|
|
236
|
+
filterParams.forEach(param => {
|
|
237
|
+
const [key, value] = param.split('=');
|
|
238
|
+
if (key && value) {
|
|
239
|
+
parsedFilters[key] = value;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
initialFilters.value = parsedFilters;
|
|
244
|
+
} else {
|
|
245
|
+
// Очищаємо фільтри, якщо в URL немає параметрів
|
|
246
|
+
filter.value = '';
|
|
247
|
+
initialFilters.value = {};
|
|
248
|
+
// Примусово перерендеруємо компонент фільтрів
|
|
249
|
+
filterKey.value++;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
onMounted(() => {
|
|
254
|
+
initializeFiltersFromUrl();
|
|
255
|
+
fetchArticles();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
watch(() => route.params.id, () => {
|
|
259
|
+
// Reset filters when collection changes
|
|
260
|
+
initialFilters.value = {};
|
|
261
|
+
filter.value = '';
|
|
262
|
+
scheme.value = [];
|
|
263
|
+
|
|
264
|
+
// Clear URL filter parameters when switching collections
|
|
265
|
+
const query = { ...route.query };
|
|
266
|
+
delete query.filter;
|
|
267
|
+
router.replace({ query });
|
|
268
|
+
|
|
269
|
+
// Примусово перерендеруємо компонент фільтрів
|
|
270
|
+
filterKey.value++;
|
|
271
|
+
|
|
272
|
+
initializeFiltersFromUrl();
|
|
273
|
+
fetchArticles();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Відстежуємо зміни в query параметрах для обробки навігації назад/вперед
|
|
277
|
+
watch(() => route.query, async () => {
|
|
278
|
+
initializeFiltersFromUrl();
|
|
279
|
+
await nextTick(); // Чекаємо, поки всі зміни застосуються
|
|
280
|
+
fetchArticles();
|
|
281
|
+
}, { deep: true });
|
|
282
|
+
|
|
283
|
+
watch(page, async () => {
|
|
284
|
+
// При зміні сторінки перевіряємо, чи є фільтри в URL
|
|
285
|
+
if (!route.query.filter) {
|
|
286
|
+
filter.value = '';
|
|
287
|
+
initialFilters.value = {};
|
|
288
|
+
// Примусово перерендеруємо компонент фільтрів
|
|
289
|
+
filterKey.value++;
|
|
290
|
+
}
|
|
291
|
+
await nextTick(); // Чекаємо, поки всі зміни застосуються
|
|
292
|
+
fetchArticles();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// useHead({
|
|
296
|
+
// title: () => t("articles.title") + " | CMS",
|
|
297
|
+
// });
|
|
298
|
+
</script>
|
|
299
|
+
|
|
300
|
+
<template>
|
|
301
|
+
<div class="space-y-6 max-w-7xl mx-auto">
|
|
302
|
+
<CollectionsBreadcrumb
|
|
303
|
+
:items="breadcrumbItems"
|
|
304
|
+
@navigate="handleBreadcrumbNavigate"
|
|
305
|
+
/>
|
|
306
|
+
<div
|
|
307
|
+
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
|
308
|
+
>
|
|
309
|
+
<div class="flex items-center gap-2">
|
|
310
|
+
<h1 class="text-3xl font-bold text-slate-800 dark:text-white mb-2">
|
|
311
|
+
{{ titleOfCollection }}
|
|
312
|
+
</h1>
|
|
313
|
+
<a :href="`https://cms.opengis.info/${locale}/guides/content/`" target="_blank" :title="$t('guide.content')">
|
|
314
|
+
<HelpCircle class="w-5 h-5" />
|
|
315
|
+
</a>
|
|
316
|
+
</div>
|
|
317
|
+
<button v-if="articles.length"
|
|
318
|
+
@click="handleCreateArticle"
|
|
319
|
+
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
|
|
320
|
+
hover:bg-blue-700 hover:shadow-lg hover:scale-105
|
|
321
|
+
focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring
|
|
322
|
+
disabled:pointer-events-none disabled:opacity-50"
|
|
323
|
+
>
|
|
324
|
+
<Plus class="w-4 h-4 mr-2" />
|
|
325
|
+
{{ $t("articles.createArticle") }}
|
|
326
|
+
</button>
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
<div v-if="articles.length || filter.length"
|
|
330
|
+
class="text-card-foreground shadow-lg border-0 bg-white/80 dark:bg-slate-800/80 backdrop-blur-sm"
|
|
331
|
+
>
|
|
332
|
+
<div v-if="true" class="p-2 border-b border-gray-200 sm:px-4 sm:py-2 dark:border-gray-700">
|
|
333
|
+
<div class="flex flex-col gap-4 sm:flex-row">
|
|
334
|
+
<VsFilter
|
|
335
|
+
:key="filterKey"
|
|
336
|
+
:schema="scheme"
|
|
337
|
+
:history="true"
|
|
338
|
+
:value="initialFilters"
|
|
339
|
+
view="inline"
|
|
340
|
+
@change="filterChange"
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
<div class="relative flex items-center gap-2 ml-auto">
|
|
344
|
+
<div class="flex items-center gap-2 cursor-pointer text-sm border border-gray-300 rounded-md p-2 dark:text-white" @click="showColumns = !showColumns" v-if="options.length > 0">
|
|
345
|
+
<p>{{ $t("articles.showColumns") }}</p>
|
|
346
|
+
<ChevronDown class="w-4 h-4" />
|
|
347
|
+
</div>
|
|
348
|
+
<div class="absolute top-full right-1/2 translate-x-1/2 min-w-full p-4 z-10 bg-white border border-gray-300 rounded-md mt-2 dark:bg-slate-800 dark:border-slate-700" v-if="options.length > 0 && showColumns">
|
|
349
|
+
<div class="max-h-60 overflow-y-auto overflow-x-hidden">
|
|
350
|
+
<VsInputCheckbox
|
|
351
|
+
v-if="options.length > 0 && showColumns"
|
|
352
|
+
:options="options"
|
|
353
|
+
v-model="baseColumns"
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
<button class="text-sm bg-blue-600 text-white rounded-md p-2 mt-4 w-full" @click="updateColumns">
|
|
357
|
+
{{ $t("articles.update") }}
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<UniversalTable
|
|
364
|
+
v-if="columns.length > 0"
|
|
365
|
+
class="max-h-[calc(100vh-270px)] overflow-y-auto"
|
|
366
|
+
:rows="articles"
|
|
367
|
+
:columns="columns"
|
|
368
|
+
:query="searchQuery"
|
|
369
|
+
:filterFn="filterFn"
|
|
370
|
+
@delete="handleRemoveArticle"
|
|
371
|
+
@edit="handleEditArticle"
|
|
372
|
+
/>
|
|
373
|
+
<UniversalTablePagination
|
|
374
|
+
v-if="tableData?.total > 20"
|
|
375
|
+
:total="tableData?.filtered"
|
|
376
|
+
:count="tableData?.count"
|
|
377
|
+
v-model:page="page"
|
|
378
|
+
:limit="20"
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
<div v-else>
|
|
382
|
+
<EmptyData @action="handleCreateArticle"/>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
</template>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-6 max-w-7xl mx-auto">
|
|
3
|
+
<div class="flex items-center justify-between mb-8">
|
|
4
|
+
<div class="flex gap-4">
|
|
5
|
+
<div>
|
|
6
|
+
<h1 class="text-3xl font-bold text-slate-800 dark:text-slate-100 mb-2">
|
|
7
|
+
{{ $t("navigation.collections") }}
|
|
8
|
+
</h1>
|
|
9
|
+
<p class="text-slate-600 dark:text-slate-300">
|
|
10
|
+
{{ $t("collections.selectCollection") }}
|
|
11
|
+
</p>
|
|
12
|
+
</div>
|
|
13
|
+
<a
|
|
14
|
+
:href="`https://cms.opengis.info/${locale}/guides/collections/`"
|
|
15
|
+
target="_blank"
|
|
16
|
+
:title="$t('guide.collections')"
|
|
17
|
+
class="mt-2"
|
|
18
|
+
>
|
|
19
|
+
<HelpCircle class="w-5 h-5" />
|
|
20
|
+
</a>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="relative w-full max-w-sm">
|
|
24
|
+
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
|
25
|
+
<Search class="w-5 h-5 text-gray-400" />
|
|
26
|
+
</div>
|
|
27
|
+
<input
|
|
28
|
+
v-model="searchQuery"
|
|
29
|
+
type="text"
|
|
30
|
+
class="block w-full py-2 pl-10 pr-3 leading-5 text-gray-900 placeholder-gray-500 transition-colors bg-white border border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:placeholder-gray-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 dark:text-white sm:text-sm"
|
|
31
|
+
:placeholder="$t('collections.searchCollections')"
|
|
32
|
+
/>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div v-if="collections.length > 0">
|
|
37
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
|
38
|
+
<button v-for="collection in filteredCollections" :key="collection.id" @click="handleViewCollection(collection)" class="w-full border border-gray-200 bg-white rounded-lg p-4 text-left hover:shadow-md transition-all hover:border-gray-300">
|
|
39
|
+
<div class="flex items-start justify-between">
|
|
40
|
+
<div class="flex items-start gap-3">
|
|
41
|
+
<File v-if="collection.id === 'pages'" class="shrink-0 lucide lucide-page w-5 h-5 text-blue-600" />
|
|
42
|
+
<Layers v-else class="shrink-0 lucide lucide-layers w-5 h-5 text-blue-600" />
|
|
43
|
+
<div>
|
|
44
|
+
<h4 class="text-sm font-semibold text-gray-800 mb-1">{{ collection.title }}</h4>
|
|
45
|
+
<p class="text-xs text-gray-600">{{ collection.description || 'Опис відсутній'}}</p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="flex items-center gap-2">
|
|
49
|
+
<span class="text-xs text-gray-500 whitespace-nowrap">{{ collection.entries }} записів</span>
|
|
50
|
+
<ChevronRight class="w-4 h-4 text-gray-400" />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
</div>
|
|
57
|
+
<div v-else class="flex items-center justify-center min-h-[400px]">
|
|
58
|
+
<EmptyData />
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<script setup lang="ts">
|
|
64
|
+
import { ref, computed, onMounted, inject } from "vue";
|
|
65
|
+
import { useRouter } from "vue-router";
|
|
66
|
+
import { HelpCircle, File, Layers, ChevronRight, Search } from "lucide-vue-next";
|
|
67
|
+
import { useI18n } from "vue-i18n";
|
|
68
|
+
import CollectionsGrid from "../../components/collections/CollectionsGrid.vue";
|
|
69
|
+
import EmptyData from "../../components/ui/EmptyData.vue";
|
|
70
|
+
import type { Collection } from "../../props/builder";
|
|
71
|
+
import { confirm } from "@opengis/core";
|
|
72
|
+
|
|
73
|
+
const { t, locale } = useI18n();
|
|
74
|
+
const router = useRouter();
|
|
75
|
+
const fetchContentTypes = inject<any>("fetchContentTypes");
|
|
76
|
+
|
|
77
|
+
const collections = ref<Collection[]>([]);
|
|
78
|
+
const searchQuery = ref("");
|
|
79
|
+
|
|
80
|
+
const filteredCollections = computed(() => {
|
|
81
|
+
// Фільтруємо тільки колекції (не single)
|
|
82
|
+
const onlyCollections = collections.value.filter(
|
|
83
|
+
(c) => c.type !== "single"
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const q = searchQuery.value.trim().toLowerCase();
|
|
87
|
+
const searched = q
|
|
88
|
+
? onlyCollections.filter((c: any) => {
|
|
89
|
+
const hay = [c?.title, c?.name, c?.id, c?.description]
|
|
90
|
+
.filter(Boolean)
|
|
91
|
+
.join(" ")
|
|
92
|
+
.toLowerCase();
|
|
93
|
+
return hay.includes(q);
|
|
94
|
+
})
|
|
95
|
+
: onlyCollections;
|
|
96
|
+
|
|
97
|
+
// Сортуємо: pages завжди перші
|
|
98
|
+
const sorted = [...searched].sort((a, b) => {
|
|
99
|
+
if (a.id === "pages") return -1;
|
|
100
|
+
if (b.id === "pages") return 1;
|
|
101
|
+
return 0;
|
|
102
|
+
});
|
|
103
|
+
return sorted;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
async function fetchCollections() {
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch("/api/cms-type?type=collection");
|
|
109
|
+
if (!res.ok) throw new Error("Failed to fetch collections");
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
collections.value = data.rows || [];
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error("Error fetching collections:", e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const handleEditCollection = (collection: Collection) => {
|
|
118
|
+
router.push(`/settings/collections/edit/${collection.id || collection.name}`);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const handleViewCollection = (collection: Collection) => {
|
|
122
|
+
router.push(`/collections/${collection.id}`);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const handleDeleteCollection = async (collection: Collection) => {
|
|
126
|
+
confirm({
|
|
127
|
+
title: t("builder.deleteTitle"),
|
|
128
|
+
message: t("builder.deleteObject"),
|
|
129
|
+
type: "error",
|
|
130
|
+
onConfirm: async () => {
|
|
131
|
+
try {
|
|
132
|
+
await fetch(`/api/cms-type/${collection.id}`, { method: "DELETE" });
|
|
133
|
+
fetchCollections();
|
|
134
|
+
if (fetchContentTypes) fetchContentTypes();
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error("Error deleting collection:", error);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
onMounted(() => {
|
|
143
|
+
fetchCollections();
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="space-y-6 max-w-7xl mx-auto">
|
|
3
|
+
<div
|
|
4
|
+
class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"
|
|
5
|
+
>
|
|
6
|
+
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
7
|
+
{{ $t("singletons.title") }}
|
|
8
|
+
</h1>
|
|
9
|
+
</div>
|
|
10
|
+
<div
|
|
11
|
+
class="overflow-hidden bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700"
|
|
12
|
+
>
|
|
13
|
+
<div class="p-4 border-b border-gray-200 sm:p-6 dark:border-gray-700">
|
|
14
|
+
<div class="flex flex-col gap-4 sm:flex-row">
|
|
15
|
+
<div class="relative flex-1">
|
|
16
|
+
<div
|
|
17
|
+
class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none"
|
|
18
|
+
>
|
|
19
|
+
<Search class="w-5 h-5 text-gray-400" />
|
|
20
|
+
</div>
|
|
21
|
+
<input
|
|
22
|
+
type="text"
|
|
23
|
+
class="block w-full py-2 pl-10 pr-3 leading-5 text-gray-900 placeholder-gray-500 transition-colors bg-white border border-gray-300 rounded-md dark:border-gray-600 dark:bg-gray-700 dark:placeholder-gray-400 focus:outline-none focus:ring-sky-500 focus:border-sky-500 dark:text-white sm:text-sm"
|
|
24
|
+
:placeholder="$t('singletons.search')"
|
|
25
|
+
v-model="searchQuery"
|
|
26
|
+
/>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="flex space-x-2">
|
|
30
|
+
<button
|
|
31
|
+
class="inline-flex items-center px-3 py-2 text-sm font-medium leading-4 text-gray-700 transition-colors bg-white border border-gray-300 rounded-md shadow-sm dark:border-gray-600 dark:text-gray-200 dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500"
|
|
32
|
+
>
|
|
33
|
+
<Filter class="w-4 h-4 mr-2 text-gray-500 dark:text-gray-400" />
|
|
34
|
+
{{ $t("common.actions.filter") }}
|
|
35
|
+
</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<UniversalTable
|
|
41
|
+
class="max-h-[calc(100vh-270px)] overflow-y-auto"
|
|
42
|
+
:rows="singletons"
|
|
43
|
+
:columns="columns"
|
|
44
|
+
:query="searchQuery"
|
|
45
|
+
:filterFn="filterFn"
|
|
46
|
+
@edit="handleEditSingleton"
|
|
47
|
+
/>
|
|
48
|
+
<UniversalTablePagination
|
|
49
|
+
v-if="tableData?.total > 16"
|
|
50
|
+
:total="tableData?.total"
|
|
51
|
+
:count="tableData?.count || tableData?.total"
|
|
52
|
+
v-model:page="page"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
import { ref, onMounted, watch } from "vue";
|
|
60
|
+
import { useRouter } from "vue-router";
|
|
61
|
+
import { Plus, Search, Filter } from "lucide-vue-next";
|
|
62
|
+
import UniversalTable from "../../components/ui/UniversalTable.vue";
|
|
63
|
+
import UniversalTablePagination from "../../components/ui/UniversalTablePagination.vue";
|
|
64
|
+
import { useI18n } from "vue-i18n";
|
|
65
|
+
|
|
66
|
+
const { t } = useI18n();
|
|
67
|
+
|
|
68
|
+
interface Singleton {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
description: string;
|
|
72
|
+
status: "published" | "draft";
|
|
73
|
+
lastModified: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const router = useRouter();
|
|
77
|
+
const searchQuery = ref("");
|
|
78
|
+
const singletons = ref<any[]>([]);
|
|
79
|
+
const page = ref(1);
|
|
80
|
+
const tableData = ref<any>(null);
|
|
81
|
+
const limit = ref(16);
|
|
82
|
+
|
|
83
|
+
const columns = [
|
|
84
|
+
{
|
|
85
|
+
name: "title",
|
|
86
|
+
title: t("common.title"),
|
|
87
|
+
type: "text",
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
const handleCreateSingleton = () => {
|
|
92
|
+
router.push("/singletons/create");
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const handleEditSingleton = (singleton: Singleton) => {
|
|
96
|
+
router.push(`/collections/single/${singleton.name || singleton.id}`);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const fetchSingletons = async () => {
|
|
100
|
+
const res = await fetch(`/api/cms?page=${page.value}&limit=${limit.value}`);
|
|
101
|
+
const data = await res.json();
|
|
102
|
+
const rows = data.rows;
|
|
103
|
+
singletons.value = rows?.filter((row: any) => row.type === "single");
|
|
104
|
+
tableData.value = data;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const filterFn = (row: any, query: string) => {
|
|
108
|
+
if (!query) return true;
|
|
109
|
+
const q = query.toLowerCase();
|
|
110
|
+
return columns.value.some((col: any) => {
|
|
111
|
+
const val = row[col.name];
|
|
112
|
+
return val && val.toString().toLowerCase().includes(q);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
watch(page, fetchSingletons);
|
|
117
|
+
|
|
118
|
+
onMounted(fetchSingletons);
|
|
119
|
+
</script>
|