//! Qualified editors share core's resolver, including incomplete leaf selection. use super::*; use iced::widget::column; use oa_core::topology_context::TopologyIndex; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Purpose { Precedence, Exchange, CommonWork, } impl std::fmt::Display for Purpose { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { Self::Precedence => "Precedence", Self::Exchange => "Exchange assembly", Self::CommonWork => "Common work incidence", }) } } fn choice(p: &Project, id: Id) -> crate::app::Choice { crate::app::Choice { id, label: p .elements .get(&id) .map(|e| format!("{} (@{})", e.name, e.alias)) .or_else(|| { p.relations .get(&id) .map(|r| format!("{} (@{})", r.name, r.alias)) }) .unwrap_or_else(|| format!("Unavailable {id}")), } } pub(super) fn property<'a>( draft: &'a Draft, p: &'a Project, value: &'a Value, path: &str, ) -> Option> { if !path.contains("/ContextualLink/") { return None; } let relation: Relation = serde_json::from_value(draft.context_value()) .ok() .or_else(|| p.relations.get(&draft.original).cloned())?; let RelationBody::ContextualLink { owner, .. } = relation.body else { return None; }; if path.ends_with("/source/occurrence") || path.ends_with("/target/occurrence") { let parent = path.strip_suffix("/occurrence")?; let endpoint: OccurrenceEndpoint = serde_json::from_value(draft.context_value().pointer(parent)?.clone()).ok()?; let index = TopologyIndex::new(p); let scope = match index.resolve_scope(owner, &endpoint.references) { Ok(scope) => scope, Err(error) => return Some(text(error.to_string()).size(12).into()), }; let choices = index .contributions(scope) .into_iter() .map(|id| choice(p, id)) .collect::>(); let selected = (!endpoint.occurrence.is_nil()).then(|| choice(p, endpoint.occurrence)); return Some( column![ text("Only the leaf changes; the caller and complete reference path remain fixed.") .size(11), pick_list(choices, selected, { let path = path.to_owned(); move |choice| Message::FieldShape(path.clone(), json!(choice.id)) }) .padding(8) .text_size(12) .width(Fill) ] .spacing(6) .into(), ); } if path.ends_with("/source") || path.ends_with("/target") { let endpoint: OccurrenceEndpoint = serde_json::from_value(value.clone()).ok()?; let index = TopologyIndex::new(p); let mut out = column![ text(crate::behavior::topology::contextual::address_label( p, owner, &endpoint )) .size(12) ] .spacing(6); match index.resolve_scope(owner, &endpoint.references) { Ok(scope) => { let choices = index .contributions(scope) .into_iter() .map(|id| choice(p, id)) .collect::>(); let selected = (!endpoint.occurrence.is_nil()).then(|| choice(p, endpoint.occurrence)); out = out.push( pick_list(choices, selected, { let path = path.to_owned(); let references = endpoint.references.clone(); move |choice| { Message::FieldShape( path.clone(), json!(OccurrenceEndpoint { references: references.clone(), occurrence: choice.id }), ) } }) .placeholder("Choose a contribution in this context…") .padding(8) .text_size(12) .width(Fill), ); if index.resolve(owner, &endpoint).is_ok() && matches!( p.elements .get(&endpoint.occurrence) .and_then(|e| e.shared()), Some(SharedBody::Occurrence { body: OccurrenceBody::SequenceReference { sequence: Some(_) }, .. }) ) { let mut references = endpoint.references.clone(); references.push(endpoint.occurrence); out = out.push(button("Enter selected reference").on_press( Message::FieldShape( path.to_owned(), json!(OccurrenceEndpoint { references, occurrence: Id::nil() }), ), )); } } Err(error) => out = out.push(text(error.to_string()).size(12)), } if let Some(reference) = endpoint.references.last() { let mut references = endpoint.references.clone(); references.pop(); out = out.push(button("Up to caller context").on_press(Message::FieldShape( path.to_owned(), json!(OccurrenceEndpoint { references, occurrence: *reference }), ))); } return Some(out.push(text("Entering a reference retains an unfinished endpoint until you choose its contribution. Apply checks the complete caller path.").size(11)).into()); } if path.ends_with("/owner") { let mut choices = p .elements .values() .filter(|e| matches!(e.kind(), Kind::Process | Kind::FunctionalChain)) .map(|e| choice(p, e.id)) .collect::>(); choices.sort_by(|a, b| a.label.cmp(&b.label).then(a.id.cmp(&b.id))); return Some( pick_list(choices, Some(choice(p, owner)), { let path = path.to_owned(); move |choice| Message::FieldShape(path.clone(), json!(choice.id)) }) .padding(8) .text_size(12) .width(Fill) .into(), ); } if path.ends_with("/purpose") { let purpose: ContextualLinkPurpose = serde_json::from_value(value.clone()).ok()?; let selected = match purpose { ContextualLinkPurpose::Precedence { .. } => Purpose::Precedence, ContextualLinkPurpose::Exchange { .. } => Purpose::Exchange, ContextualLinkPurpose::CommonWork { .. } => Purpose::CommonWork, }; let mut out = column![ pick_list( vec![Purpose::Precedence, Purpose::Exchange, Purpose::CommonWork], Some(selected), { let path = path.to_owned(); move |purpose| { Message::FieldVariant( path.clone(), match purpose { Purpose::Precedence => json!(ContextualLinkPurpose::Precedence { guard: String::new() }), Purpose::Exchange => json!(ContextualLinkPurpose::Exchange { exchange: Id::nil() }), Purpose::CommonWork => { json!(ContextualLinkPurpose::CommonWork { work: Id::nil() }) } }, ) } } ) .padding(8) .text_size(12) .width(Fill) ] .spacing(7); match purpose { ContextualLinkPurpose::Precedence { .. } => { let path = format!("{path}/Precedence/guard"); let raw = draft .inputs .get(&path) .map(|input| input.raw.as_str()) .unwrap_or(""); out = out.push( text_input("Precedence guard", raw) .on_input(move |raw| Message::Field(path.clone(), raw)) .padding(8) .size(12), ); } purpose => { let (selected, choices, leaf) = match purpose { ContextualLinkPurpose::Exchange { exchange } => ( exchange, p.relations .values() .filter(|r| { matches!( r.kind(), RelationKind::Interaction | RelationKind::FunctionalExchange ) && p.layer_of(r.id) == p.layer_of(owner) }) .map(|r| choice(p, r.id)) .collect::>(), "Exchange/exchange", ), ContextualLinkPurpose::CommonWork { work } => ( work, p.elements .values() .filter(|e| e.kind().is_work() && Some(e.layer()) == p.layer_of(owner)) .map(|e| choice(p, e.id)) .collect::>(), "CommonWork/work", ), _ => unreachable!(), }; let path = format!("{path}/{leaf}"); out=out.push(pick_list(choices,(!selected.is_nil()).then(||choice(p,selected)),move|choice|Message::FieldShape(path.clone(),json!(choice.id))).placeholder("Choose exact engineering identity…").padding(8).text_size(12).width(Fill)).push(text("Assembly incidence does not imply execution order or merge executions.").size(11)); } } return Some(out.into()); } None }