Integrate typed port authoring and reviewable exchange workflows
This commit is contained in:
+199
-29
@@ -5,6 +5,7 @@ use serde_json::{Value, json};
|
||||
use std::{collections::BTreeSet, path::PathBuf, sync::Arc};
|
||||
|
||||
type FileSnapshot = (PathBuf, Project, Arc<[u8]>);
|
||||
type LoadedSnapshot = (PathBuf, persistence::DecodeResult, Arc<[u8]>);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Page {
|
||||
@@ -64,6 +65,7 @@ pub enum Message {
|
||||
CreateKind(Kind),
|
||||
Add(bool),
|
||||
AddRelation,
|
||||
PortAction(crate::port_workflow::Action),
|
||||
AddRelationKind(RelationKind, bool),
|
||||
InspectorTab(crate::navigation::InspectorTab),
|
||||
RevealInDiagram(Id, Id),
|
||||
@@ -107,7 +109,7 @@ pub enum Message {
|
||||
SourceNavigation(crate::source_navigation::Event),
|
||||
Save(bool),
|
||||
Saved(Result<Option<FileSnapshot>, String>),
|
||||
Loaded(Result<Option<FileSnapshot>, String>),
|
||||
Loaded(Result<Option<LoadedSnapshot>, String>),
|
||||
Request(Pending),
|
||||
Discard,
|
||||
Cancel,
|
||||
@@ -157,6 +159,7 @@ pub struct App {
|
||||
pub source_jump: Option<text_editor::Cursor>,
|
||||
pub source_navigation: crate::source_navigation::State,
|
||||
pub model_issues: Vec<validation::Issue>,
|
||||
pub migration: Option<persistence::MigrationReport>,
|
||||
source_past: Vec<String>,
|
||||
source_future: Vec<String>,
|
||||
source_version: u64,
|
||||
@@ -176,10 +179,19 @@ impl App {
|
||||
let source = language::format(&project);
|
||||
let model_issues = validate(&project);
|
||||
let selected = project.id;
|
||||
let populated = |layer| {
|
||||
project.elements.values().any(|e| e.layer() == layer)
|
||||
|| project.diagrams.values().any(|d| d.layer == layer)
|
||||
};
|
||||
let layer = if !populated(Layer::Oa) && populated(Layer::Sa) {
|
||||
Layer::Sa
|
||||
} else {
|
||||
Layer::Oa
|
||||
};
|
||||
let active_diagram = project
|
||||
.diagrams
|
||||
.values()
|
||||
.filter(|d| d.layer == Layer::Oa)
|
||||
.filter(|d| d.layer == layer)
|
||||
.min_by_key(|d| (d.projection != ProjectionKind::Allocation, d.id))
|
||||
.map(|d| d.id);
|
||||
let draft = Draft::from_project(&project, selected);
|
||||
@@ -190,13 +202,17 @@ impl App {
|
||||
guide: crate::guide::State::default(),
|
||||
behavior: Default::default(),
|
||||
inspector_tab: Default::default(),
|
||||
layer: Layer::Oa,
|
||||
layer,
|
||||
selected,
|
||||
selected_node: None,
|
||||
draft,
|
||||
active_diagram,
|
||||
search: String::new(),
|
||||
create_kind: Kind::Activity,
|
||||
create_kind: if layer == Layer::Sa {
|
||||
Kind::Function
|
||||
} else {
|
||||
Kind::Activity
|
||||
},
|
||||
status: "Coastal rescue example · explore both engineering perspectives".into(),
|
||||
busy: false,
|
||||
pending: None,
|
||||
@@ -217,6 +233,7 @@ impl App {
|
||||
source_jump: None,
|
||||
source_navigation: crate::source_navigation::State::default(),
|
||||
model_issues,
|
||||
migration: None,
|
||||
source_past: vec![],
|
||||
source_future: vec![],
|
||||
source_version: 0,
|
||||
@@ -316,9 +333,12 @@ impl App {
|
||||
if !self.source_dirty() {
|
||||
return true;
|
||||
}
|
||||
match language::parse(&self.source_editor.text()) {
|
||||
Ok(project) => match self.session.execute(Command::Replace(project)) {
|
||||
match language::parse_with_report(&self.source_editor.text()) {
|
||||
Ok(decoded) => match self.session.execute(Command::Replace(decoded.project)) {
|
||||
Ok(_) => {
|
||||
if decoded.migration.is_some() {
|
||||
self.migration = decoded.migration;
|
||||
}
|
||||
self.sync();
|
||||
self.status = "Text applied as one model change".into();
|
||||
true
|
||||
@@ -423,22 +443,7 @@ impl App {
|
||||
self.fit();
|
||||
}
|
||||
fn diagram_for(&self, layer: Layer, element: Id) -> Option<Id> {
|
||||
self.project()
|
||||
.diagrams
|
||||
.values()
|
||||
.filter(|d| d.layer == layer)
|
||||
.min_by_key(|d| {
|
||||
(
|
||||
!self
|
||||
.project()
|
||||
.elements
|
||||
.get(&element)
|
||||
.is_none_or(|e| d.projection.includes(e.kind())),
|
||||
!d.nodes.values().any(|n| n.element == element),
|
||||
d.projection != ProjectionKind::Allocation,
|
||||
)
|
||||
})
|
||||
.map(|d| d.id)
|
||||
preferred_diagram(self.project(), self.active_diagram, layer, element)
|
||||
}
|
||||
fn select(&mut self, id: Id) {
|
||||
if self.selected == id {
|
||||
@@ -714,13 +719,14 @@ impl App {
|
||||
}
|
||||
if let Some(mut workflow) = self.workflow.take() {
|
||||
let transition_focus = match &workflow {
|
||||
crate::workflow::Workflow::Ports(state) => Some(state.focus()),
|
||||
crate::workflow::Workflow::Transition(t) => {
|
||||
t.plan.changes.first().map(|c| c.target)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
match workflow.update(self.project(), event) {
|
||||
Ok(Some(candidate)) => {
|
||||
Ok(Some(mut candidate)) => {
|
||||
let focus = transition_focus
|
||||
.or_else(|| {
|
||||
candidate
|
||||
@@ -736,12 +742,21 @@ impl App {
|
||||
.find(|id| !self.project().traces.contains_key(id))
|
||||
.copied()
|
||||
});
|
||||
let destination = focus.and_then(|id| {
|
||||
candidate.layer_of(id).and_then(|layer| {
|
||||
preferred_diagram(&candidate, self.active_diagram, layer, id)
|
||||
})
|
||||
});
|
||||
if let (Some(id), Some(diagram)) = (focus, destination) {
|
||||
reveal_port_owner(&mut candidate, diagram, id);
|
||||
}
|
||||
if self.execute(Command::Replace(candidate)) {
|
||||
if let Some(id) = focus {
|
||||
self.selected = id;
|
||||
if let Some(layer) = self.project().layer_of(id) {
|
||||
self.layer = layer;
|
||||
self.active_diagram = self.diagram_for(layer, id);
|
||||
self.active_diagram =
|
||||
destination.or_else(|| self.diagram_for(layer, id));
|
||||
}
|
||||
self.refresh_draft();
|
||||
}
|
||||
@@ -837,6 +852,16 @@ impl App {
|
||||
self.add_relation();
|
||||
}
|
||||
}
|
||||
Message::PortAction(action) => {
|
||||
if self.apply() {
|
||||
match crate::port_workflow::State::new(self.project(), self.selected, action) {
|
||||
Ok(state) => {
|
||||
self.workflow = Some(crate::workflow::Workflow::Ports(Box::new(state)))
|
||||
}
|
||||
Err(error) => self.status = error,
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::AddRelationKind(kind, incoming) => {
|
||||
if self.apply() {
|
||||
self.workflow = Some(crate::workflow::Workflow::relationship_kind(
|
||||
@@ -1363,8 +1388,13 @@ impl App {
|
||||
Message::Loaded(result) => {
|
||||
self.busy = false;
|
||||
match result {
|
||||
Ok(Some((path, p, bytes))) => {
|
||||
self.reset(p, Some(path));
|
||||
Ok(Some((path, decoded, bytes))) => {
|
||||
self.reset(decoded.project, Some(path));
|
||||
self.migration = decoded.migration;
|
||||
if self.migration.is_some() {
|
||||
self.status = "Imported earlier schema · review migration details in Model checks before saving".into();
|
||||
self.page = Page::Validation;
|
||||
}
|
||||
self.file_snapshot = Some(bytes);
|
||||
}
|
||||
Ok(None) => self.status = "Open cancelled".into(),
|
||||
@@ -1427,6 +1457,10 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
fn add_element(&mut self, child: bool) {
|
||||
if self.create_kind == Kind::Port {
|
||||
self.status = "Select an SA function or participant, then choose a typed port action in Properties.".into();
|
||||
return;
|
||||
}
|
||||
let allocated = child
|
||||
&& self.create_kind.is_work()
|
||||
&& self
|
||||
@@ -1848,10 +1882,146 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn read_snapshot(path: PathBuf) -> Result<FileSnapshot, String> {
|
||||
fn preferred_diagram(p: &Project, active: Option<Id>, layer: Layer, element: Id) -> Option<Id> {
|
||||
if let Some(owner) = p
|
||||
.elements
|
||||
.get(&element)
|
||||
.filter(|e| e.kind() == Kind::Port)
|
||||
.and_then(|e| e.parent)
|
||||
{
|
||||
return p
|
||||
.diagrams
|
||||
.values()
|
||||
.filter(|d| d.layer == layer)
|
||||
.min_by_key(|d| {
|
||||
let view = project_view(p, d.id);
|
||||
(
|
||||
!d.projection.includes(p.elements[&owner].kind()),
|
||||
!view.ports.iter().any(|port| port.element == element),
|
||||
!d.nodes.values().any(|node| node.element == owner),
|
||||
active != Some(d.id),
|
||||
)
|
||||
})
|
||||
.map(|d| d.id);
|
||||
}
|
||||
p.diagrams
|
||||
.values()
|
||||
.filter(|d| d.layer == layer)
|
||||
.min_by_key(|d| {
|
||||
(
|
||||
!p.elements
|
||||
.get(&element)
|
||||
.is_none_or(|e| d.projection.includes(e.kind())),
|
||||
!d.nodes.values().any(|n| n.element == element),
|
||||
d.projection != ProjectionKind::Allocation,
|
||||
)
|
||||
})
|
||||
.map(|d| d.id)
|
||||
}
|
||||
fn reveal_port_owner(p: &mut Project, diagram: Id, element: Id) {
|
||||
let Some(owner) = p
|
||||
.elements
|
||||
.get(&element)
|
||||
.filter(|e| e.kind() == Kind::Port)
|
||||
.and_then(|e| e.parent)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(owner_node) = p.diagrams[&diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.find(|n| n.element == owner)
|
||||
.map(|n| n.id)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let unfold: Vec<_> = p.diagrams[&diagram]
|
||||
.nodes
|
||||
.values()
|
||||
.filter(|n| {
|
||||
n.collapsed && projection::descendant_nodes(p, diagram, n.id).contains(&owner_node)
|
||||
})
|
||||
.map(|n| n.id)
|
||||
.collect();
|
||||
for id in unfold {
|
||||
p.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.collapsed = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn read_snapshot(path: PathBuf) -> Result<LoadedSnapshot, String> {
|
||||
let bytes = std::fs::read(&path).map_err(|e| e.to_string())?;
|
||||
let project = persistence::decode(&bytes).map_err(|e| e.to_string())?;
|
||||
Ok((path, project, Arc::from(bytes)))
|
||||
let decoded = persistence::decode_with_report(&bytes).map_err(|e| e.to_string())?;
|
||||
Ok((path, decoded, Arc::from(bytes)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod port_view_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn filtered_active_view_cannot_hide_a_created_port_when_eligible_owner_view_is_folded() {
|
||||
let mut p = Project::empty();
|
||||
let system = p
|
||||
.add_element(Kind::System, Layer::Sa, "System", None)
|
||||
.unwrap();
|
||||
let function = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Function", None)
|
||||
.unwrap();
|
||||
p.relate(RelationKind::Allocation, "Performs", system, function);
|
||||
let architecture = p.ensure_diagram(Layer::Sa);
|
||||
p.diagrams.get_mut(&architecture).unwrap().projection = ProjectionKind::Allocation;
|
||||
let container = p.place(architecture, system, Position { x: 40., y: 40. });
|
||||
p.place(architecture, function, Position { x: 70., y: 130. });
|
||||
let capabilities = Id::new_v4();
|
||||
p.diagrams.insert(
|
||||
capabilities,
|
||||
Diagram {
|
||||
id: capabilities,
|
||||
alias: "filtered_view".into(),
|
||||
name: "Filtered view".into(),
|
||||
description: String::new(),
|
||||
layer: Layer::Sa,
|
||||
projection: ProjectionKind::Capabilities,
|
||||
nodes: Default::default(),
|
||||
},
|
||||
);
|
||||
p.place(capabilities, system, Position { x: 40., y: 40. });
|
||||
p.place(capabilities, function, Position { x: 70., y: 130. });
|
||||
p.diagrams
|
||||
.get_mut(&architecture)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&container)
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let mut app = App::with_project(p.clone());
|
||||
let _ = app.update(Message::Select(function));
|
||||
app.active_diagram = Some(capabilities);
|
||||
app.refresh_draft();
|
||||
let _ = app.update(Message::PortAction(crate::port_workflow::Action::Output));
|
||||
let _ = app.update(Message::Workflow(crate::workflow::Event::Apply));
|
||||
assert!(app.workflow.is_none(), "{}", app.status);
|
||||
assert_eq!(app.active_diagram, Some(architecture));
|
||||
assert!(
|
||||
project_view(app.project(), architecture)
|
||||
.ports
|
||||
.iter()
|
||||
.any(|port| port.element == app.selected)
|
||||
);
|
||||
assert!(!app.project().diagrams[&architecture].nodes[&container].collapsed);
|
||||
let _ = app.update(Message::Undo);
|
||||
assert_eq!(
|
||||
app.project(),
|
||||
&p,
|
||||
"Creation and the eligible owner reveal remain one transaction"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user