Browse message value properties by record with bounded pages
This commit is contained in:
@@ -330,104 +330,152 @@ fn message_value<'a>(
|
||||
} => project.relations.get(id).map(|r| (r, message_kind)),
|
||||
_ => None,
|
||||
});
|
||||
let choices = context
|
||||
.map(|(r, phase)| crate::data_values::choices(project, r, phase))
|
||||
.unwrap_or_else(|| Ok(vec![]));
|
||||
let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6);
|
||||
match choices {
|
||||
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)),
|
||||
Ok(mut choices) => {
|
||||
let current_key = current.as_ref().map(|v| crate::data_values::FieldPathKey {
|
||||
path: v.path.clone(),
|
||||
field: v.field,
|
||||
});
|
||||
let mut selected = current_key
|
||||
let current_key = current
|
||||
.as_ref()
|
||||
.map(|value| crate::data_values::FieldPathKey {
|
||||
path: value.path.clone(),
|
||||
field: value.field,
|
||||
});
|
||||
let selected = context.and_then(|(exchange, phase)| {
|
||||
current_key
|
||||
.as_ref()
|
||||
.and_then(|key| crate::data_values::resolve(project, exchange, phase, key))
|
||||
});
|
||||
let browser = draft
|
||||
.value_browsers
|
||||
.get(path)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ValueBrowser {
|
||||
scope: selected
|
||||
.as_ref()
|
||||
.and_then(|key| choices.iter().find(|c| &c.key == key))
|
||||
.cloned()
|
||||
.or_else(|| {
|
||||
context.and_then(|(r, phase)| {
|
||||
current_key
|
||||
.as_ref()
|
||||
.and_then(|key| crate::data_values::resolve(project, r, phase, key))
|
||||
})
|
||||
});
|
||||
if let Some(choice) = &selected
|
||||
&& !choices.iter().any(|c| c.key == choice.key)
|
||||
{
|
||||
choices.push(choice.clone());
|
||||
}
|
||||
if selected.is_none()
|
||||
&& let Some(key) = current_key.clone()
|
||||
&& !key.field.is_nil()
|
||||
{
|
||||
out = out.push(text("Current address needs review. Choose an explicit path to resolve a legacy, ambiguous or incompatible value.").size(11).color(style::MUTED));
|
||||
let retained = crate::data_values::FieldPathChoice {
|
||||
picker_label: format!(
|
||||
"{} · current address needs review",
|
||||
project.named(key.field)
|
||||
),
|
||||
label: format!(
|
||||
"Current address · {} / {}",
|
||||
key.path
|
||||
.iter()
|
||||
.map(|id| project.named(*id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" → "),
|
||||
project.named(key.field)
|
||||
),
|
||||
key,
|
||||
};
|
||||
selected = Some(retained.clone());
|
||||
choices.push(retained);
|
||||
}
|
||||
if let Some(choice) = &selected {
|
||||
out = out.push(text(choice.label.clone()).size(11).color(style::MUTED));
|
||||
}
|
||||
let owned = path.to_owned();
|
||||
out = out.push(
|
||||
pick_list(choices, selected, move |choice| {
|
||||
Message::FieldValuePath(owned.clone(), choice.key)
|
||||
})
|
||||
.placeholder("Choose a carried field path…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
);
|
||||
}
|
||||
.and_then(|choice| crate::data_values::parent(&choice.key)),
|
||||
page: 0,
|
||||
});
|
||||
let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6);
|
||||
if let Some(choice) = &selected {
|
||||
out = out.push(text(choice.label.clone()).size(11));
|
||||
} else if let Some(key) = ¤t_key
|
||||
&& !key.field.is_nil()
|
||||
{
|
||||
out = out.push(
|
||||
text(format!(
|
||||
"Current address needs review: {} / {}. Choose an explicit field.",
|
||||
key.path
|
||||
.iter()
|
||||
.map(|id| project.named(*id))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" → "),
|
||||
project.named(key.field)
|
||||
))
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
);
|
||||
}
|
||||
if let (Some((exchange, phase)), Some(current)) = (context, current) {
|
||||
let key = crate::data_values::FieldPathKey {
|
||||
path: current.path,
|
||||
field: current.field,
|
||||
};
|
||||
if crate::data_values::resolve(project, exchange, phase, &key).is_some() {
|
||||
if let Some(parent) = crate::data_values::parent(&key) {
|
||||
out = out.push(
|
||||
button(text("Up to enclosing field").size(11))
|
||||
if let Some((exchange, phase)) = context {
|
||||
let location = browser
|
||||
.scope
|
||||
.as_ref()
|
||||
.and_then(|key| crate::data_values::resolve(project, exchange, phase, key))
|
||||
.map(|choice| choice.label)
|
||||
.unwrap_or_else(|| "Root carried fields".into());
|
||||
out = out.push(
|
||||
text(format!("Browse: {location}"))
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
);
|
||||
if let Some(scope) = &browser.scope {
|
||||
let mut navigation = row![button(text("↑ Up").size(11)).style(style::nav).on_press(
|
||||
Message::FieldValueBrowse(path.into(), crate::data_values::parent(scope), 0)
|
||||
)]
|
||||
.spacing(4);
|
||||
if !scope.path.is_empty() {
|
||||
navigation = navigation.push(
|
||||
button(text("Root fields").size(11))
|
||||
.style(style::nav)
|
||||
.on_press(Message::FieldValuePath(path.into(), parent)),
|
||||
.on_press(Message::FieldValueBrowse(path.into(), None, 0)),
|
||||
);
|
||||
}
|
||||
match crate::data_values::children(project, exchange, phase, &key) {
|
||||
Ok(children) if !children.is_empty() => {
|
||||
let path = path.to_owned();
|
||||
out = out.push(navigation);
|
||||
}
|
||||
match crate::data_values::at_scope(project, exchange, phase, browser.scope.as_ref()) {
|
||||
Ok(choices) => {
|
||||
let count = choices.len();
|
||||
let (visible, page, pages) = field_page(choices, browser.page);
|
||||
// Selection detail stays visible independently of browser location/page.
|
||||
let visible_selection = selected
|
||||
.clone()
|
||||
.filter(|choice| visible.iter().any(|c| c.key == choice.key));
|
||||
let owned = path.to_owned();
|
||||
out = out.push(
|
||||
pick_list(visible, visible_selection, move |choice| {
|
||||
Message::FieldValuePath(owned.clone(), choice.key)
|
||||
})
|
||||
.placeholder("Choose a field here…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
);
|
||||
if pages > 1 {
|
||||
out = out.push(
|
||||
pick_list(
|
||||
children,
|
||||
None::<crate::data_values::FieldPathChoice>,
|
||||
move |choice| Message::FieldValuePath(path.clone(), choice.key),
|
||||
)
|
||||
.placeholder("Choose a field inside this record…")
|
||||
.text_size(12)
|
||||
.padding(8)
|
||||
.width(Fill),
|
||||
row![
|
||||
button(text("Previous").size(11))
|
||||
.style(style::nav)
|
||||
.on_press_maybe((page > 0).then(|| Message::FieldValueBrowse(
|
||||
path.into(),
|
||||
browser.scope.clone(),
|
||||
page - 1
|
||||
))),
|
||||
text(format!("{} / {pages} · {count} fields", page + 1)).size(11),
|
||||
button(text("Next").size(11))
|
||||
.style(style::nav)
|
||||
.on_press_maybe((page + 1 < pages).then(|| {
|
||||
Message::FieldValueBrowse(
|
||||
path.into(),
|
||||
browser.scope.clone(),
|
||||
page + 1,
|
||||
)
|
||||
}))
|
||||
]
|
||||
.spacing(4)
|
||||
.align_y(iced::Alignment::Center),
|
||||
);
|
||||
} else if count == 0 {
|
||||
out = out.push(
|
||||
text("No compatible fields at this location.")
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
);
|
||||
}
|
||||
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)),
|
||||
_ => {}
|
||||
}
|
||||
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)),
|
||||
}
|
||||
if let Some(choice) = &selected
|
||||
&& browser.scope.as_ref() != Some(&choice.key)
|
||||
&& crate::data_values::children(project, exchange, phase, &choice.key)
|
||||
.is_ok_and(|children| !children.is_empty())
|
||||
{
|
||||
out = out.push(
|
||||
button(
|
||||
text(format!(
|
||||
"Browse fields in {} →",
|
||||
project.named(choice.key.field)
|
||||
))
|
||||
.size(11),
|
||||
)
|
||||
.style(style::nav)
|
||||
.on_press(Message::FieldValueBrowse(
|
||||
path.into(),
|
||||
Some(choice.key.clone()),
|
||||
0,
|
||||
)),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
out = out.push(
|
||||
text("Choose a message exchange to browse its carried fields.")
|
||||
.size(11)
|
||||
.color(style::MUTED),
|
||||
);
|
||||
}
|
||||
if let Some(expression) = value.get("expression") {
|
||||
out = out.push(super::property(
|
||||
@@ -442,6 +490,25 @@ fn message_value<'a>(
|
||||
out.into()
|
||||
}
|
||||
|
||||
const FIELDS_PER_PAGE: usize = 24;
|
||||
|
||||
fn field_page(
|
||||
choices: Vec<crate::data_values::FieldPathChoice>,
|
||||
requested: usize,
|
||||
) -> (Vec<crate::data_values::FieldPathChoice>, usize, usize) {
|
||||
let pages = choices.len().div_ceil(FIELDS_PER_PAGE).max(1);
|
||||
let page = requested.min(pages - 1);
|
||||
(
|
||||
choices
|
||||
.into_iter()
|
||||
.skip(page * FIELDS_PER_PAGE)
|
||||
.take(FIELDS_PER_PAGE)
|
||||
.collect(),
|
||||
page,
|
||||
pages,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -497,6 +564,25 @@ mod tests {
|
||||
format!("{path}/expression"),
|
||||
"newly typed".into(),
|
||||
));
|
||||
let before_browse = app.draft.as_ref().unwrap().materialize().unwrap();
|
||||
for (scope, page) in [
|
||||
(None, 0),
|
||||
(
|
||||
Some(crate::data_values::FieldPathKey {
|
||||
path: vec![f.origin],
|
||||
field: next,
|
||||
}),
|
||||
999,
|
||||
),
|
||||
] {
|
||||
let _ = app.update(Message::FieldValueBrowse(path.into(), scope, page));
|
||||
let _ = super::super::view(app.draft.as_ref().unwrap(), app.project());
|
||||
assert_eq!(
|
||||
app.draft.as_ref().unwrap().materialize().unwrap(),
|
||||
before_browse
|
||||
);
|
||||
assert_eq!(app.project(), &f.project);
|
||||
}
|
||||
let _ = app.update(Message::FieldValuePath(
|
||||
path.into(),
|
||||
crate::data_values::FieldPathKey {
|
||||
@@ -560,6 +646,43 @@ mod tests {
|
||||
let _ = super::super::view(app.draft.as_ref().unwrap(), app.project());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn field_browser_pages_every_direct_field_without_flattening_other_records() {
|
||||
let f = crate::data_values::tests::wide_payload();
|
||||
let exchange = &f.project.relations[&f.exchange];
|
||||
let roots =
|
||||
crate::data_values::at_scope(&f.project, exchange, &MessageKind::Call, None).unwrap();
|
||||
assert!(roots.len() > FIELDS_PER_PAGE);
|
||||
assert!(roots.iter().all(|choice| choice.key.path.is_empty()));
|
||||
let scope = crate::data_values::FieldPathKey {
|
||||
path: vec![],
|
||||
field: f.origin,
|
||||
};
|
||||
let children =
|
||||
crate::data_values::at_scope(&f.project, exchange, &MessageKind::Call, Some(&scope))
|
||||
.unwrap();
|
||||
assert!(children.len() > FIELDS_PER_PAGE);
|
||||
assert!(
|
||||
children
|
||||
.iter()
|
||||
.all(|choice| choice.key.path == vec![f.origin])
|
||||
);
|
||||
for choices in [roots, children] {
|
||||
let (_, _, pages) = field_page(choices.clone(), 0);
|
||||
let visited: Vec<_> = (0..pages)
|
||||
.flat_map(|page| {
|
||||
let (visible, actual, _) = field_page(choices.clone(), page);
|
||||
assert_eq!(actual, page);
|
||||
assert!(visible.len() <= FIELDS_PER_PAGE);
|
||||
visible
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(visited, choices);
|
||||
assert_eq!(field_page(choices, usize::MAX).1, pages - 1);
|
||||
}
|
||||
assert_eq!(field_page(vec![], usize::MAX), (vec![], 0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state() {
|
||||
let mut p = Project::empty();
|
||||
|
||||
Reference in New Issue
Block a user