//! Live form adapter. Compatibility stays in the existing parent adapter; //! browser metadata and scoped writes never reinterpret historical UUID text. use super::super::{Draft, friendly, history_fields, optional_reference, reference, reference_ids}; use super::{Candidate, Context, Current, State, trigger}; use crate::{app::Message, style}; use iced::{ Element, Fill, Rectangle, widget::{column, container, row, text, tooltip}, }; use oa_core::{Id, Project}; use serde_json::{Value, json}; use std::collections::BTreeSet; #[derive(Debug, Clone)] pub struct Open { pub document: Id, pub session: Id, pub generation: Id, pub record: Id, pub path: String, pub anchor: Rectangle, } #[derive(Debug, Clone)] pub struct Set { pub context: Context, pub generation: Id, pub value: Option, } impl Draft { pub fn reference_session(&self) -> Id { self.reference_session } pub fn reference_generation(&self) -> Id { self.reference_generation } pub fn reference_open(&self, project: &Project, path: &str, anchor: Rectangle) -> Open { Open { document: project.id, session: self.reference_session, generation: self.reference_generation, record: self.original, path: path.into(), anchor, } } pub fn reference_matches(&self, request: &Open) -> bool { self.reference_session == request.session && self.reference_generation == request.generation && self.original == request.record && self.live_reference(&request.path) } fn live_reference(&self, path: &str) -> bool { !history_fields::is_path(path) && reference(path) && self .value .pointer(path) .is_some_and(|v| v.is_string() || v.is_null()) } pub fn reference_catalog( &self, project: &Project, request: &Open, revision: u64, ) -> Result { if request.document != project.id || !self.reference_matches(request) { return Err("The reference field changed. Open its chooser again.".into()); } let allowed: BTreeSet<_> = reference_ids(self, project, &request.path) .into_iter() .filter(|id| !id.is_nil()) .collect(); let candidates = allowed .iter() .filter_map(|id| candidate(project, *id)) .collect(); let raw = self .inputs .get(&request.path) .map(|v| v.raw.as_str()) .or_else(|| self.value.pointer(&request.path).and_then(Value::as_str)); let current = match raw { None => Current::Unassigned, Some(raw) => match raw.parse::() { Ok(id) if id.is_nil() => Current::Unassigned, Ok(id) => match candidate(project, id) { Some(c) if allowed.contains(&id) => Current::Valid(c), Some(c) => Current::Incompatible(c), None => Current::Missing(id), }, Err(_) => Current::InvalidRaw(raw.into()), }, }; let context = Context::live( project.id, revision, self.reference_session, self.original, &request.path, optional_reference(&request.path), ) .map_err(str::to_owned)?; State::open( context, candidates, current, format!( "{} · compatible references", friendly(request.path.rsplit('/').next().unwrap_or("Reference")) ), ) .map_err(str::to_owned) } pub fn reference_set(&mut self, project: &Project, intent: &Set) -> Result<(), String> { let value = self.reference_value(project, intent)?; self.shape_scoped(&intent.context.path, value) } pub fn reference_value(&self, project: &Project, intent: &Set) -> Result { let context = &intent.context; if context.document != project.id || context.draft != self.reference_session || context.record != self.original || intent.generation != self.reference_generation || !self.live_reference(&context.path) || context.optional != optional_reference(&context.path) { return Err("The originating reference field is no longer current.".into()); } let value = match intent.value { Some(id) if !id.is_nil() && reference_ids(self, project, &context.path).contains(&id) => { json!(id) } None if context.optional => Value::Null, _ => return Err("Choose a currently compatible reference.".into()), }; Ok(value) } } fn named(project: &Project, id: Id) -> String { if let Some(e) = project.elements.get(&id) { format!("{} · @{}", e.name, e.alias) } else { project.named(id).into() } } fn ancestry(project: &Project, id: Id) -> String { let mut parts = vec![]; let mut seen = BTreeSet::new(); let mut current = project.elements.get(&id).and_then(|e| e.parent); while let Some(id) = current { if !seen.insert(id) { parts.push("[containment cycle]".into()); break; } parts.push(named(project, id)); current = project.elements.get(&id).and_then(|e| e.parent); } parts.reverse(); if parts.is_empty() { project.name.clone() } else { parts.join(" / ") } } pub(crate) fn candidate(project: &Project, id: Id) -> Option { let layer = |layer| format!("{layer:?}").to_uppercase(); if id == project.id { return Some(Candidate { id, name: project.name.clone(), alias: project.alias.clone(), kind: "Project".into(), scope: "Model".into(), containment: "Model root".into(), }); } if let Some(e) = project.elements.get(&id) { return Some(Candidate { id, name: e.name.clone(), alias: e.alias.clone(), kind: e.kind().to_string(), scope: layer(e.layer()), containment: ancestry(project, id), }); } if let Some(r) = project.relations.get(&id) { return Some(Candidate { id, name: r.name.clone(), alias: r.alias.clone(), kind: r.kind().to_string(), scope: project.layer_of(id).map_or_else(|| "Model".into(), layer), containment: format!( "{} → {}", named(project, r.source()), named(project, r.target()) ), }); } for diagram in project.diagrams.values() { if id == diagram.id { return Some(Candidate { id, name: diagram.name.clone(), alias: diagram.alias.clone(), kind: format!("{:?} diagram", diagram.projection), scope: layer(diagram.layer), containment: project.name.clone(), }); } if let Some(node) = diagram.nodes.get(&id) { return Some(Candidate { id, name: project.named(node.element).into(), alias: node.alias.clone(), kind: "Placement".into(), scope: layer(diagram.layer), containment: format!( "{} · @{} / {}", diagram.name, diagram.alias, named(project, node.element) ), }); } } if let Some(link) = project.artifact_links.get(&id) { return Some(Candidate { id, name: link.name.clone(), alias: link.alias.clone(), kind: "Artifact link".into(), scope: project .layer_of(link.owner) .map_or_else(|| "Model".into(), layer), containment: named(project, link.owner), }); } project.traces.get(&id).map(|trace| Candidate { id, name: trace.name.clone(), alias: trace.alias.clone(), kind: "Trace".into(), scope: "OA ↔ SA".into(), containment: format!( "{} ↔ {}", named(project, trace.oa), named(project, trace.sa) ), }) } pub fn field<'a, R: iced::advanced::text::Renderer + 'a>( draft: &'a Draft, project: &'a Project, path: &str, ) -> Element<'a, Message, iced::Theme, R> { let raw = draft .inputs .get(path) .map(|v| v.raw.as_str()) .or_else(|| draft.value.pointer(path).and_then(Value::as_str)); let (label, detail) = match raw { None => ( if optional_reference(path) { "Not assigned" } else { "Choose a reference…" } .into(), String::new(), ), Some(raw) => match raw.parse::() { Ok(id) if id.is_nil() => ("Choose a reference…".into(), String::new()), Ok(id) => match candidate(project, id) { Some(c) => (format!("{} · @{}", c.name, c.alias), c.detail()), None => ("Missing reference".into(), raw.into()), }, Err(_) => ("Invalid reference retained".into(), raw.into()), }, }; let label = crate::views::short(&label, 35); let body = container(row![text(label).size(12).width(Fill), text("⌄").size(12)].spacing(6)) .padding(8) .width(Fill); let path = path.to_owned(); let id = trigger::field_id(draft.reference_generation, &path); let field = trigger::Field::new(body, id, move |anchor| { Message::FieldReferenceOpen(draft.reference_open(project, &path, anchor)) }); if detail.is_empty() { field.into() } else { tooltip( field, container(column![text(detail).size(11)]) .padding(8) .style(style::card), tooltip::Position::Left, ) .into() } }