# 通知設計書 4-エントリ保存成功通知

## 概要

本ドキュメントは、Decap CMSにおけるエントリの保存が正常に完了した場合に表示される成功トースト通知の設計仕様を定義する。コンテンツ編集画面でユーザーがエントリを保存し、バックエンドへの永続化が成功した際にフィードバックとして表示される。

### 本通知の処理概要

本通知は、Decap CMSのエントリ編集機能においてエントリの保存（永続化）が成功した場合に発火する成功トースト通知である。`persistEntry`（通常保存）および `persistUnpublishedEntry`（編集ワークフロー経由の保存）の両関数で、バックエンドの `persistEntry` API呼び出しが正常に完了した直後に通知が発行される。

**業務上の目的・背景**：コンテンツ編集者がエントリの保存操作を行った後、その操作が正常に完了したことを即座にフィードバックする。保存が成功したことを視覚的に確認できることで、編集者は安心して次の作業に移ることができる。

**通知の送信タイミング**：バックエンドの `backend.persistEntry()` または `backend.persistUnpublishedEntry()` のPromiseが正常にresolveした直後に通知が発行される。

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

**通知内容の概要**：i18nキー `ui.toast.entrySaved` に基づくメッセージが表示される。英語ロケールでは `Entry saved` という簡潔な固定メッセージ。

**期待されるアクション**：ユーザーは保存成功を確認し、引き続き編集を行うか、別のエントリの編集に移るか、エントリ一覧に戻る。特別な対応は不要。

## 通知種別

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

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

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（Redux dispatch → react-toastify） |
| 優先度 | 中（成功フィードバック） |
| リトライ | なし（UIトースト通知のため再送不要） |

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

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

## 通知テンプレート

### メール通知の場合

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

### 本文テンプレート

```
Entry saved
```

i18nキー `ui.toast.entrySaved` の英語テンプレート。動的変数を含まない固定メッセージ。

### 添付ファイル

該当なし

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| （なし） | 本通知には動的変数がない | - | - |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| 画面操作 | エントリ保存操作 | `persistEntry` 内で `backend.persistEntry()` が正常に完了 | 通常のエントリ保存成功時（entries.ts 938-947行目） |
| 画面操作 | エントリ保存操作（ワークフロー） | `persistUnpublishedEntry` 内で `persistAction.call()` が正常に完了 | 編集ワークフロー経由の保存成功時（editorialWorkflow.ts 378-385行目） |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 保存失敗時 | バックエンドAPIがrejectした場合は成功通知を発行せず、代わりにエントリ保存失敗通知（No.5）が発行される |
| バリデーションエラー時 | fieldsErrorsが存在する場合はバックエンドへの保存自体が実行されないため、成功通知も発行されない |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[ユーザーが保存ボタン押下] --> B[persistEntry / persistUnpublishedEntry 発火]
    B --> C{バリデーションチェック}
    C -->|エラーあり| D[必須フィールド未入力通知 - 処理中断]
    C -->|エラーなし| E[backend.persistEntry 呼び出し]
    E -->|成功| F[dispatch addNotification - 成功トースト表示]
    E -->|失敗| G[エントリ保存失敗通知]
    F --> H[メディアライブラリ再読み込み - assetProxiesがある場合]
    H --> I[dispatch entryPersisted]
    I --> J[Notifications コンポーネントがトースト描画]
    J --> K[4秒後に自動消去]
    K --> L[終了]
    D --> L
    G --> L
