From 1d33ea81beccbaebe2f7524e156c3d319a48afad Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:06:52 +0200 Subject: [PATCH] Write comment and hyperlink maps with A1 keys so JSON save works (#126) --- crates/engine/src/tests.rs | 17 +++++++++++++ crates/model/src/cellmap.rs | 51 +++++++++++++++++++++++++++++++++++++ crates/model/src/lib.rs | 1 + crates/model/src/sheet.rs | 2 ++ 4 files changed, 71 insertions(+) create mode 100644 crates/model/src/cellmap.rs diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index b6caa09b4..a2ae4be0e 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1018,3 +1018,20 @@ fn malformed_xlsb_keeps_the_current_workbook() { assert_eq!(v(&s, "A1"), Value::from("Keep me")); assert!(s.doc().unwrap().is_dirty()); } + +#[test] +fn notes_comments_and_links_survive_a_json_save() { + let mut s = s(); + s.execute("cell.set", json!({"cell": "A1", "input": "x"})).unwrap(); + s.execute("review.newNote", json!({"cell": "A1", "text": "note", "author": "Example"})).unwrap(); + s.execute("review.newComment", json!({"cell": "B2", "text": "comment", "author": "Example"})).unwrap(); + s.execute("insert.link", json!({"cell": "C3", "target": "Sheet1!A1", "tooltip": "jump"})).unwrap(); + let wb = &s.doc().unwrap().wb; + let bytes = crate::io::save_bytes(wb, "book.json").unwrap(); + let (back, _) = crate::io::open_bytes("book.json", &bytes).unwrap(); + let (before, after) = (wb.active().unwrap(), back.active().unwrap()); + assert_eq!((before.comments.len(), before.hyperlinks.len()), (2, 1)); + assert_eq!(after.comments, before.comments); + assert_eq!(after.hyperlinks, before.hyperlinks); + assert_eq!(after.cells, before.cells); +} diff --git a/crates/model/src/cellmap.rs b/crates/model/src/cellmap.rs new file mode 100644 index 000000000..6bfd1dea1 --- /dev/null +++ b/crates/model/src/cellmap.rs @@ -0,0 +1,51 @@ +//! Serde helpers for maps keyed by `CellRef`. JSON object keys must be strings, so the keys are +//! written as `A1`-style text. + +use std::collections::BTreeMap; + +use gridcraft_core::CellRef; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +pub fn serialize(map: &BTreeMap, s: S) -> Result { + s.collect_map(map.iter().map(|(k, v)| (k.a1(), v))) +} + +pub fn deserialize<'de, D: Deserializer<'de>, V: Deserialize<'de>>(d: D) -> Result, D::Error> { + let raw = BTreeMap::::deserialize(d)?; + let mut out = BTreeMap::new(); + for (k, v) in raw { + let cell = CellRef::parse(&k).ok_or_else(|| D::Error::custom(format!("invalid cell reference `{k}`")))?; + out.insert(cell, v); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use crate::{Comment, Hyperlink, Sheet}; + use gridcraft_core::CellRef; + + #[test] + fn comments_and_hyperlinks_survive_json() { + let mut s = Sheet::new("S"); + let a1 = CellRef::new(0, 0); + let c3 = CellRef::new(2, 2); + s.comments.insert( + a1, + Comment { + author: "Example".into(), + text: "note".into(), + replies: vec![("B".into(), "r".into())], + threaded: true, + resolved: false, + visible: false, + }, + ); + s.hyperlinks.insert(c3, Hyperlink { target: "Sheet1!A1".into(), tooltip: Some("jump".into()) }); + let json = serde_json::to_string(&s).expect("serialize"); + let back: Sheet = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(back.comments, s.comments); + assert_eq!(back.hyperlinks, s.hyperlinks); + } +} diff --git a/crates/model/src/lib.rs b/crates/model/src/lib.rs index ba0f32c43..57b8d05d0 100644 --- a/crates/model/src/lib.rs +++ b/crates/model/src/lib.rs @@ -6,6 +6,7 @@ mod b64; pub mod cell; +mod cellmap; pub mod features; pub mod sheet; pub mod store; diff --git a/crates/model/src/sheet.rs b/crates/model/src/sheet.rs index 4b4200d39..2e33f3441 100644 --- a/crates/model/src/sheet.rs +++ b/crates/model/src/sheet.rs @@ -48,7 +48,9 @@ pub struct Sheet { pub validations: Vec, pub tables: Vec, pub autofilter: Option, + #[serde(with = "crate::cellmap")] pub comments: BTreeMap, + #[serde(with = "crate::cellmap")] pub hyperlinks: BTreeMap, pub charts: Vec, pub images: Vec,