diff --git a/crates/engine/src/cmd/data.rs b/crates/engine/src/cmd/data.rs index 7372380..1c4c778 100644 --- a/crates/engine/src/cmd/data.rs +++ b/crates/engine/src/cmd/data.rs @@ -431,7 +431,16 @@ fn toggle_filter(s: &mut Session, p: &Json) -> Result { 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()})) diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index bb63d67..4ed3fd2 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -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(); diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 72d33e2..b838a7b 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -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::>(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::>(key)); + ui.ctx().data_mut(|d| { + d.remove::>(key); + d.remove::(search_key); + }); app.grid.filter_menu = None; } }