Skip to content

Commit c4ee153

Browse files
committed
feat: add card identifier search (KAN-197)
1 parent c482309 commit c4ee153

File tree

1 file changed

+82
-3
lines changed
  • crates/kanban-domain/src/search

1 file changed

+82
-3
lines changed

crates/kanban-domain/src/search/mod.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
109140
pub enum SearchBy {
110141
Title(TitleSearcher),
111142
BranchName(BranchNameSearcher),
143+
CardIdentifier(CardIdentifierSearcher),
112144
}
113145

114146
impl 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

Comments
 (0)