# 機能設計書 59-リレーションウィジェット

## 概要

本ドキュメントは、Decap CMSにおけるリレーションウィジェット（Relation Widget）の設計を記述する。他のコレクションのエントリを参照・リンクするウィジェットであり、検索フィールド・表示フィールド・値フィールドの設定が可能である。

### 本機能の処理概要

**業務上の目的・背景**：CMSのコンテンツ間にはリレーション（関連付け）が必要な場面が多い。例えば、ブログ記事に著者を紐付ける、商品にカテゴリを関連付けるといったケースである。本ウィジェットは他のコレクションのエントリを非同期検索し、選択した値をフィールドに保存する。検索にはデバウンス付きの非同期クエリを使用し、結果はキャッシュされる。複数選択時はドラッグ＆ドロップによる並べ替えも可能。

**機能の利用シーン**：エントリ編集画面・エントリ新規作成画面において、`widget: relation`が指定されたフィールドに対して非同期検索付きセレクトをレンダリングする。ユーザーは検索キーワードを入力し、候補一覧から値を選択する。

**主要な処理内容**：
1. AsyncSelect（react-select/async）による非同期検索UIのレンダリング
2. 500msデバウンスによる検索クエリの発行（query関数経由）
3. RelationCacheによる検索結果のキャッシュ管理（最大100件）
4. テンプレート変数によるネストフィールドの解決（stringTemplate）
5. 複数選択時のドラッグ＆ドロップ並べ替え（@dnd-kit使用）
6. react-windowによる大量オプションの仮想スクロール（FixedSizeList）
7. メタデータ付きのonChange通知（参照先データの伝搬）
8. min/maxバリデーション（複数選択時）
9. i18nロケール対応（ローカライズされたフィールド値の解決）
10. フィルター機能（filtersによる候補の絞り込み）

**関連システム・外部連携**：react-select/async、@dnd-kit（ドラッグ＆ドロップ）、react-window（仮想リスト）、decap-cms-lib-widgets（stringTemplate, validations）

**権限による制御**：なし

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 6 | エントリ編集画面 | 主機能 | 他コレクションエントリの参照選択 |
| 7 | エントリ新規作成画面 | 主機能 | 他コレクションエントリの参照選択 |

## 機能種別

UI入力コンポーネント / データ連携 / バリデーション

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| onChange | function | Yes | 値変更時のコールバック（metadata付き） | - |
| forID | string | Yes | select要素のid属性 | - |
| value | node | No | 現在の選択値 | - |
| field | ImmutableMap | Yes | フィールド設定 | collection, value_field, search_fields必須 |
| query | function | Yes | 検索クエリ実行関数 | - |
| queryHits | array | No | クエリ結果のヒット配列 | - |
| classNameWrapper | string | Yes | ラッパーのCSSクラス名 | - |
| setActiveStyle | function | Yes | フォーカス時のスタイル設定関数 | - |
| setInactiveStyle | function | Yes | ブラー時のスタイル設定関数 | - |
| locale | string | No | 現在のロケール（i18n対応） | - |
| t | function | No | ローカライズ関数 | - |

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

| 設定名 | 型 | 必須 | 説明 |
|--------|-----|-----|------|
| collection | string | Yes | 参照先コレクション名 |
| value_field | string | Yes | 値として保存するフィールドパス |
| search_fields | array | Yes | 検索対象フィールド一覧 |
| display_fields | array | No | 表示用フィールド一覧（デフォルト: [value_field]） |
| file | string | No | ファイルベースコレクションのファイル名 |
| multiple | boolean | No | 複数選択モード |
| min | integer | No | 最小選択数 |
| max | integer | No | 最大選択数 |
| options_length | integer | No | 表示するオプション数上限（デフォルト: 20） |
| filters | array | No | 候補フィルタ条件（{field, values}の配列） |

### 入力データソース

- エディタのコントロールペインから提供されるprops
- query関数経由でバックエンドAPIから取得するエントリデータ
- RelationCacheからのキャッシュデータ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| value（単一） | string | 選択されたvalue_fieldの値 |
| value（複数） | ImmutableList | 選択されたvalue_fieldの値リスト |
| metadata | object | 参照先エントリのデータ（{fieldName: {collection: {value: data}}}） |

### 出力先

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

## 処理フロー

### 処理シーケンス

```
1. コンポーネントマウント
   └─ PropTypesの手動バリデーション
   └─ 初期値がある場合、loadInitialOptionsで初期オプション取得
   └─ triggerInitialOnChangeでメタデータ付きのonChange呼び出し
2. ユーザーが検索テキスト入力
   └─ 500msデバウンス後にloadOptionsが発火
   └─ RelationCacheでキャッシュチェック
   └─ キャッシュミス時はquery関数でバックエンドに問い合わせ
3. 検索結果のパース
   └─ parseHitOptionsでヒットをオプション配列に変換
   └─ filtersによるフィルタリング
   └─ テンプレート変数の展開（expandPath, compileStringTemplate）
   └─ display_fieldsによるラベル生成
4. ユーザーが選択/選択解除
   └─ handleChange → 単一/複数で分岐
   └─ メタデータ付きonChange呼び出し
5. ドラッグ＆ドロップ（複数選択時）
   └─ onSortEnd → arrayMoveで並べ替え → onChange
```

