Files
ss-tools/frontend/src/lib/components/git/GitFeatureWorkflow.svelte

174 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 Git.FeatureWorkflow [C:3] [TYPE Component] [SEMANTICS git,feature,bi,workflow,release] -->
<!-- @ingroup Git -->
<!-- @BRIEF BI-facing feature draft list that delegates branch operations to GitManager. -->
<!-- @LAYER UI -->
<!-- @UX_STATE Empty -> Explains that no separate drafts exist. -->
<!-- @UX_STATE Ready -> Shows each feature draft with one primary business action. -->
<!-- @UX_STATE Current -> Marks the feature currently open for editing. -->
<!-- @UX_FEEDBACK Opening or transferring a feature is delegated to the parent flow. -->
<!-- @UX_RECOVERY Technical Git operations remain available in the details section. -->
<!-- @UX_REACTIVITY Props -> $props(), DerivedState -> $derived(...). -->
<!-- @UX_TEST: Ready -> {click: transfer feature, expected: business merge confirmation opens}. -->
<script lang="ts">
import { Button, Icon } from '$lib/ui';
import { t } from '$lib/i18n/index.svelte.js';
import { gitService } from '../../../services/gitService.js';
import { addToast } from '$lib/toasts.svelte.js';
type Branch = {
name?: string;
commit_hash?: string;
is_remote?: boolean;
last_updated?: string;
ahead_of_dev?: number | null;
};
let {
branches = [] as Branch[],
currentBranch = '',
dashboardId = '',
envId = null as string | null,
openFeature = (_branch: string) => {},
onCreate = () => {},
onTransferred = () => {},
} = $props();
let featureBranches = $derived(branches.filter((branch) => {
const name = String(branch.name || '');
return !branch.is_remote && (name.startsWith('feature/') || name.startsWith('hotfix/'));
}));
let currentFeature = $derived(featureBranches.find((branch) => branch.name === currentBranch) || null);
let workingFeatureBranches = $derived(featureBranches.filter((branch) => branch.ahead_of_dev !== 0 || branch.name === currentBranch));
let archivedFeatureBranches = $derived(featureBranches.filter((branch) => branch.ahead_of_dev === 0 && branch.name !== currentBranch));
let transferBranch = $state<string | null>(null);
let transferring = $state(false);
let transferError = $state<string | null>(null);
function featureTitle(name: string): string {
return name.replace(/^(feature|hotfix)\//, '').replace(/[-_]/g, ' ');
}
function formatDate(value?: string): string {
if (!value) return $t.git?.feature_flow_date_unknown || 'дата неизвестна';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return $t.git?.feature_flow_date_unknown || 'дата неизвестна';
return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
}
async function transferToDev(): Promise<void> {
if (!transferBranch || !dashboardId) return;
transferring = true;
transferError = null;
try {
await gitService.mergeBranch(
dashboardId, transferBranch, 'dev',
`feat(flow): merge ${transferBranch} into dev`, false, envId,
);
addToast(`${featureTitle(transferBranch)}: ${$t.git?.feature_flow_transferred || 'передана в DEV'}`, 'success');
transferBranch = null;
onTransferred();
} catch (error: unknown) {
transferError = error instanceof Error ? error.message : ($t.git?.feature_flow_transfer_failed || 'Не удалось передать доработку в DEV');
} finally {
transferring = false;
}
}
</script>
<section class={`rounded-lg border border-border bg-surface-card ${workingFeatureBranches.length > 0 ? 'p-4' : 'px-4 py-3'}`} aria-label={$t.git?.feature_flow_title || 'Черновики доработок'}>
<div class="flex flex-wrap items-start justify-between gap-2">
<div>
<h3 class="text-sm font-semibold text-text">{$t.git?.feature_flow_title || 'Черновики доработок'}</h3>
<p class="mt-0.5 text-xs text-text-muted">
{workingFeatureBranches.length > 0
? ($t.git?.feature_flow_hint || 'Передайте готовую доработку в общую разработку DEV. Это ещё не отправляет её на проверку или пользователям.')
: ($t.git?.feature_flow_no_active || 'Нет отдельных доработок, ожидающих передачи в DEV.')}
</p>
</div>
<div class="flex items-center gap-2">
<span class="rounded-full bg-surface-muted px-2 py-1 text-xs text-text-muted">{featureBranches.length}</span>
<Button variant="secondary" size="sm" onclick={onCreate}><Icon name="plus" size={14} class="mr-1" />{$t.git?.feature_flow_create || 'Новая доработка'}</Button>
</div>
</div>
{#if workingFeatureBranches.length > 0}
<div class="mt-3 grid gap-3 xl:grid-cols-[minmax(0,1fr)_17rem]">
<div class="grid gap-2 sm:grid-cols-2">
{#each workingFeatureBranches as branch (branch.name)}
{@const branchName = String(branch.name)}
{@const isCurrent = branchName === currentBranch}
{@const alreadyInDev = branch.ahead_of_dev === 0}
<article class={`rounded-md border px-3 py-2.5 ${isCurrent ? 'border-primary/40 bg-primary-light' : 'border-border bg-surface-page'}`}>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0">
<h4 class="truncate text-sm font-medium text-text">{featureTitle(branchName)}</h4>
<p class="mt-0.5 truncate font-mono text-xs text-text-muted" title={branchName}>{branchName}</p>
</div>
<span class={`shrink-0 rounded-full px-2 py-1 text-[11px] font-medium ${isCurrent ? 'bg-primary text-white' : 'bg-warning-light text-warning'}`}>
{isCurrent ? ($t.git?.feature_flow_current || 'Открыта сейчас') : alreadyInDev ? ($t.git?.feature_flow_in_dev || 'Уже в DEV') : ($t.git?.feature_flow_ready || 'Готов к передаче')}
</span>
</div>
<p class="mt-2 text-xs text-text-muted">
{#if alreadyInDev}{$t.git?.feature_flow_no_changes || 'Нет изменений для передачи'} · {:else if typeof branch.ahead_of_dev === 'number'}{branch.ahead_of_dev} {$t.git?.feature_flow_commits || 'коммитов сверх DEV'} · {/if}{$t.git?.feature_flow_updated || 'обновлена'} {formatDate(branch.last_updated)}
</p>
<div class="mt-2 flex flex-wrap items-center justify-between gap-2">
<span class="font-mono text-[11px] text-text-muted">{branch.commit_hash?.slice(0, 12) || '—'}</span>
<div class="flex gap-2">
<Button variant="secondary" size="sm" onclick={() => openFeature(branchName)}>
<Icon name="edit" size={14} class="mr-1" />{isCurrent ? ($t.git?.feature_flow_continue || 'Продолжить') : ($t.git?.feature_flow_open || 'Открыть')}
</Button>
{#if !alreadyInDev}
<Button variant="primary" size="sm" onclick={() => { transferBranch = branchName; transferError = null; }}>
{$t.git?.feature_flow_transfer || 'Передать в DEV'}
</Button>
{/if}
</div>
</div>
</article>
{/each}
</div>
<aside class="rounded-md border border-border bg-surface-page px-3 py-2.5">
<h4 class="text-xs font-semibold uppercase tracking-wide text-text-muted">{$t.git?.feature_flow_context || 'Текущая работа'}</h4>
{#if currentFeature}
<p class="mt-2 truncate text-sm font-medium text-text">{featureTitle(String(currentFeature.name))}</p>
<p class="mt-1 text-xs text-text-muted">{$t.git?.feature_flow_current || 'Открыта сейчас'} · {typeof currentFeature.ahead_of_dev === 'number' ? `${currentFeature.ahead_of_dev} ${$t.git?.feature_flow_commits || 'коммитов сверх DEV'}` : formatDate(currentFeature.last_updated)}</p>
{:else}
<p class="mt-2 text-sm font-medium text-text">DEV</p>
<p class="mt-1 text-xs leading-5 text-text-muted">{$t.git?.feature_flow_context_dev || 'Вы работаете в общей разработке. Откройте черновик, чтобы продолжить отдельную доработку.'}</p>
{/if}
</aside>
</div>
{/if}
{#if archivedFeatureBranches.length > 0}
<details class="mt-3 rounded-md border border-border bg-surface-page px-3 py-2 text-xs text-text-muted">
<summary class="cursor-pointer font-medium text-text">{$t.git?.feature_flow_archived || 'Черновики уже в DEV'} · {archivedFeatureBranches.length}</summary>
<div class="mt-2 flex flex-wrap gap-2">
{#each archivedFeatureBranches as branch (branch.name)}
<span class="rounded bg-surface-muted px-2 py-1 font-mono text-[11px]">{branch.name}</span>
{/each}
</div>
</details>
{/if}
</section>
{#if transferBranch}
<div class="fixed inset-0 z-[70] flex items-center justify-center bg-surface-overlay p-4" role="dialog" aria-modal="true" aria-label={$t.git?.feature_flow_transfer_title || 'Передать доработку в общую разработку DEV'}>
<div class="w-full max-w-md rounded-lg border border-border bg-surface-card p-5 shadow-2xl">
<h3 class="text-lg font-semibold text-text">{$t.git?.feature_flow_transfer_title || 'Передать доработку в общую разработку DEV'}</h3>
<p class="mt-2 text-sm text-text-muted">{$t.git?.feature_flow_transfer_confirm || 'Изменения из черновика станут доступны в DEV. В PREPROD и PROD они не попадут, пока вы не запустите публикацию.'}</p>
<dl class="mt-4 space-y-2 rounded-md bg-surface-page p-3 text-sm">
<div class="flex justify-between gap-3"><dt class="text-text-muted">{$t.git?.feature_flow_draft || 'Черновик'}</dt><dd class="font-mono text-text">{transferBranch}</dd></div>
<div class="flex justify-between gap-3"><dt class="text-text-muted">{$t.git?.feature_flow_dev || 'Общая разработка'}</dt><dd class="font-mono text-text">dev</dd></div>
</dl>
{#if transferError}<p class="mt-3 rounded-md border border-destructive-ring bg-destructive-light p-2 text-sm text-destructive" role="alert">{transferError}</p>{/if}
<div class="mt-5 flex justify-end gap-2">
<Button variant="secondary" size="sm" onclick={() => { transferBranch = null; transferError = null; }} disabled={transferring}>{$t.common?.cancel || 'Отмена'}</Button>
<Button variant="primary" size="sm" onclick={transferToDev} isLoading={transferring}>{$t.git?.feature_flow_transfer || 'Передать в DEV'}</Button>
</div>
</div>
</div>
{/if}
<!-- #endregion Git.FeatureWorkflow -->