@motor-cms/ui-admin 4.0.5 → 4.0.6

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.
@@ -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
- "frontend_config": {
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
- "frontend_config": {
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
- if (!validateFrontendConfig()) {
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 = getFrontendConfigSubmitData()
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
- name: freshClient.data.name,
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
- name: freshClient.data.name,
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
- <ClientFrontendConfigSection
178
- :state="frontendConfigState"
179
- :fields="frontendConfigFields"
180
- :groups="frontendConfigGroups"
181
- :errors="frontendConfigErrors"
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
- :color-scheme-options="colorSchemeOptions"
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motor-cms/ui-admin",
3
- "version": "4.0.5",
3
+ "version": "4.0.6",
4
4
  "type": "module",
5
5
  "main": "./nuxt.config.ts",
6
6
  "files": [
@@ -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
- }