Skip to content

Commit a180264

Browse files
authored
Merge pull request #700 from objectstack-ai/copilot/complete-development-roadmap-one-more-time
2 parents 5948cb2 + ba6b5af commit a180264

16 files changed

Lines changed: 901 additions & 143 deletions

File tree

ROADMAP.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313

1414
ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind + Shadcn. It renders JSON metadata from the @objectstack/spec protocol into pixel-perfect, accessible, and interactive enterprise interfaces.
1515

16-
**Where We Are:** Foundation is **solid and shipping** — 35 packages, 91+ components, 5,070+ tests, 78 Storybook stories, 42/42 builds passing, ~85% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Designer Phase 1, Console through Phase 19 (L3), and **AppShell Navigation Renderer** (P0.1) — all ✅ complete.
16+
**Where We Are:** Foundation is **solid and shipping** — 35 packages, 91+ components, 5,100+ tests, 78 Storybook stories, 42/42 builds passing, ~85% protocol alignment. SpecBridge, Expression Engine, Action Engine, data binding, all view plugins (Grid/Kanban/Calendar/Gantt/Timeline/Map/Gallery), Record components, Report engine, Dashboard BI features, mobile UX, i18n (11 locales), WCAG AA accessibility, Designer Phase 1 (ViewDesigner drag-to-reorder ✅), Console through Phase 20 (L3), and **AppShell Navigation Renderer** (P0.1) — all ✅ complete.
1717

1818
**What Remains:** The gap to **Airtable-level UX** is primarily in:
1919
1. ~~**AppShell** — No dynamic navigation renderer from spec JSON (last P0 blocker)~~ ✅ Complete
20-
2. **Designer Interaction** — ViewDesigner and DataModelDesigner have undo/redo, field type selectors, inline editing, Ctrl+S save (column drag-to-reorder with dnd-kit pending)
20+
2. **Designer Interaction** — ViewDesigner and DataModelDesigner have undo/redo, field type selectors, inline editing, Ctrl+S save, column drag-to-reorder with dnd-kit
2121
3. **Dashboard Config Panel** — Airtable-style right-side configuration panel for dashboards (data source, layout, widget properties, sub-editors, type definitions)
2222
4. **Console Advanced Polish** — Remaining upgrades for forms, import/export, automation, comments
2323
5. **PWA Sync** — Background sync is simulated only
@@ -47,7 +47,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
4747
> Source: ROADMAP_DESIGNER Phase 2. These two designers are the core user workflow.
4848
4949
**ViewDesigner:**
50-
- [ ] Column drag-to-reorder via `@dnd-kit/core` (replace up/down buttons with drag handles)
50+
- [x] Column drag-to-reorder via `@dnd-kit/core` (replace up/down buttons with drag handles)
5151
- [x] Add `Ctrl+S`/`Cmd+S` keyboard shortcut to save
5252
- [x] Add field type selector dropdown with icons from `DESIGNER_FIELD_TYPES`
5353
- [x] Column width validation (min/max/pattern check)
@@ -139,7 +139,7 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
139139
> Airtable-style right-side configuration panel for dashboards. Phased rollout from shared infrastructure to full type-safe editing.
140140
141141
**Phase 0 — Component Infrastructure:**
142-
- [ ] Extract `ConfigRow` / `SectionHeader` from `ViewConfigPanel` into `@object-ui/components` as reusable primitives
142+
- [x] Extract `ConfigRow` / `SectionHeader` from `ViewConfigPanel` into `@object-ui/components` as reusable primitives
143143

144144
**Phase 1 — Dashboard-Level Config Panel:**
145145
- [ ] Develop `DashboardConfigPanel` supporting data source, layout (columns/gap), filtering, appearance, user filters & actions
@@ -156,8 +156,8 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind
156156
- [ ] Add Storybook stories for `DashboardConfigPanel` and `DashboardWithConfig`
157157

158158
**Phase 5 — Type Definitions & Validation:**
159-
- [ ] Add `DashboardConfig` types to `@object-ui/types`
160-
- [ ] Add Zod schema validation for `DashboardConfig`
159+
- [x] Add `DashboardConfig` types to `@object-ui/types`
160+
- [x] Add Zod schema validation for `DashboardConfig`
161161

