Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
788 lines
28 KiB
Rust
788 lines
28 KiB
Rust
//! Lazy qualified authoring over the core address resolver. A folded reference
|
|
//! never replaces its retained endpoint with the reference or a same-named leaf.
|
|
use super::*;
|
|
use iced::widget::column;
|
|
use oa_core::topology_context::TopologyIndex;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
|
pub enum Purpose {
|
|
#[default]
|
|
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",
|
|
})
|
|
}
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub enum Event {
|
|
Side(bool),
|
|
Pick(OccurrenceEndpoint),
|
|
Expand(OccurrenceEndpoint),
|
|
Up,
|
|
Root,
|
|
Purpose(Purpose),
|
|
Evidence(Choice),
|
|
Guard(String),
|
|
Search(String),
|
|
Page(usize),
|
|
}
|
|
#[derive(Default, serde::Serialize, serde::Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub(super) struct Draft {
|
|
edit: Option<Id>,
|
|
source: Option<OccurrenceEndpoint>,
|
|
target: Option<OccurrenceEndpoint>,
|
|
target_side: bool,
|
|
purpose: Purpose,
|
|
evidence: Option<Id>,
|
|
guard: String,
|
|
search: String,
|
|
page: usize,
|
|
#[serde(default)]
|
|
scope: Vec<Id>,
|
|
}
|
|
impl Draft {
|
|
pub(super) fn seed_source(&mut self, endpoint: OccurrenceEndpoint) {
|
|
if self.edit.is_none() {
|
|
self.source = Some(endpoint);
|
|
}
|
|
}
|
|
pub(super) fn new(p: &Project, owner: Id, selected: Option<Id>) -> Self {
|
|
if let Some(r) = selected.and_then(|id| p.relations.get(&id))
|
|
&& let RelationBody::ContextualLink {
|
|
owner: caller,
|
|
source,
|
|
target,
|
|
purpose,
|
|
} = &r.body
|
|
&& *caller == owner
|
|
{
|
|
let (purpose, evidence, guard) = match purpose {
|
|
ContextualLinkPurpose::Precedence { guard } => {
|
|
(Purpose::Precedence, None, guard.clone())
|
|
}
|
|
ContextualLinkPurpose::Exchange { exchange } => {
|
|
(Purpose::Exchange, Some(*exchange), String::new())
|
|
}
|
|
ContextualLinkPurpose::CommonWork { work } => {
|
|
(Purpose::CommonWork, Some(*work), String::new())
|
|
}
|
|
};
|
|
return Self {
|
|
edit: Some(r.id),
|
|
source: Some(source.clone()),
|
|
target: Some(target.clone()),
|
|
purpose,
|
|
evidence,
|
|
guard,
|
|
..Self::default()
|
|
};
|
|
}
|
|
Self {
|
|
source: selected
|
|
.filter(|id| is_contribution(p, owner, *id))
|
|
.map(|occurrence| OccurrenceEndpoint {
|
|
references: vec![],
|
|
occurrence,
|
|
}),
|
|
..Self::default()
|
|
}
|
|
}
|
|
pub(super) fn update(&mut self, event: Event) {
|
|
match event {
|
|
Event::Side(target) => self.target_side = target,
|
|
Event::Pick(endpoint) => {
|
|
if self.target_side {
|
|
self.target = Some(endpoint);
|
|
} else {
|
|
self.source = Some(endpoint);
|
|
}
|
|
}
|
|
Event::Expand(endpoint) => {
|
|
self.scope = endpoint.references;
|
|
self.scope.push(endpoint.occurrence);
|
|
self.search.clear();
|
|
self.page = 0;
|
|
}
|
|
Event::Up => {
|
|
self.scope.pop();
|
|
self.search.clear();
|
|
self.page = 0;
|
|
}
|
|
Event::Root => {
|
|
self.scope.clear();
|
|
self.search.clear();
|
|
self.page = 0;
|
|
}
|
|
Event::Purpose(purpose) => self.purpose = purpose,
|
|
Event::Evidence(choice) => self.evidence = Some(choice.id),
|
|
Event::Guard(raw) => self.guard = raw,
|
|
Event::Search(raw) => {
|
|
self.search = raw;
|
|
self.page = 0;
|
|
}
|
|
Event::Page(page) => self.page = page,
|
|
}
|
|
}
|
|
fn purpose(&self) -> Result<ContextualLinkPurpose, String> {
|
|
Ok(match self.purpose {
|
|
Purpose::Precedence => ContextualLinkPurpose::Precedence {
|
|
guard: self.guard.clone(),
|
|
},
|
|
Purpose::Exchange => ContextualLinkPurpose::Exchange {
|
|
exchange: self
|
|
.evidence
|
|
.ok_or("Choose the exact exchange used for assembly.")?,
|
|
},
|
|
Purpose::CommonWork => ContextualLinkPurpose::CommonWork {
|
|
work: self.evidence.ok_or("Choose the shared work identity.")?,
|
|
},
|
|
})
|
|
}
|
|
pub(super) fn command(&self, p: &Project, owner: Id, name: &str) -> Result<Command, String> {
|
|
let source = self
|
|
.source
|
|
.as_ref()
|
|
.ok_or("Choose the qualified source contribution.")?;
|
|
let target = self
|
|
.target
|
|
.as_ref()
|
|
.ok_or("Choose the qualified target contribution.")?;
|
|
let purpose = self.purpose()?;
|
|
TopologyIndex::new(p)
|
|
.validate_link(owner, source, target, &purpose)
|
|
.map_err(|e| e.to_string())?;
|
|
let mut relation = if let Some(id) = self.edit {
|
|
p.relations.get(&id).filter(|r| matches!(&r.body, RelationBody::ContextualLink { owner: caller, .. } if *caller==owner)).ok_or("This caller-owned link no longer exists.")?.clone()
|
|
} else {
|
|
let id = Id::new_v4();
|
|
Relation {
|
|
id,
|
|
alias: p.fresh_alias("contextual_link", "relation", id),
|
|
name: self.purpose.to_string(),
|
|
description: String::new(),
|
|
body: RelationBody::ContextualLink {
|
|
owner,
|
|
source: source.clone(),
|
|
target: target.clone(),
|
|
purpose: purpose.clone(),
|
|
},
|
|
}
|
|
};
|
|
if !name.trim().is_empty() {
|
|
relation.name = name.to_owned();
|
|
}
|
|
relation.body = RelationBody::ContextualLink {
|
|
owner,
|
|
source: source.clone(),
|
|
target: target.clone(),
|
|
purpose,
|
|
};
|
|
let command = if self.edit.is_some() {
|
|
Command::EditRelation(relation.id, relation)
|
|
} else {
|
|
Command::CreateRelation(relation)
|
|
};
|
|
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
|
|
Ok(command)
|
|
}
|
|
}
|
|
|
|
pub(crate) fn address_label(p: &Project, owner: Id, endpoint: &OccurrenceEndpoint) -> String {
|
|
std::iter::once(&owner)
|
|
.chain(endpoint.references.iter())
|
|
.chain(std::iter::once(&endpoint.occurrence))
|
|
.map(|id| {
|
|
p.elements
|
|
.get(id)
|
|
.map(|e| format!("{} (@{})", e.name, e.alias))
|
|
.unwrap_or_else(|| format!("Unavailable {id}"))
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" → ")
|
|
}
|
|
struct AddressRow {
|
|
endpoint: OccurrenceEndpoint,
|
|
reference: bool,
|
|
}
|
|
fn rows(p: &Project, owner: Id, scope: &[Id], search: &str) -> Result<Vec<AddressRow>, String> {
|
|
let index = TopologyIndex::new(p);
|
|
let sequence = index
|
|
.resolve_scope(owner, scope)
|
|
.map_err(|e| e.to_string())?;
|
|
let query = search.trim().to_lowercase();
|
|
Ok(index
|
|
.contributions(sequence)
|
|
.into_iter()
|
|
.filter_map(|occurrence| {
|
|
let endpoint = OccurrenceEndpoint {
|
|
references: scope.to_vec(),
|
|
occurrence,
|
|
};
|
|
if !address_label(p, owner, &endpoint)
|
|
.to_lowercase()
|
|
.contains(&query)
|
|
{
|
|
return None;
|
|
}
|
|
let reference = matches!(
|
|
p.elements.get(&occurrence).and_then(|e| e.shared()),
|
|
Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::SequenceReference { sequence: Some(_) },
|
|
..
|
|
})
|
|
);
|
|
Some(AddressRow {
|
|
endpoint,
|
|
reference,
|
|
})
|
|
})
|
|
.collect())
|
|
}
|
|
fn event(value: Event) -> Message {
|
|
super::event(super::Event::Contextual(value))
|
|
}
|
|
|
|
pub(super) fn preview(
|
|
index: &TopologyIndex<'_>,
|
|
p: &Project,
|
|
owner: Id,
|
|
reference: Id,
|
|
expanded: &BTreeSet<OccurrenceEndpoint>,
|
|
limit: usize,
|
|
) -> Vec<OccurrenceEndpoint> {
|
|
let children = |endpoint: &OccurrenceEndpoint| {
|
|
if index.resolve(owner, endpoint).is_err() {
|
|
return vec![];
|
|
}
|
|
let sequence = p
|
|
.elements
|
|
.get(&endpoint.occurrence)
|
|
.and_then(|e| e.shared())
|
|
.and_then(|body| match body {
|
|
SharedBody::Occurrence {
|
|
body: OccurrenceBody::SequenceReference { sequence },
|
|
..
|
|
} => *sequence,
|
|
_ => None,
|
|
});
|
|
let Some(sequence) = sequence else {
|
|
return vec![];
|
|
};
|
|
let available: BTreeSet<_> = index.contributions(sequence).into_iter().collect();
|
|
let mut references = endpoint.references.clone();
|
|
references.push(endpoint.occurrence);
|
|
p.elements
|
|
.get(&sequence)
|
|
.and_then(|e| e.steps())
|
|
.into_iter()
|
|
.flatten()
|
|
.filter(|id| available.contains(id))
|
|
.map(|occurrence| OccurrenceEndpoint {
|
|
references: references.clone(),
|
|
occurrence: *occurrence,
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let root = OccurrenceEndpoint {
|
|
references: vec![],
|
|
occurrence: reference,
|
|
};
|
|
let mut pending = children(&root);
|
|
pending.reverse();
|
|
let mut visible = vec![];
|
|
while let Some(endpoint) = pending.pop() {
|
|
if visible.len() == limit {
|
|
break;
|
|
}
|
|
if index.resolve(owner, &endpoint).is_err() {
|
|
continue;
|
|
}
|
|
if expanded.contains(&endpoint) {
|
|
pending.extend(children(&endpoint).into_iter().rev());
|
|
}
|
|
visible.push(endpoint);
|
|
}
|
|
visible
|
|
}
|
|
pub(super) fn view<'a>(p: &'a Project, owner: Id, d: &'a Draft) -> Element<'a, Message> {
|
|
let mut out = column![
|
|
text("Choose each endpoint in its caller context. Enter a reference to browse all its contributions; repeated uses stay distinct. Browsing preserves both selected endpoints.").size(12),
|
|
row![button("Choose source").style(if !d.target_side { button::primary } else { style::nav }).on_press(event(Event::Side(false))), button("Choose target").style(if d.target_side { button::primary } else { style::nav }).on_press(event(Event::Side(true)))].spacing(6),
|
|
text(format!("Source: {}", d.source.as_ref().map(|a| address_label(p,owner,a)).unwrap_or("Choose a contribution".into()))).size(12),
|
|
text(format!("Target: {}", d.target.as_ref().map(|a| address_label(p,owner,a)).unwrap_or("Choose a contribution".into()))).size(12),
|
|
pick_list(vec![Purpose::Precedence,Purpose::Exchange,Purpose::CommonWork],Some(d.purpose),|p| event(Event::Purpose(p))).width(Fill).text_size(12).padding(8)
|
|
].spacing(7);
|
|
match d.purpose {
|
|
Purpose::Precedence => {
|
|
out = out.push(
|
|
text_input("Precedence guard (optional)", &d.guard)
|
|
.on_input(|v| event(Event::Guard(v)))
|
|
.padding(8)
|
|
.size(12),
|
|
)
|
|
}
|
|
purpose => {
|
|
let choices = if purpose == Purpose::Exchange {
|
|
targets(p, owner, Tool::Exchange)
|
|
} else {
|
|
targets(p, owner, Tool::Work)
|
|
};
|
|
let selected = d.evidence.map(|id| Choice {
|
|
id,
|
|
label: p.named(id).to_owned(),
|
|
});
|
|
out = out
|
|
.push(
|
|
pick_list(choices, selected, |c| event(Event::Evidence(c)))
|
|
.placeholder("Choose exact engineering identity…")
|
|
.width(Fill)
|
|
.text_size(12)
|
|
.padding(8),
|
|
)
|
|
.push(
|
|
text("Assembly incidence does not imply execution order or fuse executions.")
|
|
.size(11),
|
|
);
|
|
}
|
|
}
|
|
out = out.push(
|
|
text_input("Search all contributions in this scope…", &d.search)
|
|
.on_input(|v| event(Event::Search(v)))
|
|
.padding(7)
|
|
.size(12),
|
|
);
|
|
let scope_label = std::iter::once(&owner)
|
|
.chain(d.scope.iter())
|
|
.map(|id| {
|
|
p.elements
|
|
.get(id)
|
|
.map(|e| format!("{} (@{})", e.name, e.alias))
|
|
.unwrap_or_else(|| format!("Unavailable {id}"))
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" → ");
|
|
out = out
|
|
.push(text(format!("Browsing: {scope_label}")).size(12))
|
|
.push(
|
|
row![
|
|
button("Root").on_press_maybe((!d.scope.is_empty()).then(|| event(Event::Root))),
|
|
button("Up one reference")
|
|
.on_press_maybe((!d.scope.is_empty()).then(|| event(Event::Up)))
|
|
]
|
|
.spacing(6),
|
|
);
|
|
let rows = match rows(p, owner, &d.scope, &d.search) {
|
|
Ok(rows) => rows,
|
|
Err(error) => return out.push(text(error).size(12)).into(),
|
|
};
|
|
let range = super::view::choice_page(rows.len(), d.page);
|
|
let page = range.start / 24;
|
|
out = out.push(
|
|
row![
|
|
button("Previous").on_press_maybe((page > 0).then(|| event(Event::Page(page - 1)))),
|
|
text(format!(
|
|
"{} matching contributions · page {}",
|
|
rows.len(),
|
|
page + 1
|
|
))
|
|
.size(11),
|
|
button("Next")
|
|
.on_press_maybe((range.end < rows.len()).then(|| event(Event::Page(page + 1))))
|
|
]
|
|
.spacing(6),
|
|
);
|
|
for r in &rows[range] {
|
|
let mut line = row![
|
|
button(text(address_label(p, owner, &r.endpoint)).size(12))
|
|
.width(Fill)
|
|
.on_press(event(Event::Pick(r.endpoint.clone())))
|
|
]
|
|
.spacing(5);
|
|
if r.reference {
|
|
line = line
|
|
.push(button("Enter reference").on_press(event(Event::Expand(r.endpoint.clone()))));
|
|
}
|
|
out = out.push(line);
|
|
}
|
|
out.into()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::document::Document;
|
|
use oa_core::source_project::ProjectLimits;
|
|
|
|
fn fixture(layer: Layer) -> (Project, Id, Id, Id, Id, Id) {
|
|
let f = super::super::direct_tests::fixture(layer);
|
|
let mut p = f.p;
|
|
let sequence = p
|
|
.make_element(
|
|
if layer == Layer::Oa {
|
|
Kind::Process
|
|
} else {
|
|
Kind::FunctionalChain
|
|
},
|
|
layer,
|
|
"Reusable check",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let sequence_id = sequence.id;
|
|
p = execute_batch(&p, &[Command::CreateElement(sequence)]).unwrap();
|
|
let leaf = occurrence(
|
|
&p,
|
|
sequence_id,
|
|
"Verify pressure",
|
|
OccurrenceBody::Action {
|
|
action: Some(f.work[0]),
|
|
lifeline: None,
|
|
},
|
|
);
|
|
let leaf_id = leaf.id;
|
|
p = execute_batch(&p, &[Command::CreateElement(leaf)]).unwrap();
|
|
let a = occurrence(
|
|
&p,
|
|
f.owner,
|
|
"First check",
|
|
OccurrenceBody::SequenceReference {
|
|
sequence: Some(sequence_id),
|
|
},
|
|
);
|
|
let b = occurrence(
|
|
&p,
|
|
f.owner,
|
|
"Second check",
|
|
OccurrenceBody::SequenceReference {
|
|
sequence: Some(sequence_id),
|
|
},
|
|
);
|
|
let (a_id, b_id) = (a.id, b.id);
|
|
p = execute_batch(&p, &[Command::CreateElement(a), Command::CreateElement(b)]).unwrap();
|
|
(p, f.owner, a_id, b_id, leaf_id, f.work[0])
|
|
}
|
|
#[test]
|
|
fn qualified_link_gui_repeated_sources_remain_distinct_and_apply_has_exact_source_history() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let (p, owner, a, b, leaf, work) = fixture(layer);
|
|
let mut state = State::default();
|
|
state
|
|
.update(
|
|
&p,
|
|
super::super::Event::Start(owner, Tool::Contextual, None),
|
|
)
|
|
.unwrap();
|
|
let source = OccurrenceEndpoint {
|
|
references: vec![a],
|
|
occurrence: leaf,
|
|
};
|
|
let target = OccurrenceEndpoint {
|
|
references: vec![b],
|
|
occurrence: leaf,
|
|
};
|
|
for event in [
|
|
Event::Pick(source.clone()),
|
|
Event::Side(true),
|
|
Event::Pick(target.clone()),
|
|
Event::Purpose(Purpose::CommonWork),
|
|
Event::Evidence(Choice {
|
|
id: work,
|
|
label: String::new(),
|
|
}),
|
|
] {
|
|
state
|
|
.update(&p, super::super::Event::Contextual(event))
|
|
.unwrap();
|
|
}
|
|
let raw = state.recovery_snapshot().unwrap().unwrap();
|
|
let restored = State::from_recovery(raw.clone()).unwrap();
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw));
|
|
let command = restored.prepare_pending(&p).unwrap();
|
|
let Command::CreateRelation(record) = &command else {
|
|
panic!("one caller-owned record")
|
|
};
|
|
assert_eq!(
|
|
record.body,
|
|
RelationBody::ContextualLink {
|
|
owner,
|
|
source: source.clone(),
|
|
target: target.clone(),
|
|
purpose: ContextualLinkPurpose::CommonWork { work }
|
|
}
|
|
);
|
|
let expected = execute_batch(&p, std::slice::from_ref(&command)).unwrap();
|
|
let mut doc = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
|
|
let before = doc.bundle().clone();
|
|
doc.execute(command.clone()).unwrap();
|
|
assert_eq!(doc.project(), &expected);
|
|
assert_eq!(
|
|
doc.project().elements,
|
|
p.elements,
|
|
"no copied work or source occurrences"
|
|
);
|
|
let after = doc.bundle().clone();
|
|
let reopened = Document::from_bundle(after.clone(), ProjectLimits::default()).unwrap();
|
|
assert_eq!(reopened.project(), &expected);
|
|
assert!(doc.undo());
|
|
assert_eq!(doc.bundle(), &before);
|
|
assert!(doc.redo());
|
|
assert_eq!(doc.bundle(), &after);
|
|
assert_ne!(
|
|
address_label(&p, owner, &source),
|
|
address_label(&p, owner, &target)
|
|
);
|
|
assert!(
|
|
Graph::new(&expected, owner, record.id)
|
|
.edges
|
|
.iter()
|
|
.any(|edge| edge.id == record.id && edge.incidence)
|
|
);
|
|
}
|
|
}
|
|
#[test]
|
|
fn qualified_link_gui_rejected_endpoint_keeps_exact_raw_intent_after_recovery() {
|
|
let (p, owner, a, _, leaf, _) = fixture(Layer::Oa);
|
|
let mut state = State::default();
|
|
state
|
|
.update(
|
|
&p,
|
|
super::super::Event::Start(owner, Tool::Contextual, None),
|
|
)
|
|
.unwrap();
|
|
for event in [
|
|
Event::Pick(OccurrenceEndpoint {
|
|
references: vec![a],
|
|
occurrence: leaf,
|
|
}),
|
|
Event::Side(true),
|
|
Event::Pick(OccurrenceEndpoint {
|
|
references: vec![Id::new_v4()],
|
|
occurrence: leaf,
|
|
}),
|
|
Event::Guard(" pressure > && ".into()),
|
|
] {
|
|
state
|
|
.update(&p, super::super::Event::Contextual(event))
|
|
.unwrap();
|
|
}
|
|
let before = state.recovery_snapshot().unwrap();
|
|
assert!(state.prepare_pending(&p).is_err());
|
|
assert_eq!(state.recovery_snapshot().unwrap(), before);
|
|
let restored = State::from_recovery(before.clone().unwrap()).unwrap();
|
|
assert!(restored.prepare_pending(&p).is_err());
|
|
assert_eq!(restored.recovery_snapshot().unwrap(), before);
|
|
}
|
|
#[test]
|
|
fn qualified_link_gui_deleted_transient_selection_does_not_seed_or_hide_retained_authored_endpoint()
|
|
{
|
|
let (p, owner, reference, _, leaf, _) = fixture(Layer::Oa);
|
|
let endpoint = OccurrenceEndpoint {
|
|
references: vec![reference],
|
|
occurrence: leaf,
|
|
};
|
|
let mut state = State::default();
|
|
state.select_context(oa_core::topology_context::ContextAddress {
|
|
owner,
|
|
endpoint: endpoint.clone(),
|
|
});
|
|
state
|
|
.update(
|
|
&p,
|
|
super::super::Event::Start(owner, Tool::Contextual, None),
|
|
)
|
|
.unwrap();
|
|
let before = state.recovery_snapshot().unwrap();
|
|
let deleted = execute_batch(&p, &[Command::Delete(leaf)]).unwrap();
|
|
assert!(state.qualified_selection(&deleted, owner).is_none());
|
|
assert_eq!(
|
|
state.recovery_snapshot().unwrap(),
|
|
before,
|
|
"Authored endpoint is retained for explicit repair"
|
|
);
|
|
assert!(state.prepare_pending(&deleted).is_err());
|
|
state.remember(owner, reference);
|
|
assert_eq!(state.back(&deleted), Some((owner, reference)));
|
|
assert!(state.selected_context.is_none());
|
|
state.draft = None;
|
|
state
|
|
.update(
|
|
&deleted,
|
|
super::super::Event::Start(owner, Tool::Contextual, None),
|
|
)
|
|
.unwrap();
|
|
assert!(
|
|
state
|
|
.draft
|
|
.as_ref()
|
|
.unwrap()
|
|
.contextual
|
|
.as_ref()
|
|
.unwrap()
|
|
.source
|
|
.is_none()
|
|
);
|
|
}
|
|
#[test]
|
|
fn qualified_link_gui_scope_paging_and_search_reach_tail_beyond_512_without_losing_endpoints() {
|
|
let (p, owner, reference, _, leaf, _) = fixture(Layer::Oa);
|
|
let sequence = p.elements[&leaf].parent.unwrap();
|
|
let mut commands = Vec::new();
|
|
let mut tails = Vec::new();
|
|
for (scope_index, scope) in [owner, sequence].into_iter().enumerate() {
|
|
for n in 0..520 {
|
|
let mut record = occurrence(
|
|
&p,
|
|
scope,
|
|
&format!("Contribution {n:04}"),
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
record.alias = format!("scope_{scope_index}_contribution_{n:04}");
|
|
if n == 519 {
|
|
tails.push(record.id);
|
|
}
|
|
commands.push(Command::CreateElement(record));
|
|
}
|
|
}
|
|
let p = execute_batch(&p, &commands).unwrap();
|
|
let all = rows(&p, owner, &[], "").unwrap();
|
|
assert!(all.len() > 512);
|
|
let last_page = super::super::view::choice_page(all.len(), usize::MAX);
|
|
assert!(
|
|
all[last_page]
|
|
.iter()
|
|
.any(|row| row.endpoint.occurrence == tails[0])
|
|
);
|
|
let found = rows(&p, owner, &[], "Contribution 0519").unwrap();
|
|
assert_eq!(found.len(), 1);
|
|
assert_eq!(found[0].endpoint.occurrence, tails[0]);
|
|
let mut draft = Draft::default();
|
|
draft.update(Event::Pick(OccurrenceEndpoint {
|
|
references: vec![reference],
|
|
occurrence: leaf,
|
|
}));
|
|
draft.update(Event::Side(true));
|
|
draft.update(Event::Pick(found[0].endpoint.clone()));
|
|
let chosen = (draft.source.clone(), draft.target.clone());
|
|
draft.update(Event::Expand(OccurrenceEndpoint {
|
|
references: vec![],
|
|
occurrence: reference,
|
|
}));
|
|
draft.update(Event::Search("Contribution 0519".into()));
|
|
let nested = rows(&p, owner, &draft.scope, &draft.search).unwrap();
|
|
assert_eq!(nested.len(), 1);
|
|
assert_eq!(
|
|
nested[0].endpoint,
|
|
OccurrenceEndpoint {
|
|
references: vec![reference],
|
|
occurrence: tails[1]
|
|
}
|
|
);
|
|
let saved = serde_json::to_value(&draft).unwrap();
|
|
let mut restored: Draft = serde_json::from_value(saved.clone()).unwrap();
|
|
assert_eq!(serde_json::to_value(&restored).unwrap(), saved);
|
|
restored.update(Event::Up);
|
|
assert!(restored.scope.is_empty());
|
|
assert_eq!((restored.source.clone(), restored.target.clone()), chosen);
|
|
assert!(restored.command(&p, owner, "Tail constraint").is_ok());
|
|
let mut historical = saved;
|
|
historical.as_object_mut().unwrap().remove("scope");
|
|
let restored: Draft = serde_json::from_value(historical).unwrap();
|
|
assert!(restored.scope.is_empty());
|
|
assert_eq!((restored.source, restored.target), chosen);
|
|
}
|
|
#[test]
|
|
fn qualified_link_gui_nested_preview_folds_only_the_exact_reference_instance() {
|
|
let (mut p, owner, a, b, leaf, _) = fixture(Layer::Oa);
|
|
let source = p.elements[&leaf].parent.unwrap();
|
|
let deep = p
|
|
.make_element(Kind::Process, Layer::Oa, "Nested definition", None)
|
|
.unwrap();
|
|
let deep_id = deep.id;
|
|
p = execute_batch(&p, &[Command::CreateElement(deep)]).unwrap();
|
|
let deep_leaf = occurrence(
|
|
&p,
|
|
deep_id,
|
|
"Nested control",
|
|
OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
);
|
|
let deep_leaf_id = deep_leaf.id;
|
|
let nested = occurrence(
|
|
&p,
|
|
source,
|
|
"Nested use",
|
|
OccurrenceBody::SequenceReference {
|
|
sequence: Some(deep_id),
|
|
},
|
|
);
|
|
let nested_id = nested.id;
|
|
p = execute_batch(
|
|
&p,
|
|
&[
|
|
Command::CreateElement(deep_leaf),
|
|
Command::CreateElement(nested),
|
|
],
|
|
)
|
|
.unwrap();
|
|
let path = OccurrenceEndpoint {
|
|
references: vec![a],
|
|
occurrence: nested_id,
|
|
};
|
|
let mut state = State::default();
|
|
assert!(state.expand_context(&p, owner, path.clone()));
|
|
state.expanded.insert(b);
|
|
let graph = Graph::expanded_qualified(
|
|
&p,
|
|
owner,
|
|
owner,
|
|
&state.expanded,
|
|
&state.contextual_expanded,
|
|
);
|
|
assert!(
|
|
graph
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.id == a)
|
|
.unwrap()
|
|
.preview
|
|
.iter()
|
|
.any(|(v, _)| v.references == vec![a, nested_id] && v.occurrence == deep_leaf_id)
|
|
);
|
|
assert!(
|
|
!graph
|
|
.nodes
|
|
.iter()
|
|
.find(|n| n.id == b)
|
|
.unwrap()
|
|
.preview
|
|
.iter()
|
|
.any(|(v, _)| v.occurrence == deep_leaf_id)
|
|
);
|
|
assert!(state.expand_context(&p, owner, path));
|
|
let graph = Graph::expanded_qualified(
|
|
&p,
|
|
owner,
|
|
owner,
|
|
&state.expanded,
|
|
&state.contextual_expanded,
|
|
);
|
|
assert!(
|
|
!graph
|
|
.nodes
|
|
.iter()
|
|
.any(|n| n.preview.iter().any(|(v, _)| v.occurrence == deep_leaf_id))
|
|
);
|
|
}
|
|
}
|