Merge branch 'main' into worktree-improvise-ewi-formula-crate
# Conflicts: # src/model/types.rs # src/view/layout.rs
This commit is contained in:
162
src/ui/app.rs
162
src/ui/app.rs
@ -280,6 +280,7 @@ impl App {
|
||||
tile_cat_idx: self.tile_cat_idx,
|
||||
view_back_stack: &self.view_back_stack,
|
||||
view_forward_stack: &self.view_forward_stack,
|
||||
has_drill_state: self.drill_state.is_some(),
|
||||
display_value: {
|
||||
let key = layout.cell_key(sel_row, sel_col);
|
||||
if let Some(k) = &key {
|
||||
@ -708,6 +709,167 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression: pressing `o` in an empty records view should create the
|
||||
/// first synthetic row instead of only entering edit mode on empty space.
|
||||
#[test]
|
||||
fn add_record_row_in_empty_records_view_creates_first_row() {
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Region").unwrap();
|
||||
wb.model.category_mut("Region").unwrap().add_item("East");
|
||||
let mut app = App::new(wb, None);
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(app.layout.is_records_mode(), "R should enter records mode");
|
||||
assert_eq!(app.layout.row_count(), 0, "fresh records view starts empty");
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
app.layout.row_count(),
|
||||
1,
|
||||
"o should create the first record row in an empty records view"
|
||||
);
|
||||
assert!(
|
||||
matches!(app.mode, AppMode::Editing { .. }),
|
||||
"o should leave the app in edit mode, got {:?}",
|
||||
app.mode
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression: editing the first row in a blank model's records view
|
||||
/// should persist the typed value even though plain records mode does not
|
||||
/// use drill state. With _Measure as the first column, `o` lands on it;
|
||||
/// type a measure name, Tab to Value, type the number, Enter to commit.
|
||||
#[test]
|
||||
fn edit_record_row_in_blank_model_persists_value() {
|
||||
use crate::model::cell::CellKey;
|
||||
|
||||
let mut app = App::new(Workbook::new("T"), None);
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
// `o` adds a record row and enters edit at (0, 0) = _Measure column
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
// Type a measure name
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
// Tab to commit _Measure and move to Value column
|
||||
app.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
// Type the value
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
// Enter to commit
|
||||
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
app.workbook.model.get_cell(&CellKey::new(vec![
|
||||
("_Measure".to_string(), "Rev".to_string()),
|
||||
])),
|
||||
Some(&crate::model::cell::CellValue::Number(5.0)),
|
||||
"editing a synthetic row in plain records mode should write the value"
|
||||
);
|
||||
}
|
||||
|
||||
/// Drill-view edits should stay staged in drill state until the user
|
||||
/// navigates back, at which point ApplyAndClearDrill writes them through.
|
||||
#[test]
|
||||
fn drill_edit_is_staged_until_view_back() {
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Region").unwrap();
|
||||
wb.add_category("Month").unwrap();
|
||||
wb.model.category_mut("Region").unwrap().add_item("East");
|
||||
wb.model.category_mut("Month").unwrap().add_item("Jan");
|
||||
let record_key = CellKey::new(vec![
|
||||
("Month".to_string(), "Jan".to_string()),
|
||||
("Region".to_string(), "East".to_string()),
|
||||
]);
|
||||
wb.model.set_cell(record_key.clone(), CellValue::Number(1.0));
|
||||
let mut app = App::new(wb, None);
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('>'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
assert!(app.drill_state.is_some(), "drill should create drill state");
|
||||
let value_col = (0..app.layout.col_count())
|
||||
.find(|&col| app.layout.col_label(col) == "Value")
|
||||
.expect("drill view should include a Value column");
|
||||
app.workbook.active_view_mut().selected = (0, value_col);
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('9'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
app.workbook.model.get_cell(&record_key),
|
||||
Some(&CellValue::Number(1.0)),
|
||||
"drill edit should remain staged until leaving the drill view"
|
||||
);
|
||||
assert_eq!(
|
||||
app.drill_state
|
||||
.as_ref()
|
||||
.and_then(|s| s.pending_edits.get(&(0, "Value".to_string()))),
|
||||
Some(&"9".to_string()),
|
||||
"drill edit should be recorded in pending_edits"
|
||||
);
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
app.workbook.model.get_cell(&record_key),
|
||||
Some(&CellValue::Number(9.0)),
|
||||
"leaving drill view should apply the staged edit"
|
||||
);
|
||||
}
|
||||
|
||||
/// Suspected bug: blanking a records-mode category coordinate should not
|
||||
/// create an item with an empty name.
|
||||
#[test]
|
||||
fn blanking_records_category_does_not_create_empty_item() {
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
|
||||
let mut wb = Workbook::new("T");
|
||||
wb.add_category("Region").unwrap();
|
||||
wb.model.category_mut("Region").unwrap().add_item("East");
|
||||
wb.model.set_cell(
|
||||
CellKey::new(vec![("Region".to_string(), "East".to_string())]),
|
||||
CellValue::Number(1.0),
|
||||
);
|
||||
let mut app = App::new(wb, None);
|
||||
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('R'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
app.handle_key(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
for _ in 0..4 {
|
||||
app.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
}
|
||||
app.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!app.workbook.model.category("Region").unwrap().items.contains_key(""),
|
||||
"records-mode edits should not create empty category items"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_mode_buffer_cleared_on_reentry() {
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
@ -6,6 +6,8 @@ use crate::view::Axis;
|
||||
|
||||
use super::app::{App, AppMode};
|
||||
|
||||
pub(crate) const RECORD_COORDS_CANNOT_BE_EMPTY: &str = "Record coordinates cannot be empty";
|
||||
|
||||
/// A discrete state change produced by a command.
|
||||
/// Effects know how to apply themselves to the App.
|
||||
pub trait Effect: Debug {
|
||||
@ -459,6 +461,10 @@ impl Effect for ApplyAndClearDrill {
|
||||
};
|
||||
app.workbook.model.set_cell(orig_key.clone(), value);
|
||||
} else {
|
||||
if new_value.is_empty() {
|
||||
app.status_msg = RECORD_COORDS_CANNOT_BE_EMPTY.to_string();
|
||||
continue;
|
||||
}
|
||||
// Rename a coordinate: remove old cell, insert new with updated coord
|
||||
let value = match app.workbook.model.get_cell(orig_key) {
|
||||
Some(v) => v.clone(),
|
||||
|
||||
Reference in New Issue
Block a user