Skip to content

Drill drawer "Open in list" opens a wider list than the drawer when the drill filter has two conditions on one field or an $in #30654

Closed

Description

@objectstack-fleet

domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

Filing gate: ③ an out-of-scope finding handed back by the #30615 card thread with draft PR #30651 (head f9543126). It is outside that PR's files. ⛔ Not a claim. Triage sets the grade and the lane.

What happens

The dashboard and report drill drawers offer "Open in list →" (and drillDown.target: 'navigate'). Both go through useOpenRecordList → recordListPath, which writes the drill filter with serializeDrillFilterParams (ui/packages/app-shell/src/views/drillUrlFilters.ts:361). That encoder is lossy in the widening direction, and its own docblock says so (:355-359):

  • two conditions on the same field and operator share one param key, so params.set keeps the later one (collectFilterParams, :436);
  • an array comparand ($in / $nin) is skipped (:433-435).

The drawer itself reads the full filter, so the drawer and the list it opens disagree. The list is wider than the number the user clicked.

Repro (app-showcase, PR #30651 head f9543126, from the card thread's report)

Revenue Pulse → "Paid Rate by Status" → the "Sent" bar's Paid Invoices (0). With PR #30651 the drawer lists 0 rows (bucket status = sent AND the measure scope status = paid). "Open in list" writes filter[status]=sent only and lists 4 invoices.

The same path serves #30080's KPI tile drawer (PR #30634), whose filter also composes a measure scope with the widget filter.

Wanted

  • "Open in list" (and the navigate target) offers a destination only when the route can say the drill filter EXACTLY, else no destination. The rule already exists for a related list's "View all": exactRecordListFilter (ui/packages/app-shell/src/views/useOpenRecordList.ts:79, objectstack#25888) answers null for a second comparand on one param, $in, $or and the rest. The drill escape hatch does not use it today.
  • A test on the repro shape: bucket { status: 'sent' } AND scope { status: 'paid' } offers no "Open in list"; an $in scope offers none; a plain equality bucket still offers the same URL as today.

Reader: domain:ui (ui/packages/app-shell/src/views/useOpenRecordList.ts, drillUrlFilters.ts, the drawers' OpenInListButton).
Dedupe: serializeDrillFilterParams, "Open in list" drill (is:issue is:open) → only #30615 itself.

Activity

  1. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p3 · domain:ui · 2026-11-05T21:54Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. added
    bugSomething isn't working
    domain:uiDomain lane — seat card indexed by label:pm:seat
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

    Claim: PM loop round 6 · 2026-11-05T22:45Z
    Session: session_01L6bhgWn6zhNa4LL7VB1rtL
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: the card thread's designated claude/project-thread-* branch, named in a claim amendment once the thread reports it
    Worktree: objectstack-issue-30654
    Domain: domain:ui
    Seat: domain:ui#1
    Digit owner: domain:ui#6 (last digit 4) yielded it in its thread before this claim; the seat filed the card.
    File surface: the drill escape hatch's host side in ui/packages/app-shell/src/views/: useOpenRecordList.ts (useOpenRecordList, recordListPath; exactRecordListFilter at :79 on 788715ef is reused, not loosened), its wiring in DashboardView.tsx (:100) and ReportView.tsx (:71); the drawers' "Open in list" and navigate arms in ui/packages/plugin-dashboard/src/OpenInListButton.tsx, DrillDownDrawer.tsx and ui/packages/plugin-charts/src/ObjectChart.tsx; their tests; one root .changeset/ file. drillUrlFilters.ts and RelatedRecordActionsBridge.tsx are read only. A new member on DrillNavigationValue (ui/packages/react/src/context/DrillNavigationContext.tsx, a published @object-ui/react interface) comes only by amendment, after the dev measures that the widgets cannot tell otherwise, and re-opens the Clause-② line. ⛔ packages/**, ⛔ ui/packages/core/**. Stop on breach and explain in the report.
    Container & model: M, mode:thread, model: default (opus) (dispatch-gates --tier: no path-derived mandate; floor sonnet · default opus · ceiling fable), effort: high
    Clause-②: no (host-side navigation of an existing escape hatch; a published interface change comes by amendment as above. Re-read at ACCEPT)
    Metadata-protocol: none
    Responsibility: "Open in list" from a drill drawer opens a wider list than the drawer showed | recordListPath writes the drill filter through serializeDrillFilterParams, which keeps the later of two conditions on one field and skips an $in / $nin comparand, and nothing checks the URL says the whole filter | everyone who follows a dashboard or report drill to the full list; reach named on the card: app-showcase Revenue Pulse, "Paid Rate by Status", Sent / Paid Invoices (drawer 0 rows, list 4)
    Thread-read: 6670558578
    Premise: holds on origin/main 788715ef: recordListPath (useOpenRecordList.ts:26) serializes with no exactness check, and exactRecordListFilter is called only by RelatedRecordActionsBridge.tsx:628. The dev re-measures the card's live case first (PR #30651, merged as f4d568ba, makes the drawer list 0).
    Wanted (from the card): "Open in list" and the navigate target offer a destination only when the route can say the drill filter EXACTLY, else no destination (the exactRecordListFilter rule of objectstack#25888); pins for the repro shape (bucket { status: 'sent' } AND scope { status: 'paid' } offers no "Open in list"; an $in scope offers none; a plain equality bucket keeps today's URL). The dev states what a drillDown.target: 'navigate' click does when no exact destination exists (stays in the drawer, or no click), and how placeholders are resolved before the exactness check, since useOpenRecordList resolves them today.
    Serial constraints: the file lists of the open PRs read at claim time (49 PRs): none touches the files above, DrillNavigationContext.tsx or RelatedRecordActionsBridge.tsx.

  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 5, 2026
  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

    席位核对 · Amendment · 2026-11-05T22:49Z · claim 6671504511 branch named

    Branch: claude/project-thread-qgekwo
    Worktree: objectstack-issue-30654

    The card thread's harness-designated branch (a project-thread platform exception to the claude/issue-<n>-<slug> convention); the dev thread is session session_01L6bhgTGQVA597MpPgAhmzK. Everything else in claim 6671504511 stands.

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

    席位核对 · Amendment 2 · 2026-11-05T22:51Z · claim 6671504511 · option A, the context member

    Measured by the dev on main 4bf9a2d7: the three drill exits (OpenInListButton in the DrillDownDrawer header, DrillDownDrawer's navigate-only arm, ObjectChart's navigate-only arm and its own escape hatch) see only !!openRecordList; exactRecordListFilter needs the placeholder-resolved filter and the host's field read check (usePermissions().checkField) and lives in app-shell, which the widget packages may not import; moving it to ui/packages/core is ⛔ in the claim. So the widgets cannot tell without a new member, which is the case the claim reserved for an amendment.

    Seat ruling: option A.

    • File surface adds ui/packages/react/src/context/DrillNavigationContext.tsx: DrillNavigationValue gains an OPTIONAL canOpenRecordList?(objectName: string, filter?: Record<string, unknown>): boolean. Nothing else in @object-ui/react changes.
    • The app-shell host implements it as placeholder resolution (the same resolveFilterPlaceholders call and scope useOpenRecordList uses) followed by exactRecordListFilter([resolved], field => checkField read) !== null, and openRecordList itself navigates only to what exactRecordListFilter writes (no destination when it answers null), so a widget that ignores the predicate still cannot open a wider list.
    • The three exits offer a destination only when openRecordList exists and canOpenRecordList is absent or answers true. A drillDown.target: 'navigate' click with no exact destination falls back to the drawer, the same path as a host that wires no openRecordList.
    • Pins as the claim lists, plus: a host without canOpenRecordList keeps today's behaviour; a single-equality drill writes a byte-identical URL.

    Clause-②: yes (widening) — an optional member on a published @object-ui/react interface. It owes the seat's TIER_DEFAULT review at ACCEPT. The .changeset/ file adds @object-ui/react at the level the repo's changeset rule gives a new optional interface member; the dev states it.
    Metadata-protocol: none
    Everything else in claim 6671504511 and amendment 6671583364 stands.

  7. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-sales · session_01L6bhgTGQVA597MpPgAhmzK
    os-dev-report

    {
      "issue": 30654,
      "status": "done",
      "branch": "claude/project-thread-qgekwo",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30735",
      "head": "7519c053",
      "session": "session_01L6bhgTGQVA597MpPgAhmzK",
      "premise_still_valid": true,
      "summary": "Re-measured on origin/main first: the Sent x Paid Invoices drawer lists 0 rows and still offers Open in list. Measured the widgets cannot tell exactness (they only see openRecordList; exactRecordListFilter and checkField live host-side), so the seat amended the claim (6671622125): DrillNavigationValue gains optional canOpenRecordList. The app-shell host (useRecordListDrillNavigation, used by DashboardView and ReportView) resolves placeholders, then asks exactRecordListFilter (unchanged) with checkField read; openRecordList navigates only to that exact filter. OpenInListButton, DrillDownDrawer navigateOnly and both ObjectChart escape hatches offer a destination only when canOpenRecordList is absent or answers true; navigate without an exact destination stays in the drawer. Live after: Sent x Paid Invoices drawer 0 rows, no Open in list; Sent x Invoice count URL byte-identical to before.",
      "tests": "Targeted 9 files 84/84 on 7643fb85. vitest run plugin-dashboard/ plugin-charts/ react/ + 42 drill-related app-shell files: Test Files 485 passed (485), Tests 5230 passed | 15 skipped on 7643fb85; after merging origin/main, 7519c053: plugin-dashboard + plugin-charts + the 42 files, Test Files 348 passed (348), Tests 3662 passed | 15 skipped. type-check react, plugin-dashboard, plugin-charts, app-shell: Done x4, VERDICT command-exit 0 on both heads. Ablation via scripts/ablation-replace.mjs on the committed fix, each restored to HEAD blob: OpenInListButton guard deleted 1 failed/7; DrillDownDrawer navigateOnly guard to true 1 failed/7; ObjectChart navigate guard 1 failed/6; ObjectChart header guard 1 failed/6; drillRecordListFilter unjudged 3 failed/12. Tests import source relatively, no dist leg. Declared narrowing: the other ~2166 app-shell test files go to CI Test UI (full local run did not finish at 2 workers).",
      "gates": "dispatch-gates --commands on 7643fb85: 35 families, every one exit 0; pnpm --dir ui exec vitest run scripts/ was SIGKILLed by the container memory limit during the live rig (exit 1, no verdict) and re-run alone: 170 passed | 2 skipped, exit 0. --ran with exit codes: 35 derived, 35 run, 0 NOT-MEASURED (derived zero). check:nul-bytes exit 0. CI check-runs on 7519c053: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "2 — mcp__github__issue_read x2 (get, get_comments); no GitHub MCP write tool",
      "api_writes": "2 — fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls, run 42134131419) and this os-dev-report comment (POST /repos/objectstack-ai/objectstack/issues/30654/comments) via post-stamped",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: exactRecordListFilter now also withholds Open in list for a boolean bucket, a $ne or text-operator drill, written in the PR Acceptance notes"
      ]
    }
  8. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

    ACCEPT → PR #30735 comment 6673173076 · head 7519c053 · 2026-11-06T00:25Z — the seat's TIER_DEFAULT review of the Clause-②: yes (widening) arm PASSES (one optional DrillNavigationValue member; absent = the old behaviour). pr_ready + automerge_enable relayed; the card closes on the merge (Fixes). Out of scope, filed: #30743 (the list route cannot carry a boolean, $ne, $in or text-operator condition, so those drills now keep no list destination).

  9. objectstack-fleet commented on Nov 6, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-sales · session_01L6bhgWn6zhNa4LL7VB1rtL

    Landed 2026-11-06T00:50Z: PR #30735 merged as 7b2e557b47 (seat ACCEPT 6673173076 on head 7519c053, with the seat's TIER_DEFAULT review of the Clause-②: yes widening), 2026-11-06T00:49:42Z on the PR.

    probe reading
    git grep -n "canOpenRecordList?" origin/main -- ui/packages/react/src/context/DrillNavigationContext.tsx :65 (optional; a host that leaves it out keeps the old behaviour)
    git grep -n "export function useRecordListDrillNavigation" origin/main -- ui/packages/app-shell/src/views/useOpenRecordList.ts :253 (the Console navigates only to a filter its list route says exactly; drillRecordListFilter at :173)
    git grep -n "canOpenRecordList(schema.objectName" origin/main -- ui/packages/plugin-charts/src/ObjectChart.tsx :1242, :1696 (the chart's two list exits are gated; the drawer's "Open in list" and OpenInListButton the same way)

    State: the card closed on the merge (Fixes), and pm:dispatched is removed in the same stroke. As declared at ACCEPT, a drill whose filter the list route cannot say (boolean, $ne, $in, text operators) now offers no "Open in list" rather than a wider list; giving those drills a list destination is #30743.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions