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
14 changes: 14 additions & 0 deletions .changeset/dashboard-stageorder-doc-names-only-funnel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@objectstack/spec": patch
---

docs(spec): `options.stageOrder` no longer documents a chart type that cannot be built, and says plainly that only `funnel` reads it (#17344)

`DashboardWidgetOptionsSchema.stageOrder` is an ungated member of the open widget `options` bag, so its one sentence of prose is the whole author-time surface: nothing warns, nothing refuses, and a widget carrying the key renders with the authored order simply absent. That sentence said *"Explicit category order for ordered-sequence charts — `funnel` / `pyramid` stages above all"*, and it was wrong twice over.

- **`pyramid` is not a widget type.** It was removed from `ChartTypeSchema` as a variant that only ever rendered as `funnel`, and `chart.test.ts` pins that refusal alongside its fallback-only siblings — so the headline example in the option's own documentation could not be authored at all.
- **The plural framing promised more than the renderer delivers.** "ordered-sequence charts" and "stages above all" read as a statement about ordered marks generally. It is not one: `funnel` is the only type whose branch consults the forwarded order, measured against this repo's pinned objectui renderer.

The corrected JSDoc and `.describe()` name `funnel` only, state outright that no other widget type reads the key, and send the other types to `sortBy` / `sortOrder`, which lower into the dataset query itself. The generated reference page (`content/docs/references/ui/dashboard.mdx`) is regenerated from the new `.describe()`.

No schema shape changes: `stageOrder` still parses exactly as before, on every widget type. Whether the key should be *gated* to the type that honours it is ADR-0049 enforce-or-remove on an accepted key — a published-surface narrowing, and deliberately not this change; it stays open on #17344 together with the locale-dependent order/colour drop, which lives in the objectui renderer rather than here.
4 changes: 2 additions & 2 deletions content/docs/references/ui/dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ Dashboard header action
| **sortBy** | `string` | optional | Dimension/measure name to order by |
| **sortOrder** | `Enum<'asc' \| 'desc'>` | optional | Sort direction for sortBy |
| **limit** | `integer` | optional | Max rows (applied after ordering) |
| **stageOrder** | `(string \| number \| boolean)[]` | optional | Explicit category order for funnel/pyramid stages (stored values) |
| **stageOrder** | `(string \| number \| boolean)[]` | optional | Explicit stage order for a funnel widget, as the dimension's stored values. `funnel` is the only widget type that reads it: on any other type the key parses and is never consulted, so order those with sortBy/sortOrder instead. There is no `pyramid` widget type — write `funnel`. |


---
Expand All @@ -263,7 +263,7 @@ Widget configuration — declared query keys + open renderer extras
| **sortBy** | `string` | optional | Dimension/measure name to order by |
| **sortOrder** | `Enum<'asc' \| 'desc'>` | optional | Sort direction for sortBy |
| **limit** | `integer` | optional | Max rows (applied after ordering) |
| **stageOrder** | `(string \| number \| boolean)[]` | optional | Explicit category order for funnel/pyramid stages (stored values) |
| **stageOrder** | `(string \| number \| boolean)[]` | optional | Explicit stage order for a funnel widget, as the dimension's stored values. `funnel` is the only widget type that reads it: on any other type the key parses and is never consulted, so order those with sortBy/sortOrder instead. There is no `pyramid` widget type — write `funnel`. |


---
Expand Down
80 changes: 80 additions & 0 deletions packages/spec/src/ui/dashboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
GlobalFilterOptionsFromSchema,
DATE_RANGE_PRESETS,
DATE_RANGE_DEFAULT_RANGES,
DashboardWidgetOptionsSchema,
} from './dashboard.zod';
import { ChartTypeSchema } from './chart.zod';
import { dashboardForm } from './dashboard.form';

