Merge pull request #263 from alexanderalber/fix/201-filter-header-row

AutoFilter: a selected header row filters the data below it; search box in the filter menu (#201)
This commit is contained in:
Brandon Thomas 2026-10-11 11:42:38 -04:00
commit f6628954fc
3 changed files with 53 additions and 7 deletions

View File

@ -431,7 +431,16 @@ fn toggle_filter(s: &mut Session, p: &Json) -> Result<Json> {
Ok(json!({"filter": false}))
});
}
let r = data_range(s, p)?;
let mut r = data_range(s, p)?;
// A selected header row filters the data below it (#201).
if p.get("range").is_none() && r.height() == 1 {
let d = s.doc()?;
let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
let below = current_region(sh, r.start).end.row;
if below > r.end.row {
r = RangeRef::new(r.start, CellRef::new(below, r.end.col));
}
}
edit(s, |cx| {
cx.sheet_mut(sheet)?.autofilter = Some(AutoFilter { range: r, criteria: vec![] });
Ok(json!({"filter": true, "range": r.a1()}))

View File

@ -512,6 +512,26 @@ fn sort_and_filter() {
assert_eq!(r["hiddenRows"], 1);
}
/// Turning the filter on with only the header row selected filters the data below it (#201).
#[test]
fn filter_on_a_selected_header_row_covers_the_data_below() {
let mut s = s();
s.execute(
"range.setValues",
json!({"range": "A1", "values": [["Name", "City"], ["Ana", "SP"], ["Bia", "RJ"], ["Caio", "SP"], ["Duda", "BH"], ["Eva", "RJ"]]}),
)
.unwrap();
s.execute("selection.set", json!({"range": "A1:B1"})).unwrap();
let r = s.execute("data.filter", json!({})).unwrap();
assert_eq!(r["range"], "A1:B6");
let r = s.execute("data.filterBy", json!({"column": "B", "values": ["SP"]})).unwrap();
assert_eq!(r["hiddenRows"], 3);
// An explicit range is taken as given.
s.execute("data.filter", json!({})).unwrap();
let r = s.execute("data.filter", json!({"range": "A1:B1"})).unwrap();
assert_eq!(r["range"], "A1:B1");
}
/// A sheet with locked data in A1:B3, unlocked cells in D1:D3 and row 10, then protected.
fn protected(extra: serde_json::Value) -> Session {
let mut s = s();

View File

@ -1803,6 +1803,7 @@ fn filter_menu(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool)
let wb = app.session.active().map(|d| d.wb.clone()).unwrap_or(wb);
let values = gridcraft_engine::cmd::data::filter_values(&wb, si, col);
let key = egui::Id::new(("filter_sel", col));
let search_key = egui::Id::new(("filter_search", col));
let mut close = false;
let area = egui::Area::new(egui::Id::new("filter_menu")).fixed_pos(at + vec2(-180.0, 10.0)).order(egui::Order::Foreground);
let response = area.show(ui.ctx(), |ui| {
@ -1819,19 +1820,32 @@ fn filter_menu(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool)
ui.separator();
ui.label(egui::RichText::new("Filter").strong());
let mut checks: Vec<(String, bool)> = ui.ctx().data_mut(|d| d.get_temp::<Vec<(String, bool)>>(key)).unwrap_or(values.clone());
let all = checks.iter().all(|(_, b)| *b);
// A search narrows the list; Select All and Apply then only cover the matches.
let mut search: String = ui.ctx().data_mut(|d| d.get_temp(search_key)).unwrap_or_default();
let field = ui.add(egui::TextEdit::singleline(&mut search).hint_text("Search").desired_width(f32::INFINITY));
if !was_open {
field.request_focus();
}
let needle = search.trim().to_lowercase();
let matches = |v: &str| needle.is_empty() || v.to_lowercase().contains(&needle);
let all = checks.iter().filter(|(v, _)| matches(v)).all(|(_, b)| *b);
let mut all_new = all;
if ui.checkbox(&mut all_new, "(Select All)").changed() {
for c in checks.iter_mut() {
let label = if needle.is_empty() { "(Select All)" } else { "(Select All Search Results)" };
if ui.checkbox(&mut all_new, label).changed() {
for c in checks.iter_mut().filter(|(v, _)| matches(v)) {
c.1 = all_new;
}
}
egui::ScrollArea::vertical().max_height(220.0).show(ui, |ui| {
for (v, on) in checks.iter_mut() {
for (v, on) in checks.iter_mut().filter(|(v, _)| matches(v)) {
ui.checkbox(on, v.as_str());
}
});
ui.ctx().data_mut(|d| d.insert_temp(key, checks.clone()));
ui.ctx().data_mut(|d| {
d.insert_temp(key, checks.clone());
d.insert_temp(search_key, search.clone());
});
let checks: Vec<(String, bool)> = checks.iter().map(|(v, on)| (v.clone(), *on && matches(v))).collect();
ui.separator();
ui.horizontal(|ui| {
if ui.button("Clear Filter").clicked() {
@ -1853,7 +1867,10 @@ fn filter_menu(app: &mut SheetApp, ui: &mut egui::Ui, geo: &Geo, was_open: bool)
});
});
if close || popup_dismissed(&response.response, was_open) {
ui.ctx().data_mut(|d| d.remove::<Vec<(String, bool)>>(key));
ui.ctx().data_mut(|d| {
d.remove::<Vec<(String, bool)>>(key);
d.remove::<String>(search_key);
});
app.grid.filter_menu = None;
}
}