feat(block-editor): isolate session history and add save checkpoints

This commit is contained in:
Karim Jerbi committed 2026-10-13 15:35:24 +01:00
1 parent 535710e5d1
commit f39fb31137
15 files changed
+1002 -255

No files matched your search

+5 -7
View File
@@ -887,11 +887,9 @@ fn locked_palette_remove_refuses_a_block_with_an_open_bedit_session() {
);
}
/// Nested BEDIT sessions all count: the outer and the inner record are both
/// open, so both are refused — the whole session stack is handed to the
/// service, not just the active tab.
/// Switching replaces the open session; the active definition stays protected.
#[test]
fn locked_palette_remove_refuses_every_block_of_a_nested_bedit_session() {
fn locked_palette_remove_refuses_the_open_block_after_switching() {
let mut app = app();
let inner = "BO_NEST_INNER";
define_block(&mut app, inner);
@@ -915,10 +913,10 @@ fn locked_palette_remove_refuses_every_block_of_a_nested_bedit_session() {
begin_bedit(&mut app, inner_reference);
assert_eq!(
app.tabs[tab(&app)].block_edits.len(),
2,
"the nested session did not open"
1,
"switching must keep one session"
);
for name in [outer, inner] {
for name in [inner] {
palette_remove(&mut app, name);
assert!(record_exists(&app, name), "{name} went while open");
let expected = command_line_error(&format!(
+20 -3
View File
@@ -151,7 +151,7 @@ impl OpenCADStudio {
/// Close queued drawings until a dirty tab requires confirmation. Queue
/// entries use stable document ids because removing an earlier tab changes
/// every later vector index.
pub(super) fn continue_tab_close_queue(&mut self) -> Task<Message> {
pub(in crate::app) fn continue_tab_close_queue(&mut self) -> Task<Message> {
let mut tasks = Vec::new();
while let Some(tab_id) = self.pending_tab_closes.pop_front() {
let Some(idx) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
@@ -160,6 +160,13 @@ impl OpenCADStudio {
if self.tabs[idx].is_start {
continue;
}
if self.tabs[idx].active_block_edit.is_some() {
tasks.push(self.on_tab_close(idx));
if self.pending_block_close.is_some() || self.pending_close.is_some() {
break;
}
continue;
}
if self.tabs[idx].dirty {
self.pending_close = Some(crate::app::PendingClose::Tab(tab_id));
tasks.push(self.open_unsaved_dialog_window());
@@ -170,11 +177,21 @@ impl OpenCADStudio {
Task::batch(tasks)
}
pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
pub(in crate::app) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
// Start tab is fixed — close requests on it are no-ops.
if self.tabs.get(idx).map_or(false, |t| t.is_start) {
return Task::none();
}
if self
.tabs
.get(idx)
.is_some_and(|tab| tab.active_block_edit.is_some())
{
return self.request_block_edit_close(
idx,
crate::app::block_editor::BlockEditContinuation::Tab(self.tabs[idx].id),
);
}
// Closing a tab shifts indices / the active tab. The attribute
// editor holds a document-local handle into one tab, so drop it
// now rather than risk it applying to a different tab's document.
@@ -4344,7 +4361,7 @@ impl OpenCADStudio {
/// Close the attribute editor without applying, if it is open. Used when
/// the tab it belongs to closes or the active tab switches — its handle is
/// document-local and must not outlive its tab.
pub(super) fn cancel_attr_editor(&mut self) {
pub(in crate::app) fn cancel_attr_editor(&mut self) {
if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) {
self.active_modal = None;
self.reset_modal_geometry();
+8 -5
View File
@@ -215,6 +215,7 @@ impl OpenCADStudio {
match self.active_modal {
// Dismissing these via ✕ is the cancel/decline path.
Some(Unsaved) => self.pending_close = None,
Some(BlockEditChanges) => self.cancel_block_edit_close(),
Some(AssocPrompt) => self.mark_assoc_prompted(),
// Cancel: leave the layer (and its objects) untouched.
Some(LayerDeleteWarning) => self.layer_delete_pending = None,
@@ -3895,11 +3896,7 @@ impl OpenCADStudio {
Message::WindowCloseRequested(id) => {
if self.main_window == Some(id) {
if self.tabs.iter().any(|t| t.dirty) {
self.pending_close = Some(super::PendingClose::Quit);
return self.open_unsaved_dialog_window();
}
return self.exit_app();
return self.request_quit_with_block_edits();
}
Task::none()
}
@@ -10434,6 +10431,12 @@ impl OpenCADStudio {
Message::UnsavedDialogDiscard => self.on_unsaved_dialog_discard(),
Message::UnsavedDialogSave => self.on_unsaved_dialog_save(),
Message::BlockEditChangesSave => self.resolve_block_edit_close(true),
Message::BlockEditChangesDiscard => self.resolve_block_edit_close(false),
Message::BlockEditChangesCancel => {
self.cancel_block_edit_close();
Task::none()
}
Message::AecDropSameVersion => self.on_aec_drop_same_version(),
Message::AecDropProceed => self.on_aec_drop_proceed(),
+12
View File
@@ -6446,6 +6446,12 @@ properties={:.1}ms picked={}",
}
pub(crate) fn on_layout_switch(&mut self, name: String) -> Task<Message> {
if self.tabs[self.active_tab].active_block_edit.is_some() {
return self.request_block_edit_close(
self.active_tab,
crate::app::block_editor::BlockEditContinuation::Layout(name),
);
}
self.on_layout_switch_inner(name, false)
}
@@ -6508,6 +6514,12 @@ properties={:.1}ms picked={}",
/// define a view and then returns to its paper layout. This is the only
/// layout transition allowed to preserve an active command.
pub(crate) fn on_layout_switch_preserving_command(&mut self, name: String) -> Task<Message> {
if self.tabs[self.active_tab].active_block_edit.is_some() {
return self.request_block_edit_close(
self.active_tab,
crate::app::block_editor::BlockEditContinuation::Layout(name),
);
}
self.on_layout_switch_inner(name, true)
}