Skip to content

plugin-kanban: a lane that refuses the dragged card is dimmed (data-kanban-drop-blocked, aria-disabled) with no title or aria-describedby, so the user is not told why the drop is blocked #27099

Closed

Description

@objectstack-fleet

Filing gate ①: a user-visible UX defect with a named location, reproduced in a QA run. Raised by devx seat 2 (session_01QSeKYmTYfaPbLQD4BJfomW) from the #25807 wave-2 slice A report (os-dev-report 6456266552), after PR #26969 landed. ⛔ Not a claim. ⛔ No domain:*: triage sets the lane.

Business reason. A user dragging a card on a kanban board sees some lanes go dim and refuse the drop, with nothing saying why (a transition rule, a required field, a permission). The checklist's ux-conventions.disabled-control-reason convention asks that a disabled control explain itself. Without the reason, the user retries or files a support ticket.

Reach. UI: QA run #26456, kanban drag on showcase_task. A blocked lane is dimmed with data-kanban-drop-blocked and carries no title or aria-describedby.

Where. ui/packages/plugin-kanban/src/KanbanImpl.tsx: dropBlocked (L699/L732) sets aria-disabled (L783) and suppresses the highlight (L749). No reason text is attached. Read at origin/main 69df290ea5.

Expected. The blocked lane exposes the reason the drop is refused, through a tooltip or title for pointer users and aria-describedby for assistive tech, worded from the rule that blocks it. The block needs no new widget: it uses the lane's existing element plus the reason the drop check already computes, if it has one. If the drop check computes no reason today, the PR says so and the convention item decides the wording.

Reader who acts. The seat that owns ui/packages/plugin-kanban, after triage. One PR with a test asserting the reason is exposed on a blocked lane.

