174 lines
11 KiB
Svelte
174 lines
11 KiB
Svelte
<!-- #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 -->
|