Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/stagefield-false-optout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@object-ui/plugin-detail': patch
---

detail synth: `detail.stageField: false` (or `null`) now explicitly opts a
record page out of the auto status-path stepper.

`detectStatusField()` previously only treated a truthy `stageField` (a field
name) specially and otherwise auto-detected a `status` / `stage` / `state` /
`phase` field by name or type. Objects with a non-linear `status` picklist
(e.g. 正常 / 暂停 / 作废) had no way to suppress the inappropriate ordered
`record:path` stepper.

The hint is now read from the spec's `detail` block (`object.zod.ts` — a
`.passthrough()` object already documented as "Detail-page UI hints consumed by
@object-ui/plugin-detail synth"), which is the author-reachable location:
`ObjectSchema.create()` rejects unknown *top-level* keys, so a bare top-level
`stageField` could never be set on a spec-authored object. Authors now write
`detail: { stageField: false }` to opt out, or `detail: { stageField: 'field' }`
to pick the path field. The top-level `stageField` is kept as a back-compat
fallback for raw/duck-typed defs. Default behavior unchanged.
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,50 @@ describe('detectStatusField', () => {
expect(detectStatusField(undefined)).toBeNull();
});

it('honours explicit stageField', () => {
it('honours detail.stageField (author-reachable location)', () => {
expect(
detectStatusField({ detail: { stageField: 'pipeline' }, fields: { pipeline: {} } }),
).toBe('pipeline');
});

it('opts out when detail.stageField is false', () => {
// A `status` picklist would normally be auto-detected by name; `false`
// explicitly suppresses the path stepper and skips detection.
expect(
detectStatusField({ detail: { stageField: false }, fields: { status: {} } }),
).toBeNull();
});

it('opts out when detail.stageField is null', () => {
expect(
detectStatusField({ detail: { stageField: null }, fields: { status: {} } }),
).toBeNull();
});

it('honours top-level stageField (back-compat for raw defs)', () => {
expect(detectStatusField({ stageField: 'pipeline', fields: { pipeline: {} } }))
.toBe('pipeline');
});

it('opts out via top-level stageField:false (back-compat)', () => {
expect(
detectStatusField({ stageField: false, fields: { status: {} } }),
).toBeNull();
});

it('detail.stageField takes precedence over top-level', () => {
// detail block wins; here it opts out even though top-level names a field.
expect(
detectStatusField({ detail: { stageField: false }, stageField: 'status', fields: { status: {} } }),
).toBeNull();
});

it('detail without stageField defers to top-level / detection', () => {
expect(
detectStatusField({ detail: { hideRelatedTab: true }, fields: { status: {} } }),
).toBe('status');
});

it('picks status by name', () => {
expect(detectStatusField(leadDef)).toBe('status');
});
Expand Down
35 changes: 29 additions & 6 deletions packages/plugin-detail/src/synth/buildDefaultPageSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ export interface ObjectDefLike {
name?: string;
label?: string;
fields?: Record<string, ObjectFieldLike>;
/** Optional stage hints — when present we emit a `record:path`. */
stageField?: string;
/** Detail-page UI hints — the spec's `detail` block (object.zod.ts), a
* `.passthrough()` object that is the author-reachable home for synth hints.
* `stageField`: a field name selects the `record:path` status field; an
* explicit `false` / `null` opts out of the auto status stepper entirely. */
detail?: {
stageField?: string | false | null;
[k: string]: unknown;
};
/** @deprecated Back-compat top-level alias of `detail.stageField`, kept for
* raw/duck-typed defs. Spec-authored objects must use `detail.stageField`
* (top-level unknown keys are rejected by `ObjectSchema.create()`). */
stageField?: string | false | null;
stages?: Array<{ value: any; label: string }>;
/** Optional list of fields to surface in the highlight strip. */
highlightFields?: string[];
Expand Down Expand Up @@ -188,14 +198,27 @@ function toNodeArray(slot: any | any[] | undefined): any[] {
/**
* Detect the canonical "status" / "stage" field on an object definition.
*
* Heuristic — same as DetailView's `autoSummaryFields`:
* 1) prefer an explicit `objectDef.stageField`
* The stage hint comes from `def.detail.stageField` (spec's passthrough detail
* block — the author-reachable location) and falls back to a top-level
* `def.stageField` for raw/duck-typed defs. Heuristic:
* 0) stage hint === false | null → explicit opt-out, never render a path
* 1) stage hint is a field name → use it
* 2) else first field named status / stage / state / phase
* 3) else null
* 3) else first field whose type is status / stage
* 4) else null
*/
export function detectStatusField(def?: ObjectDefLike): string | null {
if (!def) return null;
if (def.stageField) return def.stageField;
// Prefer the spec's `detail` block (author-reachable; top-level unknown keys
// are rejected by `ObjectSchema.create()`), fall back to top-level for raw
// defs. `?? undefined` so a `detail` without `stageField` defers to top-level.
const stageHint =
def.detail?.stageField !== undefined ? def.detail.stageField : def.stageField;
// Explicit opt-out: `stageField: false` (or null) declares "this object has
// no ordered status pipeline" — suppress the auto `record:path` stepper and
// skip name/type-based detection. Lets non-linear `status` picklists hide it.
if (stageHint === false || stageHint === null) return null;
if (stageHint) return stageHint;
const fields = def.fields || {};
const candidates = ['status', 'stage', 'state', 'phase'];
for (const key of candidates) {
Expand Down
Loading