6a929c1d21
improvise-2zq step 2, on the user's ruling. Step 1 made this coherent by stopping `on_category_added` from ejecting the user; this adds the column. Typing a name into `+cat` creates that category, and because `GridLayout::records_columns` filters on `kind.is_regular()` and never looks at items or at an axis, the coordinate column appears immediately and is typeable on the next keystroke. **The two questions the ruling left open, and the answers.** *Where the column goes.* Not a second trailing column, and not one `+` disambiguated by a prefix. A prefix would put a sigil into the name space — and a category and a measure may legitimately share a name, which is the exact ambiguity `_Dim` already carries a column *index* rather than a label to avoid (gotcha 1). A key was rejected too: the name still has to be typed somewhere, and a minibuffer lands in the records-exit class bug this PR characterizes but does not fix. So each `+` sits adjacent to what it adds to — `[coords…, +cat, measures…, +]`. Position tells the two apart instead of a label, which is the clutter the ticket was worried about. It costs a mid-grid column index, and the consequence worth naming is that a **blank** records grid now opens with the cursor on `+cat` rather than `+`: a record needs coordinates before it needs measures, so that order is defensible, but it does change what the first keystroke in an empty grid does. *Whether committing also sets the coordinate on the current row.* It does not. It is symmetric with the measure `+`, which creates an empty column and writes no value; and setting it would move **every** measure cell in the row onto the new coordinate (gotcha 1) — a data migration across N cells attached to the same Enter that made a schema change, in an app with no undo. The follow-up edit is one column away. **The refusal is visible, which took a change of order.** `MAX_CATEGORIES` is reachable here rather than theoretical. `AddCategory` now aborts the batch on refusal — same reason a failed save does (gotcha 21): what follows assumes the category exists, and the trailing `EnterEditAtCursor` would re-open the minibuffer *over* the message, since `draw_bottom_bar` renders the minibuffer instead of the status line. And `CommitAndAdvance` now emits its mode change **before** the commit rather than after, so an abort cannot eat it. That is the third time this PR has applied one rule — tidy-up goes where an abort cannot reach it — and the comment at that site had already described the pattern for the *advance* failing; the commit failing was simply not considered. The limit itself is still checked in exactly one place, `Model::add_category`. Evidence. Three new tests drive real keys: `the_new_category_column_creates_a_category_and_its_column` (the column is usable immediately), `..._writes_no_coordinate` (the deliberate answer to question 2, asserted over every row), and `..._refuses_past_the_limit_where_the_user_can_see_it`, which asserts the mode has **no minibuffer** — proved discriminating by swapping the commit back in front of the mode change and watching it report "Maximum of 12 categories reached" from inside `RecordsEditing`, where nothing would have drawn it. Four existing characterization tests were updated knowingly, not incidentally: the shared records fixture's column list (which accounted for 8 of the 9 initial failures on its own), the core layout and persistence smoke expectations, and the blank-model bootstrap, which now has to step to the `+` column and so records the cursor-position consequence above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
68 lines
2.4 KiB
Rust
68 lines
2.4 KiB
Rust
//! End-to-end check that a real imported model renders records mode in wide
|
|
//! format: CSV → import → save → load → records grid (improvise-c5v).
|
|
|
|
use improvise::import::wizard::ImportPipeline;
|
|
use improvise::persistence;
|
|
use improvise::view::{Axis, GridLayout};
|
|
|
|
/// Import a two-record CSV, round-trip it through `.improv`, and read the
|
|
/// records grid off the loaded model.
|
|
#[test]
|
|
fn imported_model_shows_one_row_per_record_with_measure_columns() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
let csv = dir.path().join("w.csv");
|
|
std::fs::write(
|
|
&csv,
|
|
"region,product,revenue,cost\nEast,Shirts,100,40\nWest,Pants,200,80\n",
|
|
)
|
|
.expect("write csv");
|
|
|
|
let records = improvise::import::csv_parser::parse_csv(&csv).expect("csv parses");
|
|
let pipeline = ImportPipeline::new(serde_json::Value::Array(records));
|
|
let workbook = pipeline.build_model().expect("model builds");
|
|
|
|
let saved = dir.path().join("w.improv");
|
|
persistence::save(&workbook, &saved).expect("saves");
|
|
let mut wb = persistence::load(&saved).expect("loads");
|
|
|
|
let v = wb.active_view_mut();
|
|
v.set_axis("_Index", Axis::Row);
|
|
v.set_axis("_Dim", Axis::Column);
|
|
for cat in ["region", "product", "_Measure"] {
|
|
v.set_axis(cat, Axis::None);
|
|
}
|
|
let layout = GridLayout::new(&wb.model, wb.active_view());
|
|
assert!(layout.is_records_mode());
|
|
|
|
let cols: Vec<String> = (0..layout.col_count())
|
|
.map(|i| layout.col_label(i))
|
|
.collect();
|
|
assert_eq!(
|
|
cols,
|
|
vec!["product", "region", "+cat", "cost", "revenue", "+"],
|
|
"categories, the new-category column, one column per measure, then \
|
|
the new-measure column — each `+` sits next to what it adds to \
|
|
(improvise-2zq)"
|
|
);
|
|
assert_eq!(layout.row_count(), 2, "two source records, not four cells");
|
|
|
|
// Row order follows the store; the contract under test is that each
|
|
// source record is one row carrying both its measures side by side.
|
|
let mut rows: Vec<Vec<String>> = (0..layout.row_count())
|
|
.map(|r| {
|
|
(0..layout.col_count())
|
|
.map(|c| layout.records_display(r, c).unwrap())
|
|
.collect()
|
|
})
|
|
.collect();
|
|
rows.sort();
|
|
assert_eq!(
|
|
rows,
|
|
vec![
|
|
vec!["Pants", "West", "", "80", "200", ""],
|
|
vec!["Shirts", "East", "", "40", "100", ""],
|
|
],
|
|
"got {rows:?}"
|
|
);
|
|
}
|