Dedupe. search_issues (semantic) for "kanban drop blocked lane dimmed no reason tooltip disabled control": 1 hit (#13692, closed, empty-group fallback lane; unrelated). New.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: domain:ui, p3, area:records · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-28T05:59Z

    ⛔ Not a claim.

    • Read fresh. A kanban lane that refuses the dragged card is dimmed (aria-disabled) and says nothing about why, against ux-conventions.disabled-control-reason. It is UX, with no wrong data, so p3.
    • The reason text comes from the rule that blocks the drop (the transition, a required field or a permission), as the lane already knows it. No new widget: title plus aria-describedby on the existing element.
    • pm:queue.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T06:20Z

    You are the os-dev developer agent for exactly ONE card: #27099, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27099" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take a third ui lane seat" (cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Uc2nCvWvLyYyaVYaUhakpJ) one message with your branch name and wait. Read-only measuring is fine meanwhile. Worktree objectstack-issue-27099 on that branch from a fresh origin/main (git checkout --detach the main checkout first, then git worktree add --no-track -B <branch> ../objectstack-issue-27099 origin/main); pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=27099 --file=F --json run ONCE with output captured to a file (no time-shaped stamp inside the fenced report JSON). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Fixes #27099, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran (merge origin/main first if it refuses as STALE TREE); node scripts/check-sdui-manifest.mjs is a gate on every ui card. Build the closure your tests import first (bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '<pkg>^...' build"). Changeset in the root .changeset/ for each published package your diff changes (app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread one message (PR, head sha, report gist); keep the thread open through review and drive CI to green. If the premise fails, stop and report premise_still_valid: false with the evidence; ⛔ never force a fix.

    Ruling (cannot be re-ruled)

    Triage 6461935458: domain:ui, p3, the filer's Expected stands. Reason text from the rule that blocks the drop; no new widget (title plus aria-describedby on the existing lane element). No maintainer ruling.

    PM mechanism assumptions (measure; disproof welcome)

    On origin/main d8a9cc59 (read-only check):

    • ui/packages/plugin-kanban/src/KanbanImpl.tsx: KanbanBoardProps.canDropCard?: (fromColumnId, toColumnId) => boolean (≈L241) is a bare boolean, so no reason exists today. isDropBlocked (≈L1103) feeds dropBlocked to the lane (≈L1671 → aria-disabled ≈L783, data-kanban-drop-blocked ≈L784, opacity-40 ≈L794). The swimlane cell (≈L1637–1644) draws the same three attributes separately.
    • ui/packages/plugin-kanban/src/ObjectKanban.tsx ≈L1822 is the board's only supplier. The gate is ONLY the object's blocking state_machine rule on the groupBy field, through stateMachineNextValues (ui/packages/core/src/validation/validators/state-machine-next-values.ts), which intersects every blocking update machine's transitions[from]. A required field never blocks a lane: it prompts after the drop (objectui#4254). A missing permission makes the cards not movable (kanbanCardMove affordance), so no lane is dimmed. The card's "transition, required field or permission" therefore reduces to the state-machine rule here; confirm it.
    • Every validation rule carries a required message (BASE_VALIDATION_SHAPE, packages/spec/src/data/validation.zod.ts ≈L157). It is the text the server's refusal of the same move shows.

    PM suggested route (optional; measurement wins)

    1. Let the gate return the reason beside the verdict, without breaking the boolean prop. For example, add an optional dropBlockedReason?: (from, to) => string | undefined on KanbanBoardProps and KanbanBoardCore, or a sibling helper in core that returns the blocking rule(s), not just the set.
    2. In ObjectKanban, the reason is the message of the blocking rule that excludes the target. Localize it the way the record form or the server refusal renders a validation message, if such a path exists; otherwise use it as authored. If several rules block, join their messages. With no message, fall back to a new i18n key along the lines of "This move isn't allowed from {from} to {to}", added in every locale pack the plugin's other keys live in.
    3. On a blocked lane (and the swimlane cell), set title plus aria-describedby, pointing at a visually hidden element that carries the reason. Set them only while blocked, so an open lane gains nothing.

    Tests (beside the existing #25933 drop-gate tests in ui/packages/plugin-kanban/src/__tests__/):

    • During a drag, a blocked lane exposes the rule's message through title and through the aria-describedby target.
    • CONTROL: an open lane carries neither.
    • CONTROL: the fallback text when the rule has no usable message, if you add a fallback.

    Ablation: drop the reason attributes; the blocked-lane test goes red.

    Scope

    File surface:

    • ui/packages/plugin-kanban/src/{KanbanImpl,KanbanBoardCore,ObjectKanban}.tsx, plus ui/packages/core/src/validation/validators/state-machine-next-values.ts only if the reason needs a core helper (keep stateMachineNextValues' signature and behaviour; the grid's inline editor reads it).
    • Tests beside them.
    • i18n locale packs only for a fallback key.
    • Root .changeset/: @object-ui/plugin-kanban patch, plus @object-ui/core or @object-ui/i18n if touched.

    ⛔ No spec edit: the rule message is already authored.

    Serial: no open PR edits ui/packages/plugin-kanban/** (open PR list read 2026-10-28T06:20Z).

    Tier

    No path-derived mandate (dispatch-gates --tier, read 2026-10-28T06:20Z). TIER_DEFAULT, effort medium (S–M). Clause-②: no (renderer accessibility text; no accept set changes), Metadata-protocol: none.

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_01JQwxbXpF772qWJjojJvRXw
    Account: os-warren
    Branch: claude/project-thread-284e5p
    Worktree: objectstack-issue-27099
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: ui/packages/plugin-kanban/src/{KanbanImpl,KanbanBoardCore,ObjectKanban}.tsx, ui/packages/core/src/validation/validators/state-machine-next-values.ts only if the reason needs a core helper, tests beside them, i18n locale packs only for a fallback key, root .changeset/ (stop on breach; explain in the report)
    Container & model: S–M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: a kanban lane that refuses the dragged card is dimmed and aria-disabled with no reason attached | the drop gate is the blocking state_machine rule, whose authored message already says why | every user dragging cards on a board whose groupBy field has a blocking state machine
    Thread-read: 6462220361
    Premise: holds (gate read on origin/main d8a9cc59; readings in dispatch order 6462220361)
    Serial constraints cleared: no open PR edits ui/packages/plugin-kanban/**.

    Dispatch order: comment 6462220361. Card thread claude/project-thread-284e5p may start editing.

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 27099,
      "status": "done",
      "branch": "claude/project-thread-284e5p",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27133",
      "head": "18f0af77",
      "session": "session_01JQwxbZzYH3oXtLp9fbSkzn",
      "premise_still_valid": true,
      "summary": "A lane the drop gate closes now carries the reason as its title and as the text its aria-describedby names, on the flat lane and on every swimlane cell; an open lane and every lane outside a drag carry neither. ObjectKanban supplies the message of each blocking state_machine rule on groupBy that refuses the move, asking each rule alone through stateMachineNextValues (no copy of the gate predicate, core untouched); several closing rules join, a blank message falls through to the new kanban.dropBlocked pack sentence (ten packs). Premise confirmed: the gate's only supplier is ObjectKanban canDropCard, which reads only the blocking state_machine rule; a required field prompts after the drop and a missing grant makes cards not movable, so neither dims a lane.",
      "files_changed": [
        ".changeset/27099-kanban-blocked-lane-reason.md",
        "ui/packages/plugin-kanban/src/KanbanImpl.tsx",
        "ui/packages/plugin-kanban/src/KanbanBoardCore.tsx",
        "ui/packages/plugin-kanban/src/ObjectKanban.tsx",
        "ui/packages/plugin-kanban/src/ObjectKanban.dropBlockedReason-27099.test.tsx",
        "ui/packages/plugin-kanban/src/__tests__/dropBlockedReason-27099.test.tsx",
        "ui/packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts"
      ],
      "tests": "head 18f0af77: new and neighbour files (2 new + stateMachineDropGate-25933) Test Files 3/3, Tests 14/14; plugin-kanban + i18n dirs Test Files 171/171, Tests 1910 passed 13 skipped (lock VERDICT command-exit 0); type-check plugin-kanban and i18n exit 0. Ablation on 6dc7501f via ablation-replace.mjs: flat lane title+aria-describedby deleted -> ObjectKanban.dropBlockedReason-27099 4 failed 1 passed (CONTROL); swimlane cell title+aria-describedby deleted -> __tests__/dropBlockedReason-27099 1 failed 2 passed; both restored, blob == HEAD, git diff HEAD empty. Tests import source by relative path and @object-ui/i18n is vitest-aliased to src, so no dist leg.",
      "gates": "dispatch-gates --commands: 34 derived, all 34 run, every exit 0 captured before any pipe (incl. node scripts/check-sdui-manifest.mjs exit 0, pnpm --dir ui exec vitest run scripts/ exit 0 with Test Files 175 passed 2 skipped); plus check:changeset-fixed exit 0 and ui check:new-line-citations exit 0 outside the derivation. --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. CI check-runs: in_progress at report time.",
      "line_budget": "469 changed lines before merge (+465 / -4), under the 3000 human-merge threshold",
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "deviations": [
        "origin/main 6817bf1e merged into the branch before the PR (it touched the same ten locale packs); suites, type-check and all gates re-run on the merged head 18f0af77. origin/main has since moved again (e6e41998); not merged, no conflict reported.",
        "The ablations ran on the pre-merge commit 6dc7501f, which carries the same feature diff.",
        "PR template headings folded into the os-dev body order (Why and Human Minimum kept)."
      ],
      "mcp_calls": "0",
      "api_writes": "2: pr_create through the fleet-write relay (pulls, PR 27133); this report through post-stamped (issues/27099/comments). git push is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: under a translated locale, a blocked lane (and every client surface reading /meta validations) shows the authored validations[].message while the server refusal of the same move shows the objects.OBJECT._validations.RULE.message translation, because translateObject in @objectstack/spec translates fields and actions but not validations · evidence: translateObject maps doc.fields and doc.actions only; objectValidationMessageKey is read only by the ObjectQL rule validator; nothing under ui/ reads _validations · dedupe words: translateObject validations message locale, _validations served meta, rule message translation client"
      ]
    }
  5. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat review: PR #27133 ACCEPT (record on the PR, head 18f0af77cb), 2026-10-28T07:37Z. The out-of-scope finding (translateObject leaves validations[].message untranslated) is filed as #27135 (domain:spec). Landing waits for every check on the head to be green.

  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing held · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-28T07:50Z

    PR #27133 (ACCEPT 6463403177) is green on head 18f0af77cb: 43 checks succeeded and 11 were skipped (path-filtered core / docs / dogfood jobs). mergeable_state is clean and the PR is not governed. This session's permission classifier refused the landing stroke (pr_ready + automerge_enable through the fleet-write relay, "Merge Without Review") on PR #27013 and on PR #27035 at 2026-10-28T02:42Z, so the seat does not retry it here. It asks the maintainer to mark the PR ready for review and enable auto-merge. The out-of-scope finding is filed as #27135 (domain:spec).

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions