# 画面設計書 140-スニペット編集

## 概要

本ドキュメントは、GitLabにおけるプロジェクトスニペット編集画面の設計を定義するものである。

### 本画面の処理概要

**業務上の目的・背景**：既存のプロジェクトスニペットを編集するための画面である。タイトル、説明、ファイル名、コンテンツ、可視性レベルなどを変更できる。複数ファイルを含むスニペットの場合、ファイルの追加・削除・変更も可能。スニペットはGitリポジトリとして管理されているため、変更はコミットとして保存される。

**画面へのアクセス方法**：スニペット詳細画面から「Edit」ボタンをクリック、またはスニペット一覧画面のアクションメニューから編集を選択できる。URLパスは `/:namespace/:project/-/snippets/:id/edit` となる。

**主要な操作・処理内容**：
1. スニペットのタイトルを変更
2. スニペットの説明（オプション、Markdown対応）を変更
3. ファイル名とコンテンツを変更
4. 可視性レベル（公開/内部/非公開）を変更
5. ファイルの追加・削除（複数ファイル対応）
6. 変更を保存

**画面遷移**：
- 遷移元：スニペット詳細画面、スニペット一覧画面
- 遷移先：スニペット詳細画面（更新成功時）、同一画面（エラー時）

**権限による表示制御**：スニペットの更新権限（`update_snippet`）を持つユーザーのみがこの画面にアクセスできる。可視性レベルの選択肢は、ユーザーの権限とシステム設定に応じて制限される場合がある。

## 関連機能

| 機能No | 機能名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 71 | プロジェクトスニペット | 主機能 | スニペットの編集 |

## 画面種別

編集

## URL/ルーティング

| メソッド | パス | コントローラ#アクション |
|---------|------|------------------------|
| GET | `/:namespace_id/:project_id/-/snippets/:id/edit` | `projects/snippets#edit` |

スニペット更新はGraphQL Mutation `UpdateSnippet` で行われる。

## 入出力項目

| 項目名 | 項目ID | 型 | 必須 | 入出力 | バリデーション | 説明 |
|--------|--------|-----|------|--------|---------------|------|
| タイトル | title | String | Yes | 入力 | 最大255文字 | スニペットのタイトル |
| 説明 | description | String | No | 入力 | 最大1MB | Markdown対応の説明文 |
| ファイル名 | file_name | String | No | 入力 | 最大255文字 | スニペットのファイル名 |
| コンテンツ | content | Text | Yes | 入力 | サイズ制限あり | スニペットの内容 |
| 可視性レベル | visibility_level | Integer | Yes | 入力 | 0/10/20 | 0=非公開,10=内部,20=公開 |
| Blobアクション | blob_actions | Array | No | 入力 | - | ファイル操作（追加/更新/削除） |

## 表示項目

この画面はVue.jsコンポーネント（`#js-snippet-edit`）によりレンダリングされる。

| 項目名 | 項目ID | 型 | 説明 |
|--------|--------|-----|------|
| パンくずリスト | breadcrumbs | Component | Snippets > $参照番号 > Edit |
| ページタイトル | page_title | String | 「Edit」、スニペットタイトル（参照番号） |
| ページ見出し | heading | Component | 「Edit snippet」（PageHeadingComponent） |
| スニペットフォーム | snippet_form | Vue Component | 編集フォーム |

### Vueコンポーネントへ渡すデータ

| 項目名 | 説明 |
|--------|------|
| project_path | プロジェクトのフルパス |
| snippet-gid | スニペットのGlobal ID |
| markdown-preview-path | Markdownプレビュー用APIパス |
| markdown-docs-path | Markdownドキュメントへのパス |
| visibility_levels | 選択可能な可視性レベル（JSON） |
| selected_level | 現在の可視性レベル |
| multiple_levels_restricted | 複数レベル制限フラグ |

## イベント仕様

### 1-スニペット更新

**トリガー**：Vueコンポーネント内の「Save changes」ボタンクリック

**処理フロー**：
1. フロントエンドでフォームのバリデーション実行
2. GraphQL Mutation `UpdateSnippet` を呼び出し
3. バックエンドで `Snippets::UpdateService` を実行
4. 可視性レベル変更の権限チェック
5. スニペット属性を更新
6. リポジトリが存在する場合、ファイル変更をコミット
7. 更新成功時、スニペット詳細画面にリダイレクト
8. 更新失敗時、エラーメッセージを表示

### 2-ファイル追加

**トリガー**：「Add file」ボタンクリック

**処理フロー**：
1. 新しいファイル入力フィールドを追加
2. 最大10ファイルまで追加可能

### 3-ファイル削除

