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
21 changes: 21 additions & 0 deletions .changeset/dashboard-widget-layout-optional.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@objectstack/spec": patch
---

fix(spec): make dashboard widget `layout` optional (auto-flowed when omitted)

`DashboardWidgetSchema.layout` was required, but the entire runtime treats it as
optional: the renderer (`DashboardGridLayout`) auto-flows any widget without a
layout (`x: (i % 4) * 3, y: ⌊i/4⌋ * 4, w: 3, h: 4`), 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 correctly in the canvas. Found by dogfooding the dashboard designer in
the browser.

`layout` is now optional; absence means "auto-place". Authors may still pin an
explicit grid position. Backward-compatible — existing dashboards that specify
`layout` are unaffected.
23 changes: 23 additions & 0 deletions packages/spec/src/ui/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ describe('DashboardWidgetSchema (dataset-bound)', () => {
expect(w.values).toEqual(['revenue']);
});

// Regression (Studio dashboard designer): the designer adds widgets WITHOUT a
// `layout`; the renderer auto-flows them. `layout` must be OPTIONAL — when it
// was required, every designer-authored dashboard failed validation (422 on
// draft save) and Publish stayed disabled even though the widget rendered.
it('accepts a widget with NO layout (auto-flowed; Studio designer omits it)', () => {
const w = DashboardWidgetSchema.parse({
id: 'widget_1', type: 'metric', dataset: 'showcase_task_metrics', values: ['task_count'],
});
expect(w.layout).toBeUndefined();
expect(w.values).toEqual(['task_count']);
});

it('accepts a chart widget with dimensions', () => {
const w = DashboardWidgetSchema.parse({
id: 'by_stage', type: 'bar', dataset: 'sales', dimensions: ['stage'], values: ['revenue'],
Expand Down Expand Up @@ -82,6 +94,17 @@ describe('DashboardSchema', () => {
expect(d.widgets).toHaveLength(2);
});

it('parses a designer-authored dashboard whose widgets omit layout', () => {
const d = DashboardSchema.parse({
name: 'delivery_exec_overview', label: 'Delivery Executive Overview',
widgets: [
{ id: 'widget_1', type: 'metric', dataset: 'showcase_task_metrics', values: ['task_count'] },
{ id: 'widget_2', type: 'donut', dataset: 'showcase_task_metrics', dimensions: ['status'], values: ['task_count'] },
],
});
expect(d.widgets.every((w) => w.layout === undefined)).toBe(true);
});

it('Dashboard.create factory parses + returns a typed dashboard', () => {
const d = Dashboard.create({
name: 'dash_x', label: 'D',
Expand Down
10 changes: 9 additions & 1 deletion packages/spec/src/ui/dashboard.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,21 @@ export const DashboardWidgetSchema = lazySchema(() => z.object({
* y: row
* w: width (1-12)
* h: height
*
* OPTIONAL — when omitted, the renderer auto-flows the widget into the grid
* (DashboardGridLayout falls back to `x: (i % 4) * 3, y: Math.floor(i/4) * 4,
* w: 3, h: 4`). The Studio dashboard designer adds widgets WITHOUT a layout
* and relies on this auto-flow; requiring `layout` here made every
* designer-authored dashboard fail validation (422 on draft save, Publish
* disabled) even though it rendered correctly. Authors may still pin an
* explicit grid position; absence means "auto-place".
*/
layout: z.object({
x: z.number(),
y: z.number(),
w: z.number(),
h: z.number(),
}).describe('Grid layout position'),
}).optional().describe('Grid layout position (auto-flowed when omitted)'),

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