# 通知設計書 5-エントリ保存失敗通知

## 概要

本ドキュメントは、Decap CMSにおけるエントリの保存（永続化）に失敗した場合に表示されるエラートースト通知の設計仕様を定義する。バックエンドへのエントリ永続化API呼び出しがエラーを返した際に、ユーザーに失敗を通知する仕組みの詳細を記載する。

### 本通知の処理概要

本通知は、Decap CMSのエントリ保存機能においてバックエンドへの永続化処理が失敗した場合に発火するエラートースト通知である。`persistEntry`（通常保存）および `persistUnpublishedEntry`（編集ワークフロー経由の保存）の両関数で、`backend.persistEntry()` の呼び出しがPromise rejectされた場合のcatchブロック内で通知が発行される。

**業務上の目的・背景**：コンテンツ編集者がエントリの保存操作を行った際に、バックエンド側でのデータ永続化が何らかの理由で失敗したことをユーザーに伝える。ユーザーの編集内容が失われないよう、再保存の試行やデータのバックアップを促す。

**通知の送信タイミング**：バックエンドの `backend.persistEntry()` または `persistAction.call()` のPromiseがrejectされた時点で即座に通知が発行される。これはバリデーションチェックを通過した後、実際のAPI呼び出し段階でのエラーである。

**通知の受信者**：エントリの保存操作を行った当該ユーザー本人。ブラウザ画面上のトースト通知として表示される。

**通知内容の概要**：i18nキー `ui.toast.onFailToPersist` に基づくメッセージが表示される。英語ロケールでは `Failed to persist entry: %{details}` として、バックエンドから返されたエラーの詳細がメッセージに含まれる。

**期待されるアクション**：ユーザーはエラーメッセージを確認し、再度保存を試みるか、エントリ内容をローカルにコピーしてデータ損失を防ぐか、システム管理者に連絡することが期待される。

## 通知種別

アプリ内通知（トースト通知）

react-toastify ライブラリを使用したブラウザ内トースト通知。画面右上（`top-right`）に `colored` テーマで表示される。

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（Redux dispatch → react-toastify） |
| 優先度 | 高（データ損失のリスクに関連） |
| リトライ | なし（UIトースト通知のため再送不要。バックエンドAPI自体のリトライは別途） |

### 送信先決定ロジック

エントリ保存操作を実行したユーザーのブラウザセッションに対して直接表示する。

## 通知テンプレート

### メール通知の場合

該当なし（本通知はアプリ内トースト通知のみ）

### 本文テンプレート

```
Failed to persist entry: %{details}
```

i18nキー `ui.toast.onFailToPersist` の英語テンプレート。

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| details | 保存失敗の詳細情報 | catchブロックで捕捉されたerrorオブジェクト | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | エントリ保存操作 | `persistEntry` 内で `backend.persistEntry()` がrejectされた場合 | 通常のエントリ保存失敗時（entries.ts 962-975行目） |
| 画面操作 | エントリ保存操作（ワークフロー） | `persistUnpublishedEntry` 内で `persistAction.call()` が例外をスローした場合 | 編集ワークフロー経由の保存失敗時（editorialWorkflow.ts 393-407行目） |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 保存成功時 | APIが正常にresolveした場合は通知を発行しない |
| バリデーションエラー時 | バリデーションエラーでバックエンドへの保存自体が実行されない場合は本通知は発行されない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[ユーザーが保存ボタン押下] --> B[persistEntry / persistUnpublishedEntry 発火]
    B --> C{バリデーションチェック}
    C -->|エラーあり| D[必須フィールド未入力通知 - 処理中断]
    C -->|エラーなし| E[backend.persistEntry 呼び出し]
    E -->|成功| F[エントリ保存成功通知]
    E -->|失敗| G[catch ブロック実行]
    G --> H[console.error でエラーログ出力]
    H --> I[dispatch addNotification - エラートースト表示]
    I --> J[dispatch entryPersistFail / unpublishedEntryPersistedFail]
    J --> K[Promise.reject で呼び出し元に伝播]
    K --> L[Notifications コンポーネントがトースト描画]
    L --> M[8秒後に自動消去]
    M --> N[終了]
    D --> N
    F --> N
