Skip to content
Open
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
22 changes: 22 additions & 0 deletions .changeset/drop-dead-spec-bridge-page-dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@object-ui/react": major
---

refactor(spec-bridge): remove the dead page/dashboard bridges (#1892)

`SpecBridge`'s `page` and `dashboard` bridges — `bridgePage`, `bridgeDashboard`,
and the `SpecBridge#transformPage` / `#transformDashboard` methods — had no
runtime consumer. Pages render through their own renderer and dashboards
through `DashboardView → DashboardRenderer → DatasetWidget` (ADR-0021); neither
path routes through `SpecBridge`. The dashboard bridge's input shape
(`object` / `categoryField` / `valueField` / `aggregate`) is the pre-ADR-0021
widget model, which the strict `DashboardWidgetSchema` now rejects — so the
bridge could not receive a spec-valid dashboard even in principle.

Flagged dead by the metadata-liveness audit (framework #1878 / #1892). The
`list` and `form` bridges are unaffected and remain the live authoring path.

BREAKING CHANGE: the public exports `bridgePage`, `bridgeDashboard`, and the
`SpecBridge#transformPage` / `#transformDashboard` methods are removed. There
is no replacement — render pages and dashboards through their renderers
(`DashboardRenderer` / the page renderer) directly.
14 changes: 0 additions & 14 deletions packages/react/src/spec-bridge/SpecBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import type { SchemaNode } from '@object-ui/core';
import type { BridgeContext, BridgeFn } from './types';
import { bridgeListView } from './bridges/list-view';
import { bridgeFormView } from './bridges/form-view';
import { bridgePage } from './bridges/page';
import { bridgeDashboard } from './bridges/dashboard';

export class SpecBridge {
private bridges = new Map<string, BridgeFn>();
Expand All @@ -21,8 +19,6 @@ export class SpecBridge {
this.context = context;
this.register('list', bridgeListView);
this.register('form', bridgeFormView);
this.register('page', bridgePage);
this.register('dashboard', bridgeDashboard);
}

register(specType: string, bridge: BridgeFn): void {
Expand All @@ -48,16 +44,6 @@ export class SpecBridge {
return this.transform('form', spec);
}

/** Transform a Page spec */
transformPage(spec: any): SchemaNode {
return this.transform('page', spec);
}

/** Transform a Dashboard spec */
transformDashboard(spec: any): SchemaNode {
return this.transform('dashboard', spec);
}

updateContext(ctx: Partial<BridgeContext>): void {
this.context = { ...this.context, ...ctx };
}
Expand Down
318 changes: 1 addition & 317 deletions packages/react/src/spec-bridge/__tests__/P1SpecBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/**
* P1 SpecBridge Protocol Alignment Tests
* Tests for the enhanced bridge transformations: ListView, FormView, Dashboard, Page
* Tests for the enhanced bridge transformations: ListView, FormView
*/
import { describe, it, expect } from 'vitest';
import { SpecBridge } from '../SpecBridge';
Expand Down Expand Up @@ -336,322 +336,6 @@ describe('P1 SpecBridge Protocol Alignment', () => {
});
});

// ========================================================================
// P1.3 Dashboard Bridge Enhancements
// ========================================================================
describe('P1.3 Dashboard bridge enhancements', () => {
it('should map widget data binding properties', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'sales',
widgets: [
{
title: 'Revenue',
type: 'bar-chart',
object: 'Opportunity',
filter: [['stage', '=', 'Closed Won']],
categoryField: 'region',
valueField: 'amount',
aggregate: 'sum',
},
],
});

const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
const widgets = widgetGrid.body as any[];
expect(widgets[0].object).toBe('Opportunity');
expect(widgets[0].categoryField).toBe('region');
expect(widgets[0].valueField).toBe('amount');
expect(widgets[0].aggregate).toBe('sum');
});

it('should map widget color variants', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'colored',
widgets: [
{ title: 'Success', type: 'metric', colorVariant: 'success' },
{ title: 'Danger', type: 'metric', colorVariant: 'danger' },
],
});

const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
const widgets = widgetGrid.body as any[];
expect(widgets[0].colorVariant).toBe('success');
expect(widgets[1].colorVariant).toBe('danger');
});

it('should map widget measures (pivot/matrix)', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'pivot',
widgets: [
{
title: 'Matrix',
type: 'pivot',
measures: [
{ valueField: 'amount', aggregate: 'sum', label: 'Total', format: '$0,0' },
],
},
],
});

const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
const widgets = widgetGrid.body as any[];
expect(widgets[0].measures).toHaveLength(1);
expect(widgets[0].measures[0].label).toBe('Total');
});

it('should map globalFilters with optionsFrom', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'filtered',
widgets: [],
globalFilters: [
{
field: 'region',
label: 'Region',
type: 'select',
optionsFrom: {
object: 'Region',
valueField: 'id',
labelField: 'name',
},
targetWidgets: ['widget-0'],
},
],
});

const filterBar = (node.body as any[]).find((b: any) => b.type === 'filter-bar');
expect(filterBar).toBeDefined();
expect(filterBar.filters[0].field).toBe('region');
expect(filterBar.filters[0].optionsFrom.object).toBe('Region');
expect(filterBar.filters[0].targetWidgets).toEqual(['widget-0']);
});

