diff --git a/admin/src/views/FormView.vue b/admin/src/views/FormView.vue index e33162b..4da5440 100644 --- a/admin/src/views/FormView.vue +++ b/admin/src/views/FormView.vue @@ -78,6 +78,8 @@ const recordId = mode === 'update' ? Number(route.params.id) : null const schema = ref(null) const values = ref({}) const labels = ref({}) +/** The controller's record title (meta.title, pact.FormRecordTitle); '' when it names none. */ +const recordTitle = ref('') const errors = ref>({}) // The text of a save the server refused with 403 (UI-SPEC S6). It is a // banner, not a toast: it stays readable until the next save attempt. @@ -157,9 +159,11 @@ const panelFields = computed(() => { return fields.value.filter((field) => tabOf(field) === key) }) -/** 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)), +/** The record's display name: the controller's title, else the first text-like field's value, in schema order. */ +const recordName = computed( + () => + recordTitle.value || + recordDisplayName(schema.value?.fields, values.value, schemaEnabledLocales(schema.value?.meta)), ) const title = computed(() => { @@ -217,6 +221,7 @@ function adopt(record: RecordEnvelope | undefined): void { } values.value = next labels.value = record.meta.labels ?? {} + recordTitle.value = record.meta.title ?? '' } saved.value = snapshot(editablePayload(fields.value, values.value, mode)) } diff --git a/admin/src/views/PreviewView.vue b/admin/src/views/PreviewView.vue index 684a2ed..76aff54 100644 --- a/admin/src/views/PreviewView.vue +++ b/admin/src/views/PreviewView.vue @@ -54,6 +54,8 @@ const recordPath = `${listPath}/${recordId}` const schema = ref(null) const values = ref({}) const labels = ref({}) +/** The controller's record title (meta.title, pact.FormRecordTitle); '' when it names none. */ +const recordTitle = ref('') const actions = ref([]) const loading = ref(true) const failed = ref(false) @@ -112,9 +114,11 @@ const panelFields = computed(() => { return fields.value.filter((field) => tabOf(field) === key) }) -/** 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)), +/** The record's display name: the controller's title, else the first text-like field's value, in schema order. */ +const recordName = computed( + () => + recordTitle.value || + recordDisplayName(schema.value?.fields, values.value, schemaEnabledLocales(schema.value?.meta)), ) const subtitle = computed(() => message(schema.value?.messages.preview, undefined, { name: recordName.value })) @@ -146,6 +150,7 @@ async function loadRecord(): Promise { } values.value = { ...result.data.data } labels.value = result.data.meta.labels ?? {} + recordTitle.value = result.data.meta.title ?? '' actions.value = result.data.meta.actions ?? [] return true } catch { diff --git a/admin/tests/form/FormView.test.ts b/admin/tests/form/FormView.test.ts index 49b15a9..b45c0c5 100644 --- a/admin/tests/form/FormView.test.ts +++ b/admin/tests/form/FormView.test.ts @@ -623,6 +623,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 controller's meta.title when it sends one, and publishes it as the crumb", async () => { + const record = clone(rosterRecordFixture) + ;(record.meta as { title?: string }).title = 'Person #1 (Ada)' + const { wrapper } = await mountApp('/acme/roster/people/1', people({ [`GET ${PERSON}`]: { body: record } })) + expect(wrapper.find('h1').text()).toBe('Person #1 (Ada)') + expect(recordCrumb.value).toBe('Person #1 (Ada)') + }) + 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' } diff --git a/admin/tests/form/PreviewView.test.ts b/admin/tests/form/PreviewView.test.ts index 24e2065..4cce0ef 100644 --- a/admin/tests/form/PreviewView.test.ts +++ b/admin/tests/form/PreviewView.test.ts @@ -139,6 +139,19 @@ describe('preview view (UI-SPEC S3, D-11)', () => { expect(recordCrumb.value).toBe('Ada Lovelace') }) + it("heads the record by the controller's meta.title when it sends one, before the first text field", async () => { + const { wrapper } = await mountApp( + PREVIEW, + routes({ + [`GET ${RECORD}`]: recordWith((record) => { + ;(record.meta as { title?: string }).title = 'Person #1 (Ada)' + }), + }), + ) + expect(wrapper.find('h1').text()).toBe('Person #1 (Ada)') + expect(recordCrumb.value).toBe('Person #1 (Ada)') + }) + 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('—')