Skip to content

Commit ec66ac0

Browse files
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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/plugin-kanban': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(plugin-kanban): a lane that refuses the dragged card now says why
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
While a card is dragged, a lane the board's drop gate closes was dimmed and `aria-disabled` with nothing saying why. It now carries the reason as its `title` (a pointer user's tooltip) and as the text its `aria-describedby` names (assistive tech). An open lane carries neither, and the reason leaves with the drag. The swimlane layout's cells carry it the same way.
12+
13+
- **`object-kanban`.** The reason is the `message` of each blocking `state_machine` rule on the grouped field that refuses the move, the sentence the server's refusal of the same move carries. A rule the gate ignores (warning-only, inactive, insert-only, or silent on the card's value) is never named; several closing rules join their messages.
14+
- **Hosts of the board.** `KanbanBoardProps` gains an optional `dropBlockedReason(fromColumnId, toColumnId)` beside `canDropCard`. A host that answers nothing, or passes no function, gets the new `kanban.dropBlocked` sentence ("A card in To Do cannot be moved to Done.") with the two column titles, in all ten language packs.

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -986,6 +986,7 @@ const ar = {
986986
noCards: "لا بطاقات",
987987
columnCards: "بطاقات العمود {{column}}",
988988
laneCards: "بطاقات {{column}} - {{lane}}",
989+
dropBlocked: "لا يمكن نقل بطاقة من {{from}} إلى {{to}}.",
989990
cardTitlePlaceholder: "أدخل عنوان البطاقة…",
990991
requiredFieldsTitle: "أكمل الحقول المطلوبة",
991992
requiredFieldsDescription: "يجعل هذا النقل الحقول أدناه مطلوبة. أكملها للمتابعة.",

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ const de = {
881881
noCards: "Keine Karten",
882882
columnCards: "Karten in Spalte {{column}}",
883883
laneCards: "Karten in {{column}} - {{lane}}",
884+
dropBlocked: "Eine Karte aus {{from}} kann nicht nach {{to}} verschoben werden.",
884885
cardTitlePlaceholder: "Kartentitel eingeben…",
885886
requiredFieldsTitle: "Erforderliche Felder ausfüllen",
886887
requiredFieldsDescription: "Durch diese Verschiebung werden die folgenden Felder zu Pflichtfeldern. Füllen Sie sie aus, um fortzufahren.",

‎ui/packages/i18n/src/locales/en.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,7 @@ const en = {
11371137
noCards: 'No cards',
11381138
columnCards: '{{column}} cards',
11391139
laneCards: '{{column}} - {{lane}} cards',
1140+
dropBlocked: 'A card in {{from}} cannot be moved to {{to}}.',
11401141
cardTitlePlaceholder: 'Enter card title…',
11411142
uncategorized: 'Uncategorized',
11421143
loadMore: 'Load more',

‎ui/packages/i18n/src/locales/es.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,7 @@ const es = {
910910
noCards: "Sin tarjetas",
911911
columnCards: "Tarjetas de la columna {{column}}",
912912
laneCards: "Tarjetas de {{column}} - {{lane}}",
913+
dropBlocked: "Una tarjeta de {{from}} no se puede mover a {{to}}.",
913914
cardTitlePlaceholder: "Escriba el título de la tarjeta…",
914915
requiredFieldsTitle: "Complete los campos obligatorios",
915916
requiredFieldsDescription: "Este movimiento hace que los campos siguientes sean obligatorios. Complételos para continuar.",

‎ui/packages/i18n/src/locales/fr.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@ const fr = {
905905
noCards: "Aucune carte",
906906
columnCards: "Cartes de la colonne {{column}}",
907907
laneCards: "Cartes de {{column}} - {{lane}}",
908+
dropBlocked: "Une carte de {{from}} ne peut pas être déplacée vers {{to}}.",
908909
cardTitlePlaceholder: "Saisir le titre de la carte…",
909910
requiredFieldsTitle: "Compléter les champs obligatoires",
910911
requiredFieldsDescription: "Ce déplacement rend les champs ci-dessous obligatoires. Remplissez-les pour continuer.",

‎ui/packages/i18n/src/locales/ja.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ const ja = {
881881
noCards: "カードがありません",
882882
columnCards: "{{column}} のカード",
883883
laneCards: "{{column}} - {{lane}} のカード",
884+
dropBlocked: "{{from}} のカードは {{to}} に移動できません。",
884885
cardTitlePlaceholder: "カードのタイトルを入力…",
885886
requiredFieldsTitle: "必須項目を入力してください",
886887
requiredFieldsDescription: "この移動により以下の項目が必須になります。入力して続行してください。",

‎ui/packages/i18n/src/locales/ko.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,7 @@ const ko = {
881881
noCards: "카드 없음",
882882
columnCards: "{{column}} 열의 카드",
883883
laneCards: "{{column}} - {{lane}}의 카드",
884+
dropBlocked: "{{from}}의 카드는 {{to}}에 놓을 수 없습니다.",
884885
cardTitlePlaceholder: "카드 제목 입력…",
885886
requiredFieldsTitle: "필수 항목 입력",
886887
requiredFieldsDescription: "이 이동으로 아래 항목이 필수가 됩니다. 입력한 후 계속하세요.",

‎ui/packages/i18n/src/locales/pt.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,7 @@ const pt = {
905905
noCards: "Sem cartões",
906906
columnCards: "Cartões da coluna {{column}}",
907907
laneCards: "Cartões de {{column}} - {{lane}}",
908+
dropBlocked: "Um cartão em {{from}} não pode ser movido para {{to}}.",
908909
cardTitlePlaceholder: "Digite o título do cartão…",
909910
requiredFieldsTitle: "Preencha os campos obrigatórios",
910911
requiredFieldsDescription: "Esta movimentação torna os campos abaixo obrigatórios. Preencha-os para continuar.",

‎ui/packages/i18n/src/locales/ru.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,7 @@ const ru = {
935935
noCards: "Нет карточек",
936936
columnCards: "Карточки в колонке {{column}}",
937937
laneCards: "Карточки: {{column}} - {{lane}}",
938+
dropBlocked: "Карточку из {{from}} нельзя переместить в {{to}}.",
938939
cardTitlePlaceholder: "Введите название карточки…",
939940
requiredFieldsTitle: "Заполните обязательные поля",
940941
requiredFieldsDescription: "Это перемещение делает поля ниже обязательными. Заполните их, чтобы продолжить.",

0 commit comments

Comments
 (0)