Files
ForgeBucket/frontend/src/api/queries/prs.ts
T
erangel1 0310986644 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
2026-05-07 17:07:16 +02:00

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'] })
},
})
}