Files
ARCADIA-Studio/crates/oa-desktop/src/forms/reference_chooser/adapter.rs
T

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()
}
}