コンテンツにスキップ

データグリッドの一括操作エラー

datagrid-bulk-actions は 1 つのアクションを多数の行に対して実行します。このレシピはその 一部が失敗したときの話で、まず実行セマンティクスを明示することから 始まります。すべてロールバックした操作に「3 件成功、1 件失敗」と書くのは 間違った文だからです。 バージョニングポリシーの 対象です。

102 や 105 を含む選択を Archive してください。クロームは1 行のまま (件数・移動・失敗行だけ表示)で、理由別の内訳はグリッドの横に ドッキングしたパネルReasons (N) レールの奥で待ちます。パネルは ドラッグでもキーボードでもリサイズできます。両方が生きたままなのが 要点で、理由をクリックしても背後の行はそこにあります。

1 つの選択に対して 2 つのアクションがあります。102 / 105 / 108 は 「出荷済み」、107 は「権限なし」です。これらのどれかを通常の行と 一緒に選んでください:

  • Archive(best-effort)は実行してから結果を報告します。失敗行は マーク付きで戻り、レポートが理由別に集約し、再試行は絞り込みリンク になります。
  • Post to ledger(atomic)はまずプリフライトし、阻害行を除いて 実行する選択肢を出します。阻害行を含めたまま実行すると、サーバーは 拒否します — 行は変わらず、選択も残ったままです。
ProductStatus
Product 101Active
Product 102Active
Product 103Active
Product 104Active
Product 105Active
Product 106Active
Product 107Active
Product 108Active
best-effortatomic
適する操作独立した項目(アーカイブ、タグ付け、通知)不変条件のある操作(仕訳、振替、権限)
失敗時の応答200 + 実際に起きたことを反映した行409 / 422 + 行は変更なし
失敗行data-attention="error" と理由でマークステータスは変えないが、阻害行にはマークを付ける
文言「113 succeeded / 87 failed」Nothing was executed(2 件が条件を満たさない)」
選択状態再試行対象は選択されたまま — 再試行可能な失敗を checked で返す保持する — チェックボックスを checked で再描画
リカバリーもう一度アクションを押す(失敗行だけに適用される)原因を直す、または阻害行を除いて再実行

**atomic ブランチでの選択保持は必須です。**ベースレシピの「選択は 構造的にクリアされる」が成り立つのはアクションが実行された場合だけ です。拒否のついでに手作業で選んだ 200 行を消すのは、ユーザーが取り消せ ないデータ損失です。選択の真実はチェックボックスなので、checked で 描画し返すだけで直ります。

**atomic ブランチでも阻害行にはマークを付けます。**以前のルールは 「何も変わっていないのでマークは嘘になる — atomic では一切マークしない」 でした。嘘になるのは「この行は失敗した」という主張です。マークが 言っているのはそれとは別のこと — この行は先に進めないです。これは プリフライトでも、409 の拒否でも、best-effort の失敗後でも等しく真 です。行の性質であって試行の結果ではないからです。選択を変えても 陳腐化しません(「出荷済み」は選択と無関係に真のままです)。

マークがないと、レポートの行リンクを踏んで着地しても他の行と見分けが つかず、レポート唯一の誘導手段が無駄になります。

プリフライトが返すのは行ではなくレポートなので、マークは <template> で包んだアウトオブバンドの行更新として同送します。 atomic で決してやってはいけないのはステータスの変更です。何も 実行していないので、Posted にも Archived にもなりません。

severity は「行が何を必要としているか」

Section titled “severity は「行が何を必要としているか」”

data-attention の severity は、いつ判明したかではなく何を要求して いるかで決めます。そうしないと、行の状況は何も変わっていないのに ボタンを押す前は warning、押した後は error になってしまいます。

severity意味
error直さないと進めない必須未入力、値が不正、状態違い(出荷済み)、権限なし
warning判断が要る。値自体は正しい未来日の出荷日、規定超えの割引 — 確認付き警告の分岐

したがって必須入力チェックはどこに現れても error ですし、プリ フライトの阻害行も error です。「まだエラーではない」のではなく、 「先に進めない行」だからです。

**部分失敗のときも再試行対象は選択されたままにします。**全行を未 チェックで返すと、アクションバーが消え(0 件で隠れる仕様)、ユーザーは 全行の中から失敗行を手作業で選び直すことになります — サーバーが既に 特定している行なのにです。再試行可能な失敗を checked で返せば、 再試行は同じボタンを 1 回押すだけになります。

再試行可能かどうかはサーバーの判断です:

