From 3809ee1af10673b32e8a7fd7343220342289edbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Sun, 19 Jul 2026 11:03:48 -0700 Subject: [PATCH] fix(plugin-detail): #2688 header Record-#id floor + raw audit user id in meta footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two rendering defects on record surfaces opened with a partial context (e.g. the gantt row drawer fetching a business record + schema): 1. DetailView header: resolveDisplayTitle passed deriveFromRecordKeys:false and no caller set schema.title, so a record whose name lives in a field the type-aware derivation deliberately skips (an autonumber 'name') floored to 'Record #'. Add a record-key probe as the LAST resort before the floor — only reached when schema.title is absent, so the 'guessed key must not outrank the object label' rule still holds. 2. RecordMetaFooter: UserRef degraded created_by/updated_by to type 'text' (raw opaque id) whenever the fetched schema omitted the audit system fields. They are always user references on ObjectStack — default the reference target to sys_user so the reference renderer resolves a name (or shows the muted placeholder). Verified live against a real app backend: gantt right-click 查看详情 now titles the drawer with the record name and the footer shows resolved creator/updater names. plugin-detail 236/236, plugin-gantt 336/336, tsc clean; new regression suite DetailView.title2688.test.tsx (5 cases). Co-Authored-By: Claude Fable 5 --- .changeset/detail-title-creator-2688.md | 15 +++ packages/plugin-detail/src/DetailView.tsx | 17 ++++ .../plugin-detail/src/RecordMetaFooter.tsx | 10 +- .../__tests__/DetailView.title2688.test.tsx | 98 +++++++++++++++++++ 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 .changeset/detail-title-creator-2688.md create mode 100644 packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx diff --git a/.changeset/detail-title-creator-2688.md b/.changeset/detail-title-creator-2688.md new file mode 100644 index 000000000..44ef67e71 --- /dev/null +++ b/.changeset/detail-title-creator-2688.md @@ -0,0 +1,15 @@ +--- +"@object-ui/plugin-detail": patch +--- + +fix(plugin-detail): #2688 — record surfaces without a caller title no longer floor to `Record #`, and the meta footer never prints a raw audit user id + +- `DetailView` header: after every declared-identity step misses and no + `schema.title` was provided, probe name-ish record keys (`name`, `title`, + `*_name`, …) before falling to the `Record #` floor. Fixes records whose + name lives in a field the type-aware derivation deliberately skips (e.g. an + `autonumber` `name`) opened from surfaces like the gantt row drawer. +- `RecordMetaFooter`: `created_by` / `updated_by` are always user references on + ObjectStack — when the fetched schema omits the audit system fields, default + the reference target to `sys_user` so the footer renders a resolved user name + (or the muted placeholder) instead of the raw opaque id. diff --git a/packages/plugin-detail/src/DetailView.tsx b/packages/plugin-detail/src/DetailView.tsx index b948c7819..e81583221 100644 --- a/packages/plugin-detail/src/DetailView.tsx +++ b/packages/plugin-detail/src/DetailView.tsx @@ -66,6 +66,10 @@ const EMPTY_DRAFT: Record = {}; * unified `@object-ui/core#getRecordDisplayName` (ADR-0079) — so the detail * header matches gallery / calendar / lookup / search. * 4. `schema.title` (caller-provided override, typically the object label). + * 4b. Record-key probe (`name`/`title`/`*_name`/…) — last resort before the + * floor, for records whose name lives in a field the type-aware derivation + * skips (e.g. an `autonumber` `name`) and whose caller set no title + * (objectui#2688). * 5. `Record #` floor, else the translated "Details" fallback. */ function resolveDisplayTitle( @@ -106,6 +110,19 @@ function resolveDisplayTitle( } // 4. Caller-provided title override (object label). if (schema.title) return schema.title; + // 4b. Record-key probe as the LAST resort before the id floor (objectui#2688). + // Only reached when the caller provided no title, so the "guessed key must + // not outrank schema.title" rule above still holds — but a name-ish value + // sitting right on the record (e.g. `name` typed `autonumber`, which the + // type-aware derivation deliberately skips) beats a bare `Record #`. + if (data && typeof data === 'object') { + const id = (data as any).id ?? (data as any)._id; + const guessed = getRecordDisplayName(objectSchema, data); + const guessedIsFloor = + guessed === 'Untitled' || + (id !== null && id !== undefined && guessed === `Record #${id}`); + if (!guessedIsFloor) return guessed; + } // 5. `Record #` floor, else the translated "Details" fallback. if (data && typeof data === 'object') { const id = (data as any).id ?? (data as any)._id; diff --git a/packages/plugin-detail/src/RecordMetaFooter.tsx b/packages/plugin-detail/src/RecordMetaFooter.tsx index eca58af2a..4af11fa1b 100644 --- a/packages/plugin-detail/src/RecordMetaFooter.tsx +++ b/packages/plugin-detail/src/RecordMetaFooter.tsx @@ -84,11 +84,15 @@ interface UserRefProps { const UserRef: React.FC = ({ value, objectSchema, fieldName }) => { if (value === null || value === undefined || value === '') return null; const fieldDef = objectSchema?.fields?.[fieldName]; - const refTarget = fieldDef?.reference_to || fieldDef?.reference; + // created_by / updated_by are ALWAYS user references on ObjectStack, but many + // fetched schemas omit the audit system fields from `fields`. Without a + // fallback the field degrades to `type: 'text'` and the footer prints the raw + // user id (objectui#2688) — so default the reference target to `sys_user`. + const refTarget = fieldDef?.reference_to || fieldDef?.reference || 'sys_user'; const enrichedField: Record = { name: fieldName, - type: fieldDef?.type || (refTarget ? 'lookup' : 'text'), - ...(refTarget && { reference_to: refTarget }), + type: fieldDef?.type || 'lookup', + reference_to: refTarget, ...(fieldDef?.reference_field && { reference_field: fieldDef.reference_field }), }; const resolvedType = resolveCellRendererType(enrichedField as { type?: string }) || enrichedField.type; diff --git a/packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx b/packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx new file mode 100644 index 000000000..2a192e789 --- /dev/null +++ b/packages/plugin-detail/src/__tests__/DetailView.title2688.test.tsx @@ -0,0 +1,98 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#2688 — the record surface opened from a gantt row (and any other + * caller that provides neither `schema.title` nor a resolvable declared name + * field) floored the header to `Record #` and the meta footer printed the + * raw `created_by` user id. + * + * - Header: when everything above it misses, a name-ish key sitting right on + * the record (e.g. a `name` typed `autonumber`, which the type-aware + * derivation deliberately skips) must beat the `Record #` floor. + * - Footer: `created_by` / `updated_by` are always user references on + * ObjectStack; when the fetched schema omits the audit system fields the + * footer must still render them through the reference renderer (which shows + * a resolved name or a muted placeholder) — never the raw opaque id. + */ + +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import { DetailView } from '../DetailView'; +import { RecordMetaFooter } from '../RecordMetaFooter'; +import type { DetailViewSchema } from '@object-ui/types'; + +describe('DetailView header title — record-key probe before the Record # floor (#2688)', () => { + it('uses a name-ish record key when no schema.title and no declared name field resolve', () => { + const schema: DetailViewSchema = { + type: 'detail-view', + objectName: 'production_plan', + data: { id: 'A1', name: '甘特计划A 组焊' }, + fields: [{ name: 'status', label: '状态' }], + }; + const { container } = render(); + const h1 = container.querySelector('h1'); + expect(h1?.textContent).toBe('甘特计划A 组焊'); + }); + + it('still floors to Record # when the record has no name-ish key at all', () => { + const schema: DetailViewSchema = { + type: 'detail-view', + objectName: 'thing', + data: { id: 'B2', qty: 3 }, + fields: [{ name: 'qty', label: 'Qty' }], + }; + const { container } = render(); + expect(container.querySelector('h1')?.textContent).toBe('Record #B2'); + }); + + it('keeps preferring schema.title over a guessed record key', () => { + const schema: DetailViewSchema = { + type: 'detail-view', + objectName: 'thing', + title: 'Object Label', + data: { id: 'C3', name: 'Real Name' }, + fields: [{ name: 'name', label: 'Name' }], + }; + const { container } = render(); + // The unified resolver (step 3) resolves `name` via the schema-typed path + // here; the point is the header is never the raw floor when a title exists. + expect(container.querySelector('h1')?.textContent).not.toBe('Record #C3'); + }); +}); + +describe('RecordMetaFooter — audit fields default to a sys_user reference (#2688)', () => { + const OPAQUE_ID = 'g3WkZnvugj4DnYw8u5Mo6ig3ljDhiFGO'; + + it('never prints the raw created_by id when the schema omits the audit field', () => { + render( + , + ); + expect(screen.getByTestId('record-meta-footer')).toBeInTheDocument(); + // Reference renderer shows a resolved name or a muted placeholder — the + // opaque id itself must not leak into the footer text. + expect(screen.queryByText(OPAQUE_ID)).toBeNull(); + }); + + it('still honours an explicit audit-field definition from the schema', () => { + render( + , + ); + expect(screen.queryByText(OPAQUE_ID)).toBeNull(); + }); +});