Skip to content

Commit 144ab55

Browse files
os-zhuangclaude
andauthored
feat: consume ADR-0085 semantic roles from spec 11.7.0 (single-source fieldGroups, stageField:false, highlightFields) (#2168)
- plugin-form/plugin-detail grouping now adapts @objectstack/spec's deriveFieldGroupLayout (ADR-0085 §5) — one derivation for forms, modals and detail pages; canonical collapse enum honoured with deprecated collapsible/collapsed/defaultExpanded read for pre-11.7 metadata. - detectStatusField reads the top-level stageField role; false now suppresses the record:path stepper (was wired to the removed detail.stageField, so spec-authored false fell through to the heuristic — dogfood-confirmed gap). - Default grid columns / cards / highlight strip / child previews / interface pages read highlightFields with compactLayout fallback. - Dead reads removed: objectDef.views.* and the ADR-0085-removed detail.* hint block. detail.renderViaSchema survives only as the legacy-renderer kill-switch (removed with that path in a follow-up). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 3c7abf9 commit 144ab55

24 files changed

Lines changed: 325 additions & 415 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/plugin-form': minor
3+
'@object-ui/plugin-detail': minor
4+
'@object-ui/plugin-grid': minor
5+
'@object-ui/app-shell': minor
6+
---
7+
8+
Consume the ADR-0085 object semantic roles from `@objectstack/spec@11.7.0`, retiring the per-surface hint dialects:
9+
10+
- **Single-source fieldGroups derivation**: `plugin-form`'s `deriveFieldGroupSections` and `plugin-detail`'s `deriveFieldGroupDetailSections` are now thin adapters over the spec's `deriveFieldGroupLayout` (ADR-0085 §5) — forms, modals and detail pages render the SAME grouping from one implementation. The canonical `collapse: 'none' | 'expanded' | 'collapsed'` enum is honoured everywhere (deprecated `collapsible`/`collapsed` and `defaultExpanded` spellings still read for pre-11.7 metadata).
11+
- **`stageField` semantic role**: the detail stepper reads the top-level `stageField`; `stageField: false` now actually suppresses stage detection (previously the `false` handling was wired to the removed `detail.stageField` key, so spec-authored `false` fell through to the name heuristic).
12+
- **`highlightFields` rename**: default grid columns, card compact views, the detail highlight strip, child-record preview fields and interface-page default columns read the object's `highlightFields` (deprecated `compactLayout` spelling read as fallback for pre-11.7 metadata).
13+
- **Removed dead reads**: the never-spec-writable `objectDef.views.*` UI hints and the ADR-0085-removed `detail.*` block (`sections`, `sectionGroups`, `highlightFields`, `stageField`, `useFieldGroups`, `showReferenceRail`, `hideReferenceRail`, `hideRelatedTab`, `relatedLayout`) are no longer consulted. Per-page customization goes through an assigned Page schema (`record:reference_rail` remains available there as a renderer capability). `detail.renderViaSchema` survives only as the legacy-renderer kill-switch and is removed together with that path.

apps/console/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"@object-ui/react": "workspace:*",
8989
"@object-ui/types": "workspace:*",
9090
"@objectstack/client": "^11.5.0",
91-
"@objectstack/spec": "^11.5.0",
91+
"@objectstack/spec": "^11.7.0",
9292
"@tailwindcss/postcss": "^4.3.1",
9393
"@tailwindcss/typography": "^0.5.20",
9494
"@testing-library/jest-dom": "^6.9.1",

apps/site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"@object-ui/plugin-view": "workspace:*",
3232
"@object-ui/react": "workspace:*",
3333
"@object-ui/types": "workspace:*",
34-
"@objectstack/spec": "^11.5.0",
34+
"@objectstack/spec": "^11.7.0",
3535
"fumadocs-core": "16.10.6",
3636
"fumadocs-mdx": "15.0.13",
3737
"fumadocs-ui": "16.10.6",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"devDependencies": {
6262
"@changesets/cli": "^2.31.0",
6363
"@eslint/js": "^10.0.1",
64-
"@objectstack/spec": "^11.5.0",
64+
"@objectstack/spec": "^11.7.0",
6565
"@playwright/test": "^1.61.1",
6666
"@testing-library/dom": "^10.4.1",
6767
"@testing-library/jest-dom": "^6.9.1",

packages/app-shell/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"@object-ui/providers": "workspace:*",
4949
"@object-ui/react": "workspace:*",
5050
"@object-ui/types": "workspace:*",
51-
"@objectstack/spec": "^11.5.0",
51+
"@objectstack/spec": "^11.7.0",
5252
"@sentry/react": "^10.62.0",
5353
"jsonc-parser": "^3.3.1",
5454
"lucide-react": "^1.22.0",

packages/app-shell/src/views/InterfaceListPage.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ function resolveSourceView(objectDef: any, sourceView?: string): any | undefined
6969
/**
7070
* Default column set when the resolved view carries none — mirrors
7171
* ObjectView's data-mode fallback so an interface page never renders a
72-
* column-less grid. Priority: curated `compactLayout`, else the first
72+
* column-less grid. Priority: the `highlightFields` semantic role
73+
* (ADR-0085; deprecated `compactLayout` read as fallback), else the first
7374
* business fields (system/audit columns excluded).
7475
*/
7576
const SYSTEM_FIELDS = new Set([
@@ -78,8 +79,11 @@ const SYSTEM_FIELDS = new Set([
7879
'updated_by', 'updatedBy', '_version', '_rev',
7980
]);
8081
function defaultColumnsFromObject(objectDef: any): string[] {
81-
if (Array.isArray(objectDef?.compactLayout) && objectDef.compactLayout.length > 0) {
82-
return objectDef.compactLayout.filter((n: string) => objectDef.fields?.[n]);
82+
const curated = Array.isArray(objectDef?.highlightFields) && objectDef.highlightFields.length > 0
83+
? objectDef.highlightFields
84+
: objectDef?.compactLayout;
85+
if (Array.isArray(curated) && curated.length > 0) {
86+
return curated.filter((n: string) => objectDef.fields?.[n]);
8387
}
8488
const fields = objectDef?.fields;
8589
if (fields && typeof fields === 'object') {

packages/app-shell/src/views/ObjectView.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -236,8 +236,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
236236
'updated_by', 'updatedBy', '_version', '_rev',
237237
]);
238238
let defaultColumns: string[] = [];
239-
if (Array.isArray(objectDef?.compactLayout) && objectDef.compactLayout.length > 0) {
240-
defaultColumns = objectDef.compactLayout.filter((n: string) => objectDef.fields?.[n]);
239+
const curated = Array.isArray(objectDef?.highlightFields) && objectDef.highlightFields.length > 0
240+
? objectDef.highlightFields
241+
: objectDef?.compactLayout;
242+
if (Array.isArray(curated) && curated.length > 0) {
243+
defaultColumns = curated.filter((n: string) => objectDef.fields?.[n]);
241244
} else if (objectDef?.fields) {
242245
defaultColumns = Object.entries(objectDef.fields)
243246
.filter(([name, f]: [string, any]) => f && !f.hidden && !SYSTEM_FIELDS.has(name))
@@ -486,7 +489,8 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
486489
// Resolve Views from objectDef.listViews (camelCase per @objectstack/spec)
487490
const views = useMemo(() => {
488491
// Default column resolution priority:
489-
// 1. `compactLayout` (curated primary business fields).
492+
// 1. The `highlightFields` semantic role (ADR-0085; deprecated
493+
// `compactLayout` read as fallback).
490494
// 2. Business fields only — exclude system-managed identifiers/audit
491495
// columns (id, created_at, updated_at, …) and fields explicitly
492496
// marked hidden/readonly on the schema. First 5 kept for compactness.
@@ -496,8 +500,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
496500
'updated_by', 'updatedBy', '_version', '_rev',
497501
]);
498502
const resolveDefaultColumns = (): string[] => {
499-
if (Array.isArray(objectDef.compactLayout) && objectDef.compactLayout.length > 0) {
500-
return objectDef.compactLayout.filter((n: string) => objectDef.fields?.[n]);
503+
const curated = Array.isArray(objectDef.highlightFields) && objectDef.highlightFields.length > 0
504+
? objectDef.highlightFields
505+
: objectDef.compactLayout;
506+
if (Array.isArray(curated) && curated.length > 0) {
507+
return curated.filter((n: string) => objectDef.fields?.[n]);
501508
}
502509
if (objectDef.fields) {
503510
return Object.entries(objectDef.fields)

packages/app-shell/src/views/RecordDetailView.tsx

Lines changed: 29 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { ActionResultDialog, type ResultDialogState } from './ActionResultDialog
2828
import { FlowRunner, type ScreenFlowState } from './FlowRunner';
2929
import { resolveActionParams } from '../utils/resolveActionParams';
3030
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
31-
import type { DetailViewSchema, FeedItem, HighlightField, SectionGroup } from '@object-ui/types';
31+
import type { DetailViewSchema, FeedItem, HighlightField } from '@object-ui/types';
3232
import type { ActionDef, ActionParamDef } from '@object-ui/core';
3333
import { useRecordApprovals } from '../hooks/useRecordApprovals';
3434
import { getRecordDisplayName } from '../utils';
@@ -77,9 +77,8 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
7777
/**
7878
* System/tenant fields that the framework auto-injects on every record but
7979
* which carry no business value on a detail page. Hidden from the
80-
* auto-generated section (when the object has no explicit form sections).
81-
* Authors who really want to show these can still list them in
82-
* `objectDef.views.form.sections`.
80+
* auto-generated sections. Authors who really want to surface one can
81+
* assign it to a `fieldGroups` group explicitly (explicit listing wins).
8382
*/
8483
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
8584
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
@@ -1225,49 +1224,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
12251224
(key) => key === 'name' || key === 'title'
12261225
);
12271226

1228-
// Build sections, in priority order:
1229-
// 1) explicit sections — the spec-writable `detail.sections` block,
1230-
// else legacy `views.form.sections` (the spec's ObjectSchema has no
1231-
// `views` key, so that source only exists on non-spec metadata);
1232-
// 2) sections derived from the designer's `fieldGroups` metadata
1233-
// (see the fieldGroups branch in the fallback below);
1234-
// 3) auto-grouping (primary + collapsible "More details").
1235-
const formSections = (objectDef as any).detail?.sections ?? objectDef.views?.form?.sections;
1236-
const sections = formSections && formSections.length > 0
1237-
? formSections.map((sec: any) => ({
1238-
title: sec.name ? sectionLabel(objectDef.name, sec.name, sec.title || sec.name) : sec.title,
1239-
collapsible: sec.collapsible,
1240-
defaultCollapsed: sec.defaultCollapsed,
1241-
fields: (sec.fields || [])
1242-
.filter((f: any) => {
1243-
// Honor `hidden: true` on a field def even when the form
1244-
// section explicitly lists it. Hidden fields are typically
1245-
// internal artifacts (e.g. database URL, environment id)
1246-
// that platform actions read but end-users shouldn't see.
1247-
const fieldName = typeof f === 'string' ? f : f.name;
1248-
const fieldDef = objectDef.fields?.[fieldName];
1249-
return !fieldDef?.hidden;
1250-
})
1251-
.map((f: any) => {
1252-
const fieldName = typeof f === 'string' ? f : f.name;
1253-
const fieldDef = objectDef.fields[fieldName];
1254-
if (!fieldDef) {
1255-
console.warn(`[RecordDetailView] Field "${fieldName}" not found in ${objectDef.name} definition`);
1256-
return { name: fieldName, label: fieldName };
1257-
}
1258-
const refTarget = fieldDef.reference_to || fieldDef.reference;
1259-
return {
1260-
name: fieldName,
1261-
label: fieldDef.label || fieldName,
1262-
type: fieldDef.type || 'text',
1263-
...(fieldDef.options && { options: fieldDef.options }),
1264-
...(refTarget && { reference_to: refTarget }),
1265-
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
1266-
...(fieldDef.currency && { currency: fieldDef.currency }),
1267-
};
1268-
}),
1269-
}))
1270-
: (() => {
1227+
// Build sections (ADR-0085: grouping is the `fieldGroups` semantic
1228+
// role — there is no per-surface sections override; per-page
1229+
// customization goes through an assigned Page schema):
1230+
// 1) sections derived from the object's `fieldGroups`;
1231+
// 2) auto-grouping (primary + collapsible "More details").
1232+
const sections = (() => {
12711233
const toField = (key: string) => {
12721234
const fieldDef = objectDef.fields[key];
12731235
const refTarget = fieldDef.reference_to || fieldDef.reference;
@@ -1319,13 +1281,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
13191281
];
13201282
};
13211283

1322-
// 2) fieldGroups-derived sections (object-designer metadata,
1323-
// same source the runtime form honours). Declared groups
1324-
// render as titled cards in declared order; the helper's
1284+
// 1) fieldGroups-derived sections (the ADR-0085 semantic role,
1285+
// same shared derivation the runtime form honours). Declared
1286+
// groups render as titled cards in declared order; the
13251287
// trailing untitled bucket (ungrouped fields) still goes
13261288
// through the primary/"More details" split so long-form
1327-
// fields stay tucked away. Objects can opt out via
1328-
// `detail.useFieldGroups: false`.
1289+
// fields stay tucked away.
13291290
const grouped = deriveFieldGroupDetailSections(objectDef as any);
13301291
if (grouped) {
13311292
return grouped.flatMap((sec: any) => {
@@ -1418,19 +1379,16 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
14181379
return base;
14191380
})();
14201381

1421-
// Build highlightFields: exclusively from objectDef metadata (no
1422-
// hardcoded fallback). The spec-writable `detail.highlightFields`
1423-
// wins; `views.detail.highlightFields` stays as back-compat for
1424-
// non-spec metadata (the spec's ObjectSchema has no `views` key).
1425-
// Entries may be bare field names — normalize them to the
1426-
// HighlightField shape by resolving label/type from the field def.
1382+
// Build highlightFields from the object's semantic role (ADR-0085):
1383+
// top-level `highlightFields`, with the deprecated `compactLayout`
1384+
// spelling as fallback for pre-11.7 metadata that reaches consumers
1385+
// un-normalized. Bare field names resolve label/type from the field def.
14271386
const rawHighlightFields =
1428-
(objectDef as any).detail?.highlightFields ?? objectDef.views?.detail?.highlightFields ?? [];
1387+
(objectDef as any).highlightFields ?? (objectDef as any).compactLayout ?? [];
14291388
const highlightFields: HighlightField[] = (Array.isArray(rawHighlightFields) ? rawHighlightFields : [])
14301389
.map((f: any): HighlightField | null => {
14311390
const name = typeof f === 'string' ? f : f?.name;
14321391
if (!name) return null;
1433-
if (typeof f === 'object' && f.label) return f as HighlightField;
14341392
const fieldDef = objectDef.fields?.[name];
14351393
return {
14361394
name,
@@ -1440,12 +1398,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
14401398
})
14411399
.filter((f): f is HighlightField => !!f);
14421400

1443-
// Build sectionGroups from objectDef detail/form config if available
1444-
const sectionGroups: SectionGroup[] | undefined =
1445-
(objectDef as any).detail?.sectionGroups
1446-
?? objectDef.views?.detail?.sectionGroups
1447-
?? objectDef.views?.form?.sectionGroups;
1448-
14491401
// Build related entries from reverse-reference child objects.
14501402
// `referenceField` is the FK field on the child pointing back to this
14511403
// record — passed so the related-list renderer can hide the redundant
@@ -1538,10 +1490,13 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
15381490
// right-rail can show meaningful labels (`user_agent`, `email`,
15391491
// …) instead of opaque IDs like `kCc8mhJr0bRs0r9Ykd09…`.
15401492
displayField:
1493+
childObjectDef?.nameField ||
15411494
childObjectDef?.displayNameField ||
1542-
(Array.isArray(childObjectDef?.compactLayout)
1543-
? childObjectDef.compactLayout[0]
1544-
: undefined),
1495+
(Array.isArray(childObjectDef?.highlightFields)
1496+
? childObjectDef.highlightFields[0]
1497+
: Array.isArray(childObjectDef?.compactLayout)
1498+
? childObjectDef.compactLayout[0]
1499+
: undefined),
15451500
onNew,
15461501
onViewAll,
15471502
onRowClick,
@@ -1576,7 +1531,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
15761531
}),
15771532
...(related.length > 0 && { related }),
15781533
...(highlightFields.length > 0 && { highlightFields }),
1579-
...(sectionGroups && sectionGroups.length > 0 && { sectionGroups }),
15801534
...(recordHeaderActions.length > 0 && {
15811535
actions: [{
15821536
type: 'action:bar',
@@ -1793,17 +1747,11 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
17931747
headerActions: synthHeaderActions,
17941748
related: synthRelated,
17951749
history: synthHistory,
1796-
// Per-object opt-outs read from `objectDef.detail.*`. Lets
1797-
// catalog/atomic objects (product, task, ...) keep a focused
1798-
// single-column layout instead of inheriting the rail.
1799-
showReferenceRail:
1800-
(objectDef as any)?.detail?.showReferenceRail === true || undefined,
1801-
hideReferenceRail:
1802-
(objectDef as any)?.detail?.hideReferenceRail === true || undefined,
1803-
hideRelatedTab:
1804-
(objectDef as any)?.detail?.hideRelatedTab === true || undefined,
1805-
relatedLayout:
1806-
(objectDef as any)?.detail?.relatedLayout === 'tabs' ? 'tabs' : undefined,
1750+
// ADR-0085 removed the per-object `detail.*` presentation
1751+
// toggles (show/hideReferenceRail, hideRelatedTab, relatedLayout)
1752+
// — the synth defaults apply; per-page layout goes through an
1753+
// assigned Page schema (`record:reference_rail` stays available
1754+
// there as a renderer capability).
18071755
...(assignedSlots ? { slots: assignedSlots } : {}),
18081756
});
18091757
return (

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"dependencies": {
3434
"@object-ui/types": "workspace:*",
3535
"@objectstack/formula": "^11.5.0",
36-
"@objectstack/spec": "^11.5.0",
36+
"@objectstack/spec": "^11.7.0",
3737
"lodash": "^4.18.1",
3838
"zod": "^4.4.3"
3939
},

packages/plugin-detail/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
},
3333
"dependencies": {
3434
"@object-ui/i18n": "workspace:*",
35+
"@objectstack/spec": "^11.7.0",
3536
"lucide-react": "^1.22.0"
3637
},
3738
"peerDependencies": {

0 commit comments

Comments
 (0)