Integrate release recovery, semantic review, coverage and Windows packaging
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
mod checkpoints;
|
||||
mod hosted;
|
||||
use crate::forms::Draft;
|
||||
use crate::{
|
||||
@@ -260,6 +261,7 @@ pub struct RecoverySnapshot {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Page {
|
||||
Home,
|
||||
Diagram,
|
||||
Catalog,
|
||||
Behavior,
|
||||
@@ -281,6 +283,8 @@ impl std::fmt::Display for Choice {
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Pending {
|
||||
Recent,
|
||||
Recover(Id),
|
||||
HostedOpen,
|
||||
New,
|
||||
Demo,
|
||||
@@ -301,6 +305,10 @@ pub struct FilePrompt {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
Home(crate::home::Event),
|
||||
CheckpointTick,
|
||||
CheckpointWritten(Id, Result<(), String>),
|
||||
CheckpointLoaded(Result<Arc<crate::recovery::Snapshot>, String>),
|
||||
Hosted(crate::hosted::Event),
|
||||
PortAnchor(crate::port_edit::Event),
|
||||
Connection(crate::connection_edit::Event),
|
||||
@@ -413,6 +421,10 @@ pub enum Message {
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub home: crate::home::State,
|
||||
pub recovery: crate::recovery::State,
|
||||
recent_target: Option<PathBuf>,
|
||||
checkpoint_close: Option<window::Id>,
|
||||
pub hosted: crate::hosted::State,
|
||||
pub connection: Option<crate::connection_edit::Session>,
|
||||
pub model_menu: Option<crate::model_menu::Opened>,
|
||||
@@ -424,6 +436,8 @@ pub struct App {
|
||||
pub exact_strings: bool,
|
||||
pub document: Document,
|
||||
pub page: Page,
|
||||
/// Home resumes this page; explicit Architecture navigation remains separate.
|
||||
last_workspace_page: Page,
|
||||
pub guide: crate::guide::State,
|
||||
pub behavior: crate::behavior::State,
|
||||
pub data_state: crate::data_view::State,
|
||||
@@ -473,7 +487,11 @@ pub struct App {
|
||||
|
||||
impl App {
|
||||
pub fn new() -> Self {
|
||||
let mut app = Self::with_project(Project::demo());
|
||||
let mut app = Self::with_project(Project::empty());
|
||||
app.home = crate::home::State::load_default();
|
||||
app.recovery = crate::recovery::State::load_default();
|
||||
app.page = Page::Home;
|
||||
app.status = "Create a model, open a project or explore the example.".into();
|
||||
app.fit();
|
||||
app
|
||||
}
|
||||
@@ -502,7 +520,11 @@ impl App {
|
||||
let draft = Draft::from_project(&project, selected);
|
||||
let explorer_index = crate::explorer::Index::new(&project, layer);
|
||||
let explorer_collapsed = project.elements.values().filter_map(|e| e.parent).collect();
|
||||
Self {
|
||||
let mut app = Self {
|
||||
home: Default::default(),
|
||||
recovery: Default::default(),
|
||||
recent_target: None,
|
||||
checkpoint_close: None,
|
||||
connection: None,
|
||||
model_menu: None,
|
||||
source_tree: Default::default(),
|
||||
@@ -513,6 +535,7 @@ impl App {
|
||||
exact_strings: false,
|
||||
document,
|
||||
page: Page::Diagram,
|
||||
last_workspace_page: Page::Diagram,
|
||||
guide: crate::guide::State::default(),
|
||||
behavior: Default::default(),
|
||||
data_state: Default::default(),
|
||||
@@ -562,7 +585,10 @@ impl App {
|
||||
explorer_group: None,
|
||||
explorer_page: 0,
|
||||
workflow: None,
|
||||
}
|
||||
};
|
||||
app.route_state_diagram_workspace();
|
||||
app.last_workspace_page = app.page;
|
||||
app
|
||||
}
|
||||
pub fn project(&self) -> &Project {
|
||||
self.document.project()
|
||||
@@ -586,6 +612,7 @@ impl App {
|
||||
}
|
||||
pub fn dirty(&self) -> bool {
|
||||
self.document.dirty()
|
||||
|| self.workflow.is_some()
|
||||
|| self.draft_dirty()
|
||||
|| self.source_dirty()
|
||||
|| self.behavior.has_pending()
|
||||
@@ -844,6 +871,9 @@ impl App {
|
||||
}
|
||||
}
|
||||
fn reset(&mut self, project: Project, path: Option<PathBuf>) {
|
||||
self.retire_checkpoint();
|
||||
let home = std::mem::take(&mut self.home);
|
||||
let recovery = std::mem::take(&mut self.recovery);
|
||||
let window_size = self.window_size;
|
||||
let canvas_size = self.canvas_size;
|
||||
let source_version = self.source_version + 1;
|
||||
@@ -852,6 +882,8 @@ impl App {
|
||||
let mut hosted = std::mem::take(&mut self.hosted);
|
||||
hosted.leave_document();
|
||||
*self = Self::with_project(project);
|
||||
self.home = home;
|
||||
self.recovery = recovery;
|
||||
self.hosted = hosted;
|
||||
self.window_size = window_size;
|
||||
self.canvas_size = canvas_size;
|
||||
@@ -923,6 +955,7 @@ impl App {
|
||||
}
|
||||
pub fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch([
|
||||
iced::time::every(std::time::Duration::from_secs(10)).map(|_| Message::CheckpointTick),
|
||||
window::close_requests().map(|id| Message::Request(Pending::Close(id))),
|
||||
window::resize_events().map(|(_, size)| Message::WindowResized(size)),
|
||||
iced::event::listen_with(|event, status, _| {
|
||||
@@ -992,6 +1025,9 @@ impl App {
|
||||
&& !matches!(
|
||||
message,
|
||||
Message::Menu(_)
|
||||
| Message::CheckpointWritten(..)
|
||||
| Message::CheckpointLoaded(..)
|
||||
| Message::CheckpointTick
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::Lint(_)
|
||||
@@ -1030,7 +1066,9 @@ impl App {
|
||||
if self.busy
|
||||
&& !matches!(
|
||||
message,
|
||||
Message::Saved(_)
|
||||
Message::CheckpointWritten(..)
|
||||
| Message::CheckpointLoaded(..)
|
||||
| Message::Saved(_)
|
||||
| Message::Hosted(crate::hosted::Event::Completed(..))
|
||||
| Message::Loaded(_)
|
||||
| Message::IncludesLoaded(_, _)
|
||||
@@ -1073,7 +1111,10 @@ impl App {
|
||||
}
|
||||
let modal_event = matches!(
|
||||
message,
|
||||
Message::WindowResized(_)
|
||||
Message::CheckpointWritten(..)
|
||||
| Message::CheckpointTick
|
||||
| Message::CheckpointLoaded(..)
|
||||
| Message::WindowResized(_)
|
||||
| Message::CanvasResized(_)
|
||||
| Message::SourceViewport
|
||||
| Message::Lint(_)
|
||||
@@ -1145,7 +1186,14 @@ impl App {
|
||||
let previous_layer = self.layer;
|
||||
let entering_catalog =
|
||||
self.page != Page::Catalog && matches!(message, Message::Page(Page::Catalog));
|
||||
if self.page != Page::Home {
|
||||
self.last_workspace_page = self.page;
|
||||
}
|
||||
let mut task = self.handle(message);
|
||||
self.route_state_diagram_workspace();
|
||||
if self.page != Page::Home {
|
||||
self.last_workspace_page = self.page;
|
||||
}
|
||||
if let Some(context) = self.topology_context() {
|
||||
self.behavior
|
||||
.topology
|
||||
@@ -1206,6 +1254,16 @@ impl App {
|
||||
}
|
||||
fn handle(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Home(event) => return self.home_event(event),
|
||||
Message::CheckpointTick => return self.checkpoint_tick(),
|
||||
Message::CheckpointWritten(id, result) => {
|
||||
self.checkpoint_written(id, result);
|
||||
if let Some(window) = self.checkpoint_close.take() {
|
||||
self.retire_checkpoint();
|
||||
return window::close(window);
|
||||
}
|
||||
}
|
||||
Message::CheckpointLoaded(result) => return self.checkpoint_loaded(result),
|
||||
Message::Hosted(event) => return self.hosted_event(event),
|
||||
Message::ScenarioCanvas(event) => {
|
||||
use crate::behavior::scenario::{self, CanvasEvent};
|
||||
@@ -1600,6 +1658,27 @@ impl App {
|
||||
}
|
||||
Message::Page(page) => {
|
||||
if page != Page::Text || !self.draft_dirty() || self.apply() {
|
||||
if page == Page::Diagram
|
||||
&& self.active_diagram.is_some_and(|id| {
|
||||
self.project()
|
||||
.diagrams
|
||||
.get(&id)
|
||||
.is_some_and(|d| d.projection.state_machine().is_some())
|
||||
})
|
||||
{
|
||||
// Architecture remains reachable from a state workspace.
|
||||
// Its picker can still open a saved state view explicitly.
|
||||
self.active_diagram = self
|
||||
.project()
|
||||
.diagrams
|
||||
.values()
|
||||
.filter(|d| {
|
||||
d.layer == self.layer && d.projection.state_machine().is_none()
|
||||
})
|
||||
.min_by_key(|d| (d.projection != ProjectionKind::Allocation, d.id))
|
||||
.map(|d| d.id);
|
||||
self.fit();
|
||||
}
|
||||
if page == Page::Behavior && self.page != Page::Behavior {
|
||||
let previous_state_view = self.state_view_identity();
|
||||
if let Some(owner) =
|
||||
@@ -2545,7 +2624,9 @@ impl App {
|
||||
self.pending_recovery = None;
|
||||
}
|
||||
if receipt.snapshot.bundle.entry != self.document.bundle().entry {
|
||||
if receipt.revision != self.document.revision() {
|
||||
if receipt.revision != self.document.revision()
|
||||
|| self.source_dirty()
|
||||
{
|
||||
self.status = format!(
|
||||
"Snapshot saved to {}. Newer edits retain the current source location.",
|
||||
receipt.path.display()
|
||||
@@ -2553,8 +2634,9 @@ impl App {
|
||||
self.continue_after_save = false;
|
||||
return Task::none();
|
||||
}
|
||||
if let Err(error) =
|
||||
self.document.apply_bundle(receipt.snapshot.bundle.clone())
|
||||
if let Err(error) = self
|
||||
.document
|
||||
.adopt_saved_entry(&receipt.snapshot, receipt.revision)
|
||||
{
|
||||
self.status = error.to_string();
|
||||
self.continue_after_save = false;
|
||||
@@ -2568,6 +2650,10 @@ impl App {
|
||||
self.hosted.review = None;
|
||||
self.hosted.uncertain = None;
|
||||
self.document.mark_saved(receipt.snapshot);
|
||||
self.remember_local();
|
||||
if !self.dirty() {
|
||||
self.retire_checkpoint();
|
||||
}
|
||||
self.prune_closed_files(!self.document.dirty());
|
||||
self.status = "All source files saved".into();
|
||||
} else {
|
||||
@@ -2608,6 +2694,10 @@ impl App {
|
||||
match result {
|
||||
Ok(Some(LoadedSnapshot::Legacy(path, decoded, _bytes))) => {
|
||||
self.reset(decoded.project, None);
|
||||
let name = self.project().name.clone();
|
||||
if let Err(error) = self.home.remember(&path, &name) {
|
||||
self.home.notice = Some(error);
|
||||
}
|
||||
self.migration = decoded.migration;
|
||||
if self.migration.is_some() {
|
||||
self.status = "Imported earlier schema · review migration details in Model checks before saving".into();
|
||||
@@ -2905,6 +2995,11 @@ impl App {
|
||||
fn proceed(&mut self, pending: Pending) -> Task<Message> {
|
||||
self.pending = None;
|
||||
match pending {
|
||||
Pending::Recent => self
|
||||
.recent_target
|
||||
.take()
|
||||
.map_or_else(Task::none, |path| self.load_from(path)),
|
||||
Pending::Recover(id) => self.load_checkpoint(id),
|
||||
Pending::HostedOpen => self.open_hosted(),
|
||||
Pending::New => {
|
||||
self.reset(Project::empty(), None);
|
||||
@@ -2915,7 +3010,16 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
Pending::Open => self.choose_file(FileOperation::Open),
|
||||
Pending::Close(id) => window::close(id),
|
||||
Pending::Close(id) => {
|
||||
self.retire_checkpoint();
|
||||
if self.recovery.writing {
|
||||
self.checkpoint_close = Some(id);
|
||||
self.busy = true;
|
||||
Task::none()
|
||||
} else {
|
||||
window::close(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn choose_file(&mut self, operation: FileOperation) -> Task<Message> {
|
||||
@@ -2990,6 +3094,25 @@ impl App {
|
||||
)
|
||||
}
|
||||
fn save_to(&mut self, path: PathBuf) -> Task<Message> {
|
||||
if !path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
|
||||
{
|
||||
let result = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.ok_or_else(|| "Choose a portable .arcadia entry file name.".to_owned())
|
||||
.and_then(|entry| {
|
||||
self.document
|
||||
.check_save_entry(entry)
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
if let Err(error) = result {
|
||||
self.status = error;
|
||||
self.continue_after_save = false;
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
self.busy = true;
|
||||
let snapshot = self.document.snapshot();
|
||||
let document = self.canvas_document;
|
||||
@@ -3084,6 +3207,9 @@ impl App {
|
||||
}
|
||||
self.pending_recovery = pending_recovery;
|
||||
self.disk_project = Some(disk);
|
||||
if self.source_workspace.applied {
|
||||
self.remember_local();
|
||||
}
|
||||
}
|
||||
|
||||
fn prune_closed_files(&mut self, prune_expectations: bool) {
|
||||
@@ -3731,6 +3857,10 @@ fn read_snapshot(path: PathBuf) -> Result<LoadedSnapshot, String> {
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "app/save_as_tests.rs"]
|
||||
mod save_as_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod source_project_tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user