diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 2272bde..bea94a2 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -13,6 +13,7 @@ import { ToastStack } from './components/ToastStack' import { useIssues } from './hooks/useIssues' import { useManagedSessions } from './hooks/useManagedSessions' import { useProfiles } from './hooks/useProfiles' +import { filterIssues } from './lib/issueFilter' import { compareIssuesInColumn } from './lib/issueSort' import { COLUMN_ORDER } from './types' @@ -127,6 +128,9 @@ export function App() { setShowNewIssueModal(false) } + // 顶部筛选框内容。纯前端过滤,不影响拖拽与详情面板拿到的全量列表。 + const [filterQuery, setFilterQuery] = useState('') + // Drop selectedId if it's no longer present after issues refresh. const readyIssues = state.status === 'ready' ? state.issues : null useEffect(() => { @@ -153,13 +157,15 @@ export function App() { // Issues partitioned by column, in COLUMN_ORDER. Used for arrow navigation. // 排序与看板视觉一致:完成列按 PR 合并时间降序,其余列按工单号降序。 + // 键盘导航只在筛选后可见的卡片之间走,和看板显示保持一致。 const ordered = useMemo(() => { if (!readyIssues) return [] + const visible = filterIssues(readyIssues, filterQuery) return COLUMN_ORDER.map(col => - readyIssues.filter(i => i.column === col).sort((a, b) => compareIssuesInColumn(col, a, b)), + visible.filter(i => i.column === col).sort((a, b) => compareIssuesInColumn(col, a, b)), ) - }, [readyIssues]) + }, [readyIssues, filterQuery]) // Resolved selected issue (or null) for the detail panel. const selectedIssue = useMemo(() => { @@ -312,6 +318,8 @@ export function App() { {state.status === 'loading' && ( <> setShowNewIssueModal(true)} selectedId={selectedId} @@ -445,6 +456,8 @@ export function App() { {state.status === 'error' && ( <> void /** Renders the + button on the todo column when provided. */ onCreateIssue?: () => void @@ -103,6 +109,7 @@ function buildTodoTree(todoIssues: Issue[]): Array<{ issue: Issue, depth: number export function KanbanBoard({ issues, + filterQuery = '', onIssuesChange, onCreateIssue, selectedId, @@ -127,7 +134,7 @@ export function KanbanBoard({ 'review': [], 'done': [], } - for (const issue of issues) + for (const issue of filterIssues(issues, filterQuery)) map[issue.column].push(issue) // 各列排序:完成列按 PR 合并时间降序(最近合并在最前),其余列按 issue.number // 降序让新工单置顶。todo 列的降序顺序同时是 `buildTodoTree` 的输入顺序——它按 @@ -135,6 +142,14 @@ export function KanbanBoard({ for (const col of Object.keys(map) as IssueColumn[]) map[col].sort((a, b) => compareIssuesInColumn(col, a, b)) return map + }, [issues, filterQuery]) + + // 未筛选时的每列总数,供列头角标显示「可见 / 总数」。 + const totalByColumn = useMemo(() => { + const map: Record = { 'todo': 0, 'in-progress': 0, 'review': 0, 'done': 0 } + for (const issue of issues) + map[issue.column] += 1 + return map }, [issues]) const activeIssue = activeId ? issues.find(i => i.id === activeId) ?? null : null @@ -240,6 +255,7 @@ export function KanbanBoard({ key={column} column={column} issues={columnIssues} + totalCount={totalByColumn[column]} onCreate={column === 'todo' ? onCreateIssue : undefined} > diff --git a/vscode/webview-ui/src/components/KanbanColumn.tsx b/vscode/webview-ui/src/components/KanbanColumn.tsx index 3fde9d3..c58c389 100644 --- a/vscode/webview-ui/src/components/KanbanColumn.tsx +++ b/vscode/webview-ui/src/components/KanbanColumn.tsx @@ -9,12 +9,15 @@ import { Badge } from './ui/badge' interface KanbanColumnProps { column: IssueColumn issues: Issue[] + /** 该列未经筛选的卡片总数。与 `issues.length` 不等时角标显示「可见 / 总数」。 */ + totalCount?: number children: ReactNode /** When provided, renders a + button in the header. Used for the todo column. */ onCreate?: () => void } -export function KanbanColumn({ column, issues, children, onCreate }: KanbanColumnProps) { +export function KanbanColumn({ column, issues, totalCount, children, onCreate }: KanbanColumnProps) { + const filtered = totalCount !== undefined && totalCount !== issues.length const { setNodeRef, isOver } = useDroppable({ id: column }) return ( @@ -31,7 +34,7 @@ export function KanbanColumn({ column, issues, children, onCreate }: KanbanColum {COLUMN_LABELS[column]}
- {issues.length} + {filtered ? `${issues.length} / ${totalCount}` : issues.length} {onCreate && (
) : ( diff --git a/vscode/webview-ui/src/components/PanelHeader.tsx b/vscode/webview-ui/src/components/PanelHeader.tsx index 7ba593c..05b04ff 100644 --- a/vscode/webview-ui/src/components/PanelHeader.tsx +++ b/vscode/webview-ui/src/components/PanelHeader.tsx @@ -1,8 +1,11 @@ import type { ReleaseAction, ReleaseStatus } from '../hooks/useIssues' -import { GitCommit, ListPlus, Loader2, Lock, RefreshCw, Rocket, Settings, Unlock, User, Users } from 'lucide-react' +import { GitCommit, ListPlus, Loader2, Lock, RefreshCw, Rocket, Search, Settings, Unlock, User, Users, X } from 'lucide-react' import { useEffect, useRef, useState } from 'react' interface Props { + /** 顶部筛选框当前内容;按标题 / 工单号 / PR 号过滤看板卡片。 */ + filterQuery: string + onFilterChange: (query: string) => void onRefresh: () => void /** host 正在重拉看板;为 true 时刷新图标转起来。 */ refreshing: boolean @@ -66,6 +69,8 @@ function releaseTitle(release: ReleaseStatus | undefined): string { } export function PanelHeader({ + filterQuery, + onFilterChange, onRefresh, refreshing, scope, @@ -113,7 +118,35 @@ export function PanelHeader({ } return ( -
+
+
+ + onFilterChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Escape') { + e.stopPropagation() + onFilterChange('') + } + }} + placeholder="筛选:标题 / 工单号 / PR 号" + aria-label="筛选工单" + className="h-6 w-56 rounded border border-[var(--vscode-input-border,transparent)] bg-[var(--vscode-input-background)] pl-6 pr-6 text-xs text-[var(--vscode-input-foreground)] outline-none placeholder:text-[var(--vscode-input-placeholderForeground)] focus:border-[var(--vscode-focusBorder)]" + /> + {filterQuery !== '' && ( + + )} +