# 機能設計書 60-ファイルウィジェット

## 概要

本ドキュメントは、Decap CMSにおけるファイルウィジェット（File Widget）の設計を記述する。ファイルアップロード機能を提供し、メディアライブラリと連携してファイルの選択・アップロード・削除を行うウィジェットである。

### 本機能の処理概要

**業務上の目的・背景**：CMSのコンテンツにはPDF・ドキュメント・画像など各種ファイルを添付する需要がある。本ウィジェットはメディアライブラリとの連携により、ファイルの選択・アップロード・URL指定・削除をUIから行えるようにする。単一ファイルと複数ファイルの両方に対応し、複数ファイル時はドラッグ＆ドロップによる並べ替えも可能。画像ウィジェット（Image Widget）の基盤としても使用される（withFileControl HOCパターン）。

**機能の利用シーン**：エントリ編集画面・エントリ新規作成画面において、`widget: file`が指定されたフィールドに対してファイル選択UIをレンダリングする。メディアライブラリダイアログの呼び出し、URL直接入力、選択済みファイルの表示・削除を行う。

**主要な処理内容**：
1. メディアライブラリとの連携によるファイル選択・アップロード
2. URL直接入力（choose_url設定時）
3. 単一/複数ファイルモードの切り替え
4. 選択済みファイルのリンク表示（テキスト省略表示付き）
5. 画像ウィジェットモードでの画像プレビュー表示
6. 複数ファイル時のドラッグ＆ドロップ並べ替え（@dnd-kit使用）
7. 個別ファイルの削除・差し替え
8. mediaPathsを介したメディアライブラリとの双方向同期

**関連システム・外部連携**：メディアライブラリ（機能No.25）、@dnd-kit（ドラッグ＆ドロップ）、decap-cms-ui-default（UIコンポーネント）、decap-cms-lib-util（basename関数）

**権限による制御**：field.private設定によるプライベートアップロード対応。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主機能 | ファイルフィールドの選択・アップロード |
| 7 | エントリ新規作成画面 | 主機能 | ファイルフィールドの選択・アップロード |
| 9 | メディアライブラリ画面 | 連携先 | メディアライブラリからのファイル選択 |

## 機能種別

UI入力コンポーネント / ファイルアップロード

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| field | object | Yes | フィールド設定 | - |
| getAsset | function | Yes | アセット取得関数 | - |
| mediaPaths | ImmutableMap | Yes | メディアパスの状態マップ | - |
| onAddAsset | function | Yes | アセット追加関数 | - |
| onChange | function | Yes | 値変更時のコールバック | - |
| onRemoveInsertedMedia | function | Yes | 挿入メディア除去関数 | - |
| onOpenMediaLibrary | function | Yes | メディアライブラリオープン関数 | - |
| onClearMediaControl | function | Yes | メディアコントロールクリア関数 | - |
| onRemoveMediaControl | function | Yes | メディアコントロール除去関数 | - |
| classNameWrapper | string | Yes | ラッパーのCSSクラス名 | - |
| value | string / string[] / ImmutableList | No | ファイルパス（デフォルト: ''） | - |
| t | function | Yes | ローカライズ関数 | - |

### フィールド設定（config.yml）

| 設定名 | 型 | 説明 |
|--------|-----|------|
| private | boolean | プライベートアップロードフラグ |
| choose_url | boolean | URL直接入力の可否（デフォルト: true） |
| media_library | object | メディアライブラリ設定（config.multiple等） |
| allow_multiple | boolean | 複数ファイル許可 |

### 入力データソース

- エディタのコントロールペインから提供されるprops
- Reduxストアのmediapathsマップ
- メディアライブラリダイアログからのファイルパス

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| value | string / ImmutableList / null | ファイルパス文字列（複数時はリスト、削除時はnullまたは空文字列） |

### 出力先

onChangeコールバック経由でReduxストアのエントリドラフトに反映される。

## 処理フロー

### 処理シーケンス

```
1. コンポーネントマウント
   └─ PropTypesの手動バリデーション
   └─ controlIDの生成（uuid v4）
2. ファイル選択（handleChange）
   └─ onOpenMediaLibraryでメディアライブラリダイアログを開く
   └─ controlID, forImage, privateUpload, allowMultiple, config等を渡す
3. メディアライブラリからのパス受信（componentDidUpdate）
   └─ mediaPaths.get(controlID)でパスを取得
   └─ mediaPath !== value の場合、onChange(mediaPath)
   └─ mediaPath === value の場合、onRemoveInsertedMedia(controlID)
4. URL直接入力（handleUrl）
   └─ window.prompt()でURLを入力
   └─ onChange(url)
5. ファイル削除（handleRemove）
   └─ onClearMediaControl(controlID)
   └─ onChange('')
6. 個別ファイル削除（onRemoveOne）
   └─ value配列からindex位置を削除
   └─ 残りがあればonChange([...remaining])、なければonChange(null)
7. 個別ファイル差し替え（onReplaceOne）
   └─ onOpenMediaLibrary with replaceIndex
8. ドラッグ＆ドロップ並べ替え（onSortEnd）
   └─ arrayMoveでリスト並べ替え → onChange
9. コンポーネントアンマウント
   └─ onRemoveMediaControl(controlID)
```

