/** * Pull-request transport: every call the PR pages make, wrapped in the shared * envelope contract so the UI only ever sees unwrapped rows or an `ApiFailure`. * * PRs are addressable by uuid *or* number (`pull_ref`), so every `pull` * parameter below accepts either form. */ import Apis from '@/shared/api' import { unwrapApi, type ApiEnvelope } from '@/shared/lib' import type { InlineDraft, MergeCmd, MergeOutcome, MergeStatus, MyPullScope, NewPullCmd, NewPullCommentCmd, Pull, PullAssignee, PullChecks, PullComment, PullCommentThread, PullEvent, IssuePullLink, PullGate, PullLabel, PullPage, PullPatchCmd, PullQuery, PullReview, PullReviewer, PullState, PullSummary, ChangedFile, MergeCommitRow, ReviewCmd, } from './pull' /** `pull_ref` (number as string or uuid) plus the repo the PR lives in. */ export interface PullRef { space: string repo: string pull: string } const nz = (value: T | '' | null | undefined): T | undefined => (value ? value : undefined) const pageOf = (method: PromiseLike>): Promise> => unwrapApi | null>(method as PromiseLike | null>>).then(p => ({ items: p?.items ?? [], total: p?.total ?? 0, limit: p?.limit ?? 0, offset: p?.offset ?? 0, })) const listOf = (method: PromiseLike>): Promise => unwrapApi(method as PromiseLike>).then(rows => rows ?? []) const oneOf = (method: PromiseLike>): Promise => unwrapApi(method as PromiseLike>) /** Mutation endpoints whose payload we ignore (the generated types disagree * on whether `data` is null or the updated entity — accept both). */ const ignore = (method: PromiseLike>): Promise => unwrapApi(method).then(() => null) const pathOf = (ref: PullRef) => ({ space_name: ref.space, repo_name: ref.repo, pull_ref: ref.pull, }) /* ------------------------------------------------------------------- list */ export const listPulls = (space: string, repo: string, query: PullQuery = {}) => pageOf( Apis.pulls.list_pulls({ pathParams: { space_name: space, repo_name: repo }, params: { // `draft` is a UI-only choice the backend spells as state=open + draft=true. state: query.state === 'all' || !query.state ? 'all' : query.state === 'draft' ? 'open' : query.state, draft: query.state === 'draft' ? true : query.draft, author: nz(query.author), base: nz(query.base), head: nz(query.head), search: nz(query.search), sort: nz(query.sort), order: nz(query.order), limit: query.limit, offset: query.offset ?? 0, }, }), ) /** Cross-repo inbox: PRs created by / needing review from / reviewed by me. */ export const listMyPulls = (scope: MyPullScope, opts: { state?: PullState | 'all' | 'draft'; limit?: number; offset?: number } = {}) => pageOf( Apis.pulls.list_my_pulls({ pathParams: { scope }, params: { // The inbox endpoint has no draft filter; `draft` maps to open. state: opts.state && opts.state !== 'all' && opts.state !== 'draft' ? opts.state : 'open', limit: opts.limit, offset: opts.offset ?? 0, }, }), ) export const getPull = (ref: PullRef) => oneOf(Apis.pulls.get_pull({ pathParams: pathOf(ref) })) export const createPull = (space: string, repo: string, cmd: NewPullCmd) => oneOf( Apis.pulls.create_pull({ pathParams: { space_name: space, repo_name: repo }, data: { title: cmd.title, head_branch: cmd.head_branch, base_branch: cmd.base_branch ?? null, body: cmd.body ?? null, draft: cmd.draft ?? false, issue_id: cmd.issue_id ?? null, }, }), ) export const updatePull = (ref: PullRef, cmd: PullPatchCmd) => oneOf(Apis.pulls.update_pull({ pathParams: pathOf(ref), data: cmd })) export const deletePull = (ref: PullRef) => ignore(Apis.pulls.delete_pull({ pathParams: pathOf(ref) })) export const closePull = (ref: PullRef) => oneOf(Apis.pulls.close_pull({ pathParams: pathOf(ref) })) export const reopenPull = (ref: PullRef) => oneOf(Apis.pulls.reopen_pull({ pathParams: pathOf(ref) })) export const lockPull = (ref: PullRef) => ignore(Apis.pulls.lock_pull({ pathParams: pathOf(ref) })) export const unlockPull = (ref: PullRef) => ignore(Apis.pulls.unlock_pull({ pathParams: pathOf(ref) })) /* ----------------------------------------------------------------- merge */ export const mergePull = (ref: PullRef, cmd: MergeCmd) => oneOf(Apis.pulls.merge_pull({ pathParams: pathOf(ref), data: cmd })) export const updateBranchPull = (ref: PullRef) => oneOf(Apis.pulls.update_branch_pull({ pathParams: pathOf(ref) })) export const getPullGate = (ref: PullRef) => oneOf(Apis.pulls.get_pull_gate({ pathParams: pathOf(ref) })) export const refreshPullSummary = (ref: PullRef) => oneOf(Apis.pulls.refresh_pull_summary({ pathParams: pathOf(ref) })) /** Mergeability probe for the create dialog (`GET …/conflicts?base=&head=`). */ export const checkMergeStatus = (space: string, repo: string, base: string, head: string) => oneOf( Apis.conflicts.get_conflicts({ pathParams: { space_name: space, repo_name: repo }, params: { base, head }, }), ) /** Raw `.patch` download URL (browser navigation, not a JSON call). */ export const pullPatchUrl = (ref: PullRef) => `/api/v1/repos/${encodeURIComponent(ref.space)}/${encodeURIComponent(ref.repo)}/pulls/${encodeURIComponent(ref.pull)}/patch` /* --------------------------------------------------------------- commits */ export const listPullCommits = (ref: PullRef) => listOf(Apis.pulls.list_pull_commits({ pathParams: pathOf(ref) })) export const listPullFiles = (ref: PullRef) => listOf(Apis.pulls.get_pull_files({ pathParams: pathOf(ref) })) /* ---------------------------------------------------------------- checks */ export const getPullChecks = (ref: PullRef) => oneOf(Apis.pulls.get_pull_checks({ pathParams: pathOf(ref) })) /* --------------------------------------------------------------- comment */ export const listCommentTree = (ref: PullRef) => listOf(Apis.pulls.list_pull_comment_tree({ pathParams: pathOf(ref) })) export const listCommentsPage = (ref: PullRef, opts: { limit?: number; offset?: number } = {}) => pageOf( Apis.pulls.list_pull_comments({ pathParams: pathOf(ref), params: { limit: opts.limit, offset: opts.offset ?? 0 }, }), ) export const createComment = (ref: PullRef, cmd: NewPullCommentCmd) => oneOf( Apis.pulls.create_pull_comment({ pathParams: pathOf(ref), data: { body: cmd.body, parent_comment_id: cmd.parent_comment_id ?? null, path: cmd.path ?? null, line: cmd.line ?? null, side: cmd.side ?? null, commit_sha: cmd.commit_sha ?? null, review_id: cmd.review_id ?? null, }, }), ) export const updateComment = (ref: PullRef, commentId: string, body: string) => oneOf( Apis.pulls.update_pull_comment({ pathParams: { ...pathOf(ref), comment_id: commentId }, data: { body }, }), ) export const deleteComment = (ref: PullRef, commentId: string) => ignore( Apis.pulls.delete_pull_comment({ pathParams: { ...pathOf(ref), comment_id: commentId } }), ) export const resolveComment = (ref: PullRef, commentId: string) => ignore( Apis.pulls.resolve_pull_comment({ pathParams: { ...pathOf(ref), comment_id: commentId } }), ) export const unresolveComment = (ref: PullRef, commentId: string) => ignore( Apis.pulls.unresolve_pull_comment({ pathParams: { ...pathOf(ref), comment_id: commentId } }), ) /* ---------------------------------------------------------------- review */ export const listReviews = (ref: PullRef) => listOf(Apis.reviews.list_reviews({ pathParams: pathOf(ref) })) export const getReview = (ref: PullRef, reviewId: string) => oneOf( Apis.reviews.get_review({ pathParams: { ...pathOf(ref), review_id: reviewId } }), ) export const submitReview = (ref: PullRef, cmd: ReviewCmd) => oneOf(Apis.reviews.submit_review({ pathParams: pathOf(ref), data: cmd })) /** Publish a pending review (optionally with final body/inline drafts). */ export const publishReview = (ref: PullRef, reviewId: string, cmd: ReviewCmd) => oneOf( Apis.reviews.publish_review({ pathParams: { ...pathOf(ref), review_id: reviewId }, data: cmd }), ) export const dismissReview = (ref: PullRef, reviewId: string) => oneOf( Apis.reviews.dismiss_review({ pathParams: { ...pathOf(ref), review_id: reviewId } }), ) export const listRequestedReviewers = (ref: PullRef) => listOf(Apis.reviews.list_requested_reviewers({ pathParams: pathOf(ref) })) export const requestReviewers = (ref: PullRef, reviewers: string[], required?: boolean) => listOf( Apis.reviews.request_reviewers({ pathParams: pathOf(ref), data: { reviewers, required: required ?? undefined }, }), ) export const removeRequestedReviewer = (ref: PullRef, userId: number) => ignore( Apis.reviews.delete_requested_reviewer({ pathParams: { ...pathOf(ref), user_id: userId } }), ) /* ------------------------------------------------------------- meta side */ export const listEvents = (ref: PullRef, opts: { limit?: number; offset?: number } = {}) => pageOf( Apis.pulls.list_pull_events({ pathParams: pathOf(ref), params: { limit: opts.limit, offset: opts.offset ?? 0 }, }), ) export const listAssignees = (ref: PullRef) => listOf(Apis.pulls.list_pull_assignees({ pathParams: pathOf(ref) })) export const addAssignee = (ref: PullRef, userRef: string) => ignore( Apis.pulls.add_pull_assignee({ pathParams: { ...pathOf(ref), user_ref: userRef } }), ) export const removeAssignee = (ref: PullRef, userRef: string) => ignore( Apis.pulls.remove_pull_assignee({ pathParams: { ...pathOf(ref), user_ref: userRef } }), ) export const listPullLabels = (ref: PullRef) => listOf(Apis.pulls.list_pull_labels({ pathParams: pathOf(ref) })) /** 可附加的标签候选(仓库目录标签库,未归档),供侧栏搜索下拉使用。 */ export const listPullLabelOptions = (space: string, repo: string) => listOf( Apis.pulls.list_label_options({ pathParams: { space_name: space, repo_name: repo } }), ) export const attachLabel = (ref: PullRef, label: string) => ignore(Apis.pulls.attach_pull_label({ pathParams: pathOf(ref), data: { label } })) export const detachLabel = (ref: PullRef, labelRef: string) => ignore( Apis.pulls.detach_pull_label({ pathParams: { ...pathOf(ref), label_ref: labelRef } }), ) /* ----------------------------------------------------- issue-side links */ /** PRs linked to one issue (`GET …/issues/{id}/pulls`), with relation ids. */ export const listIssuePulls = (space: string, board: string, issueId: string) => listOf( Apis.pulls.list_issue_pulls({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, }), ) export const linkIssuePull = ( space: string, board: string, issueId: string, cmd: { space_name: string; repo_name: string; pull_ref: string }, ) => ignore( Apis.pulls.link_issue_pull({ pathParams: { space_name: space, board_id: board, issue_id: issueId }, data: cmd, }), ) export const unlinkIssuePull = (space: string, board: string, issueId: string, relationId: string) => ignore( Apis.pulls.unlink_issue_pull({ pathParams: { space_name: space, board_id: board, issue_id: issueId, relation_id: relationId, }, }), ) /** Inline-draft helper kept next to the transport the files tab uses. */ export const draftFrom = (path: string, line: number, side: string | null, body: string): InlineDraft => ({ path, line, side, body, })