Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40a2a6584c | ||
|
|
b6b194e5eb | ||
|
|
a714bb4629 |
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-09-16)
|
|||||||
Phase: 15 (Journal plugin) — EXECUTING
|
Phase: 15 (Journal plugin) — EXECUTING
|
||||||
Plan: 4 of 4
|
Plan: 4 of 4
|
||||||
Status: Ready for phase verification
|
Status: Ready for phase verification
|
||||||
Last activity: 2026-10-07 — Completed quick task 261007-p4y: toggle sizing and settings controller links
|
Last activity: 2026-10-07 — Completed quick task 261007-qgx: record heading from translatable (mltext) fields
|
||||||
|
|
||||||
Progress: [██████████] 100%
|
Progress: [██████████] 100%
|
||||||
|
|
||||||
@@ -653,6 +653,7 @@ Recent decisions affecting current work:
|
|||||||
| 16 | Default icon shouldn't be a square but a sun. | 2026-10-06 | 4a94380 | — |
|
| 16 | Default icon shouldn't be a square but a sun. | 2026-10-06 | 4a94380 | — |
|
||||||
| 261007-p4y | Switch toggle pixel sizing; settings entries linking to admin controllers; translate Locales moved to Settings | 2026-10-07 | 0b09acd | [261007-p4y-toggle-proportions-and-settings-controll](./quick/261007-p4y-toggle-proportions-and-settings-controll/) |
|
| 261007-p4y | Switch toggle pixel sizing; settings entries linking to admin controllers; translate Locales moved to Settings | 2026-10-07 | 0b09acd | [261007-p4y-toggle-proportions-and-settings-controll](./quick/261007-p4y-toggle-proportions-and-settings-controll/) |
|
||||||
| 18 | cabana list: join to-many relation column values with ", " | 2026-10-07 | fa25b4e | — |
|
| 18 | cabana list: join to-many relation column values with ", " | 2026-10-07 | fa25b4e | — |
|
||||||
|
| 261007-qgx | Admin record heading, crumb and title from the first text or mltext field (default locale) | 2026-10-07 | a714bb4 | [261007-qgx-admin-record-heading-uses-translatable-m](./quick/261007-qgx-admin-record-heading-uses-translatable-m/) |
|
||||||
|
|
||||||
## Deferred Items
|
## Deferred Items
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
quick_id: 261007-qgx
|
||||||
|
mode: quick
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick 261007-qgx: admin record heading uses translatable (mltext) fields
|
||||||
|
|
||||||
|
The admin record heading, breadcrumb record crumb and document title read the
|
||||||
|
first `text` field only, so records whose name field is `mltext` (journal Post
|
||||||
|
title, Category name) fall back to "Edit record".
|
||||||
|
|
||||||
|
## Task 1: shared recordDisplayName helper
|
||||||
|
|
||||||
|
- files: admin/src/components/form/formState.ts, admin/src/views/FormView.vue, admin/src/views/PreviewView.vue
|
||||||
|
- action: add `recordDisplayName(fields, values, enabledLocales)`: first field
|
||||||
|
of type `text` or an ML type (`isMLFieldType`) in schema order; plain values
|
||||||
|
as today (string/number, trimmed); ML values use the default (first enabled)
|
||||||
|
locale, else the first non-empty locale, else ''. Both views use it with
|
||||||
|
`schemaEnabledLocales(schema.meta)`.
|
||||||
|
- done: plain `text` behaviour unchanged; an mltext first field names the record.
|
||||||
|
|
||||||
|
## Task 2: tests
|
||||||
|
|
||||||
|
- files: admin/tests/form/formState.test.ts, admin/tests/form/FormView.test.ts, admin/tests/form/PreviewView.test.ts
|
||||||
|
- action: helper cases (plain, default locale filled, default empty + other
|
||||||
|
filled, all empty, non-text first field skipped); FormView and PreviewView
|
||||||
|
show the default-locale value as heading and record crumb for an mltext field.
|
||||||
|
- verify: `npm test`, `vue-tsc --noEmit`.
|
||||||
|
|
||||||
|
## Task 3: ship
|
||||||
|
|
||||||
|
- rebuild the embedded admin shell (`npm run build` → modules/boardwalk/dist),
|
||||||
|
`go vet ./... && go test ./...`, tag and push v0.1.12.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
quick_id: 261007-qgx
|
||||||
|
status: complete
|
||||||
|
commits: [a714bb4, b6b194e]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick 261007-qgx: admin record heading uses translatable (mltext) fields
|
||||||
|
|
||||||
|
- `recordDisplayName(fields, values, enabledLocales)` in
|
||||||
|
`admin/src/components/form/formState.ts`: the first `text` or ML field in
|
||||||
|
schema order names the record; an ML value reads the default (first enabled)
|
||||||
|
locale, then the first non-empty locale, else '' (the generic message stays).
|
||||||
|
- FormView and PreviewView both use it with `schemaEnabledLocales(schema.meta)`;
|
||||||
|
plain `text` behaviour is unchanged.
|
||||||
|
- Tests: five helper cases in `formState.test.ts`; FormView and PreviewView
|
||||||
|
cases for an mltext first field (heading and record crumb). Both view tests
|
||||||
|
fail without the fix. Admin suite 1090/1090, `go vet` and `go test ./...` green.
|
||||||
|
- Embedded admin shell rebuilt (`modules/boardwalk/dist`), shipped as v0.1.12.
|
||||||
@@ -217,6 +217,35 @@ export function mergeMLValue(incoming: unknown, enabledLocales: readonly string[
|
|||||||
return { ...seeded, ...localeRecord(incoming) }
|
return { ...seeded, ...localeRecord(incoming) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The record's display name: the value of the first text-like field (plain
|
||||||
|
* `text` or an ML type) in schema order. An ML value reads the default (first
|
||||||
|
* enabled) locale, then the first non-empty locale; '' when nothing is set. */
|
||||||
|
export function recordDisplayName(
|
||||||
|
fields: readonly FormField[] | undefined,
|
||||||
|
values: AdminRecord,
|
||||||
|
enabledLocales: readonly string[],
|
||||||
|
): string {
|
||||||
|
const first = fields?.find((field) => field.type === 'text' || isMLFieldType(field.type))
|
||||||
|
const value = first ? values[first.name] : undefined
|
||||||
|
if (typeof value === 'string' || typeof value === 'number') {
|
||||||
|
return String(value).trim()
|
||||||
|
}
|
||||||
|
if (!first || !isMLFieldType(first.type)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const texts = localeRecord(value)
|
||||||
|
const preferred = enabledLocales[0] !== undefined ? (texts[enabledLocales[0]] ?? '').trim() : ''
|
||||||
|
if (preferred !== '') {
|
||||||
|
return preferred
|
||||||
|
}
|
||||||
|
for (const text of Object.values(texts)) {
|
||||||
|
if (text.trim() !== '') {
|
||||||
|
return text.trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
/** D-10 422 details (field -> messages) as a string-list map. */
|
/** D-10 422 details (field -> messages) as a string-list map. */
|
||||||
export function fieldErrors(details: ErrorBody['details'] | undefined): Record<string, string[]> {
|
export function fieldErrors(details: ErrorBody['details'] | undefined): Record<string, string[]> {
|
||||||
const out: Record<string, string[]> = {}
|
const out: Record<string, string[]> = {}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
mergeMLValue,
|
mergeMLValue,
|
||||||
panelDomId,
|
panelDomId,
|
||||||
presetUpdates,
|
presetUpdates,
|
||||||
|
recordDisplayName,
|
||||||
schemaEnabledLocales,
|
schemaEnabledLocales,
|
||||||
snapshot,
|
snapshot,
|
||||||
tabDomId,
|
tabDomId,
|
||||||
@@ -139,12 +140,10 @@ const panelFields = computed(() => {
|
|||||||
return fields.value.filter((field) => tabOf(field) === key)
|
return fields.value.filter((field) => tabOf(field) === key)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** The record's display name: the first text field's value, in schema order. */
|
/** The record's display name: the first text-like field's value, in schema order. */
|
||||||
const recordName = computed(() => {
|
const recordName = computed(() =>
|
||||||
const first = schema.value?.fields.find((field) => field.type === 'text')
|
recordDisplayName(schema.value?.fields, values.value, schemaEnabledLocales(schema.value?.meta)),
|
||||||
const value = first ? values.value[first.name] : undefined
|
)
|
||||||
return typeof value === 'string' || typeof value === 'number' ? String(value).trim() : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
if (mode === 'update' && recordName.value !== '') {
|
if (mode === 'update' && recordName.value !== '') {
|
||||||
|
|||||||
@@ -14,7 +14,16 @@ import FormTabs from '../components/form/FormTabs.vue'
|
|||||||
import PreviewField from '../components/form/PreviewField.vue'
|
import PreviewField from '../components/form/PreviewField.vue'
|
||||||
import RecordActions from '../components/form/RecordActions.vue'
|
import RecordActions from '../components/form/RecordActions.vue'
|
||||||
import { RELATION_MANAGER } from '../components/form/registry'
|
import { RELATION_MANAGER } from '../components/form/registry'
|
||||||
import { DEFAULT_TAB, contextAllows, panelDomId, tabDomId, tabOf, type TabItem } from '../components/form/formState'
|
import {
|
||||||
|
DEFAULT_TAB,
|
||||||
|
contextAllows,
|
||||||
|
panelDomId,
|
||||||
|
recordDisplayName,
|
||||||
|
schemaEnabledLocales,
|
||||||
|
tabDomId,
|
||||||
|
tabOf,
|
||||||
|
type TabItem,
|
||||||
|
} from '../components/form/formState'
|
||||||
import PartialHost from '../components/partial/PartialHost.vue'
|
import PartialHost from '../components/partial/PartialHost.vue'
|
||||||
import Button from '../components/ui/Button.vue'
|
import Button from '../components/ui/Button.vue'
|
||||||
import { clearRecordCrumb, setRecordCrumb } from '../state/useBreadcrumbs'
|
import { clearRecordCrumb, setRecordCrumb } from '../state/useBreadcrumbs'
|
||||||
@@ -96,12 +105,10 @@ const panelFields = computed(() => {
|
|||||||
return fields.value.filter((field) => tabOf(field) === key)
|
return fields.value.filter((field) => tabOf(field) === key)
|
||||||
})
|
})
|
||||||
|
|
||||||
/** The record's display name: the first text field's value, in schema order. */
|
/** The record's display name: the first text-like field's value, in schema order. */
|
||||||
const recordName = computed(() => {
|
const recordName = computed(() =>
|
||||||
const first = schema.value?.fields.find((field) => field.type === 'text')
|
recordDisplayName(schema.value?.fields, values.value, schemaEnabledLocales(schema.value?.meta)),
|
||||||
const value = first ? values.value[first.name] : undefined
|
)
|
||||||
return typeof value === 'string' || typeof value === 'number' ? String(value).trim() : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const subtitle = computed(() => message(schema.value?.messages.preview, undefined, { name: recordName.value }))
|
const subtitle = computed(() => message(schema.value?.messages.preview, undefined, { name: recordName.value }))
|
||||||
const hintPartial = computed(() => schema.value?.preview?.headerPartial ?? '')
|
const hintPartial = computed(() => schema.value?.preview?.headerPartial ?? '')
|
||||||
|
|||||||
@@ -611,6 +611,14 @@ describe('password and preset on the form (UI-SPEC S7, D-19, D-27 G7)', () => {
|
|||||||
expect(editor(wrapper, 'slug').element.value).toBe('ada-lovelace')
|
expect(editor(wrapper, 'slug').element.value).toBe('ada-lovelace')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('titles a record by the default locale of an mltext first field, and publishes it as the crumb', async () => {
|
||||||
|
const record = clone(rosterRecordFixture)
|
||||||
|
record.data.name = { en: 'Ada Lovelace', pl: 'Ada Lovelace PL' }
|
||||||
|
const { wrapper } = await mountApp('/acme/roster/people/1', people({ [`GET ${PEOPLE}/schema/form`]: mlSchema('mltext', 'text'), [`GET ${PERSON}`]: { body: record } }))
|
||||||
|
expect(wrapper.find('h1').text()).toBe('Ada Lovelace')
|
||||||
|
expect(recordCrumb.value).toBe('Ada Lovelace')
|
||||||
|
})
|
||||||
|
|
||||||
it('drives a text slug from the active locale of an mltext name, until edited', async () => {
|
it('drives a text slug from the active locale of an mltext name, until edited', async () => {
|
||||||
const { wrapper } = await mountApp('/acme/roster/people/create', people({ [`GET ${PEOPLE}/schema/form`]: mlSchema('mltext', 'text') }))
|
const { wrapper } = await mountApp('/acme/roster/people/create', people({ [`GET ${PEOPLE}/schema/form`]: mlSchema('mltext', 'text') }))
|
||||||
await editor(wrapper, 'name').setValue('Hello World')
|
await editor(wrapper, 'name').setValue('Hello World')
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||||
import { enableAutoUnmount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
import { enableAutoUnmount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
||||||
import { setBundle } from '../../src/app/i18n'
|
import { setBundle } from '../../src/app/i18n'
|
||||||
|
import { recordCrumb } from '../../src/state/useBreadcrumbs'
|
||||||
import { toasts } from '../../src/state/useToasts'
|
import { toasts } from '../../src/state/useToasts'
|
||||||
import PreviewView from '../../src/views/PreviewView.vue'
|
import PreviewView from '../../src/views/PreviewView.vue'
|
||||||
import { clone, langFixture, rosterFormSchemaFixture, rosterRecordFixture } from '../fixtures/typed'
|
import { clone, langFixture, rosterFormSchemaFixture, rosterRecordFixture } from '../fixtures/typed'
|
||||||
@@ -121,6 +122,23 @@ describe('preview view (UI-SPEC S3, D-11)', () => {
|
|||||||
expect(editor.findAll('[role="radiogroup"]').every((group) => group.attributes('aria-disabled') === 'true')).toBe(true)
|
expect(editor.findAll('[role="radiogroup"]').every((group) => group.attributes('aria-disabled') === 'true')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('heads the record by the default locale of an mltext first field, and publishes it as the crumb', async () => {
|
||||||
|
const { wrapper } = await mountApp(
|
||||||
|
PREVIEW,
|
||||||
|
routes({
|
||||||
|
[`GET ${LIST}/schema/form`]: schemaWith((schema) => {
|
||||||
|
schema.meta.enabledLocales = ['en', 'pl']
|
||||||
|
schema.fields.find((field) => field.name === 'name')!.type = 'mltext'
|
||||||
|
}),
|
||||||
|
[`GET ${RECORD}`]: recordWith((record) => {
|
||||||
|
record.data.name = { en: 'Ada Lovelace', pl: 'Ada Lovelace PL' }
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(wrapper.find('h1').text()).toBe('Ada Lovelace')
|
||||||
|
expect(recordCrumb.value).toBe('Ada Lovelace')
|
||||||
|
})
|
||||||
|
|
||||||
it('shows the muted dash for an empty value and the label of a relation, never its id', async () => {
|
it('shows the muted dash for an empty value and the label of a relation, never its id', async () => {
|
||||||
const { wrapper } = await mountApp(PREVIEW, routes({ [`GET ${RECORD}`]: recordWith((record) => (record.data.joined_ip = null)) }))
|
const { wrapper } = await mountApp(PREVIEW, routes({ [`GET ${RECORD}`]: recordWith((record) => (record.data.joined_ip = null)) }))
|
||||||
expect(wrapper.find('[data-preview-field="joined_ip"] [data-empty]').text()).toBe('—')
|
expect(wrapper.find('[data-preview-field="joined_ip"] [data-empty]').text()).toBe('—')
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
panelDomId,
|
panelDomId,
|
||||||
presetUpdates,
|
presetUpdates,
|
||||||
presetValue,
|
presetValue,
|
||||||
|
recordDisplayName,
|
||||||
snapshot,
|
snapshot,
|
||||||
tabDomId,
|
tabDomId,
|
||||||
tabOf,
|
tabOf,
|
||||||
@@ -271,3 +272,33 @@ describe('ML create seed and adopt merge', () => {
|
|||||||
expect(mergeMLValue({ en: 'Hello', pl: 'Witaj' }, ['en', 'pl'])).toEqual({ en: 'Hello', pl: 'Witaj' })
|
expect(mergeMLValue({ en: 'Hello', pl: 'Witaj' }, ['en', 'pl'])).toEqual({ en: 'Hello', pl: 'Witaj' })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('recordDisplayName', () => {
|
||||||
|
const field = (name: string, type: string): FormField => ({ ...formField('name'), name, type })
|
||||||
|
const text = field('name', 'text')
|
||||||
|
const title = field('title', 'mltext')
|
||||||
|
const locales = ['en', 'pl']
|
||||||
|
|
||||||
|
it('reads a plain text value, trimmed', () => {
|
||||||
|
expect(recordDisplayName([text], { name: ' Anna ' }, locales)).toBe('Anna')
|
||||||
|
expect(recordDisplayName([text], { name: 42 }, locales)).toBe('42')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reads the default locale of an ML value', () => {
|
||||||
|
expect(recordDisplayName([title], { title: { en: 'Hello', pl: 'Cześć' } }, locales)).toBe('Hello')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('falls back to the first non-empty locale when the default is empty', () => {
|
||||||
|
expect(recordDisplayName([title], { title: { en: ' ', pl: 'Cześć' } }, locales)).toBe('Cześć')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is empty when every locale is empty', () => {
|
||||||
|
expect(recordDisplayName([title], { title: { en: '', pl: '' } }, locales)).toBe('')
|
||||||
|
expect(recordDisplayName([title], {}, locales)).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skips fields that are not text-like, in favour of a later text field', () => {
|
||||||
|
const fields = [field('notify', 'checkbox'), field('tags', 'relation'), title, text]
|
||||||
|
expect(recordDisplayName(fields, { notify: true, tags: [1], title: { en: 'Post', pl: '' }, name: 'Anna' }, locales)).toBe('Post')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
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
@@ -7,7 +7,7 @@
|
|||||||
<meta name="summer-admin-base" content="__SUMMER_ADMIN_BASE__" />
|
<meta name="summer-admin-base" content="__SUMMER_ADMIN_BASE__" />
|
||||||
<link rel="icon" href="./favicon.ico" sizes="48x48" />
|
<link rel="icon" href="./favicon.ico" sizes="48x48" />
|
||||||
<title>SummerCMS</title>
|
<title>SummerCMS</title>
|
||||||
<script type="module" crossorigin src="./assets/index-BK2Ktgov.js"></script>
|
<script type="module" crossorigin src="./assets/index-BFvYYWYJ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-BhDUy2Lr.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-BhDUy2Lr.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user