```

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

### 参照テーブル一覧

該当なし（本通知はクライアントサイドのRedux storeのみを使用）

### 更新テーブル一覧

該当なし（バックエンドへの永続化処理自体は別途実行されるが、通知の発行に関してはデータベース更新はない）

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| 該当なし | 本通知は成功時のみ発行されるため、エラーケースは存在しない | - |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | なし |
| リトライ間隔 | 該当なし |
| リトライ対象エラー | 該当なし |

## 配信設定

### レート制限

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

### 配信時間帯

制限なし

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

- 固定メッセージのみが表示されるため、セキュリティ上の懸念はない

## 備考

- 成功通知の自動消去時間は4000ms（4秒）で、エラー通知（8000ms）よりも短い
- `persistEntry` の成功後、エントリにメディアファイルが含まれる場合（`assetProxies.length > 0`）はメディアライブラリの再読み込みが実行される（entries.ts 950-952行目）
- 新規エントリの場合（`entry.get('slug') !== newSlug`）、保存成功後にエントリの再読み込みとURL遷移が行われる（entries.ts 957-959行目）
- `persistUnpublishedEntry` では `existingUnpublishedEntry` フラグによって `backend.persistUnpublishedEntry` と `backend.persistEntry` のいずれかが呼ばれる（editorialWorkflow.ts 366-368行目）

---

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

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

### 推奨読解順序

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

成功通知のペイロード構造を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | notifications.ts (actions) | `packages/decap-cms-core/src/actions/notifications.ts` | `NotificationPayload` の `type` フィールドに `'success'` が指定される点を理解する |

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | entries.ts | `packages/decap-cms-core/src/actions/entries.ts` | `persistEntry` 関数（893-977行目）。特に成功時の `.then` ブロック（938-960行目）に注目。 |

**主要処理フロー**:
1. **930行目**: `backend.persistEntry({...})` 呼び出し
2. **938行目**: `.then(async (newSlug) => {...})` 成功コールバック開始
3. **939-946行目**: `addNotification` で成功トースト通知を発行（type: 'success', dismissAfter: 4000）
4. **950-952行目**: メディアファイルがある場合にメディアライブラリ再読み込み
5. **953行目**: `entryPersisted` アクション発行

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | editorialWorkflow.ts | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | `persistUnpublishedEntry` 関数（323-409行目）。成功時のtryブロック（378-385行目）に注目。 |

**主要処理フロー**:
- **371行目**: `persistAction.call(backend, {...})` 呼び出し
- **378-385行目**: `addNotification` で成功トースト通知を発行
- **387行目**: `unpublishedEntryPersisted` アクション発行

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | index.js (en) | `packages/decap-cms-locales/src/en/index.js` | 275行目: `entrySaved: 'Entry saved'` |

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

```
persistEntry (entries.ts:893)
    |
    +-- バリデーションチェック (entries.ts:901)
    |
    +-- backend.persistEntry({...}) (entries.ts:930-937)
            |
            +-- [成功時] .then (entries.ts:938)
            |       |
            |       +-- dispatch(addNotification({key:'ui.toast.entrySaved', type:'success'}))
            |       +-- dispatch(loadMedia()) [メディアファイルがある場合]
            |       +-- dispatch(entryPersisted())
            |       +-- dispatch(loadEntry()) / navigateToEntry() [新規スラグの場合]
            |
            +-- [失敗時] .catch (entries.ts:962)
                    +-- エントリ保存失敗通知

persistUnpublishedEntry (editorialWorkflow.ts:323)
    |
    +-- persistAction.call(backend, {...}) (editorialWorkflow.ts:371)
            |
            +-- [成功時] (editorialWorkflow.ts:378)
            |       +-- dispatch(addNotification({key:'ui.toast.entrySaved', type:'success'}))
            |       +-- dispatch(unpublishedEntryPersisted())
            |
            +-- [失敗時] (editorialWorkflow.ts:393)
                    +-- エントリ保存失敗通知
```

### データフロー図

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

保存ボタン押下         -->  backend.persistEntry()    -->  [成功] newSlug
                           (バックエンド永続化)              |
                                                              v
(固定メッセージ)       -->  addNotification()          -->  Redux store
                           (i18nキー: ui.toast.               (notifications配列)
                            entrySaved, type:'success')        |
                                                              v
notifications          -->  Notifications コンポーネント -->  react-toastify
(Redux state)               (t() でi18n変換)                 トースト UI表示
                                                              (画面右上, 4秒で消去)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| 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リソース | 英語ロケール定義 |
