From 09f78cb3a8dd116d524b3850608312c8e062be89 Mon Sep 17 00:00:00 2001 From: "D. Fender" <26173007+postfen@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:12:09 -0600 Subject: [PATCH] Tables: Format as Table inside a table restyles it Home > Format as Table on a cell or range inside an existing table failed with "A table can't overlap another table." Excel applies the picked style to that table instead. The command now returns that table and its range, as it does for a new table. A range crossing a table's edge is still rejected. Without a range, the active area of the selection decides, as it does for Insert > Table. The native Format menu's Format as Table now runs home.formatAsTable instead of insert.table. --- apps/gridcraft/src/native_menu.rs | 2 +- crates/engine/src/cmd/insert.rs | 19 +++++- .../engine/tests/format_as_table_restyle.rs | 61 +++++++++++++++++++ 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 crates/engine/tests/format_as_table_restyle.rs diff --git a/apps/gridcraft/src/native_menu.rs b/apps/gridcraft/src/native_menu.rs index 6985af3c0..7dc549cb7 100644 --- a/apps/gridcraft/src/native_menu.rs +++ b/apps/gridcraft/src/native_menu.rs @@ -125,7 +125,7 @@ fn tree() -> Vec<(&'static str, Vec)> { ("Unhide Sheet…", "sheet.unhide", None), ("-", "", None), ("Conditional Formatting…", "dialog:manageRules", None), - ("Format as Table", "insert.table", None), + ("Format as Table", "home.formatAsTable", None), ], ), ( diff --git a/crates/engine/src/cmd/insert.rs b/crates/engine/src/cmd/insert.rs index aae5a9faa..2388d1f25 100644 --- a/crates/engine/src/cmd/insert.rs +++ b/crates/engine/src/cmd/insert.rs @@ -26,9 +26,9 @@ pub fn specs() -> Vec { "Format as Table", ["Home", "Styles"], None, - "{range?, style: \"TableStyleMedium2\", header?}", + "{range?, style: \"TableStyleMedium2\", header?} (inside a table: restyles it)", has_doc, - insert_table + format_as_table ), cmd!("table.totalRow", "Total Row", ["Table", "Table Style Options"], None, "{table?, on?: bool}", has_doc, |s, p| table_flag( s, p, "totals" @@ -351,6 +351,21 @@ fn insert_table(s: &mut Session, p: &Json) -> Result { }) } +/// Format as Table inside a table restyles that table, as in Excel; elsewhere it makes a new one. +fn format_as_table(s: &mut Session, p: &Json) -> Result { + // Without a range, the active area decides (as in insert_table), not the first one. + let r = if p.get("range").is_some() { target_range(s, p)? } else { s.doc()?.selection.current() }; + let sheet = target_sheet(s, p)?; + let inside = s.doc()?.wb.sheet(sheet).and_then(|sh| sh.tables.iter().find(|t| t.range.contains_range(&r))).map(|t| (t.name.clone(), t.range)); + match inside { + Some((table, range)) => { + table_style(s, &with(p, "table", json!(table)))?; + Ok(json!({"table": table, "range": range.a1()})) + } + None => insert_table(s, p), + } +} + fn find_table(s: &Session, p: &Json) -> Result<(usize, usize)> { let d = s.doc()?; if let Some(n) = str_param(p, "table") { diff --git a/crates/engine/tests/format_as_table_restyle.rs b/crates/engine/tests/format_as_table_restyle.rs new file mode 100644 index 000000000..0419027c6 --- /dev/null +++ b/crates/engine/tests/format_as_table_restyle.rs @@ -0,0 +1,61 @@ +use gridcraft_engine::Session; +use serde_json::json; + +fn table() -> Session { + let mut s = Session::new(); + s.new_workbook(); + s.execute("range.setValues", json!({"range":"A1", "values":[["Item", "Qty"], ["x", "7"], ["y", "8"]]})).unwrap(); + s.execute("insert.table", json!({"range":"A1:B3", "header":true, "name":"T"})).unwrap(); + s +} + +fn styles(s: &Session) -> Vec { + s.doc().unwrap().wb.active().unwrap().tables.iter().map(|t| t.style.clone()).collect() +} + +#[test] +fn format_as_table_inside_a_table_restyles_it() { + let mut s = table(); + s.execute("selection.set", json!({"range":"B2"})).unwrap(); + s.execute("home.formatAsTable", json!({"style":"TableStyleLight9"})).unwrap(); + assert_eq!(styles(&s), ["TableStyleLight9"]); +} + +#[test] +fn format_as_table_on_a_range_inside_a_table_restyles_it() { + let mut s = table(); + s.execute("home.formatAsTable", json!({"range":"A2:B3", "style":"TableStyleDark3"})).unwrap(); + assert_eq!(styles(&s), ["TableStyleDark3"]); +} + +#[test] +fn format_as_table_across_a_table_edge_is_rejected() { + let mut s = table(); + let e = s.execute("home.formatAsTable", json!({"range":"A1:C3", "style":"TableStyleLight9"})).unwrap_err(); + assert!(e.to_string().contains("overlap"), "{e}"); + assert_eq!(styles(&s), ["TableStyleMedium2"]); +} + +#[test] +fn format_as_table_inside_a_table_returns_that_table() { + let mut s = table(); + let r = s.execute("home.formatAsTable", json!({"range":"B2", "style":"TableStyleLight9"})).unwrap(); + assert_eq!(r, json!({"table":"T", "range":"A1:B3"})); +} + +#[test] +fn format_as_table_uses_the_active_area_of_a_multi_area_selection() { + let mut s = table(); + s.execute("selection.set", json!({"range":"D10:E11,B2"})).unwrap(); + s.execute("home.formatAsTable", json!({"style":"TableStyleLight9"})).unwrap(); + assert_eq!(styles(&s), ["TableStyleLight9"]); +} + +#[test] +fn format_as_table_inside_a_table_with_data_beside_it_restyles_it() { + let mut s = table(); + s.execute("range.setValues", json!({"range":"C1", "values":[["Note"], ["a"], ["b"]]})).unwrap(); + s.execute("selection.set", json!({"range":"B2"})).unwrap(); + s.execute("home.formatAsTable", json!({"style":"TableStyleLight9"})).unwrap(); + assert_eq!(styles(&s), ["TableStyleLight9"]); +}