import { ErrorPanel, PanelNote } from '@app/components' import type { MyPullScope } from '@app/domains/pulls/model' import { listMyPulls, listPulls, MY_PULL_SCOPES } from '@app/domains/pulls/model' import { useT } from '@app/i18n' import { fmtRelative, repoKey, useAsync } from '@app/lib' import { Button } from '@app/ui' import { IconInbox as Inbox, IconPlus as Plus, IconSearch as Search } from '@tabler/icons-react' import { useMemo, useState } from 'react' import { PullFilter, type PullFilterValue } from '../model/filters' import { userName } from '../model/meta' import { Avatar, MergeableChip, PullStateBadge } from './parts' import { PullFormModal } from './pull-form-modal' const PAGE_SIZE = 20 export interface PullsTabProps { space: string repo: string /** `RepoDetail.id` — the inbox can only deep-link PRs of this repo. */ repoId: string defaultBranch: string /** Repo write access (space owner / repo collaborator server-side). */ canWrite: boolean /** 详情面板当前打开的 pull_ref;与其匹配的行再次点击即关闭详情。 */ activePull?: string | null onOpenPull: (pullRef: string) => void onClosePull: () => void } const SCOPE_KEYS: Record< MyPullScope, 'pull.list.scope.created' | 'pull.list.scope.reviewRequested' | 'pull.list.scope.reviewed' > = { created: 'pull.list.scope.created', 'review-requested': 'pull.list.scope.reviewRequested', reviewed: 'pull.list.scope.reviewed', } /** 合并请求 页签:本仓库列表 + “我的 PR” 收件箱(限定本仓库,可深链)。 */ export function PullsTab({ space, repo, repoId, defaultBranch, canWrite, activePull, onOpenPull, onClosePull, }: PullsTabProps) { const { t, tc } = useT() const [source, setSource] = useState<'repo' | 'mine'>('repo') const [scope, setScope] = useState('review-requested') const [filter, setFilter] = useState({ state: 'open', search: '' }) const [offset, setOffset] = useState(0) const [creating, setCreating] = useState(false) const [tick, setTick] = useState(0) // Filter/source changes reset paging inline (no effect-state ping-pong). const withReset = (fn: () => void) => () => { fn() setOffset(0) } const state = useAsync( async () => { if (source === 'repo') { return listPulls(space, repo, { state: filter.state, ...(filter.search ? { search: filter.search } : {}), limit: PAGE_SIZE, offset, }) } const page = await listMyPulls(scope, { state: filter.state, limit: 50, offset: 0 }) const rows = page.items.filter( (pull) => pull.repo_id === repoId && (filter.state !== 'draft' || pull.is_draft) && (!filter.search || pull.title.toLowerCase().includes(filter.search.toLowerCase())), ) return { items: rows.slice(offset, offset + PAGE_SIZE), total: rows.length, limit: PAGE_SIZE, offset, } }, [space, repo, repoId, source, scope, filter.state, filter.search, offset, tick], { cacheKey: repoKey(space, repo, `pulls:${source}:${scope}:${filter.state}:${filter.search}:${offset}`) }, ) const page = state.data const rows = useMemo(() => page?.items ?? [], [page]) const failure = state.error as { code?: number; message?: string } | undefined return (
{(['repo', 'mine'] as const).map((value) => ( ))}
{source === 'mine' ? (
{MY_PULL_SCOPES.map((value) => ( ))}
) : ( )} {canWrite ? ( ) : null}
withReset(() => setFilter(next))()} /> {state.loading && !page ? : null} {!state.loading && failure ? ( setTick((tick) => tick + 1)} /> ) : null} {!state.loading && !failure && rows.length === 0 ? ( ) : null} {rows.length > 0 ? (
    {rows.map((pull) => { const ref = String(pull.number) const active = activePull != null && (activePull === ref || activePull === pull.id) return (
  • ) })}
) : null} {page && page.total > PAGE_SIZE ? (
{Math.floor(offset / PAGE_SIZE) + 1} / {Math.max(1, Math.ceil(page.total / PAGE_SIZE))} {tc('pull.list.total', page.total)}
) : null} {creating ? ( setCreating(false)} onCreated={(pull) => { setCreating(false) setTick((t) => t + 1) onOpenPull(String(pull.number)) }} /> ) : null}
) }