Skip to content
Clone
cc0c515
329 lines 12.0 KB
/**
* 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 { type ApiEnvelope, unwrapApi } from '@app/lib'
import Apis from '@app/services/api'
import type {
ChangedFile,
InlineDraft,
IssuePullLink,
MergeCmd,
MergeCommitRow,
MergeOutcome,
MergeStatus,
MyPullScope,
NewPullCmd,
NewPullCommentCmd,
Pull,
PullAssignee,
PullChecks,
PullComment,
PullCommentThread,
PullEvent,
PullGate,
PullLabel,
PullPage,
PullPatchCmd,
PullQuery,
PullReview,
PullReviewer,
PullState,
PullSummary,
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 = <T>(value: T | '' | null | undefined): T | undefined => (value ? value : undefined)
const pageOf = <T>(method: PromiseLike<ApiEnvelope<unknown>>): Promise<PullPage<T>> =>
unwrapApi<PullPage<T> | null>(method as PromiseLike<ApiEnvelope<PullPage<T> | null>>).then((p) => ({
items: p?.items ?? [],
total: p?.total ?? 0,
limit: p?.limit ?? 0,
offset: p?.offset ?? 0,
}))
const listOf = <T>(method: PromiseLike<ApiEnvelope<unknown>>): Promise<T[]> =>
unwrapApi<T[] | null>(method as PromiseLike<ApiEnvelope<T[] | null>>).then((rows) => rows ?? [])
const oneOf = <T>(method: PromiseLike<ApiEnvelope<unknown>>): Promise<T> =>
unwrapApi<T>(method as PromiseLike<ApiEnvelope<T>>)
/** Mutation endpoints whose payload we ignore (the generated types disagree
* on whether `data` is null or the updated entity — accept both). */
const ignore = <T>(method: PromiseLike<ApiEnvelope<T>>): Promise<null> => 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<Pull>(
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<Pull>(
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<Pull>(Apis.pulls.get_pull({ pathParams: pathOf(ref) }))
export const createPull = (space: string, repo: string, cmd: NewPullCmd) =>
oneOf<Pull>(
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<Pull>(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<Pull>(Apis.pulls.close_pull({ pathParams: pathOf(ref) }))
export const reopenPull = (ref: PullRef) => oneOf<Pull>(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<MergeOutcome>(Apis.pulls.merge_pull({ pathParams: pathOf(ref), data: cmd }))
export const updateBranchPull = (ref: PullRef) =>
oneOf<Pull>(Apis.pulls.update_branch_pull({ pathParams: pathOf(ref) }))
export const getPullGate = (ref: PullRef) => oneOf<PullGate>(Apis.pulls.get_pull_gate({ pathParams: pathOf(ref) }))
export const refreshPullSummary = (ref: PullRef) =>
oneOf<PullSummary>(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<MergeStatus>(
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<MergeCommitRow>(Apis.pulls.list_pull_commits({ pathParams: pathOf(ref) }))
export const listPullFiles = (ref: PullRef) =>
listOf<ChangedFile>(Apis.pulls.get_pull_files({ pathParams: pathOf(ref) }))
/* ---------------------------------------------------------------- checks */
export const getPullChecks = (ref: PullRef) =>
oneOf<PullChecks>(Apis.pulls.get_pull_checks({ pathParams: pathOf(ref) }))
/* --------------------------------------------------------------- comment */
export const listCommentTree = (ref: PullRef) =>
listOf<PullCommentThread>(Apis.pulls.list_pull_comment_tree({ pathParams: pathOf(ref) }))
export const listCommentsPage = (ref: PullRef, opts: { limit?: number; offset?: number } = {}) =>
pageOf<PullComment>(
Apis.pulls.list_pull_comments({
pathParams: pathOf(ref),
params: { limit: opts.limit, offset: opts.offset ?? 0 },
}),
)
export const createComment = (ref: PullRef, cmd: NewPullCommentCmd) =>
oneOf<PullComment>(
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<PullComment>(
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<PullReview>(Apis.reviews.list_reviews({ pathParams: pathOf(ref) }))
export const getReview = (ref: PullRef, reviewId: string) =>
oneOf<PullReview>(Apis.reviews.get_review({ pathParams: { ...pathOf(ref), review_id: reviewId } }))
export const submitReview = (ref: PullRef, cmd: ReviewCmd) =>
oneOf<PullReview>(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<PullReview>(Apis.reviews.publish_review({ pathParams: { ...pathOf(ref), review_id: reviewId }, data: cmd }))
export const dismissReview = (ref: PullRef, reviewId: string) =>
oneOf<PullReview>(Apis.reviews.dismiss_review({ pathParams: { ...pathOf(ref), review_id: reviewId } }))
export const listRequestedReviewers = (ref: PullRef) =>
listOf<PullReviewer>(Apis.reviews.list_requested_reviewers({ pathParams: pathOf(ref) }))
export const requestReviewers = (ref: PullRef, reviewers: string[], required?: boolean) =>
listOf<PullReviewer>(
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<PullEvent>(
Apis.pulls.list_pull_events({
pathParams: pathOf(ref),
params: { limit: opts.limit, offset: opts.offset ?? 0 },
}),
)
export const listAssignees = (ref: PullRef) =>
listOf<PullAssignee>(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<PullLabel>(Apis.pulls.list_pull_labels({ pathParams: pathOf(ref) }))
/** 可附加的标签候选(仓库目录标签库,未归档),供侧栏搜索下拉使用。 */
export const listPullLabelOptions = (space: string, repo: string) =>
listOf<PullLabel>(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<IssuePullLink>(
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,
})