Skip to content

bug: fix cancel action for kanban #4183

Closed

Description

@KATO-Hiro
No description provided.

Activity

  1. added theissue type on Oct 18, 2026
  2. KATO-Hiro commented on Oct 18, 2026

    @KATO-Hiro
    CollaboratorAuthor

    dnd-kit 0.5.1 の Kanban 実操作確認とドラッグ終了処理の修正

    概要

    管理画面 Kanban(src/routes/(admin)/workbooks/order/)について、次の 2 つを行った。

    1. dnd-kit 0.5.0 → 0.5.1 の手動確認:#4165 は、継続ログで「実操作確認を条件に反映可」と判定されていた。2026-10-18 に staging へマージ済み。
    2. ドラッグ終了処理の不具合修正:確認中に見つかった既存の不具合 C1・C2 と、snapshot の競合を直す。0.5.1 とは無関係なので、fix(deps): update dnd-kit monorepo to v0.5.1 #4165 には混ぜず #4183(ブランチ #4183)で扱う。

    状況(2026-10-18 時点)

    Phase 内容 状況
    1・2 0.5.0 と 0.5.1 での手動確認(コード変更なし) M1〜M9 をローカルで確認し、#4165 はマージ済み。C1・C2 が予想どおり起きることを確認
    3 判定を純関数 resolveDragEndAction に切り出し、テストを先行 完了
    4 KanbanBoard.svelte への組み込み 完了(format・lint・check・unit test とローカルでの手動確認)

    不具合と原因

    • C1:Escape でキャンセルしても、移動後の並びが保存される。 onDragOver が move() でアプリの状態を書き換えるため、dnd-kit はキャンセル時の DOM 復元を行わない(@dnd-kit/dom の sortable.js の dragend 監視で、hasChanged が真なら何もせず return する。0.5.0・0.5.1 とも同じ)。さらに旧 onDragEnd は event.canceled を見ずに差分を保存していた。
    • C2:列の外で離すと、表示だけ移動後の並びになり、DB は元のまま残る。 列の外では target が無い。旧 onDragEnd は target が無いと何もせず return したが、表示は onDragOver の時点ですでに移動後の並びになっていた。
    • snapshot の競合:旧コードは snapshot を保存完了後の finally で null にしていた。保存待ちの間に次のドラッグを始めると、前のドラッグの finally が新しい snapshot を消す。次の保存は reCalcPriorities({}, …) になってタブ内の全カードを更新し、失敗しても元に戻せない。

    意思決定

    • キャンセルされたら、ドラッグ開始時の snapshot に戻す。 dnd-kit の DOM 復元には頼れないため(C1 の原因を参照)。
    • 列の外で離したら、表示どおりに保存する(2026-10-18 決定)。 離した時点のカードは最後に重ねた位置に見えているので、それを保存すれば表示と DB が一致する。取り消したいときは Escape を使う。この決定により、target が無いときの早期 return は削除した。
    • snapshot は await より前に null へ戻す。 同じ関数の同じ変数の扱いで、直すコストがほぼないため同じ PR に含めた。
    • 「何もしない・元に戻す・保存する」の判定を純関数にする。 DnD の経路には自動テストが無く、jsdom ではドラッグを動かせない。分岐を純関数にすれば unit テストで固定できる。
    • 判定関数の引数は、dnd-kit のイベント型ではなく canceled: boolean にする。 テストで dnd-kit のクラスを偽装せずに済み、util が dnd-kit の型に依存しない。

    却下した案

    • fix(deps): update dnd-kit monorepo to v0.5.1 #4165 に修正を同梱する:依存更新と挙動修正が混ざり、切り戻しの単位が崩れる。
    • コンポーネントに if (event.canceled) を足すだけにする:分岐をテストで固定できない。
    • dnd-kit のキャンセル時の DOM 復元に任せる:アプリの状態が変わっていると復元されない。
    • 列の外で離したら元に戻す:誤って離したときに保存されない利点はある。ただ、最後に重ねた位置に見えているカードが、離した後に元の位置へ戻るので、見た目と結果が食い違う。取り消しは Escape で足りる。
    • onDragOver で canceled を見る:move() はキャンセル時に items をそのまま返すので意味がない(@dnd-kit/helpers の mutate)。
    • move() を onDragEnd だけで呼び、snapshot と復元をなくす:理由は 2 つ。1 つ目は、{#each} の DOM と OptimisticSortingPlugin が食い違って固まること(2026-02-28 のメモ「@dnd-kit/svelte 実装上の注意点」)。2 つ目は C2 が悪化すること。列の外で離すと target が無く move() は何もしないが、dnd-kit はキャンセル時しか DOM を戻さないので、画面だけ移動後の並びになる。
    • ドラッグ操作の e2e を足す:マウス操作は過去に不安定で断念しており、CI でも e2e は動いていない。キーボード操作の e2e も今回は見送り。

    既知の課題(#4183 では扱わない)

    • 保存失敗時の復元が、後続のドラッグの結果を上書きする:保存 A の待機中にドラッグ B を始め、その後 A が失敗すると、A のドラッグ前の並びで B の結果を上書きする。B が保存済みなら、表示と DB がずれる。以前からある競合で、snapshot の修正では解消しない。保存の失敗と保存待ち中のドラッグが重なったときだけ起きるので、記録のみとした。直す場合は、保存が終わるまで次のドラッグを始められないようにするのが最小。

    レビュー(2026-10-18)

    • Codex:Critical・Severe は無し。Moderate の 1 件は「既知の課題」に記録した。Minor の「並びが変わっていないキャンセルでも restore を返す」テストの追加は見送った。この場合は元に戻しても戻さなくても画面が同じで、価値が低いため。復元で dnd-kit の並びがずれたり固まったりしないかはコードから判断できないとされたので、Phase 4 の手動確認で見る。
    • simplify(4 観点):Reuse・Simplification の指摘を受け、テストで 3 か所に重複していた並びを fixture solutionColumnsGraphSwapped に切り出した。Efficiency・Altitude は指摘なし。Altitude の補足を受けて、「move() を onDragEnd だけで呼ぶ」案を却下理由とともに追記した。

    手動確認

    自動テストで代替できない理由

    • CI:e2e のステップはコメントアウトされている(.github/workflows/ci.yml の TODO: Run integration test)。
    • e2e:e2e/workbook_order.spec.ts は並べ替えを API への直接 POST で検証しており、ドラッグを通らない。
    • unit:jsdom には画面配置の計算が無い(getBoundingClientRect が 0 を返す)ので、dnd-kit の重なり判定や並び位置の計算を動かせない。
    • 見た目:半透明・ハイライト・自動スクロール・素早い操作への追従は、テストで判定すること自体が難しい。

    環境

    • 保存を伴う確認はローカルで行い、共有 DB には書き込まない。ローカル DB なので、確認後に並びを戻す必要はない。
    • データは pnpm db:seed で placement が作られる。未配置の問題集があれば、画面の初期化ボタンで作る。カードが 2 枚以上ある列と、スクロールが出るほどカードの多い列が必要。
    • 管理者でログインする。ブラウザーは普段使うもの 1 つでよい。
    • devcontainer で pnpm i が registry.npmjs.org からの取得で止まった場合は、sudo /usr/local/bin/init-firewall.sh で許可リストを作り直す。ファイアウォールは起動時に 1 回だけ DNS を引いて IP を許可するので、そのとき一部の IP しか返らないと、他の IP への接続が拒否される(推定。ipset の中身は未確認)。

    dnd-kit 更新時の確認項目

    ID 操作 期待
    M1 列をまたいで移動(未分類 → グラフ)し、リロード ドラッグ中に移動先の列が濃いグレー、掴んだカードが半透明になる。固まらない。リロード後も同じ
    M2 同じ列の中で並べ替え、リロード 挿入位置が正しく、リロード後も同じ
    M3 カードの余白(ラベル付近)を掴み、別カードのタイトル上で離す 離した直後に問題集の詳細ページへ移動しない
    M4 タイトルリンクを普通にクリック 詳細ページへ移動する
    M5 動かしたカードを続けて再ドラッグ(往復、3 回以上) カードがポインターに追従し、位置がずれない。挿入位置が正しい
    M6 キーボード:Tab でフォーカス → Space / Enter で掴む → 矢印で移動 → Space / Enter で確定 各キーが反応し、確定後にリロードしても同じ
    M7 タブ切替(解法別 ⇄ カリキュラム)の後に M1・M5 M1・M5 と同じ
    M8 カードの多い列で上下端へ、級列の多いカリキュラムタブで左右端へドラッグ 自動スクロールが効き、離した位置に入る
    M9 掴んですぐ素早く動かす カードがポインター位置に追いつく

    0.5.1 で M1〜M9 を選んだ根拠は、上流の変更との対応である。M1・M2 は #2135(Svelte 版の状態購読)、M3・M4 は #2152(ドロップ直後のクリック)、M5 は #2149・#2142、M6 は #2094(キー判定を event.key へ変更)、M7 は #2142、M8 は #2126、M9 は #2151 に対応する。NG が出たら旧バージョンでも同じ操作を行い、退行かどうかを切り分ける。回避用のコードは足さない。

    #4183 の確認項目(Phase 4)

    • C1:Escape 後に表示が元に戻り、リロード後も元の並びのまま。ポインター操作中とキーボード操作中の両方で確認する。
    • C2:列の外で離した後、リロードしても離した時点の表示と同じ並びになる。
    • キャンセル直後の再ドラッグ:同じカードをすぐ再ドラッグでき、固まらない。
    • 通常のドラッグ:M1・M2・M5 が従来どおり保存される。
    • 保存失敗時の復元:DevTools でオフラインにしてドラッグすると、エラー表示が出て元の並びに戻る。

    確認結果

    ID 修正前 修正後(#4183) メモ
    C1 保存される(予想どおり) OK 2026-10-18
    C2 表示と DB がずれる(予想どおり) OK 2026-10-18
    再ドラッグ — OK 2026-10-18
    M1 OK OK 2026-10-18
    M2 OK OK 2026-10-18
    M5 OK OK 2026-10-18
    保存失敗の復元 — OK 2026-10-18

    修正前の M1〜M9 は、0.5.1(#4165)でローカルで確認した。この表に無い M3・M4・M6〜M9 も同じ確認に含まれる。

    修正後の確認は、#4183 のブランチでローカルで行った。

    確認日:2026-10-18

  3. added a commit that references this issue on Oct 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions