import { useMemo, useState } from 'react' import { PanelNote, ErrorPanel, Button, modalTextareaClass } from '@/shared/ui' import { useAsync } from '@/shared/lib' import { createComment, listCommentTree, listPullFiles, submitReview, type InlineDraft, type PullRef, } from '@/entities/pull-request' import type { ReviewIntent } from '@/entities/pull-request' import { Copyable, InlineDiffCard } from './parts' import { anchorKey, changedFileToDiff, type AnchoredComment, type InlineAnchor } from '../model/meta' export interface FilesTabProps { refObj: PullRef headSha: string /** File path to expand/scroll to when jumping from an inline comment. */ focusPath: string onChanged: () => void } /** 变更文件页签:逐文件 diff + 行内评论 + 待提交评审草稿。 */ export function FilesTab({ refObj, headSha, focusPath, onChanged }: FilesTabProps) { const [tick, setTick] = useState(0) const [collapsed, setCollapsed] = useState>({}) const [draftAnchor, setDraftAnchor] = useState(null) const [draftBody, setDraftBody] = useState('') const [pending, setPending] = useState([]) const [reviewBody, setReviewBody] = useState('') const [busy, setBusy] = useState(false) const [error, setError] = useState(null) const files = useAsync(() => listPullFiles(refObj), [refObj.space, refObj.repo, refObj.pull, tick]) const threads = useAsync(() => listCommentTree(refObj), [refObj.space, refObj.repo, refObj.pull, tick]) const commentsByLine = useMemo(() => { const map = new Map() for (const thread of threads.data ?? []) { const root = thread.root if (!root.path || root.line == null) continue const key = anchorKey(root.path, root.line, 'right') const list = map.get(key) ?? [] list.push({ id: root.id, body: root.body }, ...thread.replies.map(r => ({ id: r.id, body: r.body }))) map.set(key, list) } return map }, [threads.data]) const rows = useMemo(() => (files.data ?? []).map(file => changedFileToDiff(file)), [files.data]) const totalAdd = rows.reduce((sum, row) => sum + row.additions, 0) const totalDel = rows.reduce((sum, row) => sum + row.deletions, 0) const failure = files.error as { code?: number; message?: string } | undefined async function postLineComment() { if (!draftAnchor || !draftBody.trim()) return setBusy(true) setError(null) try { await createComment(refObj, { body: draftBody.trim(), path: draftAnchor.path, line: draftAnchor.line, side: draftAnchor.side === 'left' ? 'left' : 'right', commit_sha: headSha || null, }) setDraftAnchor(null) setDraftBody('') setTick(t => t + 1) onChanged() } catch (f) { setError(f instanceof Error ? f.message : '评论失败') } finally { setBusy(false) } } function queueAsReviewDraft() { if (!draftAnchor || !draftBody.trim()) return setPending(prev => [...prev, { path: draftAnchor.path, line: draftAnchor.line, side: draftAnchor.side, body: draftBody.trim() }]) setDraftAnchor(null) setDraftBody('') } async function submitPendingReview(state: ReviewIntent) { setBusy(true) setError(null) try { await submitReview(refObj, { state, body: reviewBody || null, comments: pending }) setPending([]) setReviewBody('') setTick(t => t + 1) onChanged() } catch (f) { setError(f instanceof Error ? f.message : '评审提交失败') } finally { setBusy(false) } } if (files.loading && !files.data) return if (failure) return setTick(t => t + 1)} /> if (!rows.length) return return (

{rows.length} 个文件改动 · +{totalAdd}{' '} −{totalDel} {headSha ? <> · 对比版本 : null}

{rows.map(diff => { const open = !(collapsed[diff.path] ?? false) || diff.path === focusPath return (
{open ? ( { setDraftAnchor(anchor); setDraftBody('') }} renderDraft={anchor => draftAnchor && draftAnchor.path === anchor.path && draftAnchor.line === anchor.line && draftAnchor.side === anchor.side ? (