Files
ss-tools/frontend/src/components/git/CommitModal.svelte

270 lines
11 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- [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] -->