refactor!: unify records and pivot mode cell handling

Refactor records mode to use synthetic CellKeys (_Index, _Dim) for all columns,
allowing uniform handling of display values and edits across both pivot and
records modes.

- Introduce `synthetic_record_info` to extract metadata from synthetic keys.
- Update `GridLayout::cell_key` to return synthetic keys in records mode.
- Add `GridLayout::resolve_display` to handle value resolution for synthetic
  keys.
- Replace `records_col` and `records_value` in `CmdContext` with a unified
  `display_value`.
- Update `EditOrDrill` and `AddRecordRow` to use synthetic key detection.
- Refactor `CommitCellEdit` to use a shared `commit_cell_value` helper.

BREAKING CHANGE: CmdContext fields `records_col` and `records_value` are replaced by
`display_value` .
Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/gemma-4-31B-it-GGUF:UD-Q5_K_XL)
This commit is contained in:
Edward Langley
2026-04-06 21:21:29 -07:00
parent 9e02939b66
commit fbd672d5ed
5 changed files with 141 additions and 98 deletions

View File

@ -2,6 +2,14 @@ use crate::model::cell::{CellKey, CellValue};
use crate::model::Model;
use crate::view::{Axis, View};
/// Extract (record_index, dim_name) from a synthetic records-mode CellKey.
/// Returns None for normal pivot-mode keys.
pub fn synthetic_record_info(key: &CellKey) -> Option<(usize, String)> {
let idx: usize = key.get("_Index")?.parse().ok()?;
let dim = key.get("_Dim")?.to_string();
Some((idx, dim))
}
/// One entry on a grid axis: either a visual group header or a data-item tuple.
///
/// `GroupHeader` entries are always visible so the user can see the group label
@ -352,18 +360,36 @@ impl GridLayout {
.unwrap_or_default()
}
/// Resolve the display string for a synthetic records-mode CellKey.
/// Returns None for non-synthetic (pivot) keys.
pub fn resolve_display(&self, key: &CellKey) -> Option<String> {
let (idx, dim) = synthetic_record_info(key)?;
let records = self.records.as_ref()?;
let (orig_key, value) = records.get(idx)?;
if dim == "Value" {
Some(value.to_string())
} else {
Some(orig_key.get(&dim).unwrap_or("").to_string())
}
}
/// 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).
/// In records mode: returns a synthetic `(_Index, _Dim)` key for every column.
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 {
if self.records.is_some() {
let records = self.records.as_ref().unwrap();
if row >= records.len() {
return None;
}
let col_label = self.col_label(col);
if col_label.is_empty() {
return None;
}
return Some(CellKey::new(vec![
("_Index".to_string(), row.to_string()),
("_Dim".to_string(), col_label),
]));
}
let row_item = self
.row_items
@ -527,7 +553,7 @@ fn cross_product(model: &Model, view: &View, cats: &[String]) -> Vec<AxisEntry>
#[cfg(test)]
mod tests {
use super::{AxisEntry, GridLayout};
use super::{synthetic_record_info, AxisEntry, GridLayout};
use crate::model::cell::{CellKey, CellValue};
use crate::model::Model;
use crate::view::Axis;
@ -592,40 +618,66 @@ mod tests {
}
#[test]
fn records_mode_cell_key_editable_for_value_column() {
fn records_mode_cell_key_returns_synthetic_for_all_columns() {
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();
// All columns return synthetic keys
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 key = layout.cell_key(0, value_col).unwrap();
assert_eq!(key.get("_Index"), Some("0"));
assert_eq!(key.get("_Dim"), Some("Value"));
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"
);
let key = layout.cell_key(0, region_col).unwrap();
assert_eq!(key.get("_Index"), Some("0"));
assert_eq!(key.get("_Dim"), Some("Region"));
}
#[test]
fn records_mode_cell_key_maps_to_real_cell() {
fn records_mode_resolve_display_returns_values() {
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();
// Value column resolves to the cell value
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");
let display = layout.resolve_display(&key);
assert!(display.is_some(), "Value column should resolve");
// Category column resolves to the coordinate value
let region_col = cols.iter().position(|c| c == "Region").unwrap();
let key = layout.cell_key(0, region_col).unwrap();
let display = layout.resolve_display(&key).unwrap();
assert!(!display.is_empty(), "Region column should resolve to a value");
}
#[test]
fn synthetic_record_info_returns_none_for_pivot_keys() {
let key = CellKey::new(vec![
("Region".to_string(), "East".to_string()),
("Product".to_string(), "Shoes".to_string()),
]);
assert!(synthetic_record_info(&key).is_none());
}
#[test]
fn synthetic_record_info_extracts_index_and_dim() {
let key = CellKey::new(vec![
("_Index".to_string(), "3".to_string()),
("_Dim".to_string(), "Region".to_string()),
]);
let (idx, dim) = synthetic_record_info(&key).unwrap();
assert_eq!(idx, 3);
assert_eq!(dim, "Region");
}
fn coord(pairs: &[(&str, &str)]) -> CellKey {