@@ -28,6 +28,7 @@ import { Check, Circle, FolderTree, List, Search, XCircle } from 'lucide-react';
2828import { useParams } from 'next/navigation' ;
2929import { useQueryState } from 'nuqs' ;
3030import { useEffect , useMemo , useState } from 'react' ;
31+ import type { FrameworkInstanceForTasks } from '../types' ;
3132import { ModernTaskList } from './ModernTaskList' ;
3233import { TasksByCategory } from './TasksByCategory' ;
3334
@@ -42,6 +43,7 @@ const statuses = [
4243export function TaskList ( {
4344 tasks : initialTasks ,
4445 members,
46+ frameworkInstances,
4547 activeTab,
4648} : {
4749 tasks : ( Task & {
@@ -61,20 +63,34 @@ export function TaskList({
6163 } > ;
6264 } ) [ ] ;
6365 members : ( Member & { user : User } ) [ ] ;
66+ frameworkInstances : FrameworkInstanceForTasks [ ] ;
6467 activeTab : 'categories' | 'list' ;
6568} ) {
6669 const params = useParams ( ) ;
6770 const orgId = params . orgId as string ;
6871 const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
6972 const [ statusFilter , setStatusFilter ] = useQueryState ( 'status' ) ;
7073 const [ assigneeFilter , setAssigneeFilter ] = useQueryState ( 'assignee' ) ;
74+ const [ frameworkFilter , setFrameworkFilter ] = useQueryState ( 'framework' ) ;
7175 const [ currentTab , setCurrentTab ] = useState < 'categories' | 'list' > ( activeTab ) ;
7276
7377 // Sync activeTab prop with state when it changes
7478 useEffect ( ( ) => {
7579 setCurrentTab ( activeTab ) ;
7680 } , [ activeTab ] ) ;
7781
82+ // Clear frameworkFilter when it's invalid or frameworks are empty.
83+ // Prevents invisible filter (no dropdown when empty) and stale bookmarked URLs.
84+ useEffect ( ( ) => {
85+ if ( ! frameworkFilter ) return ;
86+ const isValid =
87+ frameworkInstances . length > 0 &&
88+ frameworkInstances . some ( ( fw ) => fw . id === frameworkFilter ) ;
89+ if ( ! isValid ) {
90+ setFrameworkFilter ( null ) ;
91+ }
92+ } , [ frameworkFilter , frameworkInstances , setFrameworkFilter ] ) ;
93+
7894 const handleTabChange = async ( value : string ) => {
7995 const newTab = value as 'categories' | 'list' ;
8096 setCurrentTab ( newTab ) ;
@@ -100,7 +116,17 @@ export function TaskList({
100116 } ) ;
101117 } , [ members ] ) ;
102118
103- // Filter tasks by search query, status, and assignee
119+ // Build a map of control IDs to their framework instances for efficient lookup
120+ const frameworkControlIds = useMemo ( ( ) => {
121+ const map = new Map < string , Set < string > > ( ) ;
122+ for ( const fw of frameworkInstances ) {
123+ const controlIds = new Set ( fw . requirementsMapped . map ( ( r ) => r . controlId ) ) ;
124+ map . set ( fw . id , controlIds ) ;
125+ }
126+ return map ;
127+ } , [ frameworkInstances ] ) ;
128+
129+ // Filter tasks by search query, status, assignee, and framework
104130 const filteredTasks = initialTasks . filter ( ( task ) => {
105131 const matchesSearch =
106132 searchQuery === '' ||
@@ -110,7 +136,16 @@ export function TaskList({
110136 const matchesStatus = ! statusFilter || task . status === statusFilter ;
111137 const matchesAssignee = ! assigneeFilter || task . assigneeId === assigneeFilter ;
112138
113- return matchesSearch && matchesStatus && matchesAssignee ;
139+ const matchesFramework =
140+ ! frameworkFilter ||
141+ ( ( ) => {
142+ const fwControlIds = frameworkControlIds . get ( frameworkFilter ) ;
143+ // Stale/invalid framework ID (e.g. from bookmarked URL): treat as "All frameworks" to match dropdown display
144+ if ( ! fwControlIds ) return true ;
145+ return task . controls . some ( ( c ) => fwControlIds . has ( c . id ) ) ;
146+ } ) ( ) ;
147+
148+ return matchesSearch && matchesStatus && matchesAssignee && matchesFramework ;
114149 } ) ;
115150
116151 // Calculate overall stats from all tasks (not filtered)
@@ -571,6 +606,36 @@ export function TaskList({
571606 </ SelectContent >
572607 </ Select >
573608
609+ { frameworkInstances . length > 0 && (
610+ < Select
611+ value = { frameworkFilter || 'all' }
612+ onValueChange = { ( value ) => setFrameworkFilter ( value === 'all' ? null : value ) }
613+ >
614+ < SelectTrigger size = "sm" >
615+ < SelectValue placeholder = "All frameworks" >
616+ { ( ( ) => {
617+ if ( ! frameworkFilter ) return 'All frameworks' ;
618+ const selectedFramework = frameworkInstances . find (
619+ ( fw ) => fw . id === frameworkFilter ,
620+ ) ;
621+ if ( ! selectedFramework ) return 'All frameworks' ;
622+ return selectedFramework . framework . name ;
623+ } ) ( ) }
624+ </ SelectValue >
625+ </ SelectTrigger >
626+ < SelectContent >
627+ < SelectItem value = "all" >
628+ < span className = "text-xs" > All frameworks</ span >
629+ </ SelectItem >
630+ { frameworkInstances . map ( ( fw ) => (
631+ < SelectItem key = { fw . id } value = { fw . id } >
632+ < span className = "text-xs" > { fw . framework . name } </ span >
633+ </ SelectItem >
634+ ) ) }
635+ </ SelectContent >
636+ </ Select >
637+ ) }
638+
574639 < Select
575640 value = { assigneeFilter || 'all' }
576641 onValueChange = { ( value ) => setAssigneeFilter ( value === 'all' ? null : value ) }
@@ -629,7 +694,7 @@ export function TaskList({
629694 </ Select >
630695 </ div >
631696 { /* Result Count */ }
632- { ( searchQuery || statusFilter || assigneeFilter ) && (
697+ { ( searchQuery || statusFilter || assigneeFilter || frameworkFilter ) && (
633698 < div className = "text-muted-foreground text-xs tabular-nums whitespace-nowrap lg:ml-auto" >
634699 { filteredTasks . length } { filteredTasks . length === 1 ? 'result' : 'results' }
635700 </ div >
@@ -664,7 +729,6 @@ export function TaskList({
664729 </ div >
665730 </ Stack >
666731 </ Tabs >
667-
668732 </ Stack >
669733 ) ;
670734}
0 commit comments