Compose transition lifecycle and preserve authored attachments in both directions
This commit is contained in:
@@ -13,6 +13,7 @@ use iced::{
|
||||
use oa_core::{transitions::*, *};
|
||||
use std::collections::BTreeSet;
|
||||
mod communication;
|
||||
mod counterparts;
|
||||
pub(crate) mod recovery;
|
||||
#[cfg(test)]
|
||||
mod transition_tests;
|
||||
@@ -34,6 +35,7 @@ pub enum Event {
|
||||
Scope(String),
|
||||
Destination(Choice),
|
||||
MappingTarget(Id, Choice),
|
||||
MappingQuery(Id, String),
|
||||
MappingKind(Id, Kind),
|
||||
CommunicationPort(Id, bool, communication::PortMenuChoice),
|
||||
OperationOmission(Id, bool),
|
||||
@@ -274,6 +276,7 @@ pub struct Transition {
|
||||
pub request: TransitionRequest,
|
||||
pub plan: TransitionPlan,
|
||||
communication_ports: std::collections::BTreeMap<Id, communication::PortDraft>,
|
||||
counterpart_queries: std::collections::BTreeMap<Id, String>,
|
||||
}
|
||||
impl Transition {
|
||||
fn presentation(&self, p: &Project) -> (Project, Vec<Id>) {
|
||||
@@ -516,6 +519,7 @@ impl Workflow {
|
||||
request,
|
||||
plan,
|
||||
communication_ports: Default::default(),
|
||||
counterpart_queries: Default::default(),
|
||||
})))
|
||||
}
|
||||
/// Return a complete accepted candidate only on explicit Apply. Intermediate
|
||||
@@ -620,6 +624,12 @@ impl Workflow {
|
||||
_ => {}
|
||||
},
|
||||
Self::Transition(d) => {
|
||||
if let Event::MappingQuery(source, query) = event {
|
||||
d.counterpart_queries.insert(source, query);
|
||||
// Searching is presentation only: preserve reviewed decisions,
|
||||
// preview identities and the exact recovery snapshot.
|
||||
return Ok(None);
|
||||
}
|
||||
match &event {
|
||||
Event::OperationOmission(source, enabled) => {
|
||||
d.request
|
||||
@@ -1149,48 +1159,17 @@ impl Workflow {
|
||||
content=content.push(text("The SA operation remains unchanged. Correlated replies derive this Call's abstraction.").size(11));
|
||||
}
|
||||
let mapping = d.request.mappings.iter().find(|m| m.source == source);
|
||||
let existing = p
|
||||
.traces
|
||||
.values()
|
||||
.filter_map(|t| {
|
||||
if d.request.direction == TransitionDirection::OaToSa && t.oa == source
|
||||
{
|
||||
Some(t.sa)
|
||||
} else if d.request.direction == TransitionDirection::SaToOa
|
||||
&& t.sa == source
|
||||
{
|
||||
Some(t.oa)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let target = mapping.map(|m| m.target).unwrap_or_else(|| {
|
||||
if existing.len() == 1 {
|
||||
Some(existing[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let target_choice = counterparts::target(p, source, &d.request);
|
||||
let target = target_choice.flatten();
|
||||
let query = d
|
||||
.counterpart_queries
|
||||
.get(&source)
|
||||
.map_or("", String::as_str);
|
||||
let targets = counterparts::choices(p, source, d.request.direction, query);
|
||||
let no_matches = targets.len() == 1;
|
||||
let selected = target_choice.map(|target| {
|
||||
counterparts::selected(p, source, d.request.direction, target)
|
||||
});
|
||||
let mut targets = vec![Choice {
|
||||
id: Id::nil(),
|
||||
label: "Create new counterpart".into(),
|
||||
}];
|
||||
targets.extend(
|
||||
p.elements
|
||||
.keys()
|
||||
.chain(p.relations.keys())
|
||||
.filter(|id| {
|
||||
p.layer_of(**id) == Some(d.request.direction.target_layer())
|
||||
&& p.elements.contains_key(&source)
|
||||
== p.elements.contains_key(id)
|
||||
})
|
||||
.map(|id| choice(p, *id)),
|
||||
);
|
||||
let selected = targets
|
||||
.iter()
|
||||
.find(|c| Some(c.id) == target.or(Some(Id::nil())))
|
||||
.cloned();
|
||||
let mut item = column![
|
||||
text(format!(
|
||||
"{} · {}",
|
||||
@@ -1201,14 +1180,29 @@ impl Workflow {
|
||||
.unwrap_or("Relationship")
|
||||
))
|
||||
.size(14),
|
||||
text_input("Find counterpart by name or @alias", query)
|
||||
.on_input(move |query| Message::Workflow(Event::MappingQuery(
|
||||
source, query
|
||||
)))
|
||||
.size(12)
|
||||
.padding(8),
|
||||
pick_list(targets, selected, move |v| Message::Workflow(
|
||||
Event::MappingTarget(source, v)
|
||||
))
|
||||
.placeholder("Choose a counterpart")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(7);
|
||||
if no_matches {
|
||||
let message = if query.trim().is_empty() {
|
||||
"No compatible existing counterparts. Create a new one."
|
||||
} else {
|
||||
"No matching existing counterparts. Clear the search or create a new one."
|
||||
};
|
||||
item = item.push(text(message).size(11));
|
||||
}
|
||||
if target.is_none()
|
||||
&& let Some(e) = p.elements.get(&source)
|
||||
{
|
||||
@@ -1414,16 +1408,6 @@ impl Workflow {
|
||||
.color(style::MUTED),
|
||||
);
|
||||
}
|
||||
content = content.push(
|
||||
text(format!(
|
||||
"{} changes · {} required boundary choices · {} unresolved conflicts · {} modeling warnings",
|
||||
d.plan.changes.len(),
|
||||
d.plan.conflicts.iter().filter(|c| d.boundary_choice(p, c)).count(),
|
||||
d.plan.conflicts.iter().filter(|c| !d.boundary_choice(p, c)).count(),
|
||||
d.plan.warnings.len()
|
||||
))
|
||||
.size(12),
|
||||
);
|
||||
}
|
||||
}
|
||||
let enabled = match self {
|
||||
@@ -1440,12 +1424,50 @@ impl Workflow {
|
||||
.color(iced::Color::from_rgb8(178, 54, 58)),
|
||||
);
|
||||
}
|
||||
column![
|
||||
scrollable(content.padding(iced::Padding {
|
||||
right: 14.,
|
||||
..Default::default()
|
||||
}))
|
||||
.height(480),
|
||||
let mut footer = column![].spacing(8);
|
||||
if let Self::Transition(d) = self {
|
||||
let boundary_choices = d
|
||||
.plan
|
||||
.conflicts
|
||||
.iter()
|
||||
.filter(|c| d.boundary_choice(p, c))
|
||||
.count();
|
||||
footer = footer.push(
|
||||
text(format!(
|
||||
"{} reviewed items · {} boundary choices · {} conflicts · {} warnings",
|
||||
d.plan.changes.len(),
|
||||
boundary_choices,
|
||||
d.plan.conflicts.len() - boundary_choices,
|
||||
d.plan.warnings.len()
|
||||
))
|
||||
.size(12),
|
||||
);
|
||||
let guidance = if d.destination_error(p).is_some() {
|
||||
Some("Choose a compatible diagram in Show result in, above the mappings.".into())
|
||||
} else if boundary_choices > 0 {
|
||||
Some("Choose the counterpart kind in each mapping that needs a system boundary decision.".into())
|
||||
} else if let Some(field) = d.plan.conflicts.iter().find_map(|c| c.field) {
|
||||
let field = match field {
|
||||
TransitionField::Name => "Name",
|
||||
TransitionField::Description => "Description",
|
||||
};
|
||||
Some(format!(
|
||||
"Review {field} below the mappings: choose Keep target, Take source, or merged text."
|
||||
))
|
||||
} else if !d.plan.conflicts.is_empty() {
|
||||
Some("Review the conflict messages below the mappings and complete the required decisions.".into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(guidance) = guidance {
|
||||
footer = footer.push(
|
||||
text(guidance)
|
||||
.size(12)
|
||||
.color(iced::Color::from_rgb8(178, 54, 58)),
|
||||
);
|
||||
}
|
||||
}
|
||||
footer = footer.push(
|
||||
row![
|
||||
Space::new().width(Fill),
|
||||
button(text("Cancel").size(12))
|
||||
@@ -1465,7 +1487,15 @@ impl Workflow {
|
||||
.padding(10)
|
||||
.on_press_maybe(enabled.then_some(Message::Workflow(Event::Apply)))
|
||||
]
|
||||
.spacing(10)
|
||||
.spacing(10),
|
||||
);
|
||||
column![
|
||||
scrollable(content.padding(iced::Padding {
|
||||
right: 14.,
|
||||
..Default::default()
|
||||
}))
|
||||
.height(480),
|
||||
footer
|
||||
]
|
||||
.spacing(18)
|
||||
.into()
|
||||
|
||||
Reference in New Issue
Block a user