# 機能設計書 17-ワークフロー管理

## 概要

本ドキュメントは、Decap CMSにおけるエディトリアルワークフロー管理機能の設計を記述する。Draft / Pending Review / Pending Publishの3段階ステータスによるエントリの公開ワークフロー管理の仕様を網羅する。

### 本機能の処理概要

ワークフロー管理機能は、CMSのエントリ公開プロセスをDraft（下書き）、Pending Review（レビュー待ち）、Pending Publish（公開待ち）の3つのステータスで管理するエディトリアルワークフローを提供する。

**業務上の目的・背景**：コンテンツ公開プロセスにおいて、作成者 → レビュアー → 承認者の多段階承認フローを実現する必要がある。エディトリアルワークフローにより、未完成のコンテンツが誤って公開されることを防ぎ、品質管理プロセスを確保する。

**機能の利用シーン**：設定ファイル（config.yml）で`publish_mode: editorial_workflow`が有効な場合に機能する。エントリ編集画面でのステータス変更、ワークフロー画面でのカンバンボード形式の管理、ドラッグ＆ドロップによるステータス移動に使用される。

**主要な処理内容**：
1. `loadUnpublishedEntries`: バックエンドから全未公開エントリを取得し、Redux状態に格納
2. `loadUnpublishedEntry`: 特定の未公開エントリを取得し、メディアファイルのAssetProxyを復元
3. `updateUnpublishedEntryStatus`: エントリのステータスをバックエンドAPIで更新
4. ステータスの3段階遷移: Draft → Pending Review → Pending Publish（双方向遷移可能）
5. 未公開エントリはGitブランチベースで管理される（PR運用）

**関連システム・外部連携**：Gitバックエンド（GitHub/GitLab/Bitbucket等）のブランチ・PR機能と連携。各バックエンドのAPIを通じてステータス管理が実現される。

**権限による制御**：Open Authoringモードでは、フォーク元リポジトリへの書き込み権限に応じてステータス変更の可否が制御される。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主画面 | Editorial Workflow有効時のステータス変更。updateUnpublishedEntryStatusアクション |
| 8 | ワークフロー画面 | 主画面 | ドラフト・レビュー待ち・公開待ちの3カラムカンバンボードによるエントリステータス管理 |

## 機能種別

ワークフロー管理 - エントリ公開プロセスのステータス管理

## 入力仕様

### 入力パラメータ（updateUnpublishedEntryStatus）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| collection | string | Yes | コレクション名 | - |
| slug | string | Yes | エントリスラッグ | - |
| oldStatus | Status | Yes | 現在のステータス | draft / pending_review / pending_publish |
| newStatus | Status | Yes | 新しいステータス | draft / pending_review / pending_publish |

### ステータス定義

| ステータス | 定数名 | 表示名 | 説明 |
|-----------|--------|--------|------|
| draft | Statues.DRAFT | Draft | 下書き状態 |
| pending_review | Statues.PENDING_REVIEW | Waiting for Review | レビュー待ち状態 |
| pending_publish | Statues.PENDING_PUBLISH | Waiting to go live | 公開待ち状態 |

### 入力データソース

- エントリ編集画面のステータスドロップダウン
- ワークフロー画面のカンバンボード（ドラッグ＆ドロップ）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| entities | Map | コレクション名.スラッグをキーとした未公開エントリのマップ |
| pages | Map | ページネーション情報と未公開エントリのIDリスト |
| status | string | 各エントリの現在のステータス |

### 出力先

- Redux Store（`editorialWorkflow.entities`、`editorialWorkflow.pages`）
- バックエンドAPI（ステータス変更の永続化）

## 処理フロー

### 処理シーケンス

```
■ 未公開エントリ一覧読み込み
1. ワークフロー画面またはアプリ初期化時
   └─ loadUnpublishedEntriesアクションをディスパッチ
2. publish_mode !== EDITORIAL_WORKFLOWの場合は早期リターン
3. UNPUBLISHED_ENTRIES_REQUESTをディスパッチ
4. backend.unpublishedEntries(collections)でバックエンドから取得
5. UNPUBLISHED_ENTRIES_SUCCESSでRedux状態に格納

■ ステータス変更
1. ユーザーがステータスを変更（UIまたはD&D）
2. oldStatus === newStatusの場合は何もしない
3. UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUESTをディスパッチ
4. backend.updateUnpublishedEntryStatus(collection, slug, newStatus)を実行
5. 成功: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS + 通知
6. 失敗: UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE + エラー通知

■ 未公開エントリ読み込み（個別）
1. エントリ編集画面で未公開エントリを開く
2. loadUnpublishedEntryアクションをディスパッチ
3. backend.unpublishedEntry(state, collection, slug)で取得
4. メディアファイルのAssetProxy復元
5. UNPUBLISHED_ENTRY_SUCCESSでRedux状態に格納
6. createDraftFromEntryでドラフト作成
7. EDITORIAL_WORKFLOW_ERRORの場合はリダイレクトして通常エントリとして読み込み
```

