Skip to content

Commit c77108c

Browse files
os-zhuangclaude
andauthored
refactor(spec-bridge): remove the dead page/dashboard bridges (#1892) (#2817)
`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 routing 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 the live authoring path and are untouched. BREAKING CHANGE: removes the public exports `bridgePage`, `bridgeDashboard`, and `SpecBridge#transformPage` / `#transformDashboard`. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 059a052 commit c77108c

7 files changed

Lines changed: 24 additions & 931 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
"@object-ui/react": major
3+
---
4+
5+
refactor(spec-bridge): remove the dead page/dashboard bridges (#1892)
6+
7+
`SpecBridge`'s `page` and `dashboard` bridges — `bridgePage`, `bridgeDashboard`,
8+
and the `SpecBridge#transformPage` / `#transformDashboard` methods — had no
9+
runtime consumer. Pages render through their own renderer and dashboards
10+
through `DashboardView → DashboardRenderer → DatasetWidget` (ADR-0021); neither
11+
path routes through `SpecBridge`. The dashboard bridge's input shape
12+
(`object` / `categoryField` / `valueField` / `aggregate`) is the pre-ADR-0021
13+
widget model, which the strict `DashboardWidgetSchema` now rejects — so the
14+
bridge could not receive a spec-valid dashboard even in principle.
15+
16+
Flagged dead by the metadata-liveness audit (framework #1878 / #1892). The
17+
`list` and `form` bridges are unaffected and remain the live authoring path.
18+
19+
BREAKING CHANGE: the public exports `bridgePage`, `bridgeDashboard`, and the
20+
`SpecBridge#transformPage` / `#transformDashboard` methods are removed. There
21+
is no replacement — render pages and dashboards through their renderers
22+
(`DashboardRenderer` / the page renderer) directly.

‎packages/react/src/spec-bridge/SpecBridge.ts‎

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import type { SchemaNode } from '@object-ui/core';
1010
import type { BridgeContext, BridgeFn } from './types';
1111
import { bridgeListView } from './bridges/list-view';
1212
import { bridgeFormView } from './bridges/form-view';
13-
import { bridgePage } from './bridges/page';
14-
import { bridgeDashboard } from './bridges/dashboard';
1513

1614
export class SpecBridge {
1715
private bridges = new Map<string, BridgeFn>();
@@ -21,8 +19,6 @@ export class SpecBridge {
2119
this.context = context;
2220
this.register('list', bridgeListView);
2321
this.register('form', bridgeFormView);
24-
this.register('page', bridgePage);
25-
this.register('dashboard', bridgeDashboard);
2622
}
2723

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

51-
/** Transform a Page spec */
52-
transformPage(spec: any): SchemaNode {
53-
return this.transform('page', spec);
54-
}
55-
56-
/** Transform a Dashboard spec */
57-
transformDashboard(spec: any): SchemaNode {
58-
return this.transform('dashboard', spec);
59-
}
60-
6147
updateContext(ctx: Partial<BridgeContext>): void {
6248
this.context = { ...this.context, ...ctx };
6349
}

‎packages/react/src/spec-bridge/__tests__/P1SpecBridge.test.ts‎

Lines changed: 1 addition & 317 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
/**
1010
* P1 SpecBridge Protocol Alignment Tests
11-
* Tests for the enhanced bridge transformations: ListView, FormView, Dashboard, Page
11+
* Tests for the enhanced bridge transformations: ListView, FormView
1212
*/
1313
import { describe, it, expect } from 'vitest';
1414
import { SpecBridge } from '../SpecBridge';
@@ -336,322 +336,6 @@ describe('P1 SpecBridge Protocol Alignment', () => {
336336
});
337337
});
338338

339-
// ========================================================================
340-
// P1.3 Dashboard Bridge Enhancements
341-
// ========================================================================
342-
describe('P1.3 Dashboard bridge enhancements', () => {
343-
it('should map widget data binding properties', () => {
344-
const bridge = new SpecBridge();
345-
const node = bridge.transformDashboard({
346-
name: 'sales',
347-
widgets: [
348-
{
349-
title: 'Revenue',
350-
type: 'bar-chart',
351-
object: 'Opportunity',
352-
filter: [['stage', '=', 'Closed Won']],
353-
categoryField: 'region',
354-
valueField: 'amount',
355-
aggregate: 'sum',
356-
},
357-
],
358-
});
359-
360-
const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
361-
const widgets = widgetGrid.body as any[];
362-
expect(widgets[0].object).toBe('Opportunity');
363-
expect(widgets[0].categoryField).toBe('region');
364-
expect(widgets[0].valueField).toBe('amount');
365-
expect(widgets[0].aggregate).toBe('sum');
366-
});
367-
368-
it('should map widget color variants', () => {
369-
const bridge = new SpecBridge();
370-
const node = bridge.transformDashboard({
371-
name: 'colored',
372-
widgets: [
373-
{ title: 'Success', type: 'metric', colorVariant: 'success' },
374-
{ title: 'Danger', type: 'metric', colorVariant: 'danger' },
375-
],
376-
});
377-
378-
const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
379-
const widgets = widgetGrid.body as any[];
380-
expect(widgets[0].colorVariant).toBe('success');
381-
expect(widgets[1].colorVariant).toBe('danger');
382-
});
383-
384-
it('should map widget measures (pivot/matrix)', () => {
385-
const bridge = new SpecBridge();
386-
const node = bridge.transformDashboard({
387-
name: 'pivot',
388-
widgets: [
389-
{
390-
title: 'Matrix',
391-
type: 'pivot',
392-
measures: [
393-
{ valueField: 'amount', aggregate: 'sum', label: 'Total', format: '$0,0' },
394-
],
395-
},
396-
],
397-
});
398-
399-
const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
400-
const widgets = widgetGrid.body as any[];
401-
expect(widgets[0].measures).toHaveLength(1);
402-
expect(widgets[0].measures[0].label).toBe('Total');
403-
});
404-
405-
it('should map globalFilters with optionsFrom', () => {
406-
const bridge = new SpecBridge();
407-
const node = bridge.transformDashboard({
408-
name: 'filtered',
409-
widgets: [],
410-
globalFilters: [
411-
{
412-
field: 'region',
413-
label: 'Region',
414-
type: 'select',
415-
optionsFrom: {
416-
object: 'Region',
417-
valueField: 'id',
418-
labelField: 'name',
419-
},
420-
targetWidgets: ['widget-0'],
421-
},
422-
],
423-
});
424-
425-
const filterBar = (node.body as any[]).find((b: any) => b.type === 'filter-bar');
426-
expect(filterBar).toBeDefined();
427-
expect(filterBar.filters[0].field).toBe('region');
428-
expect(filterBar.filters[0].optionsFrom.object).toBe('Region');
429-
expect(filterBar.filters[0].targetWidgets).toEqual(['widget-0']);
430-
});
431-
432-
it('should map dateRange filter', () => {
433-
const bridge = new SpecBridge();
434-
const node = bridge.transformDashboard({
435-
name: 'dated',
436-
widgets: [],
437-
dateRange: {
438-
field: 'created_at',
439-
defaultRange: 'last_30_days',
440-
allowCustomRange: true,
441-
},
442-
});
443-
444-
const dateFilter = (node.body as any[]).find((b: any) => b.type === 'date-range-filter');
445-
expect(dateFilter).toBeDefined();
446-
expect(dateFilter.field).toBe('created_at');
447-
expect(dateFilter.defaultRange).toBe('last_30_days');
448-
expect(dateFilter.allowCustomRange).toBe(true);
449-
});
450-
451-
it('should map DashboardHeader with actions', () => {
452-
const bridge = new SpecBridge();
453-
const node = bridge.transformDashboard({
454-
name: 'with_header',
455-
widgets: [],
456-
header: {
457-
showTitle: true,
458-
actions: [
459-
{ label: 'Refresh', actionType: 'refresh', icon: 'RefreshCw' },
460-
],
461-
},
462-
});
463-
464-
const header = (node.body as any[]).find((b: any) => b.type === 'dashboard-header');
465-
expect(header).toBeDefined();
466-
expect(header.showTitle).toBe(true);
467-
expect(header.actions).toHaveLength(1);
468-
});
469-
470-
it('should pass through dashboard description and refreshInterval', () => {
471-
const bridge = new SpecBridge();
472-
const node = bridge.transformDashboard({
473-
name: 'auto_refresh',
474-
label: 'Live Dashboard',
475-
description: 'Real-time sales data',
476-
refreshInterval: 30,
477-
widgets: [],
478-
});
479-
480-
expect(node.description).toBe('Real-time sales data');
481-
expect(node.refreshInterval).toBe(30);
482-
});
483-
484-
it('should pass through dashboard aria', () => {
485-
const bridge = new SpecBridge();
486-
const node = bridge.transformDashboard({
487-
name: 'accessible',
488-
widgets: [],
489-
aria: { ariaLabel: 'Sales Dashboard', role: 'region' },
490-
});
491-
492-
expect(node.aria).toBeDefined();
493-
expect(node.aria.ariaLabel).toBe('Sales Dashboard');
494-
});
495-
496-
it('should map widget action properties', () => {
497-
const bridge = new SpecBridge();
498-
const node = bridge.transformDashboard({
499-
name: 'clickable',
500-
widgets: [
501-
{
502-
title: 'Open Deals',
503-
type: 'metric',
504-
actionUrl: '/opportunities?status=open',
505-
actionType: 'navigate',
506-
actionIcon: 'ArrowRight',
507-
},
508-
],
509-
});
510-
511-
const widgetGrid = (node.body as any[]).find((b: any) => b.type === 'widget-grid');
512-
const widgets = widgetGrid.body as any[];
513-
expect(widgets[0].actionUrl).toBe('/opportunities?status=open');
514-
expect(widgets[0].actionType).toBe('navigate');
515-
expect(widgets[0].actionIcon).toBe('ArrowRight');
516-
});
517-
});
518-
519-
// ========================================================================
520-
// P1.4 Page Bridge Enhancements
521-
// ========================================================================
522-
describe('P1.4 Page bridge enhancements', () => {
523-
it('should map expanded page types', () => {
524-
const bridge = new SpecBridge();
525-
const pageTypes = [
526-
'record', 'home', 'app', 'utility',
527-
'grid', 'list', 'gallery', 'kanban', 'calendar', 'timeline',
528-
];
529-
530-
pageTypes.forEach((pageType) => {
531-
const node = bridge.transformPage({
532-
name: `${pageType}_page`,
533-
type: pageType,
534-
});
535-
expect(node.pageType).toBe(pageType);
536-
});
537-
});
538-
539-
// (Removed "should map blank page layout" — the bridge no longer maps
540-
// blankLayout; the `blank` page type has no renderer, dropped from
541-
// @objectstack/spec (framework#2265).)
542-
543-
it('should map page variables with source', () => {
544-
const bridge = new SpecBridge();
545-
const node = bridge.transformPage({
546-
name: 'record_page',
547-
type: 'record',
548-
variables: [
549-
{ name: 'recordId', type: 'record_id', source: 'url_param' },
550-
{ name: 'tab', type: 'string', defaultValue: 'details' },
551-
],
552-
});
553-
554-
expect(node.variables).toHaveLength(2);
555-
expect(node.variables[0].name).toBe('recordId');
556-
expect(node.variables[0].type).toBe('record_id');
557-
expect(node.variables[0].source).toBe('url_param');
558-
});
559-
560-
it('should map event handlers on page components', () => {
561-
const bridge = new SpecBridge();
562-
const node = bridge.transformPage({
563-
name: 'interactive',
564-
regions: [
565-
{
566-
name: 'main',
567-
components: [
568-
{
569-
type: 'element:button',
570-
id: 'save-btn',
571-
label: 'Save',
572-
events: {
573-
onClick: '${actions.save()}',
574-
onHover: '${actions.highlight()}',
575-
},
576-
},
577-
],
578-
},
579-
],
580-
});
581-
582-
const region = (node.body as any[])[0];
583-
const component = region.body[0];
584-
expect(component.events).toBeDefined();
585-
expect(component.events.onClick).toBe('${actions.save()}');
586-
});
587-
588-
it('should map responsive config on page components', () => {
589-
const bridge = new SpecBridge();
590-
const node = bridge.transformPage({
591-
name: 'responsive',
592-
regions: [
593-
{
594-
name: 'main',
595-
components: [
596-
{
597-
type: 'element:text',
598-
id: 'heading',
599-
responsive: {
600-
sm: { hidden: true },
601-
md: { hidden: false },
602-
},
603-
},
604-
],
605-
},
606-
],
607-
});
608-
609-
const region = (node.body as any[])[0];
610-
const component = region.body[0];
611-
expect(component.responsive).toBeDefined();
612-
expect(component.responsive.sm.hidden).toBe(true);
613-
});
614-
615-
it('should pass through page description and icon', () => {
616-
const bridge = new SpecBridge();
617-
const node = bridge.transformPage({
618-
name: 'detailed',
619-
label: 'Home',
620-
description: 'Main landing page',
621-
icon: 'Home',
622-
});
623-
624-
expect(node.description).toBe('Main landing page');
625-
expect(node.icon).toBe('Home');
626-
});
627-
628-
it('should pass through page aria', () => {
629-
const bridge = new SpecBridge();
630-
const node = bridge.transformPage({
631-
name: 'accessible',
632-
aria: { ariaLabel: 'Home Page', role: 'main' },
633-
});
634-
635-
expect(node.aria).toBeDefined();
636-
expect(node.aria.ariaLabel).toBe('Home Page');
637-
});
638-
639-
it('should map region width', () => {
640-
const bridge = new SpecBridge();
641-
const node = bridge.transformPage({
642-
name: 'sidebar_page',
643-
regions: [
644-
{ name: 'sidebar', width: 'small', components: [] },
645-
{ name: 'main', width: 'large', components: [] },
646-
],
647-
});
648-
649-
const regions = node.body as any[];
650-
expect(regions[0].width).toBe('small');
651-
expect(regions[1].width).toBe('large');
652-
});
653-
});
654-
655339
// ========================================================================
656340
// P2 Sharing / ExportOptions / Pagination Protocol Alignment
657341
// ========================================================================

0 commit comments

Comments
 (0)