失敗後でチェック?理由
一時的(ロック、上流のタイムアウト、レート制限)はい同じリクエストが成功しうる
成功した行いいえやることが残っていない
恒久的(状態違い、権限なし、入力不正)いいえ再送しても同じエラーになる

両者が混在するときはレポートで言明してください(「3 件は再試行可能、 2 件は先に修正が必要」)。そうしないと、部分的にチェックされたグリッド は不具合に見えます。

該当する全件に対して実行する

Section titled “該当する全件に対して実行する”

行にチェックを付ける方式は、データより先に破綻します。4,873 件が該当する とき、ユーザーがやりたいのは「全部アーカイブ」であり、4,873 個の id は クエリストリングにもフォーム POST にも収まりません。プロキシを通ればなお さらです。

そこでアクションをクエリそのものとして表現できるようにします。

<form method="post" action="/products/bulk">
<input type="hidden" name="f-status" value="open">
<input type="hidden" name="f-ship-from" value="@week-start">
<input type="hidden" name="scope" value="matching">
<input type="hidden" name="count-token" value="ct_9f2c1a">
<button class="hc-button" type="submit" name="action" value="archive">
該当する 4,873 件をすべてアーカイブ
</button>
</form>

scope=matching は「ids ではなくクエリに対して実行せよ」の意味です。 両者は排他で、両方を載せたリクエストはクライアントのバグなので 400 に 値します。

「該当全件をアーカイブ」は目をつぶって押せる操作ではありません。ボタンは 件数を明示し、サーバーは実行前に数え直します。

ケースレスポンス
件数がトークンと一致実行し、通常どおり報告
件数が変わった409 + 変更前後の件数と、新しいトークンを持つボタン
トークンが無い・不明409 — 数え直して再確認

トークンがこの操作を誠実にします。これがないと「4,873 件をすべて」は 実行時点で該当する件数に対して走り、ユーザーが同意した操作とは別物に なります。他人の編集や、深夜に切り替わった相対日付だけで動きます。

制約を 2 つ明記します。クエリ指定の一括操作も他と同様に再認可が必要 です(条件に、そのユーザーがもう読めないものが含まれうるため)。そして 一定の規模を超えたら、同期 POST ではなく進捗つきの非同期ジョブの領分です。

atomic なアクションは実行前に検証します — 確認ステップが実行可否の レポートになります:

18 of 20 rows are executable; 2 are blocked (reason: period closed)
[ Exclude 2 and run 18 ] [ Cancel ]

GET /products/bulk/preflight?ids=…&action=… がこのフラグメントを 返します。「除いて実行」はサーバーが描画した隠し入力で実行可能な id だけを送るため、原子性を保ったままユーザーに前進の道が残ります。 実行できる行が 1 つもないときはレポートがそう告げ、submit を描画 しません — 行き止まりは見えるべきで、無効化されたボタンの謎であって はなりません。

全高の一覧ページ ではクロームが固定で、グリッドが残りを取ります。したがって理由の数に 応じて高さが伸びるレポート領域は、グリッドをゼロまで押し潰します — しかもそのレポートが「直しに行け」と言っている当の行を隠して。

**クロームは O(1)。**データ量に応じて高さが伸びるものは、スクロール 領域かオーバーレイに置く。クロームには置かない。

面は 2 つ。選ぶ基準は「グリッド側に作業があるか」の一問です。

セマンティクス
best-effort — 一部の行は変わったクロームには1 行の要約。失敗行は data-attention="error" と自分のメッセージ行を持ち、失敗行だけ表示 (N) を添える
理由別の内訳グリッドの横のドッキングパネルhc-splitter)— サイドパネルが使うのはの空間で、このレイアウトに余っているのは横
atomic — 何も適用されていないモーダルダイアログ。グリッド側に作業は無く、ユーザーが返すのは判断。塞ぐこと自体がメッセージ

既定は畳んだ状態、ただし畳んでも消えません — 件数(Reasons (5))と 戻り道を持つレールになります。常時開いているパネルは、稀な出来事のために 毎日すべての画面から幅を取り続けます。閉じたら消えるパネルは行き止まりです。 そしてレスポンスは開きません。何が起きたかは要約が既に述べており、 グリッドの幅を渡すかどうかは行を読んでいる人の判断だからです。仕事そのものが トリアージの画面は開いた状態で始めて構いません — それは画面の性質であって、 レポートの性質ではありません。開いているかどうかは作業状態なので、 URL ではなくユーザー単位で覚えます。

