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:
@@ -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"
|
||||
|
||||
@@ -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' }"
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
2
modules/boardwalk/dist/index.html
vendored
2
modules/boardwalk/dist/index.html
vendored
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user