Add a recovery path for autosaved documents that don't open successfully (#4157)

* Add a recovery path for autosaved documents that don't open successfully

* Fix file naming edge case

* Add edge cases for Windows zip names

* Simplify comments and remove desktop portion
This commit is contained in:
Keavon Chambers
2026-05-22 21:14:28 -07:00
committed by GitHub
parent c3b0c87cfe
commit 62287d7893
9 changed files with 471 additions and 15 deletions
@@ -0,0 +1,113 @@
// TODO: Eventually remove this document upgrade code
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
pub struct FailedToLoadDocumentsDialog {
pub failed_document_names: Vec<String>,
}
impl DialogLayoutHolder for FailedToLoadDocumentsDialog {
const ICON: &'static str = "Warning";
const TITLE: &'static str = "Failed to Open Documents";
fn layout_buttons(&self) -> Layout {
let widgets = vec![
TextButton::new("Download")
.emphasized(true)
.tooltip_description("Save the raw document data to disk so it can be recovered later.")
.on_update(|_| {
DialogMessage::CloseAndThen {
followups: vec![PortfolioMessage::DownloadFailedToLoadDocuments.into()],
}
.into()
})
.widget_instance(),
TextButton::new("Discard")
.tooltip_description("Permanently delete the autosaved data for these documents.")
.on_update(|_| {
DialogMessage::CloseAndThen {
followups: vec![PortfolioMessage::DiscardFailedToLoadDocuments.into()],
}
.into()
})
.widget_instance(),
TextButton::new("Dismiss")
.tooltip_description("Close this dialog. The autosaved data is kept and this dialog will reappear on next launch.")
.on_update(|_| FrontendMessage::DialogClose.into())
.widget_instance(),
];
Layout(vec![LayoutGroup::row(widgets)])
}
}
impl LayoutHolder for FailedToLoadDocumentsDialog {
fn layout(&self) -> Layout {
let count = self.failed_document_names.len();
let header = format!("{count} document{} couldn't be reopened.", if count == 1 { "" } else { "s" });
let list = "• ".to_string() + &self.failed_document_names.join("\n• ");
let plural_s = if count == 1 { "" } else { "s" };
let plural_it_them = if count == 1 { "it" } else { "them" };
Layout(vec![
LayoutGroup::row(vec![TextLabel::new(header).bold(true).multiline(true).widget_instance()]),
LayoutGroup::row(vec![
TextLabel::new(format!(
"Sorry about that!\n\
This shouldn't happen, and we'd like to help.\n\
\n\
Click \"Download\" to save a copy of the affected file{plural_s},\n\
then please share {plural_it_them} with us so we can investigate:"
))
.multiline(true)
.widget_instance(),
]),
LayoutGroup::row(vec![
TextButton::new("Ask on Discord")
.icon("Volunteer")
.flush(true)
.on_update(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://discord.graphite.art".into(),
}
.into()
})
.widget_instance(),
]),
LayoutGroup::row(vec![
TextButton::new("Report on GitHub")
.icon("Bug")
.flush(true)
.on_update(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(),
}
.into()
})
.widget_instance(),
]),
LayoutGroup::row(vec![
TextLabel::new(
"In the meantime, you can keep working in the\n\
previous version of Graphite:",
)
.multiline(true)
.widget_instance(),
]),
LayoutGroup::row(vec![
TextButton::new("Sept. 2025 Release")
.icon("GraphiteLogo")
.flush(true)
.on_update(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://57130155.graphite.pages.dev/".into(),
}
.into()
})
.widget_instance(),
]),
LayoutGroup::row(vec![TextLabel::new(format!("Affected document{plural_s}:\n{list}")).multiline(true).widget_instance()]),
])
}
}
@@ -0,0 +1,84 @@
use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::prelude::*;
pub struct FailedToOpenDocumentDialog {
pub document_name: String,
}
impl DialogLayoutHolder for FailedToOpenDocumentDialog {
const ICON: &'static str = "Warning";
const TITLE: &'static str = "Failed to Open Document";
fn layout_buttons(&self) -> Layout {
let widgets = vec![TextButton::new("OK").emphasized(true).on_update(|_| FrontendMessage::DialogClose.into()).widget_instance()];
Layout(vec![LayoutGroup::row(widgets)])
}
}
impl LayoutHolder for FailedToOpenDocumentDialog {
fn layout(&self) -> Layout {
let header = if self.document_name.trim().is_empty() {
"The document couldn't be opened.".to_string()
} else {
format!("\"{}\" couldn't be opened.", self.document_name)
};
Layout(vec![
LayoutGroup::row(vec![TextLabel::new(header).bold(true).multiline(true).widget_instance()]),
LayoutGroup::row(vec![
TextLabel::new(
"Sorry about that!\n\
This shouldn't happen, and we'd like to help.\n\
\n\
Please share the file with us so we can investigate:",
)
.multiline(true)
.widget_instance(),
]),
LayoutGroup::row(vec![
TextButton::new("Ask on Discord")
.icon("Volunteer")
.flush(true)
.on_update(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://discord.graphite.art".into(),
}
.into()
})
.widget_instance(),
]),
LayoutGroup::row(vec![
TextButton::new("Report on GitHub")
.icon("Bug")
.flush(true)
.on_update(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://github.com/GraphiteEditor/Graphite/issues/new".into(),
}
.into()
})
.widget_instance(),
]),
LayoutGroup::row(vec![
TextLabel::new(
"In the meantime, you can keep working in the\n\
previous version of Graphite:",
)
.multiline(true)
.widget_instance(),
]),
LayoutGroup::row(vec![
TextButton::new("Sept. 2025 Release")
.icon("GraphiteLogo")
.flush(true)
.on_update(|_| {
FrontendMessage::TriggerVisitLink {
url: "https://57130155.graphite.pages.dev/".into(),
}
.into()
})
.widget_instance(),
]),
])
}
}
@@ -4,6 +4,8 @@ mod close_document_dialog;
mod confirm_restart_dialog;
mod demo_artwork_dialog;
mod error_dialog;
mod failed_to_load_documents_dialog;
mod failed_to_open_document_dialog;
mod licenses_dialog;
mod licenses_third_party_dialog;
@@ -14,5 +16,7 @@ pub use confirm_restart_dialog::ConfirmRestartDialog;
pub use demo_artwork_dialog::ARTWORK;
pub use demo_artwork_dialog::DemoArtworkDialog;
pub use error_dialog::ErrorDialog;
pub use failed_to_load_documents_dialog::FailedToLoadDocumentsDialog;
pub use failed_to_open_document_dialog::FailedToOpenDocumentDialog;
pub use licenses_dialog::LicensesDialog;
pub use licenses_third_party_dialog::LicensesThirdPartyDialog;