feat(14.2.1-05): hydrate ML create/GET/save through TranslatedExact

Form schema meta lists enabled locales, Show/save expand mltext maps
without D-11 fallback, and the SPA seeds and adopt-merges every locale.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-06 15:37:00 +02:00
parent ed81f2a0c1
commit 0f8d13738d
11 changed files with 309 additions and 34 deletions

View File

@@ -1,16 +1,18 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { computed, inject, onMounted, onUnmounted, ref } from 'vue'
import type { FieldControlProps } from '../control'
import TextField from './TextField.vue'
import { FORM_ENABLED_LOCALES } from '../formContext'
import { ML_LOCALE_CHANGE, broadcastMLLocale, localeRecord } from '../mlLocale'
const props = defineProps<FieldControlProps>()
const emit = defineEmits<{ 'update:modelValue': [value: Record<string, string>] }>()
const values = computed(() => localeRecord(props.modelValue))
const enabledLocales = inject(FORM_ENABLED_LOCALES, null)
const locales = computed(() => {
const keys = Object.keys(values.value)
return keys.length > 0 ? keys : ['en']
const codes = enabledLocales?.value
return codes && codes.length > 0 ? [...codes] : []
})
const activeLocale = ref(locales.value[0] ?? 'en')
const copyFrom = ref('')

View File

@@ -16,6 +16,9 @@ export const FORM_PATCH: InjectionKey<(name: string, value: unknown) => void> =
/** The form schema's locale (meta.locale). */
export const FORM_LOCALE: InjectionKey<Readonly<Ref<string>>> = Symbol('summer.form.locale')
/** Enabled content locales for ML controls (schema meta.enabledLocales). */
export const FORM_ENABLED_LOCALES: InjectionKey<Readonly<Ref<readonly string[]>>> = Symbol('summer.form.enabledLocales')
/**
* The form controller's plugin assets: resolves once its scripts have loaded
* or failed, with the failed URLs (pluginAssets.loadControllerAssets).

View File

@@ -92,8 +92,9 @@ export function presetValue(type: string, source: unknown): string {
.replace(/^-+|-+$/g, '')
}
/** Initial values of a new record: schema defaults, toggles off, no ids. */
export function initialValues(fields: FormField[]): AdminRecord {
/** Initial values of a new record: schema defaults, toggles off, no ids.
* ML fields seed an empty string for every enabled locale. */
export function initialValues(fields: FormField[], enabledLocales: readonly string[] = []): AdminRecord {
const out: AdminRecord = {}
for (const field of fields) {
if (field.default !== undefined) {
@@ -103,12 +104,38 @@ export function initialValues(fields: FormField[]): AdminRecord {
} else if (field.type === 'relation' && field.multiple) {
out[field.name] = []
} else if (field.type === 'mltext' || field.type === 'mlmarkdown') {
out[field.name] = {}
const seed: Record<string, string> = {}
for (const code of enabledLocales) {
seed[code] = ''
}
out[field.name] = seed
}
}
return out
}
/** Content locales from form schema meta; empty when the writer is unpublished. */
export function schemaEnabledLocales(meta: { enabledLocales?: readonly string[] } | null | undefined): readonly string[] {
return meta?.enabledLocales ?? []
}
/** Merge a GET/save ML value onto a seed map of every enabled locale. A host
* string becomes the first enabled locale; it never replaces sibling keys. */
export function mergeMLValue(incoming: unknown, enabledLocales: readonly string[]): Record<string, string> {
const seeded: Record<string, string> = {}
for (const code of enabledLocales) {
seeded[code] = ''
}
if (typeof incoming === 'string') {
const fallback = enabledLocales[0]
if (fallback) {
seeded[fallback] = incoming
}
return seeded
}
return { ...seeded, ...localeRecord(incoming) }
}
/** D-10 422 details (field -> messages) as a string-list map. */
export function fieldErrors(details: ErrorBody['details'] | undefined): Record<string, string[]> {
const out: Record<string, string[]> = {}

View File

@@ -13,7 +13,7 @@ import { mapWinterUrl } from '../app/winterUrl'
import FormErrorBanner from '../components/form/FormErrorBanner.vue'
import FormGrid from '../components/form/FormGrid.vue'
import FormTabs from '../components/form/FormTabs.vue'
import { FORM_ASSETS, FORM_LOCALE, FORM_PATCH, FORM_SESSION, FORM_VALUES, createUploadGate } from '../components/form/formContext'
import { FORM_ASSETS, FORM_ENABLED_LOCALES, FORM_LOCALE, FORM_PATCH, FORM_SESSION, FORM_VALUES, createUploadGate } from '../components/form/formContext'
import { RELATION_MANAGER, needsRecord } from '../components/form/registry'
import {
DEFAULT_TAB,
@@ -22,8 +22,10 @@ import {
fieldErrors,
focusField,
initialValues,
mergeMLValue,
panelDomId,
presetValue,
schemaEnabledLocales,
snapshot,
tabDomId,
tabOf,
@@ -168,9 +170,16 @@ const dirty = computed(
function adopt(record: RecordEnvelope | undefined): void {
if (record) {
// A record response never carries a password, so every password field
// is empty again after a save (UI-SPEC S7).
values.value = { ...record.data }
const locales = schemaEnabledLocales(schema.value?.meta)
const next: AdminRecord = { ...record.data }
for (const field of fields.value) {
if (field.type === 'password') {
next[field.name] = ''
} else if (field.type === 'mltext' || field.type === 'mlmarkdown') {
next[field.name] = mergeMLValue(record.data[field.name], locales)
}
}
values.value = next
labels.value = record.meta.labels ?? {}
}
saved.value = snapshot(editablePayload(fields.value, values.value, mode))
@@ -193,7 +202,7 @@ async function load(): Promise<void> {
assetsReady = loadControllerAssets(controllerId, schema.value.assets)
}
if (schema.value && mode === 'create') {
values.value = initialValues(fields.value)
values.value = initialValues(fields.value, schemaEnabledLocales(schema.value.meta))
}
adopt(recordResult?.data)
activeTab.value = tabs.value[0]?.key ?? DEFAULT_TAB
@@ -229,6 +238,7 @@ function update(name: string, value: unknown): void {
provide(FORM_VALUES, computed(() => values.value))
provide(FORM_PATCH, update)
provide(FORM_LOCALE, computed(() => schema.value?.meta.locale || currentLocale.value))
provide(FORM_ENABLED_LOCALES, computed(() => schemaEnabledLocales(schema.value?.meta)))
provide(FORM_ASSETS, () => assetsReady)
provide(FORM_SESSION, {
key: sessionKey,