feat(web): enable Apollo data masking with useSuspenseFragment

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

Enable dataMasking on the Apollo Client and adopt useSuspenseFragment
for fragment colocation. Components now receive masked fragment data
via a `from` prop typed with FragmentType, and unmask it internally.
This prepares the codebase for @defer support on fragments.

Components updated:
- CommentCard.AuthorAvatar: from={identity}
- LabelBadge/LabelBadgeLink: from={label}

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

Change summary

webui2/src/__generated__/gql.ts                  | 18 ++++----
webui2/src/__generated__/graphql.ts              |  0 
webui2/src/components/bugs/comment-box.tsx       |  2 
webui2/src/components/bugs/label-editor.tsx      | 17 +++-----
webui2/src/components/bugs/timeline.tsx          | 20 +++++-----
webui2/src/components/shared/comment-card.tsx    | 19 ++++++---
webui2/src/components/shared/issue-filters.tsx   |  9 ++--
webui2/src/components/shared/issue-row.tsx       |  3 +
webui2/src/components/shared/label-badge.tsx     | 33 +++++++++--------
webui2/src/lib/apollo.ts                         |  1 
webui2/src/routes/$repo/_issues.tsx              |  1 
webui2/src/routes/$repo/_issues/issues/index.tsx |  3 -
webui2/src/routes/$repo/_issues/user/$id.tsx     |  2 
13 files changed, 68 insertions(+), 60 deletions(-)

Detailed changes

webui2/src/__generated__/gql.ts 🔗

