import { useId, useMemo, useState } from "react"; import type { GithubDetail, GithubFile, GithubFilter, GithubItem, GithubKind, GithubList, } from "@ace/channel/protocol"; import { Button, DiffView, type Event, Input, ProjectSidebar, type SessionSidebarRepo, Timeline, useLayoutLeft, useLocalStorage, useMedia, } from "@ace/ui"; import { IconChevronRight, IconCircleCheck, IconExternal, IconIssue, IconListTree, IconLoader, IconMerge, IconPullRequest, IconPullRequestClosed, IconRotate, IconSearch, IconSidebar, } from "@ace/ui/icons"; import { type Query, useGithub } from "./github-cache"; import type { GithubTarget } from "./github-link"; import { host } from "./host"; import type { AppProject } from "./projects"; const NAMES = { issues: "Issues", prs: "Pull requests" }; const REVIEWS = { approved: "Approved", changes_requested: "Requested changes", commented: "Reviewed", dismissed: "Review dismissed", }; const DATE = new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", year: "numeric", }); type Filter = { state: GithubFilter; search: string; limit: number }; const DEFAULT_FILTER: Filter = { state: "open", search: "", limit: 50 }; const ICONS = { issues: { open: IconIssue, closed: IconCircleCheck, merged: IconMerge, draft: IconIssue }, prs: { open: IconPullRequest, closed: IconPullRequestClosed, merged: IconMerge, draft: IconPullRequest, }, }; const COLORS = { issues: { open: "text-accent", closed: "text-merged", merged: "text-merged", draft: "text-muted-foreground", }, prs: { open: "text-accent", closed: "text-destructive", merged: "text-merged", draft: "text-muted-foreground", }, }; function useGithubFilter(kind: GithubKind, project: AppProject) { return useLocalStorage( `ace:github-filter:${host.url}:${project.id}:${kind}`, DEFAULT_FILTER, ); } export function GithubSidebar({ kind, project, repos, connected, onProject, onOpen, onTarget }: { kind: GithubKind; project: AppProject; repos: SessionSidebarRepo[]; connected: boolean; onProject: (id: string) => void; onOpen: () => void; onTarget: (target?: GithubTarget) => void; }) { const [filter, setFilter] = useGithubFilter(kind, project); const left = useLayoutLeft(); const phone = useMedia("(width < 40rem)"); return ( onProject(repo.id)} onAddRepo={connected ? onOpen : undefined} >

{NAMES[kind]}

); } function StateIcon({ item }: { item: Pick }) { const Icon = ICONS[item.kind][item.state]; const color = COLORS[item.kind][item.state]; return ; } function Notice({ error, loading, empty, onRetry }: { error?: string; loading?: boolean; empty: string; onRetry: () => void; }) { return (
{loading && !error && }

{error || (loading ? "Loading from GitHub…" : empty)}

{error && }
); } function Refresh({ loading, onClick }: { loading: boolean; onClick: () => void }) { return ( ); } function Labels({ labels }: Pick) { return labels.map((label) => ( {label.name} )); } function Items({ items }: Pick) { return ( ); } function discussion(item: GithubDetail): Event[] { const entries = [ { id: "description", author: item.author, body: item.body || "No description provided.", created: item.created, }, ...item.comments.map((comment) => ({ ...comment, body: comment.review ? `**${REVIEWS[comment.review]}**\n\n${comment.body}` : comment.body, })), ]; return entries.map((entry) => ({ id: `${item.url}:${entry.id}`, uid: entry.id, type: "message", topic: item.url, created_at: Math.floor(Date.parse(entry.created) / 1000), sender: { kind: "user", value: entry.author, display: entry.author }, content: [{ type: "text", text: entry.body }], })); } function DetailHeader({ item, target, onTarget, id }: { item: GithubDetail; target: GithubTarget; onTarget: (target: GithubTarget) => void; id: string; }) { return (

{item.title} #{item.number}

{item.state} {item.author} opened on {DATE.format(new Date(item.created))} {item.pull && ( {item.pull.head} → {item.pull.base} )}
{item.pull && (
{ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return; event.preventDefault(); const view = event.key === "Home" ? "discussion" : event.key === "End" ? "files" : target.view === "files" ? "discussion" : "files"; onTarget({ ...target, view, anchor: "" }); document.getElementById(`${id}-${view}`)?.focus(); }} > {(["discussion", "files"] as const).map((view) => ( ))}
)}
); } function Files( { data, pull }: { data: Query; pull: NonNullable }, ) { if (!data.value) { return ( ); } return ( <> {data.error &&

{data.error}

} ); } function DetailBody({ item, target, files, id }: { item: GithubDetail; target: GithubTarget; files: Query; id: string; }) { const events = useMemo(() => discussion(item), [item]); const comment = item.comments.find((comment) => target.anchor && comment.url.endsWith(target.anchor) ); return (
{target.view === "files" && item.pull ? : ( )}
); } function Detail({ target, onTarget }: { target: GithubTarget; onTarget: (target?: GithubTarget) => void; }) { const id = useId(); const data = useGithub({ op: "github-detail", repo: target.repo, kind: target.kind, number: target.number, }); const files = useGithub( target.view === "files" ? { op: "github-files", repo: target.repo, number: target.number } : undefined, ); const item = data.value; function refresh() { data.refresh(); files.refresh(); } return (
{target.repo} #{target.number} Open in GitHub
{!item ? ( ) : ( <> {data.error && (

{data.error}

)} )}
); } function Results({ kind, repo, data, filter, onFilter, onRetry }: { kind: GithubKind; repo: Query; data: Query; filter: Filter; onFilter: (filter: Filter) => void; onRetry: () => void; }) { const error = repo.error || data.error; const loading = repo.loading || data.loading; if (!repo.value) { return ( ); } if (!data.value?.items.length) { const state = filter.state === "all" ? "" : `${filter.state} `; const scope = filter.search ? " match your search" : " in this project"; return ( ); } return ( <> {error &&

{error}

} {data.value.more && filter.limit < 1000 && (
)} {data.value.items.length === 1000 && (

Showing the first 1,000 results. Search to narrow the list.

)} ); } export function Github({ kind, project, target, onTarget }: { kind: GithubKind; project: AppProject; target?: GithubTarget; onTarget: (target?: GithubTarget) => void; }) { const [filter, setFilter] = useGithubFilter(kind, project); const [search, setSearch] = useState(filter.search); const left = useLayoutLeft(); const repo = useGithub( target ? undefined : { op: "project-repo", project: project.path, host: project.host }, ); const list = useGithub( repo.value && !target ? { op: "github-list", repo: repo.value, kind, ...filter } : undefined, ); const loading = repo.loading || list.loading; function refresh() { repo.refresh(); list.refresh(); } return (

{NAMES[kind]}

{!target && (
{repo.value}
)}
{target ? : ( <>
{ event.preventDefault(); setFilter({ ...filter, search: search.trim(), limit: 50 }); }} > setSearch(event.target.value)} placeholder={`Search ${NAMES[kind].toLowerCase()}…`} aria-label={`Search ${NAMES[kind].toLowerCase()}`} />
)}
); }