### フローチャート

```mermaid
flowchart TD
    A[AsyncSelect レンダリング] --> B[ユーザー検索入力]
    B --> C[500ms デバウンス]
    C --> D[loadOptions]
    D --> E{キャッシュあり?}
    E -->|Yes| F[キャッシュから取得]
    E -->|No| G[query関数でAPI呼び出し]
    G --> H[結果をキャッシュ]
    F --> I[parseHitOptions]
    H --> I
    I --> J[filters適用]
    J --> K[テンプレート変数展開]
    K --> L[オプション一覧表示]
    L --> M[ユーザー選択]
    M --> N[handleChange]
    N --> O[メタデータ生成]
    O --> P["onChange(value, metadata)"]
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-59-01 | デバウンス検索 | 検索クエリは500msのデバウンスで発行される | テキスト入力時 |
| BR-59-02 | キャッシュ | 検索結果はRelationCacheで管理。キャッシュキーは「collection-searchFields-term-file」形式。最大100件 | 検索時 |
| BR-59-03 | オプション件数制限 | 表示オプション数はoptions_length（デフォルト20）で制限 | 検索結果表示時 |
| BR-59-04 | メタデータ伝搬 | 選択時に参照先エントリのdataをメタデータとしてonChangeに渡す | 選択変更時 |
| BR-59-05 | フィルタ適用 | filters設定時、ネストフィールドの値がfilter.valuesに含まれるヒットのみ候補とする | filters設定時 |
| BR-59-06 | テンプレート変数 | value_fieldとdisplay_fieldsにテンプレート変数（{{slug}}等）が含まれる場合、expandPathとcompileStringTemplateで展開 | テンプレート変数含む場合 |
| BR-59-07 | i18n対応 | locale設定時、hit.i18n[locale].dataからフィールド値を取得 | locale設定時 |
| BR-59-08 | ドラッグ＆ドロップ | 複数選択時、@dnd-kitによる値の並べ替えが可能 | multiple=true時 |
| BR-59-09 | 仮想スクロール | react-windowのFixedSizeListで大量オプションの仮想スクロールを実現（アイテムサイズ30px、最大高さ300px） | 常時 |
| BR-59-10 | 初期値ロード | マウント時に既存値がある場合、空検索で初期オプションをロードしメタデータを補完 | 初期値あり時 |

### 計算ロジック

- キャッシュキー: `${collection}-${searchFields.join(',')}-${term || ''}-${file || ''}`
- オプション解決: expandPath → compileStringTemplate → display_fields.map().join(' ')

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

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

該当なし（query関数経由でバックエンドAPIに問い合わせるが、直接のDB操作は行わない）。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| - | 初期ロード失敗 | loadInitialOptionsのquery失敗 | console.errorでログ出力、初期オプション空 |
| - | 検索失敗 | loadOptionsのquery失敗 | console.errorでログ出力、callback([])で空結果返却 |
| - | 範囲エラー | 複数選択で選択数がmin/maxの範囲外 | validateMinMaxによるエラーメッセージ返却 |

### リトライ仕様

該当なし。キャッシュにpendingRequests管理があり、同一キーの並行リクエストを防止する。

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

該当なし。

## パフォーマンス要件

- デバウンス（500ms）による検索リクエスト頻度の制限
- RelationCacheによる検索結果のキャッシュ（最大100件、LRU的な管理）
- react-window（FixedSizeList）による大量オプションの仮想スクロール（アイテムサイズ30px）
- shouldComponentUpdate最適化（value, hasActiveStyle, queryHitsの変更時のみ再レンダリング）

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

react-selectによるレンダリングのため、ブラウザのデフォルトのセキュリティ制御が適用される。query関数はRedux経由でバックエンドに委譲されるため、認証はバックエンドレイヤーで管理される。

## 備考

- RelationCacheはシングルトンインスタンスとして管理される
- pending requestsの管理により同一キーの並行リクエストを防止
- uuid v4で各選択肢にユニークIDを付与（ドラッグ＆ドロップ用）

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | schema.js | `packages/decap-cms-widget-relation/src/schema.js` | フィールドスキーマ: collection, value_field, search_fields必須。filters, display_fields, options_length等のオプション |
| 1-2 | RelationCache.js | `packages/decap-cms-widget-relation/src/RelationCache.js` | キャッシュ管理クラス: Map/pendingRequestsによるキャッシュと並行リクエスト防止（1-55行目） |

**読解のコツ**: RelationCacheは`getOptions`で「キャッシュ確認 → pending確認 → 新規リクエスト → キャッシュ保存」の流れを実装している。最大100件のキャッシュサイズ制限もある。

#### Step 2: ユーティリティ関数群を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | RelationControl.js | `packages/decap-cms-widget-relation/src/RelationControl.js` | ヘルパー関数群（31-192行目）: arrayMove, MultiValue, SortableSelect, MenuList, optionToString, convertToOption, getSelectedValue等 |

**主要処理フロー**:
- **31-35行目**: arrayMove - 配列要素の並べ替え
- **37-70行目**: MultiValue, MultiValueLabel - ドラッグ＆ドロップ対応の複数選択値コンポーネント
- **72-106行目**: SortableSelect - DndContext/SortableContext でラップしたAsyncSelect
- **108-130行目**: MenuList - FixedSizeListによる仮想スクロール（itemSize: 30, maxHeight: 300）
- **154-156行目**: uniqOptions - 初期オプションと検索結果の重複除去

#### Step 3: メインコントロールロジックを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | RelationControl.js | `packages/decap-cms-widget-relation/src/RelationControl.js` | RelationControlクラス（194-505行目）: loadInitialOptions, parseHitOptions, loadOptions, handleChange, onSortEnd |

**主要処理フロー**:
- **242-251行目**: componentDidMount - 初期値チェックとloadInitialOptions呼び出し
- **261-290行目**: loadInitialOptions - RelationCache経由で初期オプションロード、triggerInitialOnChange
- **292-330行目**: triggerInitialOnChange - 初期値のメタデータ付きonChange
- **354-381行目**: handleChange - 単一/複数分岐、メタデータ生成、fromJS変換
- **383-397行目**: parseNestedFields - i18n対応のネストフィールド解決、テンプレート変数展開
- **403-443行目**: parseHitOptions - ヒット配列からオプション配列への変換（filters, expandPath, display_fields結合）
- **445-466行目**: loadOptions - 500msデバウンス、RelationCache経由の検索、options_length制限

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

```
DecapCmsWidgetRelation (index.js)
    |
    +-- Widget()
    |       └── { name: 'relation', controlComponent, previewComponent, schema }
    |
    +-- RelationControl (RelationControl.js)
    |       +-- componentDidMount()
    |       |       └── loadInitialOptions()
    |       |               +── RelationCache.getOptions()
    |       |               └── triggerInitialOnChange()
    |       +-- loadOptions() [debounced 500ms]
    |       |       └── RelationCache.getOptions()
    |       |               └── query() [バックエンドAPI]
    |       +-- parseHitOptions()
    |       |       +── filters適用
    |       |       +── stringTemplate.expandPath()
    |       |       +── parseNestedFields()
    |       |       └── display_fields結合
    |       +-- handleChange()
    |       |       └── onChange(value, metadata)
    |       +-- onSortEnd()
    |       |       └── arrayMove + onChange
    |       +-- isValid()
    |       |       └── validations.validateMinMax()
    |       └── render()
    |               └── SortableSelect
    |                       +── AsyncSelect (react-select/async)
    |                       +── MenuList (react-window)
    |                       +── MultiValue (@dnd-kit)
    |                       └── MultiValueLabel (@dnd-kit)
    |
    +-- RelationCache (RelationCache.js)
    |       +── cache (Map, max 100)
    |       +── pendingRequests (Map)
    |       └── getOptions() / invalidateCollection() / clear()
    |
    └── RelationPreview (RelationPreview.js)
            └── WidgetPreviewContainer で値表示
```

### データフロー図

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

検索テキスト    ──> loadOptions (debounce 500ms)
                        |
                        v
                   RelationCache.getOptions()
                        |
                        v
                   query(forID, collection,
                         searchFields, term, file)
                        |
                        v
                   parseHitOptions()
                   - filters適用
                   - テンプレート展開
                   - display_fields結合
                        |
                        v
                   オプション一覧 ──> ユーザー選択
                                        |
                                        v
                                   handleChange()
                                   - metadata生成
                                        |
                                        v
                                   onChange(value, metadata) ──> Reduxストア
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| index.js | `packages/decap-cms-widget-relation/src/index.js` | ソース | ウィジェットエクスポート定義 |
| RelationControl.js | `packages/decap-cms-widget-relation/src/RelationControl.js` | ソース | リレーションコントロール（メイン） |
| RelationPreview.js | `packages/decap-cms-widget-relation/src/RelationPreview.js` | ソース | プレビューコンポーネント |
| RelationCache.js | `packages/decap-cms-widget-relation/src/RelationCache.js` | ソース | 検索結果キャッシュ管理 |
| schema.js | `packages/decap-cms-widget-relation/src/schema.js` | ソース | フィールドスキーマ定義 |
