diff --git a/.changeset/dashboard-stageorder-doc-names-only-funnel.md b/.changeset/dashboard-stageorder-doc-names-only-funnel.md new file mode 100644 index 00000000000..2bffef75413 --- /dev/null +++ b/.changeset/dashboard-stageorder-doc-names-only-funnel.md @@ -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. diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index 4173838ab8f..7b0b1dcfd66 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -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`. | --- @@ -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`. | --- diff --git a/packages/spec/src/ui/dashboard.test.ts b/packages/spec/src/ui/dashboard.test.ts index c65805ea319..71619cd54a2 100644 --- a/packages/spec/src/ui/dashboard.test.ts +++ b/packages/spec/src/ui/dashboard.test.ts @@ -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'; /** @@ -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']); + }); +}); diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 4cd9d43e2e4..d7ba6af241f 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -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) ────────────────────────────