<div id="bulk-report" aria-live="polite">
<div class="hc-alert" data-variant="warning" role="status">
<p class="hc-alert__body">
<strong>12 of 40 rows could not be updated.</strong>
<a href="/orders?f-last-result=failed">Show only failed (12)</a> ·
<a href="/orders/bulk/report">Review reasons</a>
</p>
</div>
</div>

**重要なのは「失敗行だけ表示」**です。グリッドそのものをレポートに 変え、条件バーや保存ビューと合成でき、その後の再実行は通常の 全選択→アクションのループになります。200 件失敗したときに成立する形は これだけです。(上のライブデモではこのリンクは例示です — 実アプリでは 失敗フィルタを適用したグリッド URL を指させます。)

5,000 行に散った 12 件の失敗は待ち行列です。だから要約が移動手段も 持ちます — O(1) のままで。

<p class="hc-alert__body">
<strong>12 of 40 rows could not be updated.</strong>
<a href="#row-4903">Previous</a>
<span>Error 3 of 12 — row 137</span>
<a href="#row-5012">Next</a> ·
<a href="/orders?f-last-result=failed">Show only failed (12)</a>
</p>

本物のフラグメントリンクなので Back もキーボードも効きます。 installDatagrid() がフラグメントの指す行にアクティブセルを着地させます。カウンタと 2 つの href はレポートと同じ失敗リストからサーバーが描画するので、行と パネルがずれることはありません。

行を名指しするのは ID、隣に 序数表示します。「137 行目」は失敗を語るときの言い方ですが、序数は ソートや条件で動き、ID は動きません。行番号へ移動?goto=137)は 口頭で伝えられた番号のためのもので、137 行目が今どこにあるかを知って いるのはサーバーだけなので、解決もサーバーが行います。

グリッド上部の aria-live="polite" 領域 1 つで、行と一緒にアウト オブバンドでスワップされます — プリフライトの行更新と同じく <template> で包みます。そうしないと、tbody をターゲットにした スワップに同乗する <div> はテーブルパーサーに里子に出されて テーブルの外へ追い出されます。 csv-import の検証 レポートの型を再利用し、3 つの規則に従います。

  1. 行ではなく理由でグルーピング — 87 件の失敗では理由が行動の 単位です。平坦な一覧はレビューできません。
  2. インライン詳細に上限(理由ごとに 10 行が良い既定値)を設け、 上限と全件リンク(ページか CSV — ただの <a href>)を必ずセットで 出します。上限のない 1 万件のレポートは壊れたページです。
  3. 名指しした行をすべてリンク: 同一ページなら #row-101、別ページ なら /products?focus=101#row-101 — 失敗はページをまたぐもので、 どのページにその行があるかを知っているのはサーバーです。

行リンクをクリックすると行がスクロール表示され、:target でマークされ、 グリッドのアクティブセルが移動 するのでキーボード利用者も到達します。行からは <a href="#bulk-report">Details</a> で戻れます。

best-effort ではレポートに**「Filter to the failed rows」**も出します — ただのフィルタ URL(?f-last-result=failed)で、 datagrid-filter / saved-views と合成 します。再試行は通常の全選択 → アクションのループになり、新しい クライアント状態は不要です。

失敗行の理由は該当セル内に hc-tooltip として 一度だけ描画し、セルが aria-describedby で指します。グリッドはその ようなセルのオーバーフローツールチップを抑制するので、1 つの操作は 1 つの意味だけを運びます。ツールチップは「気になった行を確認する」 ための道具です — 理由をツールチップだけに置いてはいけません。 レビュー可能な面はレポートであり、タッチ・印刷・エクスポートに ホバーはありません。

トーストは見出しであってペイロードではありません。件数とレポートへの 誘導に徹し、失敗が存在する間は自動で消えてはいけません — ペイロードで duration: 0 を送ります(best-effort は warning、 拒否は error)。

プログレッシブエンハンスメント

Section titled “プログレッシブエンハンスメント”

ベースレシピの 303 post/redirect/get がそのまま適用されます。同じ レポートをリダイレクト先ページの先頭に描画してください(フラグメント リンクはネイティブに動きます — アンカーである理由がまさにこれです)。 プリフライトは通常の中間ページになります。

  • レポート領域は丁寧に読み上げられ、フォーカスを奪いません。
  • 行リンクはアクティブセル経由でグリッドにフォーカスを移します — 無言のスクロールだけのジャンプにはなりません。
  • 失敗は決して色だけではありません: トーン理由テキスト レポートの項目。
  • プリフライトの行き止まりは、説明のない無効ボタンではなく理由を 描画します。