Files
ARCADIA-Studio/crates/oa-desktop/src/data_view.rs
T

1096 lines
38 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! A bounded catalog and focused dictionary preserve original member identities.
use crate::{
app::{App, Choice, Message},
style,
};
use iced::{
Element as View, Fill,
widget::{button, column, container, pick_list, row, scrollable, text},
};
use oa_core::*;
use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy)]
pub enum Template {
Item,
Operation,
PhysicalFlow,
Record,
Boolean,
Integer,
Real,
Text,
Enumeration,
Interface,
Field,
}
#[derive(Debug, Clone)]
pub enum Event {
Create(Template, Option<Id>),
Allocate {
interface: Id,
item: Id,
},
Associate {
source: Id,
target: Id,
},
Page {
section: Section,
owner: Option<Id>,
page: usize,
},
}
const PAGE_SIZE: usize = 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Section {
Catalog,
Fields,
Allocations,
Associations,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct State {
catalog: usize,
owner: Option<Id>,
fields: usize,
allocations: usize,
associations: usize,
}
impl State {
/// Browsing changes no model or property draft.
pub fn update(&mut self, event: &Event) -> bool {
let Event::Page {
section,
owner,
page,
} = event
else {
return false;
};
if *section != Section::Catalog && self.owner != *owner {
self.owner = *owner;
self.fields = 0;
self.allocations = 0;
self.associations = 0;
}
*match section {
Section::Catalog => &mut self.catalog,
Section::Fields => &mut self.fields,
Section::Allocations => &mut self.allocations,
Section::Associations => &mut self.associations,
} = *page;
true
}
fn page(&self, section: Section, owner: Option<Id>) -> usize {
if section != Section::Catalog && self.owner != owner {
return 0;
}
match section {
Section::Catalog => self.catalog,
Section::Fields => self.fields,
Section::Allocations => self.allocations,
Section::Associations => self.associations,
}
}
}
fn page_range(total: usize, page: usize) -> std::ops::Range<usize> {
let page = page.min(total.saturating_sub(1) / PAGE_SIZE);
let start = page * PAGE_SIZE;
start..start.saturating_add(PAGE_SIZE).min(total)
}
pub fn command(project: &Project, layer: Layer, event: Event) -> Result<Command, String> {
match event {
Event::Page { .. } => Err("Paging is a view action.".into()),
Event::Create(template, parent) => {
let (kind, name) = match template {
Template::Item => (Kind::ExchangeItem, "New exchange item"),
Template::Operation => (Kind::ExchangeItem, "New operation"),
Template::PhysicalFlow => (Kind::ExchangeItem, "New physical flow"),
Template::Interface => (Kind::Interface, "New interface"),
Template::Field => (Kind::DataField, "New field"),
_ => (Kind::DataType, "New data type"),
};
let mut e = project
.make_element(kind, layer, name, parent)
.ok_or("This data kind is unavailable in the selected layer.")?;
let body = match template {
Template::Item | Template::Operation | Template::PhysicalFlow => {
SharedBody::ExchangeItem {
kind: if matches!(template, Template::PhysicalFlow) {
ExchangeItemKind::Material
} else {
ExchangeItemKind::Information
},
mechanism: match template {
Template::Operation => ExchangeMechanism::Operation,
Template::PhysicalFlow => ExchangeMechanism::Flow,
_ => ExchangeMechanism::Unspecified,
},
supertype: None,
}
}
Template::Interface => SharedBody::Interface { supertype: None },
Template::Field => {
let owner = parent
.and_then(|id| project.elements.get(&id))
.ok_or("Choose the record or item that owns this field.")?;
if owner.layer() != layer
|| !(owner.kind() == Kind::ExchangeItem
|| data::is_record(project, owner.id))
{
return Err("Choose a record or exchange item in this layer.".into());
}
let names: BTreeSet<_> = data::effective_fields(project, owner.id)
.into_iter()
.map(|f| f.name.as_str())
.collect();
let mut i = 1;
while names.contains(e.name.as_str()) {
i += 1;
e.name = format!("New field {i}");
}
SharedBody::DataField {
type_ref: TypeRef::String,
minimum: 1,
maximum: Some(1),
unit: String::new(),
direction: if data::item_mechanism(project, owner.id)
== Some(ExchangeMechanism::Operation)
{
ParameterDirection::In
} else {
ParameterDirection::Unspecified
},
storage: FieldStorage::Value,
default: None,
}
}
_ => SharedBody::DataType {
definition: match template {
Template::Boolean => DataTypeDefinition::Boolean,
Template::Integer => DataTypeDefinition::Integer {
minimum: None,
maximum: None,
},
Template::Real => DataTypeDefinition::Real {
minimum: None,
maximum: None,
},
Template::Text => DataTypeDefinition::String {
minimum_length: 0,
maximum_length: None,
},
Template::Enumeration => DataTypeDefinition::Enumeration {
literals: vec!["New value".into()],
},
_ => DataTypeDefinition::Record { supertype: None },
},
default: None,
unit: String::new(),
},
};
e.body = ElementBody::shared_in(layer, body);
Ok(Command::CreateElement(e))
}
Event::Allocate { interface, item } => {
let id = Id::new_v4();
let order = data::effective_interface_allocations(project, interface).iter().filter_map(|r| match r.body { RelationBody::InterfaceItemAllocation { order, .. } => Some(order), _ => None }).max().map_or(Ok(0), |order| order.checked_add(1).ok_or("Allocation order is full; edit existing orders before adding another allocation."))?;
Ok(Command::CreateRelation(Relation {
id,
alias: project.fresh_alias("item_allocation", "allocation", id),
name: "Interface item allocation".into(),
description: String::new(),
body: RelationBody::InterfaceItemAllocation {
source: interface,
target: item,
order,
transmission: TransmissionProtocol::Unspecified,
acquisition: AcquisitionProtocol::Unspecified,
},
}))
}
Event::Associate { source, target } => {
let id = Id::new_v4();
Ok(Command::CreateRelation(Relation {
id,
alias: project.fresh_alias("data_association", "association", id),
name: "Data association".into(),
description: String::new(),
body: RelationBody::DataAssociation {
source,
target,
source_role: "source".into(),
target_role: "target".into(),
source_minimum: 0,
source_maximum: None,
target_minimum: 0,
target_maximum: None,
source_navigable: false,
target_navigable: false,
},
}))
}
}
}
fn action(label: &str, event: Event) -> View<'static, Message> {
button(text(label.to_owned()).size(11))
.style(style::nav)
.padding(7)
.on_press(Message::Data(event))
.into()
}
fn open(label: impl Into<String>, id: Id) -> View<'static, Message> {
button(text(label.into()).size(12))
.style(style::nav)
.padding(6)
.on_press(Message::Select(id))
.into()
}
fn count(minimum: u32, maximum: Option<u32>) -> String {
format!(
"{minimum}..{}",
maximum.map_or("*".into(), |v| v.to_string())
)
}
fn type_label(p: &Project, reference: &TypeRef) -> String {
match reference {
TypeRef::Named(id) => p.named(*id).into(),
other => format!("{other:?}"),
}
}
fn short_label(value: &str) -> String {
let mut chars = value.chars();
let mut text: String = chars.by_ref().take(120).collect();
if chars.next().is_some() {
text.push('…');
}
text
}
fn literal_label(value: &DataValue) -> String {
match value {
DataValue::Boolean(v) => v.to_string(),
DataValue::Integer(v) => v.to_string(),
DataValue::Real(v) => v.to_string(),
DataValue::String(v) | DataValue::Enumeration(v) => serde_json::json!(v).to_string(),
}
}
fn default_label(value: &Option<Vec<DataValue>>) -> String {
match value {
None => "default absent".into(),
Some(v) if v.is_empty() => "explicit empty default []".into(),
Some(v) => format!(
"default [{}{}]",
v.iter()
.take(3)
.map(|v| short_label(&literal_label(v)))
.collect::<Vec<_>>()
.join(", "),
if v.len() > 3 { ", …" } else { "" }
),
}
}
fn definition_label(definition: &DataTypeDefinition) -> String {
match definition {
DataTypeDefinition::Boolean => "Boolean".into(),
DataTypeDefinition::Integer { minimum, maximum } => format!(
"Integer · {} … {}",
minimum.map_or("−∞".into(), |v| v.to_string()),
maximum.map_or("+∞".into(), |v| v.to_string())
),
DataTypeDefinition::Real { minimum, maximum } => format!(
"Real · {} … {}",
minimum.map_or("−∞".into(), |v| v.to_string()),
maximum.map_or("+∞".into(), |v| v.to_string())
),
DataTypeDefinition::String {
minimum_length,
maximum_length,
} => format!(
"Text · {} Unicode characters",
count(*minimum_length, *maximum_length)
),
DataTypeDefinition::Enumeration { literals } => format!(
"Enumeration · {} literals · {}",
literals.len(),
literals
.iter()
.take(4)
.map(|v| short_label(v))
.collect::<Vec<_>>()
.join(" · ")
),
DataTypeDefinition::Record { .. } => "Record".into(),
}
}
fn pager(
state: &State,
section: Section,
owner: Option<Id>,
total: usize,
) -> View<'static, Message> {
let range = page_range(total, state.page(section, owner));
let page = range.start / PAGE_SIZE;
let mut previous = button(text("Previous").size(11)).style(style::nav);
if page > 0 {
previous = previous.on_press(Message::Data(Event::Page {
section,
owner,
page: page - 1,
}));
}
let mut next = button(text("Next").size(11)).style(style::nav);
if range.end < total {
next = next.on_press(Message::Data(Event::Page {
section,
owner,
page: page + 1,
}));
}
row![
previous,
text(if total == 0 {
"0 entries".into()
} else {
format!("{}–{} of {total}", range.start + 1, range.end)
})
.size(11),
next
]
.spacing(8)
.align_y(iced::Center)
.into()
}
fn catalog_entries(p: &Project, layer: Layer) -> Vec<&Element> {
let mut entries = p
.elements
.values()
.filter(|e| {
e.layer() == layer
&& matches!(
e.kind(),
Kind::ExchangeItem | Kind::DataType | Kind::Interface | Kind::Port
)
})
.collect::<Vec<_>>();
entries.sort_by(|a, b| {
(a.kind().label(), &a.name, &a.alias).cmp(&(b.kind().label(), &b.name, &b.alias))
});
entries
}
fn focused_owner(p: &Project, selected: Id) -> Option<&Element> {
if let Some(element) = p.elements.get(&selected) {
return if element.kind() == Kind::DataField {
element.parent.and_then(|id| p.elements.get(&id))
} else if matches!(
element.kind(),
Kind::ExchangeItem | Kind::DataType | Kind::Interface | Kind::Port
) {
Some(element)
} else {
None
};
}
p.relations
.get(&selected)
.filter(|r| {
matches!(
r.kind(),
RelationKind::InterfaceItemAllocation | RelationKind::DataAssociation
)
})
.and_then(|r| p.elements.get(&r.source()))
}
fn field_row(p: &Project, owner: Id, field: &Element, selected: Id) -> View<'static, Message> {
let Some(SharedBody::DataField {
type_ref,
minimum,
maximum,
direction,
storage,
default,
..
}) = field.shared()
else {
return text("").into();
};
let mut content =
column![open(format!("{} · @{}", field.name, field.alias), field.id)].spacing(4);
let unit = data::effective_field_unit(p, field)
.filter(|s| !s.is_empty())
.map(|s| format!(" · {s}"))
.unwrap_or_default();
let type_default = if default.is_none() {
data::effective_element_default(p, field)
.map(|d| {
format!(
" · type default {} (per element)",
short_label(&literal_label(d))
)
})
.unwrap_or_default()
} else {
String::new()
};
content = content.push(
text(format!(
"{} [{}] · {storage:?} · {direction:?}{unit} · {}{type_default}",
type_label(p, type_ref),
count(*minimum, *maximum),
default_label(default)
))
.size(11)
.color(style::MUTED),
);
if let Some(origin) = field.parent.filter(|id| *id != owner) {
content = content.push(open(
format!("Inherited from {} · open original owner", p.named(origin)),
origin,
));
}
if let TypeRef::Named(id) = type_ref {
content = content.push(open(
if data::is_record(p, *id) {
format!(
"{} · {}",
if *storage == FieldStorage::Reference {
"Open referenced record"
} else {
"Explore nested value fields"
},
p.named(*id)
)
} else {
format!("Open named type · {}", p.named(*id))
},
*id,
));
}
container(content)
.padding(8)
.width(Fill)
.style(if selected == field.id {
style::card
} else {
style::subtle
})
.into()
}
fn focused_details(app: &App, e: &Element) -> View<'static, Message> {
let p = app.project();
let state = &app.data_state;
let mut card = column![
text("Focused definition").size(16),
open(format!("{} · @{}", e.name, e.alias), e.id),
button(text("Edit definition").size(11))
.style(style::nav)
.on_press(Message::EditProperties(e.id))
]
.spacing(8);
if let Some(base) = data::supertype(e) {
card = card.push(open(format!("Specializes {}", p.named(base)), base));
}
match e.shared() {
Some(SharedBody::ExchangeItem {
kind, mechanism, ..
}) => card = card.push(text(format!("{kind:?} · {mechanism:?}")).size(12)),
Some(SharedBody::DataType {
definition,
default,
unit,
}) => {
card = card.push(
text(format!(
"{}{} · {}",
definition_label(definition),
if unit.is_empty() {
String::new()
} else {
format!(" · {unit}")
},
default
.as_ref()
.map_or("default absent".into(), |d| format!(
"default {}",
short_label(&literal_label(d))
))
))
.size(12),
);
}
Some(SharedBody::Interface { .. }) => {
let allocations = data::effective_interface_allocations(p, e.id);
card = card.push(text("Effective item allocations").size(13));
card = card.push(pager(
state,
Section::Allocations,
Some(e.id),
allocations.len(),
));
for allocation in &allocations[page_range(
allocations.len(),
state.page(Section::Allocations, Some(e.id)),
)] {
let RelationBody::InterfaceItemAllocation {
target,
order,
transmission,
acquisition,
..
} = allocation.body
else {
continue;
};
let inherited = allocation.source() != e.id;
card = card.push(open(
format!(
"{order} · {} · {transmission:?} / {acquisition:?}{}",
p.named(target),
if inherited {
format!(" · inherited from {}", p.named(allocation.source()))
} else {
String::new()
}
),
allocation.id,
));
let mut controls = row![
open("Open item", target),
button(
text(if inherited {
"Edit original allocation"
} else {
"Protocols / order"
})
.size(11)
)
.style(style::nav)
.on_press(Message::EditProperties(allocation.id))
]
.spacing(5);
if !inherited {
controls = controls.push(
button(text("Remove allocation").size(11))
.style(style::nav)
.on_press(Message::Command(Command::Delete(allocation.id))),
);
}
card = card.push(controls);
}
let allocated = allocations
.iter()
.map(|r| r.target())
.collect::<BTreeSet<_>>();
let choices = p
.elements
.values()
.filter(|item| {
item.layer() == e.layer()
&& item.kind() == Kind::ExchangeItem
&& !allocated.contains(&item.id)
})
.map(|item| Choice {
id: item.id,
label: format!("{} · @{}", item.name, item.alias),
})
.collect::<Vec<_>>();
let interface = e.id;
card = card.push(
pick_list(choices, None::<Choice>, move |c| {
Message::Data(Event::Allocate {
interface,
item: c.id,
})
})
.placeholder("Allocate an existing item…")
.text_size(12)
.padding(7),
);
}
_ => {}
}
if e.kind() == Kind::ExchangeItem || data::is_record(p, e.id) {
let fields = data::effective_fields(p, e.id);
card = card.push(text("Effective fields · base fields followed by local fields").size(13));
card = card.push(pager(state, Section::Fields, Some(e.id), fields.len()));
for field in &fields[page_range(fields.len(), state.page(Section::Fields, Some(e.id)))] {
card = card.push(field_row(p, e.id, field, app.selected));
}
card = card.push(action(
if data::item_mechanism(p, e.id) == Some(ExchangeMechanism::Operation) {
"+ Input parameter"
} else {
"+ Field"
},
Event::Create(Template::Field, Some(e.id)),
));
}
if data::is_record(p, e.id) {
let source = e.id;
let choices = p
.elements
.values()
.filter(|target| target.layer() == e.layer() && data::is_record(p, target.id))
.map(|target| Choice {
id: target.id,
label: format!("{} · @{}", target.name, target.alias),
})
.collect::<Vec<_>>();
card = card.push(
pick_list(choices, None::<Choice>, move |c| {
Message::Data(Event::Associate {
source,
target: c.id,
})
})
.placeholder("Associate with a record…")
.text_size(12)
.padding(7),
);
let associations = p
.relations
.values()
.filter(|r| {
r.kind() == RelationKind::DataAssociation
&& (r.source() == e.id || r.target() == e.id)
})
.collect::<Vec<_>>();
if !associations.is_empty() {
card = card.push(text("Reference associations").size(13));
card = card.push(pager(
state,
Section::Associations,
Some(e.id),
associations.len(),
));
for relation in &associations[page_range(
associations.len(),
state.page(Section::Associations, Some(e.id)),
)] {
card = card.push(open(
format!(
"Association · {} ↔ {} · @{}",
p.named(relation.source()),
p.named(relation.target()),
relation.alias
),
relation.id,
));
if let RelationBody::DataAssociation {
source_navigable,
target_navigable,
..
} = &relation.body
{
card = card.push(
text(format!(
"{}{} · {}{}",
data::association_end_label(relation, true).unwrap_or_default(),
if *source_navigable {
" · navigable"
} else {
""
},
data::association_end_label(relation, false).unwrap_or_default(),
if *target_navigable {
" · navigable"
} else {
""
}
))
.size(11)
.color(style::MUTED),
);
}
}
}
}
container(card)
.padding(12)
.width(Fill)
.style(style::card)
.into()
}
pub fn view(app: &App) -> View<'_, Message> {
let p = app.project();
let mut content = column![
text("Data & interfaces").size(23),
text("Select a definition to explore its fields, nested types and interface contracts.")
.size(12)
.color(style::MUTED),
row![
action("+ Item", Event::Create(Template::Item, None)),
action("+ Operation", Event::Create(Template::Operation, None)),
action(
"+ Physical flow",
Event::Create(Template::PhysicalFlow, None)
),
action("+ Interface", Event::Create(Template::Interface, None))
]
.spacing(5),
row![
action("+ Record", Event::Create(Template::Record, None)),
action("+ Boolean", Event::Create(Template::Boolean, None)),
action("+ Integer", Event::Create(Template::Integer, None)),
action("+ Real", Event::Create(Template::Real, None)),
action("+ Text", Event::Create(Template::Text, None)),
action("+ Enumeration", Event::Create(Template::Enumeration, None))
]
.spacing(5)
]
.spacing(14);
if let Some(owner) = focused_owner(p, app.selected).filter(|e| e.layer() == app.layer) {
content = content.push(focused_details(app, owner));
}
let entries = catalog_entries(p, app.layer);
content = content.push(text("Dictionary catalog").size(16));
content = content.push(pager(
&app.data_state,
Section::Catalog,
None,
entries.len(),
));
for e in &entries[page_range(entries.len(), app.data_state.page(Section::Catalog, None))] {
content = content.push(
container(
column![
open(format!("{} · @{}", e.name, e.alias), e.id),
text(e.kind().label()).size(11).color(style::MUTED)
]
.spacing(3),
)
.padding(8)
.width(Fill)
.style(style::subtle),
);
}
scrollable(content.padding(24)).into()
}
#[cfg(test)]
mod tests {
use super::*;
fn send(app: &mut App, message: Message) {
let _ = app.update(message);
}
#[test]
fn contextual_data_actions_use_typed_defaults_and_one_undo() {
let mut app = App::with_project(Project::empty());
let before = app.project().clone();
send(
&mut app,
Message::Data(Event::Create(Template::PhysicalFlow, None)),
);
let flow = app.selected;
assert!(
matches!(
app.project().elements[&flow].shared(),
Some(SharedBody::ExchangeItem {
kind: ExchangeItemKind::Material,
mechanism: ExchangeMechanism::Flow,
..
})
),
"{}",
app.status
);
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
send(
&mut app,
Message::Data(Event::Create(Template::Operation, None)),
);
let operation = app.selected;
send(
&mut app,
Message::Data(Event::Create(Template::Field, Some(operation))),
);
let field = app.selected;
assert_eq!(app.project().elements[&field].parent, Some(operation));
assert!(
matches!(
app.project().elements[&field].shared(),
Some(SharedBody::DataField {
minimum: 1,
maximum: Some(1),
direction: ParameterDirection::In,
storage: FieldStorage::Value,
default: None,
..
})
),
"{}",
app.status
);
send(&mut app, Message::Undo);
assert!(!app.project().contains(field));
assert!(app.project().contains(operation));
}
#[test]
fn allocation_rows_edit_relationship_identity_and_remove_only_that_allocation() {
let mut app = App::with_project(Project::empty());
send(&mut app, Message::Data(Event::Create(Template::Item, None)));
let item = app.selected;
send(
&mut app,
Message::Data(Event::Create(Template::Interface, None)),
);
let first = app.selected;
send(
&mut app,
Message::Data(Event::Create(Template::Interface, None)),
);
let second = app.selected;
send(
&mut app,
Message::Data(Event::Allocate {
interface: first,
item,
}),
);
let allocation = app.selected;
assert_eq!(
app.project().relations[&allocation].source(),
first,
"{}",
app.status
);
send(
&mut app,
Message::Data(Event::Allocate {
interface: second,
item,
}),
);
let other = app.selected;
send(&mut app, Message::EditProperties(allocation));
send(
&mut app,
Message::FieldShape(
"/body/InterfaceItemAllocation/transmission".into(),
serde_json::json!("Synchronous"),
),
);
send(&mut app, Message::Apply);
assert!(matches!(
app.project().relations[&allocation].body,
RelationBody::InterfaceItemAllocation {
transmission: TransmissionProtocol::Synchronous,
..
}
));
assert!(matches!(
app.project().relations[&other].body,
RelationBody::InterfaceItemAllocation {
transmission: TransmissionProtocol::Unspecified,
..
}
));
let before = app.project().clone();
let source_before = app.source_snapshot.clone();
send(&mut app, Message::Command(Command::Delete(allocation)));
assert!(!app.project().contains(allocation));
assert!(app.project().contains(item));
assert!(app.project().contains(other));
send(&mut app, Message::Undo);
assert_eq!(app.project(), &before);
assert_eq!(app.source_snapshot, source_before);
assert_eq!(data::interface_items(app.project(), first), vec![item]);
let _ = view(&app);
}
#[test]
fn association_creation_is_a_separate_relation_with_editable_end_contracts() {
let mut app = App::with_project(Project::empty());
send(
&mut app,
Message::Data(Event::Create(Template::Record, None)),
);
let record = app.selected;
send(
&mut app,
Message::Data(Event::Associate {
source: record,
target: record,
}),
);
let association = app.selected;
assert!(
matches!(app.project().relations[&association].body,RelationBody::DataAssociation {source,target,source_navigable:false,target_navigable:false,..} if source==record&&target==record),
"{}",
app.status
);
send(&mut app, Message::EditProperties(association));
send(
&mut app,
Message::Field("/body/DataAssociation/source_role".into(), "parent".into()),
);
send(
&mut app,
Message::Field(
"/body/DataAssociation/target_role".into(),
"children".into(),
),
);
send(
&mut app,
Message::FieldShape(
"/body/DataAssociation/source_maximum".into(),
serde_json::json!(1),
),
);
send(
&mut app,
Message::FieldShape(
"/body/DataAssociation/target_navigable".into(),
serde_json::json!(true),
),
);
send(&mut app, Message::Apply);
assert_eq!(
data::association_end_label(&app.project().relations[&association], true).as_deref(),
Some("parent [0..1]")
);
assert!(data::effective_fields(app.project(), record).is_empty());
}
#[test]
fn large_inherited_dictionary_has_bounded_widgets_and_every_original_field_is_reachable() {
let mut p = Project::empty();
let mut base = None;
let mut originals = vec![];
for i in 0..512 {
let record = p
.add_element(Kind::DataType, Layer::Oa, &format!("Record {i:03}"), None)
.unwrap();
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType {
definition: DataTypeDefinition::Record { supertype: base },
default: None,
unit: String::new(),
};
let field = p
.add_element(
Kind::DataField,
Layer::Oa,
&format!("Value {i}"),
Some(record),
)
.unwrap();
originals.push(field);
base = Some(record);
}
let selected = base.unwrap();
let mut app = App::with_project(p);
send(&mut app, Message::Select(selected));
let fields = data::effective_fields(app.project(), selected);
let mut visited = vec![];
for page in 0..fields.len().div_ceil(PAGE_SIZE) {
let range = page_range(fields.len(), page);
assert!(range.len() <= PAGE_SIZE);
visited.extend(fields[range].iter().map(|e| e.id));
}
assert_eq!(visited, originals);
assert_eq!(
focused_owner(app.project(), originals[300]).unwrap().id,
app.project().elements[&originals[300]].parent.unwrap()
);
let catalog = catalog_entries(app.project(), Layer::Oa);
let catalog_ids = (0..catalog.len().div_ceil(PAGE_SIZE))
.flat_map(|page| {
catalog[page_range(catalog.len(), page)]
.iter()
.map(|e| e.id)
})
.collect::<BTreeSet<_>>();
assert_eq!(catalog_ids.len(), 512);
let view = view(&app);
let tree = iced::advanced::widget::Tree::new(view.as_widget());
fn nodes(tree: &iced::advanced::widget::Tree) -> usize {
1 + tree.children.iter().map(nodes).sum::<usize>()
}
assert!(
nodes(&tree) < 1000,
"{} widget nodes for a single focused page",
nodes(&tree)
);
}
#[test]
fn browsing_pages_preserves_invalid_numeric_draft_and_inherited_allocation_identity() {
let mut app = App::with_project(Project::empty());
send(
&mut app,
Message::Data(Event::Create(Template::Integer, None)),
);
let integer = app.selected;
let bound = "/body/Oa/Shared/DataType/definition/Integer/minimum";
send(
&mut app,
Message::FieldShape(bound.into(), serde_json::json!(0)),
);
send(&mut app, Message::Field(bound.into(), "-".into()));
let accepted = app.project().clone();
send(
&mut app,
Message::Data(Event::Page {
section: Section::Catalog,
owner: None,
page: usize::MAX,
}),
);
assert_eq!(app.project(), &accepted);
assert_eq!(app.selected, integer);
assert_eq!(app.draft.as_ref().unwrap().inputs[bound].raw, "-");
let _ = view(&app);
assert_eq!(page_range(0, usize::MAX), 0..0);
let mut p = Project::empty();
let base = p
.add_element(Kind::Interface, Layer::Oa, "Base contract", None)
.unwrap();
let child = p
.add_element(Kind::Interface, Layer::Oa, "Derived contract", None)
.unwrap();
*p.elements.get_mut(&child).unwrap().shared_mut().unwrap() = SharedBody::Interface {
supertype: Some(base),
};
let mut originals = BTreeSet::new();
for i in 0..73 {
let item = p
.add_element(Kind::ExchangeItem, Layer::Oa, &format!("Item {i}"), None)
.unwrap();
let allocation = p.relate(
RelationKind::InterfaceItemAllocation,
&format!("Allocation {i}"),
base,
item,
);
originals.insert(allocation);
}
let allocations = data::effective_interface_allocations(&p, child);
let visited = (0..allocations.len().div_ceil(PAGE_SIZE))
.flat_map(|page| {
allocations[page_range(allocations.len(), page)]
.iter()
.map(|r| r.id)
})
.collect::<BTreeSet<_>>();
assert_eq!(visited, originals);
for id in visited {
assert_eq!(focused_owner(&p, id).unwrap().id, base);
}
let mut state = State::default();
state.update(&Event::Page {
section: Section::Allocations,
owner: Some(child),
page: 2,
});
assert_eq!(state.page(Section::Allocations, Some(child)), 2);
assert_eq!(state.page(Section::Allocations, Some(base)), 0);
}
}