From 0807986f58bbddaa1014445cddec8d4688ba17a7 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Sun, 29 Mar 2026 20:32:04 +0200 Subject: [PATCH] refactor(web): move page components into route files, add data preloading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit inline all page components from src/pages/ into their route files in src/routes/, following TanStack Router conventions add Apollo preloadQuery integration for router-level data loading: - create preloadQuery helper via createQueryPreloader(client) - preload repos query in / route loader - preload bug detail query in /$repo/issues/$id loader - preload commit query in /$repo/commit/$hash loader - preload refs query in /$repo/ (code page) loader - use useReadQuery() in components to read preloaded data move loading skeletons to route-level pendingComponent: - BugDetailSkeleton, CommitPageSkeleton, CodePageSkeleton delete src/pages/ directory — all UI now lives in src/routes/ Co-Authored-By: Claude Opus 4.6 (1M context) --- webui2/src/lib/apollo.ts | 5 + webui2/src/pages/BugDetailPage.tsx | 139 -------- webui2/src/pages/BugListPage.tsx | 388 -------------------- webui2/src/pages/CodePage.tsx | 275 --------------- webui2/src/pages/CommitPage.tsx | 174 --------- webui2/src/pages/ErrorPage.tsx | 34 -- webui2/src/pages/IdentitySelectPage.tsx | 131 ------- webui2/src/pages/NewBugPage.tsx | 118 ------- webui2/src/pages/RepoPickerPage.tsx | 79 ----- webui2/src/pages/UserProfilePage.tsx | 292 --------------- webui2/src/routes/$repo/commit/$hash.tsx | 170 ++++++++- webui2/src/routes/$repo/index.tsx | 287 ++++++++++++++- webui2/src/routes/$repo/issues/$id.tsx | 136 ++++++- webui2/src/routes/$repo/issues/index.tsx | 390 ++++++++++++++++++++- webui2/src/routes/$repo/issues/new.tsx | 121 ++++++- webui2/src/routes/$repo/user/$id.tsx | 295 +++++++++++++++- webui2/src/routes/__root.tsx | 33 +- webui2/src/routes/auth/select-identity.tsx | 133 ++++++- webui2/src/routes/index.tsx | 79 ++++- 19 files changed, 1625 insertions(+), 1654 deletions(-) delete mode 100644 webui2/src/pages/BugDetailPage.tsx delete mode 100644 webui2/src/pages/BugListPage.tsx delete mode 100644 webui2/src/pages/CodePage.tsx delete mode 100644 webui2/src/pages/CommitPage.tsx delete mode 100644 webui2/src/pages/ErrorPage.tsx delete mode 100644 webui2/src/pages/IdentitySelectPage.tsx delete mode 100644 webui2/src/pages/NewBugPage.tsx delete mode 100644 webui2/src/pages/RepoPickerPage.tsx delete mode 100644 webui2/src/pages/UserProfilePage.tsx diff --git a/webui2/src/lib/apollo.ts b/webui2/src/lib/apollo.ts index 3b0744fa63e98fad5fed4b86f275b26288cf08ef..c1cb3597f62178787f7feea2ddd93db20c73d315 100644 --- a/webui2/src/lib/apollo.ts +++ b/webui2/src/lib/apollo.ts @@ -1,4 +1,5 @@ import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"; +import { createQueryPreloader } from "@apollo/client/react"; const httpLink = new HttpLink({ uri: "/graphql", @@ -18,3 +19,7 @@ export const client = new ApolloClient({ }, }), }); + +// Preloader for use in TanStack Router loaders. Returns a QueryRef +// that components read with useReadQuery() for suspense-based rendering. +export const preloadQuery = createQueryPreloader(client); diff --git a/webui2/src/pages/BugDetailPage.tsx b/webui2/src/pages/BugDetailPage.tsx deleted file mode 100644 index d4b1999f695c4b7f7aba1fdda4955f46065f8ed0..0000000000000000000000000000000000000000 --- a/webui2/src/pages/BugDetailPage.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useParams, Link } from "@tanstack/react-router"; -import { formatDistanceToNow } from "date-fns"; -import { ArrowLeft } from "lucide-react"; - -import { useBugDetailQuery } from "@/__generated__/graphql"; -import { CommentBox } from "@/components/bugs/CommentBox"; -import { LabelEditor } from "@/components/bugs/LabelEditor"; -import { StatusBadge } from "@/components/bugs/StatusBadge"; -import { Timeline } from "@/components/bugs/Timeline"; -import { TitleEditor } from "@/components/bugs/TitleEditor"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { Separator } from "@/components/ui/separator"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useRepo } from "@/lib/repo"; - -// Issue detail page (/:repo/issues/:id). Shows title, status, timeline of -// comments and events, and a sidebar with labels and participants. -export function BugDetailPage() { - const { id } = useParams({ strict: false }); - const repo = useRepo(); - const { data, loading, error } = useBugDetailQuery({ - variables: { ref: repo, prefix: id! }, - }); - - if (error) { - return ( -
- Failed to load issue: {error.message} -
- ); - } - - if (loading && !data) { - return ; - } - - const bug = data?.repository?.bug; - if (!bug) { - return
Issue not found.
; - } - - return ( -
- - - Back to issues - - - {/* Title row — hover reveals edit button when logged in */} -
- -
- -
- - - - {bug.author.displayName} - {" "} - opened this issue {formatDistanceToNow(new Date(bug.createdAt), { addSuffix: true })} - -
- - - -
- {/* Timeline + comment box */} -
- - -
- - {/* Sidebar */} - -
-
- ); -} - -function BugDetailSkeleton() { - return ( -
- - - -
-
- {Array.from({ length: 3 }).map((_, i) => ( -
- - -
- ))} -
-
- - -
-
-
- ); -} diff --git a/webui2/src/pages/BugListPage.tsx b/webui2/src/pages/BugListPage.tsx deleted file mode 100644 index ea7224671be6e67c34e5bd7d224bda566cb37f3c..0000000000000000000000000000000000000000 --- a/webui2/src/pages/BugListPage.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import { CircleDot, CircleCheck, ChevronLeft, ChevronRight } from "lucide-react"; -import { useState, useEffect } from "react"; - -import { useBugListQuery } from "@/__generated__/graphql"; -import { BugRow } from "@/components/bugs/BugRow"; -import { IssueFilters } from "@/components/bugs/IssueFilters"; -import type { SortValue } from "@/components/bugs/IssueFilters"; -import { QueryInput } from "@/components/bugs/QueryInput"; -import { Button } from "@/components/ui/button"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useRepo } from "@/lib/repo"; -import { cn } from "@/lib/utils"; - -const PAGE_SIZE = 25; - -type StatusFilter = "open" | "closed"; - -// Issue list page (/:repo/issues). Search bar with structured query, open/closed toggle, -// label+author filter dropdowns, and paginated bug rows. -export function BugListPage() { - const repo = useRepo(); - const [statusFilter, setStatusFilter] = useState("open"); - const [selectedLabels, setSelectedLabels] = useState([]); - // humanId — uniquely identifies the selection for the dropdown UI - const [selectedAuthorId, setSelectedAuthorId] = useState(null); - // query value (login/name) — what goes into author:... in the query string - const [selectedAuthorQuery, setSelectedAuthorQuery] = useState(null); - const [freeText, setFreeText] = useState(""); - const [sort, setSort] = useState("creation-desc"); - const [draft, setDraft] = useState(() => buildQueryString("open", [], null, "", "creation-desc")); - - // Cursor-stack pagination: cursors[i] is the `after` value to fetch page i. - // cursors[0] is always undefined (first page needs no cursor). - const [cursors, setCursors] = useState<(string | undefined)[]>([undefined]); - const page = cursors.length - 1; // 0-indexed current page - - // Build separate query strings: two for the always-visible counts (open/closed), - // one for the paginated list. The count queries share all filters except status. - const baseQuery = buildBaseQuery(selectedLabels, selectedAuthorQuery, freeText); - const openQuery = `status:open ${baseQuery}`.trim(); - const closedQuery = `status:closed ${baseQuery}`.trim(); - const listQuery = buildQueryString( - statusFilter, - selectedLabels, - selectedAuthorQuery, - freeText, - sort, - ); - - const { data, loading, error } = useBugListQuery({ - variables: { - ref: repo, - openQuery, - closedQuery, - listQuery, - first: PAGE_SIZE, - after: cursors[page], - }, - }); - - const openCount = data?.repository?.openCount.totalCount ?? 0; - const closedCount = data?.repository?.closedCount.totalCount ?? 0; - const bugs = data?.repository?.bugs; - const totalCount = bugs?.totalCount ?? 0; - const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)); - const hasNext = bugs?.pageInfo.hasNextPage ?? false; - const hasPrev = page > 0; - - // Reset to page 1 whenever the list query changes. - useEffect(() => { - setCursors([undefined]); - }, [listQuery]); - - // Apply all filters at once, keeping draft in sync with the structured state. - function applyFilters( - status: StatusFilter, - labels: string[], - authorId: string | null, - authorQuery: string | null, - text: string, - sortVal: SortValue = sort, - ) { - setStatusFilter(status); - setSelectedLabels(labels); - setSelectedAuthorId(authorId); - setSelectedAuthorQuery(authorQuery); - setFreeText(text); - setSort(sortVal); - setDraft(buildQueryString(status, labels, authorQuery, text, sortVal)); - } - - // Parse the draft text box on submit so manual edits update the dropdowns too. - // When parsing we don't know the humanId — clear it so the dropdown resets. - // Called both from the
onSubmit (with event) and from QueryInput's - // Enter-key handler (without event), so e is optional. - function handleSearch(e?: React.FormEvent) { - e?.preventDefault(); - const p = parseQueryString(draft); - applyFilters(p.status, p.labels, null, p.author, p.freeText, p.sort); - } - - function goNext() { - const endCursor = bugs?.pageInfo.endCursor; - if (!endCursor) return; - setCursors((prev) => [...prev, endCursor]); - } - - function goPrev() { - setCursors((prev) => prev.slice(0, -1)); - } - - return ( -
- {/* Search bar */} - - - - - - {/* List container */} -
- {/* Open / Closed toggle + filter dropdowns */} -
-
- - - -
- -
- - applyFilters(statusFilter, labels, selectedAuthorId, selectedAuthorQuery, freeText) - } - selectedAuthorId={selectedAuthorId} - onAuthorChange={(id, qv) => - applyFilters(statusFilter, selectedLabels, id, qv, freeText) - } - recentAuthorIds={bugs?.nodes?.map((b) => b.author.humanId) ?? []} - sort={sort} - onSortChange={(s) => - applyFilters( - statusFilter, - selectedLabels, - selectedAuthorId, - selectedAuthorQuery, - freeText, - s, - ) - } - /> -
-
- - {/* Bug rows */} - {error && ( -

- Failed to load issues: {error.message} -

- )} - - {loading && !data && } - - {bugs?.nodes.length === 0 && ( -

- No {statusFilter} issues found. -

- )} - - {bugs?.nodes.map((bug) => ( - { - if (!selectedLabels.includes(name)) { - applyFilters( - statusFilter, - [...selectedLabels, name], - selectedAuthorId, - selectedAuthorQuery, - freeText, - ); - } - }} - /> - ))} - - {totalPages > 1 && ( -
- - - Page {page + 1} of {totalPages} - - -
- )} -
-
- ); -} - -// buildBaseQuery returns the filter parts (labels, author, freeText) without -// the status prefix, so it can be combined with "status:open" / "status:closed". -function buildBaseQuery(labels: string[], author: string | null, freeText: string): string { - const parts: string[] = []; - for (const label of labels) { - parts.push(label.includes(" ") ? `label:"${label}"` : `label:${label}`); - } - if (author) { - parts.push(author.includes(" ") ? `author:"${author}"` : `author:${author}`); - } - if (freeText.trim()) parts.push(freeText.trim()); - return parts.join(" "); -} - -// Build the structured query string sent to the GraphQL allBugs(query:) argument. -// Multi-word label/author values are wrapped in quotes so the backend parser -// treats them as a single token (e.g. label:"my label" vs label:my label). -function buildQueryString( - status: StatusFilter, - labels: string[], - author: string | null, - freeText: string, - sort: SortValue = "creation-desc", -): string { - const parts = [`status:${status}`]; - const base = buildBaseQuery(labels, author, freeText); - if (base) parts.push(base); - if (sort !== "creation-desc") parts.push(`sort:${sort}`); - return parts.join(" "); -} - -// Tokenize a query string, keeping quoted spans (e.g. author:"René Descartes") -// as single tokens. Quotes are preserved in the output so callers can strip them -// when extracting values. -function tokenizeQuery(input: string): string[] { - const tokens: string[] = []; - let current = ""; - let inQuote = false; - for (const ch of input.trim()) { - if (ch === '"') { - inQuote = !inQuote; - current += ch; - } else if (ch === " " && !inQuote) { - if (current) { - tokens.push(current); - current = ""; - } - } else current += ch; - } - if (current) tokens.push(current); - return tokens; -} - -// Parse a free-text query string back into structured filter state so that -// manual edits to the search box are reflected in the dropdown UI on submit. -// Strips surrounding quotes from values (they're an encoding detail, not part -// of the value itself). Unknown tokens fall through to freeText. -const VALID_SORTS = new Set(["creation-desc", "creation-asc", "edit-desc", "edit-asc"]); - -function parseQueryString(input: string): { - status: StatusFilter; - labels: string[]; - author: string | null; - freeText: string; - sort: SortValue; -} { - let status: StatusFilter = "open"; - const labels: string[] = []; - let author: string | null = null; - let sort: SortValue = "creation-desc"; - const free: string[] = []; - - for (const token of tokenizeQuery(input)) { - if (token === "status:open") status = "open"; - else if (token === "status:closed") status = "closed"; - else if (token.startsWith("label:")) labels.push(token.slice(6)); - else if (token.startsWith("author:")) author = token.slice(7).replace(/^"|"$/g, ""); - else if (token.startsWith("sort:")) { - const v = token.slice(5) as SortValue; - if (VALID_SORTS.has(v)) sort = v; - } else free.push(token); - } - - return { status, labels, author, freeText: free.join(" "), sort }; -} - -function BugListSkeleton() { - return ( -
- {Array.from({ length: 8 }).map((_, i) => ( -
- -
- - -
-
- ))} -
- ); -} diff --git a/webui2/src/pages/CodePage.tsx b/webui2/src/pages/CodePage.tsx deleted file mode 100644 index 64b981454ec761a070ecf5a3564fe0b21516ef28..0000000000000000000000000000000000000000 --- a/webui2/src/pages/CodePage.tsx +++ /dev/null @@ -1,275 +0,0 @@ -// Code browser page. Switches between tree view, file viewer, and commit -// history via ?type= search param. Ref is selected via ?ref=. - -import { gql } from "@apollo/client"; -import { useQuery } from "@apollo/client/react"; -import { useNavigate, useSearch } from "@tanstack/react-router"; -import { AlertCircle, GitCommit } from "lucide-react"; -import { useEffect } from "react"; - -import type { GitRef, GitTreeEntry, GitBlob, GitLastCommit } from "@/__generated__/graphql"; -import { CodeBreadcrumb } from "@/components/code/CodeBreadcrumb"; -import { CommitList } from "@/components/code/CommitList"; -import { FileTree } from "@/components/code/FileTree"; -import type { TreeEntryWithCommit } from "@/components/code/FileTree"; -import { FileViewer } from "@/components/code/FileViewer"; -import { RefSelector } from "@/components/code/RefSelector"; -import { Markdown } from "@/components/content/Markdown"; -import { ButtonLink } from "@/components/ui/button-link"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useRepo } from "@/lib/repo"; - -const REFS_QUERY = gql` - query CodePageRefs($repo: String) { - repository(ref: $repo) { - name - refs { - nodes { - name - shortName - type - hash - isDefault - } - } - } - } -`; - -const TREE_QUERY = gql` - query CodePageTree($repo: String, $ref: String!, $path: String) { - repository(ref: $repo) { - tree(ref: $ref, path: $path) { - name - type - hash - } - } - } -`; - -const LAST_COMMITS_QUERY = gql` - query CodePageLastCommits($repo: String, $ref: String!, $path: String, $names: [String!]!) { - repository(ref: $repo) { - lastCommits(ref: $ref, path: $path, names: $names) { - name - commit { - hash - shortHash - message - date - } - } - } - } -`; - -const BLOB_QUERY = gql` - query CodePageBlob($repo: String, $ref: String!, $path: String!) { - repository(ref: $repo) { - blob(ref: $ref, path: $path) { - path - hash - text - size - isBinary - isTruncated - } - } - } -`; - -interface RefsQueryData { - repository: { - name: string; - refs: { nodes: GitRef[] } | null; - } | null; -} - -interface TreeQueryData { - repository: { - tree: GitTreeEntry[] | null; - } | null; -} - -interface LastCommitsQueryData { - repository: { - lastCommits: GitLastCommit[] | null; - } | null; -} - -interface BlobQueryData { - repository: { - blob: GitBlob | null; - } | null; -} - -import type { CodePageSearch } from "@/routes/$repo/index"; - -type ViewMode = CodePageSearch["type"]; - -export function CodePage() { - const repo = useRepo(); - const navigate = useNavigate({ from: "/$repo/" }); - const { ref: currentRef, path: currentPath, type: viewMode } = useSearch({ from: "/$repo/" }); - - const { - data: refsData, - loading: refsLoading, - error: refsError, - } = useQuery(REFS_QUERY, { - variables: { repo }, - }); - const refs: GitRef[] = refsData?.repository?.refs?.nodes ?? []; - - // Set default ref from query result once loaded - useEffect(() => { - if (refsLoading || refs.length === 0 || currentRef) return; - const defaultRef = refs.find((r: GitRef) => r.isDefault) ?? refs[0]; - if (defaultRef) { - void navigate({ - search: (prev) => ({ ...prev, ref: defaultRef.shortName }), - replace: true, - }); - } - }, [refsLoading, refs.length]); // eslint-disable-line react-hooks/exhaustive-deps - - const inTreeMode = viewMode === "tree" && !!currentRef; - const inBlobMode = viewMode === "blob" && !!currentRef && !!currentPath; - - const { data: treeData, loading: treeLoading } = useQuery(TREE_QUERY, { - variables: { repo, ref: currentRef, path: currentPath || null }, - skip: !inTreeMode, - }); - const entries: GitTreeEntry[] = treeData?.repository?.tree ?? []; - - const entryNames = entries.map((e: GitTreeEntry) => e.name); - const { data: lastCommitsData } = useQuery(LAST_COMMITS_QUERY, { - variables: { repo, ref: currentRef, path: currentPath || null, names: entryNames }, - skip: !inTreeMode || entryNames.length === 0, - }); - const lastCommitsByName = new Map( - (lastCommitsData?.repository?.lastCommits ?? []).map((lc: GitLastCommit) => [lc.name, lc]), - ); - const entriesWithCommits: TreeEntryWithCommit[] = entries.map((e: GitTreeEntry) => ({ - ...e, - lastCommit: lastCommitsByName.get(e.name)?.commit ?? undefined, - })); - - const { data: blobData, loading: blobLoading } = useQuery(BLOB_QUERY, { - variables: { repo, ref: currentRef, path: currentPath }, - skip: !inBlobMode, - }); - const blob: GitBlob | null = blobData?.repository?.blob ?? null; - - const readmeEntry = entries.find( - (e: GitTreeEntry) => e.type === "BLOB" && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name), - ); - const readmePath = readmeEntry - ? currentPath - ? `${currentPath}/${readmeEntry.name}` - : readmeEntry.name - : null; - const { data: readmeBlobData } = useQuery(BLOB_QUERY, { - variables: { repo, ref: currentRef, path: readmePath }, - skip: !inTreeMode || !readmePath, - }); - const readme: string | null = readmeBlobData?.repository?.blob?.text ?? null; - - const repoName = refsData?.repository?.name ?? repo ?? "default-repo"; - - function navigateTo(path: string, type: ViewMode = "tree") { - void navigate({ search: (prev) => ({ ...prev, path, type }) }); - } - - function handleEntryClick(entry: TreeEntryWithCommit) { - const newPath = currentPath ? `${currentPath}/${entry.name}` : entry.name; - navigateTo(newPath, entry.type === "BLOB" ? "blob" : "tree"); - } - - function handleNavigateUp() { - const parts = currentPath.split("/").filter(Boolean); - parts.pop(); - navigateTo(parts.join("/"), "tree"); - } - - function handleRefSelect(ref: GitRef) { - void navigate({ search: { ref: ref.shortName, path: "", type: "tree" } }); - } - - if (refsError) { - return ( -
- -

