mirror of
https://github.com/storytold/gridcraft.git
synced 2026-11-10 12:29:58 -06:00
Merge PR #3244
This commit is contained in:
@@ -262,21 +262,36 @@ fn clear(s: &mut Session, p: &Json, what: &str) -> Result<Json> {
|
||||
})
|
||||
})
|
||||
};
|
||||
// Notes and links aren't cell contents, so the cell comparison that guards a protected sheet
|
||||
// doesn't see them go: a locked cell's are refused here, stored cell or not.
|
||||
if matches!(what, "all" | "comments" | "hyperlinks") {
|
||||
let d = s.doc()?;
|
||||
if let Some(sh) = d.wb.sheet(sheet).filter(|sh| sh.is_protected()) {
|
||||
for &whole in &ranges {
|
||||
let skip = filtered_rows(sh, whole);
|
||||
let notes = matches!(what, "all" | "comments").then(|| sh.comments.keys()).into_iter().flatten();
|
||||
let links = matches!(what, "all" | "hyperlinks").then(|| sh.hyperlinks.keys()).into_iter().flatten();
|
||||
for &c in notes.chain(links).filter(|c| whole.contains(**c) && !skip.contains(&c.row)) {
|
||||
if d.wb.styles.get(sh.style_id(c)).protection.locked
|
||||
&& let Some(m) = locked_cell_error(sh, c, &d.unlocked_ranges)
|
||||
{
|
||||
return Err(EngineError::Other(m));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
edit(s, |cx| {
|
||||
for r in &ranges {
|
||||
let r = match cx.wb.sheet(sheet).and_then(|sh| sh.used_range()) {
|
||||
Some(u) => match r.intersection(&u) {
|
||||
Some(x) => x,
|
||||
None => continue,
|
||||
},
|
||||
None => continue,
|
||||
for &whole in &ranges {
|
||||
// Cells are only stored inside the used range. Notes and links are kept apart from
|
||||
// them, so they are cleared over the whole range: one from a file can sit on a cell
|
||||
// with nothing stored in it.
|
||||
let used = cx.wb.sheet(sheet).and_then(|sh| sh.used_range()).and_then(|u| whole.intersection(&u));
|
||||
let skip = cx.wb.sheet(sheet).map(|sh| filtered_rows(sh, whole)).unwrap_or_default();
|
||||
let cells: Vec<(CellRef, Cell)> = match (cx.wb.sheet(sheet), used) {
|
||||
(Some(sh), Some(r)) => sh.cells.iter_range(r).filter(|(c, _)| !skip.contains(&c.row)).map(|(c, x)| (c, x.clone())).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
let skip = cx.wb.sheet(sheet).map(|sh| filtered_rows(sh, r)).unwrap_or_default();
|
||||
let cells: Vec<(CellRef, Cell)> = cx
|
||||
.wb
|
||||
.sheet(sheet)
|
||||
.map(|sh| sh.cells.iter_range(r).filter(|(c, _)| !skip.contains(&c.row)).map(|(c, x)| (c, x.clone())).collect())
|
||||
.unwrap_or_default();
|
||||
// Report every stored cell we touch, even beyond a million coordinates in a sparse
|
||||
// selection. Removed formula anchors must also reach calc so their spills disappear.
|
||||
cx.changed.extend(cells.iter().map(|(c, _)| (sheet, *c)));
|
||||
@@ -296,6 +311,18 @@ fn clear(s: &mut Session, p: &Json, what: &str) -> Result<Json> {
|
||||
}
|
||||
}
|
||||
}
|
||||
let cleared = |c: CellRef| whole.contains(c) && !skip.contains(&c.row);
|
||||
// A removed note or link changes its cell even with nothing stored there.
|
||||
let metadata: Vec<CellRef> = cx
|
||||
.wb
|
||||
.sheet(sheet)
|
||||
.map(|sh| {
|
||||
let notes = matches!(what, "all" | "comments").then(|| sh.comments.keys()).into_iter().flatten();
|
||||
let links = matches!(what, "all" | "hyperlinks").then(|| sh.hyperlinks.keys()).into_iter().flatten();
|
||||
notes.chain(links).copied().filter(|c| cleared(*c)).collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
cx.changed.extend(metadata.into_iter().map(|c| (sheet, c)));
|
||||
let sh = cx.sheet_mut(sheet)?;
|
||||
for (c, mut cell) in cells {
|
||||
match what {
|
||||
@@ -312,14 +339,15 @@ fn clear(s: &mut Session, p: &Json, what: &str) -> Result<Json> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let cleared = |c: CellRef| r.contains(c) && !skip.contains(&c.row);
|
||||
if matches!(what, "all" | "comments") {
|
||||
sh.comments.retain(|c, _| !cleared(*c));
|
||||
}
|
||||
if matches!(what, "all" | "hyperlinks") {
|
||||
sh.hyperlinks.retain(|c, _| !cleared(*c));
|
||||
}
|
||||
if matches!(what, "all" | "formats") {
|
||||
if let Some(r) = used
|
||||
&& matches!(what, "all" | "formats")
|
||||
{
|
||||
let mut unmerged = false;
|
||||
sh.merges.retain(|m| {
|
||||
let keep = !r.contains_range(m) || skip.range(m.start.row..=m.end.row).next().is_some();
|
||||
|
||||
@@ -5476,3 +5476,99 @@ fn workbook_display_options_are_commands() {
|
||||
assert!(view(&s).show_vertical_scroll);
|
||||
assert_eq!(s.execute("view.showSheetTabs", json!({})).unwrap()["on"], json!(true), "toggles back");
|
||||
}
|
||||
|
||||
/// A link from a file can sit on a cell with nothing stored in it, past the used range (or on a
|
||||
/// sheet with no cells at all). Clear Hyperlinks, and Remove Hyperlink in the cell menu, must
|
||||
/// still remove it.
|
||||
#[test]
|
||||
fn clear_hyperlinks_removes_links_on_cells_with_nothing_stored() {
|
||||
let mut s = s();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "x"})).unwrap();
|
||||
let link = || gridcraft_model::Hyperlink { target: "https://example.com".into(), tooltip: None, external: true };
|
||||
let (a1, c5) = (CellRef::parse("A1").unwrap(), CellRef::parse("C5").unwrap());
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap().hyperlinks.insert(c5, link());
|
||||
s.execute("edit.clearHyperlinks", json!({"range": "C5"})).unwrap();
|
||||
assert!(!s.doc().unwrap().wb.active().unwrap().hyperlinks.contains_key(&c5), "the link past the used range is removed");
|
||||
|
||||
// No cells at all.
|
||||
s.execute("edit.clearAll", json!({"range": "A1"})).unwrap();
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap().hyperlinks.insert(a1, link());
|
||||
s.execute("edit.clearHyperlinks", json!({"range": "A1"})).unwrap();
|
||||
assert!(!s.doc().unwrap().wb.active().unwrap().hyperlinks.contains_key(&a1), "the link on an empty sheet is removed");
|
||||
}
|
||||
|
||||
/// Notes and links aren't cell contents, so the cell comparison that guards a protected sheet
|
||||
/// doesn't see them go: Clear Hyperlinks and Clear Comments refuse locked cells themselves,
|
||||
/// stored or not.
|
||||
#[test]
|
||||
fn clearing_links_and_notes_respects_sheet_protection() {
|
||||
let mut s = s();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "x"})).unwrap();
|
||||
let link = || gridcraft_model::Hyperlink { target: "https://example.com".into(), tooltip: None, external: true };
|
||||
let (a1, c5) = (CellRef::parse("A1").unwrap(), CellRef::parse("C5").unwrap());
|
||||
{
|
||||
let sh = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap();
|
||||
sh.hyperlinks.insert(a1, link());
|
||||
sh.hyperlinks.insert(c5, link());
|
||||
}
|
||||
s.execute("review.newNote", json!({"cell": "C5", "text": "note", "author": "Example"})).unwrap();
|
||||
s.execute("review.protectSheet", json!({})).unwrap();
|
||||
for (cmd, range) in [("edit.clearHyperlinks", "A1"), ("edit.clearHyperlinks", "C5"), ("edit.clearComments", "C5")] {
|
||||
assert!(s.execute(cmd, json!({"range": range})).is_err(), "{cmd} on locked {range}");
|
||||
}
|
||||
let sh = s.doc().unwrap().wb.active().unwrap().clone();
|
||||
assert!(sh.hyperlinks.contains_key(&a1) && sh.hyperlinks.contains_key(&c5) && sh.comments.contains_key(&c5), "nothing removed");
|
||||
|
||||
s.execute("review.unprotectSheet", json!({})).unwrap();
|
||||
s.execute("edit.clearHyperlinks", json!({"range": "A1:C5"})).unwrap();
|
||||
s.execute("edit.clearComments", json!({"range": "C5"})).unwrap();
|
||||
let sh = s.doc().unwrap().wb.active().unwrap().clone();
|
||||
assert!(sh.hyperlinks.is_empty() && sh.comments.is_empty());
|
||||
}
|
||||
|
||||
/// A removed link or note is a change to its cell even when nothing is stored there, so a
|
||||
/// recorded macro's footprint lists it.
|
||||
#[test]
|
||||
fn clearing_links_and_notes_records_their_cells() {
|
||||
let mut s = s();
|
||||
let c5 = CellRef::parse("C5").unwrap();
|
||||
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb)
|
||||
.sheet_mut(0)
|
||||
.unwrap()
|
||||
.hyperlinks
|
||||
.insert(c5, gridcraft_model::Hyperlink { target: "https://example.com".into(), tooltip: None, external: true });
|
||||
s.execute("review.newNote", json!({"cell": "D7", "text": "note", "author": "Example"})).unwrap();
|
||||
s.doc_mut().unwrap().touched = Some(Vec::new());
|
||||
s.execute("edit.clearHyperlinks", json!({"range": "C5"})).unwrap();
|
||||
s.execute("edit.clearComments", json!({"range": "D7"})).unwrap();
|
||||
let touched: Vec<String> = s.doc().unwrap().touched.clone().unwrap_or_default().into_iter().map(|(_, c)| c.a1()).collect();
|
||||
assert!(touched.contains(&"C5".to_string()) && touched.contains(&"D7".to_string()), "{touched:?}");
|
||||
}
|
||||
|
||||
/// The locked-cell check for notes and links keeps Allow Edit Ranges: a range without a password
|
||||
/// allows them at once, one with a password once it's unlocked in this session.
|
||||
#[test]
|
||||
fn clearing_links_and_notes_keeps_allow_edit_ranges() {
|
||||
let mut s = s();
|
||||
s.execute("cell.set", json!({"cell": "A1", "input": "x"})).unwrap();
|
||||
let link = || gridcraft_model::Hyperlink { target: "https://example.com".into(), tooltip: None, external: true };
|
||||
let (a1, c5) = (CellRef::parse("A1").unwrap(), CellRef::parse("C5").unwrap());
|
||||
{
|
||||
let sh = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().wb).sheet_mut(0).unwrap();
|
||||
sh.hyperlinks.insert(a1, link());
|
||||
sh.hyperlinks.insert(c5, link());
|
||||
}
|
||||
s.execute("review.newNote", json!({"cell": "C5", "text": "note", "author": "Example"})).unwrap();
|
||||
// Ranges are added before protecting: a protected sheet refuses `add`.
|
||||
s.execute("review.allowEditRanges", json!({"action": "add", "title": "Open", "range": "A1"})).unwrap();
|
||||
s.execute("review.allowEditRanges", json!({"action": "add", "title": "Kept", "range": "C5", "password": "example"})).unwrap();
|
||||
s.execute("review.protectSheet", json!({})).unwrap();
|
||||
|
||||
s.execute("edit.clearHyperlinks", json!({"range": "A1"})).unwrap();
|
||||
assert!(s.execute("edit.clearComments", json!({"range": "C5"})).is_err(), "a password range stays locked until unlocked");
|
||||
s.execute("review.allowEditRanges", json!({"action": "unlock", "title": "Kept", "password": "example"})).unwrap();
|
||||
s.execute("edit.clearHyperlinks", json!({"range": "C5"})).unwrap();
|
||||
s.execute("edit.clearComments", json!({"range": "C5"})).unwrap();
|
||||
let sh = s.doc().unwrap().wb.active().unwrap().clone();
|
||||
assert!(sh.hyperlinks.is_empty() && sh.comments.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user