mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-24 10:46:50 -06:00
Merge PR #1531: xlsx: keep a table's filter criteria when the sheet AutoFilter coincides with it
This commit is contained in:
@@ -76,10 +76,29 @@ pub fn write_sheet(wb: &Workbook, si: usize, selected: bool, out: &mut Out) -> (
|
||||
let nstyles = wb.styles.len() as u32;
|
||||
let sid = |s: gridcraft_model::StyleId| if s.0 < nstyles { s.0 } else { 0 };
|
||||
|
||||
// A table's dropdown filter is kept in `sheet.autofilter`, not in `Table::filter` (the
|
||||
// engine's only live filter state, which the reader also fills). Where the two coincide,
|
||||
// fold the sheet filter's criteria into the table so they reach the file: the stored
|
||||
// `colId` offsets are relative to the range's first column, and both ranges start at the
|
||||
// same column, so they carry over verbatim. A filter that only partially covers a table
|
||||
// has nowhere valid to go, so it keeps being suppressed below.
|
||||
let folded: Vec<Table> = match &sheet.autofilter {
|
||||
Some(af) if !af.criteria.is_empty() => sheet
|
||||
.tables
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut t| {
|
||||
if t.header_row && t.filter_button && t.filter.is_empty() && t.range.contains_range(&af.range) {
|
||||
t.filter = af.criteria.clone();
|
||||
}
|
||||
t
|
||||
})
|
||||
.collect(),
|
||||
_ => sheet.tables.clone(),
|
||||
};
|
||||
// Table header cells must hold the column names.
|
||||
let mut overrides: BTreeMap<CellRef, String> = BTreeMap::new();
|
||||
let tables: Vec<(&Table, Vec<String>)> = sheet
|
||||
.tables
|
||||
let tables: Vec<(&Table, Vec<String>)> = folded
|
||||
.iter()
|
||||
.filter(|t| t.range.height() > t.header_row as u32 + t.totals_row as u32 || (t.header_row && t.range.height() > t.totals_row as u32))
|
||||
.map(|t| (t, table_columns(t)))
|
||||
@@ -341,7 +360,7 @@ pub fn write_sheet(wb: &Workbook, si: usize, selected: bool, out: &mut Out) -> (
|
||||
s.push_str(&protected_ranges_xml(sheet));
|
||||
// autoFilter (a sheet filter overlapping a table would be invalid).
|
||||
if let Some(af) = &sheet.autofilter
|
||||
&& !sheet.tables.iter().any(|t| t.range.intersects(&af.range))
|
||||
&& !folded.iter().any(|t| t.range.contains_range(&af.range) && t.header_row && t.filter_button)
|
||||
{
|
||||
s.push_str(&autofilter_xml(af.range, &af.criteria, out));
|
||||
}
|
||||
|
||||
@@ -1165,3 +1165,60 @@ fn doc_props_keep_boundary_whitespace() {
|
||||
let (back, _) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
|
||||
assert_eq!(back.props, wb.props);
|
||||
}
|
||||
|
||||
/// A table's dropdown filter lives in `sheet.autofilter` (the engine's only live filter state),
|
||||
/// not in `Table::filter`. Writing used to suppress the sheet `<autoFilter>` because a table
|
||||
/// intersects it and write the table part from `Table::filter`, which was empty: the criteria
|
||||
/// were lost on every save, leaving the rows hidden with no filter to clear on reopen.
|
||||
#[test]
|
||||
fn a_table_filter_criteria_survive_a_round_trip() {
|
||||
let mut wb = Workbook::new();
|
||||
let s = wb.sheet_mut(0).unwrap();
|
||||
for (r, v) in [(0u32, "Region"), (1, "East"), (2, "West"), (3, "East")] {
|
||||
s.set_value(at(&format!("A{}", r + 1)), Value::text(v));
|
||||
}
|
||||
s.tables.push(Table {
|
||||
id: 1,
|
||||
name: "Sales".into(),
|
||||
range: rr("A1:A4"),
|
||||
header_row: true,
|
||||
totals_row: false,
|
||||
columns: vec![TableColumn {
|
||||
name: "Region".into(),
|
||||
totals: TotalsFn::None,
|
||||
totals_label: None,
|
||||
formula: None,
|
||||
unique_name: None,
|
||||
xml: None,
|
||||
}],
|
||||
style: "TableStyleMedium2".into(),
|
||||
banded_rows: true,
|
||||
banded_cols: false,
|
||||
first_col: false,
|
||||
last_col: false,
|
||||
filter_button: true,
|
||||
filter: vec![],
|
||||
table_type: None,
|
||||
connection_id: None,
|
||||
});
|
||||
// The dropdown of a table's header cell installs the sheet AutoFilter over the table's
|
||||
// range, and `data.filterBy` stores its criteria there.
|
||||
s.autofilter = Some(AutoFilter {
|
||||
range: rr("A1:A4"),
|
||||
criteria: vec![(0, FilterCriterion::Values { values: vec!["East".into()], blanks: false })],
|
||||
});
|
||||
s.rows.entry(1).or_default().hidden = true;
|
||||
|
||||
let (back, rep) = read_xlsx(&write_xlsx(&wb).unwrap()).unwrap();
|
||||
assert!(rep.warnings.is_empty(), "{:?}", rep.warnings);
|
||||
let b = back.sheet(0).unwrap();
|
||||
let t = b.tables.first().expect("the table survived");
|
||||
let (_, crit) = t.filter.first().expect("the table's filter criteria were kept on save");
|
||||
assert!(
|
||||
matches!(crit, FilterCriterion::Values { values, .. } if values == &["East".to_string()]),
|
||||
"the criteria were dropped: {:?}",
|
||||
t.filter
|
||||
);
|
||||
assert!(b.is_row_hidden(1), "the row the filter hid is still hidden");
|
||||
assert!(b.autofilter.is_none(), "a table's filter belongs to the table part, not the sheet");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user