Shared card filter panel: News set browser gets every Library filter
Extract the filter fields and LibraryFilter-building state into src/cardFilter.tsx (CardFilterPanel + useCardFilter) and use it in the Library, Card Search and News set browser, replacing three diverging copies. search_reference now honors in_deck/location too, so every filter works in every tab. Closes #13
This commit is contained in:
@@ -11,8 +11,29 @@ use crate::Catalog;
|
||||
|
||||
/// Append the card-data predicates shared by [`Catalog::search_library`] and
|
||||
/// [`Catalog::search_reference`] — everything except `in_deck`/`location`/
|
||||
/// `spares_only`, which read the library tables. Caller has already
|
||||
/// written the `FROM ... WHERE <ownership predicate>` prefix.
|
||||
/// `spares_only`, which read the library tables (see
|
||||
/// [`push_collection_filters`]). Caller has already written the
|
||||
/// `FROM ... WHERE <ownership predicate>` prefix.
|
||||
/// Append the predicates that read the library tables: `in_deck`, `location`
|
||||
/// and `spares_only`. Expects `library_cards` joined as `lc` (a LEFT JOIN is
|
||||
/// fine — an unowned card has no location, so a location filter drops it).
|
||||
fn push_collection_filters(sql: &mut String, vals: &mut Vec<Value>, filter: &LibraryFilter) {
|
||||
if let Some(deck_id) = filter.in_deck {
|
||||
sql.push_str(
|
||||
" AND EXISTS (SELECT 1 FROM deck_cards dc
|
||||
WHERE dc.oracle_id = oc.oracle_id AND dc.deck_id = ?)",
|
||||
);
|
||||
vals.push(Value::Integer(deck_id));
|
||||
}
|
||||
if let Some(loc) = filter.location.as_ref().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
||||
sql.push_str(" AND LOWER(lc.location) LIKE ?");
|
||||
vals.push(Value::Text(format!("%{}%", loc.to_lowercase())));
|
||||
}
|
||||
if filter.spares_only {
|
||||
sql.push_str(SPARES_PREDICATE);
|
||||
}
|
||||
}
|
||||
|
||||
fn push_card_filters(
|
||||
sql: &mut String,
|
||||
vals: &mut Vec<Value>,
|
||||
@@ -223,21 +244,7 @@ impl Catalog {
|
||||
);
|
||||
let mut vals: Vec<Value> = Vec::new();
|
||||
push_card_filters(&mut sql, &mut vals, filter, self.has_universes_beyond, self.has_vanilla);
|
||||
|
||||
if let Some(deck_id) = filter.in_deck {
|
||||
sql.push_str(
|
||||
" AND EXISTS (SELECT 1 FROM deck_cards dc
|
||||
WHERE dc.oracle_id = oc.oracle_id AND dc.deck_id = ?)",
|
||||
);
|
||||
vals.push(Value::Integer(deck_id));
|
||||
}
|
||||
if let Some(loc) = filter.location.as_ref().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
||||
sql.push_str(" AND LOWER(lc.location) LIKE ?");
|
||||
vals.push(Value::Text(format!("%{}%", loc.to_lowercase())));
|
||||
}
|
||||
if filter.spares_only {
|
||||
sql.push_str(SPARES_PREDICATE);
|
||||
}
|
||||
push_collection_filters(&mut sql, &mut vals, filter);
|
||||
|
||||
sql.push_str(" ORDER BY oc.name COLLATE NOCASE");
|
||||
|
||||
@@ -253,8 +260,7 @@ impl Catalog {
|
||||
}
|
||||
|
||||
/// Search the *whole* reference db (every downloaded card, owned or not)
|
||||
/// by the same card-data criteria as [`Catalog::search_library`], minus
|
||||
/// `in_deck`/`location` (library-specific — silently ignored if set).
|
||||
/// by the same criteria as [`Catalog::search_library`], plus `not_owned`.
|
||||
/// Results are still [`LibraryCard`]s so ownership/deck context is
|
||||
/// visible (`quantity_owned` is 0 and `decks` is empty for unowned
|
||||
/// cards), capped at `limit` and ordered by Scryfall's EDHREC popularity
|
||||
@@ -290,9 +296,7 @@ impl Catalog {
|
||||
if filter.not_owned {
|
||||
sql.push_str(" AND COALESCE(lc.quantity_owned, 0) = 0");
|
||||
}
|
||||
if filter.spares_only {
|
||||
sql.push_str(SPARES_PREDICATE);
|
||||
}
|
||||
push_collection_filters(&mut sql, &mut vals, filter);
|
||||
|
||||
sql.push_str(" ORDER BY ");
|
||||
// Same reasoning as `NameIndex::search`: a card whose name *is* the query
|
||||
@@ -841,6 +845,28 @@ mod tests {
|
||||
assert_eq!(bolt.quantity_owned, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_reference_honors_in_deck_and_location() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
let deck = cat.create_deck("Burn", None).unwrap();
|
||||
cat.set_deck_card(deck.id, "lightning-bolt", 4, false, false).unwrap();
|
||||
cat.add_owned("sol-ring", 1).unwrap();
|
||||
cat.set_location("sol-ring", "Box 1").unwrap();
|
||||
|
||||
let in_deck = cat.search_reference(&LibraryFilter {
|
||||
in_deck: Some(deck.id),
|
||||
..Default::default()
|
||||
}, 50).unwrap();
|
||||
assert_eq!(names(&in_deck), vec!["Lightning Bolt"], "unowned deck cards count too");
|
||||
|
||||
let boxed = cat.search_reference(&LibraryFilter {
|
||||
location: Some("box".into()),
|
||||
..Default::default()
|
||||
}, 50).unwrap();
|
||||
assert_eq!(names(&boxed), vec!["Sol Ring"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_reference_not_owned_excludes_owned_cards() {
|
||||
let fx = test_support::make();
|
||||
@@ -970,23 +996,6 @@ mod tests {
|
||||
assert_eq!(capped.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_reference_ignores_in_deck_and_location() {
|
||||
let fx = test_support::make();
|
||||
let cat = &fx.catalog;
|
||||
let deck = cat.create_deck("Some Deck", None).unwrap();
|
||||
cat.set_deck_card(deck.id, "lightning-bolt", 1, false, false).unwrap();
|
||||
|
||||
// Both library-only fields are set but should have no effect — the
|
||||
// whole (unowned) fixture is still returned.
|
||||
let got = cat.search_reference(&LibraryFilter {
|
||||
in_deck: Some(deck.id),
|
||||
location: Some("Box 1".into()),
|
||||
..Default::default()
|
||||
}, 50).unwrap();
|
||||
assert_eq!(got.len(), 5);
|
||||
}
|
||||
|
||||
/// `horizontal` is read by nearly every card query (see
|
||||
/// `Catalog::horizontal_col`), so a reference.db built before that column
|
||||
/// shipped must not break `search_library`/`search_reference`/
|
||||
|
||||
Reference in New Issue
Block a user