You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dashboard widgets: options.stageOrder is declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-en locale #17344
DashboardWidgetOptionsSchema.stageOrder is an ungated member of the generic widget options object. Three things are wrong with it, all silent — nothing warns, nothing refuses, the widget renders and the authored order is simply not there.
Only the funnel branch of the renderer reads it. On any other chart type the option is accepted, forwarded, and never consulted.
Its own documentation names pyramid as a primary case. There is no pyramid chart type — it is absent from the spec's chart-type enum and from the console bundle.
Even on a funnel, the order is matched on rendered category values, and analytics rows carry unlocalized dimension labels — so an authored stageOrder is silently discarded in every locale that is not en. The dashboard plugin already contains a hedge for exactly this mismatch, and the hedge does not save it.
Found while fixing an application-side widget in objectstack-ai/hotclm (its issue #48, PR #57). Everything below is read out of the published 17.4.0 packages — @objectstack/spec@17.4.0 and @objectstack/console@17.4.0 — plus browser measurements from that app.
1. Honoured by the funnel branch only
@objectstack/console@17.4.0, dist/assets/plugin-charts-*.js. The chart component destructures the prop as categoryOrder: c, and the only place c is read is inside the funnel guard:
h is the resolved chart type. Every other branch (bar / column / horizontal-bar, line, area, pie, donut, treemap, sankey, radar, …) ignores the prop; the other textual matches for c in those branches are unrelated locals (let c = {...n} in the pie branch, c = o.map(…) in sankey).
Meanwhile @objectstack/spec@17.4.0 declares it with no chart-type gate at all, in the same generic object as sortBy / sortOrder / limit:
Authoring a horizontal-bar widget carrying stageOrder passes validate and boots. Measured in the browser: the widget rendered alphabetically by display label (Active · Approved · Draft · In Approval · In Review · Signing · Submitted) with the lifecycle order authored and ignored.
This is ADR-0049's shape — enforce or remove. Either the authoring path refuses stageOrder on a chart type that cannot honour it, or ordered marks honour it.
2. The documentation names a chart type that does not exist
packages/spec/src/ui/dashboard.zod.ts, the JSDoc and the .describe() string that reaches the authoring UI and the generated reference:
Explicit category order for ordered-sequence charts — funnel / pyramid stages above all.
.describe('Explicit category order for funnel/pyramid stages (stored values)')
pyramid appears zero times in @objectstack/spec@17.4.0's emitted types and zero times in the console charts bundle. The widget type enum at 17.4.0 is:
column · metric · kpi · line · bar · horizontal-bar · area · pie · donut · funnel · scatter · treemap · sankey · combo · gauge · solid-gauge · bullet · radar · table · pivot
So the one sentence an author reads before using this option names two chart types, of which one is the only one that works and the other does not exist. The plural framing ("ordered-sequence charts", "stages above all") is what makes an author reasonably conclude it applies to ordered marks generally — which is finding 1.
3. On a funnel, the order is matched on labels, and analytics labels are not localized
dist/assets/plugin-dashboard-*.js builds the prop like this:
Ue=(Array.isArray(h.stageOrder) ? h.stageOrder : void0)?.flatMap(e=>{lett=String(e),n=K?.[r[0]]?.[t];// K = dimensionLabelsreturnn&&n!==t ? [t,n] : [t];// emit BOTH stored value and label});We=Ue?.length ? Ue : J;// J = order derived from picklist meta
The plugin emits both the authored stored value and its resolved display label into categoryOrder, because the chart sorts on String(row[xAxisKey]) and it cannot know which spelling the row will carry. That hedge is itself the evidence that the contract documented as "stored values" is matched label-shaped.
The hedge fails as soon as the two label sources disagree. Measured in the hotclm app, same widget, same build, two consoles:
console
authored stageOrder
rendered order
en
lifecycle
lifecycle — Draft · Submitted · In Review · In Approval · Approved · Signing · Active
zh-CN
lifecycle
alphabetical by the English label — Active · Approved · Draft · In Approval · In Review · Signing · Submitted
The mechanism: dimensionLabels resolves through the app's i18n bundle, so on zh-CN the order array carries [stored_value, 中文标签] pairs, while the rows arriving from POST /api/v1/analytics/dataset/query carry the English labels (the unlocalized-analytics-label behaviour of #5076, closed not_planned). Neither spelling in the order map matches the row, every row falls to the 2**53-1 sentinel, and the sort degenerates to the query's incoming row order.
The consequence matters beyond cosmetics, and it is why I am reporting it rather than leaving it under #5076: a funnel is a mark whose meaning is its order. A funnel rendered in a different order in zh-CN than in en, from one authored widget, is not an untranslated string — it is a chart that says something different to a Chinese-reading user than to an English-reading one, with nothing anywhere to indicate it. #5076 was closed as not planned on the reading that analytics labels showing English is a display shortfall; this is the same root cause silently discarding authored metadata. Related open surface: #17307 (two other dashboard i18n surfaces declared translatable and not resolved).
What the app did in the meantime
Nothing that patches the platform, per its own policy. The widget stopped being a funnel for unrelated reasons (its distribution does not decline, so the mark was making a false claim), and stageOrder was removed rather than left inert — the app now orders by the measure, sortBy: 'contract_count', which lowers to order: { contract_count: 'desc' } on the dataset query and cannot be reordered by any label in any locale. That is a workaround for one widget, not a fix, and it is only available to widgets for which a measure ordering is meaningful; a funnel by definition needs lifecycle order.
Suggested remedies, in the order I would take them
Fix the doc string first — it is the cheapest and it is currently pointing authors at a nonexistent chart type. Say funnel only, and say plainly that no other chart type reads it.
Match on stored values, not labels. The row payload already carries enough to identify the category (the funnel branch reads a categoryId off the payload for click handling — se(t?.payload)), so the order map has a locale-independent key available to it. Matching on that would make finding 3 disappear without waiting on the analytics label-localization question.
Then decide finding 1 on its merits — either honour categoryOrder on the ordered marks where an author would reasonably expect it (bar / column / horizontal-bar / line / area), or keep it funnel-only and let step 2 enforce that.
Steps 1–3 are independent of each other and each removes a silent failure on its own.
Application: objectstack-ai/hotclm @ 2807b3b, pnpm demo, Chromium, en and zh-CN consoles, four passes, screenshotted
Widget: legal_dashboard → stage_funnel, dataset contract_metrics, dimension clm_contract.status (a picklist with lifecycle option order declared on the object)
Summary
DashboardWidgetOptionsSchema.stageOrderis an ungated member of the generic widgetoptionsobject. Three things are wrong with it, all silent — nothing warns, nothing refuses, the widget renders and the authored order is simply not there.funnelbranch of the renderer reads it. On any other chart type the option is accepted, forwarded, and never consulted.pyramidas a primary case. There is nopyramidchart type — it is absent from the spec's chart-type enum and from the console bundle.funnel, the order is matched on rendered category values, and analytics rows carry unlocalized dimension labels — so an authoredstageOrderis silently discarded in every locale that is noten. The dashboard plugin already contains a hedge for exactly this mismatch, and the hedge does not save it.Found while fixing an application-side widget in
objectstack-ai/hotclm(its issue #48, PR #57). Everything below is read out of the published 17.4.0 packages —@objectstack/spec@17.4.0and@objectstack/console@17.4.0— plus browser measurements from that app.1. Honoured by the funnel branch only
@objectstack/console@17.4.0,dist/assets/plugin-charts-*.js. The chart component destructures the prop ascategoryOrder: c, and the only placecis read is inside the funnel guard:his the resolved chart type. Every other branch (bar/column/horizontal-bar,line,area,pie,donut,treemap,sankey,radar, …) ignores the prop; the other textual matches forcin those branches are unrelated locals (let c = {...n}in the pie branch,c = o.map(…)in sankey).Meanwhile
@objectstack/spec@17.4.0declares it with no chart-type gate at all, in the same generic object assortBy/sortOrder/limit:Authoring a
horizontal-barwidget carryingstageOrderpassesvalidateand boots. Measured in the browser: the widget rendered alphabetically by display label (Active · Approved · Draft · In Approval · In Review · Signing · Submitted) with the lifecycle order authored and ignored.This is ADR-0049's shape — enforce or remove. Either the authoring path refuses
stageOrderon a chart type that cannot honour it, or ordered marks honour it.2. The documentation names a chart type that does not exist
packages/spec/src/ui/dashboard.zod.ts, the JSDoc and the.describe()string that reaches the authoring UI and the generated reference:pyramidappears zero times in@objectstack/spec@17.4.0's emitted types and zero times in the console charts bundle. The widgettypeenum at 17.4.0 is:column · metric · kpi · line · bar · horizontal-bar · area · pie · donut · funnel · scatter · treemap · sankey · combo · gauge · solid-gauge · bullet · radar · table · pivotSo the one sentence an author reads before using this option names two chart types, of which one is the only one that works and the other does not exist. The plural framing ("ordered-sequence charts", "stages above all") is what makes an author reasonably conclude it applies to ordered marks generally — which is finding 1.
3. On a funnel, the order is matched on labels, and analytics labels are not localized
dist/assets/plugin-dashboard-*.jsbuilds the prop like this:The plugin emits both the authored stored value and its resolved display label into
categoryOrder, because the chart sorts onString(row[xAxisKey])and it cannot know which spelling the row will carry. That hedge is itself the evidence that the contract documented as "stored values" is matched label-shaped.The hedge fails as soon as the two label sources disagree. Measured in the hotclm app, same widget, same build, two consoles:
stageOrderenDraft · Submitted · In Review · In Approval · Approved · Signing · Activezh-CNActive · Approved · Draft · In Approval · In Review · Signing · SubmittedThe mechanism:
dimensionLabelsresolves through the app's i18n bundle, so onzh-CNthe order array carries[stored_value, 中文标签]pairs, while the rows arriving fromPOST /api/v1/analytics/dataset/querycarry the English labels (the unlocalized-analytics-label behaviour of #5076, closednot_planned). Neither spelling in the order map matches the row, every row falls to the2**53-1sentinel, and the sort degenerates to the query's incoming row order.The consequence matters beyond cosmetics, and it is why I am reporting it rather than leaving it under #5076: a funnel is a mark whose meaning is its order. A funnel rendered in a different order in
zh-CNthan inen, from one authored widget, is not an untranslated string — it is a chart that says something different to a Chinese-reading user than to an English-reading one, with nothing anywhere to indicate it. #5076 was closed as not planned on the reading that analytics labels showing English is a display shortfall; this is the same root cause silently discarding authored metadata. Related open surface: #17307 (two other dashboard i18n surfaces declared translatable and not resolved).What the app did in the meantime
Nothing that patches the platform, per its own policy. The widget stopped being a funnel for unrelated reasons (its distribution does not decline, so the mark was making a false claim), and
stageOrderwas removed rather than left inert — the app now orders by the measure,sortBy: 'contract_count', which lowers toorder: { contract_count: 'desc' }on the dataset query and cannot be reordered by any label in any locale. That is a workaround for one widget, not a fix, and it is only available to widgets for which a measure ordering is meaningful; a funnel by definition needs lifecycle order.Suggested remedies, in the order I would take them
funnelonly, and say plainly that no other chart type reads it.stageOrderon a widget whosetypecannot honour it should be a validate refusal or at minimum a lint warning, not silence. Same treatmentoptions.sortBygot in A dashboard widget's OWNfilterkeys andoptions.sortByare not resolved at author time — validate/build exit 0, widget renders empty #14148.categoryIdoff the payload for click handling —se(t?.payload)), so the order map has a locale-independent key available to it. Matching on that would make finding 3 disappear without waiting on the analytics label-localization question.categoryOrderon the ordered marks where an author would reasonably expect it (bar/column/horizontal-bar/line/area), or keep it funnel-only and let step 2 enforce that.Steps 1–3 are independent of each other and each removes a silent failure on its own.
Prior art / related
dateGranularity,sortBy/sortOrder, and funnel stage order", closed by feat(analytics): honour widget dateGranularity, sortBy/sortOrder, and limit in dataset queries (#3588) #3652.sortBy/sortOrder/dateGranularityare genuinely fixed (verified:sortByreaches the query asorder: { … }). Its third item — funnel stage order — is whatstageOrderanswers, and this issue is the part of that answer that did not land: the option arrived generically declared, funnel-only implemented, and label-matched.filterkeys andoptions.sortByare not resolved at author time — validate/build exit 0, widget renders empty #14148 — a widget's ownfilterkeys andoptions.sortBynot resolved at author time. Same enforcement gap, one option over.not_planned) — analytics surfaces never apply the i18n bundle. Root cause of finding 3; this issue is the authored-metadata consequence of it.GlobalFilterSchema.objectis never read, andChartAxisSchema.titlebypasses a locale resolver in its own bundle — two dashboard i18n surfaces declared translatable and not resolved #17307 — two other dashboard i18n surfaces declared translatable and not resolved.Environment
@objectstack/spec@17.4.0,@objectstack/console@17.4.0(published tarballs, read directly)objectstack-ai/hotclm@2807b3b,pnpm demo, Chromium,enandzh-CNconsoles, four passes, screenshottedlegal_dashboard→stage_funnel, datasetcontract_metrics, dimensionclm_contract.status(a picklist with lifecycle option order declared on the object)