@motor-cms/ui-admin 4.0.5 → 4.0.7
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/app/components/client/FooterSlotCard.vue +3 -2
- package/app/composables/useClientFormExtensions.ts +89 -0
- package/app/lang/de/motor-admin/clients.json +1 -21
- package/app/lang/de/motor-admin/domains.json +1 -0
- package/app/lang/en/motor-admin/clients.json +1 -21
- package/app/lang/en/motor-admin/domains.json +1 -0
- package/app/pages/motor-admin/clients/[id]/edit.vue +25 -40
- package/app/pages/motor-admin/clients/create.vue +34 -2
- package/app/pages/motor-admin/email-templates/index.vue +13 -0
- package/app/types/generated/form-meta.ts +4 -2
- package/app/types/generated/grid-meta.ts +36 -16
- package/package.json +1 -1
- package/app/components/client/FrontendConfigSection.vue +0 -133
- package/app/composables/useClientFrontendConfig.ts +0 -145
- package/app/types/frontend-config.ts +0 -258
|
@@ -22,6 +22,7 @@ const emit = defineEmits<{
|
|
|
22
22
|
|
|
23
23
|
const client = useSanctumClient()
|
|
24
24
|
const router = useRouter()
|
|
25
|
+
const route = useRoute()
|
|
25
26
|
const { t, locale } = useI18n()
|
|
26
27
|
const { success, error: notifyError } = useNotify()
|
|
27
28
|
|
|
@@ -145,7 +146,7 @@ async function onCreateFooter(): Promise<void> {
|
|
|
145
146
|
|
|
146
147
|
emit('linked', data.uuid, data.id)
|
|
147
148
|
success(t('motor-admin.clients.global_components.footer_created'))
|
|
148
|
-
await router.push(`/motor-builder/builder-pages/${data.id}/edit`)
|
|
149
|
+
await router.push(`/motor-builder/builder-pages/${data.id}/edit?returnTo=${encodeURIComponent(route.fullPath)}`)
|
|
149
150
|
} catch (err: unknown) {
|
|
150
151
|
const message = err instanceof Error ? err.message : t('motor-core.errors.create_failed')
|
|
151
152
|
notifyError(t('motor-admin.clients.global_components.footer'), message)
|
|
@@ -156,7 +157,7 @@ async function onCreateFooter(): Promise<void> {
|
|
|
156
157
|
|
|
157
158
|
function onEditFooter(): void {
|
|
158
159
|
if (pageInfo.value) {
|
|
159
|
-
router.push(`/motor-builder/builder-pages/${pageInfo.value.id}/edit`)
|
|
160
|
+
router.push(`/motor-builder/builder-pages/${pageInfo.value.id}/edit?returnTo=${encodeURIComponent(route.fullPath)}`)
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// app/composables/useClientFormExtensions.ts
|
|
2
|
+
import type { Component, Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
export interface ClientFormExtension {
|
|
5
|
+
key: string
|
|
6
|
+
order: number
|
|
7
|
+
component: Component
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const registry: ClientFormExtension[] = reactive([])
|
|
11
|
+
const validityState = reactive<Record<string, boolean>>({})
|
|
12
|
+
|
|
13
|
+
const callbackRefs = new Map<string, {
|
|
14
|
+
validate: Ref<() => boolean | Promise<boolean>>
|
|
15
|
+
getSubmitData: Ref<() => Record<string, unknown>>
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
function ensureCallbackRefs(key: string) {
|
|
19
|
+
if (!callbackRefs.has(key)) {
|
|
20
|
+
callbackRefs.set(key, {
|
|
21
|
+
validate: ref(() => true),
|
|
22
|
+
getSubmitData: ref(() => ({})),
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
return callbackRefs.get(key)!
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useClientFormExtensions() {
|
|
29
|
+
function register(ext: ClientFormExtension) {
|
|
30
|
+
if (registry.some(e => e.key === ext.key)) return
|
|
31
|
+
registry.push(ext)
|
|
32
|
+
registry.sort((a, b) => a.order - b.order)
|
|
33
|
+
validityState[ext.key] = false
|
|
34
|
+
ensureCallbackRefs(ext.key)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function setValidity(key: string, isValid: boolean) {
|
|
38
|
+
validityState[key] = isValid
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function validateAll(): Promise<boolean> {
|
|
42
|
+
for (const ext of registry) {
|
|
43
|
+
const refs = callbackRefs.get(ext.key)
|
|
44
|
+
if (refs) {
|
|
45
|
+
const result = await refs.validate.value()
|
|
46
|
+
if (!result) return false
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getAllSubmitData(): Record<string, unknown> {
|
|
53
|
+
let merged: Record<string, unknown> = {}
|
|
54
|
+
for (const ext of registry) {
|
|
55
|
+
const refs = callbackRefs.get(ext.key)
|
|
56
|
+
if (refs) {
|
|
57
|
+
merged = { ...merged, ...refs.getSubmitData.value() }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return merged
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const extensions = computed(() => [...registry])
|
|
64
|
+
|
|
65
|
+
const allExtensionsValid = computed(() =>
|
|
66
|
+
registry.length === 0 || registry.every(ext => validityState[ext.key] === true)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return { extensions, register, setValidity, allExtensionsValid, validateAll, getAllSubmitData }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function useExtensionCallbacks(key: string) {
|
|
73
|
+
const refs = ensureCallbackRefs(key)
|
|
74
|
+
|
|
75
|
+
function setValidate(fn: () => boolean | Promise<boolean>) {
|
|
76
|
+
refs.validate.value = fn
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function setGetSubmitData(fn: () => Record<string, unknown>) {
|
|
80
|
+
refs.getSubmitData.value = fn
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
onUnmounted(() => {
|
|
84
|
+
refs.validate.value = () => true
|
|
85
|
+
refs.getSubmitData.value = () => ({})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return { setValidate, setGetSubmitData }
|
|
89
|
+
}
|
|
@@ -24,27 +24,7 @@
|
|
|
24
24
|
"group_address": "Adresse",
|
|
25
25
|
"group_contact": "Kontakt",
|
|
26
26
|
"group_other": "Sonstiges",
|
|
27
|
-
"
|
|
28
|
-
"group_brand": "Marke",
|
|
29
|
-
"group_contact": "Kontakt (Frontend)",
|
|
30
|
-
"group_features": "Funktionen",
|
|
31
|
-
"group_social": "Social Media",
|
|
32
|
-
"group_seo": "SEO",
|
|
33
|
-
"brand_name": "Markenname",
|
|
34
|
-
"brand_logo_alt": "Logo Alt-Text",
|
|
35
|
-
"color_scheme": "Farbschema",
|
|
36
|
-
"logo_slug": "Logo",
|
|
37
|
-
"contact_url": "Kontaktseiten-URL",
|
|
38
|
-
"contact_email": "Kontakt-E-Mail",
|
|
39
|
-
"contact_whatsapp_url": "WhatsApp-URL",
|
|
40
|
-
"features_order_line": "Bestellstrecke",
|
|
41
|
-
"features_appointments": "Terminbuchung",
|
|
42
|
-
"features_clickpath": "Clickpath",
|
|
43
|
-
"features_footer_menu": "Footer-Menü",
|
|
44
|
-
"social_instagram": "Instagram",
|
|
45
|
-
"social_facebook": "Facebook",
|
|
46
|
-
"seo_site_name": "Seitenname"
|
|
47
|
-
},
|
|
27
|
+
"frontend_config_title": "Frontend-Konfiguration",
|
|
48
28
|
"global_components": {
|
|
49
29
|
"title": "Globale Komponenten",
|
|
50
30
|
"footer": "Footer",
|
|
@@ -24,27 +24,7 @@
|
|
|
24
24
|
"group_address": "Address",
|
|
25
25
|
"group_contact": "Contact",
|
|
26
26
|
"group_other": "Other",
|
|
27
|
-
"
|
|
28
|
-
"group_brand": "Brand",
|
|
29
|
-
"group_contact": "Contact (Frontend)",
|
|
30
|
-
"group_features": "Features",
|
|
31
|
-
"group_social": "Social Media",
|
|
32
|
-
"group_seo": "SEO",
|
|
33
|
-
"brand_name": "Brand Name",
|
|
34
|
-
"brand_logo_alt": "Logo Alt Text",
|
|
35
|
-
"color_scheme": "Color Scheme",
|
|
36
|
-
"logo_slug": "Logo",
|
|
37
|
-
"contact_url": "Contact Page URL",
|
|
38
|
-
"contact_email": "Contact Email",
|
|
39
|
-
"contact_whatsapp_url": "WhatsApp URL",
|
|
40
|
-
"features_order_line": "Order Line",
|
|
41
|
-
"features_appointments": "Appointments",
|
|
42
|
-
"features_clickpath": "Clickpath",
|
|
43
|
-
"features_footer_menu": "Footer Menu",
|
|
44
|
-
"social_instagram": "Instagram",
|
|
45
|
-
"social_facebook": "Facebook",
|
|
46
|
-
"seo_site_name": "Site Name"
|
|
47
|
-
},
|
|
27
|
+
"frontend_config_title": "Frontend Configuration",
|
|
48
28
|
"global_components": {
|
|
49
29
|
"title": "Global Components",
|
|
50
30
|
"footer": "Footer",
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
<script setup lang="ts">
|
|
3
3
|
import { clientFormMeta } from '../../../../types/generated/form-meta'
|
|
4
4
|
import { clientFormConfig } from '@motor-cms/ui-core/app/types/config/client'
|
|
5
|
-
import { useClientFrontendConfig } from '../../../../composables/useClientFrontendConfig'
|
|
6
5
|
import { useClientLanguages } from '../../../../composables/useClientLanguages'
|
|
7
6
|
|
|
8
7
|
definePageMeta({ layout: 'default', permission: 'clients.read' })
|
|
@@ -15,6 +14,8 @@ const clientId = route.params.id as string
|
|
|
15
14
|
const isFrontendConfigEnabled
|
|
16
15
|
= useRuntimeConfig().public.featureClientFrontendConfig === true
|
|
17
16
|
|
|
17
|
+
const { extensions, allExtensionsValid, validateAll, getAllSubmitData } = useClientFormExtensions()
|
|
18
|
+
|
|
18
19
|
const {
|
|
19
20
|
fields,
|
|
20
21
|
schema,
|
|
@@ -39,12 +40,17 @@ const {
|
|
|
39
40
|
formConfig: clientFormConfig,
|
|
40
41
|
mode: 'edit',
|
|
41
42
|
id: clientId,
|
|
42
|
-
beforeSubmit: (data) => {
|
|
43
|
-
if (!isFrontendConfigEnabled) return
|
|
44
|
-
|
|
43
|
+
beforeSubmit: async (data) => {
|
|
44
|
+
if (!isFrontendConfigEnabled || extensions.value.length === 0) return
|
|
45
|
+
const valid = await validateAll()
|
|
46
|
+
if (!valid) {
|
|
45
47
|
throw new Error(t('motor-core.global.validation_failed'))
|
|
46
48
|
}
|
|
47
|
-
data.frontend_config =
|
|
49
|
+
data.frontend_config = {
|
|
50
|
+
...getAllSubmitData(),
|
|
51
|
+
globalComponents: (data.frontend_config as Record<string, unknown>)?.globalComponents
|
|
52
|
+
?? (clientRecord.value?.data?.frontend_config as Record<string, unknown>)?.globalComponents,
|
|
53
|
+
}
|
|
48
54
|
}
|
|
49
55
|
})
|
|
50
56
|
|
|
@@ -52,27 +58,6 @@ const { data: clientRecord } = useNuxtData<{ data: Record<string, unknown> }>(
|
|
|
52
58
|
`entity-form-/api/v2/clients-${clientId}`
|
|
53
59
|
)
|
|
54
60
|
|
|
55
|
-
const {
|
|
56
|
-
state: frontendConfigState,
|
|
57
|
-
errors: frontendConfigErrors,
|
|
58
|
-
fields: frontendConfigFields,
|
|
59
|
-
groups: frontendConfigGroups,
|
|
60
|
-
validate: validateFrontendConfig,
|
|
61
|
-
getSubmitData: getFrontendConfigSubmitData
|
|
62
|
-
} = useClientFrontendConfig({ clientRecord, fetching })
|
|
63
|
-
|
|
64
|
-
const colorSchemeOptions = [
|
|
65
|
-
{ label: 'energis', value: 'energis' },
|
|
66
|
-
{ label: 'highspeed', value: 'highspeed' },
|
|
67
|
-
{ label: 'jaeckel', value: 'jaeckel' }
|
|
68
|
-
]
|
|
69
|
-
|
|
70
|
-
const logoSlugOptions = [
|
|
71
|
-
{ label: 'energis', value: 'energis' },
|
|
72
|
-
{ label: 'highspeed', value: 'highspeed' },
|
|
73
|
-
{ label: 'jaeckel', value: 'jaeckel' }
|
|
74
|
-
]
|
|
75
|
-
|
|
76
61
|
const clientIdRef = computed(() =>
|
|
77
62
|
isFrontendConfigEnabled ? (route.params.id as string) : ''
|
|
78
63
|
)
|
|
@@ -99,8 +84,7 @@ async function onFooterLinked(languageId: number, uuid: string, _pageId: number)
|
|
|
99
84
|
await sanctumClient(`/api/v2/clients/${clientId}`, {
|
|
100
85
|
method: 'PATCH',
|
|
101
86
|
body: {
|
|
102
|
-
|
|
103
|
-
slug: freshClient.data.slug,
|
|
87
|
+
...freshClient.data,
|
|
104
88
|
frontend_config: {
|
|
105
89
|
...freshConfig,
|
|
106
90
|
globalComponents: { ...freshGc, footer: freshFooter }
|
|
@@ -128,8 +112,7 @@ async function onFooterUnlinked(languageId: number) {
|
|
|
128
112
|
await sanctumClient(`/api/v2/clients/${clientId}`, {
|
|
129
113
|
method: 'PATCH',
|
|
130
114
|
body: {
|
|
131
|
-
|
|
132
|
-
slug: freshClient.data.slug,
|
|
115
|
+
...freshClient.data,
|
|
133
116
|
frontend_config: {
|
|
134
117
|
...freshConfig,
|
|
135
118
|
globalComponents: { ...freshGc, footer: freshFooter }
|
|
@@ -143,7 +126,6 @@ async function onFooterUnlinked(languageId: number) {
|
|
|
143
126
|
notifyError(t('motor-admin.clients.edit_title'), message)
|
|
144
127
|
}
|
|
145
128
|
}
|
|
146
|
-
|
|
147
129
|
</script>
|
|
148
130
|
|
|
149
131
|
<template>
|
|
@@ -171,17 +153,20 @@ async function onFooterUnlinked(languageId: number) {
|
|
|
171
153
|
@save-and-new="onSaveAndNew"
|
|
172
154
|
>
|
|
173
155
|
<template
|
|
174
|
-
v-if="isFrontendConfigEnabled"
|
|
156
|
+
v-if="isFrontendConfigEnabled && extensions.length > 0"
|
|
175
157
|
#after-fields
|
|
176
158
|
>
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
159
|
+
<h2 class="text-lg font-semibold text-highlighted mt-2">
|
|
160
|
+
{{ t('motor-admin.clients.frontend_config_title') }}
|
|
161
|
+
</h2>
|
|
162
|
+
<component
|
|
163
|
+
v-for="ext in extensions"
|
|
164
|
+
:key="ext.key"
|
|
165
|
+
:is="ext.component"
|
|
166
|
+
:client-id="clientId"
|
|
167
|
+
:client-record="clientRecord"
|
|
182
168
|
:disabled="!canWrite"
|
|
183
|
-
|
|
184
|
-
:logo-slug-options="logoSlugOptions"
|
|
169
|
+
mode="edit"
|
|
185
170
|
/>
|
|
186
171
|
<ClientGlobalComponentsSection
|
|
187
172
|
:client-id="route.params.id"
|
|
@@ -190,7 +175,7 @@ async function onFooterUnlinked(languageId: number) {
|
|
|
190
175
|
:languages="languages"
|
|
191
176
|
:is-multi-language="isMultiLanguage"
|
|
192
177
|
:languages-loading="languagesLoading"
|
|
193
|
-
:disabled="!canWrite"
|
|
178
|
+
:disabled="!canWrite || !allExtensionsValid"
|
|
194
179
|
@footer-linked="onFooterLinked"
|
|
195
180
|
@footer-unlinked="onFooterUnlinked"
|
|
196
181
|
/>
|
|
@@ -6,13 +6,27 @@ import { clientFormConfig } from '@motor-cms/ui-core/app/types/config/client'
|
|
|
6
6
|
definePageMeta({ layout: 'default', permission: 'clients.write' })
|
|
7
7
|
|
|
8
8
|
const { t } = useI18n()
|
|
9
|
+
|
|
10
|
+
const isFrontendConfigEnabled
|
|
11
|
+
= useRuntimeConfig().public.featureClientFrontendConfig === true
|
|
12
|
+
|
|
13
|
+
const { extensions, validateAll, getAllSubmitData } = useClientFormExtensions()
|
|
14
|
+
|
|
9
15
|
const { fields, schema, groups, state, loading, formRef, onSubmit, onSaveAndNew } = await useEntityForm({
|
|
10
16
|
apiEndpoint: '/api/v2/clients',
|
|
11
17
|
routePrefix: '/motor-admin/clients',
|
|
12
18
|
translationPrefix: 'motor-admin.clients',
|
|
13
19
|
formMeta: clientFormMeta,
|
|
14
20
|
formConfig: clientFormConfig,
|
|
15
|
-
mode: 'create'
|
|
21
|
+
mode: 'create',
|
|
22
|
+
beforeSubmit: async (data) => {
|
|
23
|
+
if (!isFrontendConfigEnabled || extensions.value.length === 0) return
|
|
24
|
+
const valid = await validateAll()
|
|
25
|
+
if (!valid) {
|
|
26
|
+
throw new Error(t('motor-core.global.validation_failed'))
|
|
27
|
+
}
|
|
28
|
+
data.frontend_config = getAllSubmitData()
|
|
29
|
+
}
|
|
16
30
|
})
|
|
17
31
|
</script>
|
|
18
32
|
|
|
@@ -32,6 +46,24 @@ const { fields, schema, groups, state, loading, formRef, onSubmit, onSaveAndNew
|
|
|
32
46
|
show-save-and-new
|
|
33
47
|
@submit="onSubmit"
|
|
34
48
|
@save-and-new="onSaveAndNew"
|
|
35
|
-
|
|
49
|
+
>
|
|
50
|
+
<template
|
|
51
|
+
v-if="isFrontendConfigEnabled && extensions.length > 0"
|
|
52
|
+
#after-fields
|
|
53
|
+
>
|
|
54
|
+
<h2 class="text-lg font-semibold text-highlighted mt-2">
|
|
55
|
+
{{ t('motor-admin.clients.frontend_config_title') }}
|
|
56
|
+
</h2>
|
|
57
|
+
<component
|
|
58
|
+
v-for="ext in extensions"
|
|
59
|
+
:key="ext.key"
|
|
60
|
+
:is="ext.component"
|
|
61
|
+
:client-id="undefined"
|
|
62
|
+
:client-record="null"
|
|
63
|
+
:disabled="false"
|
|
64
|
+
mode="create"
|
|
65
|
+
/>
|
|
66
|
+
</template>
|
|
67
|
+
</FormBase>
|
|
36
68
|
</FormPage>
|
|
37
69
|
</template>
|
|
@@ -10,6 +10,7 @@ definePageMeta({ permission: 'email-templates.read' })
|
|
|
10
10
|
type EmailTemplate = components['schemas']['EmailTemplateResource']
|
|
11
11
|
|
|
12
12
|
const { t } = useI18n()
|
|
13
|
+
const client = useSanctumClient()
|
|
13
14
|
|
|
14
15
|
const usageModalOpen = ref(false)
|
|
15
16
|
const usageEndpoint = ref('')
|
|
@@ -28,6 +29,18 @@ const rowActions: RowActionDef<EmailTemplate>[] = [
|
|
|
28
29
|
usageEndpoint.value = `/api/v2/email-templates/${row.id}/usage`
|
|
29
30
|
usageModalOpen.value = true
|
|
30
31
|
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: 'duplicate',
|
|
35
|
+
label: t('motor-core.grid.duplicate'),
|
|
36
|
+
icon: 'i-lucide-copy',
|
|
37
|
+
handler: async (row: EmailTemplate) => {
|
|
38
|
+
const response = await client<{ data: EmailTemplate }>(
|
|
39
|
+
`/api/v2/email-templates/${row.id}/duplicate`,
|
|
40
|
+
{ method: 'POST' }
|
|
41
|
+
)
|
|
42
|
+
await navigateTo(`/motor-admin/email-templates/${response.data.id}/edit`)
|
|
43
|
+
}
|
|
31
44
|
}
|
|
32
45
|
]
|
|
33
46
|
|
|
@@ -92,7 +92,8 @@ export const domainFormMeta = {
|
|
|
92
92
|
protocol: { input: 'text', required: true },
|
|
93
93
|
host: { input: 'text', required: true },
|
|
94
94
|
port: { input: 'number', required: true },
|
|
95
|
-
path: { input: 'text', required: true }
|
|
95
|
+
path: { input: 'text', required: true },
|
|
96
|
+
is_preview_domain: { input: 'toggle' }
|
|
96
97
|
}
|
|
97
98
|
},
|
|
98
99
|
patch: {
|
|
@@ -104,7 +105,8 @@ export const domainFormMeta = {
|
|
|
104
105
|
protocol: { input: 'text', required: true },
|
|
105
106
|
host: { input: 'text', required: true },
|
|
106
107
|
port: { input: 'number', required: true },
|
|
107
|
-
path: { input: 'text', required: true }
|
|
108
|
+
path: { input: 'text', required: true },
|
|
109
|
+
is_preview_domain: { input: 'toggle' }
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
} as const
|
|
@@ -7,10 +7,12 @@ export const aISystemPromptMeta = {
|
|
|
7
7
|
schemaName: 'AISystemPromptResource',
|
|
8
8
|
fields: {
|
|
9
9
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
10
|
+
name: { type: 'string', sortable: true, hideable: false },
|
|
11
|
+
prompt: { type: 'string' },
|
|
10
12
|
client: { type: 'ref', renderer: 'text', ref: 'ClientResource', labelKey: 'name' },
|
|
11
13
|
client_id: { type: 'integer', renderer: 'number' },
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
created_at: { type: 'string' },
|
|
15
|
+
updated_at: { type: 'string' }
|
|
14
16
|
}
|
|
15
17
|
} as const
|
|
16
18
|
|
|
@@ -29,7 +31,10 @@ export const clientMeta = {
|
|
|
29
31
|
is_active: { type: 'boolean', renderer: 'boolean' },
|
|
30
32
|
contact_name: { type: 'string' },
|
|
31
33
|
contact_phone: { type: 'string' },
|
|
32
|
-
contact_email: { type: 'string', renderer: 'link' }
|
|
34
|
+
contact_email: { type: 'string', renderer: 'link' },
|
|
35
|
+
frontend_config: { type: 'unknown' },
|
|
36
|
+
created_at: { type: 'string' },
|
|
37
|
+
updated_at: { type: 'string' }
|
|
33
38
|
}
|
|
34
39
|
} as const
|
|
35
40
|
|
|
@@ -41,7 +46,9 @@ export const configVariableMeta = {
|
|
|
41
46
|
group: { type: 'string' },
|
|
42
47
|
name: { type: 'string', sortable: true, hideable: false },
|
|
43
48
|
value: { type: 'string' },
|
|
44
|
-
is_invisible: { type: 'boolean', renderer: 'boolean' }
|
|
49
|
+
is_invisible: { type: 'boolean', renderer: 'boolean' },
|
|
50
|
+
created_at: { type: 'string' },
|
|
51
|
+
updated_at: { type: 'string' }
|
|
45
52
|
}
|
|
46
53
|
} as const
|
|
47
54
|
|
|
@@ -49,14 +56,17 @@ export const domainMeta = {
|
|
|
49
56
|
schemaName: 'DomainResource',
|
|
50
57
|
fields: {
|
|
51
58
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
59
|
+
name: { type: 'string', sortable: true, hideable: false },
|
|
52
60
|
client: { type: 'ref', renderer: 'text', ref: 'ClientResource', labelKey: 'name' },
|
|
53
61
|
client_id: { type: 'integer', renderer: 'number' },
|
|
54
62
|
is_active: { type: 'boolean', renderer: 'boolean' },
|
|
55
|
-
|
|
63
|
+
is_preview_domain: { type: 'boolean', renderer: 'boolean' },
|
|
56
64
|
protocol: { type: 'string' },
|
|
57
65
|
host: { type: 'string' },
|
|
58
66
|
port: { type: 'integer', renderer: 'number' },
|
|
59
|
-
path: { type: 'string' }
|
|
67
|
+
path: { type: 'string' },
|
|
68
|
+
created_at: { type: 'string' },
|
|
69
|
+
updated_at: { type: 'string' }
|
|
60
70
|
}
|
|
61
71
|
} as const
|
|
62
72
|
|
|
@@ -64,12 +74,12 @@ export const emailTemplateMeta = {
|
|
|
64
74
|
schemaName: 'EmailTemplateResource',
|
|
65
75
|
fields: {
|
|
66
76
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
77
|
+
name: { type: 'string', sortable: true, hideable: false },
|
|
78
|
+
slug: { type: 'string' },
|
|
67
79
|
client: { type: 'ref', renderer: 'text', ref: 'ClientResource', labelKey: 'name' },
|
|
68
80
|
client_id: { type: 'integer', renderer: 'number' },
|
|
69
81
|
language: { type: 'ref', renderer: 'text', ref: 'LanguageResource', labelKey: 'name' },
|
|
70
82
|
language_id: { type: 'integer', renderer: 'number' },
|
|
71
|
-
name: { type: 'string', sortable: true, hideable: false },
|
|
72
|
-
slug: { type: 'string' },
|
|
73
83
|
subject: { type: 'string' },
|
|
74
84
|
body_text: { type: 'string' },
|
|
75
85
|
body_html: { type: 'string' },
|
|
@@ -105,7 +115,9 @@ export const languageMeta = {
|
|
|
105
115
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
106
116
|
iso_639_1: { type: 'string' },
|
|
107
117
|
english_name: { type: 'string' },
|
|
108
|
-
native_name: { type: 'string' }
|
|
118
|
+
native_name: { type: 'string' },
|
|
119
|
+
created_at: { type: 'string' },
|
|
120
|
+
updated_at: { type: 'string' }
|
|
109
121
|
}
|
|
110
122
|
} as const
|
|
111
123
|
|
|
@@ -115,7 +127,10 @@ export const permissionGroupMeta = {
|
|
|
115
127
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
116
128
|
name: { type: 'string', sortable: true, hideable: false },
|
|
117
129
|
sort_position: { type: 'integer', renderer: 'number' },
|
|
118
|
-
|
|
130
|
+
permission_names: { type: 'array' },
|
|
131
|
+
permissions: { type: 'ref[]', renderer: 'list', ref: 'PermissionResource', labelKey: 'name' },
|
|
132
|
+
created_at: { type: 'string' },
|
|
133
|
+
updated_at: { type: 'string' }
|
|
119
134
|
}
|
|
120
135
|
} as const
|
|
121
136
|
|
|
@@ -125,7 +140,9 @@ export const permissionMeta = {
|
|
|
125
140
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
126
141
|
name: { type: 'string', sortable: true, hideable: false },
|
|
127
142
|
guard_name: { type: 'string' },
|
|
128
|
-
permission_group: { type: 'ref', renderer: 'text', ref: 'PermissionGroupResource', labelKey: 'name' }
|
|
143
|
+
permission_group: { type: 'ref', renderer: 'text', ref: 'PermissionGroupResource', labelKey: 'name' },
|
|
144
|
+
created_at: { type: 'string' },
|
|
145
|
+
updated_at: { type: 'string' }
|
|
129
146
|
}
|
|
130
147
|
} as const
|
|
131
148
|
|
|
@@ -135,7 +152,9 @@ export const roleMeta = {
|
|
|
135
152
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
136
153
|
name: { type: 'string', sortable: true, hideable: false },
|
|
137
154
|
guard_name: { type: 'string' },
|
|
138
|
-
permissions: { type: 'ref[]', renderer: 'list', ref: 'PermissionResource', labelKey: 'name' }
|
|
155
|
+
permissions: { type: 'ref[]', renderer: 'list', ref: 'PermissionResource', labelKey: 'name' },
|
|
156
|
+
created_at: { type: 'string' },
|
|
157
|
+
updated_at: { type: 'string' }
|
|
139
158
|
}
|
|
140
159
|
} as const
|
|
141
160
|
|
|
@@ -143,12 +162,13 @@ export const userMeta = {
|
|
|
143
162
|
schemaName: 'UserResource',
|
|
144
163
|
fields: {
|
|
145
164
|
id: { type: 'integer', renderer: 'number', hideable: false },
|
|
165
|
+
name: { type: 'string', sortable: true, hideable: false },
|
|
166
|
+
email: { type: 'string', renderer: 'link' },
|
|
167
|
+
avatar: { type: 'ref', renderer: 'avatar', ref: 'MediaResource' },
|
|
146
168
|
clients: { type: 'ref[]', renderer: 'list', ref: 'ClientResource', labelKey: 'name' },
|
|
147
169
|
roles: { type: 'ref[]', renderer: 'list', ref: 'RoleResource', labelKey: 'name' },
|
|
148
170
|
permissions: { type: 'ref[]', renderer: 'list', ref: 'PermissionResource', labelKey: 'name' },
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
show_onboarding: { type: 'boolean', renderer: 'boolean' },
|
|
152
|
-
avatar: { type: 'ref', renderer: 'avatar', ref: 'MediaResource' }
|
|
171
|
+
created_at: { type: 'string' },
|
|
172
|
+
updated_at: { type: 'string' }
|
|
153
173
|
}
|
|
154
174
|
} as const
|
package/package.json
CHANGED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
<!-- app/components/client/FrontendConfigSection.vue -->
|
|
2
|
-
<script setup lang="ts">
|
|
3
|
-
import type { FormFieldConfig, FormGroupConfig } from '@motor-cms/ui-core/app/types/form'
|
|
4
|
-
|
|
5
|
-
const props = defineProps<{
|
|
6
|
-
state: Record<string, unknown>
|
|
7
|
-
fields: FormFieldConfig[]
|
|
8
|
-
groups: FormGroupConfig[]
|
|
9
|
-
errors: Record<string, string>
|
|
10
|
-
disabled?: boolean
|
|
11
|
-
colorSchemeOptions: { label: string, value: string }[]
|
|
12
|
-
logoSlugOptions: { label: string, value: string }[]
|
|
13
|
-
}>()
|
|
14
|
-
|
|
15
|
-
const appSettings = useAppSettingsStore()
|
|
16
|
-
const isCompact = computed(() => appSettings.formLayout === 'compact')
|
|
17
|
-
|
|
18
|
-
const formFieldUi = computed(() => isCompact.value
|
|
19
|
-
? { container: 'w-full' }
|
|
20
|
-
: { container: 'w-full max-w-2xl' }
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
const cardUi = computed(() => isCompact.value
|
|
24
|
-
? { root: 'relative flex rounded-lg items-start', container: 'relative flex flex-col p-4 sm:p-6 gap-x-8 gap-y-4', wrapper: 'flex flex-col items-start', body: '' }
|
|
25
|
-
: undefined
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
// ============================================
|
|
29
|
-
// Dot-path helpers
|
|
30
|
-
// ============================================
|
|
31
|
-
|
|
32
|
-
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
|
33
|
-
return path.split('.').reduce<unknown>((current, key) => {
|
|
34
|
-
if (current != null && typeof current === 'object') {
|
|
35
|
-
return (current as Record<string, unknown>)[key]
|
|
36
|
-
}
|
|
37
|
-
return undefined
|
|
38
|
-
}, obj)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
|
|
42
|
-
const keys = path.split('.')
|
|
43
|
-
let current: Record<string, unknown> = obj
|
|
44
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
45
|
-
const key = keys[i]!
|
|
46
|
-
if (current[key] == null || typeof current[key] !== 'object') {
|
|
47
|
-
current[key] = {}
|
|
48
|
-
}
|
|
49
|
-
current = current[key] as Record<string, unknown>
|
|
50
|
-
}
|
|
51
|
-
const lastKey = keys[keys.length - 1]!
|
|
52
|
-
current[lastKey] = value
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ============================================
|
|
56
|
-
// Grouped field helpers
|
|
57
|
-
// ============================================
|
|
58
|
-
|
|
59
|
-
function fieldsForGroup(groupKey: string): FormFieldConfig[] {
|
|
60
|
-
return props.fields.filter(f => f.group === groupKey)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ============================================
|
|
64
|
-
// Options resolver
|
|
65
|
-
// ============================================
|
|
66
|
-
|
|
67
|
-
function optionsForField(fieldKey: string): { label: string, value: string }[] {
|
|
68
|
-
if (fieldKey === 'colorScheme') return props.colorSchemeOptions
|
|
69
|
-
if (fieldKey === 'logoSlug') return props.logoSlugOptions
|
|
70
|
-
return []
|
|
71
|
-
}
|
|
72
|
-
</script>
|
|
73
|
-
|
|
74
|
-
<template>
|
|
75
|
-
<div :class="isCompact ? 'grid grid-cols-2 gap-4' : 'flex flex-col gap-4'">
|
|
76
|
-
<UPageCard
|
|
77
|
-
v-for="(group, groupIdx) in groups"
|
|
78
|
-
:key="group.key"
|
|
79
|
-
:class="isCompact && groups.length % 2 === 1 && groupIdx === groups.length - 1 ? 'col-span-2' : ''"
|
|
80
|
-
:title="group.label"
|
|
81
|
-
:ui="cardUi"
|
|
82
|
-
>
|
|
83
|
-
<div :class="isCompact ? 'grid grid-cols-12 gap-x-4 gap-y-3' : 'space-y-4'">
|
|
84
|
-
<template
|
|
85
|
-
v-for="field in fieldsForGroup(group.key)"
|
|
86
|
-
:key="field.key"
|
|
87
|
-
>
|
|
88
|
-
<div :class="isCompact ? 'col-span-12' : ''">
|
|
89
|
-
<UFormField
|
|
90
|
-
:label="field.label"
|
|
91
|
-
:name="field.key"
|
|
92
|
-
:required="field.required"
|
|
93
|
-
:error="errors[field.key]"
|
|
94
|
-
:orientation="isCompact ? 'vertical' : 'horizontal'"
|
|
95
|
-
:ui="formFieldUi"
|
|
96
|
-
>
|
|
97
|
-
<!-- Toggle -->
|
|
98
|
-
<USwitch
|
|
99
|
-
v-if="field.input === 'toggle'"
|
|
100
|
-
:model-value="(getNestedValue(state, field.key) as boolean) ?? false"
|
|
101
|
-
:disabled="disabled"
|
|
102
|
-
@update:model-value="setNestedValue(state, field.key, $event)"
|
|
103
|
-
/>
|
|
104
|
-
|
|
105
|
-
<!-- Select -->
|
|
106
|
-
<USelectMenu
|
|
107
|
-
v-else-if="field.input === 'select'"
|
|
108
|
-
:model-value="(getNestedValue(state, field.key) as string | undefined)"
|
|
109
|
-
:items="optionsForField(field.key)"
|
|
110
|
-
value-key="value"
|
|
111
|
-
label-key="label"
|
|
112
|
-
:placeholder="field.label"
|
|
113
|
-
:disabled="disabled"
|
|
114
|
-
class="w-full"
|
|
115
|
-
@update:model-value="setNestedValue(state, field.key, $event)"
|
|
116
|
-
/>
|
|
117
|
-
|
|
118
|
-
<!-- Text / Email / URL -->
|
|
119
|
-
<UInput
|
|
120
|
-
v-else
|
|
121
|
-
:model-value="(getNestedValue(state, field.key) as string) ?? ''"
|
|
122
|
-
:type="field.input === 'email' ? 'email' : field.input === 'url' ? 'url' : 'text'"
|
|
123
|
-
:disabled="disabled"
|
|
124
|
-
class="w-full"
|
|
125
|
-
@update:model-value="setNestedValue(state, field.key, $event)"
|
|
126
|
-
/>
|
|
127
|
-
</UFormField>
|
|
128
|
-
</div>
|
|
129
|
-
</template>
|
|
130
|
-
</div>
|
|
131
|
-
</UPageCard>
|
|
132
|
-
</div>
|
|
133
|
-
</template>
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import type { Ref, ComputedRef } from 'vue'
|
|
2
|
-
import type { FormFieldConfig, FormGroupConfig } from '@motor-cms/ui-core/app/types/form'
|
|
3
|
-
import {
|
|
4
|
-
type FrontendConfigFormState,
|
|
5
|
-
frontendConfigSchema,
|
|
6
|
-
emptyFrontendConfig,
|
|
7
|
-
frontendConfigFields,
|
|
8
|
-
frontendConfigGroups
|
|
9
|
-
} from '../types/frontend-config'
|
|
10
|
-
|
|
11
|
-
// ============================================
|
|
12
|
-
// Types
|
|
13
|
-
// ============================================
|
|
14
|
-
|
|
15
|
-
export interface UseClientFrontendConfigOptions {
|
|
16
|
-
clientRecord: Ref<{ data: Record<string, unknown> } | null | undefined>
|
|
17
|
-
fetching: Ref<boolean>
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface UseClientFrontendConfigReturn {
|
|
21
|
-
state: FrontendConfigFormState
|
|
22
|
-
errors: Ref<Record<string, string>>
|
|
23
|
-
fields: ComputedRef<FormFieldConfig[]>
|
|
24
|
-
groups: ComputedRef<FormGroupConfig[]>
|
|
25
|
-
validate(): boolean
|
|
26
|
-
getSubmitData(): Record<string, unknown>
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// ============================================
|
|
30
|
-
// Composable
|
|
31
|
-
// ============================================
|
|
32
|
-
|
|
33
|
-
export function useClientFrontendConfig(
|
|
34
|
-
options: UseClientFrontendConfigOptions
|
|
35
|
-
): UseClientFrontendConfigReturn {
|
|
36
|
-
const { t } = useI18n()
|
|
37
|
-
|
|
38
|
-
// Reactive form state — starts empty, hydrated on load
|
|
39
|
-
const state = reactive<FrontendConfigFormState>(emptyFrontendConfig())
|
|
40
|
-
|
|
41
|
-
// Flat validation error map: dot-path → first message
|
|
42
|
-
const errors = ref<Record<string, string>>({})
|
|
43
|
-
|
|
44
|
-
// ============================================
|
|
45
|
-
// Hydration
|
|
46
|
-
// ============================================
|
|
47
|
-
|
|
48
|
-
watch(
|
|
49
|
-
() => options.fetching.value,
|
|
50
|
-
(isFetching) => {
|
|
51
|
-
// Hydrate when fetching completes (or data is already loaded)
|
|
52
|
-
if (!isFetching) {
|
|
53
|
-
const raw = options.clientRecord.value?.data?.frontend_config
|
|
54
|
-
if (!raw) return
|
|
55
|
-
|
|
56
|
-
const parsed = frontendConfigSchema(t).safeParse(raw)
|
|
57
|
-
|
|
58
|
-
if (parsed.success) {
|
|
59
|
-
Object.assign(state, parsed.data)
|
|
60
|
-
} else {
|
|
61
|
-
// Best-effort deep merge of whatever passed validation
|
|
62
|
-
const rawConfig = raw as Record<string, unknown>
|
|
63
|
-
for (const key of Object.keys(state) as Array<keyof FrontendConfigFormState>) {
|
|
64
|
-
const incoming = rawConfig[key]
|
|
65
|
-
if (incoming !== undefined && incoming !== null) {
|
|
66
|
-
if (typeof incoming === 'object' && !Array.isArray(incoming)) {
|
|
67
|
-
Object.assign(
|
|
68
|
-
state[key] as Record<string, unknown>,
|
|
69
|
-
incoming as Record<string, unknown>
|
|
70
|
-
)
|
|
71
|
-
} else {
|
|
72
|
-
// @ts-expect-error -- best-effort assignment for primitive fields
|
|
73
|
-
state[key] = incoming
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
{ immediate: true }
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
// ============================================
|
|
84
|
-
// Validation
|
|
85
|
-
// ============================================
|
|
86
|
-
|
|
87
|
-
function validate(): boolean {
|
|
88
|
-
const result = frontendConfigSchema(t).safeParse(state)
|
|
89
|
-
if (result.success) {
|
|
90
|
-
errors.value = {}
|
|
91
|
-
return true
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const flat: Record<string, string> = {}
|
|
95
|
-
for (const issue of result.error.issues) {
|
|
96
|
-
const path = issue.path.join('.')
|
|
97
|
-
if (!flat[path]) {
|
|
98
|
-
flat[path] = issue.message
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
errors.value = flat
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ============================================
|
|
106
|
-
// Submit data
|
|
107
|
-
// ============================================
|
|
108
|
-
|
|
109
|
-
function getSubmitData(): Record<string, unknown> {
|
|
110
|
-
const clone = JSON.parse(JSON.stringify(state)) as Record<string, unknown>
|
|
111
|
-
|
|
112
|
-
// Preserve globalComponents from the raw API record — these are
|
|
113
|
-
// footer UUIDs managed separately (e.g. by GlobalComponentsSection) and
|
|
114
|
-
// must not be overwritten by the frontend config form.
|
|
115
|
-
const originalConfig = options.clientRecord.value?.data?.frontend_config as
|
|
116
|
-
| { globalComponents?: Record<string, unknown> }
|
|
117
|
-
| undefined
|
|
118
|
-
|
|
119
|
-
if (originalConfig?.globalComponents !== undefined) {
|
|
120
|
-
clone.globalComponents = originalConfig.globalComponents
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return clone
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ============================================
|
|
127
|
-
// Field / group definitions (i18n-aware)
|
|
128
|
-
// ============================================
|
|
129
|
-
|
|
130
|
-
const fields = computed<FormFieldConfig[]>(() => frontendConfigFields(t))
|
|
131
|
-
const groups = computed<FormGroupConfig[]>(() => frontendConfigGroups(t))
|
|
132
|
-
|
|
133
|
-
// ============================================
|
|
134
|
-
// Return
|
|
135
|
-
// ============================================
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
state,
|
|
139
|
-
errors,
|
|
140
|
-
fields,
|
|
141
|
-
groups,
|
|
142
|
-
validate,
|
|
143
|
-
getSubmitData
|
|
144
|
-
}
|
|
145
|
-
}
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import type { FormFieldConfig, FormGroupConfig } from '@motor-cms/ui-core/app/types/form'
|
|
3
|
-
|
|
4
|
-
// ============================================
|
|
5
|
-
// FrontendConfig Interface (full JSON column)
|
|
6
|
-
// ============================================
|
|
7
|
-
|
|
8
|
-
export interface FrontendConfig {
|
|
9
|
-
brand: {
|
|
10
|
-
name: string
|
|
11
|
-
logoAlt: string
|
|
12
|
-
}
|
|
13
|
-
colorScheme: string
|
|
14
|
-
logoSlug: string
|
|
15
|
-
contact: {
|
|
16
|
-
contactUrl: string
|
|
17
|
-
email: string
|
|
18
|
-
whatsappUrl: string | null
|
|
19
|
-
}
|
|
20
|
-
features: {
|
|
21
|
-
orderLine: boolean
|
|
22
|
-
appointments: boolean
|
|
23
|
-
clickpath: boolean
|
|
24
|
-
footerMenu: boolean
|
|
25
|
-
}
|
|
26
|
-
social: {
|
|
27
|
-
instagram: string | null
|
|
28
|
-
facebook: string | null
|
|
29
|
-
}
|
|
30
|
-
seo: {
|
|
31
|
-
siteName: string
|
|
32
|
-
}
|
|
33
|
-
globalComponents?: {
|
|
34
|
-
footer?: Record<string, string> // language_id -> builder_page_uuid
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ============================================
|
|
39
|
-
// Zod Schema (editable fields only, no globalComponents)
|
|
40
|
-
// ============================================
|
|
41
|
-
|
|
42
|
-
type TranslateFunction = (key: string) => string
|
|
43
|
-
|
|
44
|
-
function optionalUrlSchema(t: TranslateFunction) {
|
|
45
|
-
return z.string().refine(
|
|
46
|
-
(val) => val === '' || val === null || (() => { try { new URL(val); return true } catch { return false } })(),
|
|
47
|
-
{ message: t('motor-core.global.validation_url') }
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function frontendConfigSchema(t: TranslateFunction) {
|
|
52
|
-
return z.object({
|
|
53
|
-
brand: z.object({
|
|
54
|
-
name: z.string().min(1, { message: t('motor-core.global.validation_required') }),
|
|
55
|
-
logoAlt: z.string().min(1, { message: t('motor-core.global.validation_required') })
|
|
56
|
-
}),
|
|
57
|
-
colorScheme: z.string().min(1, { message: t('motor-core.global.validation_required') }),
|
|
58
|
-
logoSlug: z.string().min(1, { message: t('motor-core.global.validation_required') }),
|
|
59
|
-
contact: z.object({
|
|
60
|
-
contactUrl: z.string().url({ message: t('motor-core.global.validation_url') }),
|
|
61
|
-
email: z.string().email({ message: t('motor-core.global.validation_email') }),
|
|
62
|
-
whatsappUrl: optionalUrlSchema(t).nullable().optional().transform((v) => v ?? null)
|
|
63
|
-
}),
|
|
64
|
-
features: z.object({
|
|
65
|
-
orderLine: z.boolean().default(false),
|
|
66
|
-
appointments: z.boolean().default(false),
|
|
67
|
-
clickpath: z.boolean().default(false),
|
|
68
|
-
footerMenu: z.boolean().default(false)
|
|
69
|
-
}),
|
|
70
|
-
social: z.object({
|
|
71
|
-
instagram: optionalUrlSchema(t).nullable().optional().transform((v) => v ?? null),
|
|
72
|
-
facebook: optionalUrlSchema(t).nullable().optional().transform((v) => v ?? null)
|
|
73
|
-
}),
|
|
74
|
-
seo: z.object({
|
|
75
|
-
siteName: z.string().min(1, { message: t('motor-core.global.validation_required') })
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export type FrontendConfigFormState = z.infer<ReturnType<typeof frontendConfigSchema>>
|
|
81
|
-
|
|
82
|
-
// ============================================
|
|
83
|
-
// Form Field Definitions
|
|
84
|
-
// ============================================
|
|
85
|
-
|
|
86
|
-
export function frontendConfigFields(t: (key: string) => string): FormFieldConfig[] {
|
|
87
|
-
return [
|
|
88
|
-
// Brand group
|
|
89
|
-
{
|
|
90
|
-
key: 'brand.name',
|
|
91
|
-
label: t('motor-admin.clients.frontend_config.brand_name'),
|
|
92
|
-
input: 'text',
|
|
93
|
-
required: true,
|
|
94
|
-
group: 'fc_brand'
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
key: 'brand.logoAlt',
|
|
98
|
-
label: t('motor-admin.clients.frontend_config.brand_logo_alt'),
|
|
99
|
-
input: 'text',
|
|
100
|
-
required: true,
|
|
101
|
-
group: 'fc_brand'
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
key: 'colorScheme',
|
|
105
|
-
label: t('motor-admin.clients.frontend_config.color_scheme'),
|
|
106
|
-
input: 'select',
|
|
107
|
-
required: true,
|
|
108
|
-
group: 'fc_brand'
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
key: 'logoSlug',
|
|
112
|
-
label: t('motor-admin.clients.frontend_config.logo_slug'),
|
|
113
|
-
input: 'select',
|
|
114
|
-
required: true,
|
|
115
|
-
group: 'fc_brand'
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
// Contact group
|
|
119
|
-
{
|
|
120
|
-
key: 'contact.contactUrl',
|
|
121
|
-
label: t('motor-admin.clients.frontend_config.contact_url'),
|
|
122
|
-
input: 'url',
|
|
123
|
-
required: true,
|
|
124
|
-
group: 'fc_contact'
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
key: 'contact.email',
|
|
128
|
-
label: t('motor-admin.clients.frontend_config.contact_email'),
|
|
129
|
-
input: 'email',
|
|
130
|
-
required: true,
|
|
131
|
-
group: 'fc_contact'
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
key: 'contact.whatsappUrl',
|
|
135
|
-
label: t('motor-admin.clients.frontend_config.contact_whatsapp_url'),
|
|
136
|
-
input: 'url',
|
|
137
|
-
required: false,
|
|
138
|
-
group: 'fc_contact'
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
// Features group
|
|
142
|
-
{
|
|
143
|
-
key: 'features.orderLine',
|
|
144
|
-
label: t('motor-admin.clients.frontend_config.features_order_line'),
|
|
145
|
-
input: 'toggle',
|
|
146
|
-
required: false,
|
|
147
|
-
group: 'fc_features'
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
key: 'features.appointments',
|
|
151
|
-
label: t('motor-admin.clients.frontend_config.features_appointments'),
|
|
152
|
-
input: 'toggle',
|
|
153
|
-
required: false,
|
|
154
|
-
group: 'fc_features'
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
key: 'features.clickpath',
|
|
158
|
-
label: t('motor-admin.clients.frontend_config.features_clickpath'),
|
|
159
|
-
input: 'toggle',
|
|
160
|
-
required: false,
|
|
161
|
-
group: 'fc_features'
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
key: 'features.footerMenu',
|
|
165
|
-
label: t('motor-admin.clients.frontend_config.features_footer_menu'),
|
|
166
|
-
input: 'toggle',
|
|
167
|
-
required: false,
|
|
168
|
-
group: 'fc_features'
|
|
169
|
-
},
|
|
170
|
-
|
|
171
|
-
// Social group
|
|
172
|
-
{
|
|
173
|
-
key: 'social.instagram',
|
|
174
|
-
label: t('motor-admin.clients.frontend_config.social_instagram'),
|
|
175
|
-
input: 'url',
|
|
176
|
-
required: false,
|
|
177
|
-
group: 'fc_social'
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
key: 'social.facebook',
|
|
181
|
-
label: t('motor-admin.clients.frontend_config.social_facebook'),
|
|
182
|
-
input: 'url',
|
|
183
|
-
required: false,
|
|
184
|
-
group: 'fc_social'
|
|
185
|
-
},
|
|
186
|
-
|
|
187
|
-
// SEO group
|
|
188
|
-
{
|
|
189
|
-
key: 'seo.siteName',
|
|
190
|
-
label: t('motor-admin.clients.frontend_config.seo_site_name'),
|
|
191
|
-
input: 'text',
|
|
192
|
-
required: true,
|
|
193
|
-
group: 'fc_seo'
|
|
194
|
-
}
|
|
195
|
-
]
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ============================================
|
|
199
|
-
// Form Group Definitions
|
|
200
|
-
// ============================================
|
|
201
|
-
|
|
202
|
-
export function frontendConfigGroups(t: (key: string) => string): FormGroupConfig[] {
|
|
203
|
-
return [
|
|
204
|
-
{
|
|
205
|
-
key: 'fc_brand',
|
|
206
|
-
label: t('motor-admin.clients.frontend_config.group_brand')
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
key: 'fc_contact',
|
|
210
|
-
label: t('motor-admin.clients.frontend_config.group_contact')
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
key: 'fc_features',
|
|
214
|
-
label: t('motor-admin.clients.frontend_config.group_features')
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
key: 'fc_social',
|
|
218
|
-
label: t('motor-admin.clients.frontend_config.group_social')
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
key: 'fc_seo',
|
|
222
|
-
label: t('motor-admin.clients.frontend_config.group_seo')
|
|
223
|
-
}
|
|
224
|
-
]
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ============================================
|
|
228
|
-
// Default Empty State
|
|
229
|
-
// ============================================
|
|
230
|
-
|
|
231
|
-
export function emptyFrontendConfig(): FrontendConfigFormState {
|
|
232
|
-
return {
|
|
233
|
-
brand: {
|
|
234
|
-
name: '',
|
|
235
|
-
logoAlt: ''
|
|
236
|
-
},
|
|
237
|
-
colorScheme: '',
|
|
238
|
-
logoSlug: '',
|
|
239
|
-
contact: {
|
|
240
|
-
contactUrl: '',
|
|
241
|
-
email: '',
|
|
242
|
-
whatsappUrl: null
|
|
243
|
-
},
|
|
244
|
-
features: {
|
|
245
|
-
orderLine: false,
|
|
246
|
-
appointments: false,
|
|
247
|
-
clickpath: false,
|
|
248
|
-
footerMenu: false
|
|
249
|
-
},
|
|
250
|
-
social: {
|
|
251
|
-
instagram: null,
|
|
252
|
-
facebook: null
|
|
253
|
-
},
|
|
254
|
-
seo: {
|
|
255
|
-
siteName: ''
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|