Bound scenario value authoring to focused paged fields
This commit is contained in:
@@ -80,7 +80,15 @@ pub enum Event {
|
||||
Binding(Id, Choice),
|
||||
Value(crate::data_values::FieldPathKey, String),
|
||||
ValueFocus(crate::data_values::FieldPathKey),
|
||||
ValueBrowse(Option<crate::data_values::FieldPathKey>),
|
||||
ValuePage(ValueSection, usize),
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ValueSection {
|
||||
Fields,
|
||||
Authored,
|
||||
}
|
||||
const VALUE_PAGE_SIZE: usize = 24;
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
draft: Option<Draft>,
|
||||
@@ -103,6 +111,9 @@ struct Draft {
|
||||
bindings: BTreeMap<Id, Id>,
|
||||
values: BTreeMap<crate::data_values::FieldPathKey, String>,
|
||||
value_focus: Option<crate::data_values::FieldPathKey>,
|
||||
value_browse: Option<crate::data_values::FieldPathKey>,
|
||||
value_browse_page: usize,
|
||||
value_authored_page: usize,
|
||||
}
|
||||
impl State {
|
||||
pub(super) fn pending_owner(&self) -> Option<Id> {
|
||||
@@ -149,6 +160,9 @@ impl State {
|
||||
bindings: BTreeMap::new(),
|
||||
values: BTreeMap::new(),
|
||||
value_focus: None,
|
||||
value_browse: None,
|
||||
value_browse_page: 0,
|
||||
value_authored_page: 0,
|
||||
});
|
||||
}
|
||||
Event::Cancel => self.acknowledge_commit(),
|
||||
@@ -166,8 +180,7 @@ impl State {
|
||||
Event::Exchange(c) => {
|
||||
if d.exchange != Some(c.id) {
|
||||
d.exchange = Some(c.id);
|
||||
d.values.clear();
|
||||
d.value_focus = None;
|
||||
d.reset_values();
|
||||
d.sender = None;
|
||||
d.receiver = None;
|
||||
}
|
||||
@@ -207,8 +220,7 @@ impl State {
|
||||
}) = p.elements.get(&c.id).and_then(|e| e.shared())
|
||||
{
|
||||
if d.exchange != *exchange {
|
||||
d.values.clear();
|
||||
d.value_focus = None;
|
||||
d.reset_values();
|
||||
}
|
||||
d.exchange = *exchange;
|
||||
d.sender = *receiver;
|
||||
@@ -230,6 +242,21 @@ impl State {
|
||||
{
|
||||
d.value_focus = Some(key);
|
||||
}
|
||||
Event::ValueBrowse(key) => {
|
||||
if key.as_ref().is_none_or(|key| {
|
||||
d.exchange
|
||||
.and_then(|id| p.relations.get(&id))
|
||||
.and_then(|r| crate::data_values::resolve(p, r, &d.kind(), key))
|
||||
.is_some()
|
||||
}) {
|
||||
d.value_browse = key;
|
||||
d.value_browse_page = 0;
|
||||
}
|
||||
}
|
||||
Event::ValuePage(section, page) => match section {
|
||||
ValueSection::Fields => d.value_browse_page = page,
|
||||
ValueSection::Authored => d.value_authored_page = page,
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -389,6 +416,9 @@ fn calls(p: &Project, d: &Draft) -> Vec<Choice> {
|
||||
let mut candidate = d.clone();
|
||||
candidate.style = MessageStyle::Reply;
|
||||
candidate.call = Some(e.id);
|
||||
if candidate.exchange != *exchange {
|
||||
candidate.reset_values();
|
||||
}
|
||||
candidate.exchange = *exchange;
|
||||
candidate.sender = *receiver;
|
||||
candidate.receiver = *sender;
|
||||
@@ -401,6 +431,13 @@ fn calls(p: &Project, d: &Draft) -> Vec<Choice> {
|
||||
.collect()
|
||||
}
|
||||
impl Draft {
|
||||
fn reset_values(&mut self) {
|
||||
self.values.clear();
|
||||
self.value_focus = None;
|
||||
self.value_browse = None;
|
||||
self.value_browse_page = 0;
|
||||
self.value_authored_page = 0;
|
||||
}
|
||||
fn retain_value_paths(&mut self, p: &Project) {
|
||||
let kind = self.kind();
|
||||
let exchange = self.exchange.and_then(|id| p.relations.get(&id));
|
||||
@@ -416,21 +453,31 @@ impl Draft {
|
||||
}) {
|
||||
self.value_focus = None;
|
||||
}
|
||||
if self.value_browse.as_ref().is_some_and(|key| {
|
||||
exchange
|
||||
.and_then(|r| crate::data_values::resolve(p, r, &kind, key))
|
||||
.is_none()
|
||||
}) {
|
||||
self.value_browse = None;
|
||||
self.value_browse_page = 0;
|
||||
}
|
||||
}
|
||||
fn value_choices(
|
||||
fn browsed_values(
|
||||
&self,
|
||||
p: &Project,
|
||||
exchange: &Relation,
|
||||
) -> Result<Vec<crate::data_values::FieldPathChoice>, String> {
|
||||
let mut choices = crate::data_values::choices(p, exchange, &self.kind())?;
|
||||
for key in self.values.keys().chain(self.value_focus.iter()) {
|
||||
if !choices.iter().any(|choice| &choice.key == key)
|
||||
&& let Some(choice) = crate::data_values::resolve(p, exchange, &self.kind(), key)
|
||||
{
|
||||
choices.push(choice);
|
||||
}
|
||||
match &self.value_browse {
|
||||
Some(key) => crate::data_values::children(p, exchange, &self.kind(), key),
|
||||
None => Ok(crate::data_values::roots(p, exchange, &self.kind())),
|
||||
}
|
||||
Ok(choices)
|
||||
}
|
||||
fn authored_values(&self) -> Vec<(&crate::data_values::FieldPathKey, &str)> {
|
||||
self.values
|
||||
.iter()
|
||||
.filter(|(_, value)| !value.trim().is_empty())
|
||||
.map(|(key, value)| (key, value.as_str()))
|
||||
.collect()
|
||||
}
|
||||
fn kind(&self) -> MessageKind {
|
||||
match self.style {
|
||||
@@ -670,6 +717,144 @@ impl Draft {
|
||||
fn event(e: Event) -> Message {
|
||||
Message::Behavior(super::Event::Scenario(e))
|
||||
}
|
||||
fn value_page(len: usize, requested: usize) -> std::ops::Range<usize> {
|
||||
let start = requested.min(len.saturating_sub(1) / VALUE_PAGE_SIZE) * VALUE_PAGE_SIZE;
|
||||
start..(start + VALUE_PAGE_SIZE).min(len)
|
||||
}
|
||||
fn value_pager(len: usize, requested: usize, section: ValueSection) -> Element<'static, Message> {
|
||||
let range = value_page(len, requested);
|
||||
let page = range.start / VALUE_PAGE_SIZE;
|
||||
row![
|
||||
button("Previous")
|
||||
.on_press_maybe((page > 0).then(|| event(Event::ValuePage(section, page - 1)))),
|
||||
text(format!(
|
||||
"Page {} of {} · {} fields",
|
||||
page + 1,
|
||||
len.div_ceil(VALUE_PAGE_SIZE).max(1),
|
||||
len
|
||||
))
|
||||
.size(11),
|
||||
button("Next")
|
||||
.on_press_maybe((range.end < len).then(|| event(Event::ValuePage(section, page + 1))))
|
||||
]
|
||||
.spacing(6)
|
||||
.into()
|
||||
}
|
||||
fn value_panel<'a>(p: &Project, d: &'a Draft, exchange: &Relation) -> Element<'a, Message> {
|
||||
use crate::data_values;
|
||||
let mut out = column![text("Message values").size(14)].spacing(7);
|
||||
if let Some(browse) = &d.value_browse {
|
||||
let title = data_values::resolve(p, exchange, &d.kind(), browse)
|
||||
.map(|choice| format!("Fields inside {}", choice.label))
|
||||
.unwrap_or_else(|| "This record path is no longer available in this message.".into());
|
||||
out = out.push(text(title).size(11)).push(
|
||||
row![
|
||||
button("Root fields").on_press(event(Event::ValueBrowse(None))),
|
||||
button("Up one level")
|
||||
.on_press(event(Event::ValueBrowse(data_values::parent(browse))))
|
||||
]
|
||||
.spacing(6),
|
||||
);
|
||||
} else {
|
||||
out = out.push(text("Root fields · choose a record to browse its fields").size(11));
|
||||
}
|
||||
match d.browsed_values(p, exchange) {
|
||||
Ok(choices) if !choices.is_empty() => {
|
||||
let range = value_page(choices.len(), d.value_browse_page);
|
||||
let visible = choices[range].to_vec();
|
||||
let selected = visible
|
||||
.iter()
|
||||
.find(|choice| Some(&choice.key) == d.value_focus.as_ref())
|
||||
.cloned();
|
||||
out = out
|
||||
.push(value_pager(
|
||||
choices.len(),
|
||||
d.value_browse_page,
|
||||
ValueSection::Fields,
|
||||
))
|
||||
.push(
|
||||
pick_list(visible, selected, |choice| {
|
||||
event(Event::ValueFocus(choice.key))
|
||||
})
|
||||
.placeholder("Choose a field on this page…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
Ok(_) => {
|
||||
out = out.push(text("No fields at this level for this message phase.").size(11));
|
||||
}
|
||||
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)),
|
||||
}
|
||||
match d
|
||||
.value_focus
|
||||
.as_ref()
|
||||
.map(|key| data_values::resolve(p, exchange, &d.kind(), key))
|
||||
{
|
||||
Some(Some(focus)) => {
|
||||
let key = focus.key;
|
||||
out = out
|
||||
.push(text(focus.label).size(12))
|
||||
.push(
|
||||
text_input(
|
||||
"Optional value expression",
|
||||
d.values.get(&key).map(String::as_str).unwrap_or(""),
|
||||
)
|
||||
.on_input({
|
||||
let key = key.clone();
|
||||
move |value| event(Event::Value(key.clone(), value))
|
||||
})
|
||||
.padding(8)
|
||||
.size(12),
|
||||
)
|
||||
.push(text("An empty expression omits this value.").size(11));
|
||||
let mut actions = row![
|
||||
button("Show enclosing fields")
|
||||
.on_press(event(Event::ValueBrowse(data_values::parent(&key))))
|
||||
]
|
||||
.spacing(6);
|
||||
if matches!(p.elements.get(&key.field).and_then(oa_core::Element::shared),
|
||||
Some(SharedBody::DataField { type_ref: TypeRef::Named(record),
|
||||
storage: FieldStorage::Value, .. }) if data::is_record(p, *record))
|
||||
{
|
||||
actions = actions.push(
|
||||
button("Browse record fields").on_press(event(Event::ValueBrowse(Some(key)))),
|
||||
);
|
||||
}
|
||||
out = out.push(actions);
|
||||
}
|
||||
Some(None) => {
|
||||
out = out.push(text("The selected path is unavailable in this message. Choose a compatible field; entered values remain in the draft.")
|
||||
.size(11).color(style::MUTED));
|
||||
}
|
||||
None => out = out.push(text("Choose a field to enter its value expression.").size(11)),
|
||||
}
|
||||
let authored = d.authored_values();
|
||||
out = out.push(text(format!("Authored values ({})", authored.len())).size(12));
|
||||
if authored.is_empty() {
|
||||
out = out.push(text("No value expressions entered.").size(11));
|
||||
} else {
|
||||
out = out.push(value_pager(
|
||||
authored.len(),
|
||||
d.value_authored_page,
|
||||
ValueSection::Authored,
|
||||
));
|
||||
for (key, expression) in &authored[value_page(authored.len(), d.value_authored_page)] {
|
||||
let title = data_values::resolve(p, exchange, &d.kind(), key)
|
||||
.map(|choice| choice.label)
|
||||
.unwrap_or_else(|| format!("Unavailable field {} · review this value", key.field));
|
||||
out = out.push(
|
||||
button(
|
||||
column![text(title).size(11), text(short(expression, 100)).size(11)].spacing(3),
|
||||
)
|
||||
.on_press(event(Event::ValueFocus((*key).clone())))
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
fn picker<'a>(
|
||||
choices: Vec<Choice>,
|
||||
selected: Option<Id>,
|
||||
@@ -822,66 +1007,7 @@ pub fn view<'a>(app: &'a App, owner: Id, mut out: Column<'a, Message>) -> Elemen
|
||||
);
|
||||
}
|
||||
if let Some(exchange) = d.exchange.and_then(|id| p.relations.get(&id)) {
|
||||
match d.value_choices(p, exchange) {
|
||||
Ok(choices) => {
|
||||
let focus = d.value_focus.as_ref().and_then(|key| {
|
||||
crate::data_values::resolve(p, exchange, &d.kind(), key)
|
||||
});
|
||||
form = form.push(
|
||||
pick_list(choices.clone(), focus.clone(), |choice| {
|
||||
event(Event::ValueFocus(choice.key))
|
||||
})
|
||||
.placeholder("Explore a record or choose a value path…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
);
|
||||
if let Some(focus) = focus {
|
||||
if let Some(parent) = crate::data_values::parent(&focus.key) {
|
||||
form = form.push(
|
||||
button(text("Up to enclosing field").size(11))
|
||||
.on_press(event(Event::ValueFocus(parent))),
|
||||
);
|
||||
}
|
||||
match crate::data_values::children(p, exchange, &d.kind(), &focus.key) {
|
||||
Ok(children) if !children.is_empty() => {
|
||||
form = form.push(
|
||||
pick_list(
|
||||
children,
|
||||
None::<crate::data_values::FieldPathChoice>,
|
||||
|choice| event(Event::ValueFocus(choice.key)),
|
||||
)
|
||||
.placeholder("Choose a field inside this record…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
)
|
||||
}
|
||||
Err(error) => {
|
||||
form = form.push(text(error).size(11).color(style::MUTED))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for choice in choices {
|
||||
let key = choice.key;
|
||||
form = form.push(
|
||||
column![
|
||||
text(choice.label).size(11),
|
||||
text_input(
|
||||
"Optional value expression",
|
||||
d.values.get(&key).map(String::as_str).unwrap_or("")
|
||||
)
|
||||
.on_input(move |s| event(Event::Value(key.clone(), s)))
|
||||
.padding(8)
|
||||
.size(12)
|
||||
]
|
||||
.spacing(4),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) => form = form.push(text(error).size(12).color(style::MUTED)),
|
||||
}
|
||||
form = form.push(value_panel(p, d, exchange));
|
||||
}
|
||||
if d.style == MessageStyle::CallReply {
|
||||
form=form.push(label("Creates the call and reversed, correlated reply together in one undoable operation."));
|
||||
@@ -1438,6 +1564,12 @@ mod data_contract_tests {
|
||||
path: vec![f.origin, next],
|
||||
field: f.leaf,
|
||||
};
|
||||
state
|
||||
.update(
|
||||
&f.project,
|
||||
Event::ValueBrowse(crate::data_values::parent(&key)),
|
||||
)
|
||||
.unwrap();
|
||||
state
|
||||
.update(&f.project, Event::ValueFocus(key.clone()))
|
||||
.unwrap();
|
||||
@@ -1446,7 +1578,7 @@ mod data_contract_tests {
|
||||
.draft
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.value_choices(&f.project, &f.project.relations[&f.exchange])
|
||||
.browsed_values(&f.project, &f.project.relations[&f.exchange])
|
||||
.unwrap()
|
||||
.iter()
|
||||
.any(|c| c.key == key)
|
||||
@@ -1490,4 +1622,361 @@ mod data_contract_tests {
|
||||
.unwrap();
|
||||
assert_eq!(state.draft.as_ref().unwrap().values[&out], "reply value");
|
||||
}
|
||||
|
||||
fn panel_counts(p: &Project, d: &Draft, exchange: Id) -> (usize, usize) {
|
||||
let input: Element<'_, Message> = text_input("", "").into();
|
||||
let input_tag = iced::advanced::widget::Tree::new(input.as_widget()).tag;
|
||||
let panel = value_panel(p, d, &p.relations[&exchange]);
|
||||
let tree = iced::advanced::widget::Tree::new(panel.as_widget());
|
||||
fn count(
|
||||
tree: &iced::advanced::widget::Tree,
|
||||
tag: iced::advanced::widget::tree::Tag,
|
||||
) -> (usize, usize) {
|
||||
tree.children.iter().fold(
|
||||
(1, usize::from(tree.tag == tag)),
|
||||
|(nodes, inputs), child| {
|
||||
let (child_nodes, child_inputs) = count(child, tag);
|
||||
(nodes + child_nodes, inputs + child_inputs)
|
||||
},
|
||||
)
|
||||
}
|
||||
count(&tree, input_tag)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_payload_has_one_editor_with_reachable_field_and_authored_pages() {
|
||||
let f = crate::data_values::tests::wide_payload();
|
||||
let p = &f.project;
|
||||
let exchange = &p.relations[&f.exchange];
|
||||
let mut state = State::default();
|
||||
state
|
||||
.update(p, Event::Start(f.scenario, Tool::Message))
|
||||
.unwrap();
|
||||
state
|
||||
.update(p, Event::Exchange(named_choice(p, f.exchange)))
|
||||
.unwrap();
|
||||
let roots = crate::data_values::roots(p, exchange, &MessageKind::Call);
|
||||
assert_eq!(roots.len(), 96);
|
||||
assert_eq!(
|
||||
panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1,
|
||||
0
|
||||
);
|
||||
let mut visited = BTreeSet::new();
|
||||
for page in 0..roots.len().div_ceil(VALUE_PAGE_SIZE) {
|
||||
state
|
||||
.update(p, Event::ValuePage(ValueSection::Fields, page))
|
||||
.unwrap();
|
||||
let d = state.draft.as_ref().unwrap();
|
||||
let choices = d.browsed_values(p, exchange).unwrap();
|
||||
let visible = choices[value_page(choices.len(), d.value_browse_page)].to_vec();
|
||||
assert!(visible.len() <= VALUE_PAGE_SIZE);
|
||||
for choice in visible {
|
||||
visited.insert(choice.key.clone());
|
||||
state
|
||||
.update(p, Event::ValueFocus(choice.key.clone()))
|
||||
.unwrap();
|
||||
state
|
||||
.update(p, Event::Value(choice.key, format!("draft {page}")))
|
||||
.unwrap();
|
||||
}
|
||||
let (nodes, inputs) = panel_counts(p, state.draft.as_ref().unwrap(), f.exchange);
|
||||
assert_eq!(
|
||||
inputs, 1,
|
||||
"one input for over twelve thousand carried paths"
|
||||
);
|
||||
assert!(nodes < 200, "{nodes} widgets for the bounded value panel");
|
||||
}
|
||||
assert_eq!(
|
||||
visited,
|
||||
roots.iter().map(|choice| choice.key.clone()).collect()
|
||||
);
|
||||
|
||||
state
|
||||
.update(p, Event::ValueBrowse(Some(roots[0].key.clone())))
|
||||
.unwrap();
|
||||
assert_eq!(state.draft.as_ref().unwrap().value_browse_page, 0);
|
||||
let children = state
|
||||
.draft
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.browsed_values(p, exchange)
|
||||
.unwrap();
|
||||
assert_eq!(children.len(), 128);
|
||||
let mut visited_children = BTreeSet::new();
|
||||
for page in 0..children.len().div_ceil(VALUE_PAGE_SIZE) {
|
||||
state
|
||||
.update(p, Event::ValuePage(ValueSection::Fields, page))
|
||||
.unwrap();
|
||||
let d = state.draft.as_ref().unwrap();
|
||||
let choices = d.browsed_values(p, exchange).unwrap();
|
||||
for choice in &choices[value_page(choices.len(), d.value_browse_page)] {
|
||||
assert_eq!(choice.key.path, [roots[0].key.field]);
|
||||
visited_children.insert(choice.key.clone());
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
visited_children,
|
||||
children.iter().map(|choice| choice.key.clone()).collect()
|
||||
);
|
||||
|
||||
let nested = children[0].key.clone();
|
||||
state.update(p, Event::ValueFocus(nested.clone())).unwrap();
|
||||
state
|
||||
.update(p, Event::Value(nested.clone(), "unfinished(\"".into()))
|
||||
.unwrap();
|
||||
let before = state.draft.as_ref().unwrap().values.clone();
|
||||
let before_browse = state.draft.as_ref().unwrap().value_browse.clone();
|
||||
let before_page = state.draft.as_ref().unwrap().value_browse_page;
|
||||
for event in [
|
||||
Event::Style(MessageStyle::Call),
|
||||
Event::Exchange(named_choice(p, f.exchange)),
|
||||
Event::Style(MessageStyle::CallReply),
|
||||
] {
|
||||
state.update(p, event).unwrap();
|
||||
}
|
||||
let d = state.draft.as_ref().unwrap();
|
||||
assert_eq!(d.values, before);
|
||||
assert_eq!(d.value_browse, before_browse);
|
||||
assert_eq!(d.value_browse_page, before_page);
|
||||
assert_eq!(d.value_focus, Some(nested));
|
||||
|
||||
let mut authored = BTreeSet::new();
|
||||
for page in 0..before.len().div_ceil(VALUE_PAGE_SIZE) {
|
||||
state
|
||||
.update(p, Event::ValuePage(ValueSection::Authored, page))
|
||||
.unwrap();
|
||||
let d = state.draft.as_ref().unwrap();
|
||||
let values = d.authored_values();
|
||||
for (key, _) in &values[value_page(values.len(), d.value_authored_page)] {
|
||||
authored.insert((*key).clone());
|
||||
}
|
||||
let (nodes, inputs) = panel_counts(p, d, f.exchange);
|
||||
assert_eq!(inputs, 1);
|
||||
assert!(nodes < 200, "{nodes} widget nodes");
|
||||
}
|
||||
assert_eq!(authored, before.keys().cloned().collect());
|
||||
state.update(p, Event::ValueBrowse(None)).unwrap();
|
||||
state
|
||||
.update(p, Event::ValuePage(ValueSection::Fields, usize::MAX))
|
||||
.unwrap();
|
||||
let d = state.draft.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
d.values, before,
|
||||
"browsing and page clamping never remove drafts"
|
||||
);
|
||||
let OccurrenceBody::Exchange { values, .. } = d.message_body() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(
|
||||
values.len(),
|
||||
before.len(),
|
||||
"commit includes every authored page"
|
||||
);
|
||||
assert_eq!(panel_counts(p, d, f.exchange).1, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recursive_browsing_up_and_root_keep_values_and_report_the_depth_limit() {
|
||||
let (f, next) = crate::data_values::tests::recursive_payload();
|
||||
let p = &f.project;
|
||||
let exchange = &p.relations[&f.exchange];
|
||||
let mut state = State::default();
|
||||
state
|
||||
.update(p, Event::Start(f.scenario, Tool::Message))
|
||||
.unwrap();
|
||||
state
|
||||
.update(p, Event::Exchange(named_choice(p, f.exchange)))
|
||||
.unwrap();
|
||||
let root = crate::data_values::FieldPathKey {
|
||||
path: vec![],
|
||||
field: f.origin,
|
||||
};
|
||||
state.update(p, Event::ValueFocus(root.clone())).unwrap();
|
||||
state
|
||||
.update(p, Event::ValueBrowse(Some(root.clone())))
|
||||
.unwrap();
|
||||
let inner = state
|
||||
.draft
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.browsed_values(p, exchange)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|choice| choice.key.field == next)
|
||||
.unwrap()
|
||||
.key;
|
||||
state.update(p, Event::ValueFocus(inner.clone())).unwrap();
|
||||
state
|
||||
.update(p, Event::ValueBrowse(Some(inner.clone())))
|
||||
.unwrap();
|
||||
let leaf = state
|
||||
.draft
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.browsed_values(p, exchange)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|choice| choice.key.field == f.leaf)
|
||||
.unwrap()
|
||||
.key;
|
||||
assert_eq!(leaf.path, [f.origin, next]);
|
||||
state.update(p, Event::ValueFocus(leaf.clone())).unwrap();
|
||||
state
|
||||
.update(p, Event::Value(leaf.clone(), "keep this draft".into()))
|
||||
.unwrap();
|
||||
state
|
||||
.update(p, Event::ValueBrowse(crate::data_values::parent(&inner)))
|
||||
.unwrap();
|
||||
assert_eq!(state.draft.as_ref().unwrap().value_browse, Some(root));
|
||||
state.update(p, Event::ValueBrowse(None)).unwrap();
|
||||
assert_eq!(
|
||||
state.draft.as_ref().unwrap().values[&leaf],
|
||||
"keep this draft"
|
||||
);
|
||||
assert_eq!(
|
||||
panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1,
|
||||
1
|
||||
);
|
||||
let deepest = crate::data_values::FieldPathKey {
|
||||
path: std::iter::once(f.origin)
|
||||
.chain(std::iter::repeat_n(next, data::MAX_VALUE_PATH_DEPTH - 1))
|
||||
.collect(),
|
||||
field: next,
|
||||
};
|
||||
state.update(p, Event::ValueBrowse(Some(deepest))).unwrap();
|
||||
assert!(
|
||||
state
|
||||
.draft
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.browsed_values(p, exchange)
|
||||
.unwrap_err()
|
||||
.contains("128")
|
||||
);
|
||||
assert_eq!(
|
||||
panel_counts(p, state.draft.as_ref().unwrap(), f.exchange).1,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
state.draft.as_ref().unwrap().values[&leaf],
|
||||
"keep this draft"
|
||||
);
|
||||
state
|
||||
.update(p, Event::Value(leaf.clone(), String::new()))
|
||||
.unwrap();
|
||||
let d = state.draft.as_ref().unwrap();
|
||||
assert!(d.authored_values().is_empty());
|
||||
assert_eq!(d.values[&leaf], "", "empty input remains an editable draft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_call_picker_previews_the_same_value_reset_as_selecting_another_exchange() {
|
||||
let mut f = crate::data_values::tests::payload();
|
||||
let p = &mut f.project;
|
||||
let source = p.relations[&f.exchange].source();
|
||||
let target = p.relations[&f.exchange].target();
|
||||
let mut lifelines = vec![];
|
||||
for (name, represented) in [("Caller", source), ("Callee", target)] {
|
||||
let id = p
|
||||
.add_element(Kind::Lifeline, Layer::Oa, name, Some(f.scenario))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
||||
represented: Some(represented),
|
||||
};
|
||||
lifelines.push(id);
|
||||
}
|
||||
let other_item = p
|
||||
.add_element(Kind::ExchangeItem, Layer::Oa, "Other operation", None)
|
||||
.unwrap();
|
||||
*p.elements
|
||||
.get_mut(&other_item)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = p.elements[&f.item].shared().unwrap().clone();
|
||||
let other_field = p
|
||||
.add_element(Kind::DataField, Layer::Oa, "Result", Some(other_item))
|
||||
.unwrap();
|
||||
*p.elements
|
||||
.get_mut(&other_field)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = SharedBody::DataField {
|
||||
type_ref: TypeRef::Real,
|
||||
direction: ParameterDirection::Out,
|
||||
minimum: 1,
|
||||
maximum: Some(1),
|
||||
unit: String::new(),
|
||||
storage: FieldStorage::Value,
|
||||
default: None,
|
||||
};
|
||||
let other_exchange = p.relate(RelationKind::Interaction, "Other exchange", source, target);
|
||||
p.relations.get_mut(&other_exchange).unwrap().body = RelationBody::Interaction {
|
||||
source,
|
||||
target,
|
||||
items: vec![other_item],
|
||||
};
|
||||
let mut call_ids = vec![];
|
||||
for (name, exchange) in [("First call", f.exchange), ("Other call", other_exchange)] {
|
||||
let call = occurrence(
|
||||
p,
|
||||
f.scenario,
|
||||
name,
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(exchange),
|
||||
sender: Some(lifelines[0]),
|
||||
receiver: Some(lifelines[1]),
|
||||
message_kind: MessageKind::Call,
|
||||
reply_to: None,
|
||||
values: vec![],
|
||||
},
|
||||
);
|
||||
call_ids.push(call.id);
|
||||
*p = execute_batch(p, &[Command::CreateElement(call)]).unwrap();
|
||||
}
|
||||
let mut state = State::default();
|
||||
state
|
||||
.update(p, Event::Start(f.scenario, Tool::Message))
|
||||
.unwrap();
|
||||
state.update(p, Event::Style(MessageStyle::Reply)).unwrap();
|
||||
state
|
||||
.update(p, Event::Call(named_choice(p, call_ids[0])))
|
||||
.unwrap();
|
||||
let key = crate::data_values::FieldPathKey {
|
||||
path: vec![f.destination],
|
||||
field: f.leaf,
|
||||
};
|
||||
state.update(p, Event::ValueFocus(key.clone())).unwrap();
|
||||
state
|
||||
.update(p, Event::Value(key.clone(), "reply expression".into()))
|
||||
.unwrap();
|
||||
state
|
||||
.update(p, Event::ValueBrowse(crate::data_values::parent(&key)))
|
||||
.unwrap();
|
||||
let offered = calls(p, state.draft.as_ref().unwrap());
|
||||
assert!(
|
||||
offered.iter().any(|choice| choice.id == call_ids[1]),
|
||||
"a different exchange remains selectable after entering a value"
|
||||
);
|
||||
assert_eq!(
|
||||
state.draft.as_ref().unwrap().values[&key],
|
||||
"reply expression",
|
||||
"preview is read-only"
|
||||
);
|
||||
state
|
||||
.update(p, Event::Call(named_choice(p, call_ids[0])))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
state.draft.as_ref().unwrap().values[&key],
|
||||
"reply expression"
|
||||
);
|
||||
state
|
||||
.update(p, Event::Call(named_choice(p, call_ids[1])))
|
||||
.unwrap();
|
||||
let d = state.draft.as_ref().unwrap();
|
||||
assert!(d.values.is_empty());
|
||||
assert!(d.value_focus.is_none());
|
||||
assert!(d.value_browse.is_none());
|
||||
assert_eq!(d.exchange, Some(other_exchange));
|
||||
assert!(state.prepare_pending(p).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,35 @@ pub fn choices(
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Direct carried fields only: an unrelated deep branch must not prevent
|
||||
/// browsing a root field. Preserve the core resolver's first carried owner.
|
||||
pub fn roots(project: &Project, exchange: &Relation, phase: &MessageKind) -> Vec<FieldPathChoice> {
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
exchange
|
||||
.body
|
||||
.items()
|
||||
.iter()
|
||||
.flat_map(|item| {
|
||||
data::effective_fields(project, *item)
|
||||
.into_iter()
|
||||
.filter_map(|field| {
|
||||
let SharedBody::DataField { direction, .. } = field.shared()? else {
|
||||
return None;
|
||||
};
|
||||
Some(data::CarriedFieldPath {
|
||||
item: *item,
|
||||
path: vec![],
|
||||
field: field.id,
|
||||
direction: *direction,
|
||||
})
|
||||
})
|
||||
})
|
||||
.filter(|entry| seen.insert(entry.field))
|
||||
.filter(|entry| phase_allows(project, entry, phase))
|
||||
.map(|entry| named_choice(project, entry))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve authored finite paths as well as the finite representative picker list.
|
||||
/// In particular, optional recursive Value records may be traversed repeatedly.
|
||||
pub fn resolve(
|
||||
@@ -113,9 +142,11 @@ pub fn children(
|
||||
phase: &MessageKind,
|
||||
key: &FieldPathKey,
|
||||
) -> Result<Vec<FieldPathChoice>, String> {
|
||||
if resolve(project, exchange, phase, key).is_none() {
|
||||
let Some(entry) = data::carried_value_path(project, exchange, &key.path, key.field)
|
||||
.filter(|entry| phase_allows(project, entry, phase))
|
||||
else {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
};
|
||||
let Some(SharedBody::DataField {
|
||||
type_ref: TypeRef::Named(record),
|
||||
storage: FieldStorage::Value,
|
||||
@@ -137,14 +168,14 @@ pub fn children(
|
||||
path.push(key.field);
|
||||
Ok(data::effective_fields(project, *record)
|
||||
.into_iter()
|
||||
.filter_map(|field| {
|
||||
resolve(
|
||||
.map(|field| {
|
||||
named_choice(
|
||||
project,
|
||||
exchange,
|
||||
phase,
|
||||
&FieldPathKey {
|
||||
data::CarriedFieldPath {
|
||||
item: entry.item,
|
||||
path: path.clone(),
|
||||
field: field.id,
|
||||
direction: entry.direction,
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -337,6 +368,85 @@ pub(crate) mod tests {
|
||||
validate_candidate(&f.project).unwrap();
|
||||
(f, next)
|
||||
}
|
||||
pub fn wide_payload() -> Payload {
|
||||
let mut f = payload();
|
||||
let record = f.project.elements[&f.leaf].parent.unwrap();
|
||||
for (owner, template, count) in [(f.item, f.origin, 95), (record, f.leaf, 127)] {
|
||||
for index in 0..count {
|
||||
let body = f.project.elements[&template].shared().unwrap().clone();
|
||||
let id = f
|
||||
.project
|
||||
.add_element(
|
||||
Kind::DataField,
|
||||
Layer::Oa,
|
||||
&format!("Field {template} {index}"),
|
||||
Some(owner),
|
||||
)
|
||||
.unwrap();
|
||||
*f.project
|
||||
.elements
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = body;
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
#[test]
|
||||
fn roots_and_one_hop_children_do_not_expand_unrelated_payload_branches() {
|
||||
let mut f = wide_payload();
|
||||
let exchange = &f.project.relations[&f.exchange];
|
||||
let direct = roots(&f.project, exchange, &MessageKind::Call);
|
||||
assert_eq!(direct.len(), 96);
|
||||
assert!(direct.iter().all(|choice| choice.key.path.is_empty()));
|
||||
let representative = choices(&f.project, exchange, &MessageKind::Call).unwrap();
|
||||
assert_eq!(representative.len(), 96 * 129);
|
||||
for root in &direct {
|
||||
assert_eq!(
|
||||
resolve(&f.project, exchange, &MessageKind::Call, &root.key),
|
||||
Some(root.clone())
|
||||
);
|
||||
}
|
||||
let nested = children(&f.project, exchange, &MessageKind::Call, &direct[0].key).unwrap();
|
||||
assert_eq!(nested.len(), 128);
|
||||
for child in &nested {
|
||||
assert_eq!(
|
||||
resolve(&f.project, exchange, &MessageKind::Call, &child.key),
|
||||
Some(child.clone())
|
||||
);
|
||||
assert!(child.label.ends_with(" · In"));
|
||||
}
|
||||
// A temporarily over-limit model still exposes its unrelated root fields;
|
||||
// the editor must not expand the whole graph before showing a choice.
|
||||
for index in 0..32 {
|
||||
let body = f.project.elements[&f.origin].shared().unwrap().clone();
|
||||
let id = f
|
||||
.project
|
||||
.add_element(
|
||||
Kind::DataField,
|
||||
Layer::Oa,
|
||||
&format!("Extra {index}"),
|
||||
Some(f.item),
|
||||
)
|
||||
.unwrap();
|
||||
*f.project
|
||||
.elements
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.shared_mut()
|
||||
.unwrap() = body;
|
||||
}
|
||||
let exchange = &f.project.relations[&f.exchange];
|
||||
assert!(choices(&f.project, exchange, &MessageKind::Call).is_err());
|
||||
assert_eq!(roots(&f.project, exchange, &MessageKind::Call).len(), 128);
|
||||
assert_eq!(
|
||||
children(&f.project, exchange, &MessageKind::Call, &direct[0].key)
|
||||
.unwrap()
|
||||
.len(),
|
||||
128
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn recursive_records_can_be_drilled_into_and_valid_imported_paths_resolve() {
|
||||
let (f, next) = recursive_payload();
|
||||
|
||||
Reference in New Issue
Block a user