feat: add records-mode drill-down with staged edits
Introduce records-mode drill-down functionality that allows users to edit individual records without immediately modifying the underlying model. Key changes: - Added DrillState struct to hold frozen records snapshot and pending edits - New effects: StartDrill, ApplyAndClearDrill, SetDrillPendingEdit - Extended CmdContext with records_col and records_value for records mode - CommitCellEdit now stages edits in pending_edits when in records mode - DrillIntoCell captures a snapshot before switching to drill view - GridLayout supports frozen records for stable view during edits - GridWidget renders with drill_state for pending edit display In records mode, edits are staged and only applied to the model when the user navigates away or commits. This prevents data loss and allows batch editing of records. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
@ -35,6 +35,27 @@ pub struct GridLayout {
|
||||
}
|
||||
|
||||
impl GridLayout {
|
||||
/// Build a layout. When records-mode is active and `frozen_records`
|
||||
/// is provided, use that snapshot instead of re-querying the store.
|
||||
pub fn with_frozen_records(
|
||||
model: &Model,
|
||||
view: &View,
|
||||
frozen_records: Option<Vec<(CellKey, CellValue)>>,
|
||||
) -> Self {
|
||||
let mut layout = Self::new(model, view);
|
||||
if layout.is_records_mode() {
|
||||
if let Some(records) = frozen_records {
|
||||
// Re-build with the frozen records instead
|
||||
let row_items: Vec<AxisEntry> = (0..records.len())
|
||||
.map(|i| AxisEntry::DataItem(vec![i.to_string()]))
|
||||
.collect();
|
||||
layout.row_items = row_items;
|
||||
layout.records = Some(records);
|
||||
}
|
||||
}
|
||||
layout
|
||||
}
|
||||
|
||||
pub fn new(model: &Model, view: &View) -> Self {
|
||||
let row_cats: Vec<String> = view
|
||||
.categories_on(Axis::Row)
|
||||
@ -131,7 +152,7 @@ impl GridLayout {
|
||||
.map(|i| AxisEntry::DataItem(vec![i.to_string()]))
|
||||
.collect();
|
||||
|
||||
// Synthesize col items: one per regular category + "Value"
|
||||
// Synthesize col items: one per category + "Value"
|
||||
let cat_names: Vec<String> = model
|
||||
.category_names()
|
||||
.into_iter()
|
||||
@ -227,7 +248,17 @@ impl GridLayout {
|
||||
|
||||
/// Build the CellKey for the data cell at (row, col), including the active
|
||||
/// page-axis filter. Returns None if row or col is out of bounds.
|
||||
/// In records mode: returns the real underlying CellKey when the column
|
||||
/// is "Value" (editable); returns None for coord columns (read-only).
|
||||
pub fn cell_key(&self, row: usize, col: usize) -> Option<CellKey> {
|
||||
if let Some(records) = &self.records {
|
||||
// Records mode: only the Value column maps to a real, editable cell.
|
||||
if self.col_label(col) == "Value" {
|
||||
return records.get(row).map(|(k, _)| k.clone());
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let row_item = self
|
||||
.row_items
|
||||
.iter()
|
||||
@ -393,6 +424,79 @@ mod tests {
|
||||
use super::{AxisEntry, GridLayout};
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::Model;
|
||||
use crate::view::Axis;
|
||||
|
||||
fn records_model() -> Model {
|
||||
let mut m = Model::new("T");
|
||||
m.add_category("Region").unwrap();
|
||||
m.add_category("Measure").unwrap();
|
||||
m.category_mut("Region").unwrap().add_item("North");
|
||||
m.category_mut("Measure").unwrap().add_item("Revenue");
|
||||
m.category_mut("Measure").unwrap().add_item("Cost");
|
||||
m.set_cell(
|
||||
CellKey::new(vec![
|
||||
("Region".into(), "North".into()),
|
||||
("Measure".into(), "Revenue".into()),
|
||||
]),
|
||||
CellValue::Number(100.0),
|
||||
);
|
||||
m.set_cell(
|
||||
CellKey::new(vec![
|
||||
("Region".into(), "North".into()),
|
||||
("Measure".into(), "Cost".into()),
|
||||
]),
|
||||
CellValue::Number(50.0),
|
||||
);
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_mode_activated_when_index_and_dim_on_axes() {
|
||||
let mut m = records_model();
|
||||
let v = m.active_view_mut();
|
||||
v.set_axis("_Index", Axis::Row);
|
||||
v.set_axis("_Dim", Axis::Column);
|
||||
let layout = GridLayout::new(&m, m.active_view());
|
||||
assert!(layout.is_records_mode());
|
||||
assert_eq!(layout.row_count(), 2); // 2 cells
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_mode_cell_key_editable_for_value_column() {
|
||||
let mut m = records_model();
|
||||
let v = m.active_view_mut();
|
||||
v.set_axis("_Index", Axis::Row);
|
||||
v.set_axis("_Dim", Axis::Column);
|
||||
let layout = GridLayout::new(&m, m.active_view());
|
||||
assert!(layout.is_records_mode());
|
||||
// Find the "Value" column index
|
||||
let cols: Vec<String> = (0..layout.col_count()).map(|i| layout.col_label(i)).collect();
|
||||
let value_col = cols.iter().position(|c| c == "Value").unwrap();
|
||||
// cell_key should be Some for Value column
|
||||
let key = layout.cell_key(0, value_col);
|
||||
assert!(key.is_some(), "Value column should be editable");
|
||||
// cell_key should be None for coord columns
|
||||
let region_col = cols.iter().position(|c| c == "Region").unwrap();
|
||||
assert!(
|
||||
layout.cell_key(0, region_col).is_none(),
|
||||
"Region column should not be editable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_mode_cell_key_maps_to_real_cell() {
|
||||
let mut m = records_model();
|
||||
let v = m.active_view_mut();
|
||||
v.set_axis("_Index", Axis::Row);
|
||||
v.set_axis("_Dim", Axis::Column);
|
||||
let layout = GridLayout::new(&m, m.active_view());
|
||||
let cols: Vec<String> = (0..layout.col_count()).map(|i| layout.col_label(i)).collect();
|
||||
let value_col = cols.iter().position(|c| c == "Value").unwrap();
|
||||
// The CellKey at (0, Value) should look up a real cell value
|
||||
let key = layout.cell_key(0, value_col).unwrap();
|
||||
let val = m.evaluate(&key);
|
||||
assert!(val.is_some(), "cell_key should resolve to a real cell");
|
||||
}
|
||||
|
||||
fn coord(pairs: &[(&str, &str)]) -> CellKey {
|
||||
CellKey::new(
|
||||
|
||||
Reference in New Issue
Block a user