Skip to content

List toolbar: offer the "In a queue" toggle only when the organization has at least one queue — today every owned object shows it, and in an app with no queues it always returns an empty list #28428

Open

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

Blocked-by: #28508

Body updated 2026-11-01T03:59Z: route A ruled (comment 6553134732); the producer half is #28508. The claim, the os-dev-report and the release above are read and carried into that ruling.

Filed by the record-page epic seat (part of the record and list review under #26676) after the maintainer asked, on the latest main, 「右侧显示的 在队列中是什么,每个业务对象都需要显示吗」 and answered the seat's recommendation with 「立卡」 (2026-10-31).

Reading (origin/main 5da3a8ce31)

  • The list toolbar's In a queue toggle (list.sittingInQueue, zh 「在队列中」, hint "Show only records waiting in a queue for someone to claim them") came with the ownership-queue work (ADR-0142, feat(console): ownership queues in the Console — two-source queue pickers, Claim on a queued record, an In-a-queue list filter, and the authoring guide (ADR-0142) #27264). When it is on, the list adds owner_id is_empty AND queue is_not_empty (QUEUE_SITTING_CONDITIONS, ui/packages/plugin-list/src/ListView.tsx around line 864).
  • It is offered whenever objectHasOwnershipQueue(objectDef) is true (ListView.tsx line 878, read at line 2147, rendered at line 5707). That check asks only whether the served definition carries the platform-injected system field queue.
  • ADR-0142 D3 injects queue on every object that gets owner_id, which means every ownership: 'user' object. 'user' is the default. So the toggle sits on practically every business list, whether or not the deployment has a single queue.
  • In both apps the seat previews, it is a control that can never return a row. HotCRM declares no queue: none of its 18 objects overrides the default, so all 18 lists show the toggle. examples/app-crm declares none either. Neither app has a sys_queue row or an assignment rule that would route a record into a queue.

Target

Offer the toggle only when the current organization resolves at least one queue. Use the same two sources ADR-0142 D6 defines and the console's queue picker already reads: declared queues plus the organization's sys_queue rows (useTwoSourceCandidates / mergeQueueCandidates, used by ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx). With no queue at all, the toggle is not rendered.

  • Keep the injected queue column and everything the server does with it (claim, release, the read rule). This card narrows only when the toolbar offers the filter. No spec or contract change.
  • Read the queue set once per session or organization and reuse it across lists, so list mounts add no request.
  • Keep the record page's Claim affordance (useQueueClaimOffered, ui/packages/plugin-detail/src/queueClaim.ts) as it is. It already asks the server per record.

Premise to verify first

An ordinary end user, not only an admin, can read both sources: the declared-queue registry and sys_queue rows, or an equivalent served list of queue names.

  • If not, stop and report it as a fork in out_of_scope_findings.
  • Do not fall back to showing the toggle everywhere, and do not hide it everywhere.
  • The seat will decide whether the record page payload or the session should carry an "organization has queues" bit.

Do not narrow per object (offer the filter only where a queue or an assignment rule targets this object) on this card. That needs a server reading the console does not get today. File it as a follow-up finding if the dev sees real pull for it.

Why (four axes)

  • Business need: in a deployment without queues, the toggle has no scenario. Both real apps the seat reads are such deployments. Where queues exist, nothing changes.
  • Long-term fit: a control that can only return an empty list is the same class of defect as List header: a single-record action (Reassign…) is live with no row selected, and a dead end where rows cannot be selected #27831, which stopped the list header from offering single-record actions when no row can be selected. The rule is the same: offer an affordance only when it can do something.
  • AI-error resistance: an AI that writes an app gets no queue UI it did not ask for. The gate reads data the app declares, not a new key.
  • Startup focus: this removes an affordance and adds no surface: no new key, no new gate.

Acceptance

  1. On HotCRM and examples/app-crm on main, no list shows In a queue.
  2. Declare one queue, or insert one sys_queue row. The toggle appears on owned objects' lists, and turning it on filters exactly as it does today.
  3. The existing tests in ListView.queueSittingFilter-25769.test.tsx still pass. New tests pin the no-queue case (toggle absent) and the one-queue case (toggle present).
  4. Opening a list adds no request; the network count is the same before and after.

Activity

  1. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgWqjXeTcS3jtug46Dv

    Claim: PM loop round 3 · 2026-10-31T16:20Z
    Session: session_01L6bhgWqjXeTcS3jtug46Dv (seat; implementation by card thread session session_01L6bhgWQvHUMoWtK3q6mQxV, mode:thread)
    Account: os-sales (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-k1hius
    Worktree: objectstack-issue-28428
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/plugin-list/src/ListView.tsx and tests beside it (including ListView.queueSittingFilter-25769.test.tsx), one shared queue-set hook in ui/packages/plugin-list/src/ or ui/packages/app-shell/src/hooks/ if needed (named in the report), .changeset/28428-*.md (stop on breach; explain in the report). ⛔ packages/spec/**, server code, plugin-detail/src/queueClaim.ts, plugin-grid/** and components/src/renderers/complex/data-table.tsx (#28434, seat 5) and #27524 PR ④'s surface
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: yes
    Metadata-protocol: none
    Responsibility: n/a — not a defect card (affordance narrowing)
    Thread-read: none
    Premise: to verify first, as the card says: an ordinary end user can read both queue sources (declared queues and sys_queue rows) or an equivalent served list. If not, the thread stops and reports a fork in out_of_scope_findings (no fallback to "show everywhere" or "hide everywhere")
    Serial constraints cleared: no open non-mailbox PR touches plugin-list/src/ListView.tsx at this read; #28423 (rows 7/17 on the same file) landed as 5da3a8ce; seat 5 was notified on #28286 (6545969256)
    Resume-from: none

    Scope: Fixes #28428. Offer the "In a queue" toggle only when the current organization resolves at least one queue (declared plus sys_queue, ADR-0142 D6's two sources). Read the set once per session or organization, and add no request on list mount. Do not narrow per object. Clause-②: yes (widening) if a new export or key is added, so the seat owes a TIER_DEFAULT review before the queue.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgWqjXeTcS3jtug46Dv
    os-dev-report

    Card thread session_01L6bhgWQvHUMoWtK3q6mQxV, report at 2026-10-31T16:29Z. Premise measured false for one of the two sources; stopped before code, as the claim orders.

    {
      "issue": 28428,
      "status": "needs_decision",
      "branch": "claude/project-thread-k1hius",
      "pr": null,
      "session": "session_01L6bhgWQvHUMoWtK3q6mQxV",
      "premise_still_valid": false,
      "summary": "Premise measured on origin/main 5517005d through the real HTTP door (@objectstack/verify bootStack, closure built 49/49, VERDICT command-exit 0): one fixture stack declaring queue pq_support, one sys_queue row pq_org_desk inserted by the dev admin (201), then a fresh sign-up as a plain member (member_default baseline only). Declared half: the member reads GET /api/v1/meta/queue -> 200 with pq_support, same as the admin. Organization half: the member reads GET /api/v1/data/sys_queue -> 403 PERMISSION_DENIED (object sys_queue); the admin reads it -> 200, total 1. No equivalent served list exists for the member: GET /data/sys_queue_member -> 403, the injected queue column on GET /meta/object/pq_thing is a plain text field (system, hidden) with no options, /discovery carries no queue bit. Cause in source: member_default (plugin-security default-permission-sets.ts) has no wildcard since #5491 and names no sys_queue entry; sys_queue declares no default row scope. So the console cannot tell, for an ordinary user, whether the organization has a queue: reading declared queues alone hides the toggle for every member of an organization whose only queues are sys_queue rows, which is the hide-everywhere fallback the card forbids for that case. No code written, no PR; the branch stays at main 5517005d with no commit.",
      "tests": "Scratch probe only (never committed): packages/verify/src/zz-probe-28428.test.ts, copied with its run log to the project files (issue-28428/). Run: pnpm --filter @objectstack/verify exec vitest run --maxWorkers=2 src/zz-probe-28428.test.ts under os-verify-lock, Test Files 1 passed (1), VERDICT command-exit 0. Readings: admin POST /data/sys_queue 201; admin GET /meta/queue 200 [pq_support]; admin GET /data/sys_queue 200 total 1; member GET /meta/queue 200 [pq_support]; member GET /data/sys_queue 403 PERMISSION_DENIED; member GET /data/sys_queue_member 403 PERMISSION_DENIED; member GET /meta/object/sys_queue 200 (schema readable, rows not). Probe file removed from the worktree afterwards (git status clean).",
      "gates": "n/a — no diff. dispatch-gates not run (empty change set).",
      "line_budget": "0 files changed",
      "files_changed": [],
      "deviations": "1) The seat's order names ListView.queueSittingFilter-25769.test.tsx beside ListView.tsx; it lives at ui/packages/plugin-list/src/__tests__/. 2) The existing queue-filter tests will need a queue-resolving data source double once the toggle is gated, since their fixtures serve no queue; noted for whichever route is ruled.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; no PR",
      "mcp_calls": "0 — GitHub reads went through the REST proxy (gh api); no GitHub MCP tool called",
      "api_writes": "1 — this os-dev-report comment via post-stamped (POST /repos/objectstack-ai/objectstack/issues/28428/comments); git push of the empty branch is not a REST write",
      "open_questions": [
        {
          "question": "Where does an ordinary user learn that the organization has at least one sys_queue row?",
          "options": [
            "A — server: the console's session or bootstrap payload carries one organization-has-queues bit (declared OR sys_queue, resolved server-side under system context); ui reads it once per session. Server lane + ui follow-up; no permission widening.",
            "B — permission: member_default gains read on sys_queue (name and label only, through FLS). Widens an everyone-anchored baseline the #5491 ruling narrowed to explicit allow: maintainer call, not a seat's.",
            "C — server: a dedicated read (for example the ownership-queue service answering the caller's organization queue names) served to any member; ui merges it with GET /meta/queue as mergeQueueCandidates does.",
            "D — keep the card at the declared half only (toggle shown when GET /meta/queue answers at least one item, or the viewer is an admin who can read sys_queue). Accepts a known miss for organizations whose only queues are sys_queue rows."
          ],
          "recommendation": "A, because it answers the one question the toolbar asks (any queue?) without exposing queue names to everyone, keeps the read off list mount by construction, and needs no change to the #5491 baseline; B and C widen what a member can read and D ships the forbidden hide-for-some case."
        }
      ],
      "out_of_scope_findings": [
        "class: fork (premise) · reach: GET /api/v1/data/sys_queue as a plain member -> 403 PERMISSION_DENIED on origin/main 5517005d, while GET /api/v1/meta/queue -> 200 · evidence: probe readings above, member_default in plugin-security default-permission-sets.ts names no sys_queue · dedupe words: sys_queue member read, organization has queues, In a queue toggle, queue set session bit",
        "carrier: seat ui#4 (decides the route per the card: record page payload or session bit) · noted, not filed"
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#4 · os-sales · session_01L6bhgWqjXeTcS3jtug46Dv

    Release: claim 6545991160 (session_01L6bhgWqjXeTcS3jtug46Dv, domain:ui#4) · 2026-10-31T16:31Z · the premise check failed, so the card goes to the maintainer as needs-user-decision. No code was written and no PR was opened. The branch claude/project-thread-k1hius equals main 5517005d.
    Resume-from: none

    What failed. The card thread re-measured through the live HTTP doors (os-dev-report 6546092023; probes and logs under the project's issue-28428/ folder):

    So a member cannot tell whether the organization has a sys_queue row. The card's own stop rule applies: no fallback to "show everywhere" or "hide everywhere".

    The question (the dev's letters, verbatim in the report): where does an ordinary user learn that the organization has at least one queue?

    None of these is the seat's to choose. A and C add server surface while the server lane is off, B relaxes a permission baseline, and D ships the case the card forbids. Per the project rule, the analysis gets an independent review before the maintainer rules. Labels: pm:dispatched → needs-user-decision, assignee cleared.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

    Ruling: route A · 2026-11-01T03:58Z

    The maintainer, in the epic seat's session, on the four routes in the os-dev-report above: 「28428 同意」. That answers the seat's recommendation of A, the dev's recommendation too.

  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

    Priority · 2026-11-01T04:06Z · priority:p2 → priority:p1. The maintainer, in the epic seat's session, on #28428 and #28508 together: 「这两个都应该p1」. The pair moves as one, since #28428 is blocked on #28508 (see #28508).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uienhancementNew feature or requestpm:blockedpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions