diff --git a/apps/ui/src/components/views/github-issues-view.tsx b/apps/ui/src/components/views/github-issues-view.tsx index a34f1225..03275b02 100644 --- a/apps/ui/src/components/views/github-issues-view.tsx +++ b/apps/ui/src/components/views/github-issues-view.tsx @@ -56,19 +56,23 @@ export function GitHubIssuesView() { // Combine all issues for filtering const allIssues = useMemo(() => [...openIssues, ...closedIssues], [openIssues, closedIssues]); - // Apply filter to issues + // Apply filter to issues - now returns matched issues directly for better performance const filterResult = useIssuesFilter(allIssues, filterState, cachedValidations); - // Filter issues based on matched results - const filteredOpenIssues = useMemo( - () => openIssues.filter((issue) => filterResult.matchedIssueNumbers.has(issue.number)), - [openIssues, filterResult.matchedIssueNumbers] - ); - - const filteredClosedIssues = useMemo( - () => closedIssues.filter((issue) => filterResult.matchedIssueNumbers.has(issue.number)), - [closedIssues, filterResult.matchedIssueNumbers] - ); + // Separate filtered issues by state - this is O(n) but now only done once + // since filterResult.matchedIssues already contains the filtered issues + const { filteredOpenIssues, filteredClosedIssues } = useMemo(() => { + const open: typeof openIssues = []; + const closed: typeof closedIssues = []; + for (const issue of filterResult.matchedIssues) { + if (issue.state.toLowerCase() === 'open') { + open.push(issue); + } else { + closed.push(issue); + } + } + return { filteredOpenIssues: open, filteredClosedIssues: closed }; + }, [filterResult.matchedIssues]); // Filter state change handlers const handleStateFilterChange = useCallback((stateFilter: IssuesStateFilter) => { diff --git a/apps/ui/src/components/views/github-issues-view/components/issues-filter-controls.tsx b/apps/ui/src/components/views/github-issues-view/components/issues-filter-controls.tsx index f2378ef0..475a32d5 100644 --- a/apps/ui/src/components/views/github-issues-view/components/issues-filter-controls.tsx +++ b/apps/ui/src/components/views/github-issues-view/components/issues-filter-controls.tsx @@ -20,6 +20,11 @@ import { cn } from '@/lib/utils'; import type { IssuesStateFilter } from '../types'; import { ISSUES_STATE_FILTER_OPTIONS } from '../types'; +/** Maximum number of labels to display before showing "+N more" in normal layout */ +const VISIBLE_LABELS_LIMIT = 3; +/** Maximum number of labels to display before showing "+N more" in compact layout */ +const VISIBLE_LABELS_LIMIT_COMPACT = 2; + interface IssuesFilterControlsProps { /** Current state filter value */ stateFilter: IssuesStateFilter; @@ -156,21 +161,27 @@ export function IssuesFilterControls({ {/* Selected Labels Display - shown on separate row */} {hasSelectedLabels && (
- {selectedLabels.slice(0, compact ? 2 : 3).map((label) => ( - handleLabelToggle(label)} - > - {label} - - - ))} - {selectedLabels.length > (compact ? 2 : 3) && ( + {selectedLabels + .slice(0, compact ? VISIBLE_LABELS_LIMIT_COMPACT : VISIBLE_LABELS_LIMIT) + .map((label) => ( + handleLabelToggle(label)} + > + {label} + + + ))} + {selectedLabels.length > + (compact ? VISIBLE_LABELS_LIMIT_COMPACT : VISIBLE_LABELS_LIMIT) && ( - +{selectedLabels.length - (compact ? 2 : 3)} more + + + {selectedLabels.length - + (compact ? VISIBLE_LABELS_LIMIT_COMPACT : VISIBLE_LABELS_LIMIT)}{' '} + more )}
diff --git a/apps/ui/src/components/views/github-issues-view/hooks/use-issues-filter.ts b/apps/ui/src/components/views/github-issues-view/hooks/use-issues-filter.ts index 3ab5f7bb..987e890a 100644 --- a/apps/ui/src/components/views/github-issues-view/hooks/use-issues-filter.ts +++ b/apps/ui/src/components/views/github-issues-view/hooks/use-issues-filter.ts @@ -200,8 +200,9 @@ export function useIssuesFilter( // Normalize search query for case-insensitive matching const normalizedQuery = searchQuery.toLowerCase().trim(); - // Filter issues based on all criteria - const matchedIssueNumbers = new Set(); + // Filter issues based on all criteria - return matched issues directly + // This eliminates the redundant O(n) filtering operation in the consuming component + const matchedIssues: GitHubIssue[] = []; for (const issue of issues) { // All conditions must be true for a match @@ -214,17 +215,17 @@ export function useIssuesFilter( matchesValidationStatus(issue, validationStatusFilter, cachedValidations); if (matchesAllFilters) { - matchedIssueNumbers.add(issue.number); + matchedIssues.push(issue); } } return { - matchedIssueNumbers, + matchedIssues, availableLabels, availableAssignees, availableMilestones, hasActiveFilter, - matchedCount: matchedIssueNumbers.size, + matchedCount: matchedIssues.length, }; }, [ issues, diff --git a/apps/ui/src/components/views/github-issues-view/types.ts b/apps/ui/src/components/views/github-issues-view/types.ts index d2986c16..a66e3a96 100644 --- a/apps/ui/src/components/views/github-issues-view/types.ts +++ b/apps/ui/src/components/views/github-issues-view/types.ts @@ -72,8 +72,8 @@ export interface IssuesFilterState { * Result of applying filters to the issues list */ export interface IssuesFilterResult { - /** Set of issue numbers that match the current filters */ - matchedIssueNumbers: Set; + /** Array of GitHubIssue objects that match the current filters */ + matchedIssues: GitHubIssue[]; /** Available labels from all issues (for filter dropdown population) */ availableLabels: string[]; /** Available assignees from all issues (for filter dropdown population) */ diff --git a/apps/ui/src/hooks/index.ts b/apps/ui/src/hooks/index.ts index 4627f2b3..8a354b3d 100644 --- a/apps/ui/src/hooks/index.ts +++ b/apps/ui/src/hooks/index.ts @@ -2,7 +2,6 @@ export { useAutoMode } from './use-auto-mode'; export { useBoardBackgroundSettings } from './use-board-background-settings'; export { useElectronAgent } from './use-electron-agent'; export { useGuidedPrompts } from './use-guided-prompts'; -export { useIssuesFilter } from '@/components/views/github-issues-view/hooks/use-issues-filter'; export { useKeyboardShortcuts } from './use-keyboard-shortcuts'; export { useMessageQueue } from './use-message-queue'; export { useOSDetection, type OperatingSystem, type OSDetectionResult } from './use-os-detection';