@@ -105,17 +105,50 @@ impl CardSearcher for BranchNameSearcher {
105105 }
106106}
107107
108+ /// Search cards by card identifier (e.g. "KAN-164", "164").
109+ pub struct CardIdentifierSearcher {
110+ query : String ,
111+ }
112+
113+ impl CardIdentifierSearcher {
114+ pub fn new ( query : impl Into < String > ) -> Self {
115+ Self {
116+ query : query. into ( ) . to_lowercase ( ) ,
117+ }
118+ }
119+
120+ fn get_identifier ( & self , card : & Card , board : & Board ) -> String {
121+ let prefix = card
122+ . assigned_prefix
123+ . as_deref ( )
124+ . or ( board. card_prefix . as_deref ( ) )
125+ . unwrap_or ( "task" ) ;
126+ format ! ( "{}-{}" , prefix, card. card_number) . to_lowercase ( )
127+ }
128+ }
129+
130+ impl CardSearcher for CardIdentifierSearcher {
131+ fn matches ( & self , card : & Card , board : & Board , _sprints : & [ Sprint ] ) -> bool {
132+ if self . query . is_empty ( ) {
133+ return true ;
134+ }
135+ self . get_identifier ( card, board) . contains ( & self . query )
136+ }
137+ }
138+
108139/// Enum dispatch for searching cards by a specific field.
109140pub enum SearchBy {
110141 Title ( TitleSearcher ) ,
111142 BranchName ( BranchNameSearcher ) ,
143+ CardIdentifier ( CardIdentifierSearcher ) ,
112144}
113145
114146impl SearchBy {
115147 fn matches ( & self , card : & Card , board : & Board , sprints : & [ Sprint ] ) -> bool {
116148 match self {
117149 Self :: Title ( s) => s. matches ( card, board, sprints) ,
118150 Self :: BranchName ( s) => s. matches ( card, board, sprints) ,
151+ Self :: CardIdentifier ( s) => s. matches ( card, board, sprints) ,
119152 }
120153 }
121154}
@@ -136,14 +169,13 @@ impl CompositeSearcher {
136169 }
137170
138171 /// Create a composite searcher with all built-in searchers.
139- ///
140- /// Includes both `TitleSearcher` and `BranchNameSearcher`.
141172 pub fn all ( query : impl Into < String > ) -> Self {
142173 let query = query. into ( ) ;
143174 Self {
144175 searchers : vec ! [
145176 SearchBy :: Title ( TitleSearcher :: new( query. clone( ) ) ) ,
146- SearchBy :: BranchName ( BranchNameSearcher :: new( query) ) ,
177+ SearchBy :: BranchName ( BranchNameSearcher :: new( query. clone( ) ) ) ,
178+ SearchBy :: CardIdentifier ( CardIdentifierSearcher :: new( query) ) ,
147179 ] ,
148180 }
149181 }
@@ -236,4 +268,51 @@ mod tests {
236268 let searcher = CompositeSearcher :: new ( ) ;
237269 assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
238270 }
271+
272+ #[ test]
273+ fn test_card_identifier_searcher_with_prefix ( ) {
274+ let mut board = Board :: new ( "Test" . to_string ( ) , None ) ;
275+ board. card_prefix = Some ( "KAN" . to_string ( ) ) ;
276+ let column = crate :: Column :: new ( board. id , "Todo" . to_string ( ) , 0 ) ;
277+ let card = Card :: new ( & mut board, column. id , "Some task" . to_string ( ) , 0 , "KAN" ) ;
278+
279+ let searcher = CardIdentifierSearcher :: new ( "KAN-1" ) ;
280+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
281+
282+ let searcher = CardIdentifierSearcher :: new ( "kan-1" ) ;
283+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
284+
285+ let searcher = CardIdentifierSearcher :: new ( "1" ) ;
286+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
287+
288+ let searcher = CardIdentifierSearcher :: new ( "MVP-1" ) ;
289+ assert ! ( !searcher. matches( & card, & board, & [ ] ) ) ;
290+ }
291+
292+ #[ test]
293+ fn test_card_identifier_searcher_number_only ( ) {
294+ let mut board = Board :: new ( "Test" . to_string ( ) , None ) ;
295+ let column = crate :: Column :: new ( board. id , "Todo" . to_string ( ) , 0 ) ;
296+ let _card1 = Card :: new ( & mut board, column. id , "First" . to_string ( ) , 0 , "task" ) ;
297+ let card2 = Card :: new ( & mut board, column. id , "Second" . to_string ( ) , 1 , "task" ) ;
298+
299+ // card2 has card_number=2
300+ let searcher = CardIdentifierSearcher :: new ( "2" ) ;
301+ assert ! ( searcher. matches( & card2, & board, & [ ] ) ) ;
302+
303+ let searcher = CardIdentifierSearcher :: new ( "task-2" ) ;
304+ assert ! ( searcher. matches( & card2, & board, & [ ] ) ) ;
305+ }
306+
307+ #[ test]
308+ fn test_composite_searcher_matches_by_identifier ( ) {
309+ let mut board = Board :: new ( "Test" . to_string ( ) , None ) ;
310+ board. card_prefix = Some ( "KAN" . to_string ( ) ) ;
311+ let column = crate :: Column :: new ( board. id , "Todo" . to_string ( ) , 0 ) ;
312+ let card = Card :: new ( & mut board, column. id , "Unrelated title" . to_string ( ) , 0 , "KAN" ) ;
313+
314+ // Title doesn't contain "KAN-1", but identifier does
315+ let searcher = CompositeSearcher :: all ( "KAN-1" ) ;
316+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
317+ }
239318}
0 commit comments