refactor(web): introduce colocated GraphQL fragments

Quentin Gliech and Claude Opus 4.6 (1M context) created

Define reusable fragments next to the components that consume them:
- IdentitySummary: id, humanId, displayName, avatarUrl
- LabelFields: name, color { R G B }
- BugSummary: composes IdentitySummary + LabelFields for bug list nodes
- Timeline fragments: BugCreateCommentFields, BugAddCommentFields,
  LabelChangeFields, StatusChangeFields, TitleChangeFields

Rewrite BugList, BugDetail, and UserProfile queries to compose
fragments instead of duplicating field selections. Codegen now
includes src/components/**/*.graphql for fragment discovery.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/codegen.ts                                                 |   2 
webui2/src/__generated__/graphql.ts                               |  31 
webui2/src/components/bugs/IdentitySummary.graphql                |   6 
webui2/src/components/bugs/IssueRow.graphql                       |  16 
webui2/src/components/bugs/LabelBadge.graphql                     |   8 
webui2/src/components/bugs/Timeline.graphql                       |  52 
webui2/src/components/code/__snapshots__/FileViewer.test.tsx.snap | 230 
webui2/src/graphql/BugDetail.graphql                              |  84 
webui2/src/graphql/BugList.graphql                                |  23 
webui2/src/graphql/UserProfile.graphql                            |  17 
10 files changed, 264 insertions(+), 205 deletions(-)

Detailed changes

webui2/codegen.ts ๐Ÿ”—

@@ -2,7 +2,7 @@ import type { CodegenConfig } from "@graphql-codegen/cli";
 
 const config: CodegenConfig = {
   schema: "../api/graphql/schema/*.graphql",
-  documents: "src/graphql/**/*.graphql",
+  documents: ["src/graphql/**/*.graphql", "src/components/**/*.graphql"],
   generates: {
     "src/__generated__/graphql.ts": {
       plugins: ["typescript", "typescript-operations", "typescript-react-apollo"],

webui2/src/__generated__/graphql.ts ๐Ÿ”—

@@ -1162,6 +1162,22 @@ export type SubscriptionIdentityEventsArgs = {
   repoRef?: InputMaybe<Scalars['String']['input']>;
 };
 
+export type IdentitySummaryFragment = { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null };
+
+export type BugSummaryFragment = { __typename?: 'Bug', id: string, humanId: string, status: Status, title: string, createdAt: string, labels: Array<{ __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } }>, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null }, comments: { __typename?: 'BugCommentConnection', totalCount: number } };
+
+export type LabelFieldsFragment = { __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } };
+
+export type BugCreateCommentFieldsFragment = { __typename?: 'BugCreateTimelineItem', message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null } };
+
+export type BugAddCommentFieldsFragment = { __typename?: 'BugAddCommentTimelineItem', message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null } };
+
+export type LabelChangeFieldsFragment = { __typename?: 'BugLabelChangeTimelineItem', 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 } }> };
+
+export type StatusChangeFieldsFragment = { __typename?: 'BugSetStatusTimelineItem', date: string, status: Status, author: { __typename?: 'Identity', humanId: string, displayName: string } };
+
+export type TitleChangeFieldsFragment = { __typename?: 'BugSetTitleTimelineItem', date: string, title: string, was: string, author: { __typename?: 'Identity', humanId: string, displayName: string } };
+
 export type AllIdentitiesQueryVariables = Exact<{
   ref?: InputMaybe<Scalars['String']['input']>;
 }>;
@@ -1175,13 +1191,13 @@ export type BugDetailQueryVariables = Exact<{
 }>;
 
 