@@ -19,7 +19,7 @@ type Documents = {
     "\n  mutation BugAddCommentAndReopen($input: BugAddCommentAndReopenInput!) {\n    bugAddCommentAndReopen(input: $input) {\n      bug {\n        id\n      }\n    }\n  }\n": typeof types.BugAddCommentAndReopenDocument,
     "\n  mutation BugStatusOpen($input: BugStatusOpenInput!) {\n    bugStatusOpen(input: $input) {\n      bug {\n        id\n        status\n      }\n    }\n  }\n": typeof types.BugStatusOpenDocument,
     "\n  mutation BugStatusClose($input: BugStatusCloseInput!) {\n    bugStatusClose(input: $input) {\n      bug {\n        id\n        status\n      }\n    }\n  }\n": typeof types.BugStatusCloseDocument,
-    "\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          color {\n            R\n            G\n            B\n          }\n        }\n      }\n    }\n  }\n": typeof types.BugChangeLabelsDocument,
+    "\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          ...LabelFields\n        }\n      }\n    }\n  }\n": typeof types.BugChangeLabelsDocument,
     "\n  fragment BugCreateCommentFields on BugCreateTimelineItem {\n    author {\n      ...IdentitySummary\n    }\n    message\n    createdAt\n    lastEdit\n    edited\n  }\n": typeof types.BugCreateCommentFieldsFragmentDoc,
     "\n  fragment BugAddCommentFields on BugAddCommentTimelineItem {\n    author {\n      ...IdentitySummary\n    }\n    message\n    createdAt\n    lastEdit\n    edited\n  }\n": typeof types.BugAddCommentFieldsFragmentDoc,
     "\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,
@@ -30,7 +30,7 @@ type Documents = {
     "\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 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      ...LabelFields\n    }\n    author {\n      ...IdentitySummary\n    }\n    createdAt\n    comments {\n      totalCount\n    }\n  }\n": typeof types.BugSummaryFragmentDoc,
+    "\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        nodes {\n          name\n          shortName\n          type\n          hash\n        }\n      }\n    }\n  }\n": typeof types.CodePageRefsDocument,
@@ -39,7 +39,7 @@ type Documents = {
     "\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        }\n      }\n    }\n  }\n": typeof types.ValidLabelsDocument,
+    "\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 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,
@@ -53,7 +53,7 @@ const documents: Documents = {
     "\n  mutation BugAddCommentAndReopen($input: BugAddCommentAndReopenInput!) {\n    bugAddCommentAndReopen(input: $input) {\n      bug {\n        id\n      }\n    }\n  }\n": types.BugAddCommentAndReopenDocument,
     "\n  mutation BugStatusOpen($input: BugStatusOpenInput!) {\n    bugStatusOpen(input: $input) {\n      bug {\n        id\n        status\n      }\n    }\n  }\n": types.BugStatusOpenDocument,
     "\n  mutation BugStatusClose($input: BugStatusCloseInput!) {\n    bugStatusClose(input: $input) {\n      bug {\n        id\n        status\n      }\n    }\n  }\n": types.BugStatusCloseDocument,
-    "\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          color {\n            R\n            G\n            B\n          }\n        }\n      }\n    }\n  }\n": types.BugChangeLabelsDocument,
+    "\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          ...LabelFields\n        }\n      }\n    }\n  }\n": types.BugChangeLabelsDocument,
     "\n  fragment BugCreateCommentFields on BugCreateTimelineItem {\n    author {\n      ...IdentitySummary\n    }\n    message\n    createdAt\n    lastEdit\n    edited\n  }\n": types.BugCreateCommentFieldsFragmentDoc,
     "\n  fragment BugAddCommentFields on BugAddCommentTimelineItem {\n    author {\n      ...IdentitySummary\n    }\n    message\n    createdAt\n    lastEdit\n    edited\n  }\n": types.BugAddCommentFieldsFragmentDoc,
     "\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,
@@ -64,7 +64,7 @@ const documents: Documents = {
     "\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 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      ...LabelFields\n    }\n    author {\n      ...IdentitySummary\n    }\n    createdAt\n    comments {\n      totalCount\n    }\n  }\n": types.BugSummaryFragmentDoc,
+    "\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        nodes {\n          name\n          shortName\n          type\n          hash\n        }\n      }\n    }\n  }\n": types.CodePageRefsDocument,
@@ -73,7 +73,7 @@ const documents: Documents = {
     "\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        }\n      }\n    }\n  }\n": types.ValidLabelsDocument,
+    "\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 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,
@@ -119,7 +119,7 @@ export function graphql(source: "\n  mutation BugStatusClose($input: BugStatusCl
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          color {\n            R\n            G\n            B\n          }\n        }\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          color {\n            R\n            G\n            B\n          }\n        }\n      }\n    }\n  }\n"];
+export function graphql(source: "\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          ...LabelFields\n        }\n      }\n    }\n  }\n"): (typeof documents)["\n  mutation BugChangeLabels($input: BugChangeLabelInput) {\n    bugChangeLabels(input: $input) {\n      bug {\n        id\n        labels {\n          name\n          ...LabelFields\n        }\n      }\n    }\n  }\n"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
@@ -163,7 +163,7 @@ export function graphql(source: "\n  fragment IdentitySummary on Identity {\n
 /**
  * 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 BugSummary on Bug {\n    id\n    humanId\n    status\n    title\n    labels {\n      ...LabelFields\n    }\n    author {\n      ...IdentitySummary\n    }\n    createdAt\n    comments {\n      totalCount\n    }\n  }\n"): (typeof documents)["\n  fragment BugSummary on Bug {\n    id\n    humanId\n    status\n    title\n    labels {\n      ...LabelFields\n    }\n    author {\n      ...IdentitySummary\n    }\n    createdAt\n    comments {\n      totalCount\n    }\n  }\n"];
+export function graphql(source: "\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 documents)["\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"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
@@ -199,7 +199,7 @@ export function graphql(source: "\n  query AllIdentities($ref: String) {\n    re
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "\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        }\n      }\n    }\n  }\n"): (typeof documents)["\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        }\n      }\n    }\n  }\n"];
+export function graphql(source: "\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 documents)["\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"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */

webui2/src/components/bugs/comment-box.tsx 🔗

@@ -120,7 +120,7 @@ export function CommentBox({ bugPrefix, bugStatus, ref_ }: CommentBoxProps) {
 
   return (
     <CommentCard.Root>
-      <CommentCard.AuthorAvatar avatarUrl={user.avatarUrl} displayName={user.displayName} />
+      <CommentCard.AuthorAvatar from={user} />
       <CommentCard.Card>
         <WritePreview.Root hasContent={hasMessage} preview={preview} onPreviewChange={setPreview}>
           <WritePreview.Tabs className="border-border border-b px-4 py-2" />

webui2/src/components/bugs/label-editor.tsx 🔗

@@ -14,10 +14,11 @@ import {
 import { Settings2 } from "lucide-react";
 import { useRef, useState } from "react";
 
+import type { FragmentType } from "@apollo/client/masking";
 import { BugDetailDocument } from "@/__generated__/graphql";
 import { graphql } from "@/__generated__/gql";
 import * as Listbox from "@/components/ui/listbox";
-import { LabelBadge } from "@/components/shared/label-badge";
+import { LabelBadge, LABEL_FIELDS_FRAGMENT } from "@/components/shared/label-badge";
 import { SectionHeading } from "@/components/shared/section-heading";
 import { useAuth } from "@/lib/auth";
 
@@ -28,11 +29,7 @@ const BUG_CHANGE_LABELS_MUTATION = graphql(`
         id
         labels {
           name
-          color {
-            R
-            G
-            B
-          }
+          ...LabelFields
         }
       }
     }
@@ -41,11 +38,11 @@ const BUG_CHANGE_LABELS_MUTATION = graphql(`
 
 interface LabelEditorProps {
   bugPrefix: string;
-  currentLabels: Array<{ name: string; color: { R: number; G: number; B: number } }>;
+  currentLabels: Array<{ name: string } & FragmentType<typeof LABEL_FIELDS_FRAGMENT>>;
   /** Current repo slug, passed as `ref` in refetch query variables. */
   ref_?: string | null;
   /** Pre-fetched valid labels for the repository. */
-  validLabels: Array<{ name: string; color: { R: number; G: number; B: number } }>;
+  validLabels: Array<{ name: string; color: { R: number; G: number; B: number } } & FragmentType<typeof LABEL_FIELDS_FRAGMENT>>;
 }
 
 // Gear-icon popover in the BugDetailPage sidebar for adding/removing labels.
@@ -160,7 +157,7 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_, validLabels }: Lab
                             : {}
                         }
                       />
-                      <LabelBadge name={label.name} color={label.color} />
+                      <LabelBadge from={label} />
                     </Listbox.Item>
                   );
                 })}
@@ -175,7 +172,7 @@ export function LabelEditor({ bugPrefix, currentLabels, ref_, validLabels }: Lab
       ) : (
         <div className="flex flex-wrap gap-1">
           {currentLabels.map((label) => (
-            <LabelBadge key={label.name} name={label.name} color={label.color} />
+            <LabelBadge key={label.name} from={label} />
           ))}
         </div>
       )}

webui2/src/components/bugs/timeline.tsx 🔗

@@ -13,7 +13,7 @@ import { LabelBadge } from "@/components/shared/label-badge";
 import { Textarea } from "@/components/ui/textarea";
 import { useAuth } from "@/lib/auth";
 
-const BUG_CREATE_COMMENT_FIELDS = graphql(`
+graphql(`
   fragment BugCreateCommentFields on BugCreateTimelineItem {
     author {
       ...IdentitySummary
@@ -25,7 +25,7 @@ const BUG_CREATE_COMMENT_FIELDS = graphql(`
   }
 `);
 
-const BUG_ADD_COMMENT_FIELDS = graphql(`
+graphql(`
   fragment BugAddCommentFields on BugAddCommentTimelineItem {
     author {
       ...IdentitySummary
@@ -37,7 +37,7 @@ const BUG_ADD_COMMENT_FIELDS = graphql(`
   }
 `);
 
-const LABEL_CHANGE_FIELDS = graphql(`
+graphql(`
   fragment LabelChangeFields on BugLabelChangeTimelineItem {
     author {
       humanId
@@ -53,7 +53,7 @@ const LABEL_CHANGE_FIELDS = graphql(`
   }
 `);
 
-const STATUS_CHANGE_FIELDS = graphql(`
+graphql(`
   fragment StatusChangeFields on BugSetStatusTimelineItem {
     author {
       humanId
@@ -64,7 +64,7 @@ const STATUS_CHANGE_FIELDS = graphql(`
   }
 `);
 
-const TITLE_CHANGE_FIELDS = graphql(`
+graphql(`
   fragment TitleChangeFields on BugSetTitleTimelineItem {
     author {
       humanId
@@ -164,7 +164,7 @@ function CommentItem({
 
   return (
     <CommentCard.Root>
-      <CommentCard.AuthorAvatar avatarUrl={item.author.avatarUrl} displayName={item.author.displayName} />
+      <CommentCard.AuthorAvatar from={item.author} />
       <CommentCard.Card>
         <CommentCard.CardHeader>
           <Link
@@ -257,16 +257,16 @@ function LabelChangeItem({ item, repo }: { item: LabelChangeItem; repo: string |
         {item.added.length > 0 && (
           <>
             added{" "}
-            {item.added.map((l) => (
-              <LabelBadge key={l.name} name={l.name} color={l.color} />
+            {item.added.map((l, i) => (
+              <LabelBadge key={i} from={l} />
             ))}{" "}
           </>
         )}
         {item.removed.length > 0 && (
           <>
             removed{" "}
-            {item.removed.map((l) => (
-              <LabelBadge key={l.name} name={l.name} color={l.color} />
+            {item.removed.map((l, i) => (
+              <LabelBadge key={i} from={l} />
             ))}{" "}
           </>
         )}

webui2/src/components/shared/comment-card.tsx 🔗

@@ -1,10 +1,12 @@
-import type { IdentitySummaryFragment } from "@/__generated__/graphql";
+import { useSuspenseFragment } from "@apollo/client/react";
+import type { FragmentType } from "@apollo/client/masking";
+
 import { graphql } from "@/__generated__/gql";
 import { cn } from "@/lib/utils";
 
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 
-const IDENTITY_SUMMARY_FRAGMENT = graphql(`
+export const IDENTITY_SUMMARY_FRAGMENT = graphql(`
   fragment IdentitySummary on Identity {
     id
     humanId
@@ -22,16 +24,19 @@ export function Root({ children, className }: RootProps) {
   return <div className={cn("flex gap-3", className)}>{children}</div>;
 }
 
-type AuthorAvatarProps = Pick<IdentitySummaryFragment, "displayName" | "avatarUrl"> & {
+interface AuthorAvatarProps {
+  from: FragmentType<typeof IDENTITY_SUMMARY_FRAGMENT>;
   className?: string;
-};
+}
+
+export function AuthorAvatar({ from, className }: AuthorAvatarProps) {
+  const { data } = useSuspenseFragment({ fragment: IDENTITY_SUMMARY_FRAGMENT, from });
 
-export function AuthorAvatar({ avatarUrl, displayName, className }: AuthorAvatarProps) {
   return (
     <Avatar className={cn("mt-1 size-8 shrink-0", className)}>
-      <AvatarImage src={avatarUrl ?? undefined} alt={displayName} />
+      <AvatarImage src={data.avatarUrl ?? undefined} alt={data.displayName} />
       <AvatarFallback className="text-xs">
-        {displayName.slice(0, 2).toUpperCase()}
+        {data.displayName.slice(0, 2).toUpperCase()}
       </AvatarFallback>
     </Avatar>
   );

webui2/src/components/shared/issue-filters.tsx 🔗

@@ -14,13 +14,14 @@ import {
 import { ArrowUpDown, ChevronDown, Tag, User, X } from "lucide-react";
 import { useMemo, useRef, useState, useCallback, useEffect } from "react";
 
+import type { FragmentType } from "@apollo/client/masking";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
 import * as Listbox from "@/components/ui/listbox";
 import { useAuth } from "@/lib/auth";
 import { SORT_OPTIONS, type SortValue } from "@/lib/query-utils";
 import { cn } from "@/lib/utils";
 
-import { LabelBadge } from "@/components/shared/label-badge";
+import { LabelBadge, LABEL_FIELDS_FRAGMENT } from "@/components/shared/label-badge";
 
 // Max authors shown in the non-searching state. We intentionally cap this to
 // avoid a giant list — the current-user + recently-seen pattern covers the
@@ -46,10 +47,10 @@ function authorQueryValue(i: {
 
 export type { SortValue } from "@/lib/query-utils";
 
-export interface LabelItem {
+export type LabelItem = {
   name: string;
   color: { R: number; G: number; B: number };
-}
+} & FragmentType<typeof LABEL_FIELDS_FRAGMENT>;
 
 export interface IdentityItem {
   id: string;
@@ -372,7 +373,7 @@ function LabelFilter({
                           opacity: active ? 1 : 0.35,
                         }}
                       />
-                      <LabelBadge name={label.name} color={label.color} />
+                      <LabelBadge from={label} />
                     </Listbox.Item>
                   );
                 })}

webui2/src/components/shared/issue-row.tsx 🔗

@@ -4,13 +4,14 @@ import { Status } from "@/__generated__/graphql";
 import { graphql } from "@/__generated__/gql";
 import { cn } from "@/lib/utils";
 
-const BUG_SUMMARY_FRAGMENT = graphql(`
+graphql(`
   fragment BugSummary on Bug {
     id
     humanId
     status
     title
     labels {
+      name
       ...LabelFields
     }
     author {

webui2/src/components/shared/label-badge.tsx 🔗

@@ -1,10 +1,11 @@
+import { useSuspenseFragment } from "@apollo/client/react";
+import type { FragmentType } from "@apollo/client/masking";
 import { createLink, type LinkComponent } from "@tanstack/react-router";
 import * as React from "react";
 
-import type { LabelFieldsFragment } from "@/__generated__/graphql";
 import { graphql } from "@/__generated__/gql";
 
-const LABEL_FIELDS_FRAGMENT = graphql(`
+export const LABEL_FIELDS_FRAGMENT = graphql(`
   fragment LabelFields on Label {
     name
     color {
@@ -15,24 +16,25 @@ const LABEL_FIELDS_FRAGMENT = graphql(`
   }
 `);
 
-type LabelBadgeProps = LabelFieldsFragment & {
-  className?: string;
-};
-
 function contrastColor(r: number, g: number, b: number): string {
-  // Perceived luminance — pick black or white text for readability
   const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
   return luminance > 0.55 ? "rgba(0,0,0,0.75)" : "rgba(255,255,255,0.9)";
 }
 
+interface LabelBadgeProps {
+  from: FragmentType<typeof LABEL_FIELDS_FRAGMENT>;
+  className?: string;
+}
+
 // Coloured label pill. Always renders as a <span>.
 // Use LabelBadgeLink for a clickable variant that navigates.
 const LabelBadge = React.forwardRef<
   HTMLSpanElement,
   LabelBadgeProps & Omit<React.HTMLAttributes<HTMLSpanElement>, "color">
->(({ name, color, className, ...props }, ref) => {
-  const bg = `rgb(${color.R},${color.G},${color.B})`;
-  const text = contrastColor(color.R, color.G, color.B);
+>(({ from, className, ...props }, ref) => {
+  const { data } = useSuspenseFragment({ fragment: LABEL_FIELDS_FRAGMENT, from });
+  const bg = `rgb(${data.color.R},${data.color.G},${data.color.B})`;
+  const text = contrastColor(data.color.R, data.color.G, data.color.B);
 
   return (
     <span
@@ -41,7 +43,7 @@ const LabelBadge = React.forwardRef<
       style={{ backgroundColor: bg, color: text }}
       {...props}
     >
-      {name}
+      {data.name}
     </span>
   );
 });
@@ -52,9 +54,10 @@ const CreatedLabelBadgeLink = createLink(
   React.forwardRef<
     HTMLAnchorElement,
     LabelBadgeProps & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "color">
-  >(({ name, color, className, ...props }, ref) => {
-    const bg = `rgb(${color.R},${color.G},${color.B})`;
-    const text = contrastColor(color.R, color.G, color.B);
+  >(({ from, className, ...props }, ref) => {
+    const { data } = useSuspenseFragment({ fragment: LABEL_FIELDS_FRAGMENT, from });
+    const bg = `rgb(${data.color.R},${data.color.G},${data.color.B})`;
+    const text = contrastColor(data.color.R, data.color.G, data.color.B);
 
     return (
       <a
@@ -63,7 +66,7 @@ const CreatedLabelBadgeLink = createLink(
         style={{ backgroundColor: bg, color: text }}
         {...props}
       >
-        {name}
+        {data.name}
       </a>
     );
   }),

webui2/src/lib/apollo.ts 🔗

@@ -9,6 +9,7 @@ const httpLink = new HttpLink({
 
 export const client = new ApolloClient({
   link: httpLink,
+  dataMasking: true,
 
   cache: new InMemoryCache({
     typePolicies: {

webui2/src/routes/$repo/_issues/issues/index.tsx 🔗

@@ -290,8 +290,7 @@ function RouteComponent() {
                 {bug.labels.map((label) => (
                   <LabelBadgeLink
                     key={label.name}
-                    name={label.name}
-                    color={label.color}
+                    from={label}
                     to="/$repo/issues"
                     params={{ repo }}
                     search={{

webui2/src/routes/$repo/_issues/user/$id.tsx 🔗

@@ -191,7 +191,7 @@ function RouteComponent() {
                   {bug.title}
                 </Link>
                 {bug.labels.map((label) => (
-                  <LabelBadge key={label.name} name={label.name} color={label.color} />
+                  <LabelBadge key={label.name} from={label} />
                 ))}
               </IssueRow.TitleArea>
               <IssueRow.Meta>