it('should map dateRange filter', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'dated',
widgets: [],
dateRange: {
field: 'created_at',
defaultRange: 'last_30_days',
allowCustomRange: true,
},
});

const dateFilter = (node.body as any[]).find((b: any) => b.type === 'date-range-filter');
expect(dateFilter).toBeDefined();
expect(dateFilter.field).toBe('created_at');
expect(dateFilter.defaultRange).toBe('last_30_days');
expect(dateFilter.allowCustomRange).toBe(true);
});

it('should map DashboardHeader with actions', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'with_header',
widgets: [],
header: {
showTitle: true,
actions: [
{ label: 'Refresh', actionType: 'refresh', icon: 'RefreshCw' },
],
},
});

const header = (node.body as any[]).find((b: any) => b.type === 'dashboard-header');
expect(header).toBeDefined();
expect(header.showTitle).toBe(true);
expect(header.actions).toHaveLength(1);
});

it('should pass through dashboard description and refreshInterval', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'auto_refresh',
label: 'Live Dashboard',
description: 'Real-time sales data',
refreshInterval: 30,
widgets: [],
});

expect(node.description).toBe('Real-time sales data');
expect(node.refreshInterval).toBe(30);
});

it('should pass through dashboard aria', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'accessible',
widgets: [],
aria: { ariaLabel: 'Sales Dashboard', role: 'region' },
});

expect(node.aria).toBeDefined();
expect(node.aria.ariaLabel).toBe('Sales Dashboard');
});

it('should map widget action properties', () => {
const bridge = new SpecBridge();
const node = bridge.transformDashboard({
name: 'clickable',
widgets: [
{
title: 'Open Deals',
type: 'metric',
actionUrl: '/opportunities?status=open',
actionType: 'navigate',
actionIcon: 'ArrowRight',
},
],
});

const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
const widgets = widgetGrid.body as any[];
expect(widgets[0].actionUrl).toBe('/opportunities?status=open');
expect(widgets[0].actionType).toBe('navigate');
expect(widgets[0].actionIcon).toBe('ArrowRight');
});
});

// ========================================================================
// P1.4 Page Bridge Enhancements
// ========================================================================
describe('P1.4 Page bridge enhancements', () => {
it('should map expanded page types', () => {
const bridge = new SpecBridge();
const pageTypes = [
'record', 'home', 'app', 'utility',
'grid', 'list', 'gallery', 'kanban', 'calendar', 'timeline',
];

pageTypes.forEach((pageType) => {
const node = bridge.transformPage({
name: `${pageType}_page`,
type: pageType,
});
expect(node.pageType).toBe(pageType);
});
});

// (Removed "should map blank page layout" — the bridge no longer maps
// blankLayout; the `blank` page type has no renderer, dropped from
// @objectstack/spec (framework#2265).)

it('should map page variables with source', () => {
const bridge = new SpecBridge();
const node = bridge.transformPage({
name: 'record_page',
type: 'record',
variables: [
{ name: 'recordId', type: 'record_id', source: 'url_param' },
{ name: 'tab', type: 'string', defaultValue: 'details' },
],
});

expect(node.variables).toHaveLength(2);
expect(node.variables[0].name).toBe('recordId');
expect(node.variables[0].type).toBe('record_id');
expect(node.variables[0].source).toBe('url_param');
});

it('should map event handlers on page components', () => {
const bridge = new SpecBridge();
const node = bridge.transformPage({
name: 'interactive',
regions: [
{
name: 'main',
components: [
{
type: 'element:button',
id: 'save-btn',
label: 'Save',
events: {
onClick: '${actions.save()}',
onHover: '${actions.highlight()}',
},
},
],
},
],
});

const region = (node.body as any[])[0];
const component = region.body[0];
expect(component.events).toBeDefined();
expect(component.events.onClick).toBe('${actions.save()}');
});

it('should map responsive config on page components', () => {
const bridge = new SpecBridge();
const node = bridge.transformPage({
name: 'responsive',
regions: [
{
name: 'main',
components: [
{
type: 'element:text',
id: 'heading',
responsive: {
sm: { hidden: true },
md: { hidden: false },
},
},
],
},
],
});

const region = (node.body as any[])[0];
const component = region.body[0];
expect(component.responsive).toBeDefined();
expect(component.responsive.sm.hidden).toBe(true);
});

it('should pass through page description and icon', () => {
const bridge = new SpecBridge();
const node = bridge.transformPage({
name: 'detailed',
label: 'Home',
description: 'Main landing page',
icon: 'Home',
});

expect(node.description).toBe('Main landing page');
expect(node.icon).toBe('Home');
});

it('should pass through page aria', () => {
const bridge = new SpecBridge();
const node = bridge.transformPage({
name: 'accessible',
aria: { ariaLabel: 'Home Page', role: 'main' },
});

expect(node.aria).toBeDefined();
expect(node.aria.ariaLabel).toBe('Home Page');
});

it('should map region width', () => {
const bridge = new SpecBridge();
const node = bridge.transformPage({
name: 'sidebar_page',
regions: [
{ name: 'sidebar', width: 'small', components: [] },
{ name: 'main', width: 'large', components: [] },
],
});

const regions = node.body as any[];
expect(regions[0].width).toBe('small');
expect(regions[1].width).toBe('large');
});
});

// ========================================================================
// P2 Sharing / ExportOptions / Pagination Protocol Alignment
// ========================================================================
Expand Down
Loading
Loading