diff --git a/admin/src/components/form/formState.ts b/admin/src/components/form/formState.ts index 0ecc0c4..38df095 100644 --- a/admin/src/components/form/formState.ts +++ b/admin/src/components/form/formState.ts @@ -92,6 +92,91 @@ export function presetValue(type: string, source: unknown): string { .replace(/^-+|-+$/g, '') } +/** The locales whose text differs between two locale maps: every locale on + * either side, a missing key counting as ''. A value that is not a map counts + * as an empty map. */ +export function changedLocales(previous: unknown, next: unknown): string[] { + const before = localeRecord(previous) + const after = localeRecord(next) + const out: string[] = [] + for (const locale of new Set([...Object.keys(before), ...Object.keys(after)])) { + if ((before[locale] ?? '') !== (after[locale] ?? '')) { + out.push(locale) + } + } + return out +} + +/** Reports whether the administrator edited a preset target by hand: the + * whole field when no locale is given, one locale of an ML target otherwise. */ +export type PresetEdited = (target: string, locale?: string) => boolean + +function isMLType(type: string | undefined): boolean { + return type === 'mltext' || type === 'mlmarkdown' +} + +/** + * The preset targets of `source` (fields.yaml `preset`) to rewrite after the + * source changed from `previous` to `next`: target name -> new value. Targets + * are text and mltext fields; a preset on any other type is ignored. + * + * - text -> text: the target takes presetValue of the source, until edited. + * - ML -> text: the target takes presetValue of the source's active-locale text. + * - text -> ML: only the target's active locale is written; its other locales stay. + * - ML -> ML: per locale. Each locale whose source text changed rewrites the + * same locale of the target, unless the administrator edited that locale by + * hand; the target's other locales stay. + * + * A preset never writes a locale the administrator neither typed in (source) + * nor sees (target), so untouched locales keep the server's fallback. + */ +export function presetUpdates( + fields: FormField[], + source: string, + previous: unknown, + next: unknown, + values: AdminRecord, + activeLocale: string, + isEdited: PresetEdited, +): AdminRecord { + const out: AdminRecord = {} + const sourceML = isMLType(fields.find((field) => field.name === source)?.type) + for (const field of fields) { + const preset = field.preset + if (!preset || preset.field !== source || (field.type !== 'text' && field.type !== 'mltext')) { + continue + } + if (field.type === 'text') { + if (isEdited(field.name)) { + continue + } + out[field.name] = presetValue(preset.type, sourceML ? (localeRecord(next)[activeLocale] ?? '') : next) + continue + } + if (!sourceML) { + if (isEdited(field.name, activeLocale)) { + continue + } + out[field.name] = { ...localeRecord(values[field.name]), [activeLocale]: presetValue(preset.type, next) } + continue + } + const texts = localeRecord(next) + const target = { ...localeRecord(values[field.name]) } + let written = false + for (const locale of changedLocales(previous, next)) { + if (isEdited(field.name, locale)) { + continue + } + target[locale] = presetValue(preset.type, texts[locale] ?? '') + written = true + } + if (written) { + out[field.name] = target + } + } + return out +} + /** 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 { diff --git a/admin/src/views/FormView.vue b/admin/src/views/FormView.vue index 785b1c8..313b313 100644 --- a/admin/src/views/FormView.vue +++ b/admin/src/views/FormView.vue @@ -1,5 +1,5 @@ +