### フローチャート

```mermaid
flowchart TD
    A[ステータス変更操作] --> B{oldStatus === newStatus?}
    B -->|Yes| C[何もしない]
    B -->|No| D[STATUS_CHANGE_REQUEST dispatch]
    D --> E[backend.updateUnpublishedEntryStatus]
    E --> F{成功?}
    F -->|Yes| G[STATUS_CHANGE_SUCCESS dispatch]
    G --> H[成功通知]
    F -->|No| I[STATUS_CHANGE_FAILURE dispatch]
    I --> J[エラー通知]

    K[未公開エントリ読み込み] --> L{EDITORIAL_WORKFLOW?}
    L -->|No| M[早期リターン]
    L -->|Yes| N[UNPUBLISHED_ENTRIES_REQUEST]
    N --> O[backend.unpublishedEntries]
    O --> P{成功?}
    P -->|Yes| Q[UNPUBLISHED_ENTRIES_SUCCESS]
    P -->|No| R[UNPUBLISHED_ENTRIES_FAILURE]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-17-01 | ワークフロー有効化 | publish_mode: editorial_workflowが設定されている場合にのみ機能する | config.yml設定 |
| BR-17-02 | 同一ステータスブロック | oldStatusとnewStatusが同じ場合はステータス変更を行わない | updateUnpublishedEntryStatus時 |
| BR-17-03 | 双方向遷移 | ステータスは任意の方向に変更可能（Draft↔Review↔Publish） | ステータス変更時 |
| BR-17-04 | ブランチベース管理 | 未公開エントリはGitブランチとして管理される | バックエンド実装 |
| BR-17-05 | エントリ一覧マイグレーション | loadUnpublishedEntry時にentriesLoadedでない場合、先に一覧を読み込む | loadUnpublishedEntry時 |
| BR-17-06 | ワークフローエラーリダイレクト | EDITORIAL_WORKFLOW_ERRORかつnotUnderEditorialWorkflowの場合、通常エントリとして読み込む | loadUnpublishedEntry時 |

### 計算ロジック

特になし。ステータスの遷移は直接的な値の設定のみ。

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象 | 操作種別 | 概要 |
|-----|------|---------|------|
| loadUnpublishedEntries | Git API | SELECT | 全未公開エントリ（ブランチ/PR）の取得 |
| loadUnpublishedEntry | Git API | SELECT | 特定の未公開エントリの取得 |
| updateUnpublishedEntryStatus | Git API | UPDATE | ステータスの更新（ラベル/メタデータ変更） |

### テーブル別操作詳細

Gitバックエンドにより実装が異なる。GitHub/GitLab等のPRラベルやコミットメッセージでステータスが管理される。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| UNPUBLISHED_ENTRIES_FAILURE | 一覧取得エラー | バックエンドAPIの失敗 | エラー通知、状態リセット |
| UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE | ステータス変更エラー | バックエンドAPIの失敗 | エラー通知、isUpdatingStatus=false |
| EDITORIAL_WORKFLOW_ERROR | ワークフローエラー | エントリがワークフロー管理下にない | 通常エントリとしてリダイレクト |

### リトライ仕様

自動リトライは行わない。ユーザーが再度操作を実行する。

## トランザクション仕様

Reduxの楽観的更新（optimistic update）パターンが使用される。STATUS_CHANGE_REQUESTでisUpdatingStatus=trueを設定し、成功時にステータスを更新、失敗時にisUpdatingStatusをリセットする。

## パフォーマンス要件

- 未公開エントリの一覧取得はアプリ起動時に1回のみ（entriesLoadedチェック）
- ステータス変更はバックエンドAPIの応答時間に依存
- 楽観的更新によりUIのレスポンスは即時

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

- ステータス変更はバックエンドAPIを通じて認証が必要
- Open Authoringモードでは、ステータス変更の権限がリポジトリの権限モデルに依存

## 備考

- エディトリアルワークフローはconfig.ymlの`publish_mode`で`editorial_workflow`が設定されている場合にのみ有効
- `simple`モード（デフォルト）ではワークフロー関連の機能は無効

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | publishModes.ts | `packages/decap-cms-core/src/constants/publishModes.ts` | ステータス定義（7-11行目）とEDITORIAL_WORKFLOW定数（5行目）を確認 |
| 1-2 | editorialWorkflow.ts (reducer) | `packages/decap-cms-core/src/reducers/editorialWorkflow.ts` | 初期状態（Map({ entities, pages })）と状態構造 |

**読解のコツ**: ステータスはOrderedMapで管理されており、`status.get('DRAFT')`で'draft'文字列を取得する形式。entities内のキーは`${collection}.${slug}`形式。

#### Step 2: アクションクリエータを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | editorialWorkflow.ts (action) | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | loadUnpublishedEntries（292-321行目）、loadUnpublishedEntry（240-290行目）、updateUnpublishedEntryStatus（411-450行目） |

**主要処理フロー**:
1. **292-321行目**: `loadUnpublishedEntries` - EDITORIAL_WORKFLOWチェック、entriesLoadedチェック、backend.unpublishedEntries呼び出し
2. **240-290行目**: `loadUnpublishedEntry` - マイグレーション、未公開エントリ取得、AssetProxy復元、EDITORIAL_WORKFLOW_ERRORハンドリング
3. **411-450行目**: `updateUnpublishedEntryStatus` - 同一ステータスチェック、backend.updateUnpublishedEntryStatus呼び出し

#### Step 3: リデューサーの状態管理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | editorialWorkflow.ts (reducer) | `packages/decap-cms-core/src/reducers/editorialWorkflow.ts` | CONFIG_SUCCESSケース（28-35行目）、ステータス変更関連ケース（101-124行目） |

**主要処理フロー**:
- **28-35行目**: CONFIG_SUCCESSで`publish_mode === EDITORIAL_WORKFLOW`の場合にworkflow状態を初期化
- **101-106行目**: STATUS_CHANGE_REQUESTでisUpdatingStatus=true
- **108-118行目**: STATUS_CHANGE_SUCCESSでstatus更新、isUpdatingStatus=false
- **120-124行目**: STATUS_CHANGE_FAILUREでisUpdatingStatus=false

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

```
ワークフロー画面 / エントリ編集画面
    │
    ├─ loadUnpublishedEntries(collections)  [actions/editorialWorkflow.ts:292-321]
    │      ├─ EDITORIAL_WORKFLOWチェック
    │      ├─ dispatch(UNPUBLISHED_ENTRIES_REQUEST)
    │      └─ backend.unpublishedEntries(collections)
    │             └─ dispatch(UNPUBLISHED_ENTRIES_SUCCESS)
    │
    ├─ loadUnpublishedEntry(collection, slug)  [actions/editorialWorkflow.ts:240-290]
    │      ├─ マイグレーション: loadUnpublishedEntries
    │      ├─ dispatch(UNPUBLISHED_ENTRY_REQUEST)
    │      ├─ backend.unpublishedEntry(state, collection, slug)
    │      ├─ createAssetProxy（メディア復元）
    │      ├─ dispatch(UNPUBLISHED_ENTRY_SUCCESS)
    │      └─ createDraftFromEntry(entry)
    │
    └─ updateUnpublishedEntryStatus(collection, slug, oldStatus, newStatus)
           [actions/editorialWorkflow.ts:411-450]
           ├─ oldStatus === newStatus チェック
           ├─ dispatch(STATUS_CHANGE_REQUEST)
           └─ backend.updateUnpublishedEntryStatus(collection, slug, newStatus)
                  └─ dispatch(STATUS_CHANGE_SUCCESS / FAILURE)
