270 lines
11 KiB
Svelte
270 lines
11 KiB
Svelte
<!-- [DEF:CommitModal:Component] -->
|
||
<!--
|
||
@TIER: STANDARD
|
||
@SEMANTICS: git, commit, modal, version_control, diff
|
||
@PURPOSE: Модальное окно для создания коммита с просмотром изменений (diff).
|
||
@LAYER: Component
|
||
@RELATION: CALLS -> gitService.commit
|
||
@RELATION: CALLS -> gitService.getStatus
|
||
@RELATION: CALLS -> gitService.getDiff
|
||
@RELATION: DISPATCHES -> commit
|
||
-->
|
||
|
||
<script>
|
||
// [SECTION: IMPORTS]
|
||
import { createEventDispatcher, onMount } from "svelte";
|
||
import { gitService } from "../../services/gitService";
|
||
import { addToast as toast } from "../../lib/toasts.js";
|
||
import { api } from "../../lib/api";
|
||
import { t } from "../../lib/i18n";
|
||
// [/SECTION]
|
||
|
||
// [SECTION: PROPS]
|
||
let { dashboardId, show = false } = $props();
|
||
|
||
// [/SECTION]
|
||
|
||
// [SECTION: STATE]
|
||
let message = $state("");
|
||
let committing = $state(false);
|
||
let status = $state(null);
|
||
let diff = $state("");
|
||
let loading = $state(false);
|
||
let generatingMessage = $state(false);
|
||
// [/SECTION]
|
||
|
||
const dispatch = createEventDispatcher();
|
||
|
||
// [DEF:handleGenerateMessage:Function]
|
||
/**
|
||
* @purpose Generates a commit message using LLM.
|
||
*/
|
||
async function handleGenerateMessage() {
|
||
generatingMessage = true;
|
||
try {
|
||
console.log(
|
||
`[CommitModal][Action] Generating commit message for dashboard ${dashboardId}`,
|
||
);
|
||
// postApi returns the JSON data directly or throws an error
|
||
const data = await api.postApi(
|
||
`/git/repositories/${dashboardId}/generate-message`,
|
||
);
|
||
message = data.message;
|
||
toast($t.git?.commit_message_generated || "Commit message generated", "success");
|
||
} catch (e) {
|
||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||
toast(e.message || ($t.git?.commit_message_failed || "Failed to generate message"), "error");
|
||
} finally {
|
||
generatingMessage = false;
|
||
}
|
||
}
|
||
// [/DEF:handleGenerateMessage:Function]
|
||
|
||
// [DEF:loadStatus:Function]
|
||
/**
|
||
* @purpose Загружает текущий статус репозитория и diff.
|
||
* @pre dashboardId должен быть валидным.
|
||
*/
|
||
async function loadStatus() {
|
||
if (!dashboardId || !show) return;
|
||
loading = true;
|
||
try {
|
||
console.log(
|
||
`[CommitModal][Action] Loading status and diff for ${dashboardId}`,
|
||
);
|
||
status = await gitService.getStatus(dashboardId);
|
||
// Fetch both unstaged and staged diffs to show complete picture
|
||
const unstagedDiff = await gitService.getDiff(
|
||
dashboardId,
|
||
null,
|
||
false,
|
||
);
|
||
const stagedDiff = await gitService.getDiff(
|
||
dashboardId,
|
||
null,
|
||
true,
|
||
);
|
||
|
||
diff = "";
|
||
if (stagedDiff)
|
||
diff += "--- STAGED CHANGES ---\n" + stagedDiff + "\n\n";
|
||
if (unstagedDiff)
|
||
diff += "--- UNSTAGED CHANGES ---\n" + unstagedDiff;
|
||
|
||
if (!diff) diff = "";
|
||
} catch (e) {
|
||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||
toast($t.git?.load_changes_failed || "Failed to load changes", "error");
|
||
} finally {
|
||
loading = false;
|
||
}
|
||
}
|
||
// [/DEF:loadStatus:Function]
|
||
|
||
// [DEF:handleCommit:Function]
|
||
/**
|
||
* @purpose Создает коммит с указанным сообщением.
|
||
* @pre message не должно быть пустым.
|
||
* @post Коммит создан, событие отправлено, модальное окно закрыто.
|
||
*/
|
||
async function handleCommit() {
|
||
if (!message) return;
|
||
console.log(
|
||
`[CommitModal][Action] Committing changes for dashboard ${dashboardId}`,
|
||
);
|
||
committing = true;
|
||
try {
|
||
await gitService.commit(dashboardId, message, []);
|
||
toast($t.git?.commit_success || "Changes committed successfully", "success");
|
||
dispatch("commit");
|
||
show = false;
|
||
message = "";
|
||
console.log(`[CommitModal][Coherence:OK] Committed`);
|
||
} catch (e) {
|
||
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
|
||
toast(e.message, "error");
|
||
} finally {
|
||
committing = false;
|
||
}
|
||
}
|
||
// [/DEF:handleCommit:Function]
|
||
|
||
$effect(() => {
|
||
if (show) loadStatus();
|
||
});
|
||
</script>
|
||
|
||
<!-- [SECTION: TEMPLATE] -->
|
||
{#if show}
|
||
<div
|
||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
|
||
>
|
||
<div
|
||
class="bg-white p-6 rounded-lg shadow-xl w-full max-w-4xl max-h-[90vh] flex flex-col"
|
||
>
|
||
<h2 class="text-xl font-bold mb-4">{$t.git?.commit || "Commit Changes"}</h2>
|
||
|
||
<div class="flex flex-col md:flex-row gap-4 flex-1 overflow-hidden">
|
||
<!-- Left: Message and Files -->
|
||
<div class="w-full md:w-1/3 flex flex-col">
|
||
<div class="mb-4">
|
||
<div class="flex justify-between items-center mb-1">
|
||
<label
|
||
class="block text-sm font-medium text-gray-700"
|
||
>{$t.git?.commit_message || "Commit Message"}</label
|
||
>
|
||
<button
|
||
onclick={handleGenerateMessage}
|
||
disabled={generatingMessage || loading}
|
||
class="text-xs text-blue-600 hover:text-blue-800 disabled:opacity-50 flex items-center"
|
||
>
|
||
{#if generatingMessage}
|
||
<span class="animate-spin mr-1">↻</span> {$t.mapper?.generating || "Generating..."}
|
||
{:else}
|
||
<span class="mr-1">✨</span> {$t.git?.generate_with_ai || "Generate with AI"}
|
||
{/if}
|
||
</button>
|
||
</div>
|
||
<textarea
|
||
bind:value={message}
|
||
class="w-full border rounded p-2 h-32 focus:ring-2 focus:ring-blue-500 outline-none resize-none"
|
||
placeholder={$t.git?.describe_changes || "Describe your changes..."}
|
||
></textarea>
|
||
</div>
|
||
|
||
{#if status}
|
||
<div class="flex-1 overflow-y-auto">
|
||
<h3
|
||
class="text-sm font-bold text-gray-500 uppercase mb-2"
|
||
>
|
||
{$t.git?.changed_files || "Changed Files"}
|
||
</h3>
|
||
<ul class="text-xs space-y-1">
|
||
{#each status.staged_files as file}
|
||
<li
|
||
class="text-green-600 flex items-center font-semibold"
|
||
title="Staged"
|
||
>
|
||
<span class="mr-2">S</span>
|
||
{file}
|
||
</li>
|
||
{/each}
|
||
{#each status.modified_files as file}
|
||
<li
|
||
class="text-yellow-600 flex items-center"
|
||
title="Modified (Unstaged)"
|
||
>
|
||
<span class="mr-2">M</span>
|
||
{file}
|
||
</li>
|
||
{/each}
|
||
{#each status.untracked_files as file}
|
||
<li
|
||
class="text-blue-600 flex items-center"
|
||
title="Untracked"
|
||
>
|
||
<span class="mr-2">?</span>
|
||
{file}
|
||
</li>
|
||
{/each}
|
||
</ul>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
|
||
<!-- Right: Diff Viewer -->
|
||
<div
|
||
class="w-full md:w-2/3 flex flex-col overflow-hidden border rounded bg-gray-50"
|
||
>
|
||
<div
|
||
class="bg-gray-200 px-3 py-1 text-xs font-bold text-gray-600 border-b"
|
||
>
|
||
{$t.git?.changes_preview || "Changes Preview"}
|
||
</div>
|
||
<div class="flex-1 overflow-auto p-2">
|
||
{#if loading}
|
||
<div
|
||
class="flex items-center justify-center h-full text-gray-500"
|
||
>
|
||
{$t.git?.loading_diff || "Loading diff..."}
|
||
</div>
|
||
{:else if diff}
|
||
<pre
|
||
class="text-xs font-mono whitespace-pre-wrap">{diff}</pre>
|
||
{:else}
|
||
<div
|
||
class="flex items-center justify-center h-full text-gray-500 italic"
|
||
>
|
||
{$t.git?.no_changes || "No changes detected"}
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-end space-x-3 mt-6 pt-4 border-t">
|
||
<button
|
||
onclick={() => (show = false)}
|
||
class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
|
||
>
|
||
{$t.common?.cancel || "Cancel"}
|
||
</button>
|
||
<button
|
||
onclick={handleCommit}
|
||
disabled={committing ||
|
||
!message ||
|
||
loading ||
|
||
(!status?.is_dirty &&
|
||
status?.staged_files?.length === 0)}
|
||
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||
>
|
||
{committing ? ($t.git?.committing || "Committing...") : ($t.git?.commit || "Commit")}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
<!-- [/SECTION] -->
|
||
|
||
<!-- [/DEF:CommitModal:Component] -->
|