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.3 remaining items
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).objectstack-fleet commentedon Nov 1, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblocked · p1 ·
pm:blockedremoved · epic dispatch · 2026-11-01T17:58Z⛔ Not a claim.
Blocked-by: #28508is closed as completed (the producer half, route A).pm:blockedis removed. As a child of epic epic: record page v2 — one default open mode per object (drawer by default, page by declaration; modal / split / popover retired as metadata); the drawer and the page are one page at two widths; related records open in place #26676, it carries no otherpm:*label: the epic seat dispatches it.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: released to the lane queue ·
pm:queue· 2026-11-02T01:52Z⛔ Not a claim.
check-half-statesH13). The maintainer delegated label decisions of this kind to triage. The card goes topm:queueat p1, and thedomain:uilane claims it like any queued card. The epic's ordering still binds: read epic: record page v2 — one default open mode per object (drawer by default, page by declaration; modal / split / popover retired as metadata); the drawer and the page are one page at two widths; related records open in place #26676 before claiming.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Claim: PM loop round 10 · 2026-11-02T02:45Z
Session:
session_01QsR2jUddMTB1CyxjzoKGxi(card thread, os-dev); dispatched by seat sessionsession_01QsR2jUJ19MstFvFhjgiYTuAccount:
zhuangjianguoBranch:
claude/project-thread-0ef0f7(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-28428Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/plugin-list/src/ListView.tsx(queueFilterOffered,objectHasOwnershipQueue), thehasAnyQueueread from@object-ui/permissions(MePermissionsProvider.tsx, read only unless a hook export is needed), their tests (incl.ListView.queueSittingFilter-25769), and a root.changeset; server andpackages/specread only (stop on breach; explain in the report)Container & model: S,
mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6567195109
Premise: holds (at
origin/main76bf6c50:ListView.tsx:2166offers the filter onobjectHasOwnershipQueue(objectDef)alone (:897);hasAnyQueueis served byGET /auth/me/permissions(#28508, PRs #28654 / #28704) and typed in@object-ui/permissions'MePermissionsProvider)Serial constraints cleared: none known;
domain:ui#4's record-page cluster does not touchplugin-list.Ruling-ref: 6553134732 (route A): the toggle is offered when the object carries
queueAND the session's has-queues boolean is true; no queue list is read on list mount.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Claim amended · 2026-11-02T02:47Z · after the dev's pre-code measurement. This widens the file surface and changes one declaration line. It adds to claim 6567823409. Branch, session and size are unchanged.
76bf6c50):hasAnyQueueis on the spec response (packages/spec/src/api/auth.zod.ts:194) and inMePermissionsProvider's payload. No exportedPermissionContextValuemember carries it, and no ui code reads it.ui/packages/permissions/src/PermissionContext.tsgains one optionalhasAnyQueue?: booleanmember.MePermissionsProvider.tsxsets it from the authenticated body.usePermissions.tsleaves it undefined in the no-provider literal. One@object-ui/permissionstest is added. The role-based provider leaves it undefined. The scoped providers spread the parent, so they need no edit.Clause-②: yes (widening): one optional member on a published type of@object-ui/permissions, following the grid: "select all matching" banner counts every match, but the bulk action processes only the first 5000 #28552 precedent.Metadata-protocol: noneis unchanged. The changeset stayspatchand names@object-ui/permissions.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
os-dev-report
Card thread session_01QsR2jUddMTB1CyxjzoKGxi, report. Draft PR #28889 is open.
{ "issue": 28428, "status": "done", "branch": "claude/project-thread-0ef0f7", "pr": "https://github.com/objectstack-ai/objectstack/pull/28889", "session": "session_01QsR2jUddMTB1CyxjzoKGxi", "premise_still_valid": true, "summary": "The list toolbar offers the In a queue toggle only when the object carries the injected queue column AND the session's /auth/me/permissions hasAnyQueue is true (route A, ruling 6553134732). @object-ui/permissions gains one optional PermissionContextValue member, hasAnyQueue, set by MePermissionsProvider from the authenticated answer and undefined otherwise (no provider, role-based provider, anonymous); the scoped providers forward it by spread. Unknown = hidden, per amendment 6567840348. No new request on list mount: the flag rides the permissions answer the console already holds.", "tests": "At head c0b35cd6, from ui/: vitest run packages/plugin-list/ -> Test Files 179 passed, Tests 1727 passed (VERDICT command-exit 0); vitest run packages/permissions/ -> 16 passed, 172 passed; vitest run scripts/ -> 169 passed 2 skipped, 4900 passed 2 skipped. New/changed: ListView.queueSittingFilter-25769 (session double serving hasAnyQueue; cases: no queue = no toggle, one queue = toggle, unknown = no toggle, same data-source calls with and without a queue and no fetch) and permissions hasAnyQueue-28428 (truth table). Ablations via scripts/ablation-replace.mjs, blob restored to HEAD each time: gate removed -> 2 red (no queue, unknown); provider member removed -> 3 red (served true, served false, scope forwarding). Real console (production build, dev:crm --fresh, admin): hasAnyQueue false -> no toggle on crm_account/crm_contact; one sys_queue row (201) -> true, toggle on both, press sends filter [and,[owner_id,is_empty,true],[queue,is_not_empty,true]]; row deleted -> hidden again. List load 31 or 32 API requests in both states over two runs each, identical distinct endpoint sets, no queue URL.", "gates": "dispatch-gates --commands at c0b35cd6: 34 commands, all exit 0 (33 in one loop incl. check-sdui-manifest, check-changeset-no-major, check-adr-0087-registration, check:published-files, check:nul-bytes; plus pnpm --dir ui exec vitest run scripts/). turbo run type-check plugin-list + permissions: 51/51. PR-body gates with GITHUB_EVENT_PATH on the sent body: check-changeset-no-major exit 0, check-adr-0087-registration exit 0, check-closing-keyword-parity exit 0, check-partof-closing-keyword exit 0. PR check-runs at report time: 4 success, 11 skipped, 18 in progress or queued.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "2 — pr_create via fleet-write relay (PR #28889, read back byte-identical); this os-dev-report comment via post-stamped. git push is not a REST write.", "deviations": "1) The claim's Clause-② no was amended to yes (widening) by 6567840348 after the pre-code measurement; the PR body and changeset carry the amended line. 2) The changeset names @object-ui/plugin-list as well as @object-ui/permissions (both patch): the ListView change publishes too. 3) The PR body opens with Fixes #28428; the relay did not prepend the project attribution block, so the body has none. The full body with the block is at /mnt/project-files/issue-28428/pr-body.md if the seat wants it applied. 4) HotCRM acceptance not run (another repository); app-crm measured.", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Landed · 2026-11-02T04:01Z · PR #28889 merged through the merge queue as
61de599bat 2026-11-02T04:01Z (GitHub's clock). ACCEPT 6568528457 at headc0b35cd6.61de599b~1)origin/main)hasAnyQueue === trueinplugin-list/src/ListView.tsxhasAnyQueue?inpermissions/src/PermissionContext.tspermissions/src/__tests__/hasAnyQueue-28428.test.tsxexistsFixes #28428closed this card.pm:dispatchedcomes off in the same stroke as this note.