Review closed semantic constructor groups with retained typed drafts
This commit is contained in:
+155
-16
@@ -46,7 +46,8 @@ pub struct Draft {
|
||||
pub inputs: BTreeMap<String, Input>,
|
||||
pub exact_strings: bool,
|
||||
context: Option<Value>,
|
||||
message_timer_draft: Option<Input>,
|
||||
pub semantic_groups: bool,
|
||||
constructor_drafts: BTreeMap<(String, String), (Value, BTreeMap<String, Input>)>,
|
||||
port_contract_drafts: BTreeMap<String, Value>,
|
||||
pub value_browsers: BTreeMap<String, ValueBrowser>,
|
||||
}
|
||||
@@ -86,7 +87,8 @@ impl Draft {
|
||||
inputs: BTreeMap::new(),
|
||||
exact_strings: false,
|
||||
context: None,
|
||||
message_timer_draft: None,
|
||||
semantic_groups: false,
|
||||
constructor_drafts: BTreeMap::new(),
|
||||
port_contract_drafts: BTreeMap::new(),
|
||||
value_browsers: BTreeMap::new(),
|
||||
};
|
||||
@@ -250,27 +252,73 @@ impl Draft {
|
||||
self.rebuild_retaining(|old| (!within(old, path)).then(|| old.to_owned()));
|
||||
Ok(())
|
||||
}
|
||||
/// Message-kind selection preserves the timer's exact input/editor state
|
||||
/// while its payload is temporarily hidden by another kind.
|
||||
pub fn message_kind_variant(&mut self, path: &str, value: Value) -> Result<(), String> {
|
||||
serde_json::from_value::<MessageKind>(value.clone()).map_err(|e| e.to_string())?;
|
||||
/// Preserve each selected constructor branch, including incomplete raw input
|
||||
/// and editor state. No sibling fields are parsed or cleared by a switch.
|
||||
pub fn variant_scoped(&mut self, path: &str, proposed: Value) -> Result<(), String> {
|
||||
let current = self
|
||||
.value
|
||||
.pointer(path)
|
||||
.ok_or("Message kind no longer exists.")?;
|
||||
if same_variant(current, &value) {
|
||||
.ok_or("Property no longer exists.")?
|
||||
.clone();
|
||||
if same_variant(¤t, &proposed) {
|
||||
return Ok(());
|
||||
}
|
||||
let timer_path = format!("{path}/Timer/expression");
|
||||
if let Some(input) = self.inputs.remove(&timer_path) {
|
||||
self.message_timer_draft = Some(input);
|
||||
let constructor = path.ends_with("/DataType/definition")
|
||||
|| path.ends_with("/Port/contract")
|
||||
|| path.ends_with("/Occurrence/body")
|
||||
|| path.ends_with("/Event/body")
|
||||
|| path.ends_with("/message_kind");
|
||||
if !constructor {
|
||||
return self.shape_scoped(path, proposed);
|
||||
}
|
||||
self.shape_scoped(path, value)?;
|
||||
if self.inputs.contains_key(&timer_path)
|
||||
&& let Some(input) = self.message_timer_draft.take()
|
||||
// Input/output direction changes carry the item declaration. Read those
|
||||
// raw reference strings before stashing their editor buffers so an
|
||||
// incomplete reference cannot silently fall back to its stored UUID.
|
||||
let port_current = if path.ends_with("/Port/contract") {
|
||||
self.materialize_scopes(&[path.to_owned()])?
|
||||
.pointer(path)
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let tag = |value: &Value| {
|
||||
value
|
||||
.as_str()
|
||||
.or_else(|| value.as_object()?.keys().next().map(String::as_str))
|
||||
.unwrap_or("")
|
||||
.to_owned()
|
||||
};
|
||||
let previous_key = (path.to_owned(), tag(¤t));
|
||||
let next_key = (path.to_owned(), tag(&proposed));
|
||||
let mut inputs = BTreeMap::new();
|
||||
for key in self
|
||||
.inputs
|
||||
.keys()
|
||||
.filter(|key| within(key, path))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
{
|
||||
self.inputs.insert(timer_path, input);
|
||||
inputs.insert(key.clone(), self.inputs.remove(&key).unwrap());
|
||||
}
|
||||
self.constructor_drafts
|
||||
.insert(previous_key, (current.clone(), inputs));
|
||||
let saved = self.constructor_drafts.remove(&next_key);
|
||||
let (value, restored) = if let Some(saved) = saved {
|
||||
saved
|
||||
} else {
|
||||
let value = if path.ends_with("/Port/contract") {
|
||||
port_fields::switch_contract(
|
||||
port_current.as_ref().unwrap_or(¤t),
|
||||
proposed,
|
||||
&mut BTreeMap::new(),
|
||||
)
|
||||
} else {
|
||||
proposed
|
||||
};
|
||||
(value, BTreeMap::new())
|
||||
};
|
||||
self.shape_scoped(path, value)?;
|
||||
self.inputs.extend(restored);
|
||||
Ok(())
|
||||
}
|
||||
fn rebuild_retaining(&mut self, new_path: impl Fn(&str) -> Option<String>) {
|
||||
@@ -1315,6 +1363,7 @@ fn variants(path: &str, value: &Value, kind: RecordKind) -> Vec<(String, Value)>
|
||||
"layer" => &["Oa", "Sa"],
|
||||
"mechanism" => &["Unspecified", "Flow", "Event", "Operation", "SharedData"],
|
||||
"storage" => &["Value", "Reference"],
|
||||
"direction" if path.contains("/Port/legacy/") => &["In", "Out", "InOut"],
|
||||
"direction" if path.contains("DataField") => {
|
||||
&["Unspecified", "In", "Out", "InOut", "Return"]
|
||||
}
|
||||
@@ -1571,7 +1620,7 @@ fn property<'a>(
|
||||
return view;
|
||||
}
|
||||
let label_view = || text(label.to_owned()).size(11).color(style::MUTED);
|
||||
if path.ends_with("/Port/legacy") {
|
||||
if path.ends_with("/Port/legacy") && !draft.semantic_groups {
|
||||
let mut content = column![].spacing(4);
|
||||
if !value.is_null() {
|
||||
content = content.push(
|
||||
@@ -1585,6 +1634,29 @@ fn property<'a>(
|
||||
}
|
||||
return content.into();
|
||||
}
|
||||
if path.ends_with("/Port/legacy") && draft.semantic_groups && !skip_variant {
|
||||
let mut content = column![
|
||||
label_view(),
|
||||
text(
|
||||
"Retained generic association is part of this group. Change or clear it explicitly."
|
||||
)
|
||||
.size(11)
|
||||
]
|
||||
.spacing(5);
|
||||
if value.is_null() {
|
||||
content = content.push(button(text("Add generic association").size(11)).on_press(
|
||||
Message::FieldShape(path.into(), json!({"direction":"InOut","interface":null})),
|
||||
));
|
||||
} else {
|
||||
content = content
|
||||
.push(property(draft, project, value, path, label, true))
|
||||
.push(
|
||||
button(text("Clear generic association").size(11))
|
||||
.on_press(Message::FieldShape(path.into(), Value::Null)),
|
||||
);
|
||||
}
|
||||
return content.into();
|
||||
}
|
||||
let enum_choices = if skip_variant {
|
||||
vec![]
|
||||
} else {
|
||||
@@ -1842,6 +1914,73 @@ fn property<'a>(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn semantic_port_group_choices_use_combined_owner_and_retain_invalid_transferred_items() {
|
||||
let mut p = Project::empty();
|
||||
let function = p
|
||||
.add_element(Kind::Function, Layer::Sa, "Function", None)
|
||||
.unwrap();
|
||||
let component = p
|
||||
.add_element(Kind::System, Layer::Sa, "Component", None)
|
||||
.unwrap();
|
||||
let item = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Sa, "Item", None)
|
||||
.unwrap();
|
||||
let id = Id::new_v4();
|
||||
p.elements.insert(
|
||||
id,
|
||||
ports::make_port(
|
||||
&p,
|
||||
id,
|
||||
function,
|
||||
"Port",
|
||||
PortContract::FunctionInput {
|
||||
incoming_items: vec![item],
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let mut draft = Draft::from_project(&p, id).unwrap();
|
||||
draft.semantic_groups = true;
|
||||
let path = "/body/Sa/Shared/Port/contract";
|
||||
let input = format!("{path}/FunctionInput/incoming_items/0");
|
||||
draft.input(&input, "incomplete-id".into());
|
||||
let cursor = draft.inputs[&input].editor.cursor();
|
||||
draft
|
||||
.variant_scoped(path, json!({"FunctionOutput":{"outgoing_items":[]}}))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
draft.inputs[&format!("{path}/FunctionOutput/outgoing_items/0")].raw,
|
||||
"incomplete-id"
|
||||
);
|
||||
assert!(serde_json::from_value::<Element>(draft.materialize().unwrap()).is_err());
|
||||
draft
|
||||
.variant_scoped(path, json!({"FunctionInput":{"incoming_items":[]}}))
|
||||
.unwrap();
|
||||
assert_eq!(draft.inputs[&input].raw, "incomplete-id");
|
||||
assert_eq!(draft.inputs[&input].editor.cursor(), cursor);
|
||||
let choices =
|
||||
port_fields::variants(&draft, &p, path, draft.value.pointer(path).unwrap()).unwrap();
|
||||
assert!(
|
||||
choices
|
||||
.iter()
|
||||
.any(|(_, v)| v.get("FunctionInput").is_some())
|
||||
);
|
||||
assert!(choices.iter().any(|(_, v)| *v == json!("Legacy")));
|
||||
assert!(!choices.iter().any(|(_, v)| v.get("Component").is_some()));
|
||||
let mut context = draft.value.clone();
|
||||
context["parent"] = json!(component);
|
||||
draft.set_context(Some(context));
|
||||
let choices =
|
||||
port_fields::variants(&draft, &p, path, draft.value.pointer(path).unwrap()).unwrap();
|
||||
assert!(choices.iter().any(|(_, v)| v.get("Component").is_some()));
|
||||
assert!(
|
||||
!choices
|
||||
.iter()
|
||||
.any(|(_, v)| v.get("FunctionInput").is_some())
|
||||
);
|
||||
}
|
||||
|
||||
fn ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
|
||||
reference_ids(draft, project, path)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user