fix(git): fix 17 missing async/await bugs + UX overhaul
Backend: - fix 17 missing 'await' in git route handlers causing silent no-ops (branches, diff, history, commit, push, pull, merge, promote, sync) - fix async coroutine passed to run_blocking in git_plugin.py Frontend: - add collapsible 'How it works' onboarding (GitHelpPanel) - add status legend with color-coded repository statuses - i18n: add 50+ missing keys, replace hardcoded strings - add Refresh button in modal header - add PROD deploy confirmation dialog (replaces browser prompt()) - add CommitHistory to workspace tab with timeline nodes - add post-commit success banner with next-step guidance - increase success toast duration to 8s - group local/remote branches in selector (optgroup) - format last_modified dates timezone-aware - change PROD badge from red to neutral indigo - extract shared resolveGitStatusToken to git-utils.ts - fix 'slug' label regression - remove dead init_repo_button key UI/UX audit fixes: - add descriptions to Create/Init buttons in init panel - add actionable CTA to server mismatch warning - improve checkbox text phrasing
This commit is contained in:
@@ -39,6 +39,8 @@
|
||||
|
||||
let { dashboardId, envId = null, dashboardTitle = '', show = $bindable(false) } = $props();
|
||||
|
||||
let deployConfirmInput = $state('');
|
||||
|
||||
const model = new GitManagerModel({ dashboardId, envId, dashboardTitle });
|
||||
|
||||
$effect(() => {
|
||||
@@ -50,6 +52,10 @@
|
||||
});
|
||||
|
||||
function closeModal() { show = false; model.clearGitError(); }
|
||||
|
||||
$effect(() => {
|
||||
if (model.showDeployConfirm) deployConfirmInput = '';
|
||||
});
|
||||
function handleBackdropClick(e) { if (e.target === e.currentTarget) closeModal(); }
|
||||
|
||||
onMount(() => {
|
||||
@@ -71,9 +77,21 @@
|
||||
<p class="text-sm text-text-muted">{dashboardTitle} <span class="text-text-subtle">·</span> slug: {dashboardId}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick={closeModal} class="flex h-9 w-9 items-center justify-center rounded-lg text-text-subtle transition-colors hover:bg-surface-muted hover:text-text" aria-label={$t.common?.close || 'Close'}>
|
||||
<Icon name="close" size={20} strokeWidth={2} />
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => model.refreshStatus()}
|
||||
disabled={model.checkingStatus || model.workspaceLoading}
|
||||
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-text-muted transition-colors hover:bg-surface-muted hover:text-text disabled:opacity-50"
|
||||
aria-label={$t.common?.refresh || 'Refresh'}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5 {model.checkingStatus ? 'animate-spin' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M16.023 9.348H20M14.985 19.644H20M2.985 14.652V20" /></svg>
|
||||
{$t.common?.refresh || 'Refresh'}
|
||||
</button>
|
||||
<button type="button" onclick={closeModal} class="flex h-9 w-9 items-center justify-center rounded-lg text-text-subtle transition-colors hover:bg-surface-muted hover:text-text" aria-label={$t.common?.close || 'Close'}>
|
||||
<Icon name="close" size={20} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Git Error Banner -->
|
||||
@@ -94,7 +112,7 @@
|
||||
<p class="font-medium">{model.gitError.message || model.gitError}</p>
|
||||
{#if model.gitError.files?.length}
|
||||
<details class="text-xs text-destructive/80">
|
||||
<summary class="cursor-pointer font-medium">Файлы, которые будут перезаписаны ({model.gitError.files.length})</summary>
|
||||
<summary class="cursor-pointer font-medium">{($t.git?.error_files_overwritten || 'Файлы, которые будут перезаписаны ({count})').replace('{count}', String(model.gitError.files.length))}</summary>
|
||||
<ul class="mt-1 list-disc space-y-0.5 pl-5">
|
||||
{#each model.gitError.files as file}
|
||||
<li><code class="rounded bg-destructive-light/50 px-1">{file}</code></li>
|
||||
@@ -104,7 +122,7 @@
|
||||
{/if}
|
||||
{#if model.gitError.next_steps?.length}
|
||||
<div class="text-xs">
|
||||
<span class="font-medium">Рекомендации:</span>
|
||||
<span class="font-medium">{$t.git?.error_recommendations || 'Рекомендации:'}</span>
|
||||
<ol class="mt-1 list-decimal space-y-0.5 pl-5">
|
||||
{#each model.gitError.next_steps as step}
|
||||
<li>{step}</li>
|
||||
@@ -117,7 +135,7 @@
|
||||
type="button"
|
||||
onclick={() => model.clearGitError()}
|
||||
class="flex-shrink-0 rounded p-1 transition-colors {model.gitErrorType === 'warning' ? 'hover:bg-warning-light' : 'hover:bg-destructive-light'}"
|
||||
aria-label="Закрыть"
|
||||
aria-label={$t.common?.close || 'Закрыть'}
|
||||
>
|
||||
<Icon name="close" size={16} strokeWidth={2} />
|
||||
</button>
|
||||
@@ -134,7 +152,30 @@
|
||||
{:else}
|
||||
<div class="flex min-h-0 flex-1 flex-col gap-4">
|
||||
{#if model.hasOriginConfigMismatch}
|
||||
<div class="rounded-lg border border-warning bg-warning-light p-3 text-sm text-warning"><div class="font-semibold">Git server mismatch detected</div><div class="mt-1">Configured: <code>{model.configHost}</code>, origin: <code>{model.originHost}</code>.</div></div>
|
||||
<div class="rounded-lg border border-warning bg-warning-light p-3 text-sm text-warning">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div class="font-semibold">{$t.git?.git_server_mismatch_title || 'Git server mismatch detected'}</div>
|
||||
<div class="mt-1">{($t.git?.git_server_mismatch_desc || 'Configured: {configured}, origin: {origin}.').replace('{configured}', model.configHost).replace('{origin}', model.originHost)}</div>
|
||||
<div class="mt-1.5 text-xs text-warning/80">{$t.git?.git_server_mismatch_hint || 'Push/Pull будут отправлять на настроенный сервер, а не на origin. Обновите конфигурацию в Настройках → Git, если это не intended.'}</div>
|
||||
</div>
|
||||
<a href="/settings/git" class="shrink-0 rounded-md border border-warning bg-surface-card px-3 py-1.5 text-xs font-medium text-warning transition-colors hover:bg-warning-light">{$t.git?.git_server_mismatch_fix || 'Настроить'}</a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if model.commitCompleted}
|
||||
<div class="rounded-lg border border-success bg-success-light p-3 text-sm text-success">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
<div class="flex-1">
|
||||
<span class="font-medium">{$t.git?.commit_success_banner || '✅ Изменения закоммичены.'}</span>
|
||||
<span class="ml-1">{$t.git?.commit_next_step || 'Следующий шаг: вкладка «Релиз» для продвижения между ветками или «Серверные операции» для Pull/Push/Deploy.'}</span>
|
||||
</div>
|
||||
<button type="button" onclick={() => { model.commitCompleted = false; }} class="shrink-0 rounded p-0.5 text-success hover:bg-success-light" aria-label={$t.common?.close || 'Close'}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-border bg-surface-card px-4 py-3 shadow-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -145,12 +186,12 @@
|
||||
<span class="hidden text-xs text-text-muted sm:inline">·</span>
|
||||
<span class="flex items-center gap-1.5 text-sm text-text-muted">
|
||||
<Icon name="code" size={16} class="text-text-subtle" strokeWidth={2} />
|
||||
<span class="hidden sm:inline">Ветка:</span> <strong class="font-mono text-text">{model.currentBranch}</strong>
|
||||
<span class="hidden sm:inline">{$t.git?.branch_label || 'Ветка:'}</span> <strong class="font-mono text-text">{model.currentBranch}</strong>
|
||||
</span>
|
||||
{#if model.changedFilesCount > 0}
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-warning-light px-2.5 py-0.5 text-xs font-medium text-warning ring-1 ring-inset ring-warning-ring">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
{model.changedFilesCount} изменений
|
||||
{($t.git?.changes_count || '{count} изменений').replace('{count}', String(model.changedFilesCount))}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -159,22 +200,22 @@
|
||||
<div class="flex flex-wrap items-center gap-1 border-b border-border pb-0">
|
||||
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'workspace' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'workspace')}>
|
||||
<Icon name="edit" size={16} strokeWidth={2} />
|
||||
Фиксация изменений
|
||||
{$t.git?.tab_workspace || 'Фиксация изменений'}
|
||||
<HelpTooltip text={$t.git?.hint_workspace || ''} />
|
||||
</button>
|
||||
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'release' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'release')}>
|
||||
<Icon name="lightning" size={16} strokeWidth={2} />
|
||||
Релиз
|
||||
{$t.git?.tab_release || 'Релиз'}
|
||||
<HelpTooltip text={$t.git?.hint_release || ''} />
|
||||
</button>
|
||||
<button class={`relative -mb-px inline-flex items-center gap-2 rounded-t-lg px-4 py-2.5 text-sm font-medium transition-colors ${model.activeTab === 'operations' ? 'border border-b-white bg-surface-card text-primary shadow-sm' : 'border border-transparent text-text-muted hover:bg-surface-muted hover:text-text'}`} onclick={() => (model.activeTab = 'operations')}>
|
||||
<Icon name="settings" size={16} strokeWidth={2} />
|
||||
Серверные операции
|
||||
{$t.git?.tab_operations || 'Серверные операции'}
|
||||
<HelpTooltip text={$t.git?.hint_operations || ''} />
|
||||
</button>
|
||||
</div>
|
||||
{#if model.activeTab === 'workspace'}
|
||||
<GitWorkspacePanel hasWorkspaceChanges={model.hasWorkspaceChanges} changedFilesCount={model.changedFilesCount} workspaceLoading={model.workspaceLoading} workspaceDiff={model.workspaceDiff} committing={model.committing} generatingMessage={model.generatingMessage} bind:commitMessage={model.commitMessage} bind:autoPushAfterCommit={model.autoPushAfterCommit} loading={model.loading} pushProviderLabel={model.pushProviderLabel} onSync={() => model.handleSync()} onGenerateMessage={() => model.handleGenerateMessage()} onCommit={() => model.handleCommit()} />
|
||||
<GitWorkspacePanel {dashboardId} envId={model.resolvedEnvId} commitHistoryKey={model.commitHistoryKey} hasWorkspaceChanges={model.hasWorkspaceChanges} changedFilesCount={model.changedFilesCount} workspaceLoading={model.workspaceLoading} workspaceDiff={model.workspaceDiff} committing={model.committing} generatingMessage={model.generatingMessage} bind:commitMessage={model.commitMessage} bind:autoPushAfterCommit={model.autoPushAfterCommit} loading={model.loading} pushProviderLabel={model.pushProviderLabel} onSync={() => model.handleSync()} onGenerateMessage={() => model.handleGenerateMessage()} onCommit={() => model.handleCommit()} />
|
||||
{:else if model.activeTab === 'release'}
|
||||
<GitReleasePanel currentEnvStage={model.currentEnvStage} bind:promoteFromBranch={model.promoteFromBranch} bind:promoteToBranch={model.promoteToBranch} bind:promoteMode={model.promoteMode} bind:promoteReason={model.promoteReason} preferredDeployTargetStage={model.preferredDeployTargetStage} bind:showAdvancedPromote={model.showAdvancedPromote} promoting={model.promoting} onPromote={() => model.handlePromote()} />
|
||||
{:else}
|
||||
@@ -190,4 +231,25 @@
|
||||
{/if}
|
||||
<ConflictResolver conflicts={model.mergeConflicts} bind:show={model.showConflictResolver} onresolve={(e) => model.handleResolveConflicts(e)} />
|
||||
<DeploymentModal {dashboardId} envId={model.resolvedEnvId} preferredTargetStage={model.preferredDeployTargetStage} bind:show={model.showDeployModal} />
|
||||
|
||||
{#if model.showDeployConfirm}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onclick={() => { model.showDeployConfirm = false; }} onkeydown={(e) => { if (e.key === 'Escape') model.showDeployConfirm = false; }}>
|
||||
<div class="bg-surface-card rounded-xl shadow-xl p-6 max-w-md w-full mx-4 border border-border" role="alertdialog" aria-modal="true" aria-label={$t.git?.deploy || 'Deploy to Environment'} onclick={(e) => e.stopPropagation()}>
|
||||
<h3 class="text-lg font-semibold text-text mb-2">{$t.git?.deploy || 'Deploy to Environment'}</h3>
|
||||
<p class="text-sm text-text-muted mb-4">Подтвердите деплой в PROD. Введите slug дашборда: <strong>{model.deployConfirmSlug}</strong></p>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
bind:value={deployConfirmInput}
|
||||
class="w-full rounded-lg border border-border-strong p-2.5 text-sm text-text outline-none focus:border-primary-ring focus:ring-2 focus:ring-primary-ring mb-4"
|
||||
placeholder={model.deployConfirmSlug}
|
||||
onkeydown={(e) => { if (e.key === 'Enter' && deployConfirmInput.trim()) model.confirmDeploy(deployConfirmInput); }}
|
||||
autofocus
|
||||
/>
|
||||
<div class="flex justify-end gap-3">
|
||||
<Button variant="secondary" onclick={() => { model.showDeployConfirm = false; deployConfirmInput = ''; }}>{$t.common?.cancel || 'Cancel'}</Button>
|
||||
<Button variant="destructive" onclick={() => model.confirmDeploy(deployConfirmInput)} disabled={!deployConfirmInput.trim()}>{$t.common?.confirm || 'Confirm'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- #endregion GitManager -->
|
||||
|
||||
Reference in New Issue
Block a user