**トリガー**：ファイルの「Delete」ボタンクリック

**処理フロー**：
1. 対象ファイルを削除対象としてマーク
2. 保存時にリポジトリから削除

### 4-Markdownプレビュー

**トリガー**：説明フィールドの「Preview」タブクリック

**処理フロー**：
1. markdown-preview-path APIを呼び出し
2. Markdownをレンダリング
3. プレビュー表示

### 5-キャンセル

**トリガー**：「Cancel」ボタンまたはパンくずリストクリック

**処理フロー**：
1. スニペット詳細画面へ遷移

## データベース更新仕様

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

| 操作（イベント） | 対象テーブル | 操作種別 | 概要 |
|----------------|-------------|---------|------|
| スニペット更新 | snippets | UPDATE | スニペットレコード更新 |
| スニペット更新 | snippet_repositories | INSERT/UPDATE | リポジトリ情報（存在しない場合作成） |

### テーブル別更新項目詳細

#### snippets

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| UPDATE | title | 入力値 | タイトル |
| UPDATE | description | 入力値 | 説明（Markdown） |
| UPDATE | content | 入力値 | コンテンツ |
| UPDATE | file_name | 入力値 | ファイル名 |
| UPDATE | visibility_level | 入力値 | 0/10/20 |
| UPDATE | updated_at | 現在時刻 | 更新日時 |

## メッセージ仕様

| メッセージID | 種別 | メッセージ内容 | 表示条件 |
|-------------|------|---------------|----------|
| MSG001 | エラー | Title is required | タイトル未入力時 |
| MSG002 | エラー | Content is required | コンテンツ未入力時 |
| MSG003 | エラー | Content is too long (maximum is X bytes) | コンテンツサイズ超過時 |
| MSG004 | エラー | Description is too long (maximum is 1 MB) | 説明サイズ超過時 |
| MSG005 | エラー | File name is too long (maximum is 255 characters) | ファイル名超過時 |
| MSG006 | エラー | Maximum 10 files allowed | ファイル数超過時 |
| MSG007 | エラー | You are not allowed to change the visibility level | 可視性変更権限なし |

## 例外処理

| 例外条件 | 処理内容 | 遷移先 |
|---------|---------|--------|
| プロジェクト未検出 | 404 Not Found | Not Found画面 |
| スニペット未検出 | 404 Not Found | Not Found画面 |
| 権限不足 | 404 Not Found | Not Found画面 |
| スニペット機能無効 | 404 Not Found | Not Found画面 |
| バリデーションエラー | エラーメッセージ表示 | 同一画面 |
| リポジトリ作成エラー | エラーメッセージ表示 | 同一画面 |
| スパム検出 | キャプチャ表示 | 同一画面（reCAPTCHA） |

## 備考

- この画面はVue.jsコンポーネント（`#js-snippet-edit`）によりレンダリングされる
- 編集時は `snippet-gid` にスニペットのGlobal IDが設定される
- 可視性レベルはシステム設定とプロジェクト設定に依存
- 現在の可視性レベルは `snippets_selected_visibility_level` ヘルパーで取得
- スニペットはGitリポジトリとして保存される（複数ファイル対応）
- リポジトリが存在しない場合、最初の更新時に作成される
- 最大ファイル数は `Snippet::MAX_FILE_COUNT`（10）で制限
- コンテンツサイズは `Gitlab::CurrentSettings.snippet_size_limit` で制限
- スパム検知が有効な場合、reCAPTCHAが表示される場合がある
- `COMMITTABLE_ATTRIBUTES = %w[file_name content]` でコミット対象を判定

---

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

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

### 推奨読解順序

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

Snippetモデルとバリデーションを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | snippet.rb | `app/models/snippet.rb` | Snippetモデル、バリデーション、定数定義 |

**読解のコツ**: `MAX_FILE_COUNT = 10`、`DESCRIPTION_LENGTH_MAX = 1.megabyte`、バリデーション（title、content、file_name）を確認。編集時も同じバリデーションが適用される。

#### Step 2: エントリーポイントを理解する

ビューテンプレートとコントローラの処理を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | edit.html.haml | `app/views/projects/snippets/edit.html.haml` | スニペット編集画面のビュー |
| 2-2 | snippets_controller.rb | `app/controllers/projects/snippets_controller.rb` | editアクション、権限チェック |
| 2-3 | _form.html.haml | `app/views/shared/snippets/_form.html.haml` | スニペットフォーム（共有パーシャル） |

**主要処理フロー**:
1. **行11**: `snippet` メソッドでスニペット取得（before_action）
2. **行14**: `authorize_read_snippet!` で閲覧権限チェック
3. **行15**: `authorize_update_snippet!` で編集権限チェック
4. パーシャルでVueコンポーネントをマウント（snippet-gidにGlobal ID設定）