### フローチャート

```mermaid
flowchart TD
    A[FileControl レンダリング] --> B{値あり?}
    B -->|Yes| C[renderSelection]
    B -->|No| D[renderNoSelection]
    C --> E{forImage?}
    E -->|Yes| F[画像プレビュー表示]
    E -->|No| G[ファイルリンク表示]
    D --> H[Choose / Choose Multiple ボタン]
    H --> I[onOpenMediaLibrary]
    I --> J[メディアライブラリダイアログ]
    J --> K[mediaPaths更新]
    K --> L[componentDidUpdate]
    L --> M["onChange(mediaPath)"]
    C --> N[Choose Different / Add More ボタン]
    N --> I
    C --> O[Remove ボタン]
    O --> P["onChange('')"]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-60-01 | controlID | uuid v4で一意のコントロールIDを生成し、メディアライブラリとの通信に使用 | コンストラクタ時 |
| BR-60-02 | メディアパス同期 | componentDidUpdateでmediaPaths[controlID]を監視し、新しいパスが設定されたらonChangeを呼ぶ | 常時 |
| BR-60-03 | URL入力 | choose_url設定（デフォルトtrue）が有効な場合、window.prompt()によるURL直接入力を許可 | choose_url !== false |
| BR-60-04 | 複数ファイル判定 | media_library.config.multipleがtrueの場合に複数ファイルモード | allowsMultiple判定時 |
| BR-60-05 | テキスト省略 | 50文字を超えるファイルパスは前半25文字 + ... + 後半24文字で省略表示 | リンク表示時 |
| BR-60-06 | バリデーション値 | getValidateValueでバリデーション時はbasenameのみを返す | isValid評価時 |
| BR-60-07 | 非推奨オプション | field.options.media_libraryは非推奨。field.media_libraryを使用すべき | options.media_library使用時に警告 |
| BR-60-08 | 画像モード | withFileControl({ forImage: true })で画像ウィジェットモード。画像プレビューとチェッカーボード背景を表示 | forImage=true |
| BR-60-09 | 個別削除/差し替え | 複数画像モードでは各画像に削除ボタンと差し替えボタンを表示 | forImage && multiple |
| BR-60-10 | アンマウントクリーンアップ | componentWillUnmountでonRemoveMediaControl(controlID)を呼び出し | アンマウント時 |

### 計算ロジック

- テキスト省略: `${value.slice(0, 25)}...${value.slice(-24)}`（MAX_DISPLAY_LENGTH = 50）

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

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

該当なし（メディアライブラリ経由でのファイル操作は他の機能で管理される）。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | PropType警告 | 必須propsが未提供の場合 | React 19対応のためcomponentDidMountで手動バリデーション |

### リトライ仕様

該当なし。

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

該当なし。

## パフォーマンス要件

- shouldComponentUpdateで`value`と`getAsset`の変更時、または`mediaPaths[controlID]`が新しい値を持つ場合のみ再レンダリング

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

- field.private設定によりプライベートアップロードに対応
- URL直接入力（window.prompt）は任意のURLを受け付けるため、信頼性の確認が必要

## 備考

- withFileControl HOCパターンにより、ファイルウィジェットと画像ウィジェットで共通のコードを再利用
- `withFileControl()`がファイルウィジェット、`withFileControl({ forImage: true })`が画像ウィジェット
- @dnd-kitによるドラッグ＆ドロップは複数画像モードで使用
- 非推奨のoptions.media_library設定に対してonce()でワンショット警告を表示

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `packages/decap-cms-widget-file/src/schema.js` | スキーマ定義: allow_multiple（boolean） |
| 1-2 | index.js | `packages/decap-cms-widget-file/src/index.js` | withFileControl()でcontrolComponentを生成。Widget名は'file'。withFileControlもエクスポート |

**読解のコツ**: withFileControlはHOC（Higher-Order Component）パターン。引数`{ forImage }`で画像モードを切り替える。ファイルウィジェット: `withFileControl()`、画像ウィジェット: `withFileControl({ forImage: true })`。

#### Step 2: HOCの構造を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | withFileControl.js | `packages/decap-cms-widget-file/src/withFileControl.js` | withFileControl関数全体（230-499行目）。FileControlクラスを返すHOC |

**主要処理フロー**:
1. **33行目**: MAX_DISPLAY_LENGTH = 50（テキスト省略の閾値）
2. **188-190行目**: isMultiple - Array/List判定のユーティリティ
3. **208-219行目**: valueListToSortableArray - ドラッグ＆ドロップ用のソート可能配列変換
4. **221-228行目**: warnDeprecatedOptions - 非推奨オプション警告（once）
5. **230行目**: withFileControl({ forImage }) HOC定義開始
6. **255-258行目**: コンストラクタ - uuid v4でcontrolIDを生成
7. **265-283行目**: shouldComponentUpdate - value/getAsset/mediaPaths変更判定
8. **285-293行目**: componentDidUpdate - mediaPaths[controlID]の監視とonChange/onRemoveInsertedMedia呼び出し
9. **299-313行目**: handleChange - onOpenMediaLibraryでメディアライブラリを開く
10. **315-323行目**: handleUrl - window.promptでURL入力
11. **325-329行目**: handleRemove - ファイル削除
12. **331-335行目**: onRemoveOne - 個別ファイル削除
13. **337-351行目**: onReplaceOne - 個別ファイル差し替え
14. **353-362行目**: getMediaLibraryFieldOptions - 非推奨オプションのフォールバック処理
15. **387-398行目**: renderFileLink - パス省略表示（50文字超で中略）
16. **444-470行目**: renderSelection - 値あり時のUI（ファイルリンク/画像プレビュー + ボタン群）
17. **472-486行目**: renderNoSelection - 値なし時のUI（Choose/ChooseURLボタン）

#### Step 3: 画像表示コンポーネントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | withFileControl.js | `packages/decap-cms-widget-file/src/withFileControl.js` | SortableMultiImageWrapper（103-155行目）: DndContext/SortableContext/SortableImageの組合せ。SortableImage（77-101行目）: useSortableフック使用 |

#### Step 4: プレビューを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | FilePreview.js | `packages/decap-cms-widget-file/src/FilePreview.js` | 単一ファイル/複数ファイルのリンク表示。getAssetでURLを取得 |

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

```
DecapCmsWidgetFile (index.js)
    |
    +-- Widget()
    |       └── { name: 'file', controlComponent, previewComponent, schema, withFileControl }
    |
    +-- withFileControl() → FileControl (withFileControl.js)
    |       +-- constructor() - controlID生成（uuid v4）
    |       +-- componentDidMount() - PropTypes検証
    |       +-- shouldComponentUpdate() - value/getAsset/mediaPaths変更判定
    |       +-- componentDidUpdate() - mediaPaths同期
    |       +-- componentWillUnmount() - クリーンアップ
    |       +-- handleChange() - onOpenMediaLibrary呼び出し
    |       +-- handleUrl() - URL直接入力（window.prompt）
    |       +-- handleRemove() - ファイル削除
    |       +-- onRemoveOne() - 個別削除
    |       +-- onReplaceOne() - 個別差し替え
    |       +-- onSortEnd() - ドラッグ＆ドロップ並べ替え
    |       +-- renderSelection() / renderNoSelection()
    |       |       +── renderImages() - 画像プレビュー
    |       |       |       └── SortableMultiImageWrapper
    |       |       |               └── SortableImage (@dnd-kit)
    |       |       └── renderFileLinks() - ファイルリンク表示
    |       └── getMediaLibraryFieldOptions() - 設定取得
    |
    └── FilePreview (FilePreview.js)
            +── FileContent - 単一/複数判定
            +── FileLinkList - 複数リンク表示
            └── FileLink - 個別リンク
```

### データフロー図

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

handleChange()        ──> onOpenMediaLibrary()         メディアライブラリ
                               |                       ダイアログ
                               v
                         ユーザーがファイル選択
                               |
                               v
                         mediaPaths[controlID] 更新
                               |
                               v
                         componentDidUpdate()
                               |
                               v
                         onChange(mediaPath)  ──>  Reduxドラフト更新
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `packages/decap-cms-widget-file/src/index.js` | ソース | ウィジェットエクスポート定義 |
| withFileControl.js | `packages/decap-cms-widget-file/src/withFileControl.js` | ソース | ファイルコントロールHOC（メイン） |
| FilePreview.js | `packages/decap-cms-widget-file/src/FilePreview.js` | ソース | プレビューコンポーネント |
| schema.js | `packages/decap-cms-widget-file/src/schema.js` | ソース | フィールドスキーマ定義 |
