Integrate typed port authoring and reviewable exchange workflows
This commit is contained in:
+396
-137
@@ -14,6 +14,7 @@ use iced::{
|
||||
use oa_core::*;
|
||||
use serde_json::{Value, json};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
mod port_fields;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RecordKind {
|
||||
@@ -37,6 +38,7 @@ pub struct Draft {
|
||||
baseline: Value,
|
||||
pub inputs: BTreeMap<String, Input>,
|
||||
pub exact_strings: bool,
|
||||
port_contract_drafts: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
impl Draft {
|
||||
@@ -72,6 +74,7 @@ impl Draft {
|
||||
value,
|
||||
inputs: BTreeMap::new(),
|
||||
exact_strings: false,
|
||||
port_contract_drafts: BTreeMap::new(),
|
||||
};
|
||||
draft.rebuild();
|
||||
Some(draft)
|
||||
@@ -179,6 +182,17 @@ impl Draft {
|
||||
}
|
||||
pub fn shape(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
let mut candidate = self.materialize()?;
|
||||
let value = if path.ends_with("/Port/contract") {
|
||||
port_fields::switch_contract(
|
||||
candidate
|
||||
.pointer(path)
|
||||
.ok_or("Property no longer exists.")?,
|
||||
value,
|
||||
&mut self.port_contract_drafts,
|
||||
)
|
||||
} else {
|
||||
value
|
||||
};
|
||||
*candidate
|
||||
.pointer_mut(path)
|
||||
.ok_or("Property no longer exists.")? = value;
|
||||
@@ -301,6 +315,15 @@ fn friendly(name: &str) -> String {
|
||||
"do_activity" => "Do behavior (description)".into(),
|
||||
"source_port" => "Output port".into(),
|
||||
"target_port" => "Input port".into(),
|
||||
"contract" => "Port contract".into(),
|
||||
"FunctionInput" => "Function input".into(),
|
||||
"FunctionOutput" => "Function output".into(),
|
||||
"Component" => "Component port".into(),
|
||||
"incoming_items" => "Incoming exchange items".into(),
|
||||
"outgoing_items" => "Outgoing exchange items".into(),
|
||||
"orientation" => "Flow orientation".into(),
|
||||
"oriented" => "Directed component exchange".into(),
|
||||
"Unset" => "Not specified".into(),
|
||||
"minimum" => "Minimum count".into(),
|
||||
"maximum" => "Maximum count".into(),
|
||||
"type_ref" => "Value type".into(),
|
||||
@@ -378,7 +401,33 @@ fn reference(path: &str) -> bool {
|
||||
| "diagram"
|
||||
) || matches!(
|
||||
parent,
|
||||
"items" | "steps" | "branches" | "occurrences" | "constrained" | "triggers" | "effects"
|
||||
"items"
|
||||
| "incoming_items"
|
||||
| "outgoing_items"
|
||||
| "steps"
|
||||
| "branches"
|
||||
| "occurrences"
|
||||
| "constrained"
|
||||
| "triggers"
|
||||
| "effects"
|
||||
)
|
||||
}
|
||||
fn optional_reference(path: &str) -> bool {
|
||||
matches!(
|
||||
path.rsplit('/').next().unwrap_or_default(),
|
||||
"parent"
|
||||
| "interface"
|
||||
| "context"
|
||||
| "represented"
|
||||
| "exchange"
|
||||
| "sender"
|
||||
| "receiver"
|
||||
| "action"
|
||||
| "sequence"
|
||||
| "scenario"
|
||||
| "reply_to"
|
||||
| "lifeline"
|
||||
| "state"
|
||||
)
|
||||
}
|
||||
fn multiline(path: &str) -> bool {
|
||||
@@ -411,7 +460,9 @@ fn free_string(path: &str) -> bool {
|
||||
// incompatible. Applying the complete draft remains the core validator's job.
|
||||
fn reference_choices(draft: &Draft, project: &Project, path: &str, current: Id) -> Vec<Choice> {
|
||||
let identities = project.identities().into_iter().collect::<BTreeMap<_, _>>();
|
||||
let mut objects = reference_ids(draft, project, path)
|
||||
let compatible = reference_ids(draft, project, path);
|
||||
let mut objects = compatible
|
||||
.clone()
|
||||
.into_iter()
|
||||
.chain((!current.is_nil()).then_some(current))
|
||||
.collect::<BTreeSet<_>>()
|
||||
@@ -430,21 +481,31 @@ fn reference_choices(draft: &Draft, project: &Project, path: &str, current: Id)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| project.named(id).to_owned());
|
||||
let label = identities
|
||||
let mut label = identities
|
||||
.get(&id)
|
||||
.map(|alias| format!("{name} · @{alias}"))
|
||||
.unwrap_or_else(|| format!("Missing reference · {id}"));
|
||||
if identities.contains_key(&id) && !compatible.contains(&id) {
|
||||
label = format!("Current choice needs review · {label}");
|
||||
}
|
||||
Choice { id, label }
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
objects.sort_by(|a, b| a.label.cmp(&b.label));
|
||||
objects.insert(
|
||||
0,
|
||||
Choice {
|
||||
id: Id::nil(),
|
||||
label: "Not assigned".into(),
|
||||
},
|
||||
);
|
||||
if optional_reference(path) || current.is_nil() {
|
||||
objects.insert(
|
||||
0,
|
||||
Choice {
|
||||
id: Id::nil(),
|
||||
label: if optional_reference(path) {
|
||||
"Not assigned"
|
||||
} else {
|
||||
"Choose a required reference"
|
||||
}
|
||||
.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
objects
|
||||
}
|
||||
|
||||
@@ -500,6 +561,9 @@ fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
|
||||
if matches!(field, "source" | "target")
|
||||
&& let Some(r) = &relation
|
||||
{
|
||||
if let Some(choices) = port_fields::relation_endpoints(project, r, field, draft.original) {
|
||||
return choices;
|
||||
}
|
||||
// can_connect treats allocations/initial exits as new links, so exclude
|
||||
// the relation being edited before asking it for endpoint choices.
|
||||
let mut context = project.clone();
|
||||
@@ -584,100 +648,257 @@ fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
|
||||
.rev()
|
||||
.nth(1)
|
||||
.and_then(|s| s.parse::<usize>().ok());
|
||||
project.elements.values().filter(|candidate| {
|
||||
if field == "element" && let RecordKind::Node(diagram) = draft.kind {
|
||||
let diagram = value.get("diagram").and_then(Value::as_str)
|
||||
.and_then(|id| id.parse().ok()).unwrap_or(diagram);
|
||||
return project.diagrams.get(&diagram).is_some_and(|d|
|
||||
(d.projection == ProjectionKind::Traceability || d.layer == candidate.layer())
|
||||
&& !d.nodes.values().any(|n| n.id != draft.original && n.element == candidate.id));
|
||||
}
|
||||
if !same_layer(candidate) { return false; }
|
||||
match field {
|
||||
"parent" => element.as_ref().is_some_and(|child| {
|
||||
if candidate.id == draft.original || candidate.id == child.id
|
||||
|| !validation::can_parent(project, child.kind(), child.layer(), Some(candidate.id)) {
|
||||
return false;
|
||||
project
|
||||
.elements
|
||||
.values()
|
||||
.filter(|candidate| {
|
||||
if field == "element"
|
||||
&& let RecordKind::Node(diagram) = draft.kind
|
||||
{
|
||||
let diagram = value
|
||||
.get("diagram")
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|id| id.parse().ok())
|
||||
.unwrap_or(diagram);
|
||||
return project.diagrams.get(&diagram).is_some_and(|d| {
|
||||
(d.projection == ProjectionKind::Traceability || d.layer == candidate.layer())
|
||||
&& !d
|
||||
.nodes
|
||||
.values()
|
||||
.any(|n| n.id != draft.original && n.element == candidate.id)
|
||||
});
|
||||
}
|
||||
if !same_layer(candidate) {
|
||||
return false;
|
||||
}
|
||||
match field {
|
||||
"parent" => element.as_ref().is_some_and(|child| {
|
||||
if candidate.id == draft.original
|
||||
|| candidate.id == child.id
|
||||
|| !validation::can_parent(
|
||||
project,
|
||||
child.kind(),
|
||||
child.layer(),
|
||||
Some(candidate.id),
|
||||
)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let mut parent = candidate.parent;
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = parent {
|
||||
if id == draft.original || id == child.id || !seen.insert(id) {
|
||||
return false;
|
||||
}
|
||||
parent = project.elements.get(&id).and_then(|e| e.parent);
|
||||
}
|
||||
match child.shared() {
|
||||
Some(SharedBody::Port { contract, .. }) => match contract {
|
||||
PortContract::FunctionInput { .. }
|
||||
| PortContract::FunctionOutput { .. } => {
|
||||
child.layer() == Layer::Sa && candidate.kind() == Kind::Function
|
||||
}
|
||||
PortContract::Component { .. } => {
|
||||
child.layer() == Layer::Sa
|
||||
&& matches!(candidate.kind(), Kind::System | Kind::SystemActor)
|
||||
}
|
||||
PortContract::Legacy => {
|
||||
candidate.kind().is_work() || candidate.kind().is_performer()
|
||||
}
|
||||
},
|
||||
Some(SharedBody::Occurrence { body, .. }) => match candidate.shared() {
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Fragment { .. },
|
||||
..
|
||||
}) => matches!(body, OccurrenceBody::Branch { .. }),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Branch { .. },
|
||||
..
|
||||
}) => !matches!(body, OccurrenceBody::Branch { .. }),
|
||||
_ if matches!(
|
||||
candidate.kind(),
|
||||
Kind::Process | Kind::FunctionalChain
|
||||
) =>
|
||||
{
|
||||
matches!(
|
||||
body,
|
||||
OccurrenceBody::Exchange { .. }
|
||||
| OccurrenceBody::Action { .. }
|
||||
| OccurrenceBody::Control { .. }
|
||||
| OccurrenceBody::SequenceReference { .. }
|
||||
)
|
||||
}
|
||||
_ => !matches!(body, OccurrenceBody::Branch { .. }),
|
||||
},
|
||||
_ => true,
|
||||
}
|
||||
}),
|
||||
"Named" => candidate.kind() == Kind::DataType,
|
||||
"interface" => candidate.kind() == Kind::Interface,
|
||||
"context" => {
|
||||
candidate.kind().is_sequence()
|
||||
|| candidate.kind().is_work()
|
||||
|| candidate.kind().is_performer()
|
||||
|| matches!(
|
||||
candidate.kind(),
|
||||
Kind::Capability
|
||||
| Kind::SystemCapability
|
||||
| Kind::Mission
|
||||
| Kind::OperationalMission
|
||||
)
|
||||
}
|
||||
let mut parent = candidate.parent;
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(id) = parent {
|
||||
if id == draft.original || id == child.id || !seen.insert(id) { return false; }
|
||||
parent = project.elements.get(&id).and_then(|e| e.parent);
|
||||
"represented" => {
|
||||
candidate.kind().is_performer()
|
||||
|| candidate.kind().is_work()
|
||||
|| candidate.kind() == Kind::Role
|
||||
}
|
||||
match child.shared() {
|
||||
Some(SharedBody::Occurrence { body, .. }) => match candidate.shared() {
|
||||
Some(SharedBody::Occurrence { body: OccurrenceBody::Fragment { .. }, .. }) => matches!(body, OccurrenceBody::Branch { .. }),
|
||||
Some(SharedBody::Occurrence { body: OccurrenceBody::Branch { .. }, .. }) => !matches!(body, OccurrenceBody::Branch { .. }),
|
||||
_ if matches!(candidate.kind(), Kind::Process | Kind::FunctionalChain) => matches!(body, OccurrenceBody::Exchange { .. } | OccurrenceBody::Action { .. } | OccurrenceBody::Control { .. } | OccurrenceBody::SequenceReference { .. }),
|
||||
_ => !matches!(body, OccurrenceBody::Branch { .. }),
|
||||
},
|
||||
"action" => candidate.kind().is_work(),
|
||||
"sequence" => element
|
||||
.as_ref()
|
||||
.and_then(|e| e.parent)
|
||||
.and_then(|id| project.elements.get(&id))
|
||||
.is_some_and(|owner| {
|
||||
candidate.kind() == owner.kind() && candidate.id != owner.id
|
||||
}),
|
||||
"scenario" => {
|
||||
candidate.kind() == Kind::Scenario
|
||||
&& scenario.is_some_and(|owner| {
|
||||
let viewpoint =
|
||||
|id| match project.elements.get(&id).and_then(Element::shared) {
|
||||
Some(SharedBody::Scenario { viewpoint, .. }) => {
|
||||
Some(*viewpoint)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let a = viewpoint(owner);
|
||||
let b = viewpoint(candidate.id);
|
||||
(a == b
|
||||
|| a == Some(ScenarioViewpoint::MixedLegacy)
|
||||
|| b == Some(ScenarioViewpoint::MixedLegacy))
|
||||
&& !scenario_reaches(project, candidate.id, owner)
|
||||
})
|
||||
}
|
||||
"reply_to" => {
|
||||
candidate.id != draft.original
|
||||
&& scenario.is_some()
|
||||
&& validation::ancestor_of_kind(project, candidate.id, Kind::Scenario)
|
||||
== scenario
|
||||
&& matches!(
|
||||
candidate.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange {
|
||||
message_kind: MessageKind::Call,
|
||||
..
|
||||
},
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
"referenced" | "local" => element.as_ref().is_some_and(|e| {
|
||||
let Some(SharedBody::Occurrence {
|
||||
body:
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: target,
|
||||
bindings,
|
||||
},
|
||||
..
|
||||
}) = e.shared()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if candidate.kind() != Kind::Lifeline
|
||||
|| candidate.parent
|
||||
!= if field == "referenced" {
|
||||
*target
|
||||
} else {
|
||||
scenario
|
||||
}
|
||||
|| candidate.parent.is_none()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if bindings.iter().enumerate().any(|(i, b)| {
|
||||
Some(i) != binding_row
|
||||
&& candidate.id
|
||||
== if field == "referenced" {
|
||||
b.referenced
|
||||
} else {
|
||||
b.local
|
||||
}
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
let other = binding_row.and_then(|i| bindings.get(i)).map(|b| {
|
||||
if field == "referenced" {
|
||||
b.local
|
||||
} else {
|
||||
b.referenced
|
||||
}
|
||||
});
|
||||
let represented = |id| match project.elements.get(&id).and_then(Element::shared)
|
||||
{
|
||||
Some(SharedBody::Lifeline { represented }) => *represented,
|
||||
_ => None,
|
||||
};
|
||||
match (represented(candidate.id), other.and_then(represented)) {
|
||||
(Some(a), Some(b)) => {
|
||||
validation::semantic_representation_compatible(
|
||||
project,
|
||||
if field == "local" { a } else { b },
|
||||
if field == "local" { b } else { a },
|
||||
) != Some(false)
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}),
|
||||
"state" => matches!(candidate.kind(), Kind::State | Kind::Mode),
|
||||
"sender" | "receiver" | "lifeline" => element.as_ref().is_some_and(|e| {
|
||||
let scenario = e
|
||||
.parent
|
||||
.and_then(|id| validation::ancestor_of_kind(project, id, Kind::Scenario));
|
||||
candidate.kind() == Kind::Lifeline
|
||||
&& candidate.parent == scenario
|
||||
&& lifeline_matches(project, e, candidate, field)
|
||||
}),
|
||||
"source_port" | "target_port" => relation.as_ref().is_some_and(|r| {
|
||||
let source = field == "source_port";
|
||||
candidate.layer() == Layer::Sa
|
||||
&& candidate.parent == Some(if source { r.source() } else { r.target() })
|
||||
&& match port_contract(candidate) {
|
||||
Some(PortContract::FunctionOutput { outgoing_items }) if source => {
|
||||
items_satisfy(project, r.body.items(), outgoing_items)
|
||||
}
|
||||
Some(PortContract::FunctionInput { incoming_items }) if !source => {
|
||||
items_satisfy(project, r.body.items(), incoming_items)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}),
|
||||
_ => match list {
|
||||
"items" | "incoming_items" | "outgoing_items" => {
|
||||
candidate.kind() == Kind::ExchangeItem
|
||||
}
|
||||
"triggers" => candidate.kind().is_work() || candidate.kind() == Kind::Event,
|
||||
"effects" => candidate.kind().is_work(),
|
||||
"branches" => {
|
||||
owned(candidate)
|
||||
&& matches!(
|
||||
candidate.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Branch { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
"steps" | "occurrences" => {
|
||||
owned(candidate) && candidate.kind() == Kind::Occurrence
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}),
|
||||
"Named" => candidate.kind() == Kind::DataType,
|
||||
"interface" => candidate.kind() == Kind::Interface,
|
||||
"context" => candidate.kind().is_sequence() || candidate.kind().is_work()
|
||||
|| candidate.kind().is_performer()
|
||||
|| matches!(candidate.kind(), Kind::Capability | Kind::SystemCapability | Kind::Mission | Kind::OperationalMission),
|
||||
"represented" => candidate.kind().is_performer() || candidate.kind().is_work() || candidate.kind() == Kind::Role,
|
||||
"action" => candidate.kind().is_work(),
|
||||
"sequence" => element.as_ref().and_then(|e| e.parent).and_then(|id| project.elements.get(&id))
|
||||
.is_some_and(|owner| candidate.kind() == owner.kind() && candidate.id != owner.id),
|
||||
"scenario" => candidate.kind() == Kind::Scenario && scenario.is_some_and(|owner| {
|
||||
let viewpoint = |id| match project.elements.get(&id).and_then(Element::shared) {
|
||||
Some(SharedBody::Scenario { viewpoint, .. }) => Some(*viewpoint),
|
||||
_ => None,
|
||||
};
|
||||
let a = viewpoint(owner);
|
||||
let b = viewpoint(candidate.id);
|
||||
(a == b || a == Some(ScenarioViewpoint::MixedLegacy) || b == Some(ScenarioViewpoint::MixedLegacy))
|
||||
&& !scenario_reaches(project, candidate.id, owner)
|
||||
}),
|
||||
"reply_to" => candidate.id != draft.original
|
||||
&& scenario.is_some()
|
||||
&& validation::ancestor_of_kind(project, candidate.id, Kind::Scenario) == scenario
|
||||
&& matches!(candidate.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Exchange { message_kind: MessageKind::Call, .. }, .. })),
|
||||
"referenced" | "local" => element.as_ref().is_some_and(|e| {
|
||||
let Some(SharedBody::Occurrence { body: OccurrenceBody::ScenarioUse { scenario: target, bindings }, .. }) = e.shared() else { return false; };
|
||||
if candidate.kind() != Kind::Lifeline
|
||||
|| candidate.parent != if field == "referenced" { *target } else { scenario }
|
||||
|| candidate.parent.is_none() { return false; }
|
||||
if bindings.iter().enumerate().any(|(i,b)| Some(i) != binding_row
|
||||
&& candidate.id == if field == "referenced" { b.referenced } else { b.local }) { return false; }
|
||||
let other = binding_row.and_then(|i| bindings.get(i)).map(|b| if field == "referenced" { b.local } else { b.referenced });
|
||||
let represented = |id| match project.elements.get(&id).and_then(Element::shared) {
|
||||
Some(SharedBody::Lifeline { represented }) => *represented,
|
||||
_ => None,
|
||||
};
|
||||
match (represented(candidate.id), other.and_then(represented)) {
|
||||
(Some(a), Some(b)) => validation::semantic_representation_compatible(project,
|
||||
if field == "local" { a } else { b }, if field == "local" { b } else { a }) != Some(false),
|
||||
_ => true,
|
||||
}
|
||||
}),
|
||||
"state" => matches!(candidate.kind(), Kind::State | Kind::Mode),
|
||||
"sender" | "receiver" | "lifeline" => element.as_ref().is_some_and(|e| {
|
||||
let scenario = e.parent.and_then(|id| validation::ancestor_of_kind(project, id, Kind::Scenario));
|
||||
candidate.kind() == Kind::Lifeline && candidate.parent == scenario
|
||||
&& lifeline_matches(project, e, candidate, field)
|
||||
}),
|
||||
"source_port" | "target_port" => relation.as_ref().is_some_and(|r| {
|
||||
let source = field == "source_port";
|
||||
candidate.parent == Some(if source { r.source() } else { r.target() })
|
||||
&& matches!(candidate.shared(), Some(SharedBody::Port { direction, .. })
|
||||
if if source { *direction != PortDirection::In } else { *direction != PortDirection::Out })
|
||||
}),
|
||||
_ => match list {
|
||||
"items" => candidate.kind() == Kind::ExchangeItem,
|
||||
"triggers" => candidate.kind().is_work() || candidate.kind() == Kind::Event,
|
||||
"effects" => candidate.kind().is_work(),
|
||||
"branches" => owned(candidate) && matches!(candidate.shared(), Some(SharedBody::Occurrence { body: OccurrenceBody::Branch { .. }, .. })),
|
||||
"steps" | "occurrences" => owned(candidate) && candidate.kind() == Kind::Occurrence,
|
||||
_ => true,
|
||||
},
|
||||
}
|
||||
}).map(|e| e.id).collect()
|
||||
},
|
||||
}
|
||||
})
|
||||
.map(|e| e.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn lifeline_matches(project: &Project, occurrence: &Element, line: &Element, field: &str) -> bool {
|
||||
@@ -774,7 +995,6 @@ fn variants(path: &str, value: &Value, kind: RecordKind) -> Vec<(String, Value)>
|
||||
"FunctionalDataflow",
|
||||
"Capabilities",
|
||||
],
|
||||
"direction" => &["In", "Out", "InOut"],
|
||||
"operator" => &["Alternative", "Parallel", "Loop", "Optional"],
|
||||
"viewpoint" if path.contains("/Oa/") => &[
|
||||
"OperationalActivity",
|
||||
@@ -929,9 +1149,15 @@ pub fn view<'a>(draft: &'a Draft, project: &'a Project) -> View<'a, Message> {
|
||||
&& let Some(shared) = body.shared()
|
||||
{
|
||||
let shared = shared.clone();
|
||||
let layers = if matches!(&shared, SharedBody::Port { contract, .. } if *contract != PortContract::Legacy)
|
||||
{
|
||||
vec![Layer::Sa]
|
||||
} else {
|
||||
vec![Layer::Oa, Layer::Sa]
|
||||
};
|
||||
content = content.push(column![
|
||||
text("Engineering perspective").size(11).color(style::MUTED),
|
||||
pick_list([Layer::Oa, Layer::Sa], Some(body.layer()), move |layer|
|
||||
pick_list(layers, Some(body.layer()), move |layer|
|
||||
Message::FieldVariant("/body".into(), json!(ElementBody::shared_in(layer, shared.clone()))))
|
||||
.text_size(12).padding(8).width(Fill),
|
||||
text("Changing perspective keeps references; incompatible references must be updated before Apply.")
|
||||
@@ -985,11 +1211,37 @@ fn property<'a>(
|
||||
skip_variant: bool,
|
||||
) -> View<'a, Message> {
|
||||
let label_view = || text(label.to_owned()).size(11).color(style::MUTED);
|
||||
if path.ends_with("/Port/legacy") {
|
||||
let mut content = column![].spacing(4);
|
||||
if !value.is_null() {
|
||||
content = content.push(
|
||||
text("Preserved generic association")
|
||||
.size(12)
|
||||
.color(style::INK),
|
||||
);
|
||||
for line in port_fields::legacy_lines(value, project) {
|
||||
content = content.push(text(line).size(11).color(style::MUTED));
|
||||
}
|
||||
}
|
||||
return content.into();
|
||||
}
|
||||
let enum_choices = if skip_variant {
|
||||
vec![]
|
||||
} else {
|
||||
variants(path, value, draft.kind)
|
||||
port_fields::variants(draft, project, path, value)
|
||||
.unwrap_or_else(|| variants(path, value, draft.kind))
|
||||
};
|
||||
if path.ends_with("/Port/contract") && !skip_variant && enum_choices.is_empty() {
|
||||
let mut content = column![
|
||||
label_view(),
|
||||
text("Choose a System Analysis function or component owner to edit this port's typed contract. The current contract remains in the draft.")
|
||||
.size(11).color(style::MUTED)
|
||||
].spacing(5);
|
||||
if value.is_object() {
|
||||
content = content.push(property(draft, project, value, path, "", true));
|
||||
}
|
||||
return content.into();
|
||||
}
|
||||
if !enum_choices.is_empty() {
|
||||
let labels = enum_choices
|
||||
.iter()
|
||||
@@ -1001,6 +1253,16 @@ fn property<'a>(
|
||||
.map(|(name, _)| name.clone());
|
||||
let owned_path = path.to_string();
|
||||
let choices = enum_choices.clone();
|
||||
let retained = selected.is_none().then(|| {
|
||||
format!(
|
||||
"Current value retained for review: {}",
|
||||
value
|
||||
.as_str()
|
||||
.or_else(|| value.as_object()?.keys().next().map(String::as_str))
|
||||
.map(friendly)
|
||||
.unwrap_or_else(|| "Unrecognized value".into())
|
||||
)
|
||||
});
|
||||
let picker = pick_list(labels, selected, move |selected| {
|
||||
Message::FieldVariant(
|
||||
owned_path.clone(),
|
||||
@@ -1016,6 +1278,9 @@ fn property<'a>(
|
||||
.padding(8)
|
||||
.width(Fill);
|
||||
let mut out = column![label_view(), picker].spacing(5);
|
||||
if let Some(retained) = retained {
|
||||
out = out.push(text(retained).size(11).color(style::MUTED));
|
||||
}
|
||||
if value.is_object() {
|
||||
out = out.push(property(draft, project, value, path, "", true));
|
||||
}
|
||||
@@ -1031,26 +1296,8 @@ fn property<'a>(
|
||||
let choices = reference_choices(draft, project, path, current);
|
||||
let selected = choices.iter().find(|c| c.id == current).cloned();
|
||||
let owned_path = path.to_string();
|
||||
let optional = value.is_null()
|
||||
|| matches!(
|
||||
path.rsplit('/').next().unwrap_or_default(),
|
||||
"parent"
|
||||
| "interface"
|
||||
| "context"
|
||||
| "represented"
|
||||
| "exchange"
|
||||
| "sender"
|
||||
| "receiver"
|
||||
| "action"
|
||||
| "sequence"
|
||||
| "scenario"
|
||||
| "reply_to"
|
||||
| "lifeline"
|
||||
| "state"
|
||||
| "source_port"
|
||||
| "target_port"
|
||||
);
|
||||
return column![
|
||||
let optional = optional_reference(path);
|
||||
let mut content = column![
|
||||
label_view(),
|
||||
pick_list(choices, selected, move |choice| Message::FieldShape(
|
||||
owned_path.clone(),
|
||||
@@ -1064,8 +1311,17 @@ fn property<'a>(
|
||||
.padding(8)
|
||||
.width(Fill)
|
||||
]
|
||||
.spacing(5)
|
||||
.into();
|
||||
.spacing(5);
|
||||
if let Some(raw) = raw
|
||||
&& raw.parse::<Id>().is_err()
|
||||
{
|
||||
content = content.push(
|
||||
text(format!("Invalid reference retained in the draft: {raw}"))
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
);
|
||||
}
|
||||
return content.into();
|
||||
}
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
@@ -1251,21 +1507,20 @@ mod tests {
|
||||
let wrong_layer = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Oa, "Operational item", None)
|
||||
.unwrap();
|
||||
let interface = p
|
||||
.add_element(Kind::Interface, Layer::Sa, "Contract", None)
|
||||
.unwrap();
|
||||
let output = p
|
||||
.add_element(Kind::Port, Layer::Sa, "Output", Some(function))
|
||||
.unwrap();
|
||||
let input = p
|
||||
.add_element(Kind::Port, Layer::Sa, "Input", Some(receiver))
|
||||
.unwrap();
|
||||
let exchange = p.relate(
|
||||
RelationKind::FunctionalExchange,
|
||||
"Message",
|
||||
function,
|
||||
receiver,
|
||||
);
|
||||
let RelationBody::FunctionalExchange {
|
||||
source_port: output,
|
||||
target_port: input,
|
||||
..
|
||||
} = p.relations[&exchange].body
|
||||
else {
|
||||
panic!("Expected functional exchange");
|
||||
};
|
||||
let draft = Draft::from_project(&p, exchange).unwrap();
|
||||
let choices = ids(&draft, &p, "/body/FunctionalExchange/items/0");
|
||||
assert_eq!(choices, vec![item]);
|
||||
@@ -1289,8 +1544,12 @@ mod tests {
|
||||
);
|
||||
let port = Draft::from_project(&p, output).unwrap();
|
||||
assert_eq!(
|
||||
ids(&port, &p, "/body/Sa/Shared/Port/interface"),
|
||||
vec![interface]
|
||||
ids(
|
||||
&port,
|
||||
&p,
|
||||
"/body/Sa/Shared/Port/contract/FunctionOutput/outgoing_items/0"
|
||||
),
|
||||
vec![item]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user