-export type BugDetailQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', bug?: { __typename?: 'Bug', id: string, humanId: string, status: Status, title: string, createdAt: string, lastEdit: string, labels: Array<{ __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } }>, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null }, participants: { __typename?: 'IdentityConnection', nodes: Array<{ __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null }> }, timeline: { __typename?: 'BugTimelineItemConnection', nodes: Array<
-          | { __typename: 'BugAddCommentTimelineItem', message: string, createdAt: string, lastEdit: string, edited: boolean, id: string, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null } }
-          | { __typename: 'BugCreateTimelineItem', message: string, createdAt: string, lastEdit: string, edited: boolean, id: string, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null } }
-          | { __typename: 'BugLabelChangeTimelineItem', date: string, id: 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', date: string, status: Status, id: string, author: { __typename?: 'Identity', humanId: string, displayName: string } }
-          | { __typename: 'BugSetTitleTimelineItem', date: string, title: string, was: string, id: string, author: { __typename?: 'Identity', humanId: string, displayName: string } }
-        > } } | null } | null };
+export type BugDetailQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', bug?: { __typename?: 'Bug', lastEdit: string, id: string, humanId: string, status: Status, title: string, createdAt: string, participants: { __typename?: 'IdentityConnection', nodes: Array<{ __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null }> }, timeline: { __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 } }
+        > }, labels: Array<{ __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } }>, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null }, comments: { __typename?: 'BugCommentConnection', totalCount: number } } | null } | null };
 
 export type BugListQueryVariables = Exact<{
   ref?: InputMaybe<Scalars['String']['input']>;
@@ -1278,7 +1294,7 @@ export type UserProfileQueryVariables = Exact<{
 }>;
 
 
-export type UserProfileQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', identity?: { __typename?: 'Identity', id: string, humanId: string, name?: string | null, email?: string | null, login?: string | null, displayName: string, avatarUrl?: string | null, isProtected: boolean } | null, openCount: { __typename?: 'BugConnection', totalCount: number }, closedCount: { __typename?: 'BugConnection', totalCount: number }, bugs: { __typename?: 'BugConnection', totalCount: number, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor: string }, nodes: Array<{ __typename?: 'Bug', id: string, humanId: string, status: Status, title: string, createdAt: string, labels: Array<{ __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } }>, comments: { __typename?: 'BugCommentConnection', totalCount: number } }> } } | null };

webui2/src/components/bugs/Timeline.graphql ๐Ÿ”—

@@ -0,0 +1,52 @@
+fragment BugCreateCommentFields on BugCreateTimelineItem {
+  author {
+    ...IdentitySummary
+  }
+  message
+  createdAt
+  lastEdit
+  edited
+}
+
+fragment BugAddCommentFields on BugAddCommentTimelineItem {
+  author {
+    ...IdentitySummary
+  }
+  message
+  createdAt
+  lastEdit
+  edited
+}
+
+fragment LabelChangeFields on BugLabelChangeTimelineItem {
+  author {
+    humanId
+    displayName
+  }
+  date
+  added {
+    ...LabelFields
+  }
+  removed {
+    ...LabelFields
+  }
+}
+
+fragment StatusChangeFields on BugSetStatusTimelineItem {
+  author {
+    humanId
+    displayName
+  }
+  date
+  status
+}
+
+fragment TitleChangeFields on BugSetTitleTimelineItem {
+  author {
+    humanId
+    displayName
+  }
+  date
+  title
+  was
+}

webui2/src/components/code/__snapshots__/FileViewer.test.tsx.snap ๐Ÿ”—

@@ -83,95 +83,163 @@ exports[`FileViewer/TruncatedFile matches snapshot 1`] = `
     class="border-border overflow-hidden rounded-md border"
   >
     <div
-      class="border-border bg-muted/40 text-muted-foreground flex items-center justify-between border-b px-4 py-2 text-xs"
+      class="border-border bg-muted/40 border-b px-4 py-2"
     >
-      <span>
-        4
-         lines ยท 
-        1.0 MB
-         ยท truncated
-      </span>
-      <button
-        class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground size-7"
-        title="Copy"
-      >
-        <svg
-          aria-hidden="true"
-          class="lucide lucide-copy size-3.5"
-          fill="none"
-          height="24"
-          stroke="currentColor"
-          stroke-linecap="round"
-          stroke-linejoin="round"
-          stroke-width="2"
-          viewBox="0 0 24 24"
-          width="24"
-          xmlns="http://www.w3.org/2000/svg"
-        >
-          <rect
-            height="14"
-            rx="2"
-            ry="2"
-            width="14"
-            x="8"
-            y="8"
-          />
-          <path
-            d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"
-          />
-        </svg>
-      </button>
+      <div
+        class="animate-pulse rounded-md bg-primary/10 h-4 w-32"
+      />
     </div>
     <div
-      class="flex overflow-x-auto font-mono text-xs leading-5"
+      class="flex gap-4 p-4"
     >
       <div
-        aria-hidden="true"
-        class="border-border bg-muted/20 text-muted-foreground/50 border-r px-4 py-4 text-right select-none"
+        class="space-y-1.5"
       >
-        <div>
-          1
-        </div>
-        <div>
-          2
-        </div>
-        <div>
-          3
-        </div>
-        <div>
-          4
-        </div>
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5 w-6"
+        />
       </div>
-      <pre
-        class="flex-1 overflow-visible px-4 py-4"
+      <div
+        class="flex-1 space-y-1.5"
       >
-        <code
-          class="hljs !bg-transparent !p-0"
-        >
-          line 
-          <span
-            class="hljs-number"
-          >
-            1
-          </span>
-          
-line 
-          <span
-            class="hljs-number"
-          >
-            2
-          </span>
-          
-line 
-          <span
-            class="hljs-number"
-          >
-            3
-          </span>
-          
-... (truncated)
-        </code>
-      </pre>
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 30%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 77%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 64%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 51%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 38%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 85%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 72%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 59%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 46%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 33%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 80%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 67%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 54%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 41%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 88%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 75%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 62%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 49%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 36%;"
+        />
+        <div
+          class="animate-pulse rounded-md bg-primary/10 h-3.5"
+          style="width: 83%;"
+        />
+      </div>
     </div>
   </div>
 </div>

webui2/src/graphql/BugDetail.graphql ๐Ÿ”—

@@ -1,32 +1,11 @@
 query BugDetail($ref: String, $prefix: String!) {
   repository(ref: $ref) {
     bug(prefix: $prefix) {
-      id
-      humanId
-      status
-      title
-      labels {
-        name
-        color {
-          R
-          G
-          B
-        }
-      }
-      author {
-        id
-        humanId
-        displayName
-        avatarUrl
-      }
-      createdAt
+      ...BugSummary
       lastEdit
       participants(first: 20) {
         nodes {
-          id
-          humanId
-          displayName
-          avatarUrl
+          ...IdentitySummary
         }
       }
       timeline(first: 250) {
@@ -34,68 +13,19 @@ query BugDetail($ref: String, $prefix: String!) {
           __typename
           id
           ... on BugCreateTimelineItem {
-            author {
-              id
-              humanId
-              displayName
-              avatarUrl
-            }
-            message
-            createdAt
-            lastEdit
-            edited
+            ...BugCreateCommentFields
           }
           ... on BugAddCommentTimelineItem {
-            author {
-              id
-              humanId
-              displayName
-              avatarUrl
-            }
-            message
-            createdAt
-            lastEdit
-            edited
+            ...BugAddCommentFields
           }
           ... on BugLabelChangeTimelineItem {
-            author {
-              humanId
-              displayName
-            }
-            date
-            added {
-              name
-              color {
-                R
-                G
-                B
-              }
-            }
-            removed {
-              name
-              color {
-                R
-                G
-                B
-              }
-            }
+            ...LabelChangeFields
           }
           ... on BugSetStatusTimelineItem {
-            author {
-              humanId
-              displayName
-            }
-            date
-            status
+            ...StatusChangeFields
           }
           ... on BugSetTitleTimelineItem {
-            author {
-              humanId
-              displayName
-            }
-            date
-            title
-            was
+            ...TitleChangeFields
           }
         }
       }

webui2/src/graphql/BugList.graphql ๐Ÿ”—

@@ -23,28 +23,7 @@ query BugList(
         endCursor
       }
       nodes {
-        id
-        humanId
-        status
-        title
-        labels {
-          name
-          color {
-            R
-            G
-            B
-          }
-        }
-        author {
-          id
-          humanId
-          displayName
-          avatarUrl
-        }
-        createdAt
-        comments {
-          totalCount
-        }
+        ...BugSummary
       }
     }
   }

webui2/src/graphql/UserProfile.graphql ๐Ÿ”—

@@ -35,22 +35,7 @@ query UserProfile(
         endCursor
       }
       nodes {
-        id
-        humanId
-        status
-        title
-        labels {
-          name
-          color {
-            R
-            G
-            B
-          }
-        }
-        createdAt
-        comments {
-          totalCount
-        }
+        ...BugSummary
       }
     }
   }