fix(ui): scope package manager search to the actively viewed category
This commit is contained in:
parent
2e7bc89dbc
commit
e6515c21ed
2 changed files with 142 additions and 20 deletions
|
|
@ -248,6 +248,32 @@ impl Tab {
|
|||
}
|
||||
}
|
||||
|
||||
/// `query` is expected to be lowercased already.
|
||||
fn matches_query(p: &PackageEntry, query: &str) -> bool {
|
||||
query.is_empty()
|
||||
|| p.name.to_lowercase().contains(query)
|
||||
|| p.file_name.to_lowercase().contains(query)
|
||||
}
|
||||
|
||||
/// Rows to show for the current toolbar state. The search is scoped to the
|
||||
/// active tab: a package has to belong to the tab *and* match the query, so
|
||||
/// typing in a category never pulls in packages from another one. `Tab::All`
|
||||
/// admits everything, which keeps a search from there a search over the whole
|
||||
/// cluster.
|
||||
fn visible_packages(
|
||||
items: &[PackageEntry],
|
||||
tab: Option<&Tab>,
|
||||
query: &str,
|
||||
show: EnabledFilter,
|
||||
) -> Vec<PackageEntry> {
|
||||
items
|
||||
.iter()
|
||||
.filter(|p| tab.is_none_or(|t| t.matches(p)) && matches_query(p, query))
|
||||
.filter(|p| show.keep(p))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn bundle_categories(bundles: &[BundleWithUpdateStatus]) -> Vec<String> {
|
||||
let mut cats: Vec<String> = Vec::new();
|
||||
for bundle in bundles {
|
||||
|
|
@ -347,26 +373,28 @@ impl Component for PackageManager {
|
|||
let card_layout = CardLayout::from(*layout.read());
|
||||
|
||||
let tab_filter = tabs.get(active_idx);
|
||||
let content_kind = match tab_filter {
|
||||
Some(Tab::Browser) => ContentKind::Browser,
|
||||
Some(Tab::Local) => ContentKind::Local,
|
||||
_ => ContentKind::Other,
|
||||
};
|
||||
|
||||
let mut filtered: Vec<PackageEntry> = items
|
||||
.iter()
|
||||
.filter(|p| tab_filter.is_none_or(|t| t.matches(p)))
|
||||
.filter(|p| {
|
||||
query.is_empty()
|
||||
|| p.name.to_lowercase().contains(query.as_str())
|
||||
|| p.file_name.to_lowercase().contains(query.as_str())
|
||||
})
|
||||
.filter(|p| show.keep(p))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let mut filtered = visible_packages(&items, tab_filter, &query, show);
|
||||
sort_mode.sort(&mut filtered);
|
||||
|
||||
// Coming up empty during a search is about the query, not about the tab
|
||||
// having nothing in it, so it gets its own empty state naming the tab
|
||||
// that was searched.
|
||||
let content_kind = if filtered.is_empty() && !query.is_empty() {
|
||||
ContentKind::NoMatches {
|
||||
scope: match tab_filter {
|
||||
Some(Tab::All) | None => None,
|
||||
Some(tab) => Some(tab.label()),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
match tab_filter {
|
||||
Some(Tab::Browser) => ContentKind::Browser,
|
||||
Some(Tab::Local) => ContentKind::Local,
|
||||
_ => ContentKind::Other,
|
||||
}
|
||||
};
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
|
|
@ -395,3 +423,69 @@ impl Component for PackageManager {
|
|||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn entry(name: &str, provider: ProviderId, categories: &[&str]) -> PackageEntry {
|
||||
PackageEntry {
|
||||
package_id: name.to_string(),
|
||||
bundle_name: (!categories.is_empty()).then(|| "bundle".to_string()),
|
||||
provider,
|
||||
name: name.to_string(),
|
||||
file_name: format!("{}.jar", name.to_lowercase()),
|
||||
author: String::new(),
|
||||
description: String::new(),
|
||||
icon_url: None,
|
||||
size: 0,
|
||||
categories: categories.iter().map(|c| (*c).to_string()).collect(),
|
||||
enabled: true,
|
||||
manifest_default: true,
|
||||
installed: true,
|
||||
hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn items() -> Vec<PackageEntry> {
|
||||
vec![
|
||||
entry("Sodium", ProviderId::Modrinth, &["Performance"]),
|
||||
entry("Sodium Extra", ProviderId::Modrinth, &["Visuals"]),
|
||||
entry("Sodium Local", ProviderId::Local, &[]),
|
||||
]
|
||||
}
|
||||
|
||||
fn names(rows: &[PackageEntry]) -> Vec<&str> {
|
||||
rows.iter().map(|p| p.name.as_str()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_stays_inside_the_active_category() {
|
||||
let items = items();
|
||||
let tab = Tab::Category("Performance".to_string());
|
||||
let rows = visible_packages(&items, Some(&tab), "sodium", EnabledFilter::All);
|
||||
assert_eq!(names(&rows), ["Sodium"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_stays_inside_the_local_tab() {
|
||||
let items = items();
|
||||
let rows = visible_packages(&items, Some(&Tab::Local), "sodium", EnabledFilter::All);
|
||||
assert_eq!(names(&rows), ["Sodium Local"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tab_searches_every_package() {
|
||||
let items = items();
|
||||
let rows = visible_packages(&items, Some(&Tab::All), "sodium", EnabledFilter::All);
|
||||
assert_eq!(names(&rows), ["Sodium", "Sodium Extra", "Sodium Local"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_query_keeps_the_whole_tab() {
|
||||
let items = items();
|
||||
let tab = Tab::Category("Visuals".to_string());
|
||||
let rows = visible_packages(&items, Some(&tab), "", EnabledFilter::All);
|
||||
assert_eq!(names(&rows), ["Sodium Extra"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -490,11 +490,16 @@ fn browse_button(cluster_id: i64, package_type: &'static str) -> impl IntoElemen
|
|||
.text("Browse Content")
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(super) enum ContentKind {
|
||||
Browser,
|
||||
Local,
|
||||
Other,
|
||||
/// A search returned nothing. `scope` names the tab that was searched when
|
||||
/// it is narrower than "All".
|
||||
NoMatches {
|
||||
scope: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
@ -538,7 +543,7 @@ impl Component for ContentBox {
|
|||
let content_type = self.content_type;
|
||||
let cluster_id = self.cluster_id;
|
||||
let noun_plural = self.noun_plural;
|
||||
let kind = self.kind;
|
||||
let kind = &self.kind;
|
||||
let layout = self.layout;
|
||||
|
||||
let dispatch = use_dispatch();
|
||||
|
|
@ -575,6 +580,9 @@ impl Component for ContentBox {
|
|||
local_empty(cluster_id, content_type, dispatch.clone(), noun_plural).into_element()
|
||||
}
|
||||
ContentKind::Other => empty_state(noun_plural).into_element(),
|
||||
ContentKind::NoMatches { scope } => {
|
||||
search_empty(noun_plural, scope.as_deref()).into_element()
|
||||
}
|
||||
});
|
||||
|
||||
let header = (count > 0)
|
||||
|
|
@ -590,7 +598,7 @@ impl Component for ContentBox {
|
|||
))
|
||||
.into_element(),
|
||||
),
|
||||
ContentKind::Other => None,
|
||||
ContentKind::Other | ContentKind::NoMatches { .. } => None,
|
||||
})
|
||||
.flatten();
|
||||
|
||||
|
|
@ -713,6 +721,26 @@ fn empty_state(noun_plural: &'static str) -> impl IntoElement {
|
|||
.into_element()
|
||||
}
|
||||
|
||||
/// The search only ever looks inside the active tab, so say which one came up
|
||||
/// empty and point at the tab that does cover everything.
|
||||
fn search_empty(noun_plural: &'static str, scope: Option<&str>) -> impl IntoElement {
|
||||
let (title, hint) = match scope {
|
||||
Some(tab) => (
|
||||
format!("No {noun_plural} in \"{tab}\" match your search."),
|
||||
"Try a different term, or search from the All tab.",
|
||||
),
|
||||
None => (
|
||||
format!("No {noun_plural} match your search."),
|
||||
"Try a different term.",
|
||||
),
|
||||
};
|
||||
|
||||
empty_shell(IconType::SearchMd)
|
||||
.child(empty_title(title))
|
||||
.child(empty_hint(hint))
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn browser_empty(cluster_id: i64, package_type: &'static str) -> impl IntoElement {
|
||||
empty_shell(IconType::SearchMd)
|
||||
.child(empty_title("No content installed from the browser."))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue