Skip to content

fix(plugin-kanban): a lane that refuses the dragged card says why - #27133

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-284e5p
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-284e5p

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #27099

维护者速读

  • 改了什么:看板拖卡时,被状态机规则挡住的列除了变暗,还在 title(鼠标悬停提示)和 aria-describedby(读屏)里说明原因;泳道布局的格子同样。
  • 为什么改:变暗的列不说为什么拒收,用户只会反复试或提工单;原因直接取自挡住这次移动的规则的 message,即服务端拒收同一移动时给出的那句话。
  • 风险与回滚:只加属性和一个视觉隐藏的 span,开着的列与拖拽外一律不变;回滚即 revert 本 PR,无数据或元数据影响。
  • 先看:ui/packages/plugin-kanban/src/ObjectKanban.tsx 的 dropBlockedReason,再看 KanbanImpl.tsx 的 dropBlockedReasonFor 与两处车道属性。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-kanban/src/ObjectKanban.dropBlockedReason-27099.test.tsx packages/plugin-kanban/src/__tests__/dropBlockedReason-27099.test.tsx,预期 8 个测试全绿。
  • 需要你做的:无,席位落地。

Clause-②: no
Metadata-protocol: none

What changes

  • KanbanImpl.tsx. KanbanBoardProps gains an optional dropBlockedReason(fromColumnId, toColumnId) beside canDropCard. While a card is dragged, a lane the gate closes now carries the reason as its title and as the text of a visually hidden span its aria-describedby names. The swimlane layout's cells, which draw the same three blocked attributes on their own, carry it the same way (one id per row and column). An open lane, and every lane outside a drag, carries neither. When the host gives no reason, the lane uses the new pack sentence kanban.dropBlocked ("A card in {{from}} cannot be moved to {{to}}.") with the two column titles.
  • ObjectKanban.tsx. Supplies the reason: the message of each blocking state_machine rule on the grouped field that refuses the move. Each rule is asked alone through the gate's own read (stateMachineNextValues), so a rule the gate ignores (warning-only, inactive, insert-only, or silent on the card's value) is never named, and no second copy of the gate's predicate exists. Several closing rules join their messages; a blank message falls through to the board sentence. stateMachineNextValues and @object-ui/core are untouched.
  • KanbanBoardCore.tsx. Threads the new prop, as it does canDropCard.
  • @object-ui/i18n. kanban.dropBlocked in all ten packs.
  • Changeset. @object-ui/plugin-kanban and @object-ui/i18n, patch.

Why

The card's Expected: a blocked lane exposes the reason the drop is refused, worded from the rule that blocks it, through a title and aria-describedby on the existing lane element, with no new widget. Measured on origin/main before the change: the only supplier of the gate is ObjectKanban's canDropCard, and the only thing it reads is the blocking state_machine rule on the groupBy field. A required field never closes a lane (it prompts after the drop), and a missing update grant makes the cards not movable, so no lane is dimmed for either. The card's "transition, required field or permission" therefore reduces to the state-machine rule, whose message the spec requires.

Tests

  • ObjectKanban.dropBlockedReason-27099.test.tsx (new, beside the drop-gate test): the closed lane's title and accessible description are the rule's message; CONTROL, an open lane and every lane before the drag explain nothing; the reason leaves with the drag; two closing rules are joined while a rule silent on the from-state and a warning-only rule are not named; a blank message falls back to the board sentence.
  • __tests__/dropBlockedReason-27099.test.tsx (new, real DndContext, pointer pick-up): every swimlane row's closed cell carries the reason and the open column's cells do not; the fallback is read from the zh pack under a zh provider (the two packs are first shown to differ); a host's dropBlockedReason wins over the fallback.

