feat(tui,wasm,io): the grid, page bar, item rows and CSV export draw displays (improvise-cq1q.12)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
This commit is contained in:
committed by
Edward Langley
parent
386b693bf5
commit
7a4dc07642
@@ -1902,6 +1902,179 @@ mod tests {
|
||||
assert_ne!(summary.name, "Overview");
|
||||
}
|
||||
|
||||
/// A category, an item, a group, a page category, a measure and two views,
|
||||
/// each shown under a name that is not its key (improvise-cq1q.12).
|
||||
fn app_shown_otherwise() -> App {
|
||||
let mut wb = Workbook::new("Test");
|
||||
wb.add_category("Region").unwrap();
|
||||
wb.add_category("Quarter").unwrap();
|
||||
wb.add_item_in_group("Region", "East", "Coastal").unwrap();
|
||||
wb.add_item("Region", "West").unwrap();
|
||||
wb.add_item("Quarter", "Q1").unwrap();
|
||||
wb.add_item("Quarter", "Q2").unwrap();
|
||||
wb.add_item("_Measure", "Revenue").unwrap();
|
||||
wb.active_view_mut().set_axis("Region", Axis::Row);
|
||||
wb.active_view_mut().set_axis("_Measure", Axis::Column);
|
||||
wb.active_view_mut().set_axis("Quarter", Axis::Page);
|
||||
wb.active_view_mut().set_expand_depth("Region", 1);
|
||||
for region in ["East", "West"] {
|
||||
for quarter in ["Q1", "Q2"] {
|
||||
wb.model.set_cell(
|
||||
CellKey::new(vec![
|
||||
("Region".into(), region.into()),
|
||||
("Quarter".into(), quarter.into()),
|
||||
("_Measure".into(), "Revenue".into()),
|
||||
]),
|
||||
CellValue::Number(1.0),
|
||||
);
|
||||
}
|
||||
}
|
||||
wb.create_view("Plan").set_axis("Region", Axis::Column);
|
||||
wb.active_view_mut().set_display("Overview");
|
||||
wb.views.get_mut("Plan").unwrap().set_display("Budget");
|
||||
let region = wb.model.category_mut("Region").unwrap();
|
||||
region.set_display("Sales Area");
|
||||
region.set_item_display("East", "Eastern Seaboard");
|
||||
region.set_group_display("Coastal", "Shoreline");
|
||||
let quarter = wb.model.category_mut("Quarter").unwrap();
|
||||
quarter.set_display("Period");
|
||||
quarter.set_item_display("Q1", "First");
|
||||
quarter.set_item_display("Q2", "Second");
|
||||
wb.model
|
||||
.category_mut("_Measure")
|
||||
.unwrap()
|
||||
.set_item_display("Revenue", "Sales");
|
||||
let mut app = App::new(wb, None);
|
||||
app.rebuild_layout();
|
||||
app
|
||||
}
|
||||
|
||||
const ALL: Viewport = Viewport {
|
||||
row_offset: 0,
|
||||
col_offset: 0,
|
||||
rows: 20,
|
||||
cols: 20,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn the_frame_shows_displays_and_its_key_tuples_still_address_the_cells() {
|
||||
let app = app_shown_otherwise();
|
||||
let grid = grid_of(&app, ALL);
|
||||
assert_eq!(grid.row_frame.cat_displays, vec!["Sales Area"]);
|
||||
assert_eq!(grid.row_frame.cats, vec!["Region"], "the key stays beside it");
|
||||
assert_eq!(grid.col_frame.header_labels(0), vec![vec!["Sales".to_string()]]);
|
||||
let header = grid
|
||||
.row_frame
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|e| match e {
|
||||
FrameEntry::Group(g) => Some(g),
|
||||
_ => None,
|
||||
})
|
||||
.expect("East sits under Coastal");
|
||||
assert_eq!((header.name.as_str(), header.display.as_str()), ("Coastal", "Shoreline"));
|
||||
assert_eq!(
|
||||
grid.row_frame.header_labels(0)[0],
|
||||
vec!["Eastern Seaboard".to_string()]
|
||||
);
|
||||
// The frame's KEY tuples, joined with the page bar, are the cell's address.
|
||||
let page: Vec<(String, String)> = grid
|
||||
.page_coords
|
||||
.iter()
|
||||
.map(|p| (p.cat.clone(), p.selected.clone()))
|
||||
.collect();
|
||||
for (r, row) in grid.row_frame.items().enumerate() {
|
||||
for (c, col) in grid.col_frame.items().enumerate() {
|
||||
let coords = grid
|
||||
.row_frame
|
||||
.cats
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(row.iter().cloned())
|
||||
.chain(grid.col_frame.cats.iter().cloned().zip(col.iter().cloned()))
|
||||
.chain(page.iter().cloned())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
Some(CellKey::new(coords)),
|
||||
app.layout.cell_key(r, c),
|
||||
"({r},{c}) must be addressed by keys"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_page_bar_carries_displays_beside_the_keys_a_page_command_names() {
|
||||
let app = app_shown_otherwise();
|
||||
let bar = grid_of(&app, ALL).page_coords;
|
||||
let quarter = &bar[0];
|
||||
assert_eq!((quarter.cat.as_str(), quarter.selected.as_str()), ("Quarter", "Q1"));
|
||||
assert_eq!(quarter.cat_display, "Period");
|
||||
assert_eq!(quarter.selected_display, "First");
|
||||
assert_eq!(quarter.items.as_deref(), Some(&["Q1".to_string(), "Q2".into()][..]));
|
||||
assert_eq!(
|
||||
quarter.item_displays.as_deref(),
|
||||
Some(&["First".to_string(), "Second".into()][..])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_item_row_shows_its_display_and_keeps_its_key() {
|
||||
let app = app_shown_otherwise();
|
||||
let rows = category_rows(&app.model_state.workbook.model, app.view());
|
||||
let region = rows.iter().find(|r| r.name == "Region").unwrap();
|
||||
let east = region.items.iter().find(|i| i.name == "East").unwrap();
|
||||
assert_eq!(east.display, "Eastern Seaboard");
|
||||
let west = region.items.iter().find(|i| i.name == "West").unwrap();
|
||||
assert_eq!(west.display, "West", "an unrenamed item shows its key");
|
||||
let measure = rows.iter().find(|r| r.name == "_Measure").unwrap();
|
||||
assert_eq!(measure.items[0].display, "Sales");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_view_summary_shows_its_own_display_and_its_categories_displays() {
|
||||
let app = app_shown_otherwise();
|
||||
let summaries = view_summaries(&app);
|
||||
let by_key = |key: &str| summaries.iter().find(|s| s.name == key).unwrap();
|
||||
assert_eq!(by_key("Default").display, "Overview");
|
||||
assert_eq!(by_key("Plan").display, "Budget", "not the active view's");
|
||||
let plan_region = by_key("Plan")
|
||||
.axes
|
||||
.iter()
|
||||
.find(|a| a.category == "Region")
|
||||
.unwrap();
|
||||
assert_eq!(plan_region.display, "Sales Area");
|
||||
assert_eq!(plan_region.axis, Axis::Column);
|
||||
let line = improvise_protocol::view_axis_summary(by_key("Plan"));
|
||||
assert_eq!(line, "C:Sales Area,Period", "the line draws displays, not keys");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_records_coordinate_shows_its_display_and_edits_its_key() {
|
||||
let mut app = app_shown_otherwise();
|
||||
let view = app.model_state.workbook.active_view_mut();
|
||||
view.set_axis("_Index", Axis::Row);
|
||||
view.set_axis("_Dim", Axis::Column);
|
||||
app.rebuild_layout();
|
||||
let grid = grid_of(&app, ALL);
|
||||
assert!(grid.records);
|
||||
let col = grid
|
||||
.col_frame
|
||||
.items()
|
||||
.position(|t| t == &vec!["Region".to_string()])
|
||||
.expect("a column per category, found by its key");
|
||||
assert_eq!(grid.col_frame.header_labels(0)[col], vec!["Sales Area".to_string()]);
|
||||
let shown: Vec<&str> = grid.cells.iter().map(|r| r[col].text.as_str()).collect();
|
||||
assert!(shown.contains(&"Eastern Seaboard"), "{shown:?}");
|
||||
let east = grid
|
||||
.cells
|
||||
.iter()
|
||||
.map(|r| &r[col])
|
||||
.find(|c| c.text == "Eastern Seaboard")
|
||||
.unwrap();
|
||||
assert_eq!(east.edit_text.as_deref(), Some("East"), "the editor seeds the key");
|
||||
}
|
||||
|
||||
/// A pivot-shaped default view plus a records-shaped second one, so a
|
||||
/// switch between them crosses the boundary `settled_mode` watches.
|
||||
fn app_with_a_records_shaped_second_view() -> App {
|
||||
|
||||
@@ -41,11 +41,11 @@ pub fn render_csv(workbook: &Workbook, view_name: &str) -> Result<Vec<u8>> {
|
||||
.from_writer(Vec::new());
|
||||
|
||||
// Header row
|
||||
let row_header = layout.row_cats.join("/");
|
||||
let row_header = layout.row_cat_displays(model).join("/");
|
||||
let page_label: Vec<String> = layout
|
||||
.page_coords
|
||||
.iter()
|
||||
.map(|p| format!("{}={}", p.cat, p.selected))
|
||||
.map(|p| format!("{}={}", p.cat_display, p.selected_display))
|
||||
.collect();
|
||||
let header_prefix = if page_label.is_empty() {
|
||||
row_header
|
||||
@@ -56,13 +56,13 @@ pub fn render_csv(workbook: &Workbook, view_name: &str) -> Result<Vec<u8>> {
|
||||
if !header_prefix.is_empty() {
|
||||
header.push(header_prefix);
|
||||
}
|
||||
header.extend((0..layout.col_count()).map(|ci| layout.col_label(ci)));
|
||||
header.extend((0..layout.col_count()).map(|ci| layout.col_display_label(ci)));
|
||||
wtr.write_record(&header)?;
|
||||
|
||||
// Data rows
|
||||
for ri in 0..layout.row_count() {
|
||||
let mut record: Vec<String> = Vec::new();
|
||||
let row_label = layout.row_label(ri);
|
||||
let row_label = layout.row_display_label(ri);
|
||||
if !row_label.is_empty() {
|
||||
record.push(row_label);
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ pub fn render_model(state: &ClientState) -> RenderModel {
|
||||
let row = match entry {
|
||||
FrameEntry::GroupEnd { .. } => return Some(None),
|
||||
FrameEntry::Group(group) => RenderRow::Group {
|
||||
label: group.name.clone(),
|
||||
label: group.display.clone(),
|
||||
collapsed: group.collapsed,
|
||||
},
|
||||
FrameEntry::Item { .. } => {
|
||||
@@ -340,12 +340,14 @@ fn page_axes(state: &ClientState) -> Vec<RenderPageAxis> {
|
||||
.page_coords
|
||||
.iter()
|
||||
.map(|axis| RenderPageAxis {
|
||||
cat: axis.cat.clone(),
|
||||
selected: axis.selected.clone(),
|
||||
cat: axis.cat_display.clone(),
|
||||
selected: axis.selected_display.clone(),
|
||||
cycle: axis.cycle().map(|(items, at)| RenderPageCycle {
|
||||
at,
|
||||
count: items.len(),
|
||||
items: items.to_vec(),
|
||||
// The menu shows displays; a pick travels as the index `at` and
|
||||
// `items` order name, so no key is read back from here.
|
||||
items: axis.item_displays.clone().unwrap_or_default(),
|
||||
}),
|
||||
})
|
||||
.collect()
|
||||
@@ -481,8 +483,8 @@ impl RenderCatRow {
|
||||
expanded: Some(*expanded),
|
||||
selected,
|
||||
},
|
||||
CatTreeEntry::Item { item_name, .. } => Self {
|
||||
label: item_name.clone(),
|
||||
CatTreeEntry::Item { display, .. } => Self {
|
||||
label: display.clone(),
|
||||
is_category: false,
|
||||
axis: String::new(),
|
||||
item_count: None,
|
||||
@@ -544,7 +546,7 @@ fn col_groups(cols: &AxisFrame) -> Vec<RenderColGroup> {
|
||||
true => runs
|
||||
.into_iter()
|
||||
.map(|(group, span)| RenderColGroup {
|
||||
label: group.map(|g| g.name.clone()),
|
||||
label: group.map(|g| g.display.clone()),
|
||||
collapsed: group.is_some_and(|g| g.collapsed),
|
||||
span,
|
||||
})
|
||||
@@ -594,6 +596,7 @@ mod tests {
|
||||
level: 0,
|
||||
depth: 0,
|
||||
name: name.into(),
|
||||
display: name.into(),
|
||||
collapsed,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user