```

### データフロー図

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

config.yml                ───▶  CONFIG_SUCCESS               ───▶  editorialWorkflow状態初期化
(publish_mode)                   └─ EDITORIAL_WORKFLOW判定

バックエンドAPI           ───▶  loadUnpublishedEntries       ───▶  editorialWorkflow.entities
(ブランチ/PR一覧)                └─ backend.unpublishedEntries       editorialWorkflow.pages

ステータス変更操作        ───▶  updateUnpublishedEntryStatus ───▶  entities[key].status更新
(oldStatus, newStatus)           └─ backend.updateUnpublishedEntryStatus
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| editorialWorkflow.ts (action) | `packages/decap-cms-core/src/actions/editorialWorkflow.ts` | ソース | ワークフロー関連アクションクリエータ |
| editorialWorkflow.ts (reducer) | `packages/decap-cms-core/src/reducers/editorialWorkflow.ts` | ソース | ワークフロー状態管理リデューサー |
| publishModes.ts | `packages/decap-cms-core/src/constants/publishModes.ts` | ソース | ステータス定数定義 |
| entryDraft.js | `packages/decap-cms-core/src/reducers/entryDraft.js` | ソース | ドラフト状態リデューサー（isUpdatingStatus管理） |
| entries.ts (action) | `packages/decap-cms-core/src/actions/entries.ts` | ソース | createDraftFromEntry、entryDeleted（ワークフローから参照） |