```

## データベース参照・更新仕様

### 参照テーブル一覧

該当なし

### 更新テーブル一覧

該当なし（永続化が失敗しているため、データベースの更新は行われていない）

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| バックエンドAPI失敗 | persistEntry APIが500エラー等を返す | エラートースト通知を表示し、entryPersistFail/unpublishedEntryPersistedFailアクションでRedux stateを更新 |
| ネットワークエラー | バックエンドへの接続が失敗 | 同上 |
| 認証エラー | バックエンドが401を返す | 同上 |
| Git操作失敗 | バックエンドがGit操作（commit/push等）に失敗 | 同上 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし |
| リトライ間隔 | 該当なし |
| リトライ対象エラー | 該当なし（ユーザーが手動で再保存を試みる想定） |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | なし |
| 1日あたり上限 | なし |

### 配信時間帯

制限なし

## セキュリティ考慮事項

- エラーオブジェクトが `details` としてそのまま画面に表示されるため、バックエンドが内部情報を含むエラーメッセージを返さないよう注意が必要
- `entries.ts` の `persistEntry` ではエラーが `console.error` にも出力される（963行目）

## 備考

- `persistEntry`（entries.ts）ではcatchブロック内で `console.error(error)` が呼ばれるが、`persistUnpublishedEntry`（editorialWorkflow.ts）ではconsole.errorの呼び出しはない
- 両関数ともcatchブロック内で `Promise.reject()` を返し、呼び出し元にエラーを伝播させる
- `entries.ts` では `entryPersistFail` アクション、`editorialWorkflow.ts` では `unpublishedEntryPersistedFail` アクションでRedux stateの永続化失敗状態が更新される

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | `NotificationMessage` の `details` フィールドの型が `unknown` であることを確認 |

#### Step 2: エントリーポイントを理解する（entries.ts）

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | entries.ts | `packages/decap-cms-core/src/actions/entries.ts` | `persistEntry` 関数の `.catch` ブロック（962-976行目）に注目 |

**主要処理フロー**:
1. **963行目**: `console.error(error)` - エラーログ出力
2. **964-973行目**: `addNotification` でエラートースト通知を発行（i18nキー: `ui.toast.onFailToPersist`、details: error）
3. **974行目**: `Promise.reject(dispatch(entryPersistFail(...)))` - 永続化失敗アクション発行とPromise reject

#### Step 3: 編集ワークフロー側のエントリーポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | editorialWorkflow.ts | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | `persistUnpublishedEntry` 関数のcatchブロック（393-408行目）に注目 |

**主要処理フロー**:
- **394-402行目**: `addNotification` でエラートースト通知を発行
- **404-406行目**: `Promise.reject(dispatch(unpublishedEntryPersistedFail(...)))` - 失敗アクション発行とPromise reject

#### Step 4: i18nメッセージテンプレートを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | index.js (en) | `packages/decap-cms-locales/src/en/index.js` | 271行目: `onFailToPersist: 'Failed to persist entry: %{details}'` |

### プログラム呼び出し階層図

```
persistEntry (entries.ts:893)
    |
    +-- backend.persistEntry({...}) (entries.ts:930)
            |
            +-- [失敗時] .catch (entries.ts:962)
                    |
                    +-- console.error(error)
                    +-- dispatch(addNotification({key:'ui.toast.onFailToPersist', details: error}))
                    +-- Promise.reject(dispatch(entryPersistFail()))

persistUnpublishedEntry (editorialWorkflow.ts:323)
    |
    +-- persistAction.call(backend, {...}) (editorialWorkflow.ts:371)
            |
            +-- [失敗時] catch (editorialWorkflow.ts:393)
                    |
                    +-- dispatch(addNotification({key:'ui.toast.onFailToPersist', details: error}))
                    +-- Promise.reject(dispatch(unpublishedEntryPersistedFail()))
```

### データフロー図

```
[入力]                      [処理]                           [出力]

保存操作               -->  backend.persistEntry()    -->  [成功] newSlug
                           (バックエンド永続化)              [失敗] Error オブジェクト
                                                              |
                                                              v
Error オブジェクト     -->  addNotification()          -->  Redux store
                           (i18nキー: ui.toast.               (notifications配列)
                            onFailToPersist)                    |
                                                              v
notifications          -->  Notifications コンポーネント -->  react-toastify
(Redux state)               (t() でi18n変換)                 トースト UI表示
                                                              (画面右上, 8秒で消去)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| entries.ts | `packages/decap-cms-core/src/actions/entries.ts` | ソース | `persistEntry` 関数が保存失敗時にトースト通知を発行 |
| editorialWorkflow.ts | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | ソース | `persistUnpublishedEntry` 関数が保存失敗時にトースト通知を発行 |
| notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | ソース | 通知アクション作成関数 |
| notifications.ts (reducers) | `packages/decap-cms-core/src/reducers/notifications.ts` | ソース | 通知リデューサー |
| Notifications.tsx | `packages/decap-cms-core/src/components/UI/Notifications.tsx` | ソース | 通知UIコンポーネント |
| index.js (en) | `packages/decap-cms-locales/src/en/index.js` | i18nリソース | 英語ロケール定義 |
