Repository navigation
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
Description
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 addsowner_id is_emptyANDqueue is_not_empty(QUEUE_SITTING_CONDITIONS,ui/packages/plugin-list/src/ListView.tsxaround line 864). - It is offered whenever
objectHasOwnershipQueue(objectDef)is true (ListView.tsxline 878, read at line 2147, rendered at line 5707). That check asks only whether the served definition carries the platform-injected system fieldqueue. - ADR-0142 D3 injects
queueon every object that getsowner_id, which means everyownership: '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-crmdeclares none either. Neither app has asys_queuerow 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
queuecolumn 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
- On HotCRM and
examples/app-crmon main, no list shows In a queue. - Declare one queue, or insert one
sys_queuerow. The toggle appears on owned objects' lists, and turning it on filters exactly as it does today. - The existing tests in
ListView.queueSittingFilter-25769.test.tsxstill pass. New tests pin the no-queue case (toggle absent) and the one-queue case (toggle present). - Opening a list adds no request; the network count is the same before and after.
Activity
objectstack-fleet commentedon Oct 31, 2026
domain:ui#4 · os-sales · session_01L6bhgWqjXeTcS3jtug46Dv
Claim: PM loop round 3 · 2026-10-31T16:20Z
Session:
session_01L6bhgWqjXeTcS3jtug46Dv(seat; implementation by card thread sessionsession_01L6bhgWQvHUMoWtK3q6mQxV,mode:thread)Account:
os-sales(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-k1hiusWorktree:
objectstack-issue-28428Domain:
domain:uiSeat:
domain:ui#4File surface:
ui/packages/plugin-list/src/ListView.tsxand tests beside it (includingListView.queueSittingFilter-25769.test.tsx), one shared queue-set hook inui/packages/plugin-list/src/orui/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/**andcomponents/src/renderers/complex/data-table.tsx(#28434, seat 5) and #27524 PR ④'s surfaceContainer & model:
M,mode:thread,model: default(dispatch-gates --tier: no path-derived mandate),effort: highClause-②: 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_queuerows) or an equivalent served list. If not, the thread stops and reports a fork inout_of_scope_findings(no fallback to "show everywhere" or "hide everywhere")Serial constraints cleared: no open non-mailbox PR touches
plugin-list/src/ListView.tsxat this read; #28423 (rows 7/17 on the same file) landed as5da3a8ce; 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 plussys_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 aTIER_DEFAULTreview before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 31, 2026
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
objectstack-fleet commentedon Oct 31, 2026
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 asneeds-user-decision. No code was written and no PR was opened. The branchclaude/project-thread-k1hiusequalsmain5517005d.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):GET /meta/queue→ 200, so declared queues are readable.GET /data/sys_queue→ 403PERMISSION_DENIED. An admin gets 200.sys_queue_memberalso returns 403.queuecolumn carries no options, and/discoveryhas no queue slot.member_defaulthas had no wildcard sincemember_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 and does not namesys_queue.So a member cannot tell whether the organization has a
sys_queuerow. 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?
member_defaultgains read onsys_queue. This widens themember_default's*wildcard object grant (C/R/E) union-merges into every org member — app-side explicit-allow object gates are erased on three axes #5491 baseline, so it is a permission call.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
objectstack-fleet commentedon Nov 1, 2026
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.
sys_queue. The ui reads it once per session.member_defaultread onsys_queue), C (a new read of queue names), D (declared queues only, with a known miss).sys_queue), with no widening of member read onsys_queue#28508, filed without adomain:*label for the triage seat to route. It names the ownership-queue service as the one place to resolve the answer,GET /auth/me/permissionsas the candidate carrier, no new route, and only the boolean exposed.sys_queue), with no widening of member read onsys_queue#28508.needs-user-decision→pm:blocked, withBlocked-by: #28508in the body.queuefield AND the session's has-queues boolean is true. The prerequisite paragraph about reading both queue sources is answered by Session payload: the console learns once per session whether the caller's organization has at least one ownership queue (declared orsys_queue), with no widening of member read onsys_queue#28508: no queue list is read on list mount. The deviation the dev noted carries over: the existing queue-filter tests need a session double that serves the boolean.objectstack-fleet commentedon Nov 1, 2026
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).