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] : []
|
return codes && codes.length > 0 ? [...codes] : []
|
||||||
})
|
})
|
||||||
const activeLocale = ref(locales.value[0] ?? 'en')
|
const activeLocale = ref(locales.value[0] ?? 'en')
|
||||||
const copyFrom = ref('')
|
|
||||||
|
|
||||||
const labelledBy = computed(() => `${props.controlId}-label`)
|
const labelledBy = computed(() => `${props.controlId}-label`)
|
||||||
const editorId = computed(() => `${props.controlId}-editor`)
|
const editorId = computed(() => `${props.controlId}-editor`)
|
||||||
const activeText = computed(() => values.value[activeLocale.value] ?? '')
|
const activeText = computed(() => values.value[activeLocale.value] ?? '')
|
||||||
const otherLocales = computed(() => locales.value.filter((code) => code !== activeLocale.value))
|
|
||||||
const markdownField = computed(() => ({ ...props.field, type: 'markdown' }))
|
const markdownField = computed(() => ({ ...props.field, type: 'markdown' }))
|
||||||
|
|
||||||
function setLocale(locale: string) {
|
function setLocale(locale: string) {
|
||||||
@@ -28,9 +26,6 @@ function setLocale(locale: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
activeLocale.value = locale
|
activeLocale.value = locale
|
||||||
if (!copyFrom.value || copyFrom.value === locale) {
|
|
||||||
copyFrom.value = otherLocales.value[0] ?? ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLocaleInput(event: Event) {
|
function onLocaleInput(event: Event) {
|
||||||
@@ -43,14 +38,6 @@ function onText(text: string) {
|
|||||||
emit('update:modelValue', { ...values.value, [activeLocale.value]: text })
|
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) {
|
function onBroadcast(event: Event) {
|
||||||
const locale = (event as CustomEvent<string>).detail
|
const locale = (event as CustomEvent<string>).detail
|
||||||
if (typeof locale === 'string' && locale !== '') {
|
if (typeof locale === 'string' && locale !== '') {
|
||||||
@@ -60,7 +47,6 @@ function onBroadcast(event: Event) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
window.addEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
||||||
copyFrom.value = otherLocales.value[0] ?? ''
|
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
window.removeEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
||||||
@@ -79,25 +65,6 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<option v-for="code in locales" :key="code" :value="code">{{ code }}</option>
|
<option v-for="code in locales" :key="code" :value="code">{{ code }}</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
<MarkdownField
|
<MarkdownField
|
||||||
:field="markdownField"
|
:field="markdownField"
|
||||||
|
|||||||
@@ -15,21 +15,16 @@ const locales = computed(() => {
|
|||||||
return codes && codes.length > 0 ? [...codes] : []
|
return codes && codes.length > 0 ? [...codes] : []
|
||||||
})
|
})
|
||||||
const activeLocale = ref(locales.value[0] ?? 'en')
|
const activeLocale = ref(locales.value[0] ?? 'en')
|
||||||
const copyFrom = ref('')
|
|
||||||
|
|
||||||
const labelledBy = computed(() => `${props.controlId}-label`)
|
const labelledBy = computed(() => `${props.controlId}-label`)
|
||||||
const editorId = computed(() => `${props.controlId}-editor`)
|
const editorId = computed(() => `${props.controlId}-editor`)
|
||||||
const activeText = computed(() => values.value[activeLocale.value] ?? '')
|
const activeText = computed(() => values.value[activeLocale.value] ?? '')
|
||||||
const otherLocales = computed(() => locales.value.filter((code) => code !== activeLocale.value))
|
|
||||||
|
|
||||||
function setLocale(locale: string) {
|
function setLocale(locale: string) {
|
||||||
if (!locale) {
|
if (!locale) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
activeLocale.value = locale
|
activeLocale.value = locale
|
||||||
if (!copyFrom.value || copyFrom.value === locale) {
|
|
||||||
copyFrom.value = otherLocales.value[0] ?? ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLocaleInput(event: Event) {
|
function onLocaleInput(event: Event) {
|
||||||
@@ -42,14 +37,6 @@ function onText(text: string) {
|
|||||||
emit('update:modelValue', { ...values.value, [activeLocale.value]: text })
|
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) {
|
function onBroadcast(event: Event) {
|
||||||
const locale = (event as CustomEvent<string>).detail
|
const locale = (event as CustomEvent<string>).detail
|
||||||
if (typeof locale === 'string' && locale !== '') {
|
if (typeof locale === 'string' && locale !== '') {
|
||||||
@@ -59,7 +46,6 @@ function onBroadcast(event: Event) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
window.addEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
||||||
copyFrom.value = otherLocales.value[0] ?? ''
|
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
window.removeEventListener(ML_LOCALE_CHANGE, onBroadcast)
|
||||||
@@ -78,25 +64,6 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<option v-for="code in locales" :key="code" :value="code">{{ code }}</option>
|
<option v-for="code in locales" :key="code" :value="code">{{ code }}</option>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
:field="{ ...field, type: 'text' }"
|
: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(title.findAll('[data-ml-locale]')).toHaveLength(1)
|
||||||
expect(body.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(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'])
|
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, {
|
const wrapper = mlMount(MLTextField, {
|
||||||
field: field('mltext'),
|
field: field('mltext'),
|
||||||
modelValue: { en: 'Hello', pl: 'Witaj' },
|
modelValue: { en: 'Hello', pl: 'Witaj' },
|
||||||
@@ -100,9 +104,8 @@ describe('ML field registry and nested save body', () => {
|
|||||||
expect(input.element.value).toBe('Hello')
|
expect(input.element.value).toBe('Hello')
|
||||||
await wrapper.find('[data-ml-locale]').setValue('pl')
|
await wrapper.find('[data-ml-locale]').setValue('pl')
|
||||||
expect(wrapper.find('input').element.value).toBe('Witaj')
|
expect(wrapper.find('input').element.value).toBe('Witaj')
|
||||||
await wrapper.find('[data-ml-copy-from]').setValue('en')
|
await wrapper.find('input').setValue('Cześć')
|
||||||
await wrapper.find('[data-ml-copy]').trigger('click')
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Cześć' }])
|
||||||
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ en: 'Hello', pl: 'Hello' }])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('switches every ML control from one selector', async () => {
|
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: 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.
|
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="robots" content="noindex, nofollow" />
|
||||||
<meta name="summer-admin-base" content="__SUMMER_ADMIN_BASE__" />
|
<meta name="summer-admin-base" content="__SUMMER_ADMIN_BASE__" />
|
||||||
<title>SummerCMS</title>
|
<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">
|
<link rel="stylesheet" crossorigin href="./assets/index-Bd6s2WVD.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user