#2120: clearing links and notes respects sheet protection

Addresses review comment 4455670315:

- Clear Hyperlinks and Clear Comments removed links and notes on
  locked cells of a protected sheet: protection compares stored cells
  only, so their removal went unseen (for stored cells too, and now
  for unstored ones since the whole range is cleared). `clear` now
  refuses a locked cell's note or link with locked_cell_error, which
  keeps Allow Edit Ranges and session-unlocked ranges.
- A removed note or link on a cell with nothing stored wasn't added
  to the changed cells, so a recorded macro's footprint missed it.

Tests: engine tests::clearing_links_and_notes_respects_sheet_protection,
tests::clearing_links_and_notes_records_their_cells.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hector Villarreal
2026-11-03 08:08:07 -06:00
co-authored by Claude Opus 5.5
parent 58e876a4d8
commit dfb3b1224b
2 changed files with 79 additions and 1 deletions
+31 -1
View File
@@ -244,6 +244,25 @@ 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 &whole in &ranges {
// Cells are only stored inside the used range. Notes and links are kept apart from
@@ -274,6 +293,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 {
@@ -290,7 +321,6 @@ fn clear(s: &mut Session, p: &Json, what: &str) -> Result<Json> {
_ => {}
}
}
let cleared = |c: CellRef| whole.contains(c) && !skip.contains(&c.row);
if matches!(what, "all" | "comments") {
sh.comments.retain(|c, _| !cleared(*c));
}