162162
### P1.9 Console — Content Area Layout & Responsiveness
163163

apps/console/src/__tests__/ViewConfigPanel.test.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,25 @@ vi.mock('@object-ui/components', () => ({
5151
{...props}
5252
/>
5353
),
54+
ConfigRow: ({ label, value, onClick, children, className }: any) => {
55+
const Wrapper = onClick ? 'button' : 'div';
56+
return (
57+
<Wrapper className={className} onClick={onClick} type={onClick ? 'button' : undefined}>
58+
<span>{label}</span>
59+
{children || <span>{value}</span>}
60+
</Wrapper>
61+
);
62+
},
63+
SectionHeader: ({ title, collapsible, collapsed, onToggle, testId }: any) => {
64+
if (collapsible) {
65+
return (
66+
<button data-testid={testId} onClick={onToggle} type="button" aria-expanded={!collapsed}>
67+
<h3>{title}</h3>
68+
</button>
69+
);
70+
}
71+
return <div data-testid={testId}><h3>{title}</h3></div>;
72+
},
5473
FilterBuilder: ({ fields, value, onChange }: any) => {
5574
let counter = 0;
5675
return (

apps/console/src/components/ViewConfigPanel.tsx

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
*/
1414

1515
import { useMemo, useEffect, useRef, useState, useCallback } from 'react';
16-
import { Button, Switch, Input, Checkbox, FilterBuilder, SortBuilder } from '@object-ui/components';
16+
import { Button, Switch, Input, Checkbox, FilterBuilder, SortBuilder, ConfigRow, SectionHeader } from '@object-ui/components';
1717
import type { FilterGroup, SortItem } from '@object-ui/components';
18-
import { X, Save, RotateCcw, ChevronDown, ChevronRight, ArrowUp, ArrowDown } from 'lucide-react';
18+
import { X, Save, RotateCcw, ArrowUp, ArrowDown } from 'lucide-react';
1919
import { useObjectTranslation } from '@object-ui/i18n';
2020

2121
// ---------------------------------------------------------------------------
@@ -235,50 +235,6 @@ export interface ViewConfigPanelProps {
235235
onCreate?: (config: Record<string, any>) => void;
236236
}
237237

238-
/** A single labeled row in the config panel */
239-
function ConfigRow({ label, value, onClick, children }: { label: string; value?: string; onClick?: () => void; children?: React.ReactNode }) {
240-
const Wrapper = onClick ? 'button' : 'div';
241-
return (
242-
<Wrapper
243-
className={`flex items-center justify-between py-1.5 min-h-[32px] w-full text-left ${onClick ? 'cursor-pointer hover:bg-accent/50 rounded-sm -mx-1 px-1' : ''}`}
244-
onClick={onClick}
245-
type={onClick ? 'button' : undefined}
246-
>
247-
<span className="text-xs text-muted-foreground shrink-0">{label}</span>
248-
{children || (
249-
<span className="text-xs text-foreground truncate ml-4 text-right">{value}</span>
250-
)}
251-
</Wrapper>
252-
);
253-
}
254-
255-
/** Section heading with optional collapse/expand support */
256-
function SectionHeader({ title, collapsible, collapsed, onToggle, testId }: { title: string; collapsible?: boolean; collapsed?: boolean; onToggle?: () => void; testId?: string }) {
257-
if (collapsible) {
258-
return (
259-
<button
260-
data-testid={testId}
261-
className="flex items-center justify-between pt-4 pb-1.5 first:pt-0 w-full text-left"
262-
onClick={onToggle}
263-
type="button"
264-
aria-expanded={!collapsed}
265-
>
266-
<h3 className="text-xs font-semibold text-foreground uppercase tracking-wider">{title}</h3>
267-
{collapsed ? (
268-
<ChevronRight className="h-3 w-3 text-muted-foreground" />
269-
) : (
270-
<ChevronDown className="h-3 w-3 text-muted-foreground" />
271-
)}
272-
</button>
273-
);
274-
}
275-
return (
276-
<div className="pt-4 pb-1.5 first:pt-0" data-testid={testId}>
277-
<h3 className="text-xs font-semibold text-foreground uppercase tracking-wider">{title}</h3>
278-
</div>
279-
);
280-
}
281-
282238
export function ViewConfigPanel({ open, onClose, mode = 'edit', activeView, objectDef, onViewUpdate, onSave, onCreate }: ViewConfigPanelProps) {
283239
const { t } = useObjectTranslation();
284240
const panelRef = useRef<HTMLDivElement>(null);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { describe, it, expect, vi } from 'vitest';
10+
import { render, screen, fireEvent } from '@testing-library/react';
11+
import { ConfigRow } from '../custom/config-row';
12+
import { SectionHeader } from '../custom/section-header';
13+
14+
describe('ConfigRow', () => {
15+
it('should render label and value', () => {
16+
render(<ConfigRow label="Color" value="Blue" />);
17+
expect(screen.getByText('Color')).toBeDefined();
18+
expect(screen.getByText('Blue')).toBeDefined();
19+
});
20+
21+
it('should render as div when not clickable', () => {
22+
const { container } = render(<ConfigRow label="Label" value="Value" />);
23+
expect(container.querySelector('button')).toBeNull();
24+
expect(container.querySelector('div')).not.toBeNull();
25+
});
26+
27+
it('should render as button when onClick is provided', () => {
28+
const onClick = vi.fn();
29+
const { container } = render(<ConfigRow label="Label" value="Value" onClick={onClick} />);
30+
const button = container.querySelector('button');
31+
expect(button).not.toBeNull();
32+
fireEvent.click(button!);
33+
expect(onClick).toHaveBeenCalledTimes(1);
34+
});
35+
36+
it('should render children instead of value when provided', () => {
37+
render(
38+
<ConfigRow label="Custom">
39+
<span data-testid="custom-child">Custom Content</span>
40+
</ConfigRow>,
41+
);
42+
expect(screen.getByTestId('custom-child')).toBeDefined();
43+
expect(screen.getByText('Custom Content')).toBeDefined();
44+
});
45+
46+
it('should apply custom className', () => {
47+
const { container } = render(<ConfigRow label="Label" className="custom-class" />);
48+
expect(container.firstElementChild?.className).toContain('custom-class');
49+
});
50+
});
51+
52+
describe('SectionHeader', () => {
53+
it('should render title text', () => {
54+
render(<SectionHeader title="Data" />);
55+
expect(screen.getByText('Data')).toBeDefined();
56+
});
57+
58+
it('should render as div when not collapsible', () => {
59+
const { container } = render(<SectionHeader title="Data" testId="section" />);
60+
const element = screen.getByTestId('section');
61+
expect(element.tagName).toBe('DIV');
62+
});
63+
64+
it('should render as button when collapsible', () => {
65+
render(<SectionHeader title="Data" collapsible testId="section" />);
66+
const element = screen.getByTestId('section');
67+
expect(element.tagName).toBe('BUTTON');
68+
});
69+
70+
it('should set aria-expanded when collapsible', () => {
71+
render(<SectionHeader title="Data" collapsible collapsed={false} testId="section" />);
72+
const element = screen.getByTestId('section');
73+
expect(element.getAttribute('aria-expanded')).toBe('true');
74+
});
75+
76+
it('should set aria-expanded=false when collapsed', () => {
77+
render(<SectionHeader title="Data" collapsible collapsed testId="section" />);
78+
const element = screen.getByTestId('section');
79+
expect(element.getAttribute('aria-expanded')).toBe('false');
80+
});
81+
82+
it('should call onToggle when collapsible button is clicked', () => {
83+
const onToggle = vi.fn();
84+
render(<SectionHeader title="Data" collapsible onToggle={onToggle} testId="section" />);
85+
fireEvent.click(screen.getByTestId('section'));
86+
expect(onToggle).toHaveBeenCalledTimes(1);
87+
});
88+
89+
it('should apply custom className', () => {
90+
render(<SectionHeader title="Data" collapsible className="custom-class" testId="section" />);
91+
const element = screen.getByTestId('section');
92+
expect(element.className).toContain('custom-class');
93+
});
94+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { cn } from "../lib/utils"
10+
11+
export interface ConfigRowProps {
12+
/** Left-side label text */
13+
label: string
14+
/** Right-side display value (used when children are not provided) */
15+
value?: string
16+
/** Makes row clickable with hover effect */
17+
onClick?: () => void
18+
/** Custom content replacing value */
19+
children?: React.ReactNode
20+
/** Additional CSS class name */
21+
className?: string
22+
}
23+
24+
/**
25+
* A single labeled row in a configuration panel.
26+
*
27+
* Renders as a `<button>` when `onClick` is provided, otherwise as a `<div>`.
28+
* Shows label on the left and either custom children or a text value on the right.
29+
*/
30+
function ConfigRow({ label, value, onClick, children, className }: ConfigRowProps) {
31+
const Wrapper = onClick ? 'button' : 'div'
32+
return (
33+
<Wrapper
34+
className={cn(
35+
'flex items-center justify-between py-1.5 min-h-[32px] w-full text-left',
36+
onClick && 'cursor-pointer hover:bg-accent/50 rounded-sm -mx-1 px-1',
37+
className,
38+
)}
39+
onClick={onClick}
40+
type={onClick ? 'button' : undefined}
41+
>
42+
<span className="text-xs text-muted-foreground shrink-0">{label}</span>
43+
{children || (
44+
<span className="text-xs text-foreground truncate ml-4 text-right">{value}</span>
45+
)}
46+
</Wrapper>
47+
)
48+
}
49+
50+
export { ConfigRow }

packages/components/src/custom/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './button-group';
22
export * from './combobox';
3+
export * from './config-row';
34
export * from './date-picker';
45
export * from './empty';
56
export * from './field';
@@ -9,6 +10,7 @@ export * from './item';
910
export * from './kbd';
1011
export * from './native-select';
1112
export * from './navigation-overlay';
13+
export * from './section-header';
1214
export * from './spinner';
1315
export * from './sort-builder';
1416
export * from './action-param-dialog';
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import { ChevronDown, ChevronRight } from "lucide-react"
10+
11+
import { cn } from "../lib/utils"
12+
13+
export interface SectionHeaderProps {
14+
/** Section heading text */
15+
title: string
16+
/** Enable collapse/expand toggle */
17+
collapsible?: boolean
18+
/** Current collapsed state */
19+
collapsed?: boolean
20+
/** Callback when toggling collapse/expand */
21+
onToggle?: () => void
22+
/** Data-testid attribute */
23+
testId?: string
24+
/** Additional CSS class name */
25+
className?: string
26+
}
27+
28+
/**
29+
* Section heading with optional collapse/expand support.
30+
*
31+
* Renders as a `<button>` when collapsible, with a chevron icon
32+
* indicating the expand/collapse state. Uses `aria-expanded` for accessibility.
33+
*/
34+
function SectionHeader({ title, collapsible, collapsed, onToggle, testId, className }: SectionHeaderProps) {
35+
if (collapsible) {
36+
return (
37+
<button
38+
data-testid={testId}
39+
className={cn("flex items-center justify-between pt-4 pb-1.5 first:pt-0 w-full text-left", className)}
40+
onClick={onToggle}
41+
type="button"
42+
aria-expanded={!collapsed}
43+
>
44+
<h3 className="text-xs font-semibold text-foreground uppercase tracking-wider">{title}</h3>
45+
{collapsed ? (
46+
<ChevronRight className="h-3 w-3 text-muted-foreground" />
47+
) : (
48+
<ChevronDown className="h-3 w-3 text-muted-foreground" />
49+
)}
50+
</button>
51+
)
52+
}
53+
return (
54+
<div className={cn("pt-4 pb-1.5 first:pt-0", className)} data-testid={testId}>
55+
<h3 className="text-xs font-semibold text-foreground uppercase tracking-wider">{title}</h3>
56+
</div>
57+
)
58+
}
59+
60+
export { SectionHeader }

packages/plugin-designer/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
"react-dom": "^18.0.0 || ^19.0.0"
3333
},
3434
"dependencies": {
35+
"@dnd-kit/core": "^6.3.1",
36+
"@dnd-kit/sortable": "^10.0.0",
37+
"@dnd-kit/utilities": "^3.2.2",
3538
"clsx": "^2.1.1",
3639
"lucide-react": "^0.574.0",
3740
"tailwind-merge": "^3.4.1"

0 commit comments

Comments
 (0)