Skip to content

Commit 5daffa5

Browse files
authored
KAN-197/add-card-identifier-search-prefix-number (#164)
* feat: add card identifier search (KAN-197) * chore: cargo fmt * chore: add changeset
1 parent c482309 commit 5daffa5

File tree

2 files changed

+97
-3
lines changed

2 files changed

+97
-3
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
bump: patch
3+
---
4+
5+
- feat: add card identifier search (KAN-197)

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

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
109144
pub enum SearchBy {
110145
Title(TitleSearcher),
111146
BranchName(BranchNameSearcher),
147+
CardIdentifier(CardIdentifierSearcher),
112148
}
113149

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

Comments
 (0)