@kinetis/vector-ui 0.0.0-stage → 0.9.0
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/CHANGELOG.md +26 -0
- package/LICENSE +18 -0
- package/README.md +72 -2
- package/app/app.config.ts +63 -0
- package/app/app.vue +11 -0
- package/app/components/Vx/Action/Dialog.vue +107 -0
- package/app/components/Vx/Cell/Boolean.vue +10 -0
- package/app/components/Vx/Cell/Color.vue +12 -0
- package/app/components/Vx/Cell/Count.vue +12 -0
- package/app/components/Vx/Cell/Date.vue +10 -0
- package/app/components/Vx/Cell/DateTime.vue +10 -0
- package/app/components/Vx/Cell/Email.vue +9 -0
- package/app/components/Vx/Cell/Enum.vue +14 -0
- package/app/components/Vx/Cell/File.vue +30 -0
- package/app/components/Vx/Cell/Image.vue +38 -0
- package/app/components/Vx/Cell/Json.vue +18 -0
- package/app/components/Vx/Cell/KeyValue.vue +25 -0
- package/app/components/Vx/Cell/Markdown.vue +18 -0
- package/app/components/Vx/Cell/Money.vue +16 -0
- package/app/components/Vx/Cell/Number.vue +16 -0
- package/app/components/Vx/Cell/Relation.vue +16 -0
- package/app/components/Vx/Cell/RichText.vue +41 -0
- package/app/components/Vx/Cell/Text.vue +13 -0
- package/app/components/Vx/Cell/Url.vue +11 -0
- package/app/components/Vx/Cell.vue +25 -0
- package/app/components/Vx/CommandPalette.vue +118 -0
- package/app/components/Vx/Confirm.vue +19 -0
- package/app/components/Vx/Dashboard.vue +27 -0
- package/app/components/Vx/FieldsForm.vue +46 -0
- package/app/components/Vx/Import/Dialog.vue +131 -0
- package/app/components/Vx/Inbox.vue +49 -0
- package/app/components/Vx/Input/Boolean.vue +10 -0
- package/app/components/Vx/Input/Currency.vue +25 -0
- package/app/components/Vx/Input/DateTime.vue +38 -0
- package/app/components/Vx/Input/Enum.vue +21 -0
- package/app/components/Vx/Input/KeyValue.vue +78 -0
- package/app/components/Vx/Input/Markdown.vue +120 -0
- package/app/components/Vx/Input/Money.vue +47 -0
- package/app/components/Vx/Input/Number.vue +24 -0
- package/app/components/Vx/Input/Relation.vue +127 -0
- package/app/components/Vx/Input/Repeater.vue +134 -0
- package/app/components/Vx/Input/RichText.vue +182 -0
- package/app/components/Vx/Input/Text.vue +21 -0
- package/app/components/Vx/Input/Textarea.vue +20 -0
- package/app/components/Vx/Input/Upload.vue +89 -0
- package/app/components/Vx/Layout.vue +111 -0
- package/app/components/Vx/List/BulkBar.vue +44 -0
- package/app/components/Vx/List/BulkEdit.vue +91 -0
- package/app/components/Vx/List/ColumnManager.vue +74 -0
- package/app/components/Vx/List/FilterBar.vue +224 -0
- package/app/components/Vx/List/ViewMenu.vue +115 -0
- package/app/components/Vx/Notifications.vue +9 -0
- package/app/components/Vx/Resource/Audit.vue +82 -0
- package/app/components/Vx/Resource/Form.vue +351 -0
- package/app/components/Vx/Resource/RelationPanel.vue +217 -0
- package/app/components/Vx/Resource/Show.vue +35 -0
- package/app/components/Vx/Resource/Table.vue +513 -0
- package/app/components/Vx/Widget/Card.vue +80 -0
- package/app/components/Vx/Widget/Partition.vue +21 -0
- package/app/components/Vx/Widget/Progress.vue +22 -0
- package/app/components/Vx/Widget/Stat.vue +22 -0
- package/app/components/Vx/Widget/Table.vue +31 -0
- package/app/components/Vx/Widget/Trend.vue +37 -0
- package/app/composables/useConfirm.ts +34 -0
- package/app/composables/useLeaveGuard.ts +21 -0
- package/app/composables/useVectorApi.ts +74 -0
- package/app/composables/useVectorAuthStore.ts +252 -0
- package/app/composables/useVectorBranding.ts +45 -0
- package/app/composables/useVectorDashboards.ts +15 -0
- package/app/composables/useVectorExtensions.ts +44 -0
- package/app/composables/useVectorFields.ts +68 -0
- package/app/composables/useVectorForm.ts +36 -0
- package/app/composables/useVectorIdle.ts +39 -0
- package/app/composables/useVectorImpersonation.ts +78 -0
- package/app/composables/useVectorInputs.ts +61 -0
- package/app/composables/useVectorJobs.ts +62 -0
- package/app/composables/useVectorLocale.ts +70 -0
- package/app/composables/useVectorNotifications.ts +24 -0
- package/app/composables/useVectorNotificationsInbox.ts +26 -0
- package/app/composables/useVectorPreferences.ts +30 -0
- package/app/composables/useVectorResource.ts +305 -0
- package/app/composables/useVectorRuntime.ts +42 -0
- package/app/composables/useVectorSchema.ts +56 -0
- package/app/composables/useVectorT.ts +9 -0
- package/app/composables/useVectorViews.ts +25 -0
- package/app/i18n/ar/actions.ts +11 -0
- package/app/i18n/ar/common.ts +23 -0
- package/app/i18n/ar/dashboard.ts +28 -0
- package/app/i18n/ar/fields.ts +77 -0
- package/app/i18n/ar/index.ts +12 -0
- package/app/i18n/ar/jobs.ts +36 -0
- package/app/i18n/ar/list.ts +160 -0
- package/app/i18n/ar/record.ts +62 -0
- package/app/i18n/ar/settings.ts +10 -0
- package/app/i18n/ar/shell.ts +54 -0
- package/app/i18n/de/actions.ts +11 -0
- package/app/i18n/de/common.ts +23 -0
- package/app/i18n/de/dashboard.ts +28 -0
- package/app/i18n/de/fields.ts +77 -0
- package/app/i18n/de/index.ts +12 -0
- package/app/i18n/de/jobs.ts +36 -0
- package/app/i18n/de/list.ts +160 -0
- package/app/i18n/de/record.ts +62 -0
- package/app/i18n/de/settings.ts +10 -0
- package/app/i18n/de/shell.ts +54 -0
- package/app/i18n/en/actions.ts +11 -0
- package/app/i18n/en/common.ts +23 -0
- package/app/i18n/en/dashboard.ts +28 -0
- package/app/i18n/en/fields.ts +77 -0
- package/app/i18n/en/index.ts +12 -0
- package/app/i18n/en/jobs.ts +36 -0
- package/app/i18n/en/list.ts +160 -0
- package/app/i18n/en/record.ts +62 -0
- package/app/i18n/en/settings.ts +10 -0
- package/app/i18n/en/shell.ts +54 -0
- package/app/i18n/es/actions.ts +11 -0
- package/app/i18n/es/common.ts +23 -0
- package/app/i18n/es/dashboard.ts +28 -0
- package/app/i18n/es/fields.ts +77 -0
- package/app/i18n/es/index.ts +12 -0
- package/app/i18n/es/jobs.ts +36 -0
- package/app/i18n/es/list.ts +160 -0
- package/app/i18n/es/record.ts +62 -0
- package/app/i18n/es/settings.ts +10 -0
- package/app/i18n/es/shell.ts +54 -0
- package/app/i18n/fr/actions.ts +11 -0
- package/app/i18n/fr/common.ts +23 -0
- package/app/i18n/fr/dashboard.ts +28 -0
- package/app/i18n/fr/fields.ts +77 -0
- package/app/i18n/fr/index.ts +12 -0
- package/app/i18n/fr/jobs.ts +36 -0
- package/app/i18n/fr/list.ts +160 -0
- package/app/i18n/fr/record.ts +62 -0
- package/app/i18n/fr/settings.ts +10 -0
- package/app/i18n/fr/shell.ts +54 -0
- package/app/i18n/he/actions.ts +11 -0
- package/app/i18n/he/common.ts +23 -0
- package/app/i18n/he/dashboard.ts +28 -0
- package/app/i18n/he/fields.ts +77 -0
- package/app/i18n/he/index.ts +12 -0
- package/app/i18n/he/jobs.ts +36 -0
- package/app/i18n/he/list.ts +160 -0
- package/app/i18n/he/record.ts +62 -0
- package/app/i18n/he/settings.ts +10 -0
- package/app/i18n/he/shell.ts +54 -0
- package/app/i18n/index.ts +111 -0
- package/app/i18n/it/actions.ts +11 -0
- package/app/i18n/it/common.ts +23 -0
- package/app/i18n/it/dashboard.ts +28 -0
- package/app/i18n/it/fields.ts +77 -0
- package/app/i18n/it/index.ts +12 -0
- package/app/i18n/it/jobs.ts +36 -0
- package/app/i18n/it/list.ts +160 -0
- package/app/i18n/it/record.ts +62 -0
- package/app/i18n/it/settings.ts +10 -0
- package/app/i18n/it/shell.ts +54 -0
- package/app/i18n/ja/actions.ts +11 -0
- package/app/i18n/ja/common.ts +23 -0
- package/app/i18n/ja/dashboard.ts +28 -0
- package/app/i18n/ja/fields.ts +77 -0
- package/app/i18n/ja/index.ts +12 -0
- package/app/i18n/ja/jobs.ts +36 -0
- package/app/i18n/ja/list.ts +160 -0
- package/app/i18n/ja/record.ts +62 -0
- package/app/i18n/ja/settings.ts +10 -0
- package/app/i18n/ja/shell.ts +54 -0
- package/app/i18n/ko/actions.ts +11 -0
- package/app/i18n/ko/common.ts +23 -0
- package/app/i18n/ko/dashboard.ts +28 -0
- package/app/i18n/ko/fields.ts +77 -0
- package/app/i18n/ko/index.ts +12 -0
- package/app/i18n/ko/jobs.ts +36 -0
- package/app/i18n/ko/list.ts +160 -0
- package/app/i18n/ko/record.ts +62 -0
- package/app/i18n/ko/settings.ts +10 -0
- package/app/i18n/ko/shell.ts +54 -0
- package/app/i18n/nl/actions.ts +11 -0
- package/app/i18n/nl/common.ts +23 -0
- package/app/i18n/nl/dashboard.ts +28 -0
- package/app/i18n/nl/fields.ts +77 -0
- package/app/i18n/nl/index.ts +12 -0
- package/app/i18n/nl/jobs.ts +36 -0
- package/app/i18n/nl/list.ts +160 -0
- package/app/i18n/nl/record.ts +62 -0
- package/app/i18n/nl/settings.ts +10 -0
- package/app/i18n/nl/shell.ts +54 -0
- package/app/i18n/pl/actions.ts +11 -0
- package/app/i18n/pl/common.ts +23 -0
- package/app/i18n/pl/dashboard.ts +28 -0
- package/app/i18n/pl/fields.ts +77 -0
- package/app/i18n/pl/index.ts +12 -0
- package/app/i18n/pl/jobs.ts +36 -0
- package/app/i18n/pl/list.ts +160 -0
- package/app/i18n/pl/record.ts +62 -0
- package/app/i18n/pl/settings.ts +10 -0
- package/app/i18n/pl/shell.ts +54 -0
- package/app/i18n/pt/actions.ts +11 -0
- package/app/i18n/pt/common.ts +23 -0
- package/app/i18n/pt/dashboard.ts +28 -0
- package/app/i18n/pt/fields.ts +77 -0
- package/app/i18n/pt/index.ts +12 -0
- package/app/i18n/pt/jobs.ts +36 -0
- package/app/i18n/pt/list.ts +160 -0
- package/app/i18n/pt/record.ts +62 -0
- package/app/i18n/pt/settings.ts +10 -0
- package/app/i18n/pt/shell.ts +54 -0
- package/app/i18n/pt-BR/actions.ts +11 -0
- package/app/i18n/pt-BR/common.ts +23 -0
- package/app/i18n/pt-BR/dashboard.ts +28 -0
- package/app/i18n/pt-BR/fields.ts +77 -0
- package/app/i18n/pt-BR/index.ts +12 -0
- package/app/i18n/pt-BR/jobs.ts +36 -0
- package/app/i18n/pt-BR/list.ts +160 -0
- package/app/i18n/pt-BR/record.ts +62 -0
- package/app/i18n/pt-BR/settings.ts +10 -0
- package/app/i18n/pt-BR/shell.ts +54 -0
- package/app/i18n/ru/actions.ts +11 -0
- package/app/i18n/ru/common.ts +23 -0
- package/app/i18n/ru/dashboard.ts +28 -0
- package/app/i18n/ru/fields.ts +77 -0
- package/app/i18n/ru/index.ts +12 -0
- package/app/i18n/ru/jobs.ts +36 -0
- package/app/i18n/ru/list.ts +160 -0
- package/app/i18n/ru/record.ts +62 -0
- package/app/i18n/ru/settings.ts +10 -0
- package/app/i18n/ru/shell.ts +54 -0
- package/app/i18n/tr/actions.ts +11 -0
- package/app/i18n/tr/common.ts +23 -0
- package/app/i18n/tr/dashboard.ts +28 -0
- package/app/i18n/tr/fields.ts +77 -0
- package/app/i18n/tr/index.ts +12 -0
- package/app/i18n/tr/jobs.ts +36 -0
- package/app/i18n/tr/list.ts +160 -0
- package/app/i18n/tr/record.ts +62 -0
- package/app/i18n/tr/settings.ts +10 -0
- package/app/i18n/tr/shell.ts +54 -0
- package/app/i18n/uk/actions.ts +11 -0
- package/app/i18n/uk/common.ts +23 -0
- package/app/i18n/uk/dashboard.ts +28 -0
- package/app/i18n/uk/fields.ts +77 -0
- package/app/i18n/uk/index.ts +12 -0
- package/app/i18n/uk/jobs.ts +36 -0
- package/app/i18n/uk/list.ts +160 -0
- package/app/i18n/uk/record.ts +62 -0
- package/app/i18n/uk/settings.ts +10 -0
- package/app/i18n/uk/shell.ts +54 -0
- package/app/i18n/zh-Hans/actions.ts +11 -0
- package/app/i18n/zh-Hans/common.ts +23 -0
- package/app/i18n/zh-Hans/dashboard.ts +28 -0
- package/app/i18n/zh-Hans/fields.ts +77 -0
- package/app/i18n/zh-Hans/index.ts +12 -0
- package/app/i18n/zh-Hans/jobs.ts +36 -0
- package/app/i18n/zh-Hans/list.ts +160 -0
- package/app/i18n/zh-Hans/record.ts +62 -0
- package/app/i18n/zh-Hans/settings.ts +10 -0
- package/app/i18n/zh-Hans/shell.ts +54 -0
- package/app/layouts/default.vue +8 -0
- package/app/layouts/vector.vue +238 -0
- package/app/middleware/vector-auth.ts +16 -0
- package/app/pages/d/[name].vue +12 -0
- package/app/pages/index.vue +22 -0
- package/app/pages/jobs/[id].vue +52 -0
- package/app/pages/jobs/index.vue +42 -0
- package/app/pages/login.vue +109 -0
- package/app/pages/p/[name].vue +37 -0
- package/app/pages/r/[resource]/[id]/edit.vue +54 -0
- package/app/pages/r/[resource]/[id]/index.vue +143 -0
- package/app/pages/r/[resource]/create.vue +53 -0
- package/app/pages/r/[resource]/index.vue +29 -0
- package/app/pages/settings.vue +104 -0
- package/app/plugins/vector-csp.client.ts +14 -0
- package/app/plugins/vector-extensions.client.ts +84 -0
- package/app/plugins/vector-i18n.ts +76 -0
- package/app/types/vector.ts +438 -0
- package/app/utils/files.ts +14 -0
- package/app/utils/format.ts +100 -0
- package/app/utils/richtext.ts +305 -0
- package/app/utils/rules.ts +113 -0
- package/app/utils/translate.ts +45 -0
- package/app/utils/widgets.ts +11 -0
- package/nuxt.config.ts +83 -0
- package/package.json +82 -5
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorAction } from '#vector/app/types/vector'
|
|
3
|
+
|
|
4
|
+
definePageMeta({ layout: 'vector', middleware: 'vector-auth', key: route => route.fullPath })
|
|
5
|
+
|
|
6
|
+
const { t, noun } = useVectorT()
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const appConfig = useAppConfig()
|
|
9
|
+
const { load, resource: find } = useVectorSchema()
|
|
10
|
+
await load()
|
|
11
|
+
|
|
12
|
+
const slug = computed(() => String(route.params.resource))
|
|
13
|
+
const id = computed(() => String(route.params.id))
|
|
14
|
+
const resource = computed(() => find(slug.value))
|
|
15
|
+
const client = useVectorResource(slug)
|
|
16
|
+
const base = computed(() => `${appConfig.vector.basePath}/r/${slug.value}`)
|
|
17
|
+
const { confirm } = useConfirm()
|
|
18
|
+
const notifications = useVectorNotifications()
|
|
19
|
+
|
|
20
|
+
const { data: response, status, error, refresh } = await useAsyncData(() => `vector.${slug.value}.${id.value}`, () => client.show(id.value))
|
|
21
|
+
|
|
22
|
+
const title = computed(() => {
|
|
23
|
+
const value = response.value?.data?.[resource.value?.titleField ?? 'id']
|
|
24
|
+
return value === undefined || value === null ? `#${id.value}` : String(value)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const trashed = computed(() => response.value?.data?._trashed === true)
|
|
28
|
+
const { schema } = useVectorSchema()
|
|
29
|
+
const impersonation = useVectorImpersonation()
|
|
30
|
+
const canImpersonate = computed(() => appConfig.vector.usersResource === slug.value && schema.value?.capabilities.impersonation === true && String(response.value?.data.id) !== String(useVectorAuthStore().user?.id))
|
|
31
|
+
|
|
32
|
+
async function impersonate() {
|
|
33
|
+
if (!resource.value || !response.value) return
|
|
34
|
+
|
|
35
|
+
if (!await confirm({ title: t('record.impersonate.title', { title: title.value }), text: t('record.impersonate.text'), confirmText: t('record.impersonate.action') })) return
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
await impersonation.start(response.value.data.id)
|
|
39
|
+
notifications.success(t('record.impersonate.started', { title: title.value }))
|
|
40
|
+
await navigateTo(appConfig.vector.basePath || '/')
|
|
41
|
+
} catch (caught) {
|
|
42
|
+
notifications.error(vectorErrorMessage(caught))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Every collection the show page shows gets a relation panel.
|
|
46
|
+
const collections = computed(() => (resource.value?.fields ?? []).filter(f => (f.type === 'hasMany' || f.type === 'manyToMany') && f.views.includes('show')))
|
|
47
|
+
const actions = computed<VectorAction[]>(() => (resource.value?.actions ?? []).filter(a => a.showOnDetail && !a.standalone && (response.value?.permissions.actions ?? []).includes(a.name)))
|
|
48
|
+
const dialogAction = ref<VectorAction | null>(null)
|
|
49
|
+
|
|
50
|
+
useHead({ title: () => resource.value ? `${title.value} · ${resource.value.label}` : '' })
|
|
51
|
+
|
|
52
|
+
async function remove(force = false) {
|
|
53
|
+
if (!resource.value) return
|
|
54
|
+
const soft = resource.value.softDelete !== null && !force
|
|
55
|
+
|
|
56
|
+
const ok = await confirm({
|
|
57
|
+
title: t(force ? 'record.deleteForeverTitle' : 'record.deleteTitle', { noun: noun(resource.value.labelSingular), title: title.value }),
|
|
58
|
+
text: soft ? t('record.deleteSoftText') : t('record.deleteHardText'),
|
|
59
|
+
confirmText: t('common.delete'),
|
|
60
|
+
destructive: true,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (!ok) return
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
if (force) await client.forceRemove(id.value)
|
|
67
|
+
else await client.remove(id.value)
|
|
68
|
+
notifications.success(t(soft ? 'record.trashed' : 'record.deleted', { label: resource.value.labelSingular }))
|
|
69
|
+
|
|
70
|
+
if (soft) await refresh()
|
|
71
|
+
else await navigateTo(base.value)
|
|
72
|
+
} catch (caught) {
|
|
73
|
+
notifications.error(vectorErrorMessage(caught))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function restore() {
|
|
78
|
+
if (!resource.value) return
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
await client.restore(id.value)
|
|
82
|
+
notifications.success(t('record.restored', { label: resource.value.labelSingular }))
|
|
83
|
+
await refresh()
|
|
84
|
+
} catch (caught) {
|
|
85
|
+
notifications.error(vectorErrorMessage(caught))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</script>
|
|
89
|
+
|
|
90
|
+
<template>
|
|
91
|
+
<div v-if="resource">
|
|
92
|
+
<v-breadcrumbs class="pa-0 mb-2" :items="[{ title: resource.label, to: base }, { title }]" />
|
|
93
|
+
<div class="d-flex align-center flex-wrap ga-2 mb-4">
|
|
94
|
+
<h1 class="text-h4">{{ title }}</h1>
|
|
95
|
+
<v-chip size="small" variant="tonal">{{ resource.labelSingular }}</v-chip>
|
|
96
|
+
<v-chip v-if="trashed" size="small" color="error" variant="tonal" prepend-icon="mdi-delete-clock-outline">{{ t('record.inTrash') }}</v-chip>
|
|
97
|
+
<v-spacer />
|
|
98
|
+
<template v-if="response">
|
|
99
|
+
<v-menu v-if="actions.length">
|
|
100
|
+
<template #activator="{ props: activator }">
|
|
101
|
+
<v-btn v-bind="activator" variant="tonal" append-icon="mdi-menu-down">{{ t('common.actions') }}</v-btn>
|
|
102
|
+
</template>
|
|
103
|
+
<v-list density="compact">
|
|
104
|
+
<v-list-item v-for="action in actions" :key="action.name" :title="action.label" :prepend-icon="action.icon ?? undefined" :base-color="action.destructive ? 'error' : undefined" @click="dialogAction = action" />
|
|
105
|
+
</v-list>
|
|
106
|
+
</v-menu>
|
|
107
|
+
<v-btn v-if="canImpersonate" prepend-icon="mdi-account-switch-outline" variant="tonal" @click="impersonate">{{ t('record.impersonate.action') }}</v-btn>
|
|
108
|
+
<v-btn v-if="resource.permissions.create && resource.replicable !== false && !trashed" :to="`${base}/create?from=${encodeURIComponent(id)}`" prepend-icon="mdi-content-copy" variant="tonal">{{ t('record.duplicate') }}</v-btn>
|
|
109
|
+
<v-btn v-if="response.permissions.update" :to="`${base}/${id}/edit`" prepend-icon="mdi-pencil-outline" color="primary">{{ t('common.edit') }}</v-btn>
|
|
110
|
+
<v-btn v-if="response.permissions.restore" prepend-icon="mdi-restore" color="primary" variant="tonal" @click="restore">{{ t('common.restore') }}</v-btn>
|
|
111
|
+
<v-btn v-if="response.permissions.forceDelete" prepend-icon="mdi-delete-forever-outline" color="error" variant="tonal" @click="remove(true)">{{ t('common.deleteForever') }}</v-btn>
|
|
112
|
+
<v-btn v-if="response.permissions.delete" prepend-icon="mdi-delete-outline" color="error" variant="tonal" @click="remove()">{{ t('common.delete') }}</v-btn>
|
|
113
|
+
</template>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<v-skeleton-loader v-if="status === 'pending'" type="article" />
|
|
117
|
+
<v-alert v-else-if="error" type="error" variant="tonal" :text="vectorErrorMessage(error)" />
|
|
118
|
+
<template v-else-if="response">
|
|
119
|
+
<VxResourceShow :resource="resource" :record="response.data" />
|
|
120
|
+
<VxResourceRelationPanel
|
|
121
|
+
v-for="field in collections"
|
|
122
|
+
:key="field.name"
|
|
123
|
+
:owner="resource"
|
|
124
|
+
:record="response.data"
|
|
125
|
+
:field="field"
|
|
126
|
+
:can-update="response.permissions.update"
|
|
127
|
+
@changed="refresh()"
|
|
128
|
+
/>
|
|
129
|
+
<VxResourceAudit :id="id" :resource="resource" :can-update="response.permissions.update" @reverted="refresh()" />
|
|
130
|
+
</template>
|
|
131
|
+
|
|
132
|
+
<VxActionDialog
|
|
133
|
+
v-if="dialogAction && response"
|
|
134
|
+
:resource="resource"
|
|
135
|
+
:action="dialogAction"
|
|
136
|
+
:selection="{ ids: [response.data.id] }"
|
|
137
|
+
:count="1"
|
|
138
|
+
@done="dialogAction = null; refresh()"
|
|
139
|
+
@cancel="dialogAction = null"
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
<v-empty-state v-else icon="mdi-help-circle-outline" :title="t('record.unknownResource')" />
|
|
143
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { VectorRecord } from '#vector/app/types/vector'
|
|
3
|
+
import type { SaveVariant } from '#vector/app/components/Vx/Resource/Form.vue'
|
|
4
|
+
|
|
5
|
+
definePageMeta({ layout: 'vector', middleware: 'vector-auth', key: route => route.fullPath })
|
|
6
|
+
|
|
7
|
+
const { t, noun } = useVectorT()
|
|
8
|
+
const route = useRoute()
|
|
9
|
+
const appConfig = useAppConfig()
|
|
10
|
+
const { load, resource: find } = useVectorSchema()
|
|
11
|
+
await load()
|
|
12
|
+
|
|
13
|
+
const slug = computed(() => String(route.params.resource))
|
|
14
|
+
const resource = computed(() => find(slug.value))
|
|
15
|
+
|
|
16
|
+
// ?from={id}: a copy of that record (Duplicate on its page)
|
|
17
|
+
const from = computed(() => typeof route.query.from === 'string' && route.query.from !== '' ? route.query.from : null)
|
|
18
|
+
const client = useVectorResource(slug)
|
|
19
|
+
const { data: source, error: sourceError } = await useAsyncData(() => `vector.${slug.value}.copy.${from.value}`, () => from.value ? client.show(from.value) : Promise.resolve(null))
|
|
20
|
+
const base = computed(() => `${appConfig.vector.basePath}/r/${slug.value}`)
|
|
21
|
+
|
|
22
|
+
useHead({ title: () => resource.value ? `${t('record.newTitle', { label: noun(resource.value.labelSingular) })} · ${resource.value.label}` : '' })
|
|
23
|
+
|
|
24
|
+
const form = ref<{ dirty: boolean, reset: () => void } | null>(null)
|
|
25
|
+
useLeaveGuard(() => form.value?.dirty === true)
|
|
26
|
+
|
|
27
|
+
function saved(record: VectorRecord, variant: SaveVariant) {
|
|
28
|
+
if (variant === 'another') {
|
|
29
|
+
// another one starts empty, even after a copy
|
|
30
|
+
if (from.value) return navigateTo(`${base.value}/create`)
|
|
31
|
+
form.value?.reset()
|
|
32
|
+
window.scrollTo({ top: 0 })
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const at = `${base.value}/${encodeURIComponent(String(record.id))}`
|
|
37
|
+
navigateTo(variant === 'continue' ? `${at}/edit` : at)
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div v-if="resource">
|
|
43
|
+
<v-breadcrumbs class="pa-0 mb-2" :items="[{ title: resource.label, to: base }, { title: t('record.new') }]" />
|
|
44
|
+
<h1 class="text-h4 mb-4">{{ t('record.newTitle', { label: noun(resource.labelSingular) }) }}</h1>
|
|
45
|
+
<v-alert v-if="!resource.permissions.create" type="warning" variant="tonal" :text="t('record.createNotAllowed')" />
|
|
46
|
+
<v-alert v-else-if="sourceError" type="error" variant="tonal" :text="vectorErrorMessage(sourceError)" />
|
|
47
|
+
<template v-else>
|
|
48
|
+
<v-alert v-if="source" type="info" variant="tonal" density="compact" class="mb-4" :text="t('record.copyOf', { title: String(source.data[resource.titleField] ?? `#${source.data.id}`) })" />
|
|
49
|
+
<VxResourceForm ref="form" :resource="resource" operation="create" :copy-of="source?.data" variants @saved="saved" @cancel="navigateTo(base)" />
|
|
50
|
+
</template>
|
|
51
|
+
</div>
|
|
52
|
+
<v-empty-state v-else icon="mdi-help-circle-outline" :title="t('record.unknownResource')" />
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
definePageMeta({ layout: 'vector', middleware: 'vector-auth', key: route => route.fullPath })
|
|
3
|
+
|
|
4
|
+
const route = useRoute()
|
|
5
|
+
const { load, resource: find } = useVectorSchema()
|
|
6
|
+
await load()
|
|
7
|
+
|
|
8
|
+
const { t, noun } = useVectorT()
|
|
9
|
+
const appConfig = useAppConfig()
|
|
10
|
+
const slug = computed(() => String(route.params.resource))
|
|
11
|
+
const resource = computed(() => find(slug.value))
|
|
12
|
+
|
|
13
|
+
useHead({ title: () => resource.value?.label ?? '' })
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div v-if="resource">
|
|
18
|
+
<div class="d-flex align-center mb-4">
|
|
19
|
+
<v-icon v-if="resource.icon" :icon="resource.icon" class="me-2" />
|
|
20
|
+
<h1 class="text-h4">{{ resource.label }}</h1>
|
|
21
|
+
<v-spacer />
|
|
22
|
+
<v-btn v-if="resource.permissions.create" :to="`${appConfig.vector.basePath}/r/${resource.slug}/create`" prepend-icon="mdi-plus" color="primary">
|
|
23
|
+
{{ t('list.page.create', { noun: noun(resource.labelSingular) }) }}
|
|
24
|
+
</v-btn>
|
|
25
|
+
</div>
|
|
26
|
+
<VxResourceTable :key="resource.slug" :resource="resource" />
|
|
27
|
+
</div>
|
|
28
|
+
<v-empty-state v-else icon="mdi-help-circle-outline" :title="t('list.page.unknown')" :text="t('list.page.unknownText', { slug })" />
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { languageName } from '#vector/app/i18n'
|
|
3
|
+
import type { SubmitEventPromise } from 'vuetify'
|
|
4
|
+
import type { VectorSettingsGroup } from '#vector/app/types/vector'
|
|
5
|
+
|
|
6
|
+
definePageMeta({ layout: 'vector', middleware: 'vector-auth' })
|
|
7
|
+
|
|
8
|
+
// Every settings group this user may manage, one tab each, rendered
|
|
9
|
+
// from the group's field list through the same registry as a form.
|
|
10
|
+
const api = useVectorApi()
|
|
11
|
+
const notifications = useVectorNotifications()
|
|
12
|
+
const { t } = useVectorT()
|
|
13
|
+
const { load: loadSchema } = useVectorSchema()
|
|
14
|
+
|
|
15
|
+
// fetched again in a new language, whose labels the server translates
|
|
16
|
+
const { current: locale } = useVectorLocale()
|
|
17
|
+
const { data, refresh } = await useAsyncData('vector.settings', () => api<{ data: VectorSettingsGroup[] }>('/vector/settings'), { watch: [locale] })
|
|
18
|
+
// Vector's own Languages group: each language keeps its own name, with its name in the admin's language beneath
|
|
19
|
+
const groups = computed(() => (data.value?.data ?? []).map(group => group.name === 'languages' ? withLanguageNames(group, locale.value) : group))
|
|
20
|
+
// what each group held when it was loaded: a group edited since keeps its edits when the page is fetched again
|
|
21
|
+
const loaded: Record<string, string> = {}
|
|
22
|
+
const tab = ref<string | null>(null)
|
|
23
|
+
const values = reactive<Record<string, Record<string, unknown>>>({})
|
|
24
|
+
const serverErrors = reactive<Record<string, Record<string, string[]>>>({})
|
|
25
|
+
const formError = ref<string | null>(null)
|
|
26
|
+
const saving = ref(false)
|
|
27
|
+
|
|
28
|
+
watch(groups, (list) => {
|
|
29
|
+
for (const group of list) {
|
|
30
|
+
const edited = values[group.name] !== undefined && loaded[group.name] !== undefined && JSON.stringify(values[group.name]) !== loaded[group.name]
|
|
31
|
+
if (!edited) values[group.name] = { ...group.values }
|
|
32
|
+
loaded[group.name] = JSON.stringify(group.values)
|
|
33
|
+
serverErrors[group.name] ??= {}
|
|
34
|
+
}
|
|
35
|
+
tab.value ??= list[0]?.name ?? null
|
|
36
|
+
}, { immediate: true })
|
|
37
|
+
|
|
38
|
+
useHead({ title: () => t('settings.pageTitle') })
|
|
39
|
+
|
|
40
|
+
async function save(group: VectorSettingsGroup, event: SubmitEventPromise) {
|
|
41
|
+
const { valid } = await event
|
|
42
|
+
if (!valid) return
|
|
43
|
+
|
|
44
|
+
saving.value = true
|
|
45
|
+
formError.value = null
|
|
46
|
+
const errors = serverErrors[group.name]!
|
|
47
|
+
for (const key of Object.keys(errors)) delete errors[key]
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await api(`/vector/settings/${encodeURIComponent(group.name)}`, { method: 'PUT', body: values[group.name] })
|
|
51
|
+
delete loaded[group.name] // saved: the server's answer replaces the form, normalised as stored
|
|
52
|
+
notifications.success(t('settings.saved', { label: group.label }))
|
|
53
|
+
await refresh()
|
|
54
|
+
await loadSchema(true) // branding rides with the descriptor
|
|
55
|
+
} catch (error) {
|
|
56
|
+
formError.value = applyViolations(error, values[group.name]!, errors)
|
|
57
|
+
} finally {
|
|
58
|
+
saving.value = false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The Languages group as shown: a switch's hint and the default's options name each language in the admin's language too. */
|
|
63
|
+
function withLanguageNames(group: VectorSettingsGroup, inLanguage: string): VectorSettingsGroup {
|
|
64
|
+
return {
|
|
65
|
+
...group,
|
|
66
|
+
fields: group.fields.map((field) => {
|
|
67
|
+
if (field.type === 'boolean') {
|
|
68
|
+
return { ...field, help: languageName(field.name, inLanguage, field.label) ?? field.help }
|
|
69
|
+
}
|
|
70
|
+
if (field.name === 'default' && field.options) {
|
|
71
|
+
return { ...field, options: field.options.map(option => ({ ...option, label: [option.label, languageName(String(option.value), inLanguage, option.label)].filter(Boolean).join(' — ') })) }
|
|
72
|
+
}
|
|
73
|
+
return field
|
|
74
|
+
}),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<template>
|
|
80
|
+
<div>
|
|
81
|
+
<h1 class="text-h4 mb-4">{{ t('settings.heading') }}</h1>
|
|
82
|
+
<v-empty-state v-if="groups.length === 0" icon="mdi-cog-off-outline" :title="t('settings.empty.title')" :text="t('settings.empty.text')" />
|
|
83
|
+
<v-card v-else>
|
|
84
|
+
<v-tabs v-model="tab" color="primary">
|
|
85
|
+
<v-tab v-for="group in groups" :key="group.name" :value="group.name" :prepend-icon="group.icon ?? undefined">{{ group.label }}</v-tab>
|
|
86
|
+
</v-tabs>
|
|
87
|
+
<v-divider />
|
|
88
|
+
<v-tabs-window v-model="tab">
|
|
89
|
+
<v-tabs-window-item v-for="group in groups" :key="group.name" :value="group.name">
|
|
90
|
+
<v-form validate-on="blur lazy" @submit.prevent="(e: SubmitEventPromise) => save(group, e)">
|
|
91
|
+
<v-card-text>
|
|
92
|
+
<v-alert v-if="formError && tab === group.name" type="error" variant="tonal" class="mb-4" :text="formError" />
|
|
93
|
+
<VxFieldsForm v-if="values[group.name]" :fields="group.fields" :values="values[group.name]!" :server-errors="serverErrors[group.name] ?? {}" @clear="(name: string) => delete serverErrors[group.name]![name]" />
|
|
94
|
+
</v-card-text>
|
|
95
|
+
<v-card-actions>
|
|
96
|
+
<v-spacer />
|
|
97
|
+
<v-btn type="submit" color="primary" :loading="saving">{{ t('common.save') }}</v-btn>
|
|
98
|
+
</v-card-actions>
|
|
99
|
+
</v-form>
|
|
100
|
+
</v-tabs-window-item>
|
|
101
|
+
</v-tabs-window>
|
|
102
|
+
</v-card>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted under a strict Content-Security-Policy, the shell hands the
|
|
3
|
+
* bundle the response's nonce; Vuetify puts it on the style element it
|
|
4
|
+
* creates for the theme, so `style-src 'nonce-…'` admits it.
|
|
5
|
+
*/
|
|
6
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
7
|
+
nuxtApp.hook('vuetify:before-create', ({ vuetifyOptions }) => {
|
|
8
|
+
const nonce = window.__VECTOR__?.cspNonce
|
|
9
|
+
|
|
10
|
+
if (nonce) {
|
|
11
|
+
vuetifyOptions.theme = { ...(typeof vuetifyOptions.theme === 'object' ? vuetifyOptions.theme : {}), cspNonce: nonce }
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as vue from 'vue'
|
|
2
|
+
import { Extension as TiptapExtension, Mark as TiptapMark, Node as TiptapNode, mergeAttributes } from '@tiptap/core'
|
|
3
|
+
import { VAlert, VAutocomplete, VBtn, VCard, VCheckbox, VChip, VIcon, VProgressLinear, VRating, VSelect, VSwitch, VTable, VTextField, VTextarea } from 'vuetify/components'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runtime UI extensions (§4.15 tier 2). `window.Vector` is the
|
|
7
|
+
* registration API an extension module calls; the URLs come from the
|
|
8
|
+
* hosted shell (`VECTOR_UI_EXTENSIONS`) or from runtime config in
|
|
9
|
+
* standalone mode, and are imported after the app is created. Vue and a
|
|
10
|
+
* curated set of Vuetify components are handed over so an extension
|
|
11
|
+
* needs no bundler and no copy of either library.
|
|
12
|
+
*/
|
|
13
|
+
export default defineNuxtPlugin(() => {
|
|
14
|
+
const registries = useVectorExtensions()
|
|
15
|
+
const i18n = useNuxtApp().$vectorI18n
|
|
16
|
+
const api$ = useVectorApi()
|
|
17
|
+
const notifications = useVectorNotifications()
|
|
18
|
+
const appConfig = useAppConfig()
|
|
19
|
+
const config = useRuntimeConfig()
|
|
20
|
+
// VTable for a custom page's tables: its wrapper, not a bare <table>, carries Vuetify's table styles
|
|
21
|
+
const vuetify = { VAlert, VAutocomplete, VBtn, VCard, VCheckbox, VChip, VIcon, VProgressLinear, VRating, VSelect, VSwitch, VTable, VTextField, VTextarea }
|
|
22
|
+
|
|
23
|
+
const api = {
|
|
24
|
+
version: 1,
|
|
25
|
+
vue,
|
|
26
|
+
vuetify,
|
|
27
|
+
// what a rich-text extension is built from, so one needs no bundler of its own
|
|
28
|
+
tiptap: { Node: TiptapNode, Mark: TiptapMark, Extension: TiptapExtension, mergeAttributes },
|
|
29
|
+
defineCell: (name: string, component: vue.Component) => registries.cells.set(name, vue.markRaw(component)),
|
|
30
|
+
defineInput: (name: string, component: vue.Component) => registries.inputs.set(name, vue.markRaw(component)),
|
|
31
|
+
defineWidget: (type: string, component: vue.Component) => registries.widgets.set(type, vue.markRaw(component)),
|
|
32
|
+
/**
|
|
33
|
+
* A rich-text node or mark type (a Tiptap extension) a field's `features` may then name. The server
|
|
34
|
+
* accepts it only when the application registered the same name with Document::extend().
|
|
35
|
+
*/
|
|
36
|
+
defineRichTextExtension: (name: string, extension: import('@tiptap/core').AnyExtension) => registries.richText.set(name, vue.markRaw(extension)),
|
|
37
|
+
/** Adds or overrides UI messages for a locale: defineMessages('de', { common: { save: 'Speichern' } }). */
|
|
38
|
+
defineMessages: (locale: string, messages: Record<string, unknown>) => i18n.global.mergeLocaleMessage(locale, messages),
|
|
39
|
+
/** A custom page at `/p/{name}`, listed in the menu: definePage('stock', { title: 'Stock', icon: 'mdi-warehouse', component }). */
|
|
40
|
+
definePage: (name: string, page: { title: string, icon?: string, group?: string, component: vue.Component }) => {
|
|
41
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) throw new Error(`Vector page name "${name}" must be lowercase letters, digits and dashes.`)
|
|
42
|
+
registries.pages.set(name, { ...page, component: vue.markRaw(page.component) })
|
|
43
|
+
},
|
|
44
|
+
/** The admin's API client, with its credentials, for extensions calling the application's own endpoints. */
|
|
45
|
+
api: api$,
|
|
46
|
+
notify: {
|
|
47
|
+
success: (message: string) => notifications.success(message),
|
|
48
|
+
error: (message: string) => notifications.error(message),
|
|
49
|
+
},
|
|
50
|
+
navigate: (path: string) => navigateTo(`${appConfig.vector.basePath}${path}`),
|
|
51
|
+
/** The admin's translator, for an extension's own messages (defineMessages) or the admin's. */
|
|
52
|
+
t: (key: string, params: Record<string, unknown> = {}, plural?: number) => vt(key, params, plural),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
window.Vector = api
|
|
56
|
+
|
|
57
|
+
const urls = [...(window.__VECTOR__?.extensions ?? []), ...((config.public.vector as { extensions?: string[] }).extensions ?? [])]
|
|
58
|
+
|
|
59
|
+
Promise.allSettled(urls.map(url => import(/* @vite-ignore */ url)
|
|
60
|
+
.then(module => (typeof module.default === 'function' ? module.default(api) : undefined))
|
|
61
|
+
.catch(error => console.error(`Vector extension ${url} failed to load`, error))))
|
|
62
|
+
.then(() => { registries.ready.value = true })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
declare global {
|
|
66
|
+
interface Window {
|
|
67
|
+
Vector?: {
|
|
68
|
+
version: number
|
|
69
|
+
vue: typeof vue
|
|
70
|
+
vuetify: Record<string, vue.Component>
|
|
71
|
+
tiptap: { Node: typeof TiptapNode, Mark: typeof TiptapMark, Extension: typeof TiptapExtension, mergeAttributes: typeof mergeAttributes }
|
|
72
|
+
defineCell: (name: string, component: vue.Component) => void
|
|
73
|
+
defineInput: (name: string, component: vue.Component) => void
|
|
74
|
+
defineWidget: (type: string, component: vue.Component) => void
|
|
75
|
+
defineRichTextExtension: (name: string, extension: import('@tiptap/core').AnyExtension) => void
|
|
76
|
+
defineMessages: (locale: string, messages: Record<string, unknown>) => void
|
|
77
|
+
definePage: (name: string, page: { title: string, icon?: string, group?: string, component: vue.Component }) => void
|
|
78
|
+
api: import('nitropack').$Fetch
|
|
79
|
+
notify: { success: (message: string) => void, error: (message: string) => void }
|
|
80
|
+
navigate: (path: string) => unknown
|
|
81
|
+
t: (key: string, params?: Record<string, unknown>, plural?: number) => string
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createI18n } from 'vue-i18n'
|
|
2
|
+
import type { VectorI18n } from '#vector/app/utils/translate'
|
|
3
|
+
import { bundledMessages, isRtl, pluralRule, rtlLanguages, vuetifyLocale } from '#vector/app/i18n'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The admin's own strings, translated through vue-i18n, in the language
|
|
7
|
+
* the user chose. The descriptor's labels arrive translated by the server.
|
|
8
|
+
*
|
|
9
|
+
* The UI ships English and sixteen machine-translated locales. It starts in
|
|
10
|
+
* the hosted shell's language (the server's answer for this user), else
|
|
11
|
+
* the one this browser last used, else app config `vector.locale`, else
|
|
12
|
+
* English; the descriptor then confirms it, and the language menu in the
|
|
13
|
+
* app bar changes it (useVectorLocale). A key a locale lacks falls back to
|
|
14
|
+
* English. An application adds or overrides messages in app config
|
|
15
|
+
* `vector.messages` ({ de: { common: { save: 'Speichern' } } }), and a
|
|
16
|
+
* runtime extension through `window.Vector.defineMessages()`. Vuetify's
|
|
17
|
+
* own component strings and the page's direction follow the same locale.
|
|
18
|
+
*/
|
|
19
|
+
export default defineNuxtPlugin({
|
|
20
|
+
name: 'vector-i18n',
|
|
21
|
+
enforce: 'pre',
|
|
22
|
+
setup(nuxtApp) {
|
|
23
|
+
const appConfig = useAppConfig()
|
|
24
|
+
const hosted = import.meta.client ? window.__VECTOR__?.locale : undefined
|
|
25
|
+
let remembered: string | null = null
|
|
26
|
+
try {
|
|
27
|
+
remembered = import.meta.client ? window.localStorage.getItem('vector.locale') : null
|
|
28
|
+
} catch {
|
|
29
|
+
// storage may be unavailable (a private window, blocked site data)
|
|
30
|
+
}
|
|
31
|
+
const locale = hosted || remembered || appConfig.vector.locale || 'en'
|
|
32
|
+
|
|
33
|
+
// each language's own plural forms (pluralCategories): one, few, many for Russian; one form for Japanese
|
|
34
|
+
const locales = [...new Set([...Object.keys(bundledMessages), ...Object.keys(appConfig.vector.messages ?? {}), locale])]
|
|
35
|
+
|
|
36
|
+
const i18n = createI18n({
|
|
37
|
+
legacy: false,
|
|
38
|
+
locale,
|
|
39
|
+
fallbackLocale: 'en',
|
|
40
|
+
messages: { ...bundledMessages },
|
|
41
|
+
pluralRules: Object.fromEntries(locales.map(code => [code, pluralRule(code)])),
|
|
42
|
+
missingWarn: false,
|
|
43
|
+
fallbackWarn: false,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
for (const [code, messages] of Object.entries(appConfig.vector.messages ?? {})) {
|
|
47
|
+
i18n.global.mergeLocaleMessage(code, messages as Record<string, unknown>)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
nuxtApp.vueApp.use(i18n)
|
|
51
|
+
const vectorI18n = i18n as VectorI18n
|
|
52
|
+
setVectorI18n(vectorI18n)
|
|
53
|
+
setFormatLocale(locale)
|
|
54
|
+
|
|
55
|
+
if (import.meta.client) {
|
|
56
|
+
document.documentElement.lang = locale
|
|
57
|
+
document.documentElement.dir = isRtl(locale) ? 'rtl' : 'ltr'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Vuetify's component strings (pagination, "No data available", …) for every language it knows, switched at runtime.
|
|
61
|
+
nuxtApp.hook('vuetify:before-create', async ({ vuetifyOptions }) => {
|
|
62
|
+
const all = await import('vuetify/locale') as Record<string, unknown>
|
|
63
|
+
const messages = Object.fromEntries(Object.entries(all).filter(([, value]) => value && typeof value === 'object' && !Array.isArray(value)))
|
|
64
|
+
const current = messages[vuetifyLocale(locale)] ? vuetifyLocale(locale) : (messages[locale.split('-')[0]!] ? locale.split('-')[0]! : 'en')
|
|
65
|
+
vuetifyOptions.locale = {
|
|
66
|
+
...(typeof vuetifyOptions.locale === 'object' ? vuetifyOptions.locale : {}),
|
|
67
|
+
locale: current,
|
|
68
|
+
fallback: 'en',
|
|
69
|
+
messages: messages as never,
|
|
70
|
+
rtl: Object.fromEntries(rtlLanguages.map(code => [code, true])),
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return { provide: { vectorI18n } }
|
|
75
|
+
},
|
|
76
|
+
})
|