From 068ebcb3604a0f808c7b84f250af28efccb621cb Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Sat, 25 Jul 2026 15:50:05 -0400 Subject: [PATCH] add pull requests tab --- src/components/repo/RepoPulls.tsx | 57 +++++++++++++++++++++++++++++++ src/hooks/useRepoPulls.ts | 26 ++++++++++++++ src/lib/tangled/repo/index.ts | 2 ++ src/lib/tangled/repo/listPulls.ts | 57 +++++++++++++++++++++++++++++++ src/pages/RepoPage.tsx | 5 ++- 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/components/repo/RepoPulls.tsx create mode 100644 src/hooks/useRepoPulls.ts create mode 100644 src/lib/tangled/repo/listPulls.ts diff --git a/src/components/repo/RepoPulls.tsx b/src/components/repo/RepoPulls.tsx new file mode 100644 index 0000000..ff8accf --- /dev/null +++ b/src/components/repo/RepoPulls.tsx @@ -0,0 +1,57 @@ +import type { Did } from '@atcute/lexicons' +import { IconGitPullRequest, IconMessageCircle } from '@tabler/icons-react' +import { useRepoPulls } from '../../hooks/useRepoPulls' +import { SurfaceCard } from '../shared/SurfaceCard' + +type RepoPullsProps = { + repoDid: Did +} + +export function RepoPulls({ repoDid }: RepoPullsProps) { + const { pulls, error } = useRepoPulls(repoDid) + + if (error) { + return

Could not load pulls: {error.message}

+ } + + if (pulls === null) { + return

Loading pulls...

+ } + + if (pulls.items.length === 0) { + return

No pulls found.

+ } + + return ( + + ) +} diff --git a/src/hooks/useRepoPulls.ts b/src/hooks/useRepoPulls.ts new file mode 100644 index 0000000..2d3f631 --- /dev/null +++ b/src/hooks/useRepoPulls.ts @@ -0,0 +1,26 @@ +import type { Did } from '@atcute/lexicons' +import { useEffect, useState } from 'react' +import { listPulls, type PullList } from '../lib/tangled/repo' + +const INITIAL_PULL_LIMIT = 20 + +export function useRepoPulls(repoDid: Did) { + const [pulls, setPulls] = useState(null) + const [error, setError] = useState(null) + + useEffect(() => { + async function loadPulls() { + setError(null) + try { + const result = await listPulls(repoDid, { limit: INITIAL_PULL_LIMIT }) + setPulls(result) + } catch (caught) { + setError(caught instanceof Error ? caught : new Error('Unable to load pulls.')) + } + } + + void loadPulls() + }, [repoDid]) + + return { pulls, error } +} diff --git a/src/lib/tangled/repo/index.ts b/src/lib/tangled/repo/index.ts index 470db78..4df9535 100644 --- a/src/lib/tangled/repo/index.ts +++ b/src/lib/tangled/repo/index.ts @@ -9,6 +9,8 @@ export type { RepoLogOptions } from './getRepoLog' export { getRepoTree } from './getRepoTree' export { listIssues } from './listIssues' export type { Issue, IssueList, ListIssuesOptions } from './listIssues' +export { listPulls } from './listPulls' +export type { ListPullsOptions, Pull, PullList } from './listPulls' export { listRepos } from './listRepos' export { getRepoName, getRepoRkey } from './types' export type { Repo, RepoList } from './types' diff --git a/src/lib/tangled/repo/listPulls.ts b/src/lib/tangled/repo/listPulls.ts new file mode 100644 index 0000000..f65d1f0 --- /dev/null +++ b/src/lib/tangled/repo/listPulls.ts @@ -0,0 +1,57 @@ +import { ok } from '@atcute/client' +import type { Did } from '@atcute/lexicons' +import { safeParse } from '@atcute/lexicons' +import type { Main as TangledPull } from '@atcute/tangled/types/repo/pull' +import { mainSchema as pullSchema } from '@atcute/tangled/types/repo/pull' +import type { $output as PullListResponse } from '@atcute/tangled/types/repo/listPulls' +import { mainSchema as pullListSchema } from '@atcute/tangled/types/repo/listPulls' +import { rpc } from '../client' +import { removeNullCursor } from '../utils' + +export type Pull = Omit & { + value: TangledPull +} + +export type PullList = { + items: Pull[] + cursor?: string +} + +export type ListPullsOptions = { + cursor?: string + limit?: number + order?: 'asc' | 'desc' + status?: 'open' | 'closed' | 'merged' +} + +export async function listPulls(repoDid: Did, options: ListPullsOptions = {}): Promise { + const response = await ok( + rpc.get('sh.tangled.repo.listPulls', { + params: { + subject: repoDid, + cursor: options.cursor, + limit: options.limit, + order: options.order, + status: options.status, + }, + }), + ) + + const listValidation = safeParse(pullListSchema.output.schema, removeNullCursor(response)) + if (!listValidation.ok) { + throw new Error(`Bobbin returned an invalid pull list: ${listValidation.message}`) + } + + const items = listValidation.value.items.map((item, index) => { + const valueValidation = safeParse(pullSchema, item.value) + if (!valueValidation.ok) { + throw new Error( + `Bobbin returned an invalid pull record at index ${index}: ${valueValidation.message}`, + ) + } + + return { ...item, value: valueValidation.value } + }) + + return { items, cursor: listValidation.value.cursor } +} diff --git a/src/pages/RepoPage.tsx b/src/pages/RepoPage.tsx index 0299dfd..e505516 100644 --- a/src/pages/RepoPage.tsx +++ b/src/pages/RepoPage.tsx @@ -3,6 +3,7 @@ import { useParams, useSearchParams } from 'react-router-dom' import { PageContainer } from '../components/layout/PageContainer' import { ProfileByline } from '../components/profile/ProfileByline' import { RepoIssues } from '../components/repo/RepoIssues' +import { RepoPulls } from '../components/repo/RepoPulls' import { RepoReadme } from '../components/repo/RepoReadme' import { parseRepoSection } from '../components/repo/repoSections' import { RepoTabs } from '../components/repo/RepoTabs' @@ -77,7 +78,9 @@ export function RepoPage() { {activeSection === 'issues' && repo.value.repoDid !== undefined && ( )} - {activeSection === 'pulls' && } + {activeSection === 'pulls' && repo.value.repoDid !== undefined && ( + + )} {activeSection === 'pipelines' && } -- 2.51.2