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
|
||||
Plan: 4 of 4
|
||||
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%
|
||||
|
||||
@@ -653,6 +653,7 @@ Recent decisions affecting current work:
|
||||
| 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/) |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
export function fieldErrors(details: ErrorBody['details'] | undefined): Record<string, string[]> {
|
||||
const out: Record<string, string[]> = {}
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
mergeMLValue,
|
||||
panelDomId,
|
||||
presetUpdates,
|
||||
recordDisplayName,
|
||||
schemaEnabledLocales,
|
||||
snapshot,
|
||||
tabDomId,
|
||||
@@ -139,12 +140,10 @@ const panelFields = computed(() => {
|
||||
return fields.value.filter((field) => tabOf(field) === key)
|
||||
})
|
||||
|
||||
/** The record's display name: the first text field's value, in schema order. */
|
||||
const recordName = computed(() => {
|
||||
const first = schema.value?.fields.find((field) => field.type === 'text')
|
||||
const value = first ? values.value[first.name] : undefined
|
||||
return typeof value === 'string' || typeof value === 'number' ? String(value).trim() : ''
|
||||
})
|
||||
/** The record's display name: the first text-like field's value, in schema order. */
|
||||
const recordName = computed(() =>
|
||||
recordDisplayName(schema.value?.fields, values.value, schemaEnabledLocales(schema.value?.meta)),
|
||||
)
|
||||
|
||||
const title = computed(() => {
|
||||
if (mode === 'update' && recordName.value !== '') {
|
||||
|
||||
@@ -14,7 +14,16 @@ import FormTabs from '../components/form/FormTabs.vue'
|
||||
import PreviewField from '../components/form/PreviewField.vue'
|
||||
import RecordActions from '../components/form/RecordActions.vue'
|
||||
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 Button from '../components/ui/Button.vue'
|
||||
import { clearRecordCrumb, setRecordCrumb } from '../state/useBreadcrumbs'
|
||||
@@ -96,12 +105,10 @@ const panelFields = computed(() => {
|
||||
return fields.value.filter((field) => tabOf(field) === key)
|
||||
})
|
||||
|
||||
/** The record's display name: the first text field's value, in schema order. */
|
||||
const recordName = computed(() => {
|
||||
const first = schema.value?.fields.find((field) => field.type === 'text')
|
||||
const value = first ? values.value[first.name] : undefined
|
||||
return typeof value === 'string' || typeof value === 'number' ? String(value).trim() : ''
|
||||
})
|
||||
/** The record's display name: the first text-like field's value, in schema order. */
|
||||
const recordName = computed(() =>
|
||||
recordDisplayName(schema.value?.fields, values.value, schemaEnabledLocales(schema.value?.meta)),
|
||||
)
|
||||
|
||||
const subtitle = computed(() => message(schema.value?.messages.preview, undefined, { name: recordName.value }))
|
||||
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')
|
||||
})
|
||||
|
||||
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 () => {
|
||||
const { wrapper } = await mountApp('/acme/roster/people/create', people({ [`GET ${PEOPLE}/schema/form`]: mlSchema('mltext', 'text') }))
|
||||
await editor(wrapper, 'name').setValue('Hello World')
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, type VueWrapper } from '@vue/test-utils'
|
||||
import { setBundle } from '../../src/app/i18n'
|
||||
import { recordCrumb } from '../../src/state/useBreadcrumbs'
|
||||
import { toasts } from '../../src/state/useToasts'
|
||||
import PreviewView from '../../src/views/PreviewView.vue'
|
||||
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)
|
||||
})
|
||||
|
||||
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 () => {
|
||||
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('—')
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
panelDomId,
|
||||
presetUpdates,
|
||||
presetValue,
|
||||
recordDisplayName,
|
||||
snapshot,
|
||||
tabDomId,
|
||||
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' })
|
||||
})
|
||||
})
|
||||
|
||||
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__" />
|
||||
<link rel="icon" href="./favicon.ico" sizes="48x48" />
|
||||
<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">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user