From 4dd00a98e402f4f8e2799b2f9bd91e96c030c506 Mon Sep 17 00:00:00 2001 From: James Date: Mon, 22 Dec 2025 19:06:05 -0500 Subject: [PATCH] add more filters about process status --- apps/ui/package.json | 2 +- .../components/graph-filter-controls.tsx | 134 +++++++++++++++++- .../views/graph-view/graph-canvas.tsx | 8 +- .../graph-view/hooks/use-graph-filter.ts | 66 +++++++-- 4 files changed, 193 insertions(+), 17 deletions(-) diff --git a/apps/ui/package.json b/apps/ui/package.json index 9c3522c6..116bb6ad 100644 --- a/apps/ui/package.json +++ b/apps/ui/package.json @@ -67,7 +67,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "dagre": "^0.8.5", + "apps/ui/src/components/views/graph-view/hooks/use-graph-filter.ts": "^0.8.5", "dotenv": "^17.2.3", "geist": "^1.5.1", "lucide-react": "^0.562.0", diff --git a/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx b/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx index 7747b8e1..4fb015d4 100644 --- a/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx +++ b/apps/ui/src/components/views/graph-view/components/graph-filter-controls.tsx @@ -4,9 +4,40 @@ import { Checkbox } from '@/components/ui/checkbox'; import { Switch } from '@/components/ui/switch'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { Filter, X, Eye, EyeOff, ChevronDown } from 'lucide-react'; +import { + Filter, + X, + Eye, + EyeOff, + ChevronDown, + Play, + Pause, + Clock, + CheckCircle2, + CircleDot, +} from 'lucide-react'; import { cn } from '@/lib/utils'; -import { GraphFilterState } from '../hooks/use-graph-filter'; +import { + GraphFilterState, + STATUS_FILTER_OPTIONS, + StatusFilterValue, +} from '../hooks/use-graph-filter'; + +// Status display configuration +const statusDisplayConfig: Record< + StatusFilterValue, + { label: string; icon: typeof Play; colorClass: string } +> = { + running: { label: 'Running', icon: Play, colorClass: 'text-[var(--status-in-progress)]' }, + paused: { label: 'Paused', icon: Pause, colorClass: 'text-[var(--status-warning)]' }, + backlog: { label: 'Backlog', icon: Clock, colorClass: 'text-muted-foreground' }, + waiting_approval: { + label: 'Waiting Approval', + icon: CircleDot, + colorClass: 'text-[var(--status-waiting)]', + }, + verified: { label: 'Verified', icon: CheckCircle2, colorClass: 'text-[var(--status-success)]' }, +}; interface GraphFilterControlsProps { filterState: GraphFilterState; @@ -14,6 +45,7 @@ interface GraphFilterControlsProps { hasActiveFilter: boolean; onSearchQueryChange: (query: string) => void; onCategoriesChange: (categories: string[]) => void; + onStatusesChange: (statuses: string[]) => void; onNegativeFilterChange: (isNegative: boolean) => void; onClearFilters: () => void; } @@ -24,10 +56,14 @@ export function GraphFilterControls({ hasActiveFilter, onSearchQueryChange, onCategoriesChange, + onStatusesChange, onNegativeFilterChange, onClearFilters, }: GraphFilterControlsProps) { - const { selectedCategories, isNegativeFilter } = filterState; + const { selectedCategories, selectedStatuses, isNegativeFilter } = filterState; + + // Suppress unused variable warning - onSearchQueryChange is used by parent for search input + void onSearchQueryChange; const handleCategoryToggle = (category: string) => { if (selectedCategories.includes(category)) { @@ -45,6 +81,22 @@ export function GraphFilterControls({ } }; + const handleStatusToggle = (status: string) => { + if (selectedStatuses.includes(status)) { + onStatusesChange(selectedStatuses.filter((s) => s !== status)); + } else { + onStatusesChange([...selectedStatuses, status]); + } + }; + + const handleSelectAllStatuses = () => { + if (selectedStatuses.length === STATUS_FILTER_OPTIONS.length) { + onStatusesChange([]); + } else { + onStatusesChange([...STATUS_FILTER_OPTIONS]); + } + }; + const categoryButtonLabel = selectedCategories.length === 0 ? 'All Categories' @@ -52,6 +104,14 @@ export function GraphFilterControls({ ? selectedCategories[0] : `${selectedCategories.length} Categories`; + const statusButtonLabel = + selectedStatuses.length === 0 + ? 'All Statuses' + : selectedStatuses.length === 1 + ? statusDisplayConfig[selectedStatuses[0] as StatusFilterValue]?.label || + selectedStatuses[0] + : `${selectedStatuses.length} Statuses`; + return ( @@ -130,6 +190,74 @@ export function GraphFilterControls({ + {/* Status Filter Dropdown */} + + + + + + + + Filter by Status + + +
+
Status
+ + {/* Select All option */} +
+ + + {selectedStatuses.length === STATUS_FILTER_OPTIONS.length + ? 'Deselect All' + : 'Select All'} + +
+ +
+ + {/* Status list */} +
+ {STATUS_FILTER_OPTIONS.map((status) => { + const config = statusDisplayConfig[status]; + const StatusIcon = config.icon; + return ( +
handleStatusToggle(status)} + > + handleStatusToggle(status)} + /> + + {config.label} +
+ ); + })} +
+
+ + + {/* Divider */}
diff --git a/apps/ui/src/components/views/graph-view/graph-canvas.tsx b/apps/ui/src/components/views/graph-view/graph-canvas.tsx index ca9a4017..1558bfe9 100644 --- a/apps/ui/src/components/views/graph-view/graph-canvas.tsx +++ b/apps/ui/src/components/views/graph-view/graph-canvas.tsx @@ -66,19 +66,21 @@ function GraphCanvasInner({ const [isLocked, setIsLocked] = useState(false); const [layoutDirection, setLayoutDirection] = useState<'LR' | 'TB'>('LR'); - // Filter state (category and negative toggle are local to graph view) + // Filter state (category, status, and negative toggle are local to graph view) const [selectedCategories, setSelectedCategories] = useState([]); + const [selectedStatuses, setSelectedStatuses] = useState([]); const [isNegativeFilter, setIsNegativeFilter] = useState(false); // Combined filter state const filterState: GraphFilterState = { searchQuery, selectedCategories, + selectedStatuses, isNegativeFilter, }; // Calculate filter results - const filterResult = useGraphFilter(features, filterState); + const filterResult = useGraphFilter(features, filterState, runningAutoTasks); // Transform features to nodes and edges with filter results const { nodes: initialNodes, edges: initialEdges } = useGraphNodes({ @@ -117,6 +119,7 @@ function GraphCanvasInner({ const handleClearFilters = useCallback(() => { onSearchQueryChange(''); setSelectedCategories([]); + setSelectedStatuses([]); setIsNegativeFilter(false); }, [onSearchQueryChange]); @@ -195,6 +198,7 @@ function GraphCanvasInner({ hasActiveFilter={filterResult.hasActiveFilter} onSearchQueryChange={onSearchQueryChange} onCategoriesChange={setSelectedCategories} + onStatusesChange={setSelectedStatuses} onNegativeFilterChange={setIsNegativeFilter} onClearFilters={handleClearFilters} /> diff --git a/apps/ui/src/components/views/graph-view/hooks/use-graph-filter.ts b/apps/ui/src/components/views/graph-view/hooks/use-graph-filter.ts index f7b469bb..615f9063 100644 --- a/apps/ui/src/components/views/graph-view/hooks/use-graph-filter.ts +++ b/apps/ui/src/components/views/graph-view/hooks/use-graph-filter.ts @@ -4,9 +4,21 @@ import { Feature } from '@/store/app-store'; export interface GraphFilterState { searchQuery: string; selectedCategories: string[]; + selectedStatuses: string[]; isNegativeFilter: boolean; } +// Available status filter values +export const STATUS_FILTER_OPTIONS = [ + 'running', + 'paused', + 'backlog', + 'waiting_approval', + 'verified', +] as const; + +export type StatusFilterValue = (typeof STATUS_FILTER_OPTIONS)[number]; + export interface GraphFilterResult { matchedNodeIds: Set; highlightedNodeIds: Set; @@ -29,7 +41,10 @@ function getAncestors( const feature = featureMap.get(featureId); if (!feature?.dependencies) return; - for (const depId of feature.dependencies) { + const deps = feature.dependencies as string[] | undefined; + if (!deps) return; + + for (const depId of deps) { if (featureMap.has(depId)) { getAncestors(depId, featureMap, visited); } @@ -44,7 +59,8 @@ function getDescendants(featureId: string, features: Feature[], visited: Set, features: Feature[ for (const feature of features) { if (!highlightedNodeIds.has(feature.id)) continue; - if (!feature.dependencies) continue; + const deps = feature.dependencies as string[] | undefined; + if (!deps) continue; - for (const depId of feature.dependencies) { + for (const depId of deps) { if (highlightedNodeIds.has(depId)) { edges.add(`${depId}->${feature.id}`); } @@ -71,13 +88,24 @@ function getHighlightedEdges(highlightedNodeIds: Set, features: Feature[ } /** - * Hook to calculate graph filter results based on search query, categories, and filter mode + * Gets the effective status of a feature (accounting for running state) + */ +function getEffectiveStatus(feature: Feature, runningAutoTasks: string[]): StatusFilterValue { + if (feature.status === 'in_progress') { + return runningAutoTasks.includes(feature.id) ? 'running' : 'paused'; + } + return feature.status as StatusFilterValue; +} + +/** + * Hook to calculate graph filter results based on search query, categories, statuses, and filter mode */ export function useGraphFilter( features: Feature[], - filterState: GraphFilterState + filterState: GraphFilterState, + runningAutoTasks: string[] = [] ): GraphFilterResult { - const { searchQuery, selectedCategories, isNegativeFilter } = filterState; + const { searchQuery, selectedCategories, selectedStatuses, isNegativeFilter } = filterState; return useMemo(() => { // Extract all unique categories @@ -88,7 +116,9 @@ export function useGraphFilter( const normalizedQuery = searchQuery.toLowerCase().trim(); const hasSearchQuery = normalizedQuery.length > 0; const hasCategoryFilter = selectedCategories.length > 0; - const hasActiveFilter = hasSearchQuery || hasCategoryFilter || isNegativeFilter; + const hasStatusFilter = selectedStatuses.length > 0; + const hasActiveFilter = + hasSearchQuery || hasCategoryFilter || hasStatusFilter || isNegativeFilter; // If no filters active, return empty sets (show all nodes normally) if (!hasActiveFilter) { @@ -108,6 +138,7 @@ export function useGraphFilter( for (const feature of features) { let matchesSearch = true; let matchesCategory = true; + let matchesStatus = true; // Check search query match (title or description) if (hasSearchQuery) { @@ -121,8 +152,14 @@ export function useGraphFilter( matchesCategory = selectedCategories.includes(feature.category); } - // Both conditions must be true for a match - if (matchesSearch && matchesCategory) { + // Check status match + if (hasStatusFilter) { + const effectiveStatus = getEffectiveStatus(feature, runningAutoTasks); + matchesStatus = selectedStatuses.includes(effectiveStatus); + } + + // All conditions must be true for a match + if (matchesSearch && matchesCategory && matchesStatus) { matchedNodeIds.add(feature.id); } } @@ -161,5 +198,12 @@ export function useGraphFilter( availableCategories, hasActiveFilter: true, }; - }, [features, searchQuery, selectedCategories, isNegativeFilter]); + }, [ + features, + searchQuery, + selectedCategories, + selectedStatuses, + isNegativeFilter, + runningAutoTasks, + ]); }