Browse message value properties by record with bounded pages

This commit is contained in:
2026-09-06 04:56:41 +03:00
parent f5467e50c5
commit b35dede75f
8 changed files with 310 additions and 111 deletions
+13
View File
@@ -130,6 +130,7 @@ pub enum Message {
FieldShape(String, Value), FieldShape(String, Value),
FieldAppend(String, Value), FieldAppend(String, Value),
FieldValuePath(String, crate::data_values::FieldPathKey), FieldValuePath(String, crate::data_values::FieldPathKey),
FieldValueBrowse(String, Option<crate::data_values::FieldPathKey>, usize),
FieldVariant(String, Value), FieldVariant(String, Value),
FieldArray(String, Option<usize>, i32), FieldArray(String, Option<usize>, i32),
Apply, Apply,
@@ -1139,6 +1140,18 @@ impl App {
self.status = error; self.status = error;
} }
} }
Message::FieldValueBrowse(path, scope, page) => {
if let Some(draft) = &mut self.draft
&& draft
.value
.pointer(&path)
.is_some_and(|row| row.get("expression").is_some())
{
draft
.value_browsers
.insert(path, crate::forms::ValueBrowser { scope, page });
}
}
Message::FieldValuePath(path, key) => { Message::FieldValuePath(path, key) => {
if let Some(draft) = &mut self.draft if let Some(draft) = &mut self.draft
&& let Err(error) = draft.value_path(&path, key) && let Err(error) = draft.value_path(&path, key)
+14
View File
@@ -97,6 +97,7 @@ fn sorted_choices(mut choices: Vec<FieldPathChoice>) -> Vec<FieldPathChoice> {
choices choices
} }
#[cfg(test)]
pub fn choices( pub fn choices(
project: &Project, project: &Project,
exchange: &Relation, exchange: &Relation,
@@ -142,6 +143,19 @@ pub fn roots(project: &Project, exchange: &Relation, phase: &MessageKind) -> Vec
) )
} }
/// Browse one record at a time, including explicitly chosen recursive hops.
pub fn at_scope(
project: &Project,
exchange: &Relation,
phase: &MessageKind,
scope: Option<&FieldPathKey>,
) -> Result<Vec<FieldPathChoice>, String> {
match scope {
Some(key) => children(project, exchange, phase, key),
None => Ok(roots(project, exchange, phase)),
}
}
/// Resolve authored finite paths as well as the finite representative picker list. /// Resolve authored finite paths as well as the finite representative picker list.
/// In particular, optional recursive Value records may be traversed repeatedly. /// In particular, optional recursive Value records may be traversed repeatedly.
pub fn resolve( pub fn resolve(
+19 -4
View File
@@ -32,6 +32,12 @@ pub struct Input {
pub raw: String, pub raw: String,
pub editor: text_editor::Content, pub editor: text_editor::Content,
} }
#[derive(Clone, Default)]
pub struct ValueBrowser {
pub scope: Option<crate::data_values::FieldPathKey>,
pub page: usize,
}
pub struct Draft { pub struct Draft {
pub kind: RecordKind, pub kind: RecordKind,
pub original: Id, pub original: Id,
@@ -40,6 +46,7 @@ pub struct Draft {
pub inputs: BTreeMap<String, Input>, pub inputs: BTreeMap<String, Input>,
pub exact_strings: bool, pub exact_strings: bool,
port_contract_drafts: BTreeMap<String, Value>, port_contract_drafts: BTreeMap<String, Value>,
pub value_browsers: BTreeMap<String, ValueBrowser>,
} }
impl Draft { impl Draft {
@@ -77,11 +84,14 @@ impl Draft {
inputs: BTreeMap::new(), inputs: BTreeMap::new(),
exact_strings: false, exact_strings: false,
port_contract_drafts: BTreeMap::new(), port_contract_drafts: BTreeMap::new(),
value_browsers: BTreeMap::new(),
}; };
draft.rebuild(); draft.rebuild();
Some(draft) Some(draft)
} }
fn rebuild(&mut self) { fn rebuild(&mut self) {
// Array/variant edits can change the meaning of a row path.
self.value_browsers.clear();
self.inputs.clear(); self.inputs.clear();
fn visit(value: &Value, path: &str, out: &mut BTreeMap<String, Input>) { fn visit(value: &Value, path: &str, out: &mut BTreeMap<String, Input>) {
match value { match value {
@@ -710,10 +720,15 @@ fn reference_ids(draft: &Draft, project: &Project, path: &str) -> Vec<Id> {
.. ..
}, },
.. ..
} => project } => project.relations.get(id).and_then(|r| {
.relations let scope = address_path.split_last().map(|(&field, path)| {
.get(id) crate::data_values::FieldPathKey {
.and_then(|r| crate::data_values::choices(project, r, message_kind).ok()), path: path.to_vec(),
field,
}
});
crate::data_values::at_scope(project, r, message_kind, scope.as_ref()).ok()
}),
_ => None, _ => None,
}) })
.unwrap_or_default() .unwrap_or_default()
+211 -88
View File
@@ -330,104 +330,152 @@ fn message_value<'a>(
} => project.relations.get(id).map(|r| (r, message_kind)), } => project.relations.get(id).map(|r| (r, message_kind)),
_ => None, _ => None,
}); });
let choices = context let current_key = current
.map(|(r, phase)| crate::data_values::choices(project, r, phase)) .as_ref()
.unwrap_or_else(|| Ok(vec![])); .map(|value| crate::data_values::FieldPathKey {
let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6); path: value.path.clone(),
match choices { field: value.field,
Err(error) => out = out.push(text(error).size(11).color(style::MUTED)), });
Ok(mut choices) => { let selected = context.and_then(|(exchange, phase)| {
let current_key = current.as_ref().map(|v| crate::data_values::FieldPathKey { current_key
path: v.path.clone(), .as_ref()
field: v.field, .and_then(|key| crate::data_values::resolve(project, exchange, phase, key))
}); });
let mut selected = current_key let browser = draft
.value_browsers
.get(path)
.cloned()
.unwrap_or_else(|| ValueBrowser {
scope: selected
.as_ref() .as_ref()
.and_then(|key| choices.iter().find(|c| &c.key == key)) .and_then(|choice| crate::data_values::parent(&choice.key)),
.cloned() page: 0,
.or_else(|| { });
context.and_then(|(r, phase)| { let mut out = column![text("Qualified carried field").size(11).color(style::MUTED)].spacing(6);
current_key if let Some(choice) = &selected {
.as_ref() out = out.push(text(choice.label.clone()).size(11));
.and_then(|key| crate::data_values::resolve(project, r, phase, key)) } else if let Some(key) = &current_key
}) && !key.field.is_nil()
}); {
if let Some(choice) = &selected out = out.push(
&& !choices.iter().any(|c| c.key == choice.key) text(format!(
{ "Current address needs review: {} / {}. Choose an explicit field.",
choices.push(choice.clone()); key.path
} .iter()
if selected.is_none() .map(|id| project.named(*id))
&& let Some(key) = current_key.clone() .collect::<Vec<_>>()
&& !key.field.is_nil() .join(" → "),
{ project.named(key.field)
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 { .size(11)
picker_label: format!( .color(style::MUTED),
"{} · 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),
);
}
} }
if let (Some((exchange, phase)), Some(current)) = (context, current) { if let Some((exchange, phase)) = context {
let key = crate::data_values::FieldPathKey { let location = browser
path: current.path, .scope
field: current.field, .as_ref()
}; .and_then(|key| crate::data_values::resolve(project, exchange, phase, key))
if crate::data_values::resolve(project, exchange, phase, &key).is_some() { .map(|choice| choice.label)
if let Some(parent) = crate::data_values::parent(&key) { .unwrap_or_else(|| "Root carried fields".into());
out = out.push( out = out.push(
button(text("Up to enclosing field").size(11)) 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) .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) { out = out.push(navigation);
Ok(children) if !children.is_empty() => { }
let path = path.to_owned(); 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( out = out.push(
pick_list( row![
children, button(text("Previous").size(11))
None::<crate::data_values::FieldPathChoice>, .style(style::nav)
move |choice| Message::FieldValuePath(path.clone(), choice.key), .on_press_maybe((page > 0).then(|| Message::FieldValueBrowse(
) path.into(),
.placeholder("Choose a field inside this record…") browser.scope.clone(),
.text_size(12) page - 1
.padding(8) ))),
.width(Fill), 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") { if let Some(expression) = value.get("expression") {
out = out.push(super::property( out = out.push(super::property(
@@ -442,6 +490,25 @@ fn message_value<'a>(
out.into() 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -497,6 +564,25 @@ mod tests {
format!("{path}/expression"), format!("{path}/expression"),
"newly typed".into(), "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( let _ = app.update(Message::FieldValuePath(
path.into(), path.into(),
crate::data_values::FieldPathKey { crate::data_values::FieldPathKey {
@@ -560,6 +646,43 @@ mod tests {
let _ = super::super::view(app.draft.as_ref().unwrap(), app.project()); 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] #[test]
fn numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state() { fn numeric_bound_drafts_keep_partial_and_full_precision_values_out_of_accepted_state() {
let mut p = Project::empty(); let mut p = Project::empty();
+6
View File
@@ -63,3 +63,9 @@ State camera/tool lifetime follows effective machine and diagram identity, even
when selection changes through Catalog or Layer outside Behavior. Reset only on when selection changes through Catalog or Layer outside Behavior. Reset only on
actual context change; preserve same-view return. Independent review reproduced actual context change; preserve same-view return. Independent review reproduced
the leak and native Windows/Linux tests verified the correction in316dc55. the leak and native Windows/Linux tests verified the correction in316dc55.
- Carried-value property browsing follows one exact record scope at a time, with
bounded pages and a separate read-only browser location. Flattened representative
paths were proven ambiguous in native recursive/reused data. Browsing does not
change the selected field or expression; choosing a field remains an explicit
draft edit through the ordinary validated command. No persistent browser schema.
+13 -11
View File
@@ -134,10 +134,10 @@ plan and decision are recorded in that checkout. No overlapping writers or targe
## Environment and native ownership ## Environment and native ownership
The user explicitly reauthorized native testing after Escape. Root's current R6 The user explicitly reauthorized native testing after Escape. Root's f5467e5 R6
window1835476 uses the frozen ab661f2 binary and ui-r6-windows/model/project.arcadia; window3476204 and exec93768 closed normally after verified source migration and
13objects/4links/1 expected missing-SA-System warning, saved. Currently1100x700. 55-object recursive scenario reopen; no runtime output. The property-browser
Original Capella is present. Older protected user7210312/6228978 were absent; replacement build is running. Original Capella is present. Older protected user7210312/6228978 were absent;
do not reset/save/close them if they return. Re-inventory before switching apps. do not reset/save/close them if they return. Re-inventory before switching apps.
Evidence: D:/CodexBuilds/Capella2-release1-01a06e2e. State source checkout/target: Evidence: D:/CodexBuilds/Capella2-release1-01a06e2e. State source checkout/target:
@@ -150,10 +150,12 @@ Windows: cargo test --locked --workspace; cargo clippy --locked --workspace
WSL Ubuntu24.04 uses target/linux-cargo.toml and Mesa software Vulkan for native WSL Ubuntu24.04 uses target/linux-cargo.toml and Mesa software Vulkan for native
WSLg. No remote push or usage reset authorized. No broad recursive target deletion. WSLg. No remote push or usage reset authorized. No broad recursive target deletion.
Latest R6 checkpoint: native recursive scenario authoring passed two distinct Latest R6 checkpoint: Windows264 desktop tests and strict Clippy pass for the
shared-leaf values, paired Create/Undo/Redo and save (55objects/5links). Runtime one-record property browser; optimized build/native retest pending. Paging and
found unreadable long-path picker names (fixed74989b6,262 desktop tests/Clippy/build) location changes retain accepted model and typed expressions. f5467e5 Windows
and single-file source3 migration discarding comments on save (desktop shortcut and Linux263 desktop tests, strict Clippy and optimized release passed. Native
now corrected; actual disk include/single-file regression passes). Final build and Windows source3 migration preserved comments/nesting through explicit Save All
native retests remain. Old native exec37598 closed normally. See the updated R6 and clean source4 reopen. Native recursive55-object scenario values and paired
verification note; earlier ab661f2 suite evidence remains valid only for that freeze. Call/Reply history were verified; flat property paths prompted the latest fix.
Linux native R6 workflows and final independent browser review remain. See
release-data-verification.md. Main remains a8f451e; R6 is not yet accepted.
+12 -8
View File
@@ -39,9 +39,13 @@ requires native workflows, semantic evaluators and fresh independent review.
whole-model equality or flat reformatting. Independent counterexamples were fixed. whole-model equality or flat reformatting. Independent counterexamples were fixed.
- R3 existing-target reconciliation remains name/description only. R6 data shape - R3 existing-target reconciliation remains name/description only. R6 data shape
expansion is active implementation, not accepted functionality. expansion is active implementation, not accepted functionality.
- R6 core/source and actual HTTP semantics pass focused Windows and full Linux - R6 ab661f2 full suites passed Windows607/Linux610; f5467e5 desktop263 and
core checks; desktop257 tests pass after independent draft/path/paging fixes. strict Clippy/release passed on both platforms. Native Windows data editing,
Native R6 acceptance and bounded scenario-value authoring remain outstanding. typed defaults, allocations, recursive qualified Call/Reply values, history and
saved reopening passed. Source3 migration now preserves comments and nesting.
Native found flat property browsing still ambiguous; one-record browsing with
bounded pages now passes264 desktop tests/Clippy, pending rebuilt native checks.
Linux native R6 acceptance remains outstanding.
## Assumptions and open questions ## Assumptions and open questions
@@ -51,11 +55,11 @@ requires native workflows, semantic evaluators and fresh independent review.
gestures still need their own occurrence/order semantics and task verification. gestures still need their own occurrence/order semantics and task verification.
- Routing offers a usable initial path plus manual pins; arbitrary dense diagrams - Routing offers a usable initial path plus manual pins; arbitrary dense diagrams
are not guaranteed crossing-free. Measured dense full solve119ms remains a limit. are not guaranteed crossing-free. Measured dense full solve119ms remains a limit.
- R3 typed historical bodies plus finite proposals appear simpler than a separate - R3 selected design C uses finite self-tagged typed facets, separate membership
recursive semantic-value schema. An isolated probe supports scalar partial review and order acknowledgements, and transient constructor groups. Source5 historical
and deleted historical references, but ordered membership/order acknowledgements references are implemented in the isolated semantic checkout. Independent review
need separate typed order checkpoints. Historical source reference parsing is found narrow-acknowledgement/canonical-agreement/rollup defects; the worker has
still unimplemented; this remains a candidate awaiting independent review. added regressions and fixes. Full catalog, GUI/API/spec and acceptance remain.
## Decisions and current strategy ## Decisions and current strategy
+22
View File
@@ -95,3 +95,25 @@ Full desktop/Clippy/build and native retest are in progress; do not accept R6 ye
The old Windows native window1835476 closed normally; exec37598 exited0 with no The old Windows native window1835476 closed normally; exec37598 exited0 with no
runtime output beyond a newline. Re-inventory before opening the rebuilt binary. runtime output beyond a newline. Re-inventory before opening the rebuilt binary.
## Migration retest and bounded property browsing
f5467e5 passed263 desktop tests, strict all-target desktop Clippy and optimized
release on Windows/Linux. Frozen Windows SHA256
33D1B2B6709BBC734E265568C7F93CB86A506497DEF3F4531669E5ABF8B2F039.
Native fresh legacy.arcadia import preserved original bytes until Save All,
displayed8defaults/2allocations/1unresolved address, and retained comments and
nested fields in its text editor. Save All kept the same file and source4 wording;
reopen was clean15objects/3links/4checks without a migration report. Saved SHA256
A8C0A689FEA6CC352007ACB127DE4A45E05290830DE815385CFFBAB777255543.
Evidence: ui-r6-final-windows/migration/legacy.arcadia.
The55-object recursive scenario reopened with both qualified expressions intact.
Leaf-first labels fixed the hidden terminal names, but existing-value properties
still flattened every representative path, causing repeated indistinguishable
leaf names. The follow-up browses one record at a time, with24-choice pages and
wrapping current address/location text. Up/root/page actions change only browser
state; draft field and expression are preserved.264 desktop tests and strict
Clippy passed, including wide-record page coverage and draft preservation across
recursive browsing. Optimized build and native retest remain pending. Frozen
f5467e5 window3476204 closed normally; exec93768 had no runtime output.