#### Step 3: 共有フォームを理解する

スニペットフォームの共有コンポーネントを把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | _form.html.haml | `app/views/shared/snippets/_form.html.haml` | Vueコンポーネントマウントポイント |
| 3-2 | snippets_helper.rb | `app/helpers/snippets_helper.rb` | 可視性レベル関連ヘルパー |

**読解のコツ**: 編集時は `@snippet.to_global_id` でGlobal IDが設定され、`@snippet.visibility_level` で現在の可視性レベルが渡される。

#### Step 4: GraphQL Mutationを理解する

スニペット更新のGraphQL処理を把握する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | update.rb | `app/graphql/mutations/snippets/update.rb` | GraphQL Mutation定義（行1-86） |
| 4-2 | update_service.rb | `app/services/snippets/update_service.rb` | 更新サービス（行1-152） |

**読解のコツ**:
- **行12-30**: Mutation引数（id, title, description, visibility_level, blob_actions）
- **行32-66**: resolve メソッドでUpdateServiceを呼び出し
- **行17-38（サービス）**: execute メソッドで可視性変更チェック、属性更新、コミット作成

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

```
projects/snippets/edit.html.haml
    │
    ├─ パンくずリスト（Snippets > $参照番号 > Edit）
    │
    ├─ Layouts::PageHeadingComponent（見出し: Edit snippet）
    │
    └─ shared/snippets/_form.html.haml
           │
           └─ #js-snippet-edit (Vue Component)
                  │
                  ├─ data: project_path
                  ├─ data: snippet-gid (スニペットGlobal ID)
                  ├─ data: markdown-preview-path
                  ├─ data: visibility_levels
                  └─ data: selected_level (現在の可視性レベル)
                         │
                         └─ Vue Form Component
                                │
                                ├─ Title入力（既存値表示）
                                ├─ Description入力（既存値表示）
                                ├─ File入力（既存ファイル表示）
                                └─ Visibility選択（現在値選択）
                                       │
                                       └─ GraphQL Mutation (UpdateSnippet)
                                              │
                                              └─ Snippets::UpdateService
                                                     │
                                                     ├─ visibility_changed?
                                                     ├─ update_snippet_attributes
                                                     └─ save_and_commit
```

### データフロー図

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

snippet_id ──────────▶ SnippetsController#edit
                              │
                              ├─ authorize_read_snippet!
                              └─ authorize_update_snippet!
                                     │
                                     ▼
                              @snippet 取得
                                     │
                                     ▼
                              _form.html.haml render
                                     │
                                     ▼
                              #js-snippet-edit マウント
                                (snippet-gid = @snippet.to_global_id)

Form Data ──────────────────────────────────▶ GraphQL Mutation (UpdateSnippet)
(id,                                                  │
 title,                                               ▼
 description,                                  authorized_find!(id)
 file_name,                                          │
 content,                                            ▼
 visibility_level,                           Snippets::UpdateService
 blob_actions)                                       │
                                                     ├─ visibility_changed?
                                                     │    └─ 変更時: visibility_allowed? チェック
                                                     │
                                                     ├─ update_snippet_attributes
                                                     │    └─ snippet.assign_attributes(params)
                                                     │
                                                     ├─ perform_spam_check
                                                     │    └─ snippet.check_for_spam
                                                     │
                                                     └─ save_and_commit
                                                          │
                                                          ├─ snippet.save
                                                          │
                                                          ├─ create_repository_for (存在しない場合)
                                                          │
                                                          └─ create_commit
                                                                 │
                                                                 ▼
                                                     スニペット詳細画面へ
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| edit.html.haml | `app/views/projects/snippets/edit.html.haml` | テンプレート | スニペット編集画面（行1-7） |
| snippets_controller.rb | `app/controllers/projects/snippets_controller.rb` | コントローラ | HTTPリクエスト処理（行1-53） |
| _form.html.haml | `app/views/shared/snippets/_form.html.haml` | パーシャル | スニペットフォーム（行1-2） |
| snippet.rb | `app/models/snippet.rb` | モデル | Snippetエンティティ |
| update.rb | `app/graphql/mutations/snippets/update.rb` | GraphQL | Mutation定義（行1-86） |
| update_service.rb | `app/services/snippets/update_service.rb` | サービス | 更新ロジック（行1-152） |
| snippets_helper.rb | `app/helpers/snippets_helper.rb` | ヘルパー | 可視性レベル等 |
| project.rb | `config/routes/project.rb` | ルーティング | URLルーティング定義（行433-438） |
