Repository navigation
Commit ec66ac0
fix(plugin-kanban): a lane that refuses the dragged card says why (#27133)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Va1s9AYECZKBfU5vttxtZo)_
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.
<details><summary>New and neighbouring tests</summary>
`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).
</details>
<details><summary>Ablations (each restored, blob equal to HEAD and `git
diff HEAD` empty, through `scripts/ablation-replace.mjs`)</summary>
- 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.
</details>
<details><summary>Package suites and type-check</summary>
- `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.
</details>
<details><summary>Derived gates</summary>
`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.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (No formatter ran; `pnpm
lint` is CI's run.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English. (The 维护者速读 block is
the dispatch order's required Chinese section.)
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbZzYH3oXtLp9fbSkzn)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 3e195be commit ec66ac0
16 files changed
Lines changed: 465 additions & 4 deletions
File tree
- .changeset
- ui/packages
- i18n/src/locales
- plugin-kanban/src
- __tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
986 | 986 | | |
987 | 987 | | |
988 | 988 | | |
| 989 | + | |
989 | 990 | | |
990 | 991 | | |
991 | 992 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
881 | 881 | | |
882 | 882 | | |
883 | 883 | | |
| 884 | + | |
884 | 885 | | |
885 | 886 | | |
886 | 887 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1137 | 1137 | | |
1138 | 1138 | | |
1139 | 1139 | | |
| 1140 | + | |
1140 | 1141 | | |
1141 | 1142 | | |
1142 | 1143 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
910 | 910 | | |
911 | 911 | | |
912 | 912 | | |
| 913 | + | |
913 | 914 | | |
914 | 915 | | |
915 | 916 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
905 | 905 | | |
906 | 906 | | |
907 | 907 | | |
| 908 | + | |
908 | 909 | | |
909 | 910 | | |
910 | 911 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
881 | 881 | | |
882 | 882 | | |
883 | 883 | | |
| 884 | + | |
884 | 885 | | |
885 | 886 | | |
886 | 887 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
881 | 881 | | |
882 | 882 | | |
883 | 883 | | |
| 884 | + | |
884 | 885 | | |
885 | 886 | | |
886 | 887 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
905 | 905 | | |
906 | 906 | | |
907 | 907 | | |
| 908 | + | |
908 | 909 | | |
909 | 910 | | |
910 | 911 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
935 | 935 | | |
936 | 936 | | |
937 | 937 | | |
| 938 | + | |
938 | 939 | | |
939 | 940 | | |
940 | 941 | | |
| |||
0 commit comments