mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
add more filters about process status
This commit is contained in:
@@ -67,7 +67,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.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",
|
"dotenv": "^17.2.3",
|
||||||
"geist": "^1.5.1",
|
"geist": "^1.5.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
|||||||
@@ -4,9 +4,40 @@ import { Checkbox } from '@/components/ui/checkbox';
|
|||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
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 { 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 {
|
interface GraphFilterControlsProps {
|
||||||
filterState: GraphFilterState;
|
filterState: GraphFilterState;
|
||||||
@@ -14,6 +45,7 @@ interface GraphFilterControlsProps {
|
|||||||
hasActiveFilter: boolean;
|
hasActiveFilter: boolean;
|
||||||
onSearchQueryChange: (query: string) => void;
|
onSearchQueryChange: (query: string) => void;
|
||||||
onCategoriesChange: (categories: string[]) => void;
|
onCategoriesChange: (categories: string[]) => void;
|
||||||
|
onStatusesChange: (statuses: string[]) => void;
|
||||||
onNegativeFilterChange: (isNegative: boolean) => void;
|
onNegativeFilterChange: (isNegative: boolean) => void;
|
||||||
onClearFilters: () => void;
|
onClearFilters: () => void;
|
||||||
}
|
}
|
||||||
@@ -24,10 +56,14 @@ export function GraphFilterControls({
|
|||||||
hasActiveFilter,
|
hasActiveFilter,
|
||||||
onSearchQueryChange,
|
onSearchQueryChange,
|
||||||
onCategoriesChange,
|
onCategoriesChange,
|
||||||
|
onStatusesChange,
|
||||||
onNegativeFilterChange,
|
onNegativeFilterChange,
|
||||||
onClearFilters,
|
onClearFilters,
|
||||||
}: GraphFilterControlsProps) {
|
}: 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) => {
|
const handleCategoryToggle = (category: string) => {
|
||||||
if (selectedCategories.includes(category)) {
|
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 =
|
const categoryButtonLabel =
|
||||||
selectedCategories.length === 0
|
selectedCategories.length === 0
|
||||||
? 'All Categories'
|
? 'All Categories'
|
||||||
@@ -52,6 +104,14 @@ export function GraphFilterControls({
|
|||||||
? selectedCategories[0]
|
? selectedCategories[0]
|
||||||
: `${selectedCategories.length} Categories`;
|
: `${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 (
|
return (
|
||||||
<Panel position="top-left" className="flex items-center gap-2">
|
<Panel position="top-left" className="flex items-center gap-2">
|
||||||
<TooltipProvider delayDuration={200}>
|
<TooltipProvider delayDuration={200}>
|
||||||
@@ -130,6 +190,74 @@ export function GraphFilterControls({
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
{/* Status Filter Dropdown */}
|
||||||
|
<Popover>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={cn(
|
||||||
|
'h-8 px-2 gap-1.5',
|
||||||
|
selectedStatuses.length > 0 && 'bg-brand-500/20 text-brand-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CircleDot className="w-4 h-4" />
|
||||||
|
<span className="text-xs max-w-[120px] truncate">{statusButtonLabel}</span>
|
||||||
|
<ChevronDown className="w-3 h-3 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Filter by Status</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent align="start" className="w-56 p-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground px-2 py-1">Status</div>
|
||||||
|
|
||||||
|
{/* Select All option */}
|
||||||
|
<div
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
||||||
|
onClick={handleSelectAllStatuses}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedStatuses.length === STATUS_FILTER_OPTIONS.length}
|
||||||
|
onCheckedChange={handleSelectAllStatuses}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{selectedStatuses.length === STATUS_FILTER_OPTIONS.length
|
||||||
|
? 'Deselect All'
|
||||||
|
: 'Select All'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-px bg-border" />
|
||||||
|
|
||||||
|
{/* Status list */}
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{STATUS_FILTER_OPTIONS.map((status) => {
|
||||||
|
const config = statusDisplayConfig[status];
|
||||||
|
const StatusIcon = config.icon;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={status}
|
||||||
|
className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-accent cursor-pointer"
|
||||||
|
onClick={() => handleStatusToggle(status)}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedStatuses.includes(status)}
|
||||||
|
onCheckedChange={() => handleStatusToggle(status)}
|
||||||
|
/>
|
||||||
|
<StatusIcon className={cn('w-3.5 h-3.5', config.colorClass)} />
|
||||||
|
<span className="text-sm">{config.label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
<div className="h-6 w-px bg-border" />
|
<div className="h-6 w-px bg-border" />
|
||||||
|
|
||||||
|
|||||||
@@ -66,19 +66,21 @@ function GraphCanvasInner({
|
|||||||
const [isLocked, setIsLocked] = useState(false);
|
const [isLocked, setIsLocked] = useState(false);
|
||||||
const [layoutDirection, setLayoutDirection] = useState<'LR' | 'TB'>('LR');
|
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<string[]>([]);
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||||
|
const [selectedStatuses, setSelectedStatuses] = useState<string[]>([]);
|
||||||
const [isNegativeFilter, setIsNegativeFilter] = useState(false);
|
const [isNegativeFilter, setIsNegativeFilter] = useState(false);
|
||||||
|
|
||||||
// Combined filter state
|
// Combined filter state
|
||||||
const filterState: GraphFilterState = {
|
const filterState: GraphFilterState = {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
selectedCategories,
|
selectedCategories,
|
||||||
|
selectedStatuses,
|
||||||
isNegativeFilter,
|
isNegativeFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate filter results
|
// Calculate filter results
|
||||||
const filterResult = useGraphFilter(features, filterState);
|
const filterResult = useGraphFilter(features, filterState, runningAutoTasks);
|
||||||
|
|
||||||
// Transform features to nodes and edges with filter results
|
// Transform features to nodes and edges with filter results
|
||||||
const { nodes: initialNodes, edges: initialEdges } = useGraphNodes({
|
const { nodes: initialNodes, edges: initialEdges } = useGraphNodes({
|
||||||
@@ -117,6 +119,7 @@ function GraphCanvasInner({
|
|||||||
const handleClearFilters = useCallback(() => {
|
const handleClearFilters = useCallback(() => {
|
||||||
onSearchQueryChange('');
|
onSearchQueryChange('');
|
||||||
setSelectedCategories([]);
|
setSelectedCategories([]);
|
||||||
|
setSelectedStatuses([]);
|
||||||
setIsNegativeFilter(false);
|
setIsNegativeFilter(false);
|
||||||
}, [onSearchQueryChange]);
|
}, [onSearchQueryChange]);
|
||||||
|
|
||||||
@@ -195,6 +198,7 @@ function GraphCanvasInner({
|
|||||||
hasActiveFilter={filterResult.hasActiveFilter}
|
hasActiveFilter={filterResult.hasActiveFilter}
|
||||||
onSearchQueryChange={onSearchQueryChange}
|
onSearchQueryChange={onSearchQueryChange}
|
||||||
onCategoriesChange={setSelectedCategories}
|
onCategoriesChange={setSelectedCategories}
|
||||||
|
onStatusesChange={setSelectedStatuses}
|
||||||
onNegativeFilterChange={setIsNegativeFilter}
|
onNegativeFilterChange={setIsNegativeFilter}
|
||||||
onClearFilters={handleClearFilters}
|
onClearFilters={handleClearFilters}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,9 +4,21 @@ import { Feature } from '@/store/app-store';
|
|||||||
export interface GraphFilterState {
|
export interface GraphFilterState {
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
selectedCategories: string[];
|
selectedCategories: string[];
|
||||||
|
selectedStatuses: string[];
|
||||||
isNegativeFilter: boolean;
|
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 {
|
export interface GraphFilterResult {
|
||||||
matchedNodeIds: Set<string>;
|
matchedNodeIds: Set<string>;
|
||||||
highlightedNodeIds: Set<string>;
|
highlightedNodeIds: Set<string>;
|
||||||
@@ -29,7 +41,10 @@ function getAncestors(
|
|||||||
const feature = featureMap.get(featureId);
|
const feature = featureMap.get(featureId);
|
||||||
if (!feature?.dependencies) return;
|
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)) {
|
if (featureMap.has(depId)) {
|
||||||
getAncestors(depId, featureMap, visited);
|
getAncestors(depId, featureMap, visited);
|
||||||
}
|
}
|
||||||
@@ -44,7 +59,8 @@ function getDescendants(featureId: string, features: Feature[], visited: Set<str
|
|||||||
visited.add(featureId);
|
visited.add(featureId);
|
||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
if (feature.dependencies?.includes(featureId)) {
|
const deps = feature.dependencies as string[] | undefined;
|
||||||
|
if (deps?.includes(featureId)) {
|
||||||
getDescendants(feature.id, features, visited);
|
getDescendants(feature.id, features, visited);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,9 +74,10 @@ function getHighlightedEdges(highlightedNodeIds: Set<string>, features: Feature[
|
|||||||
|
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
if (!highlightedNodeIds.has(feature.id)) continue;
|
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)) {
|
if (highlightedNodeIds.has(depId)) {
|
||||||
edges.add(`${depId}->${feature.id}`);
|
edges.add(`${depId}->${feature.id}`);
|
||||||
}
|
}
|
||||||
@@ -71,13 +88,24 @@ function getHighlightedEdges(highlightedNodeIds: Set<string>, 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(
|
export function useGraphFilter(
|
||||||
features: Feature[],
|
features: Feature[],
|
||||||
filterState: GraphFilterState
|
filterState: GraphFilterState,
|
||||||
|
runningAutoTasks: string[] = []
|
||||||
): GraphFilterResult {
|
): GraphFilterResult {
|
||||||
const { searchQuery, selectedCategories, isNegativeFilter } = filterState;
|
const { searchQuery, selectedCategories, selectedStatuses, isNegativeFilter } = filterState;
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// Extract all unique categories
|
// Extract all unique categories
|
||||||
@@ -88,7 +116,9 @@ export function useGraphFilter(
|
|||||||
const normalizedQuery = searchQuery.toLowerCase().trim();
|
const normalizedQuery = searchQuery.toLowerCase().trim();
|
||||||
const hasSearchQuery = normalizedQuery.length > 0;
|
const hasSearchQuery = normalizedQuery.length > 0;
|
||||||
const hasCategoryFilter = selectedCategories.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 no filters active, return empty sets (show all nodes normally)
|
||||||
if (!hasActiveFilter) {
|
if (!hasActiveFilter) {
|
||||||
@@ -108,6 +138,7 @@ export function useGraphFilter(
|
|||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
let matchesSearch = true;
|
let matchesSearch = true;
|
||||||
let matchesCategory = true;
|
let matchesCategory = true;
|
||||||
|
let matchesStatus = true;
|
||||||
|
|
||||||
// Check search query match (title or description)
|
// Check search query match (title or description)
|
||||||
if (hasSearchQuery) {
|
if (hasSearchQuery) {
|
||||||
@@ -121,8 +152,14 @@ export function useGraphFilter(
|
|||||||
matchesCategory = selectedCategories.includes(feature.category);
|
matchesCategory = selectedCategories.includes(feature.category);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both conditions must be true for a match
|
// Check status match
|
||||||
if (matchesSearch && matchesCategory) {
|
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);
|
matchedNodeIds.add(feature.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -161,5 +198,12 @@ export function useGraphFilter(
|
|||||||
availableCategories,
|
availableCategories,
|
||||||
hasActiveFilter: true,
|
hasActiveFilter: true,
|
||||||
};
|
};
|
||||||
}, [features, searchQuery, selectedCategories, isNegativeFilter]);
|
}, [
|
||||||
|
features,
|
||||||
|
searchQuery,
|
||||||
|
selectedCategories,
|
||||||
|
selectedStatuses,
|
||||||
|
isNegativeFilter,
|
||||||
|
runningAutoTasks,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user