refactor(web): migrate issues list to IssueRow + LabelBadgeLink

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

Replace BugRow with IssueRow composition components in the issues list.
Labels now use LabelBadgeLink (createLink-wrapped) instead of onClick.

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

Change summary

webui2/src/components/bugs/BugRow.tsx            |  6 
webui2/src/routes/$repo/_issues/issues/index.tsx | 75 ++++++++++++-----
2 files changed, 54 insertions(+), 27 deletions(-)

Detailed changes

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

@@ -16,11 +16,11 @@ interface BugRowProps {
   createdAt: string;
   commentCount: number;
   repo: string;
-  onLabelClick?: (name: string) => void;
 }
 
 // Single row in the issue list. Shows status icon, title, labels, author and
-// comment count. Labels are clickable to filter the list by that label.
+// comment count.
+/** @deprecated Use IssueRow composition components instead. */
 export function BugRow({
   humanId,
   status,
@@ -30,7 +30,6 @@ export function BugRow({
   createdAt,
   commentCount,
   repo,
-  onLabelClick,
 }: BugRowProps) {
   const isOpen = status === Status.Open;
   const StatusIcon = isOpen ? CircleDot : CircleCheck;
@@ -59,7 +58,6 @@ export function BugRow({
               key={label.name}
               name={label.name}
               color={label.color}
-              onClick={onLabelClick}
             />
           ))}
         </div>

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

@@ -1,12 +1,14 @@
 import { useReadQuery } from "@apollo/client/react";
 import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
+import { formatDistanceToNow } from "date-fns";
 import { CircleDot, CircleCheck, ChevronLeft, ChevronRight } from "lucide-react";
 import { useState } from "react";
 import * as v from "valibot";
 
 import { type BugListQuery, BugListDocument } from "@/__generated__/graphql";
-import { BugRow } from "@/components/bugs/BugRow";
 import { IssueFilters } from "@/components/bugs/IssueFilters";
+import * as IssueRow from "@/components/bugs/IssueRow";
+import { LabelBadgeLink } from "@/components/bugs/LabelBadge";
 import type { SortValue } from "@/components/bugs/IssueFilters";
 import { QueryInput } from "@/components/bugs/QueryInput";
 import { Button } from "@/components/ui/button";
@@ -204,28 +206,55 @@ function RouteComponent() {
         )}
 
         {bugs?.nodes.map((bug) => (
-          <BugRow
-            key={bug.id}
-            id={bug.id}
-            humanId={bug.humanId}
-            status={bug.status}
-            title={bug.title}
-            labels={bug.labels}
-            author={bug.author}
-            createdAt={bug.createdAt}
-            commentCount={bug.comments.totalCount}
-            repo={repo}
-            onLabelClick={(name) => {
-              if (!selectedLabels.includes(name)) {
-                applyFilters(
-                  statusFilter,
-                  [...selectedLabels, name],
-                  selectedAuthorQuery,
-                  parsed.freeText,
-                );
-              }
-            }}
-          />
+          <IssueRow.Root key={bug.id} className="hover:bg-muted/30">
+            <IssueRow.StatusIcon status={bug.status} />
+            <div className="min-w-0 flex-1">
+              <IssueRow.TitleArea>
+                <Link
+                  to="/$repo/issues/$id"
+                  params={{ repo, id: bug.humanId }}
+                  className="text-foreground hover:text-primary font-medium hover:underline"
+                >
+                  {bug.title}
+                </Link>
+                {bug.labels.map((label) => (
+                  <LabelBadgeLink
+                    key={label.name}
+                    name={label.name}
+                    color={label.color}
+                    to="/$repo/issues"
+                    params={{ repo }}
+                    search={{
+                      q: buildQueryString(
+                        statusFilter,
+                        selectedLabels.includes(label.name)
+                          ? selectedLabels
+                          : [...selectedLabels, label.name],
+                        selectedAuthorQuery,
+                        parsed.freeText,
+                        sort,
+                      ),
+                      after: "",
+                    }}
+                    onClick={(e: React.MouseEvent) => e.stopPropagation()}
+                  />
+                ))}
+              </IssueRow.TitleArea>
+              <IssueRow.Meta>
+                #{bug.humanId} opened{" "}
+                {formatDistanceToNow(new Date(bug.createdAt), { addSuffix: true })} by{" "}
+                <Link
+                  to="/$repo/user/$id"
+                  params={{ repo, id: bug.author.humanId }}
+                  search={{ status: "open" as const, after: "" }}
+                  className="hover:underline"
+                >
+                  {bug.author.displayName}
+                </Link>
+              </IssueRow.Meta>
+            </div>
+            <IssueRow.CommentCount count={bug.comments.totalCount} />
+          </IssueRow.Root>
         ))}
 
         {totalPages > 1 && (