#1327: Preserve file identity after rejected document replacement

This commit is contained in:
skylar taylor-barrick
2026-10-22 21:50:35 -06:00
parent 5220199b6c
commit debb125ddf
4 changed files with 53 additions and 7 deletions
+2 -2
View File
@@ -158,7 +158,7 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
- 2026-10-15: Find opens Navigation on Results; editing the query restores Results after manual Headings/Pages navigation. Typed and selected-text searches retain document contents (#2096).
| Date | What landed |
| 2026-10-22 | Compare results opened as new or revised untitled documents start without the previous document's version snapshots. A comparison rejected by protection keeps the snapshots and save target with the original document. |
| 2026-10-22 | Compare results opened as new or revised untitled documents start without the previous document's version snapshots. A comparison or mailing result rejected by protection keeps the snapshots and save target with the original document. |
| 2026-10-20 | The Document Inspector's removals are refused while the document tracks changes: they delete straight from the document, so the text would go with no revision to show for it and the revision trail would be broken behind the user's back |
| 2026-10-20 | A version snapshot belongs to the document it was taken from: the snapshots went with the document, so a restore can only ever bring back a version of the document that is open |
| 2026-10-22 | Restrict Editing: the CLI's `--author` uses the same protection check as `file.setAuthor`, and File › Options disables the User name field while the document is protected. |
@@ -578,7 +578,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps
| Date | Change | Summary |
|---|---|---|
| 2026-10-22 | minor | Compare's new and revised untitled results clear the previous document's version snapshots; a rejected protected comparison preserves the snapshots and save target. |
| 2026-10-22 | minor | Compare's new and revised untitled results clear the previous document's version snapshots; a rejected protected comparison or mailing result preserves the snapshots and file identity. |
| 2026-10-20 | minor | The Document Inspector's removals are refused while the document tracks changes (they delete straight from the document, so the text would go with no revision to show for it and the revision trail would be broken behind the user's back), and a version snapshot belongs to the document it was taken from |
| 2026-10-22 | minor | The CLI author flag and the Options user name field cannot change the session name after protection starts (#3279 follow-up). |
| 2026-10-20 | minor | Restrict Editing, three more gaps closed: the protection of a protected document only changes through the explicit stop, the session author can't change through `file.setAuthor` under protection, and the post-check compares the settings and the metadata as well as the text and the formatting |
+19 -4
View File
@@ -1445,9 +1445,19 @@ impl Session {
// These mutating commands can replace the open document and discard its saved versions.
// Keep the versions until the command and the protected-region post-check have both
// succeeded; rejected commands must leave the old document and its snapshots together.
let versions_before = matches!(id, "review.compare" | "review.combine" | "mailings.finish" | "mailings.envelopes" | "mailings.labels")
.then(|| self.versions.clone());
let comparison_file_before = matches!(id, "review.compare" | "review.combine").then(|| (self.path.clone(), self.file_name.clone()));
let replaces_document = matches!(id, "review.compare" | "review.combine" | "mailings.finish" | "mailings.envelopes" | "mailings.labels");
let versions_before = replaces_document.then(|| self.versions.clone());
let identity_before = replaces_document.then(|| {
(
self.path.clone(),
self.file_name.clone(),
self.document_id,
self.password.clone(),
self.dirty,
self.bib_style.clone(),
self.merge.preview,
)
});
let typing = spec.id == "text.insert" || spec.id == "equation.type";
if spec.mutates && !typing {
self.typing_open = false;
@@ -1532,9 +1542,14 @@ impl Session {
if let Some(versions) = versions_before {
self.versions = versions;
}
if let Some((path, file_name)) = comparison_file_before {
if let Some((path, file_name, document_id, password, dirty, bib_style, merge_preview)) = identity_before {
self.path = path;
self.file_name = file_name;
self.document_id = document_id;
self.password = password;
self.dirty = dirty;
self.bib_style = bib_style;
self.merge.preview = merge_preview;
}
if let Some((d, s, h, r, t, ev, c)) = before_doc {
self.doc = d;
+31
View File
@@ -8385,6 +8385,37 @@ fn a_rejected_comparison_keeps_the_current_versions() {
assert_eq!(s.file_name.as_deref(), Some("original.docx"));
}
/// A rejected mailing result must keep the source document's save target and versions.
#[test]
fn a_rejected_mailing_result_keeps_the_current_file_identity() {
use wordcraft_doc::{CharProps, InlineObject};
let mut s = Session::new(wordcraft_doc::Document::from_text("Protected source."));
s.path = Some(std::path::PathBuf::from("source.docx"));
s.file_name = Some("source.docx".into());
s.run("file.versions", &json!({"save": "snapshot"})).unwrap();
let end = s.doc.end_of(StoryRef::Body);
s.doc.insert_object(&end, InlineObject::PermEnd { id: 1 }, &CharProps::default()).unwrap();
let start = s.doc.start_of(StoryRef::Body);
s.doc.insert_object(&start, InlineObject::PermStart { id: 1, group: String::new(), editor: s.author.clone() }, &CharProps::default()).unwrap();
s.run("review.restrict", &json!({"mode": "readOnly"})).unwrap();
let before = s.doc.clone();
let before_id = s.document_id();
let before_dirty = s.dirty;
s.password = Some(crate::io::Password::new("synthetic file password"));
s.bib_style = "Chicago".into();
assert!(s.run("mailings.envelopes", &json!({"delivery": "Recipient"})).is_err());
assert_eq!(s.doc, before);
assert_eq!(s.versions.len(), 1);
assert_eq!(s.path.as_deref(), Some(std::path::Path::new("source.docx")));
assert_eq!(s.file_name.as_deref(), Some("source.docx"));
assert_eq!(s.document_id(), before_id);
assert_eq!(s.dirty, before_dirty);
assert_eq!(s.password.as_ref().map(crate::io::Password::as_str), Some("synthetic file password"));
assert_eq!(s.bib_style, "Chicago");
}
/// #1344: File › Info › Inspect Document finds hidden text by its effective formatting (direct,
/// character style, paragraph style) in every story — body, header, footer, footnote, comment,
/// text box — and Remove All deletes exactly that text, in one undo step, through a DOCX save.
+1 -1
View File
@@ -125,7 +125,7 @@ Footer page-table height and body wrapping are corrected. Other pagination gaps
| Date | Change | Summary |
|---|---|---|
| 2026-10-22 | minor | Compare's new and revised untitled results clear the previous document's version snapshots; a rejected protected comparison preserves the snapshots and save target. No gap row; estimates unchanged. |
| 2026-10-22 | minor | Compare's new and revised untitled results clear the previous document's version snapshots; a rejected protected comparison or mailing result preserves the snapshots and file identity. No gap row; estimates unchanged. |
| 2026-10-20 | minor | The Document Inspector's removals are refused while the document tracks changes (they aren't tracked), and a version snapshot belongs to the document it was taken from. No gap row (enforcement fix); estimates unchanged |
| 2026-10-18 | trivial | Explicit New/Open is not interrupted by startup recovery discovery; older copies stay available and Recover follows the interface theme. |
| 2026-10-21 | trivial | Bullets and Numbering toggle off style-inherited lists at their effective level, including basedOn chains and numbered ListParagraph styles; other paragraphs and list definitions stay unchanged (related to #3459) |