@@ -105,17 +105,54 @@ 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+ pub fn query ( & self ) -> & str {
121+ & self . query
122+ }
123+
124+ fn get_identifier ( & self , card : & Card , board : & Board ) -> String {
125+ let prefix = card
126+ . assigned_prefix
127+ . as_deref ( )
128+ . or ( board. card_prefix . as_deref ( ) )
129+ . unwrap_or ( "task" ) ;
130+ format ! ( "{}-{}" , prefix, card. card_number) . to_lowercase ( )
131+ }
132+ }
133+
134+ impl CardSearcher for CardIdentifierSearcher {
135+ fn matches ( & self , card : & Card , board : & Board , _sprints : & [ Sprint ] ) -> bool {
136+ if self . query . is_empty ( ) {
137+ return true ;
138+ }
139+ self . get_identifier ( card, board) . contains ( & self . query )
140+ }
141+ }
142+
108143/// Enum dispatch for searching cards by a specific field.
109144pub enum SearchBy {
110145 Title ( TitleSearcher ) ,
111146 BranchName ( BranchNameSearcher ) ,
147+ CardIdentifier ( CardIdentifierSearcher ) ,
112148}
113149
114150impl SearchBy {
115151 fn matches ( & self , card : & Card , board : & Board , sprints : & [ Sprint ] ) -> bool {
116152 match self {
117153 Self :: Title ( s) => s. matches ( card, board, sprints) ,
118154 Self :: BranchName ( s) => s. matches ( card, board, sprints) ,
155+ Self :: CardIdentifier ( s) => s. matches ( card, board, sprints) ,
119156 }
120157 }
121158}
@@ -136,14 +173,13 @@ impl CompositeSearcher {
136173 }
137174
138175 /// Create a composite searcher with all built-in searchers.
139- ///
140- /// Includes both `TitleSearcher` and `BranchNameSearcher`.
141176 pub fn all ( query : impl Into < String > ) -> Self {
142177 let query = query. into ( ) ;
143178 Self {
144179 searchers : vec ! [
145180 SearchBy :: Title ( TitleSearcher :: new( query. clone( ) ) ) ,
146- SearchBy :: BranchName ( BranchNameSearcher :: new( query) ) ,
181+ SearchBy :: BranchName ( BranchNameSearcher :: new( query. clone( ) ) ) ,
182+ SearchBy :: CardIdentifier ( CardIdentifierSearcher :: new( query) ) ,
147183 ] ,
148184 }
149185 }
@@ -236,4 +272,57 @@ mod tests {
236272 let searcher = CompositeSearcher :: new ( ) ;
237273 assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
238274 }
275+
276+ #[ test]
277+ fn test_card_identifier_searcher_with_prefix ( ) {
278+ let mut board = Board :: new ( "Test" . to_string ( ) , None ) ;
279+ board. card_prefix = Some ( "KAN" . to_string ( ) ) ;
280+ let column = crate :: Column :: new ( board. id , "Todo" . to_string ( ) , 0 ) ;
281+ let card = Card :: new ( & mut board, column. id , "Some task" . to_string ( ) , 0 , "KAN" ) ;
282+
283+ let searcher = CardIdentifierSearcher :: new ( "KAN-1" ) ;
284+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
285+
286+ let searcher = CardIdentifierSearcher :: new ( "kan-1" ) ;
287+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
288+
289+ let searcher = CardIdentifierSearcher :: new ( "1" ) ;
290+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
291+
292+ let searcher = CardIdentifierSearcher :: new ( "MVP-1" ) ;
293+ assert ! ( !searcher. matches( & card, & board, & [ ] ) ) ;
294+ }
295+
296+ #[ test]
297+ fn test_card_identifier_searcher_number_only ( ) {
298+ let mut board = Board :: new ( "Test" . to_string ( ) , None ) ;
299+ let column = crate :: Column :: new ( board. id , "Todo" . to_string ( ) , 0 ) ;
300+ let _card1 = Card :: new ( & mut board, column. id , "First" . to_string ( ) , 0 , "task" ) ;
301+ let card2 = Card :: new ( & mut board, column. id , "Second" . to_string ( ) , 1 , "task" ) ;
302+
303+ // card2 has card_number=2
304+ let searcher = CardIdentifierSearcher :: new ( "2" ) ;
305+ assert ! ( searcher. matches( & card2, & board, & [ ] ) ) ;
306+
307+ let searcher = CardIdentifierSearcher :: new ( "task-2" ) ;
308+ assert ! ( searcher. matches( & card2, & board, & [ ] ) ) ;
309+ }
310+
311+ #[ test]
312+ fn test_composite_searcher_matches_by_identifier ( ) {
313+ let mut board = Board :: new ( "Test" . to_string ( ) , None ) ;
314+ board. card_prefix = Some ( "KAN" . to_string ( ) ) ;
315+ let column = crate :: Column :: new ( board. id , "Todo" . to_string ( ) , 0 ) ;
316+ let card = Card :: new (
317+ & mut board,
318+ column. id ,
319+ "Unrelated title" . to_string ( ) ,
320+ 0 ,
321+ "KAN" ,
322+ ) ;
323+
324+ // Title doesn't contain "KAN-1", but identifier does
325+ let searcher = CompositeSearcher :: all ( "KAN-1" ) ;
326+ assert ! ( searcher. matches( & card, & board, & [ ] ) ) ;
327+ }
239328}
0 commit comments