mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-07 02:51:33 +00:00
Form controls: a macro assignment removed in another application stays removed on reopen
load_action_names gave every kept action back to a button the file assigned no macro. A button that ran a macro with a fixed input range or an output cell, whose macro another application removed while keeping the button's name and place, so came back with the assignment. Now a kept macro action comes back only onto a button the file still assigns that macro, and a kept command only onto a button the file assigns no macro, as the function's comment already said. Tests: a_macro_assignment_removed_in_another_application_stays_removed (the saved file with the controlPr macro attribute and the VML FmlaMacro removed reopens with no action and no hidden name; unedited, with the whole action), a_macro_assigned_in_another_application_replaces_a_stored_command (the file's macro wins over a kept command), and the cleared case in a_kept_button_action_comes_back_only_on_the_same_button. macros.md and file-format-parity.md describe the hidden name and the reopen rule.
This commit is contained in:
1 parent
dcefd523eb
commit
099349d91d
4 files changed
+117
-4
No files matched your search
@@ -627,7 +627,7 @@ pub(crate) fn load_action_names(wb: &mut Workbook) {
|
||||
&& text("name").is_none_or(|n| n == c.display_name())
|
||||
&& text("at").is_none_or(|a| a == c.anchor.cell.a1())
|
||||
&& match (&c.action, &action) {
|
||||
(None, _) => true,
|
||||
(None, ControlAction::Command { .. }) => true,
|
||||
(Some(ControlAction::Macro { name: now, .. }), ControlAction::Macro { name: kept, .. }) => now.eq_ignore_ascii_case(kept),
|
||||
_ => false,
|
||||
}
|
||||
|
||||
@@ -420,6 +420,109 @@ fn a_kept_button_action_comes_back_only_on_the_same_button() {
|
||||
"{i}"
|
||||
);
|
||||
}
|
||||
// Another application cleared the macro of "Fixed": the stored action (with its fixed input)
|
||||
// does not bring the assignment back. The command on a button given no macro still does.
|
||||
let mut cleared = kept(&stored);
|
||||
cleared.sheet_mut(0).unwrap().controls[2].action = None;
|
||||
load_action_names(&mut cleared);
|
||||
assert_eq!(cleared.sheets[0].controls[2].action, None, "the removed assignment stays removed");
|
||||
assert_eq!(cleared.sheets[0].controls[0].action, actions(&wb)[0], "the command comes back");
|
||||
}
|
||||
|
||||
/// The saved xlsx bytes of `s`'s workbook.
|
||||
fn saved_xlsx(s: &mut Session) -> Vec<u8> {
|
||||
let r = s.execute("file.saveBytes", json!({"format": "xlsx"})).unwrap();
|
||||
crate::io::base64_decode(r["base64"].as_str().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// `bytes` with `edit` applied to the text of every part, as another application that rewrote
|
||||
/// those parts would leave the file.
|
||||
fn edit_parts(bytes: &[u8], mut edit: impl FnMut(&str, String) -> String) -> Vec<u8> {
|
||||
use std::io::{Cursor, Read, Write};
|
||||
let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
||||
let mut out = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
for i in 0..zip.len() {
|
||||
let mut part = zip.by_index(i).unwrap();
|
||||
let name = part.name().to_string();
|
||||
let mut data = Vec::new();
|
||||
part.read_to_end(&mut data).unwrap();
|
||||
let data = match String::from_utf8(data) {
|
||||
Ok(text) => edit(&name, text).into_bytes(),
|
||||
Err(e) => e.into_bytes(),
|
||||
};
|
||||
out.start_file(name, zip::write::SimpleFileOptions::default()).unwrap();
|
||||
out.write_all(&data).unwrap();
|
||||
}
|
||||
out.finish().unwrap().into_inner()
|
||||
}
|
||||
|
||||
/// Opens `bytes` as `name`, checks the hidden name of the button actions is taken back out, and
|
||||
/// lists the controls.
|
||||
fn open_and_list(s: &mut Session, name: &str, bytes: &[u8]) -> Json {
|
||||
s.execute("file.open", json!({"name": name, "base64": crate::io::base64_encode(bytes)})).unwrap();
|
||||
assert!(s.doc().unwrap().wb.names.iter().all(|n| !n.name.contains("control_actions")), "{name}: the hidden name is taken back out");
|
||||
s.execute("control.list", json!({})).unwrap()
|
||||
}
|
||||
|
||||
/// The macro reference xlsx writes for a button that runs `Mark`.
|
||||
const MARK: &str = "[0]!Mark";
|
||||
|
||||
/// Another application removed a button's macro assignment and kept the button with its name and
|
||||
/// place: the hidden name's copy of the action (with the fixed input and the output cell) does not
|
||||
/// bring the assignment back. The same file, unedited, reopens with the whole action.
|
||||
#[test]
|
||||
fn a_macro_assignment_removed_in_another_application_stays_removed() {
|
||||
let mut s = s();
|
||||
s.execute("macro.save", json!({"name": "Mark", "steps": [{"command": "cell.set", "params": {"cell": "Input.cell(1,1)", "input": "m"}}], "inputs": [{"name": "Input", "kind": "range"}], "output": {"onConflict": "overwrite"}})).unwrap();
|
||||
s.execute("insert.button", json!({"at": "D2", "text": "Run", "macro": "Mark", "input": "F1:G2", "output": "H1"})).unwrap();
|
||||
let before = s.execute("control.list", json!({})).unwrap()[0].clone();
|
||||
let full = ControlAction::Macro { name: "Mark".into(), input: MacroInput::Fixed("F1:G2".into()), output: Some("H1".into()) };
|
||||
assert_eq!(before["action"], serde_json::to_value(&full).unwrap());
|
||||
let bytes = saved_xlsx(&mut s);
|
||||
// xlsx assigns the macro in the sheet's `<controlPr macro>` and in the VML shape's
|
||||
// `<x:FmlaMacro>` (the parts the reader takes it from); clearing the assignment drops both.
|
||||
let (attr, fmla) = (format!(" macro=\"{MARK}\""), format!("<x:FmlaMacro>{MARK}</x:FmlaMacro>"));
|
||||
let mut edited = Vec::new();
|
||||
let cleared = edit_parts(&bytes, |name, text| {
|
||||
if text.contains(&attr) || text.contains(&fmla) {
|
||||
edited.push(name.to_string());
|
||||
}
|
||||
text.replace(&attr, "").replace(&fmla, "")
|
||||
});
|
||||
edited.sort();
|
||||
assert_eq!(edited, ["xl/drawings/vmlDrawing1.vml", "xl/worksheets/sheet1.xml"]);
|
||||
let list = open_and_list(&mut s, "cleared.xlsx", &cleared);
|
||||
assert_eq!(list.as_array().unwrap().len(), 1);
|
||||
for key in ["kind", "name", "at", "text"] {
|
||||
assert_eq!(list[0][key], before[key], "{key}: the same button");
|
||||
}
|
||||
assert_eq!(list[0].get("action"), None, "the removed assignment stays removed");
|
||||
// Unedited, the file still assigns the macro, and the action comes back whole.
|
||||
let list = open_and_list(&mut s, "kept.xlsx", &bytes);
|
||||
assert_eq!(list[0]["action"], before["action"]);
|
||||
}
|
||||
|
||||
/// Another application assigned a macro to a button that ran a command (kept in the hidden name):
|
||||
/// the file's macro wins, as it does when a macro button is given another macro.
|
||||
#[test]
|
||||
fn a_macro_assigned_in_another_application_replaces_a_stored_command() {
|
||||
let mut s = s();
|
||||
s.execute("macro.save", json!({"name": "Mark", "steps": [{"command": "cell.set", "params": {"cell": "A1", "input": "m"}}]})).unwrap();
|
||||
s.execute("insert.button", json!({"at": "D2", "text": "Run", "command": "cell.set", "params": {"cell": "A9", "input": "cmd"}})).unwrap();
|
||||
let bytes = saved_xlsx(&mut s);
|
||||
let (pr, data) = ("<controlPr", "<x:ClientData ObjectType=\"Button\">");
|
||||
let mut edited = Vec::new();
|
||||
let assigned = edit_parts(&bytes, |name, text| {
|
||||
if text.contains(pr) || text.contains(data) {
|
||||
edited.push(name.to_string());
|
||||
}
|
||||
text.replace(pr, &format!("<controlPr macro=\"{MARK}\"")).replace(data, &format!("{data}<x:FmlaMacro>{MARK}</x:FmlaMacro>"))
|
||||
});
|
||||
edited.sort();
|
||||
assert_eq!(edited, ["xl/drawings/vmlDrawing1.vml", "xl/worksheets/sheet1.xml"]);
|
||||
let list = open_and_list(&mut s, "assigned.xlsx", &assigned);
|
||||
let plain = ControlAction::Macro { name: "Mark".into(), input: MacroInput::Selection, output: None };
|
||||
assert_eq!(list[0]["action"], serde_json::to_value(&plain).unwrap(), "the file's macro, not the stored command");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -76,7 +76,7 @@ past the parser's limit, and a wrapper inside the text. **Saving writes the real
|
||||
|
||||
| Format | Excel | GridCraft read | GridCraft write | Fidelity / notes | Tests |
|
||||
|---|---|---|---|---|---|
|
||||
| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, shared strings (with their Japanese phonetic runs — ふりがな — kept through open + save: `<rPh>` offsets and readings and `<phoneticPr>` are written back next to the same string; the table holds each text once, so duplicate `<si>` items whose text is the same but whose readings differ keep only the first reading, with a warning on open), styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), form controls (buttons, scroll bars and spin buttons: control properties parts, `<controls>` and VML `x:ClientData`; a VML-only control is read too), protection, calc settings, show objects (`workbookPr showObjects`). **Kept unchanged on save (pass-through, `passthrough.rs`):** custom document properties, ribbon customisation and add-in task panes, custom XML, links to external workbooks (never followed or refreshed), data connections, sheet query tables, printer settings (#324), other form controls (check boxes, list boxes, drop-downs; written in the same `<controls>` element and VML drawing as the modelled ones), ActiveX controls, OLE objects and sheet backgrounds (with their VML shapes), and chart sheets (#727: the sheet, drawing, chart, style and colour parts are written back unchanged in their place, and left out when a sheet they plot was deleted or renamed or a worksheet took their name; they can't be edited or created); dropped when their sheet is deleted or their parts point at content GridCraft rewrites. **Not preserved:** slicers and slicer caches, timelines, data model, query tables attached to tables, sheet and workbook `extLst` entries we don't model, chart details; Strict Open XML files keep nothing. Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) A picture used on several sheets is written once (#985). | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` |
|
||||
| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, shared strings (with their Japanese phonetic runs — ふりがな — kept through open + save: `<rPh>` offsets and readings and `<phoneticPr>` are written back next to the same string; the table holds each text once, so duplicate `<si>` items whose text is the same but whose readings differ keep only the first reading, with a warning on open), styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), form controls (buttons, scroll bars and spin buttons: control properties parts, `<controls>` and VML `x:ClientData`; a VML-only control is read too; a button's command, fixed input range or output cell travels in a hidden defined name and comes back only onto the same button while the file still assigns its macro, or none for a command), protection, calc settings, show objects (`workbookPr showObjects`). **Kept unchanged on save (pass-through, `passthrough.rs`):** custom document properties, ribbon customisation and add-in task panes, custom XML, links to external workbooks (never followed or refreshed), data connections, sheet query tables, printer settings (#324), other form controls (check boxes, list boxes, drop-downs; written in the same `<controls>` element and VML drawing as the modelled ones), ActiveX controls, OLE objects and sheet backgrounds (with their VML shapes), and chart sheets (#727: the sheet, drawing, chart, style and colour parts are written back unchanged in their place, and left out when a sheet they plot was deleted or renamed or a worksheet took their name; they can't be edited or created); dropped when their sheet is deleted or their parts point at content GridCraft rewrites. **Not preserved:** slicers and slicer caches, timelines, data model, query tables attached to tables, sheet and workbook `extLst` entries we don't model, chart details; Strict Open XML files keep nothing. Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) A picture used on several sheets is written once (#985). | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` |
|
||||
| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, shared strings (with their Japanese phonetic runs — ふりがな — kept through open + save: `<rPh>` offsets and readings and `<phoneticPr>` are written back next to the same string; the table holds each text once, so duplicate `<si>` items whose text is the same but whose readings differ keep only the first reading, with a warning on open), styles, themes, merges, CF, validation, tables, notes (with their shown/hidden state) and threaded comments with replies, resolved state and timestamps (`threadedComments` and `persons` parts, #300), hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings, show objects (`workbookPr showObjects`). **Not preserved:** unmodelled parts and extensions (form controls, slicer caches, timelines, external links, OLE objects, custom XML, data model, query tables, chart details). Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) A picture used on several sheets is written once (#985). | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` |
|
||||
| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, shared strings (with their Japanese phonetic runs — ふりがな — kept through open + save: `<rPh>` offsets and readings and `<phoneticPr>` are written back next to the same string; the table holds each text once, so duplicate `<si>` items whose text is the same but whose readings differ keep only the first reading, with a warning on open), styles, themes, merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, Allow Edit Ranges (`protectedRanges` with their password hashes, #307), calc settings, show objects (`workbookPr showObjects`). **Not preserved:** unmodelled parts and extensions (form controls, slicer caches, timelines, external links, OLE objects, custom XML, data model, query tables, chart details). Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) A picture used on several sheets is written once (#985). | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` |
|
||||
| `.xlsx` Excel Workbook (transitional) | R/W, native | yes | yes | Cells, shared/array/dynamic formulas, styles, themes (the major/minor `latin`, East Asian `ea` and complex-script `cs` typefaces and the per-script `<a:font script>` lists are kept through a save), merges, CF, validation, tables, comments and notes, hyperlinks, names, freeze panes, outline, print settings, pictures, shapes, charts (simplified, see [chart-parity.md](chart-parity.md)), sparklines, PivotTables (caches and tables), protection, calc settings. **Kept unchanged on save (pass-through, `passthrough.rs`):** custom document properties, ribbon customisation and add-in task panes, custom XML, links to external workbooks (never followed or refreshed), data connections, sheet query tables, printer settings (#324), form controls, ActiveX controls, OLE objects and sheet backgrounds (with their VML shapes), pictures in headers and footers (`&G`, #1257: their VML drawing and images are written back unchanged; they are not shown, and can't be inserted), and chart sheets (#727: the sheet, drawing, chart, style and colour parts are written back unchanged in their place, and left out when a sheet they plot was deleted or renamed or a worksheet took their name; they can't be edited or created); dropped when their sheet is deleted or their parts point at content GridCraft rewrites. **Not preserved:** slicers and slicer caches, timelines, data model, query tables attached to tables, sheet and workbook `extLst` entries we don't model, chart details; Strict Open XML files keep nothing. Never checked in Excel across a corpus: repair prompts unknown. 100 MB files fail (#175) | `roundtrip.rs`, `fixtures.rs`, `malformed.rs`, `pivot_tests.rs` |
|
||||
@@ -145,7 +145,7 @@ refresh. Counted under features (Get & Transform, ~10% ready), not here.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | minor | XLSX: buttons, scroll bars and spin buttons read and written (control properties parts, `<controls>`, VML `x:ClientData`); other form controls stay with pass-through |
|
||||
| 2026-11-02 | minor | XLSX: buttons, scroll bars and spin buttons read and written (control properties parts, `<controls>`, VML `x:ClientData`); other form controls stay with pass-through; a button's command, fixed input or output cell travels in a hidden defined name and comes back only while the file still assigns the button's macro (or none, for a command) |
|
||||
| 2026-11-01 | minor | Chart data tables preserve showKeys, showHorzBorder, showVertBorder and showOutline through native c:dTable XLSX import/export; synthetic tests cover all 16 flag combinations and boolean defaults. Excel-side verification remains outstanding. |
|
||||
| 2026-11-01 | minor | XLSX reads and writes Page Setup's page order: `<pageSetup pageOrder="overThenDown">` for Over, then down, and no attribute for Down, then over (the default), as Excel writes them; PDF export and printing number and order the pages by it |
|
||||
| 2026-11-01 | trivial | XLSX selects the first visible workbook view as one unit and bounds imported/exported tab ratios; isolated attributes and sheet-edit round trips covered, with firstSheet left to #2264 (#3057) |
|
||||
|
||||
+11
-1
@@ -1,6 +1,6 @@
|
||||
# Macros
|
||||
|
||||
> **Last updated:** 2026-11-02 · **Change:** minor (form buttons run macros; `macro.run` takes `output`; earlier: a close that waits on the step editor's question runs in the editor's window, and a further close asked meanwhile waits for the answer or is refused; earlier: closing a workbook or quitting asks about the step editor's unsaved draft first; recording macros and the management commands; Record Macro takes a name, shortcut and description; the macro interface: Automate › Macros, the Macros and Record Macro dialogs, the step editor, shortcuts)
|
||||
> **Last updated:** 2026-11-02 · **Change:** minor (form buttons run macros, `macro.run` takes `output`, and a button action kept in a hidden name comes back on reopen only while the file still agrees with it; earlier: a close that waits on the step editor's question runs in the editor's window, and a further close asked meanwhile waits for the answer or is refused; earlier: closing a workbook or quitting asks about the step editor's unsaved draft first; recording macros and the management commands; Record Macro takes a name, shortcut and description; the macro interface: Automate › Macros, the Macros and Record Macro dialogs, the step editor, shortcuts)
|
||||
|
||||
GridCraft macros are typed, declarative lists of commands, not VBA. Every step runs a GridCraft
|
||||
command (the same ids the ribbon, CLI and MCP use) or one of three bounded control steps. A macro
|
||||
@@ -265,6 +265,16 @@ default) or always on one range (`input: "B2:D9"`), with an optional output cell
|
||||
a step does, inside the scripts' file sandbox unless it is an approved file command, so a button
|
||||
in a workbook opened from a file asks before it runs anything.
|
||||
|
||||
In xlsx a button carries its macro as Excel writes it (`macro="[0]!Name"`, a macro run on the
|
||||
selection). A command, a fixed input range or an output cell has no place there and is kept in a
|
||||
hidden defined name (`_gc_control_actions_<k>`), keyed by the sheet, the button's place, name and
|
||||
anchor cell. When the file is opened the hidden name is removed and each kept action goes back
|
||||
onto its button only when the file still agrees with it: a kept macro action needs the file to
|
||||
assign that macro still, so an assignment removed in another application stays removed and a
|
||||
button given another macro keeps that one; a kept command needs the file to assign no macro. A
|
||||
button whose name, anchor cell or place changed, or which another application removed, gets no
|
||||
kept action.
|
||||
|
||||
Macros are saved in the workbook: as a field in GridCraft's JSON format, and in xlsx as hidden
|
||||
defined names (`_gc_macro_<n>_<k>`, the macro's JSON split into string literals within Excel's
|
||||
limits), which other spreadsheet applications keep but do not run. A visible name of that shape
|
||||
|
||||
Reference in new issue
Block a user