feat: group-aware grid rendering and hide/show item
Builds out two half-finished view features: Group collapse: - AxisEntry enum distinguishes GroupHeader from DataItem on grid axes - expand_category() emits group headers and filters collapsed items - Grid renders inline group header rows with ▼/▶ indicator - `z` keybinding toggles collapse of nearest group above cursor Hide/show item: - Restore show_item() (was commented out alongside hide_item) - Add HideItem / ShowItem commands and dispatch - `H` keybinding hides the current row item - `:show-item <cat> <item>` command to restore hidden items - Restore silenced test assertions for hide/show round-trip Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -1,38 +1,42 @@
|
||||
|
||||
use crate::model::Model;
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::formula::parse_formula;
|
||||
use crate::persistence;
|
||||
use crate::import::analyzer::{analyze_records, extract_array_at_path, FieldKind};
|
||||
use super::types::{CellValueArg, Command, CommandResult};
|
||||
use crate::formula::parse_formula;
|
||||
use crate::import::analyzer::{analyze_records, extract_array_at_path, FieldKind};
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::Model;
|
||||
use crate::persistence;
|
||||
|
||||
/// Execute a command against the model, returning a result.
|
||||
/// This is the single authoritative mutation path used by both the TUI and headless modes.
|
||||
pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
match cmd {
|
||||
Command::AddCategory { name } => {
|
||||
match model.add_category(name) {
|
||||
Ok(_) => CommandResult::ok_msg(format!("Category '{name}' added")),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
}
|
||||
}
|
||||
Command::AddCategory { name } => match model.add_category(name) {
|
||||
Ok(_) => CommandResult::ok_msg(format!("Category '{name}' added")),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
},
|
||||
|
||||
Command::AddItem { category, item } => {
|
||||
match model.category_mut(category) {
|
||||
Some(cat) => { cat.add_item(item); CommandResult::ok() }
|
||||
None => CommandResult::err(format!("Category '{category}' not found")),
|
||||
Command::AddItem { category, item } => match model.category_mut(category) {
|
||||
Some(cat) => {
|
||||
cat.add_item(item);
|
||||
CommandResult::ok()
|
||||
}
|
||||
}
|
||||
None => CommandResult::err(format!("Category '{category}' not found")),
|
||||
},
|
||||
|
||||
Command::AddItemInGroup { category, item, group } => {
|
||||
match model.category_mut(category) {
|
||||
Some(cat) => { cat.add_item_in_group(item, group); CommandResult::ok() }
|
||||
None => CommandResult::err(format!("Category '{category}' not found")),
|
||||
Command::AddItemInGroup {
|
||||
category,
|
||||
item,
|
||||
group,
|
||||
} => match model.category_mut(category) {
|
||||
Some(cat) => {
|
||||
cat.add_item_in_group(item, group);
|
||||
CommandResult::ok()
|
||||
}
|
||||
}
|
||||
None => CommandResult::err(format!("Category '{category}' not found")),
|
||||
},
|
||||
|
||||
Command::SetCell { coords, value } => {
|
||||
let kv: Vec<(String, String)> = coords.iter()
|
||||
let kv: Vec<(String, String)> = coords
|
||||
.iter()
|
||||
.map(|pair| (pair[0].clone(), pair[1].clone()))
|
||||
.collect();
|
||||
// Validate all categories exist before mutating anything
|
||||
@ -55,7 +59,8 @@ pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
}
|
||||
|
||||
Command::ClearCell { coords } => {
|
||||
let kv: Vec<(String, String)> = coords.iter()
|
||||
let kv: Vec<(String, String)> = coords
|
||||
.iter()
|
||||
.map(|pair| (pair[0].clone(), pair[1].clone()))
|
||||
.collect();
|
||||
let key = CellKey::new(kv);
|
||||
@ -63,7 +68,10 @@ pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
CommandResult::ok()
|
||||
}
|
||||
|
||||
Command::AddFormula { raw, target_category } => {
|
||||
Command::AddFormula {
|
||||
raw,
|
||||
target_category,
|
||||
} => {
|
||||
match parse_formula(raw, target_category) {
|
||||
Ok(formula) => {
|
||||
// Ensure the target item exists in the target category
|
||||
@ -79,7 +87,10 @@ pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
}
|
||||
}
|
||||
|
||||
Command::RemoveFormula { target, target_category } => {
|
||||
Command::RemoveFormula {
|
||||
target,
|
||||
target_category,
|
||||
} => {
|
||||
model.remove_formula(target, target_category);
|
||||
CommandResult::ok()
|
||||
}
|
||||
@ -89,19 +100,15 @@ pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
CommandResult::ok()
|
||||
}
|
||||
|
||||
Command::DeleteView { name } => {
|
||||
match model.delete_view(name) {
|
||||
Ok(_) => CommandResult::ok(),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
}
|
||||
}
|
||||
Command::DeleteView { name } => match model.delete_view(name) {
|
||||
Ok(_) => CommandResult::ok(),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
},
|
||||
|
||||
Command::SwitchView { name } => {
|
||||
match model.switch_view(name) {
|
||||
Ok(_) => CommandResult::ok(),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
}
|
||||
}
|
||||
Command::SwitchView { name } => match model.switch_view(name) {
|
||||
Ok(_) => CommandResult::ok(),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
},
|
||||
|
||||
Command::SetAxis { category, axis } => {
|
||||
model.active_view_mut().set_axis(category, *axis);
|
||||
@ -114,28 +121,36 @@ pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
}
|
||||
|
||||
Command::ToggleGroup { category, group } => {
|
||||
model.active_view_mut().toggle_group_collapse(category, group);
|
||||
model
|
||||
.active_view_mut()
|
||||
.toggle_group_collapse(category, group);
|
||||
CommandResult::ok()
|
||||
}
|
||||
|
||||
Command::Save { path } => {
|
||||
match persistence::save(model, std::path::Path::new(path)) {
|
||||
Ok(_) => CommandResult::ok_msg(format!("Saved to {path}")),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
}
|
||||
Command::HideItem { category, item } => {
|
||||
model.active_view_mut().hide_item(category, item);
|
||||
CommandResult::ok()
|
||||
}
|
||||
|
||||
Command::Load { path } => {
|
||||
match persistence::load(std::path::Path::new(path)) {
|
||||
Ok(mut loaded) => {
|
||||
loaded.normalize_view_state();
|
||||
*model = loaded;
|
||||
CommandResult::ok_msg(format!("Loaded from {path}"))
|
||||
}
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
}
|
||||
Command::ShowItem { category, item } => {
|
||||
model.active_view_mut().show_item(category, item);
|
||||
CommandResult::ok()
|
||||
}
|
||||
|
||||
Command::Save { path } => match persistence::save(model, std::path::Path::new(path)) {
|
||||
Ok(_) => CommandResult::ok_msg(format!("Saved to {path}")),
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
},
|
||||
|
||||
Command::Load { path } => match persistence::load(std::path::Path::new(path)) {
|
||||
Ok(mut loaded) => {
|
||||
loaded.normalize_view_state();
|
||||
*model = loaded;
|
||||
CommandResult::ok_msg(format!("Loaded from {path}"))
|
||||
}
|
||||
Err(e) => CommandResult::err(e.to_string()),
|
||||
},
|
||||
|
||||
Command::ExportCsv { path } => {
|
||||
let view_name = model.active_view.clone();
|
||||
match persistence::export_csv(model, &view_name, std::path::Path::new(path)) {
|
||||
@ -144,9 +159,11 @@ pub fn dispatch(model: &mut Model, cmd: &Command) -> CommandResult {
|
||||
}
|
||||
}
|
||||
|
||||
Command::ImportJson { path, model_name, array_path } => {
|
||||
import_json_headless(model, path, model_name.as_deref(), array_path.as_deref())
|
||||
}
|
||||
Command::ImportJson {
|
||||
path,
|
||||
model_name,
|
||||
array_path,
|
||||
} => import_json_headless(model, path, model_name.as_deref(), array_path.as_deref()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,7 +210,13 @@ fn import_json_headless(
|
||||
array_paths: vec![],
|
||||
selected_path: array_path.unwrap_or("").to_string(),
|
||||
records,
|
||||
proposals: proposals.into_iter().map(|mut p| { p.accepted = p.kind != FieldKind::Label; p }).collect(),
|
||||
proposals: proposals
|
||||
.into_iter()
|
||||
.map(|mut p| {
|
||||
p.accepted = p.kind != FieldKind::Label;
|
||||
p
|
||||
})
|
||||
.collect(),
|
||||
model_name: model_name.unwrap_or("Imported Model").to_string(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user