feat(14.2.1-05): cover mlmarkdown, child adopt, and regen admin artifacts
MLMarkdownField and relation-child forms share the enabled-locale inject and adopt merge; OpenAPI, types, docs, and boardwalk dist match. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -913,6 +913,12 @@
|
||||
},
|
||||
"cabana.FormMeta": {
|
||||
"properties": {
|
||||
"enabledLocales": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
1
admin/src/api/schema.d.ts
vendored
1
admin/src/api/schema.d.ts
vendored
@@ -4506,6 +4506,7 @@ export interface components {
|
||||
update: components["schemas"]["cabana.MessageForms"];
|
||||
};
|
||||
"cabana.FormMeta": {
|
||||
enabledLocales?: string[];
|
||||
locale: string;
|
||||
};
|
||||
"cabana.FormOption": {
|
||||
|
||||
@@ -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 MarkdownField from './MarkdownField.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('')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, provide, readonly, ref, watch } from 'vue'
|
||||
import { computed, inject, nextTick, provide, readonly, ref, watch } from 'vue'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
@@ -17,7 +17,7 @@ import { CHILD_SESSION_HEADER, SESSION_HEADER, newSessionKey } from '../../app/s
|
||||
import FormErrorBanner from '../form/FormErrorBanner.vue'
|
||||
import FormGrid from '../form/FormGrid.vue'
|
||||
import FormTabs from '../form/FormTabs.vue'
|
||||
import { FORM_PATCH, FORM_SESSION, FORM_VALUES, createUploadGate, type FormSession } from '../form/formContext'
|
||||
import { FORM_ENABLED_LOCALES, FORM_PATCH, FORM_SESSION, FORM_VALUES, createUploadGate, type FormSession } from '../form/formContext'
|
||||
import {
|
||||
DEFAULT_TAB,
|
||||
contextAllows,
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
fieldErrors,
|
||||
focusField,
|
||||
initialValues,
|
||||
mergeMLValue,
|
||||
panelDomId,
|
||||
snapshot,
|
||||
tabDomId,
|
||||
@@ -198,6 +199,8 @@ const session: FormSession = {
|
||||
provide(FORM_SESSION, session)
|
||||
provide(FORM_VALUES, computed(() => values.value))
|
||||
provide(FORM_PATCH, update)
|
||||
const enabledLocales = inject(FORM_ENABLED_LOCALES, computed(() => [] as readonly string[]))
|
||||
provide(FORM_ENABLED_LOCALES, enabledLocales)
|
||||
|
||||
function update(name: string, value: unknown): void {
|
||||
values.value = { ...values.value, [name]: value }
|
||||
@@ -209,7 +212,16 @@ function update(name: string, value: unknown): void {
|
||||
}
|
||||
|
||||
function adopt(record: AdminRecord, meta?: RecordMeta): void {
|
||||
values.value = { ...record }
|
||||
const locales = enabledLocales.value
|
||||
const next: AdminRecord = { ...record }
|
||||
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[field.name], locales)
|
||||
}
|
||||
}
|
||||
values.value = next
|
||||
labels.value = meta?.labels ?? {}
|
||||
saved.value = snapshot(editablePayload(fields.value, values.value))
|
||||
}
|
||||
@@ -239,7 +251,7 @@ async function load(): Promise<void> {
|
||||
activeTab.value = DEFAULT_TAB
|
||||
if (props.mode === 'create' || props.childId === null) {
|
||||
loading.value = false
|
||||
adopt(initialValues(fields.value))
|
||||
adopt(initialValues(fields.value, enabledLocales.value))
|
||||
activeTab.value = tabs.value[0]?.key ?? DEFAULT_TAB
|
||||
await focusFirst()
|
||||
return
|
||||
|
||||
@@ -228,7 +228,7 @@ async function goNext(): Promise<void> {
|
||||
return
|
||||
}
|
||||
step.value = 2
|
||||
pivotValues.value = initialValues(pivotFields.value)
|
||||
pivotValues.value = initialValues(pivotFields.value, [])
|
||||
pivotErrors.value = {}
|
||||
await nextTick()
|
||||
stepTwo.value?.querySelector<HTMLElement>('input, select, textarea, button, [contenteditable="true"]')?.focus()
|
||||
|
||||
@@ -16,9 +16,9 @@ function field(type: string, name = 'title'): FormField {
|
||||
|
||||
const enabledLocales = ref<readonly string[]>(['en', 'pl'])
|
||||
|
||||
function mlMount(component: typeof MLTextField | typeof MLMarkdownField, props: Record<string, unknown>) {
|
||||
function mlMount(component: typeof MLTextField | typeof MLMarkdownField, props: object) {
|
||||
return mount(component, {
|
||||
props,
|
||||
props: props as never,
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
provide: {
|
||||
@@ -135,4 +135,33 @@ describe('ML field registry and nested save body', () => {
|
||||
await wrapper.find('input').setValue('Witaj')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Witaj' }])
|
||||
})
|
||||
|
||||
it('seeds empty maps and lists every enabled locale on mlmarkdown create', () => {
|
||||
const fields = [field('mlmarkdown', 'body')]
|
||||
const values = initialValues(fields, ['en', 'pl'])
|
||||
expect(values.body).toEqual({ en: '', pl: '' })
|
||||
const wrapper = mlMount(MLMarkdownField, {
|
||||
field: fields[0],
|
||||
modelValue: values.body,
|
||||
controlId: 'f-body',
|
||||
})
|
||||
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true)
|
||||
expect(wrapper.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
||||
})
|
||||
|
||||
it('keeps sibling locales when adopt merges a GET host string on mlmarkdown', async () => {
|
||||
const fields = [field('mlmarkdown', 'body')]
|
||||
const merged = mergeMLValue('# Hello', ['en', 'pl'])
|
||||
expect(merged).toEqual({ en: '# Hello', pl: '' })
|
||||
const wrapper = mlMount(MLMarkdownField, {
|
||||
field: fields[0],
|
||||
modelValue: merged,
|
||||
controlId: 'f-body',
|
||||
})
|
||||
expect(wrapper.findComponent(MarkdownField).exists()).toBe(true)
|
||||
expect(wrapper.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
|
||||
await wrapper.find('[data-ml-locale]').setValue('pl')
|
||||
await wrapper.find('textarea').setValue('# Witaj')
|
||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: '# Hello', pl: '# Witaj' }])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
fieldErrors,
|
||||
focusField,
|
||||
initialValues,
|
||||
mergeMLValue,
|
||||
panelDomId,
|
||||
presetValue,
|
||||
snapshot,
|
||||
@@ -55,7 +56,7 @@ describe('form state helpers', () => {
|
||||
const fields = [formField('name'), ...extensionFormSchemaFixture.data.fields.filter((field) => field.type === 'widget' || field.type === 'partial')]
|
||||
expect(fields.map((field) => field.type)).toEqual(['text', 'widget', 'partial'])
|
||||
expect(editablePayload(fields, { name: 'Blue', lookup: 'x', summary: { nodes: [] } })).toEqual({ name: 'Blue' })
|
||||
expect(initialValues(fields.slice(1))).toEqual({})
|
||||
expect(initialValues(fields.slice(1), [])).toEqual({})
|
||||
})
|
||||
|
||||
it('starts a new record from defaults, toggles off and empty multiple relations', () => {
|
||||
@@ -67,7 +68,7 @@ describe('form state helpers', () => {
|
||||
formField('maker'),
|
||||
{ ...formField('weight'), default: 0 },
|
||||
]
|
||||
expect(initialValues(fields)).toEqual({ name: 'Unnamed', active: false, featured: false, tags: [], weight: 0 })
|
||||
expect(initialValues(fields, [])).toEqual({ name: 'Unnamed', active: false, featured: false, tags: [], weight: 0 })
|
||||
})
|
||||
|
||||
it('normalizes 422 details into string lists and drops empty entries', () => {
|
||||
@@ -119,7 +120,7 @@ describe('password and permission values in the save body (UI-SPEC S5 and S7)',
|
||||
})
|
||||
|
||||
it('starts password fields empty, whatever the record holds', () => {
|
||||
const initial = initialValues(fields.filter((field) => field.type === 'password'))
|
||||
const initial = initialValues(fields.filter((field) => field.type === 'password'), [])
|
||||
expect(initial.password ?? '').toBe('')
|
||||
expect(initial.password_confirmation ?? '').toBe('')
|
||||
})
|
||||
@@ -178,3 +179,22 @@ describe('preset values (D-27 G7)', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('ML create seed and adopt merge', () => {
|
||||
it('seeds every enabled locale as an empty string', () => {
|
||||
const fields = [
|
||||
{ name: 'title', type: 'mltext', label: 'Title' } as ReturnType<typeof formField>,
|
||||
{ name: 'body', type: 'mlmarkdown', label: 'Body' } as ReturnType<typeof formField>,
|
||||
]
|
||||
expect(initialValues(fields, ['en', 'pl'])).toEqual({
|
||||
title: { en: '', pl: '' },
|
||||
body: { en: '', pl: '' },
|
||||
})
|
||||
expect(initialValues(fields, [])).toEqual({ title: {}, body: {} })
|
||||
})
|
||||
|
||||
it('merges a GET host string onto the seed without dropping sibling locales', () => {
|
||||
expect(mergeMLValue('Hello', ['en', 'pl'])).toEqual({ en: 'Hello', pl: '' })
|
||||
expect(mergeMLValue({ en: 'Hello', pl: 'Witaj' }, ['en', 'pl'])).toEqual({ en: 'Hello', pl: 'Witaj' })
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user