import { useEffect, useState } from 'react' import { AlertCircle, CheckCircle2 } from 'lucide-react' import { Button } from '@/shared/ui' import { listBranches } from '@/entities/repo' import { checkMergeStatus, createPull } from '@/entities/pull-request' import type { MergeStatus, Pull } from '@/entities/pull-request' import { Modal, modalInputClass, modalLabelClass, modalTextareaClass } from '@/shared/ui' import { useT } from '@/shared/i18n' export interface PullFormModalProps { space: string repo: string defaultBase: string onClose: () => void onCreated: (pull: Pull) => void } /** 新建合并请求:选分支、填标题描述,并实时探测 base…head 的可合并性。 */ export function PullFormModal({ space, repo, defaultBase, onClose, onCreated }: PullFormModalProps) { const { t } = useT() const [branches, setBranches] = useState([]) const [head, setHead] = useState('') const [base, setBase] = useState(defaultBase) const [title, setTitle] = useState('') const [body, setBody] = useState('') const [draft, setDraft] = useState(false) const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const [probe, setProbe] = useState<{ key: string; value: MergeStatus | 'failed' } | null>(null) useEffect(() => { let active = true void listBranches(space, repo, 200) .then(page => { if (!active) return const names = page.items.map(item => item.name) setBranches(names) setHead(prev => prev || names.find(name => name !== defaultBase) || names[0] || '') }) .catch(() => undefined) return () => { active = false } }, [space, repo, defaultBase]) const pairKey = head && base && head !== base ? `${space}/${repo}:${base}...${head}` : '' // 'pending' is derived (pair selected, no result yet) so the effect only // writes state when the probe settles. const status: MergeStatus | 'pending' | null = !pairKey ? null : probe?.key === pairKey ? probe.value === 'failed' ? null : probe.value : 'pending' useEffect(() => { if (!pairKey) return let active = true const timer = setTimeout(() => { void checkMergeStatus(space, repo, base, head) .then(next => { if (active) setProbe({ key: pairKey, value: next }) }) .catch(() => { if (active) setProbe({ key: pairKey, value: 'failed' }) }) }, 400) return () => { active = false clearTimeout(timer) } }, [space, repo, base, head, pairKey]) async function submit() { if (!title.trim() || !head || !base) { setError(t('pull.form.missingFields')) return } if (head === base) { setError(t('pull.form.sameBranch')) return } setBusy(true) setError(null) try { const pull = await createPull(space, repo, { title: title.trim(), head_branch: head, base_branch: base, body: body || null, draft, }) onCreated(pull) } catch (failure) { setError(failure instanceof Error ? failure.message : t('pull.form.createFailed')) setBusy(false) } } return ( } >
{head && base && head !== base ? ( status === 'pending' ? (

{t('pull.form.comparing', { base, head })}

) : status?.mergeable ? (

{t('pull.form.fastForward')}{status.fast_forwardable ? '' : t('pull.form.willMergeCommit')}

) : status ? (

{t('pull.form.conflicts', { count: status.conflicts.length })} {status.conflicts.length ? {status.conflicts.slice(0, 5).join(', ')}{status.conflicts.length > 5 ? ' …' : ''} : null} {t('pull.form.conflictsHint')}

) : null ) : null}