fix(admin): drop ML copy-from locale chrome (quick-261006-s0v)

Copy overwrote the active locale with the other locale's often-empty string, so the editor went blank. Keep a single synchronized locale picker.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-06 20:12:47 +02:00
parent ba4427360b
commit 2f03128a58
6 changed files with 13 additions and 76 deletions

View File

@@ -15,12 +15,10 @@ const locales = computed(() => {
return codes && codes.length > 0 ? [...codes] : []
})
const activeLocale = ref(locales.value[0] ?? 'en')
const copyFrom = ref('')
const labelledBy = computed(() => `${props.controlId}-label`)
const editorId = computed(() => `${props.controlId}-editor`)
const activeText = computed(() => values.value[activeLocale.value] ?? '')
const otherLocales = computed(() => locales.value.filter((code) => code !== activeLocale.value))
const markdownField = computed(() => ({ ...props.field, type: 'markdown' }))
function setLocale(locale: string) {
@@ -28,9 +26,6 @@ function setLocale(locale: string) {
return
}
activeLocale.value = locale
if (!copyFrom.value || copyFrom.value === locale) {
copyFrom.value = otherLocales.value[0] ?? ''
}
}
function onLocaleInput(event: Event) {
@@ -43,14 +38,6 @@ function onText(text: string) {
emit('update:modelValue', { ...values.value, [activeLocale.value]: text })
}
function copyLocale() {
const from = copyFrom.value
if (!from || from === activeLocale.value) {
return
}
emit('update:modelValue', { ...values.value, [activeLocale.value]: values.value[from] ?? '' })
}
function onBroadcast(event: Event) {
const locale = (event as CustomEvent<string>).detail
if (typeof locale === 'string' && locale !== '') {
@@ -60,7 +47,6 @@ function onBroadcast(event: Event) {
onMounted(() => {
window.addEventListener(ML_LOCALE_CHANGE, onBroadcast)
copyFrom.value = otherLocales.value[0] ?? ''
})
onUnmounted(() => {
window.removeEventListener(ML_LOCALE_CHANGE, onBroadcast)
@@ -79,25 +65,6 @@ onUnmounted(() => {
>
<option v-for="code in locales" :key="code" :value="code">{{ code }}</option>
</select>
<select
v-if="otherLocales.length > 0"
data-ml-copy-from
class="rounded-control border border-border-strong bg-surface px-2 py-1 text-[13px]"
:value="copyFrom"
aria-label="Copy from locale"
@change="copyFrom = ($event.target as HTMLSelectElement).value"
>
<option v-for="code in otherLocales" :key="code" :value="code">{{ code }}</option>
</select>
<button
v-if="otherLocales.length > 0"
type="button"
data-ml-copy
class="rounded-pager px-2 py-1 text-[13px] text-muted hover:bg-hover hover:text-text"
@click="copyLocale"
>
Copy
</button>
</div>
<MarkdownField
:field="markdownField"

View File

@@ -15,21 +15,16 @@ const locales = computed(() => {
return codes && codes.length > 0 ? [...codes] : []
})
const activeLocale = ref(locales.value[0] ?? 'en')
const copyFrom = ref('')
const labelledBy = computed(() => `${props.controlId}-label`)
const editorId = computed(() => `${props.controlId}-editor`)
const activeText = computed(() => values.value[activeLocale.value] ?? '')
const otherLocales = computed(() => locales.value.filter((code) => code !== activeLocale.value))
function setLocale(locale: string) {
if (!locale) {
return
}
activeLocale.value = locale
if (!copyFrom.value || copyFrom.value === locale) {
copyFrom.value = otherLocales.value[0] ?? ''
}
}
function onLocaleInput(event: Event) {
@@ -42,14 +37,6 @@ function onText(text: string) {
emit('update:modelValue', { ...values.value, [activeLocale.value]: text })
}
function copyLocale() {
const from = copyFrom.value
if (!from || from === activeLocale.value) {
return
}
emit('update:modelValue', { ...values.value, [activeLocale.value]: values.value[from] ?? '' })
}
function onBroadcast(event: Event) {
const locale = (event as CustomEvent<string>).detail
if (typeof locale === 'string' && locale !== '') {
@@ -59,7 +46,6 @@ function onBroadcast(event: Event) {
onMounted(() => {
window.addEventListener(ML_LOCALE_CHANGE, onBroadcast)
copyFrom.value = otherLocales.value[0] ?? ''
})
onUnmounted(() => {
window.removeEventListener(ML_LOCALE_CHANGE, onBroadcast)
@@ -78,25 +64,6 @@ onUnmounted(() => {
>
<option v-for="code in locales" :key="code" :value="code">{{ code }}</option>
</select>
<select
v-if="otherLocales.length > 0"
data-ml-copy-from
class="rounded-control border border-border-strong bg-surface px-2 py-1 text-[13px]"
:value="copyFrom"
aria-label="Copy from locale"
@change="copyFrom = ($event.target as HTMLSelectElement).value"
>
<option v-for="code in otherLocales" :key="code" :value="code">{{ code }}</option>
</select>
<button
v-if="otherLocales.length > 0"
type="button"
data-ml-copy
class="rounded-pager px-2 py-1 text-[13px] text-muted hover:bg-hover hover:text-text"
@click="copyLocale"
>
Copy
</button>
</div>
<TextField
:field="{ ...field, type: 'text' }"

View File

@@ -86,11 +86,15 @@ describe('ML field registry and nested save body', () => {
})
expect(title.findAll('[data-ml-locale]')).toHaveLength(1)
expect(body.findAll('[data-ml-locale]')).toHaveLength(1)
expect(title.find('[data-ml-copy-from]').exists()).toBe(false)
expect(body.find('[data-ml-copy-from]').exists()).toBe(false)
expect(title.find('[data-ml-copy]').exists()).toBe(false)
expect(body.find('[data-ml-copy]').exists()).toBe(false)
expect(title.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
expect(body.findAll('[data-ml-locale] option').map((node) => node.text())).toEqual(['en', 'pl'])
})
it('edits the active locale and copies from another', async () => {
it('edits the active locale without overwriting sibling locales', async () => {
const wrapper = mlMount(MLTextField, {
field: field('mltext'),
modelValue: { en: 'Hello', pl: 'Witaj' },
@@ -100,9 +104,8 @@ describe('ML field registry and nested save body', () => {
expect(input.element.value).toBe('Hello')
await wrapper.find('[data-ml-locale]').setValue('pl')
expect(wrapper.find('input').element.value).toBe('Witaj')
await wrapper.find('[data-ml-copy-from]').setValue('en')
await wrapper.find('[data-ml-copy]').trigger('click')
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Hello' }])
await wrapper.find('input').setValue('Cześć')
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Cześć' }])
})
it('switches every ML control from one selector', async () => {

View File

@@ -325,7 +325,7 @@ func (MembersController) AdminSetPermissionValues(_ context.Context, field strin
`type: markdown` edits markdown source on a host text column. The admin SPA shows a source editor and may preview HTML from `cabana.RenderMarkdown`, which uses the pinned goldmark engine without unsafe HTML. Output that still contains a script or iframe tag, an event handler, or a javascript, vbscript or data URL is refused, so translated raw HTML cannot become executable preview content.
`type: mltext` and `type: mlmarkdown` reuse the ordinary text and markdown editors with a locale selector. `mlmarkdown` composes the markdown control rather than a second parser. Each ML field shows its own selector; changing one selector changes every ML control on the form. Copy-from-locale fills the active locale from another code already in the value. Selector options come from `cabana.FormMeta.EnabledLocales` on the form schema (filled from `cabana.TranslationWriter.EnabledLocales` after Lookup; `FormSchema.Localize` stays cache-only). Create seeds `{[code]: ""}` for every enabled code. A GET or save of a host scalar is merged onto that seed so sibling locales are not dropped.
`type: mltext` and `type: mlmarkdown` reuse the ordinary text and markdown editors with a locale selector. `mlmarkdown` composes the markdown control rather than a second parser. Each ML field shows its own selector; changing one selector changes every ML control on the form. Selector options come from `cabana.FormMeta.EnabledLocales` on the form schema (filled from `cabana.TranslationWriter.EnabledLocales` after Lookup; `FormSchema.Localize` stays cache-only). Create seeds `{[code]: ""}` for every enabled code. A GET or save of a host scalar is merged onto that seed so sibling locales are not dropped.
The save body sends every locale as a JSON object of code to string. Extra YAML keys are not accepted: the types reuse `label`, `comment`, `span`, `size`, `required`, `tab` and `context`. An unknown type such as `mlunknown` stops the start-up.

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
<meta name="robots" content="noindex, nofollow" />
<meta name="summer-admin-base" content="__SUMMER_ADMIN_BASE__" />
<title>SummerCMS</title>
<script type="module" crossorigin src="./assets/index-B08bxLq5.js"></script>
<script type="module" crossorigin src="./assets/index-CRrQr2H1.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-Bd6s2WVD.css">
</head>
<body>