Code browser unavailable

-

{refsError.message}

-
- ); - } - - return ( -
-
- {refsLoading ? ( - - ) : ( - navigateTo(p, "tree")} - /> - )} -
- {!refsLoading && ( - - - History - - )} - {refsLoading ? ( - - ) : ( - - )} -
-
- - {viewMode === "commits" ? ( - - ) : viewMode === "tree" || !blob ? ( - <> - - {readme && ( -
-
- README -
-
- -
-
- )} - - ) : ( - - )} -
- ); -} diff --git a/webui2/src/pages/CommitPage.tsx b/webui2/src/pages/CommitPage.tsx deleted file mode 100644 index 66d39531c9d8bb24cf6a56aee4677b4adc292f04..0000000000000000000000000000000000000000 --- a/webui2/src/pages/CommitPage.tsx +++ /dev/null @@ -1,174 +0,0 @@ -// Commit detail page (/:repo/commit/:hash). Shows commit metadata, full -// message, parent links, and changed files with lazy diffs. - -import { gql } from "@apollo/client"; -import { useQuery } from "@apollo/client/react"; -import { Link, useParams } from "@tanstack/react-router"; -import { format } from "date-fns"; -import { ArrowLeft, GitCommit } from "lucide-react"; - -import { FileDiffView } from "@/components/code/FileDiffView"; -import { Skeleton } from "@/components/ui/skeleton"; -import { useRepo } from "@/lib/repo"; - -const COMMIT_QUERY = gql` - query CommitPageDetail($repo: String, $hash: String!) { - repository(ref: $repo) { - commit(hash: $hash) { - hash - shortHash - message - fullMessage - authorName - authorEmail - date - parents - files { - nodes { - path - oldPath - status - } - } - } - } - } -`; - -interface CommitQueryData { - repository: { - commit: { - hash: string; - shortHash: string; - message: string; - fullMessage: string; - authorName: string; - authorEmail: string | null; - date: string; - parents: string[]; - files: { - nodes: { path: string; oldPath: string | null; status: string }[]; - } | null; - } | null; - } | null; -} - -export function CommitPage() { - const { hash } = useParams({ strict: false }); - const repo = useRepo(); - - const { data, loading, error } = useQuery(COMMIT_QUERY, { - variables: { repo, hash }, - skip: !hash, - }); - - if (loading) return ; - - if (error) { - return ( -
- Failed to load commit: {error.message} -
- ); - } - - const commit = data?.repository?.commit; - if (!commit) return null; - - const date = new Date(commit.date); - const files = commit.files?.nodes ?? []; - - return ( -
- - -
-
- -

{commit.message}

-
- - {commit.fullMessage.includes("\n") && ( -
-            {commit.fullMessage.split("\n").slice(1).join("\n").trim()}
-          
- )} - -
- - {commit.authorName} - {commit.authorEmail && <{commit.authorEmail}>} - - {format(date, "PPP")} -
- -
- - commit {commit.hash} - - {commit.parents.map((p: string) => ( - - parent{" "} - - {p.slice(0, 7)} - - - ))} -
-
- -
-

- {files.length} file{files.length !== 1 ? "s" : ""} changed -

-
- {files.length === 0 && ( -

No file changes.

- )} - {files.map((file: { path: string; oldPath?: string | null; status: string }) => ( - - ))} -
-
-
- ); -} - -function CommitPageSkeleton() { - return ( -
- -
- - - -
-
- {Array.from({ length: 5 }).map((_, i) => ( -
- - -
- ))} -
-
- ); -} diff --git a/webui2/src/pages/ErrorPage.tsx b/webui2/src/pages/ErrorPage.tsx deleted file mode 100644 index 9df9a641c17f58bcf193c9aac8e896a71e9e934b..0000000000000000000000000000000000000000 --- a/webui2/src/pages/ErrorPage.tsx +++ /dev/null @@ -1,34 +0,0 @@ -// Global error boundary page. Rendered by TanStack Router when a route throws. - -import { useRouter } from "@tanstack/react-router"; -import { AlertTriangle } from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { ButtonLink } from "@/components/ui/button-link"; - -export function ErrorPage({ error }: { error?: Error }) { - const router = useRouter(); - - const message = error?.message ?? "An unexpected error occurred."; - - return ( -
- -

{message}

-
- - - Go home - -
-
- ); -} diff --git a/webui2/src/pages/IdentitySelectPage.tsx b/webui2/src/pages/IdentitySelectPage.tsx deleted file mode 100644 index f013a1caf87a0dc37ea0a971234386be9c891065..0000000000000000000000000000000000000000 --- a/webui2/src/pages/IdentitySelectPage.tsx +++ /dev/null @@ -1,131 +0,0 @@ -// Identity selection page (/auth/select-identity). -// -// Reached after a successful OAuth login when no existing git-bug identity -// could be matched automatically (via provider metadata set by the bridge). -// The user can either adopt an existing identity — which links it to their -// OAuth account for future logins — or create a fresh one from their OAuth -// profile. - -import { UserCircle, Plus, AlertCircle } from "lucide-react"; -import { useEffect, useState } from "react"; - -import { Button } from "@/components/ui/button"; -import { Skeleton } from "@/components/ui/skeleton"; - -interface IdentityItem { - repoSlug: string; - id: string; - humanId: string; - displayName: string; - login?: string; - avatarUrl?: string; -} - -export function IdentitySelectPage() { - const [identities, setIdentities] = useState(null); - const [error, setError] = useState(null); - const [working, setWorking] = useState(false); - - useEffect(() => { - async function loadIdentities() { - try { - const res = await fetch("/auth/identities", { credentials: "include" }); - if (!res.ok) throw new Error(`unexpected status ${res.status}`); - const data: IdentityItem[] = await res.json(); - setIdentities(data); - } catch (e) { - setError(String(e)); - } - } - void loadIdentities(); - }, []); - - async function adopt(identityId: string | null) { - setWorking(true); - try { - const res = await fetch("/auth/adopt", { - method: "POST", - credentials: "include", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(identityId ? { identityId } : {}), - }); - if (!res.ok) throw new Error(`adopt failed: ${res.status}`); - // Full page reload to reset Apollo cache and auth state cleanly. - window.location.assign("/"); - } catch (e) { - setError(String(e)); - setWorking(false); - } - } - - return ( -
-
- -