/**
Expand Down Expand Up @@ -796,3 +798,81 @@ describe('#16458 — DashboardHeaderAction fields carry an item-level `title`',
expect('columns' in parsed).toBe(false);
});
});


/**
* `options.stageOrder` documented a chart type that cannot be built.
*
* The shipped prose is the whole surface here: `stageOrder` is an ungated
* member of the open `options` bag, so the one sentence an author reads before
* writing it is the only thing standing between them and a key that parses and
* does nothing. That sentence named `funnel` / `pyramid` "stages above all",
* and `pyramid` was removed from `ChartTypeSchema` as a variant that only ever
* rendered as `funnel` — so its headline example could not be authored at all,
* and its plural framing read as a promise about ordered marks generally.
*
* These pin the corrected prose against BOTH ways it can rot:
* - the vocabulary moving under it (a `pyramid` re-admitted to the taxonomy
* would make the sentence false in the other direction), and
* - the sentence being trimmed back to the plural framing.
*
* The renderer half is deliberately NOT pinned here: which chart types consult
* the forwarded order is objectui's fact, measured against this repo's
* `.objectui-sha` pin and reported on the issue, not something `packages/spec`
* can assert.
*/
describe('DashboardWidgetOptions.stageOrder — the shipped doc string', () => {
const description = () => {
const d = (DashboardWidgetOptionsSchema as unknown as {
shape: { stageOrder: { description?: string } };
}).shape.stageOrder.description;
expect(typeof d).toBe('string');
return d as string;
};

it('names `funnel` and never `pyramid`', () => {
const d = description();
expect(d).toMatch(/funnel/);
expect(d).not.toMatch(/pyramid.*(is|are) (a|the) (chart|widget) type/i);
// The word may only appear as the correction that it does NOT exist.
expect(d).toMatch(/no `?pyramid`? widget type/i);
});

it('states plainly that no other widget type reads the key', () => {
const d = description();
expect(d).toMatch(/only widget type that reads it/);
// and points the other types at the keys that DO order them
expect(d).toMatch(/sortBy/);
expect(d).toMatch(/sortOrder/);
});

it('CONTROL — the taxonomy behind that prose: `funnel` parses, `pyramid` does not', () => {
expect(ChartTypeSchema.safeParse('funnel').success).toBe(true);
expect(ChartTypeSchema.safeParse('pyramid').success).toBe(false);
// dark control: a type that never existed refuses the same way, so the
// `pyramid` refusal above is not an artifact of how the probe is written
expect(ChartTypeSchema.safeParse('ziggurat').success).toBe(false);
});

it('CONTROL — a `funnel` widget carrying `stageOrder` still parses unchanged', () => {
const w = DashboardWidgetSchema.parse({
id: 'stage_funnel', type: 'funnel', dataset: 'contracts',
dimensions: ['status'], values: ['count'],
layout: { x: 0, y: 0, w: 6, h: 4 },
options: { stageOrder: ['draft', 'submitted', 'approved'] },
});
expect(w.options?.stageOrder).toEqual(['draft', 'submitted', 'approved']);
});

it('CONTROL — the key is still UNGATED: a non-funnel widget carrying it parses too', () => {
// This is finding 1 of the card, recorded as a fact rather than fixed:
// gating the key is a published-surface narrowing and is not this PR.
const w = DashboardWidgetSchema.parse({
id: 'stage_bars', type: 'horizontal-bar', dataset: 'contracts',
dimensions: ['status'], values: ['count'],
layout: { x: 0, y: 0, w: 6, h: 4 },
options: { stageOrder: ['draft', 'submitted', 'approved'] },
});
expect(w.options?.stageOrder).toEqual(['draft', 'submitted', 'approved']);
});
});
31 changes: 24 additions & 7 deletions packages/spec/src/ui/dashboard.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,15 +255,32 @@ export const DashboardWidgetOptionsSchema = lazySchema(() => z.object({
limit: z.number().int().positive().optional().describe('Max rows (applied after ordering)'),

/**
* Explicit category order for ordered-sequence charts — `funnel` / `pyramid`
* stages above all. Values are the dimension's STORED values (e.g.
* `['qualification', 'needs_analysis', 'proposal', 'negotiation']`), not
* display labels. Omit to let the renderer fall back to the dimension
* field's own picklist option order, which is the pipeline order an author
* already declared on the object.
* Explicit stage order for a `funnel` widget. Values are the dimension's
* STORED values (e.g. `['qualification', 'needs_analysis', 'proposal',
* 'negotiation']`), not display labels. Omit to let the renderer fall back
* to the dimension field's own picklist option order, which is the pipeline
* order an author already declared on the object.
*
* `funnel` is the ONLY widget `type` that reads this key. On every other
* type — `bar` / `horizontal-bar` / `column`, `line`, `area`, `pie`,
* `donut`, `treemap`, `sankey`, `radar`, `scatter`, `combo`, the tabular and
* single-value families — the key parses, is forwarded to the renderer, and
* no branch consults it: the rendered order stays whatever the analytics
* query returned. Order those with `sortBy` / `sortOrder`, which lower into
* the dataset query itself.
*
* There is no `pyramid` widget type. It was removed from `ChartTypeSchema`
* as a variant that only ever rendered as `funnel` (see the taxonomy NOTE at
* the foot of `chart.zod.ts`; `chart.test.ts` pins the refusal alongside its
* fallback-only siblings). Write `type: 'funnel'`.
*/
stageOrder: z.array(z.union([z.string(), z.number(), z.boolean()])).optional()
.describe('Explicit category order for funnel/pyramid stages (stored values)'),
.describe(
'Explicit stage order for a funnel widget, as the dimension\'s stored values. '
+ '`funnel` is the only widget type that reads it: on any other type the key '
+ 'parses and is never consulted, so order those with sortBy/sortOrder instead. '
+ 'There is no `pyramid` widget type — write `funnel`.',
),
}).passthrough().describe('Widget configuration — declared query keys + open renderer extras'));

// ── `compareTo` convergence prescriptions (#5011) ────────────────────────────
Expand Down
Loading