Backend (prs.go):

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
This commit is contained in:
2026-05-07 17:07:16 +02:00
parent 7436679eac
commit 0310986644
11 changed files with 896 additions and 78 deletions
+58 -2
View File
@@ -3,6 +3,12 @@ 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(),
@@ -14,6 +20,7 @@ const prSchema = z.object({
status: z.enum(['open', 'merged', 'closed']),
createdAt: z.string(),
updatedAt: z.string(),
reviewers: z.array(prReviewerSchema).default([]),
})
const prsSchema = z.array(prSchema)
@@ -38,13 +45,62 @@ export function usePR(owner: string, repo: string, prId: number) {
})
}
export function useMergePR(owner: string, repo: string) {
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(`/api/v1/repos/${owner}/${repo}/pulls/${prId}/merge`, mergeResponseSchema, {}),
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'] })
},
})
}