Skip to content
Merged
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
13 changes: 13 additions & 0 deletions .changeset/adr-0085-consumer-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@object-ui/plugin-form': minor
'@object-ui/plugin-detail': minor
'@object-ui/plugin-grid': minor
'@object-ui/app-shell': minor
---

Consume the ADR-0085 object semantic roles from `@objectstack/spec@11.7.0`, retiring the per-surface hint dialects:

- **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).
- **`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).
- **`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).
- **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.
2 changes: 1 addition & 1 deletion apps/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/client": "^11.5.0",
"@objectstack/spec": "^11.5.0",
"@objectstack/spec": "^11.7.0",
"@tailwindcss/postcss": "^4.3.1",
"@tailwindcss/typography": "^0.5.20",
"@testing-library/jest-dom": "^6.9.1",
Expand Down
2 changes: 1 addition & 1 deletion apps/site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"@object-ui/plugin-view": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^11.5.0",
"@objectstack/spec": "^11.7.0",
"fumadocs-core": "16.10.6",
"fumadocs-mdx": "15.0.13",
"fumadocs-ui": "16.10.6",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"devDependencies": {
"@changesets/cli": "^2.31.0",
"@eslint/js": "^10.0.1",
"@objectstack/spec": "^11.5.0",
"@objectstack/spec": "^11.7.0",
"@playwright/test": "^1.61.1",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/app-shell/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@object-ui/providers": "workspace:*",
"@object-ui/react": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/spec": "^11.5.0",
"@objectstack/spec": "^11.7.0",
"@sentry/react": "^10.62.0",
"jsonc-parser": "^3.3.1",
"lucide-react": "^1.22.0",
Expand Down
10 changes: 7 additions & 3 deletions packages/app-shell/src/views/InterfaceListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ function resolveSourceView(objectDef: any, sourceView?: string): any | undefined
/**
* Default column set when the resolved view carries none — mirrors
* ObjectView's data-mode fallback so an interface page never renders a
* column-less grid. Priority: curated `compactLayout`, else the first
* column-less grid. Priority: the `highlightFields` semantic role
* (ADR-0085; deprecated `compactLayout` read as fallback), else the first
* business fields (system/audit columns excluded).
*/
const SYSTEM_FIELDS = new Set([
Expand All @@ -78,8 +79,11 @@ const SYSTEM_FIELDS = new Set([
'updated_by', 'updatedBy', '_version', '_rev',
]);
function defaultColumnsFromObject(objectDef: any): string[] {
if (Array.isArray(objectDef?.compactLayout) && objectDef.compactLayout.length > 0) {
return objectDef.compactLayout.filter((n: string) => objectDef.fields?.[n]);
const curated = Array.isArray(objectDef?.highlightFields) && objectDef.highlightFields.length > 0
? objectDef.highlightFields
: objectDef?.compactLayout;
if (Array.isArray(curated) && curated.length > 0) {
return curated.filter((n: string) => objectDef.fields?.[n]);
}
const fields = objectDef?.fields;
if (fields && typeof fields === 'object') {
Expand Down
17 changes: 12 additions & 5 deletions packages/app-shell/src/views/ObjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
'updated_by', 'updatedBy', '_version', '_rev',
]);
let defaultColumns: string[] = [];
if (Array.isArray(objectDef?.compactLayout) && objectDef.compactLayout.length > 0) {
defaultColumns = objectDef.compactLayout.filter((n: string) => objectDef.fields?.[n]);
const curated = Array.isArray(objectDef?.highlightFields) && objectDef.highlightFields.length > 0
? objectDef.highlightFields
: objectDef?.compactLayout;
if (Array.isArray(curated) && curated.length > 0) {
defaultColumns = curated.filter((n: string) => objectDef.fields?.[n]);
} else if (objectDef?.fields) {
defaultColumns = Object.entries(objectDef.fields)
.filter(([name, f]: [string, any]) => f && !f.hidden && !SYSTEM_FIELDS.has(name))
Expand Down Expand Up @@ -486,7 +489,8 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
// Resolve Views from objectDef.listViews (camelCase per @objectstack/spec)
const views = useMemo(() => {
// Default column resolution priority:
// 1. `compactLayout` (curated primary business fields).
// 1. The `highlightFields` semantic role (ADR-0085; deprecated
// `compactLayout` read as fallback).
// 2. Business fields only — exclude system-managed identifiers/audit
// columns (id, created_at, updated_at, …) and fields explicitly
// marked hidden/readonly on the schema. First 5 kept for compactness.
Expand All @@ -496,8 +500,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an
'updated_by', 'updatedBy', '_version', '_rev',
]);
const resolveDefaultColumns = (): string[] => {
if (Array.isArray(objectDef.compactLayout) && objectDef.compactLayout.length > 0) {
return objectDef.compactLayout.filter((n: string) => objectDef.fields?.[n]);
const curated = Array.isArray(objectDef.highlightFields) && objectDef.highlightFields.length > 0
? objectDef.highlightFields
: objectDef.compactLayout;
if (Array.isArray(curated) && curated.length > 0) {
return curated.filter((n: string) => objectDef.fields?.[n]);
}
if (objectDef.fields) {
return Object.entries(objectDef.fields)
Expand Down
110 changes: 29 additions & 81 deletions packages/app-shell/src/views/RecordDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { ActionResultDialog, type ResultDialogState } from './ActionResultDialog
import { FlowRunner, type ScreenFlowState } from './FlowRunner';
import { resolveActionParams } from '../utils/resolveActionParams';
import { useRecordBreadcrumbTitle } from '../context/NavigationContext';
import type { DetailViewSchema, FeedItem, HighlightField, SectionGroup } from '@object-ui/types';
import type { DetailViewSchema, FeedItem, HighlightField } from '@object-ui/types';
import type { ActionDef, ActionParamDef } from '@object-ui/core';
import { useRecordApprovals } from '../hooks/useRecordApprovals';
import { getRecordDisplayName } from '../utils';
Expand Down Expand Up @@ -77,9 +77,8 @@ const AUDIT_FIELD_NAMES = new Set(['created_at', 'created_by', 'updated_at', 'up
/**
* System/tenant fields that the framework auto-injects on every record but
* which carry no business value on a detail page. Hidden from the
* auto-generated section (when the object has no explicit form sections).
* Authors who really want to show these can still list them in
* `objectDef.views.form.sections`.
* auto-generated sections. Authors who really want to surface one can
* assign it to a `fieldGroups` group explicitly (explicit listing wins).
*/
const HIDDEN_SYSTEM_FIELD_NAMES = new Set([
'organization_id', 'tenant_id', 'is_deleted', 'deleted_at',
Expand Down Expand Up @@ -1225,49 +1224,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
(key) => key === 'name' || key === 'title'
);

// Build sections, in priority order:
// 1) explicit sections — the spec-writable `detail.sections` block,
// else legacy `views.form.sections` (the spec's ObjectSchema has no
// `views` key, so that source only exists on non-spec metadata);
// 2) sections derived from the designer's `fieldGroups` metadata
// (see the fieldGroups branch in the fallback below);
// 3) auto-grouping (primary + collapsible "More details").
const formSections = (objectDef as any).detail?.sections ?? objectDef.views?.form?.sections;
const sections = formSections && formSections.length > 0
? formSections.map((sec: any) => ({
title: sec.name ? sectionLabel(objectDef.name, sec.name, sec.title || sec.name) : sec.title,
collapsible: sec.collapsible,
defaultCollapsed: sec.defaultCollapsed,
fields: (sec.fields || [])
.filter((f: any) => {
// Honor `hidden: true` on a field def even when the form
// section explicitly lists it. Hidden fields are typically
// internal artifacts (e.g. database URL, environment id)
// that platform actions read but end-users shouldn't see.
const fieldName = typeof f === 'string' ? f : f.name;
const fieldDef = objectDef.fields?.[fieldName];
return !fieldDef?.hidden;
})
.map((f: any) => {
const fieldName = typeof f === 'string' ? f : f.name;
const fieldDef = objectDef.fields[fieldName];
if (!fieldDef) {
console.warn(`[RecordDetailView] Field "${fieldName}" not found in ${objectDef.name} definition`);
return { name: fieldName, label: fieldName };
}
const refTarget = fieldDef.reference_to || fieldDef.reference;
return {
name: fieldName,
label: fieldDef.label || fieldName,
type: fieldDef.type || 'text',
...(fieldDef.options && { options: fieldDef.options }),
...(refTarget && { reference_to: refTarget }),
...(fieldDef.reference_field && { reference_field: fieldDef.reference_field }),
...(fieldDef.currency && { currency: fieldDef.currency }),
};
}),
}))
: (() => {
// Build sections (ADR-0085: grouping is the `fieldGroups` semantic
// role — there is no per-surface sections override; per-page
// customization goes through an assigned Page schema):
// 1) sections derived from the object's `fieldGroups`;
// 2) auto-grouping (primary + collapsible "More details").
const sections = (() => {
const toField = (key: string) => {
const fieldDef = objectDef.fields[key];
const refTarget = fieldDef.reference_to || fieldDef.reference;
Expand Down Expand Up @@ -1319,13 +1281,12 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
];
};

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

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

// Build sectionGroups from objectDef detail/form config if available
const sectionGroups: SectionGroup[] | undefined =
(objectDef as any).detail?.sectionGroups
?? objectDef.views?.detail?.sectionGroups
?? objectDef.views?.form?.sectionGroups;

// Build related entries from reverse-reference child objects.
// `referenceField` is the FK field on the child pointing back to this
// record — passed so the related-list renderer can hide the redundant
Expand Down Expand Up @@ -1538,10 +1490,13 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
// right-rail can show meaningful labels (`user_agent`, `email`,
// …) instead of opaque IDs like `kCc8mhJr0bRs0r9Ykd09…`.
displayField:
childObjectDef?.nameField ||
childObjectDef?.displayNameField ||
(Array.isArray(childObjectDef?.compactLayout)
? childObjectDef.compactLayout[0]
: undefined),
(Array.isArray(childObjectDef?.highlightFields)
? childObjectDef.highlightFields[0]
: Array.isArray(childObjectDef?.compactLayout)
? childObjectDef.compactLayout[0]
: undefined),
onNew,
onViewAll,
onRowClick,
Expand Down Expand Up @@ -1576,7 +1531,6 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
}),
...(related.length > 0 && { related }),
...(highlightFields.length > 0 && { highlightFields }),
...(sectionGroups && sectionGroups.length > 0 && { sectionGroups }),
...(recordHeaderActions.length > 0 && {
actions: [{
type: 'action:bar',
Expand Down Expand Up @@ -1793,17 +1747,11 @@ export function RecordDetailView({ dataSource, objects, onEdit, objectNameOverri
headerActions: synthHeaderActions,
related: synthRelated,
history: synthHistory,
// Per-object opt-outs read from `objectDef.detail.*`. Lets
// catalog/atomic objects (product, task, ...) keep a focused
// single-column layout instead of inheriting the rail.
showReferenceRail:
(objectDef as any)?.detail?.showReferenceRail === true || undefined,
hideReferenceRail:
(objectDef as any)?.detail?.hideReferenceRail === true || undefined,
hideRelatedTab:
(objectDef as any)?.detail?.hideRelatedTab === true || undefined,
relatedLayout:
(objectDef as any)?.detail?.relatedLayout === 'tabs' ? 'tabs' : undefined,
// ADR-0085 removed the per-object `detail.*` presentation
// toggles (show/hideReferenceRail, hideRelatedTab, relatedLayout)
// — the synth defaults apply; per-page layout goes through an
// assigned Page schema (`record:reference_rail` stays available
// there as a renderer capability).
...(assignedSlots ? { slots: assignedSlots } : {}),
});
return (
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"@object-ui/types": "workspace:*",
"@objectstack/formula": "^11.5.0",
"@objectstack/spec": "^11.5.0",
"@objectstack/spec": "^11.7.0",
"lodash": "^4.18.1",
"zod": "^4.4.3"
},
Expand Down
1 change: 1 addition & 0 deletions packages/plugin-detail/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"dependencies": {
"@object-ui/i18n": "workspace:*",
"@objectstack/spec": "^11.7.0",
"lucide-react": "^1.22.0"
},
"peerDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions packages/plugin-detail/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,9 @@ ComponentRegistry.register('record:details', RecordDetailsRenderer, {
// Designer inputs mirror @objectstack/spec RecordDetailsProps (component.zod).
inputs: [
{ name: 'columns', type: 'enum', label: 'Columns', enum: ['1', '2', '3', '4'], defaultValue: '2', description: 'Number of columns for field layout (1-4)' },
{ name: 'layout', type: 'enum', label: 'Layout', enum: ['auto', 'custom'], defaultValue: 'auto', description: 'auto uses the object compactLayout; custom uses explicit sections' },
{ name: 'layout', type: 'enum', label: 'Layout', enum: ['auto', 'custom'], defaultValue: 'auto', description: 'auto uses the object highlightFields; custom uses explicit sections' },
{ name: 'sections', type: 'array', label: 'Sections', description: 'Section IDs to show (required when layout is "custom")' },
{ name: 'fields', type: 'array', label: 'Fields', description: 'Explicit field list (overrides compactLayout)' },
{ name: 'fields', type: 'array', label: 'Fields', description: 'Explicit field list (overrides highlightFields)' },
],
});

Expand Down
Loading
Loading