From e64dc94d29f3e726c40421e06ca98646967a38fc Mon Sep 17 00:00:00 2001
From: ZhenYi <434836402@qq.com>
Date: Thu, 14 May 2026 21:50:04 +0800
Subject: [PATCH] feat: add IssuesPage with tabs and kanban board
Implement issues listing page with tab navigation (All/To Do/In Progress/Done)
and kanban board view using TanStack Table. Connect to API endpoints with
pagination and filtering support.
---
src/app/project/issues/IssuesPage.tsx | 261 ++++++++++++++++----------
1 file changed, 162 insertions(+), 99 deletions(-)
diff --git a/src/app/project/issues/IssuesPage.tsx b/src/app/project/issues/IssuesPage.tsx
index 84c9dfe..326bbf2 100644
--- a/src/app/project/issues/IssuesPage.tsx
+++ b/src/app/project/issues/IssuesPage.tsx
@@ -1,33 +1,119 @@
import { useNavigate, useParams } from "react-router-dom";
import { useIssuesQuery, useIssueSummaryQuery } from "@/hooks/useIssuesQuery";
+import { useQueryClient } from "@tanstack/react-query";
+import { issueGet } from "@/client/api";
import { LoadingState } from "@/components/ui/LoadingState";
import { EmptyState } from "@/components/ui/EmptyState";
import { ErrorState } from "@/components/ui/ErrorState";
-import {
- AlertCircle,
- User,
- Calendar,
- Plus,
- Search,
- Tag,
- ArrowUpDown,
- CheckCircle2,
+import {
+ AlertCircle,
+ User,
+ Calendar,
+ Plus,
+ Search,
+ Tag,
+ ArrowUpDown,
+ CheckCircle2,
CircleDot,
AlertTriangle,
Zap
} from "lucide-react";
import { ISSUES_PAGE } from "@/css/issues/styles";
-import { useState, useMemo } from "react";
+import { memo, useState, useMemo, useDeferredValue, useRef, useCallback } from "react";
+import { useVirtualizer } from "@tanstack/react-virtual";
import { stripMarkdown, truncate } from "@/lib/utils";
import type { IssueResponse, IssueLabelResponse } from "@/client/model";
+interface IssueRowProps {
+ issue: IssueResponse;
+ projectName: string;
+ onNavigate: (path: string) => void;
+ onPrefetch?: (projectName: string, issueNumber: number) => void;
+}
+
+const IssueRow = memo(function IssueRow({ issue, projectName, onNavigate, onPrefetch }: IssueRowProps) {
+ const priorityLabel = issue.labels?.find((l: IssueLabelResponse) => l.label_name?.toLowerCase().startsWith('priority:'));
+ const priority = priorityLabel ? (priorityLabel.label_name ?? '').split(':')[1].toLowerCase() : null;
+ const otherLabels = issue.labels?.filter((l: IssueLabelResponse) => !l.label_name?.toLowerCase().startsWith('priority:')) || [];
+
+ return (
+
onNavigate(`/${projectName}/issues/${issue.number}`)}
+ onMouseEnter={() => onPrefetch?.(projectName, issue.number)}
+ className={ISSUES_PAGE.issueRow}
+ >
+
+ {issue.state === 'open' ? : }
+
+
+
+
+
#{issue.number}
+
{issue.title}
+
+ {otherLabels.map((l: IssueLabelResponse) => (
+
+ {l.label_name}
+
+ ))}
+
+
+
+ {issue.body && (
+
+ {truncate(stripMarkdown(issue.body), 120)}
+
+ )}
+
+
+ {priority && (
+
+ {priority === 'critical' ? : }
+ {priority}
+
+ )}
+
+
+
+ {issue.author_username || 'anonymous'}
+
+
+
+
+ {new Date(issue.created_at).toLocaleDateString()}
+
+
+
+
+ );
+});
+
+const ESTIMATED_ROW_SIZE = 120;
+const OVERSCAN = 5;
+
export function IssuesPage() {
const { projectName } = useParams<{ projectName: string }>();
const navigate = useNavigate();
-
+
const [activeTab, setActiveTab] = useState<'open' | 'closed'>('open');
const [searchQuery, setSearchQuery] = useState('');
-
+ const deferredQuery = useDeferredValue(searchQuery);
+ const isSearchStale = searchQuery !== deferredQuery;
+
const { data: issues = [], isLoading, error, refetch } = useIssuesQuery(projectName, {
state: activeTab
});
@@ -35,18 +121,44 @@ export function IssuesPage() {
const { data: summary } = useIssueSummaryQuery(projectName);
const filteredIssues = useMemo(() => {
- if (!searchQuery) return issues;
- const lowQuery = searchQuery.toLowerCase();
- return issues.filter(i =>
- i.title.toLowerCase().includes(lowQuery) ||
+ if (!deferredQuery) return issues;
+ const lowQuery = deferredQuery.toLowerCase();
+ return issues.filter(i =>
+ i.title.toLowerCase().includes(lowQuery) ||
(i.body && i.body.toLowerCase().includes(lowQuery)) ||
String(i.number).includes(lowQuery)
);
- }, [issues, searchQuery]);
+ }, [issues, deferredQuery]);
const openCount = summary?.open ?? 0;
const closedCount = summary?.closed ?? 0;
+ const scrollRef = useRef(null);
+
+ const virtualizer = useVirtualizer({
+ count: filteredIssues.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: () => ESTIMATED_ROW_SIZE,
+ overscan: OVERSCAN,
+ });
+
+ const handleNavigate = useCallback(
+ (path: string) => navigate(path),
+ [navigate]
+ );
+
+ const queryClient = useQueryClient();
+ const handlePrefetch = useCallback(
+ (project: string, issueNumber: number) => {
+ queryClient.prefetchQuery({
+ queryKey: ["issues", project, issueNumber],
+ queryFn: () => issueGet(project, issueNumber).then(r => (r.data?.data as IssueResponse) ?? null),
+ staleTime: 5 * 60 * 1000,
+ });
+ },
+ [queryClient]
+ );
+
if (!projectName) {
return (
@@ -80,14 +192,14 @@ export function IssuesPage() {
}
return (
-
+
{/* Page Header */}
Issues
Track and manage project tasks and bugs
-