Keep nested data field names visible in compact pickers
This commit is contained in:
@@ -11,10 +11,11 @@ pub struct FieldPathKey {
|
|||||||
pub struct FieldPathChoice {
|
pub struct FieldPathChoice {
|
||||||
pub key: FieldPathKey,
|
pub key: FieldPathKey,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
pub picker_label: String,
|
||||||
}
|
}
|
||||||
impl std::fmt::Display for FieldPathChoice {
|
impl std::fmt::Display for FieldPathChoice {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(&self.label)
|
f.write_str(&self.picker_label)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +50,7 @@ fn named_choice(project: &Project, entry: data::CarriedFieldPath) -> FieldPathCh
|
|||||||
.map(|e| format!("{} (@{})", e.name, e.alias))
|
.map(|e| format!("{} (@{})", e.name, e.alias))
|
||||||
.unwrap_or_else(|| id.to_string())
|
.unwrap_or_else(|| id.to_string())
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>();
|
||||||
.join(" → ");
|
|
||||||
let direction =
|
let direction =
|
||||||
if data::item_mechanism(project, entry.item) == Some(ExchangeMechanism::Operation) {
|
if data::item_mechanism(project, entry.item) == Some(ExchangeMechanism::Operation) {
|
||||||
match entry.direction {
|
match entry.direction {
|
||||||
@@ -63,54 +63,83 @@ fn named_choice(project: &Project, entry: data::CarriedFieldPath) -> FieldPathCh
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
};
|
};
|
||||||
|
let terminal = qualified
|
||||||
|
.last()
|
||||||
|
.expect("A field path always has a terminal");
|
||||||
|
let owner_path = qualified[..qualified.len() - 1].join(" → ");
|
||||||
|
let picker_label = format!(
|
||||||
|
"{terminal}{direction} — {}{}",
|
||||||
|
project.named(entry.item),
|
||||||
|
if owner_path.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" / {owner_path}")
|
||||||
|
},
|
||||||
|
);
|
||||||
FieldPathChoice {
|
FieldPathChoice {
|
||||||
key: FieldPathKey {
|
key: FieldPathKey {
|
||||||
path: entry.path,
|
path: entry.path,
|
||||||
field: entry.field,
|
field: entry.field,
|
||||||
},
|
},
|
||||||
label: format!("{} / {qualified}{direction}", project.named(entry.item)),
|
label: format!(
|
||||||
|
"{} / {}{direction}",
|
||||||
|
project.named(entry.item),
|
||||||
|
qualified.join(" → ")
|
||||||
|
),
|
||||||
|
picker_label,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A bounded picker must expose its distinguishing field before a long path.
|
||||||
|
// Keep the full address separately for the wrapping detail text.
|
||||||
|
fn sorted_choices(mut choices: Vec<FieldPathChoice>) -> Vec<FieldPathChoice> {
|
||||||
|
choices.sort_by_cached_key(|choice| choice.picker_label.to_lowercase());
|
||||||
|
choices
|
||||||
|
}
|
||||||
|
|
||||||
pub fn choices(
|
pub fn choices(
|
||||||
project: &Project,
|
project: &Project,
|
||||||
exchange: &Relation,
|
exchange: &Relation,
|
||||||
phase: &MessageKind,
|
phase: &MessageKind,
|
||||||
) -> Result<Vec<FieldPathChoice>, String> {
|
) -> Result<Vec<FieldPathChoice>, String> {
|
||||||
Ok(data::try_carried_field_paths(project, exchange)?
|
Ok(sorted_choices(
|
||||||
.into_iter()
|
data::try_carried_field_paths(project, exchange)?
|
||||||
.filter(|entry| phase_allows(project, entry, phase))
|
.into_iter()
|
||||||
.map(|entry| named_choice(project, entry))
|
.filter(|entry| phase_allows(project, entry, phase))
|
||||||
.collect())
|
.map(|entry| named_choice(project, entry))
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Direct carried fields only: an unrelated deep branch must not prevent
|
/// Direct carried fields only: an unrelated deep branch must not prevent
|
||||||
/// browsing a root field. Preserve the core resolver's first carried owner.
|
/// browsing a root field. Preserve the core resolver's first carried owner.
|
||||||
pub fn roots(project: &Project, exchange: &Relation, phase: &MessageKind) -> Vec<FieldPathChoice> {
|
pub fn roots(project: &Project, exchange: &Relation, phase: &MessageKind) -> Vec<FieldPathChoice> {
|
||||||
let mut seen = std::collections::BTreeSet::new();
|
let mut seen = std::collections::BTreeSet::new();
|
||||||
exchange
|
sorted_choices(
|
||||||
.body
|
exchange
|
||||||
.items()
|
.body
|
||||||
.iter()
|
.items()
|
||||||
.flat_map(|item| {
|
.iter()
|
||||||
data::effective_fields(project, *item)
|
.flat_map(|item| {
|
||||||
.into_iter()
|
data::effective_fields(project, *item)
|
||||||
.filter_map(|field| {
|
.into_iter()
|
||||||
let SharedBody::DataField { direction, .. } = field.shared()? else {
|
.filter_map(|field| {
|
||||||
return None;
|
let SharedBody::DataField { direction, .. } = field.shared()? else {
|
||||||
};
|
return None;
|
||||||
Some(data::CarriedFieldPath {
|
};
|
||||||
item: *item,
|
Some(data::CarriedFieldPath {
|
||||||
path: vec![],
|
item: *item,
|
||||||
field: field.id,
|
path: vec![],
|
||||||
direction: *direction,
|
field: field.id,
|
||||||
|
direction: *direction,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
.filter(|entry| seen.insert(entry.field))
|
||||||
.filter(|entry| seen.insert(entry.field))
|
.filter(|entry| phase_allows(project, entry, phase))
|
||||||
.filter(|entry| phase_allows(project, entry, phase))
|
.map(|entry| named_choice(project, entry))
|
||||||
.map(|entry| named_choice(project, entry))
|
.collect(),
|
||||||
.collect()
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve authored finite paths as well as the finite representative picker list.
|
/// Resolve authored finite paths as well as the finite representative picker list.
|
||||||
@@ -166,20 +195,22 @@ pub fn children(
|
|||||||
}
|
}
|
||||||
let mut path = key.path.clone();
|
let mut path = key.path.clone();
|
||||||
path.push(key.field);
|
path.push(key.field);
|
||||||
Ok(data::effective_fields(project, *record)
|
Ok(sorted_choices(
|
||||||
.into_iter()
|
data::effective_fields(project, *record)
|
||||||
.map(|field| {
|
.into_iter()
|
||||||
named_choice(
|
.map(|field| {
|
||||||
project,
|
named_choice(
|
||||||
data::CarriedFieldPath {
|
project,
|
||||||
item: entry.item,
|
data::CarriedFieldPath {
|
||||||
path: path.clone(),
|
item: entry.item,
|
||||||
field: field.id,
|
path: path.clone(),
|
||||||
direction: entry.direction,
|
field: field.id,
|
||||||
},
|
direction: entry.direction,
|
||||||
)
|
},
|
||||||
})
|
)
|
||||||
.collect())
|
})
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -482,4 +513,33 @@ pub(crate) mod tests {
|
|||||||
.contains("128")
|
.contains("128")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn recursive_picker_starts_with_distinct_leaf_names_and_sorts_them() {
|
||||||
|
let (f, next) = recursive_payload();
|
||||||
|
let exchange = &f.project.relations[&f.exchange];
|
||||||
|
let context = FieldPathKey {
|
||||||
|
path: vec![f.origin, next, next],
|
||||||
|
field: next,
|
||||||
|
};
|
||||||
|
let visible = children(&f.project, exchange, &MessageKind::Call, &context).unwrap();
|
||||||
|
let leaf = visible
|
||||||
|
.iter()
|
||||||
|
.find(|choice| choice.key.field == f.leaf)
|
||||||
|
.unwrap();
|
||||||
|
assert!(leaf.to_string().starts_with("latitude (@latitude)"));
|
||||||
|
assert!(leaf.label.contains("origin (@origin) → next (@next)"));
|
||||||
|
assert!(
|
||||||
|
visible.windows(2).all(
|
||||||
|
|pair| pair[0].to_string().to_lowercase() <= pair[1].to_string().to_lowercase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert_ne!(
|
||||||
|
visible[0].to_string().chars().take(24).collect::<String>(),
|
||||||
|
visible[1].to_string().chars().take(24).collect::<String>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
resolve(&f.project, exchange, &MessageKind::Call, &leaf.key),
|
||||||
|
Some(leaf.clone())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,6 +363,10 @@ fn message_value<'a>(
|
|||||||
{
|
{
|
||||||
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));
|
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 {
|
let retained = crate::data_values::FieldPathChoice {
|
||||||
|
picker_label: format!(
|
||||||
|
"{} · current address needs review",
|
||||||
|
project.named(key.field)
|
||||||
|
),
|
||||||
label: format!(
|
label: format!(
|
||||||
"Current address · {} / {}",
|
"Current address · {} / {}",
|
||||||
key.path
|
key.path
|
||||||
@@ -377,6 +381,9 @@ fn message_value<'a>(
|
|||||||
selected = Some(retained.clone());
|
selected = Some(retained.clone());
|
||||||
choices.push(retained);
|
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();
|
let owned = path.to_owned();
|
||||||
out = out.push(
|
out = out.push(
|
||||||
pick_list(choices, selected, move |choice| {
|
pick_list(choices, selected, move |choice| {
|
||||||
|
|||||||
Reference in New Issue
Block a user