0310986644
Update — PATCH /{prID} edits title and/or body, validates title non-empty, returns prWithReviewers
Reopen — POST /{prID}/reopen transitions closed → open, fires webhook
Close now returns prWithReviewers and fires a webhook
Merge already existed; no changes needed
Frontend — PRDetailPage.tsx full rewrite:
Inline title editing — pencil icon (visible to author/admin when open), Enter to save, Esc to cancel
Inline body editing — same pattern in the description panel
Merge sidebar — radio buttons for allowed strategies (fetched from repo's merge strategy settings), "Merge pull request" button in Bitbucket purple, "Close without merging" below it
Status banner — merged (purple) or closed (grey) with the date, shown below the description
File list — scrollable +N −N table above the diff viewer showing all changed files with addition/deletion counts
Reopen button — appears in the sidebar when the PR is closed
Reviewers panel — lists assigned reviewers with avatars/initials
Details panel — from/into branches, opened date, last updated
Quick links — back to all PRs, open new PR
PRsPage.tsx — now shows real data:
Two tabs: "My pull requests" and "Awaiting my review" (with live counts from dashboard)
Per-repo quick links at the bottom showing open PR count badges
196 lines
6.7 KiB
TypeScript
196 lines
6.7 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { z } from 'zod'
|
|
import { api } from '../client'
|
|
import type { PullRequest } from '../../types/api'
|
|
|
|
const prReviewerSchema = z.object({
|
|
userId: z.number(),
|
|
username: z.string(),
|
|
avatarUrl: z.string(),
|
|
})
|
|
|
|
const prSchema = z.object({
|
|
id: z.number(),
|
|
repoId: z.number(),
|
|
authorId: z.number(),
|
|
title: z.string(),
|
|
body: z.string(),
|
|
sourceBranch: z.string(),
|
|
targetBranch: z.string(),
|
|
status: z.enum(['open', 'merged', 'closed']),
|
|
createdAt: z.string(),
|
|
updatedAt: z.string(),
|
|
reviewers: z.array(prReviewerSchema).default([]),
|
|
})
|
|
|
|
const prsSchema = z.array(prSchema)
|
|
|
|
const mergeResponseSchema = z.object({ status: z.string() })
|
|
|
|
export function usePRs(owner: string, repo: string) {
|
|
return useQuery({
|
|
queryKey: ['repos', owner, repo, 'pulls'],
|
|
queryFn: () =>
|
|
api.get<PullRequest[]>(`/api/v1/repos/${owner}/${repo}/pulls`, prsSchema),
|
|
enabled: Boolean(owner && repo),
|
|
})
|
|
}
|
|
|
|
export function usePR(owner: string, repo: string, prId: number) {
|
|
return useQuery({
|
|
queryKey: ['repos', owner, repo, 'pulls', prId],
|
|
queryFn: () =>
|
|
api.get<PullRequest>(`/api/v1/repos/${owner}/${repo}/pulls/${prId}`, prSchema),
|
|
enabled: Boolean(owner && repo && prId),
|
|
})
|
|
}
|
|
|
|
export function useUpdatePR(owner: string, repo: string, prId: number) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (data: { title?: string; body?: string }) =>
|
|
api.patch<PullRequest>(`/api/v1/repos/${owner}/${repo}/pulls/${prId}`, prSchema, data),
|
|
onSuccess: (updated) => {
|
|
queryClient.setQueryData(['repos', owner, repo, 'pulls', prId], updated)
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'pulls'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useReopenPR(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (prId: number) =>
|
|
api.post<PullRequest>(`/api/v1/repos/${owner}/${repo}/pulls/${prId}/reopen`, prSchema, {}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'pulls'] })
|
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useCreatePR(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (data: { title: string; body: string; sourceBranch: string; targetBranch: string }) =>
|
|
api.post<PullRequest>(`/api/v1/repos/${owner}/${repo}/pulls`, prSchema, data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'pulls'] })
|
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useMergePR(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: ({ prId, strategy }: { prId: number; strategy: string }) =>
|
|
api.post(`/api/v1/repos/${owner}/${repo}/pulls/${prId}/merge`, mergeResponseSchema, { strategy }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'pulls'] })
|
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useClosePR(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (prId: number) =>
|
|
api.post<PullRequest>(`/api/v1/repos/${owner}/${repo}/pulls/${prId}/close`, prSchema, {}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'pulls'] })
|
|
queryClient.invalidateQueries({ queryKey: ['dashboard'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
// ─── PR settings ──────────────────────────────────────────────────────────────
|
|
|
|
export interface DefaultReviewer {
|
|
userId: number
|
|
username: string
|
|
avatarUrl: string
|
|
}
|
|
|
|
const reviewerSchema = z.object({
|
|
userId: z.number(),
|
|
username: z.string(),
|
|
avatarUrl: z.string(),
|
|
})
|
|
const reviewersSchema = z.array(reviewerSchema)
|
|
|
|
const descriptionSchema = z.object({ template: z.string() })
|
|
const excludedFilesSchema = z.object({ patterns: z.string() })
|
|
|
|
export function useDefaultReviewers(owner: string, repo: string) {
|
|
return useQuery({
|
|
queryKey: ['repos', owner, repo, 'default-reviewers'],
|
|
queryFn: () =>
|
|
api.get<DefaultReviewer[]>(`/api/v1/repos/${owner}/${repo}/default-reviewers`, reviewersSchema),
|
|
enabled: Boolean(owner && repo),
|
|
})
|
|
}
|
|
|
|
export function useAddDefaultReviewer(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (username: string) =>
|
|
api.post<DefaultReviewer>(`/api/v1/repos/${owner}/${repo}/default-reviewers`, reviewerSchema, { username }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'default-reviewers'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useRemoveDefaultReviewer(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (username: string) =>
|
|
api.delete(`/api/v1/repos/${owner}/${repo}/default-reviewers/${encodeURIComponent(username)}`, z.any()),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'default-reviewers'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDefaultDescription(owner: string, repo: string) {
|
|
return useQuery({
|
|
queryKey: ['repos', owner, repo, 'default-description'],
|
|
queryFn: () =>
|
|
api.get<{ template: string }>(`/api/v1/repos/${owner}/${repo}/default-description`, descriptionSchema),
|
|
enabled: Boolean(owner && repo),
|
|
})
|
|
}
|
|
|
|
export function useUpdateDefaultDescription(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (template: string) =>
|
|
api.put<{ template: string }>(`/api/v1/repos/${owner}/${repo}/default-description`, descriptionSchema, { template }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'default-description'] })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useExcludedFiles(owner: string, repo: string) {
|
|
return useQuery({
|
|
queryKey: ['repos', owner, repo, 'excluded-files'],
|
|
queryFn: () =>
|
|
api.get<{ patterns: string }>(`/api/v1/repos/${owner}/${repo}/excluded-files`, excludedFilesSchema),
|
|
enabled: Boolean(owner && repo),
|
|
})
|
|
}
|
|
|
|
export function useUpdateExcludedFiles(owner: string, repo: string) {
|
|
const queryClient = useQueryClient()
|
|
return useMutation({
|
|
mutationFn: (patterns: string) =>
|
|
api.put<{ patterns: string }>(`/api/v1/repos/${owner}/${repo}/excluded-files`, excludedFilesSchema, { patterns }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['repos', owner, repo, 'excluded-files'] })
|
|
},
|
|
})
|
|
}
|