Merge pull request #1117 from Silvertree2010/fix/h1992-table-moverow-table-dropcells-ca

Refuse table row moves across the header/footer boundary
This commit is contained in:
Brandon Thomas
2026-10-19 02:50:34 -04:00
committed by GitHub
2 changed files with 41 additions and 1 deletions
+16 -1
View File
@@ -911,7 +911,8 @@ impl Table {
}
/// Move row `from` to position `to` (indices before the move). Refused when merged cells span
/// rows (the move would cut them).
/// rows (the move would cut them) or when the move would take a row across a header/footer
/// boundary (`from` and `to` must be the same kind of row).
pub fn move_row(&mut self, from: usize, to: usize) -> Result<(), String> {
let (nr, nc) = (self.nrows(), self.ncols());
if from >= nr || to >= nr {
@@ -920,6 +921,9 @@ impl Table {
if self.regions().iter().any(|r| r.2 > 1) {
return Err("unmerge cells that span rows first".into());
}
if self.rows[from].kind != self.rows[to].kind {
return Err("header and footer rows must stay first and last".into());
}
let row = self.rows.remove(from);
self.rows.insert(to, row);
let cells: Vec<Cell> = self.cells.drain(from * nc..(from + 1) * nc).collect();
@@ -1269,6 +1273,17 @@ mod tests {
Story::with_text(StoryId(1), t, ParaFormat::default())
}
#[test]
fn move_row_keeps_header_and_footer_rows_in_place() {
let mut t = Table::new(1, 3, 2, 1, 1, 120.0);
for (from, to) in [(0, 2), (0, 4), (2, 0), (2, 4), (4, 2), (4, 0)] {
assert!(t.move_row(from, to).is_err(), "move_row({from}, {to}) should be refused");
t.check().unwrap();
}
t.move_row(1, 3).unwrap();
t.check().unwrap();
}
#[test]
fn selected_owners_include_covered_cells_without_expanding_range() {
for column in [true, false] {
+25
View File
@@ -2245,6 +2245,31 @@ mod sort_tests {
assert_eq!((cell(&s, 2, 0), cell(&s, 2, 1)), ("x".to_string(), "a".to_string()));
}
#[test]
fn move_row_cannot_move_a_header_row_out_of_place() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let r = s.execute("frame.create", &json!({"rect": [72, 72, 500, 400], "content": "text", "text": ""})).unwrap();
s.execute("text.select", &json!({"story": r["story"], "anchor": 0, "focus": 0})).unwrap();
s.execute("table.insert", &json!({"rows": 3, "cols": 2, "headerRows": 1})).unwrap();
assert!(s.execute("table.moveRow", &json!({"from": 0, "to": 2})).is_err());
s.doc().unwrap().doc.check().unwrap();
// Dragging the selected header row onto the last body row is refused too.
let d = s.doc().unwrap().doc.clone();
let (sid, tid) = d.stories.values().find_map(|st| st.tables.keys().next().map(|k| (st.id, *k))).unwrap();
let fid = d.stories[&sid].frames[0];
let cs = s.cache.get(&d, sid, None);
let cells = &cs.frames[0].tables[0].cells;
let centre = |r: usize| {
let c = cells.iter().find(|c| c.row == r && c.col == 0).unwrap().rect.center();
d.text_xf(d.item(fid).unwrap()) * c
};
let (from, to) = (centre(0), centre(2));
s.execute("table.select", &json!({"story": sid.0, "table": tid, "rows": [0, 0], "what": "row"})).unwrap();
assert!(s.execute("table.dropCells", &json!({"frame": fid.0, "from": [from.x, from.y], "to": [to.x, to.y]})).is_err());
s.doc().unwrap().doc.check().unwrap();
}
#[test]
fn table_options_limits_header_and_footer_rows() {
let mut s = Session::new();