Skip to content

feat(ui): the board dims the lanes a card's state machine closes and refuses a drop into one - #26253

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-aetoxp
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-aetoxp

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #25933 — the board half. The edit form half landed in #26033.

Clause-②: yes
Metadata-protocol: none

What changes

On a board grouped by a field that an error-severity state_machine validation governs (the showcase Task Board's status), a backlog card could be dragged onto any column. The server refused the move and the board rolled it back with a toast, which works correctly, and nothing during the drag marked the columns the card could not enter. Now:

  • While a card is dragged, each lane its column's value may not move to is dimmed (opacity-40) and draws no drop highlight, with aria-disabled and data-kanban-drop-blocked on the lane. This covers the flat layout (KanbanColumnView) and every swimlane row's cells.
  • A drop into a closed lane is refused on the client in handleDragEnd, before the optimistic move and before onCardMove, so nothing is written and the card stays in its column. A new onDragCancel handler clears the dimming when a drag is cancelled. Before this PR the board had no cancel handler, and activeCard stayed set after Escape.
  • The gate is a new optional prop, canDropCard(fromColumnId, toColumnId), on KanbanBoardProps (KanbanImpl.tsx), forwarded by KanbanBoardCore.tsx. ObjectKanban.tsx derives it from objectDef.validations through stateMachineNextValues (@object-ui/core, from feat(ui): an edit form's state-machine select offers only the moves the server accepts #26033). A lane is an option value of groupBy, so only a rule on the board's own groupBy field can close a lane, and only one the server enforces on update: severity error (the default), active, events covering update. An undeclared from-state stays open. The Uncategorized lane also stays open, because the mover writes nothing for a drop into it. KanbanRenderer (a host-mounted board) passes no gate and is unchanged.
  • The server still refuses an invalid move, and handleCardMove's rollback stays. No new visible text, so no i18n.

Tests

Run at head c0333981 from ui/ under the verify lock:

  • packages/plugin-kanban/src/ObjectKanban.stateMachineDropGate-25933.test.tsx (new, 6, driven through DndContext's captured handlers like the rollback test). Dragging a backlog card dims only Done. A drop on Done writes nothing, shows no toast, leaves the card in Backlog and clears the dimming. A drop on To Do writes { status: 'todo' }. A cancelled drag clears the dimming. CONTROLS: a warning-severity machine closes no lane and the drop is written, and so does a machine on a field the board does not group by.
  • packages/plugin-kanban/src/ObjectKanban.rejectedMoveRollback.test.tsx: still green, so the server-refusal rollback is untouched.
  • The whole @object-ui/plugin-kanban package: 79 files, 531 tests, all green. pnpm --filter @object-ui/plugin-kanban run type-check (source and test configs) exits 0. Both ran in the same locked run that re-recorded scripts/sdui-manifest.sources/plugin-kanban.json; node scripts/check-sdui-manifest.mjs exits 0 at the head.

Ablations (fix committed first; scripts/ablation-replace.mjs, mutation proven on disk, restored to the HEAD blob each time):

  • canDropCard={undefined} in ObjectKanban.tsx turned 3 pins red: the dimming, the refusal and the cancel. The 3 that expect an open lane stayed green.
  • The handleDragEnd guard replaced by false in KanbanImpl.tsx turned exactly the refusal pin red. The dimming stays driven by the gate, so the refusal is pinned on its own.

Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 47 commands at c0333981. All 47 exited 0 there. --ran reconciliation: 47 derived, 47 run, 0 NOT-MEASURED (a derived zero: every row carries its exit code). Repo-wide scans (pnpm lint, the full check:* farm) and the Console build are left to CI.

Acceptance notes

  • A refused drop is silent to a screen reader. dnd-kit's drop announcement does not speak for cross-column drops (fix(spec): reach all three persisted view spellings in every view-family conversion #13054 moved that to the write's outcome), and with no write there is no outcome sentence. Adding one would be new copy, which this card ruled out. The dimmed lanes carry aria-disabled.
  • The pointer can still hover a closed lane, because collision detection is not filtered. The lane just draws no highlight and the drop is refused. Filtering the droppables in collisionDetection would be a deeper change to the dnd wiring.
  • Like the form, only top-level state_machine rules are read. A machine nested in a conditional rule leaves lanes open, never stricter than the server.

Session: session_015Kzn5VKHAGKe251waHoUqj (card thread, domain:ui#2)


Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT · seat 2 · 2026-10-26T17:14Z · PR #26253 head c0333981 (#25933 PR 2, the board)

check reading
scope 6 files, +345/−7: plugin-kanban (KanbanImpl.tsx, KanbanBoardCore.tsx, ObjectKanban.tsx, new pin file), its sources record, a root .changeset/
contract canDropCard(from, to) is an optional PROP, absent → every lane open as before; KanbanRenderer passes none, so a host-mounted board is unchanged
refusal handleDragEnd refuses a closed cross-lane drop before the optimistic move and before onCardMove; same-column drop is never asked; onDragCancel clears the dimming
derivation ObjectKanban reads stateMachineNextValues(objectDef, groupBy, from) from @object-ui/core (the form's and grid editor's read); null (undeclared from-state) and the Uncategorized lane stay open
visuals blocked lanes: aria-disabled, data-kanban-drop-blocked, opacity-40, no isOver ring, on flat and swimlane layouts; no new visible text
governance check-governed-merges: NOT governed, 352 lines ≤ 3000
landing git merge-tree against origin/main clean; no commit on main touches plugin-kanban or its sources record since base df8aac41; no other open PR in the plugin-kanban line

Ablation evidence from the report (gate unset → 3 red; guard forced false → exactly the refusal red) is accepted as stated. Main's Test UI 48≠47 red was #26218, repaired by #26231 at 2026-10-26T17:10Z. Seat's route: ready + auto-merge.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 17:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 17:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit c14e910 Oct 26, 2026
62 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-aetoxp branch October 26, 2026 18:18
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

Projects

None yet

2 participants