Acceptance notes

  • The reason is the text /meta serves. The server's refusal of the same move resolves the message against objects.OBJECT._validations.RULE.message in the caller's language, but translateObject in @objectstack/spec (the served object's translation) does not touch validations[].message, and nothing in ui/ reads that key. Under a translated locale the lane therefore shows the authored text while the refusal toast would show the translated one. Reported to the seat as a finding; not changed here.
  • The fallback sentence names column titles, not values, so it reads the same on every board.

Verification

All runs at head 18f0af77 (the branch with origin/main 6817bf1e merged), except the two ablations, which ran on the pre-merge commit 6dc7501f carrying the same feature diff.

New and neighbouring tests

pnpm --dir ui exec vitest run --maxWorkers=2 over the two new files and ObjectKanban.stateMachineDropGate-25933.test.tsx: Test Files 3 passed (3), Tests 14 passed (14).

Ablations (each restored, blob equal to HEAD and `git diff HEAD` empty, through `scripts/ablation-replace.mjs`)
  • Deleting the flat lane's title and aria-describedby lines: ObjectKanban.dropBlockedReason-27099.test.tsx 4 failed, 1 passed (the CONTROL).
  • Deleting the swimlane cell's title and aria-describedby lines: __tests__/dropBlockedReason-27099.test.tsx 1 failed (the swimlane case), 2 passed.
Package suites and type-check
  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-kanban/ packages/i18n/: Test Files 171 passed (171), Tests 1910 passed, 13 skipped.
  • pnpm --filter @object-ui/plugin-kanban --filter @object-ui/i18n run type-check: exit 0 for both.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands; all 34 ran and exited 0 (exit captured before any pipe), plus pnpm check:changeset-fixed and pnpm --dir ui run check:new-line-citations outside the derivation, both exit 0. --ran reconciliation: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. Families CI runs beyond the derivation (lint, type-check lanes, the full UI shards) are left to this PR's check-runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (No formatter ran; pnpm lint is CI's run.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (Not touched.)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (The 维护者速读 block is the dispatch order's required Chinese section.)
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

A lane the drop gate closes was dimmed and aria-disabled with no reason
attached. It now carries the reason as its title and as the text its
aria-describedby names, on the flat lane and on every swimlane cell.
ObjectKanban answers with the message of each blocking state_machine
rule that refuses the move; a host with no reason gets the new
kanban.dropBlocked sentence in all ten packs.

Claude-Session: https://claude.ai/code/session_01JQwxbZzYH3oXtLp9fbSkzn
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head 18f0af77cb · 2026-10-28T07:35Z

席位意见:拖动卡片时,被状态机规则拒绝的列现在用规则自己的提示语说明原因(悬停提示加读屏描述),只在被拒绝时出现;规则没写提示语时用新的通用句子。

  1. Shape: draft, base main; the body says Fixes #27099, which is right, because the PR covers the whole card. 16 files, +465/−4. Not governed (check-governed-merges --pr 27133). ≤ 3000 lines.
  2. Premise measured. Seat read at head:
    • KanbanBoardProps.dropBlockedReason?: (from, to) => string | undefined is optional beside the unchanged boolean canDropCard. KanbanBoardCore passes it through.
    • dropBlockedReasonFor returns undefined unless isDropBlocked. So title, aria-describedby and the sr-only span appear only on a closed lane during a drag, on both the column view and every swimlane cell. Swimlane ids are per lane and column from one useId base, and the new useId calls sit before any return.
    • ObjectKanban asks each rule alone through stateMachineNextValues({ validations: [rule] }, …). That names exactly the rules the gate's intersection closes on, and never a warning-only, inactive or silent rule. Messages are trimmed, de-duplicated and joined. Core is untouched.
    • The fallback kanban.dropBlocked uses the column titles and is in all ten packs.
  3. Gate check-runs (pinned to head 18f0af77cb). In progress at this record; landing waits for every check green. The dev's local reading: 14 targeted tests (8 board-level, 6 ObjectKanban), 171 suite files, type-check green, two ablations red as expected, 34/34 derived gate families exit 0 with --ran reconciled.
  4. Accepted deviations. None beyond the order's route. A fallback key was added, which the order allowed. The i18n packs carry one line each.
  5. Out of scope (filed). The dev's class-a finding is that translateObject (packages/spec/src/system/i18n-resolver.ts) translates fields and actions but not validations[].message. So under a translated locale, the lane shows the authored message while the server's refusal of the same move is translated (objects.<o>._validations.<rule>.message, read only by the ObjectQL rule validator). The seat verified this at origin/main and files it for the spec lane; it does not block this PR.
  6. Landing clause. Not governed. When every check on the current head is green, the seat lands it (pr_ready + automerge_enable), or asks the maintainer to click if the classifier refuses. On merge, Fixes #27099 closes the card.

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

Labels

documentation Improvements or additions to documentation size/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants