Detailed changes
@@ -25,23 +25,25 @@ type Documents = {
"\n fragment LabelChangeFields on BugLabelChangeTimelineItem {\n author {\n humanId\n displayName\n }\n date\n added {\n ...LabelFields\n }\n removed {\n ...LabelFields\n }\n }\n": typeof types.LabelChangeFieldsFragmentDoc,
"\n fragment StatusChangeFields on BugSetStatusTimelineItem {\n author {\n humanId\n displayName\n }\n date\n status\n }\n": typeof types.StatusChangeFieldsFragmentDoc,
"\n fragment TitleChangeFields on BugSetTitleTimelineItem {\n author {\n humanId\n displayName\n }\n date\n title\n was\n }\n": typeof types.TitleChangeFieldsFragmentDoc,
+ "\n fragment TimelineItems on BugTimelineItemConnection {\n nodes {\n __typename\n id\n ... on BugCreateTimelineItem {\n ...BugCreateCommentFields\n }\n ... on BugAddCommentTimelineItem {\n ...BugAddCommentFields\n }\n ... on BugLabelChangeTimelineItem {\n ...LabelChangeFields\n }\n ... on BugSetStatusTimelineItem {\n ...StatusChangeFields\n }\n ... on BugSetTitleTimelineItem {\n ...TitleChangeFields\n }\n }\n }\n": typeof types.TimelineItemsFragmentDoc,
"\n mutation BugEditComment($input: BugEditCommentInput!) {\n bugEditComment(input: $input) {\n bug {\n id\n }\n }\n }\n": typeof types.BugEditCommentDocument,
"\n mutation BugSetTitle($input: BugSetTitleInput!) {\n bugSetTitle(input: $input) {\n bug {\n id\n title\n }\n }\n }\n": typeof types.BugSetTitleDocument,
"\n query CommitList($repo: String, $ref: String!, $path: String, $after: String, $first: Int) {\n repository(ref: $repo) {\n commits(ref: $ref, path: $path, after: $after, first: $first) {\n nodes {\n hash\n shortHash\n message\n authorName\n date\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n": typeof types.CommitListDocument,
"\n query FileDiff($repo: String, $hash: String!, $path: String!) {\n repository(ref: $repo) {\n commit(hash: $hash) {\n diff(path: $path) {\n path\n oldPath\n isBinary\n isNew\n isDelete\n hunks {\n oldStart\n oldLines\n newStart\n newLines\n lines {\n type\n content\n oldLine\n newLine\n }\n }\n }\n }\n }\n }\n": typeof types.FileDiffDocument,
+ "\n fragment FileViewerBlob on GitBlob {\n path\n hash\n text\n size\n isBinary\n isTruncated\n }\n": typeof types.FileViewerBlobFragmentDoc,
"\n fragment RefSelectorRefs on GitRefConnection {\n nodes {\n name\n shortName\n type\n }\n }\n": typeof types.RefSelectorRefsFragmentDoc,
"\n fragment IdentitySummary on Identity {\n id\n humanId\n displayName\n avatarUrl\n }\n": typeof types.IdentitySummaryFragmentDoc,
"\n fragment BugSummary on Bug {\n id\n humanId\n status\n title\n labels {\n name\n ...LabelFields\n }\n author {\n ...IdentitySummary\n }\n createdAt\n comments {\n totalCount\n }\n }\n": typeof types.BugSummaryFragmentDoc,
"\n fragment LabelFields on Label {\n name\n color {\n R\n G\n B\n }\n }\n": typeof types.LabelFieldsFragmentDoc,
"\n query UserIdentity {\n repository {\n userIdentity {\n id\n humanId\n name\n displayName\n avatarUrl\n email\n login\n }\n }\n }\n": typeof types.UserIdentityDocument,
"\n query CodePageRefs($repo: String) {\n repository(ref: $repo) {\n name\n head {\n shortName\n }\n refs {\n ...RefSelectorRefs\n }\n }\n }\n": typeof types.CodePageRefsDocument,
- "\n query CodePageBlob($repo: String, $ref: String!, $path: String!) {\n repository(ref: $repo) {\n blob(ref: $ref, path: $path) {\n path\n hash\n text\n size\n isBinary\n isTruncated\n }\n }\n }\n": typeof types.CodePageBlobDocument,
+ "\n query CodePageBlob($repo: String, $ref: String!, $path: String!) {\n repository(ref: $repo) {\n blob(ref: $ref, path: $path) {\n ...FileViewerBlob\n }\n }\n }\n": typeof types.CodePageBlobDocument,
"\n query CodePageTree($repo: String, $ref: String!, $path: String) {\n repository(ref: $repo) {\n tree(ref: $ref, path: $path) {\n name\n type\n hash\n }\n }\n }\n": typeof types.CodePageTreeDocument,
"\n query CodePageLastCommits(\n $repo: String\n $ref: String!\n $path: String\n $names: [String!]!\n ) {\n repository(ref: $repo) {\n lastCommits(ref: $ref, path: $path, names: $names) {\n name\n commit {\n hash\n shortHash\n message\n date\n }\n }\n }\n }\n": typeof types.CodePageLastCommitsDocument,
"\n query CodePageReadme($repo: String, $ref: String!, $path: String!) {\n repository(ref: $repo) {\n blob(ref: $ref, path: $path) {\n text\n }\n }\n }\n": typeof types.CodePageReadmeDocument,
"\n query AllIdentities($ref: String) {\n repository(ref: $ref) {\n allIdentities(first: 1000) {\n nodes {\n id\n humanId\n name\n email\n login\n displayName\n avatarUrl\n }\n }\n }\n }\n": typeof types.AllIdentitiesDocument,
"\n query ValidLabels($ref: String) {\n repository(ref: $ref) {\n validLabels {\n nodes {\n name\n color {\n R\n G\n B\n }\n ...LabelFields\n }\n }\n }\n }\n": typeof types.ValidLabelsDocument,
- "\n query BugDetail($ref: String, $prefix: String!) {\n repository(ref: $ref) {\n bug(prefix: $prefix) {\n ...BugSummary\n lastEdit\n participants(first: 20) {\n nodes {\n ...IdentitySummary\n }\n }\n timeline(first: 250) {\n nodes {\n __typename\n id\n ... on BugCreateTimelineItem {\n ...BugCreateCommentFields\n }\n ... on BugAddCommentTimelineItem {\n ...BugAddCommentFields\n }\n ... on BugLabelChangeTimelineItem {\n ...LabelChangeFields\n }\n ... on BugSetStatusTimelineItem {\n ...StatusChangeFields\n }\n ... on BugSetTitleTimelineItem {\n ...TitleChangeFields\n }\n }\n }\n }\n }\n }\n": typeof types.BugDetailDocument,
+ "\n query BugDetail($ref: String, $prefix: String!) {\n repository(ref: $ref) {\n bug(prefix: $prefix) {\n ...BugSummary\n lastEdit\n participants(first: 20) {\n nodes {\n ...IdentitySummary\n }\n }\n timeline(first: 250) {\n ...TimelineItems\n }\n }\n }\n }\n": typeof types.BugDetailDocument,
"\n query BugList(\n $ref: String\n $openQuery: String!\n $closedQuery: String!\n $listQuery: String!\n $first: Int\n $after: String\n ) {\n repository(ref: $ref) {\n openCount: allBugs(query: $openQuery, first: 1) {\n totalCount\n }\n closedCount: allBugs(query: $closedQuery, first: 1) {\n totalCount\n }\n bugs: allBugs(query: $listQuery, first: $first, after: $after) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n ...BugSummary\n }\n }\n }\n }\n": typeof types.BugListDocument,
"\n mutation BugCreate($input: BugCreateInput!) {\n bugCreate(input: $input) {\n bug {\n id\n humanId\n }\n }\n }\n": typeof types.BugCreateDocument,
"\n query UserProfile(\n $ref: String\n $prefix: String!\n $openQuery: String!\n $closedQuery: String!\n $listQuery: String!\n $after: String\n ) {\n repository(ref: $ref) {\n identity(prefix: $prefix) {\n id\n humanId\n name\n email\n login\n displayName\n avatarUrl\n isProtected\n }\n openCount: allBugs(query: $openQuery, first: 1) {\n totalCount\n }\n closedCount: allBugs(query: $closedQuery, first: 1) {\n totalCount\n }\n bugs: allBugs(query: $listQuery, first: 25, after: $after) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n ...BugSummary\n }\n }\n }\n }\n": typeof types.UserProfileDocument,
@@ -60,23 +62,25 @@ const documents: Documents = {
"\n fragment LabelChangeFields on BugLabelChangeTimelineItem {\n author {\n humanId\n displayName\n }\n date\n added {\n ...LabelFields\n }\n removed {\n ...LabelFields\n }\n }\n": types.LabelChangeFieldsFragmentDoc,
"\n fragment StatusChangeFields on BugSetStatusTimelineItem {\n author {\n humanId\n displayName\n }\n date\n status\n }\n": types.StatusChangeFieldsFragmentDoc,
"\n fragment TitleChangeFields on BugSetTitleTimelineItem {\n author {\n humanId\n displayName\n }\n date\n title\n was\n }\n": types.TitleChangeFieldsFragmentDoc,
+ "\n fragment TimelineItems on BugTimelineItemConnection {\n nodes {\n __typename\n id\n ... on BugCreateTimelineItem {\n ...BugCreateCommentFields\n }\n ... on BugAddCommentTimelineItem {\n ...BugAddCommentFields\n }\n ... on BugLabelChangeTimelineItem {\n ...LabelChangeFields\n }\n ... on BugSetStatusTimelineItem {\n ...StatusChangeFields\n }\n ... on BugSetTitleTimelineItem {\n ...TitleChangeFields\n }\n }\n }\n": types.TimelineItemsFragmentDoc,
"\n mutation BugEditComment($input: BugEditCommentInput!) {\n bugEditComment(input: $input) {\n bug {\n id\n }\n }\n }\n": types.BugEditCommentDocument,
"\n mutation BugSetTitle($input: BugSetTitleInput!) {\n bugSetTitle(input: $input) {\n bug {\n id\n title\n }\n }\n }\n": types.BugSetTitleDocument,
"\n query CommitList($repo: String, $ref: String!, $path: String, $after: String, $first: Int) {\n repository(ref: $repo) {\n commits(ref: $ref, path: $path, after: $after, first: $first) {\n nodes {\n hash\n shortHash\n message\n authorName\n date\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n }\n }\n": types.CommitListDocument,
"\n query FileDiff($repo: String, $hash: String!, $path: String!) {\n repository(ref: $repo) {\n commit(hash: $hash) {\n diff(path: $path) {\n path\n oldPath\n isBinary\n isNew\n isDelete\n hunks {\n oldStart\n oldLines\n newStart\n newLines\n lines {\n type\n content\n oldLine\n newLine\n }\n }\n }\n }\n }\n }\n": types.FileDiffDocument,
+ "\n fragment FileViewerBlob on GitBlob {\n path\n hash\n text\n size\n isBinary\n isTruncated\n }\n": types.FileViewerBlobFragmentDoc,
"\n fragment RefSelectorRefs on GitRefConnection {\n nodes {\n name\n shortName\n type\n }\n }\n": types.RefSelectorRefsFragmentDoc,
"\n fragment IdentitySummary on Identity {\n id\n humanId\n displayName\n avatarUrl\n }\n": types.IdentitySummaryFragmentDoc,
"\n fragment BugSummary on Bug {\n id\n humanId\n status\n title\n labels {\n name\n ...LabelFields\n }\n author {\n ...IdentitySummary\n }\n createdAt\n comments {\n totalCount\n }\n }\n": types.BugSummaryFragmentDoc,
"\n fragment LabelFields on Label {\n name\n color {\n R\n G\n B\n }\n }\n": types.LabelFieldsFragmentDoc,
"\n query UserIdentity {\n repository {\n userIdentity {\n id\n humanId\n name\n displayName\n avatarUrl\n email\n login\n }\n }\n }\n": types.UserIdentityDocument,
"\n query CodePageRefs($repo: String) {\n repository(ref: $repo) {\n name\n head {\n shortName\n }\n refs {\n ...RefSelectorRefs\n }\n }\n }\n": types.CodePageRefsDocument,
- "\n query CodePageBlob($repo: String, $ref: String!, $path: String!) {\n repository(ref: $repo) {\n blob(ref: $ref, path: $path) {\n path\n hash\n text\n size\n isBinary\n isTruncated\n }\n }\n }\n": types.CodePageBlobDocument,
+ "\n query CodePageBlob($repo: String, $ref: String!, $path: String!) {\n repository(ref: $repo) {\n blob(ref: $ref, path: $path) {\n ...FileViewerBlob\n }\n }\n }\n": types.CodePageBlobDocument,
"\n query CodePageTree($repo: String, $ref: String!, $path: String) {\n repository(ref: $repo) {\n tree(ref: $ref, path: $path) {\n name\n type\n hash\n }\n }\n }\n": types.CodePageTreeDocument,
"\n query CodePageLastCommits(\n $repo: String\n $ref: String!\n $path: String\n $names: [String!]!\n ) {\n repository(ref: $repo) {\n lastCommits(ref: $ref, path: $path, names: $names) {\n name\n commit {\n hash\n shortHash\n message\n date\n }\n }\n }\n }\n": types.CodePageLastCommitsDocument,
"\n query CodePageReadme($repo: String, $ref: String!, $path: String!) {\n repository(ref: $repo) {\n blob(ref: $ref, path: $path) {\n text\n }\n }\n }\n": types.CodePageReadmeDocument,
"\n query AllIdentities($ref: String) {\n repository(ref: $ref) {\n allIdentities(first: 1000) {\n nodes {\n id\n humanId\n name\n email\n login\n displayName\n avatarUrl\n }\n }\n }\n }\n": types.AllIdentitiesDocument,
"\n query ValidLabels($ref: String) {\n repository(ref: $ref) {\n validLabels {\n nodes {\n name\n color {\n R\n G\n B\n }\n ...LabelFields\n }\n }\n }\n }\n": types.ValidLabelsDocument,
- "\n query BugDetail($ref: String, $prefix: String!) {\n repository(ref: $ref) {\n bug(prefix: $prefix) {\n ...BugSummary\n lastEdit\n participants(first: 20) {\n nodes {\n ...IdentitySummary\n }\n }\n timeline(first: 250) {\n nodes {\n __typename\n id\n ... on BugCreateTimelineItem {\n ...BugCreateCommentFields\n }\n ... on BugAddCommentTimelineItem {\n ...BugAddCommentFields\n }\n ... on BugLabelChangeTimelineItem {\n ...LabelChangeFields\n }\n ... on BugSetStatusTimelineItem {\n ...StatusChangeFields\n }\n ... on BugSetTitleTimelineItem {\n ...TitleChangeFields\n }\n }\n }\n }\n }\n }\n": types.BugDetailDocument,
+ "\n query BugDetail($ref: String, $prefix: String!) {\n repository(ref: $ref) {\n bug(prefix: $prefix) {\n ...BugSummary\n lastEdit\n participants(first: 20) {\n nodes {\n ...IdentitySummary\n }\n }\n timeline(first: 250) {\n ...TimelineItems\n }\n }\n }\n }\n": types.BugDetailDocument,
"\n query BugList(\n $ref: String\n $openQuery: String!\n $closedQuery: String!\n $listQuery: String!\n $first: Int\n $after: String\n ) {\n repository(ref: $ref) {\n openCount: allBugs(query: $openQuery, first: 1) {\n totalCount\n }\n closedCount: allBugs(query: $closedQuery, first: 1) {\n totalCount\n }\n bugs: allBugs(query: $listQuery, first: $first, after: $after) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n ...BugSummary\n }\n }\n }\n }\n": types.BugListDocument,
"\n mutation BugCreate($input: BugCreateInput!) {\n bugCreate(input: $input) {\n bug {\n id\n humanId\n }\n }\n }\n": types.BugCreateDocument,
"\n query UserProfile(\n $ref: String\n $prefix: String!\n $openQuery: String!\n $closedQuery: String!\n $listQuery: String!\n $after: String\n ) {\n repository(ref: $ref) {\n identity(prefix: $prefix) {\n id\n humanId\n name\n email\n login\n displayName\n avatarUrl\n isProtected\n }\n openCount: allBugs(query: $openQuery, first: 1) {\n totalCount\n }\n closedCount: allBugs(query: $closedQuery, first: 1) {\n totalCount\n }\n bugs: allBugs(query: $listQuery, first: 25, after: $after) {\n totalCount\n pageInfo {\n hasNextPage\n endCursor\n }\n nodes {\n ...BugSummary\n }\n }\n }\n }\n": types.UserProfileDocument,
@@ -142,6 +146,10 @@ export function graphql(source: "\n fragment StatusChangeFields on BugSetStatus
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment TitleChangeFields on BugSetTitleTimelineItem {\n author {\n humanId\n displayName\n }\n date\n title\n was\n }\n"): (typeof documents)["\n fragment TitleChangeFields on BugSetTitleTimelineItem {\n author {\n humanId\n displayName\n }\n date\n title\n was\n }\n"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
@@ -1211,6 +1211,14 @@ export type StatusChangeFieldsFragment = { __typename: 'BugSetStatusTimelineItem
export type TitleChangeFieldsFragment = { __typename: 'BugSetTitleTimelineItem', date: string, title: string, was: string, author: { __typename: 'Identity', humanId: string, displayName: string } };
+export type TimelineItemsFragment = { __typename: 'BugTimelineItemConnection', nodes: Array<
+ | { __typename: 'BugAddCommentTimelineItem', id: string, message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename: 'Identity', id: string, humanId: string, displayName: string, avatarUrl: string | null } }
+ | { __typename: 'BugCreateTimelineItem', id: string, message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename: 'Identity', id: string, humanId: string, displayName: string, avatarUrl: string | null } }
+ | { __typename: 'BugLabelChangeTimelineItem', id: string, date: string, author: { __typename: 'Identity', humanId: string, displayName: string }, added: Array<{ __typename: 'Label', name: string, color: { __typename: 'Color', R: number, G: number, B: number } }>, removed: Array<{ __typename: 'Label', name: string, color: { __typename: 'Color', R: number, G: number, B: number } }> }
+ | { __typename: 'BugSetStatusTimelineItem', id: string, date: string, status: Status, author: { __typename: 'Identity', humanId: string, displayName: string } }
+ | { __typename: 'BugSetTitleTimelineItem', id: string, date: string, title: string, was: string, author: { __typename: 'Identity', humanId: string, displayName: string } }
+ > };
+
export type BugEditCommentMutationVariables = Exact<{
input: BugEditCommentInput;
}>;
@@ -1245,6 +1253,8 @@ export type FileDiffQueryVariables = Exact<{
export type FileDiffQuery = { repository: { __typename: 'Repository', commit: { __typename: 'GitCommit', diff: { __typename: 'GitFileDiff', path: string, oldPath: string | null, isBinary: boolean, isNew: boolean, isDelete: boolean, hunks: Array<{ __typename: 'GitDiffHunk', oldStart: number, oldLines: number, newStart: number, newLines: number, lines: Array<{ __typename: 'GitDiffLine', type: GitDiffLineType, content: string, oldLine: number, newLine: number }> }> } | null } | null } | null };
+export type FileViewerBlobFragment = { __typename: 'GitBlob', path: string, hash: string, text: string | null, size: number, isBinary: boolean, isTruncated: boolean };
+
export type RefSelectorRefsFragment = { __typename: 'GitRefConnection', nodes: Array<{ __typename: 'GitRef', name: string, shortName: string, type: GitRefType }> };
export type IdentitySummaryFragment = { __typename: 'Identity', id: string, humanId: string, displayName: string, avatarUrl: string | null };
@@ -1381,6 +1391,8 @@ export const LabelFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":
@@ -1,10 +1,11 @@
-import { useMutation } from "@apollo/client/react";
+import { useMutation, useSuspenseFragment } from "@apollo/client/react";
+import type { FragmentType } from "@apollo/client/masking";
import { Link } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns";
import { Tag, GitPullRequestClosed, Pencil, CircleDot } from "lucide-react";
import { useState } from "react";
-import { Status, type BugDetailQuery, BugDetailDocument } from "@/__generated__/graphql";
+import { Status, BugDetailDocument } from "@/__generated__/graphql";
import { graphql } from "@/__generated__/gql";
import { Markdown } from "@/components/content/markdown";
import { Button } from "@/components/ui/button";
@@ -76,6 +77,30 @@ graphql(`
}
`);
+export const TIMELINE_ITEMS_FRAGMENT = graphql(`
+ fragment TimelineItems on BugTimelineItemConnection {
+ nodes {
+ __typename
+ id
+ ... on BugCreateTimelineItem {
+ ...BugCreateCommentFields
+ }
+ ... on BugAddCommentTimelineItem {
+ ...BugAddCommentFields
+ }
+ ... on BugLabelChangeTimelineItem {
+ ...LabelChangeFields
+ }
+ ... on BugSetStatusTimelineItem {
+ ...StatusChangeFields
+ }
+ ... on BugSetTitleTimelineItem {
+ ...TitleChangeFields
+ }
+ }
+ }
+`);
+
const BUG_EDIT_COMMENT_MUTATION = graphql(`
mutation BugEditComment($input: BugEditCommentInput!) {
bugEditComment(input: $input) {
@@ -86,23 +111,29 @@ const BUG_EDIT_COMMENT_MUTATION = graphql(`
}
`);
-type TimelineNode = NonNullable<
- NonNullable<NonNullable<BugDetailQuery["repository"]>["bug"]>["timeline"]["nodes"][number]
->;
+type TimelineData = ReturnType<
+ typeof useSuspenseFragment<typeof TIMELINE_ITEMS_FRAGMENT>
+>["data"];
+type TimelineNode = TimelineData["nodes"][number];
interface TimelineProps {
repo: string | null;
bugPrefix: string;
- items: TimelineNode[];
+ timeline: FragmentType<typeof TIMELINE_ITEMS_FRAGMENT>;
}
// Ordered sequence of events on a bug: comments (create and add-comment) and
// inline events (label changes, status changes, title edits). Comment items
// support inline editing for the logged-in user.
-export function Timeline({ repo, bugPrefix, items }: TimelineProps) {
+export function Timeline({ repo, bugPrefix, timeline }: TimelineProps) {
+ const { data } = useSuspenseFragment({
+ fragment: TIMELINE_ITEMS_FRAGMENT,
+ from: timeline,
+ });
+
return (
<div className="space-y-4">
- {items.map((item) => {
+ {data.nodes.map((item) => {
switch (item.__typename) {
case "BugCreateTimelineItem":
case "BugAddCommentTimelineItem":
@@ -2,9 +2,11 @@ import { Link } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns";
import { Folder, File } from "lucide-react";
-import { GitObjectType, type GitTreeEntry } from "@/__generated__/graphql";
-
-export interface TreeEntryWithCommit extends GitTreeEntry {
+/** A single tree entry with optional last-commit metadata, merged from two queries. */
+export interface TreeEntryWithCommit {
+ name: string;
+ type: "BLOB" | "TREE" | "SYMLINK" | "SUBMODULE";
+ hash: string;
lastCommit?: {
hash: string;
shortHash: string;
@@ -25,7 +27,7 @@ interface FileTreeProps {
export function FileTree({ repo, currentRef, currentPath, entries }: FileTreeProps) {
// Directories first, then files — each group alphabetical
const sorted = entries.toSorted((a, b) => {
- if (a.type !== b.type) return a.type === GitObjectType.Tree ? -1 : 1;
+ if (a.type !== b.type) return a.type === "TREE" ? -1 : 1;
return a.name.localeCompare(b.name);
});
@@ -77,7 +79,7 @@ function FileTreeRow({
currentRef: string;
currentPath: string;
}) {
- const isDir = entry.type === GitObjectType.Tree;
+ const isDir = entry.type === "TREE";
const entryPath = currentPath ? `${currentPath}/${entry.name}` : entry.name;
const entryLink = isDir
@@ -8,13 +8,27 @@ import { useState, useEffect, useCallback, Fragment, type ReactNode } from "reac
import { jsx, jsxs } from "react/jsx-runtime";
import type { ShikiTransformer } from "shiki/core";
-import type { GitBlob } from "@/__generated__/graphql";
+import { useSuspenseFragment } from "@apollo/client/react";
+import type { FragmentType } from "@apollo/client/masking";
+
+import { graphql } from "@/__generated__/gql";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { getHighlighter, SHIKI_THEMES } from "@/lib/shiki";
import styles from "./file-viewer.module.css";
+export const FILE_VIEWER_BLOB_FRAGMENT = graphql(`
+ fragment FileViewerBlob on GitBlob {
+ path
+ hash
+ text
+ size
+ isBinary
+ isTruncated
+ }
+`);
+
interface LangEntry {
id: string;
load: () => Promise<unknown>;
@@ -124,22 +138,14 @@ function buildHash(range: LineRange): string {
// ── Component ─────────────────────────────────────────────────────────────────
interface FileViewerProps {
- blob: GitBlob | null;
+ blob: FragmentType<typeof FILE_VIEWER_BLOB_FRAGMENT>;
}
-export function FileViewer({ blob }: FileViewerProps) {
- if (!blob) {
- return (
- <div className="divide-border border-border divide-y rounded-md border">
- <div className="flex items-center gap-2 px-4 py-2">
- <Skeleton className="h-4 w-48" />
- </div>
- <div className="p-4">
- <Skeleton className="h-64 w-full" />
- </div>
- </div>
- );
- }
+export function FileViewer({ blob: blobProp }: FileViewerProps) {
+ const { data: blob } = useSuspenseFragment({
+ fragment: FILE_VIEWER_BLOB_FRAGMENT,
+ from: blobProp,
+ });
const [highlighted, setHighlighted] = useState<{ node: ReactNode; lineCount: number } | null>(
null,
@@ -11,12 +11,7 @@ const BLOB_QUERY = graphql(`
query CodePageBlob($repo: String, $ref: String!, $path: String!) {
repository(ref: $repo) {
blob(ref: $ref, path: $path) {
- path
- hash
- text
- size
- isBinary
- isTruncated
+ ...FileViewerBlob
}
}
}
@@ -51,5 +46,7 @@ function BlobView() {
const { blobRef } = Route.useLoaderData();
const { data } = useReadQuery(blobRef);
- return <FileViewer blob={data?.repository?.blob ?? null} />;
+ const blob = data?.repository?.blob;
+ if (!blob) return <BlobSkeleton />;
+ return <FileViewer blob={blob} />;
}
@@ -17,25 +17,7 @@ const BUG_DETAIL_QUERY = graphql(`
}
}
timeline(first: 250) {
- nodes {
- __typename
- id
- ... on BugCreateTimelineItem {
- ...BugCreateCommentFields
- }
- ... on BugAddCommentTimelineItem {
- ...BugAddCommentFields
- }
- ... on BugLabelChangeTimelineItem {
- ...LabelChangeFields
- }
- ... on BugSetStatusTimelineItem {
- ...StatusChangeFields
- }
- ... on BugSetTitleTimelineItem {
- ...TitleChangeFields
- }
- }
+ ...TimelineItems
}
}
}
@@ -110,7 +92,7 @@ function RouteComponent() {
<div className="flex gap-8">
{/* Timeline + comment box */}
<div className="min-w-0 flex-1 space-y-4">
- <Timeline repo={repo} bugPrefix={bug.humanId} items={bug.timeline.nodes} />
+ <Timeline repo={repo} bugPrefix={bug.humanId} timeline={bug.timeline} />
<CommentBox bugPrefix={bug.humanId} bugStatus={bug.status} ref_={ref} />
</div>