308 lines
10 KiB
Rust
308 lines
10 KiB
Rust
//! 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<Id>,
|
|
}
|
|
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<State, String> {
|
|
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::<Id>() {
|
|
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<Value, String> {
|
|
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<Candidate> {
|
|
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<Font = iced::Font> + '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::<Id>() {
|
|
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()
|
|
}
|
|
}
|