Choose your identity

-
-

- No git-bug identity was found linked to your account. Select an existing identity to link - it, or create a new one from your profile. -

- - {error && ( -
- - {error} -
- )} - - {!identities && !error && ( -
- {Array.from({ length: 3 }).map((_, i) => ( - - ))} -
- )} - -
- {identities?.map((id) => ( -
-
-

{id.displayName}

-

- {id.login ? `@${id.login} · ` : ""} - {id.repoSlug} · {id.humanId} -

-
- -
- ))} - - {/* Always offer to create a new identity */} -
-
-

Create new identity

-

- A fresh git-bug identity will be created from your OAuth profile. -

-
- -
-
-
- ); -} diff --git a/webui2/src/pages/NewBugPage.tsx b/webui2/src/pages/NewBugPage.tsx deleted file mode 100644 index 277f47d72e7c0c98b1d935696a9aad0030cf7f84..0000000000000000000000000000000000000000 --- a/webui2/src/pages/NewBugPage.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Link, useNavigate } from "@tanstack/react-router"; -import { ArrowLeft } from "lucide-react"; -import { useState } from "react"; - -import { useBugCreateMutation } from "@/__generated__/graphql"; -import { Markdown } from "@/components/content/Markdown"; -import { Button } from "@/components/ui/button"; -import { ButtonLink } from "@/components/ui/button-link"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { useRepo } from "@/lib/repo"; - -// New issue form (/:repo/issues/new). Title + body with write/preview tabs. -export function NewBugPage() { - const navigate = useNavigate(); - const repo = useRepo(); - const [title, setTitle] = useState(""); - const [message, setMessage] = useState(""); - const [preview, setPreview] = useState(false); - const [createBug, { loading, error }] = useBugCreateMutation(); - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - const result = await createBug({ - variables: { input: { title: title.trim(), message: message.trim() } }, - }); - const humanId = result.data?.bugCreate.bug.humanId; - if (humanId) { - void navigate({ - to: "/$repo/issues/$id", - params: { repo: repo!, id: humanId }, - }); - } - } - - return ( -
- - - Back to issues - - -

New issue

- -
{ - void handleSubmit(e); - }} - className="space-y-4" - > - setTitle(e.target.value)} - disabled={loading} - autoFocus - /> - -
-
-
- - -
-
- - {preview ? ( -
- -
- ) : ( -