feat: add records mode (long-format view) for drill-down
Implement records mode (long-format view) when drilling into aggregated cells. Key changes: - DrillIntoCell now creates views with _Index on Row and _Dim on Column - GridLayout detects records mode and builds a records list instead of cross-product row/col items - Added records_display() to render individual cell values in records mode - GridWidget and CSV export updated to handle records mode rendering - category_names() now includes virtual categories (_Index, _Dim) - Tests updated to reflect virtual categories counting toward limits Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
@ -1005,10 +1005,9 @@ impl Cmd for ViewForwardCmd {
|
||||
}
|
||||
}
|
||||
|
||||
/// Drill down into an aggregated cell: create a _Drill view that shows the
|
||||
/// raw (un-aggregated) data for this cell. Categories on Axis::None in the
|
||||
/// current view become visible (Row + Column) in the drill view; the cell's
|
||||
/// fixed coordinates become page filters.
|
||||
/// Drill down into an aggregated cell: create a _Drill view with _Index on
|
||||
/// Row and _Dim on Column (records/long-format view). Fixed coordinates
|
||||
/// from the drilled cell become page filters.
|
||||
#[derive(Debug)]
|
||||
pub struct DrillIntoCell {
|
||||
pub key: crate::model::cell::CellKey,
|
||||
@ -1025,46 +1024,40 @@ impl Cmd for DrillIntoCell {
|
||||
effects.push(Box::new(effect::CreateView(drill_name.clone())));
|
||||
effects.push(Box::new(effect::SwitchView(drill_name)));
|
||||
|
||||
// All categories currently exist. Set axes:
|
||||
// - none_cats → Row (first) and Column (rest) to expand them
|
||||
// - cell_key cats → Page with their specific items (filter)
|
||||
// - other cats (not in cell_key or none_cats) → Page as well
|
||||
let none_cats = &ctx.none_cats;
|
||||
// Records mode: _Index on Row, _Dim on Column
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: "_Index".to_string(),
|
||||
axis: crate::view::Axis::Row,
|
||||
}));
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: "_Dim".to_string(),
|
||||
axis: crate::view::Axis::Column,
|
||||
}));
|
||||
|
||||
// Fixed coords (from drilled cell) → Page with that value as filter
|
||||
let fixed_cats: std::collections::HashSet<String> =
|
||||
self.key.0.iter().map(|(c, _)| c.clone()).collect();
|
||||
|
||||
for (i, cat) in none_cats.iter().enumerate() {
|
||||
let axis = if i == 0 {
|
||||
crate::view::Axis::Row
|
||||
} else {
|
||||
crate::view::Axis::Column
|
||||
};
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: cat.clone(),
|
||||
axis,
|
||||
}));
|
||||
}
|
||||
|
||||
// All other categories → Page, with the cell's value as the page selection
|
||||
for cat_name in ctx.model.category_names() {
|
||||
let cat = cat_name.to_string();
|
||||
if none_cats.contains(&cat) {
|
||||
continue;
|
||||
}
|
||||
for (cat, item) in &self.key.0 {
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: cat.clone(),
|
||||
axis: crate::view::Axis::Page,
|
||||
}));
|
||||
// If this category was in the drilled cell's key, fix its page
|
||||
// selection to the cell's value
|
||||
if fixed_cats.contains(&cat) {
|
||||
if let Some((_, item)) = self.key.0.iter().find(|(c, _)| c == &cat) {
|
||||
effects.push(Box::new(effect::SetPageSelection {
|
||||
category: cat,
|
||||
item: item.clone(),
|
||||
}));
|
||||
}
|
||||
effects.push(Box::new(effect::SetPageSelection {
|
||||
category: cat.clone(),
|
||||
item: item.clone(),
|
||||
}));
|
||||
}
|
||||
// Previously-aggregated categories (none_cats) stay on Axis::None so
|
||||
// they don't filter records; they'll appear as columns in records mode.
|
||||
// Skip virtual categories — we already set _Index/_Dim above.
|
||||
for cat in &ctx.none_cats {
|
||||
if fixed_cats.contains(cat) || cat.starts_with('_') {
|
||||
continue;
|
||||
}
|
||||
effects.push(Box::new(effect::SetAxis {
|
||||
category: cat.clone(),
|
||||
axis: crate::view::Axis::None,
|
||||
}));
|
||||
}
|
||||
|
||||
effects.push(effect::set_status("Drilled into cell"));
|
||||
@ -2782,11 +2775,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn enter_tile_select_no_categories() {
|
||||
// Models always have virtual categories (_Index, _Dim), so tile
|
||||
// select always has something to operate on.
|
||||
let m = Model::new("Empty");
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = EnterTileSelect;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert!(effects.is_empty());
|
||||
assert_eq!(effects.len(), 2); // SetTileCatIdx + ChangeMode
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user