Found while fixing objectui#8944 (PR #9016). Filed rather than folded in: that card is about how the two filter SOURCES compose, this is about what the composed value still carries. Orthogonal, and this one is not chart-specific.
The defect
Every widget that drills through DrillNavigationContext.openRecordList hands it the widget's own filter composed with the click context — and that value is the raw authored filter, placeholders included. Both in-repo consumers of the handler (useOpenRecordList in app-shell, and OpenInListButton in plugin-dashboard) pass it straight to serializeDrillFilterParams, which writes it into filter[...] URL params verbatim. Nothing resolves {current_quarter_start} or {current_user_id} on the way.
Why it survived: the drawer path does NOT have the bug
The in-place drawer renders object-data-table, whose own fetch calls resolveFilterPlaceholders(schema.filter, filterScope). So a macro authored on the widget is resolved on the drawer path and is not on the navigate path.
⇒ One drill, two different scopes, decided only by drillDown.target. And the same widget's own chart bars are scoped by the resolved value — ObjectChart resolves its query legs through resolveFilterPlaceholders(schema.filter, filterScope) while composing the drill from schema.filter raw — so the drilled list disagrees with the chart the user clicked.
The failure is silent and lands on a literal
A widget filter of { close_date: { $gte: '{current_quarter_start}' } } serializes to filter[close_date][gte]={current_quarter_start}. The bare data surface parses that back as an ordinary string comparand, so the list matches nothing (or the driver refuses the comparand). Nothing errors at the seam that produced it.
Note this direction is NARROW, unlike objectui#8944's widening — but it is the same class: the widget's declared scope silently not being the scope the drilled list runs under.
Not chart-specific
openRecordList is the shared drill escape hatch: the table / pivot / metric widgets reach it through DrillDownDrawer and OpenInListButton too. Any of them carrying a placeholder in filter has the same split.
The decision this needs
Where resolution belongs is the actual question, and it should be answered once rather than per widget:
- A — at each widget's drill seam: compose from the already-resolved filter the widget queries with. Local, but it is N seams and the next hand-rolled drill panel will forget it again.
- B — inside the
openRecordList contract: resolve once where the value crosses into the host, covering every caller including future ones. Needs the filter-token scope to be reachable there.
B looks right, but the scope plumbing is a real design question, so this is not a mechanical fix.
Acceptance
- A widget whose
filter carries {current_user_id} or a relative-date macro, drilled with target: 'navigate', produces a URL carrying the RESOLVED value.
- The drawer path keeps resolving exactly once — no double resolution.
- Pinned for at least one non-chart caller as well, since the contract is shared and a chart-only pin would let the next widget regress silently.
Found while fixing objectui#8944 (PR #9016). Filed rather than folded in: that card is about how the two filter SOURCES compose, this is about what the composed value still carries. Orthogonal, and this one is not chart-specific.
The defect
Every widget that drills through
DrillNavigationContext.openRecordListhands it the widget's own filter composed with the click context — and that value is the raw authored filter, placeholders included. Both in-repo consumers of the handler (useOpenRecordListin app-shell, andOpenInListButtonin plugin-dashboard) pass it straight toserializeDrillFilterParams, which writes it intofilter[...]URL params verbatim. Nothing resolves{current_quarter_start}or{current_user_id}on the way.Why it survived: the drawer path does NOT have the bug
The in-place drawer renders
object-data-table, whose own fetch callsresolveFilterPlaceholders(schema.filter, filterScope). So a macro authored on the widget is resolved on the drawer path and is not on the navigate path.⇒ One drill, two different scopes, decided only by
drillDown.target. And the same widget's own chart bars are scoped by the resolved value —ObjectChartresolves its query legs throughresolveFilterPlaceholders(schema.filter, filterScope)while composing the drill fromschema.filterraw — so the drilled list disagrees with the chart the user clicked.The failure is silent and lands on a literal
A widget filter of
{ close_date: { $gte: '{current_quarter_start}' } }serializes tofilter[close_date][gte]={current_quarter_start}. The bare data surface parses that back as an ordinary string comparand, so the list matches nothing (or the driver refuses the comparand). Nothing errors at the seam that produced it.Note this direction is NARROW, unlike objectui#8944's widening — but it is the same class: the widget's declared scope silently not being the scope the drilled list runs under.
Not chart-specific
openRecordListis the shared drill escape hatch: the table / pivot / metric widgets reach it throughDrillDownDrawerandOpenInListButtontoo. Any of them carrying a placeholder infilterhas the same split.The decision this needs
Where resolution belongs is the actual question, and it should be answered once rather than per widget:
openRecordListcontract: resolve once where the value crosses into the host, covering every caller including future ones. Needs the filter-token scope to be reachable there.B looks right, but the scope plumbing is a real design question, so this is not a mechanical fix.
Acceptance
filtercarries{current_user_id}or a relative-date macro, drilled withtarget: 'navigate', produces a URL carrying the RESOLVED value.