Files
ss-tools/frontend/src/components/git/CommitModal.svelte
2026-05-26 09:30:41 +03:00

272 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.

<!-- #region CommitModal [C:3] [TYPE Component] [SEMANTICS git, commit, modal, diff, message] -->
<!-- @BRIEF Component component: components/git/CommitModal.svelte -->
<!-- @LAYER UI -->
<!--
@UX_STATE: Loading -> Default
@SEMANTICS: git, commit, modal, version_control, diff
@PURPOSE: Модальное окно для создания коммита с просмотром изменений (diff).
@LAYER Component
@RELATION CALLS -> [EXT:frontend:GitServiceClient]
@UX_STATE: Ready -> Commit message, status, and diff preview are available for the selected repository.
@UX_STATE: Submitting -> Commit actions stay disabled while the commit request is in flight.
-->
<script>
// [SECTION: IMPORTS]
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, envId = null, 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]
// #region handleGenerateMessage:Function [TYPE 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/${encodeURIComponent(String(dashboardId))}/generate-message${envId ? `?env_id=${encodeURIComponent(String(envId))}` : ""}`,
undefined,
{ suppressToast: true },
);
message = data.message;
toast($t.git?.commit_message_generated, "success");
} catch (e) {
console.error(`[CommitModal][Coherence:Failed] ${e.message}`);
toast(e.message || $t.git?.commit_message_failed, "error");
} finally {
generatingMessage = false;
}
}
// #endregion handleGenerateMessage:Function
// #region loadStatus:Function [TYPE 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, envId);
// Fetch both unstaged and staged diffs to show complete picture
const unstagedDiff = await gitService.getDiff(
dashboardId,
null,
false,
envId,
);
const stagedDiff = await gitService.getDiff(
dashboardId,
null,
true,
envId,
);
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, "error");
} finally {
loading = false;
}
}
// #endregion loadStatus:Function
// #region handleCommit:Function [TYPE 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, [], envId);
toast($t.git?.commit_success, "success");
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;
}
}
// #endregion 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}</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}</label
>
<button
onclick={handleGenerateMessage}
disabled={generatingMessage || loading}
class="text-xs text-primary hover:text-blue-800 disabled:opacity-50 flex items-center"
>
{#if generatingMessage}
<span class="animate-spin mr-1"></span> {$t.mapper?.generating}
{:else}
<span class="mr-1"></span> {$t.git?.generate_with_ai}
{/if}
</button>
</div>
<textarea
bind:value={message}
class="w-full border rounded p-2 h-32 focus-visible:ring-2 focus-visible:ring-primary-ring outline-none resize-none"
placeholder={$t.git?.describe_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}
</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={$t.git?.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={$t.git?.modified_unstaged}
>
<span class="mr-2">M</span>
{file}
</li>
{/each}
{#each status.untracked_files as file}
<li
class="text-primary flex items-center"
title={$t.git?.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}
</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}
</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}
</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}
</button>
<button
onclick={handleCommit}
disabled={committing ||
!message ||
loading ||
(!status?.is_dirty &&
status?.staged_files?.length === 0)}
class="px-4 py-2 bg-primary text-white rounded hover:bg-primary-hover disabled:opacity-50"
>
{committing ? $t.git?.committing : $t.git?.commit}
</button>
</div>
</div>
</div>
{/if}
<!-- [/SECTION] -->
<!-- #endregion CommitModal -->