Skip to content

Commit 6fbe91f

Browse files
os-zhuangclaude
andauthored
fix(spec): make dashboard widget layout optional (auto-flowed when omitted) (#2247)
`DashboardWidgetSchema.layout` was required, but the entire runtime treats it as optional: objectui's DashboardGridLayout auto-flows any widget without a layout, and the Studio dashboard designer adds widgets WITHOUT a layout by design. The mismatch meant every dashboard authored in the Studio designer failed spec validation the moment a widget was added — the draft PUT /meta/dashboard/... returned 422 ("widgets: Invalid type: expected object, received undefined"), so the draft never saved and Publish stayed disabled, even though the widget rendered. Found by dogfooding the dashboard designer in the browser; verified the fix end-to-end (422 -> 200, Publish enabled, 10-widget dashboard published). layout is now optional; absence means "auto-place". Backward-compatible — existing dashboards that specify layout are unaffected. Adds two regression tests + a changeset. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 23c0f9a commit 6fbe91f

3 files changed

Lines changed: 53 additions & 1 deletion

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): make dashboard widget `layout` optional (auto-flowed when omitted)
6+
7+
`DashboardWidgetSchema.layout` was required, but the entire runtime treats it as
8+
optional: the renderer (`DashboardGridLayout`) auto-flows any widget without a
9+
layout (`x: (i % 4) * 3, y: ⌊i/4⌋ * 4, w: 3, h: 4`), and the Studio dashboard
10+
designer adds widgets **without** a layout by design.
11+
12+
The mismatch meant every dashboard authored in the Studio designer failed spec
13+
validation the moment a widget was added — the draft `PUT /meta/dashboard/...`
14+
returned **422** ("widgets: Invalid type: expected object, received undefined"),
15+
so the draft never saved and **Publish stayed disabled**, even though the widget
16+
rendered correctly in the canvas. Found by dogfooding the dashboard designer in
17+
the browser.
18+
19+
`layout` is now optional; absence means "auto-place". Authors may still pin an
20+
explicit grid position. Backward-compatible — existing dashboards that specify
21+
`layout` are unaffected.

packages/spec/src/ui/dashboard.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ describe('DashboardWidgetSchema (dataset-bound)', () => {
2929
expect(w.values).toEqual(['revenue']);
3030
});
3131

32+
// Regression (Studio dashboard designer): the designer adds widgets WITHOUT a
33+
// `layout`; the renderer auto-flows them. `layout` must be OPTIONAL — when it
34+
// was required, every designer-authored dashboard failed validation (422 on
35+
// draft save) and Publish stayed disabled even though the widget rendered.
36+
it('accepts a widget with NO layout (auto-flowed; Studio designer omits it)', () => {
37+
const w = DashboardWidgetSchema.parse({
38+
id: 'widget_1', type: 'metric', dataset: 'showcase_task_metrics', values: ['task_count'],
39+
});
40+
expect(w.layout).toBeUndefined();
41+
expect(w.values).toEqual(['task_count']);
42+
});
43+
3244
it('accepts a chart widget with dimensions', () => {
3345
const w = DashboardWidgetSchema.parse({
3446
id: 'by_stage', type: 'bar', dataset: 'sales', dimensions: ['stage'], values: ['revenue'],
@@ -82,6 +94,17 @@ describe('DashboardSchema', () => {
8294
expect(d.widgets).toHaveLength(2);
8395
});
8496

97+
it('parses a designer-authored dashboard whose widgets omit layout', () => {
98+
const d = DashboardSchema.parse({
99+
name: 'delivery_exec_overview', label: 'Delivery Executive Overview',
100+
widgets: [
101+
{ id: 'widget_1', type: 'metric', dataset: 'showcase_task_metrics', values: ['task_count'] },
102+
{ id: 'widget_2', type: 'donut', dataset: 'showcase_task_metrics', dimensions: ['status'], values: ['task_count'] },
103+
],
104+
});
105+
expect(d.widgets.every((w) => w.layout === undefined)).toBe(true);
106+
});
107+
85108
it('Dashboard.create factory parses + returns a typed dashboard', () => {
86109
const d = Dashboard.create({
87110
name: 'dash_x', label: 'D',

packages/spec/src/ui/dashboard.zod.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,13 +165,21 @@ export const DashboardWidgetSchema = lazySchema(() => z.object({
165165
* y: row
166166
* w: width (1-12)
167167
* h: height
168+
*
169+
* OPTIONAL — when omitted, the renderer auto-flows the widget into the grid
170+
* (DashboardGridLayout falls back to `x: (i % 4) * 3, y: Math.floor(i/4) * 4,
171+
* w: 3, h: 4`). The Studio dashboard designer adds widgets WITHOUT a layout
172+
* and relies on this auto-flow; requiring `layout` here made every
173+
* designer-authored dashboard fail validation (422 on draft save, Publish
174+
* disabled) even though it rendered correctly. Authors may still pin an
175+
* explicit grid position; absence means "auto-place".
168176
*/
169177
layout: z.object({
170178
x: z.number(),
171179
y: z.number(),
172180
w: z.number(),
173181
h: z.number(),
174-
}).describe('Grid layout position'),
182+
}).optional().describe('Grid layout position (auto-flowed when omitted)'),
175183

176184
/** Widget specific options (colors, legend, etc.) */
177185
options: z.unknown().optional().describe('Widget specific configuration'),

0 commit comments

Comments
 (0)