feat: add new commands for records mode and category management
Add new commands for enhanced data entry and category management. AddRecordRow: Adds a new record row in records mode with empty value. TogglePruneEmpty: Toggles pruning of empty rows/columns in pivot mode. ToggleRecordsMode: Switches between records and pivot layout. DeleteCategoryAtCursor: Removes a category and all its cells. ToggleCatExpand: Expands/collapses a category in the tree. FilterToItem: Filters to show only items matching cursor position. Model gains remove_category() and remove_item() to delete categories and items along with all referencing cells and formulas. Co-Authored-By: fiddlerwoaroof/git-smart-commit (unsloth/Qwen3.5-35B-A3B-GGUF:Q5_K_M)
This commit is contained in:
@ -696,6 +696,45 @@ impl Cmd for EditOrDrill {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// In records mode, add a new row with an empty value. The new cell gets
|
||||||
|
/// coords from the current page filters. In pivot mode, this is a no-op.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AddRecordRow;
|
||||||
|
impl Cmd for AddRecordRow {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"add-record-row"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
if ctx.records_col.is_none() {
|
||||||
|
return vec![effect::set_status("add-record-row only works in records mode")];
|
||||||
|
}
|
||||||
|
// Build a CellKey from the current page filters
|
||||||
|
let view = ctx.model.active_view();
|
||||||
|
let page_cats: Vec<String> = view
|
||||||
|
.categories_on(crate::view::Axis::Page)
|
||||||
|
.into_iter()
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
let coords: Vec<(String, String)> = page_cats
|
||||||
|
.iter()
|
||||||
|
.map(|cat| {
|
||||||
|
let sel = view
|
||||||
|
.page_selection(cat)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
(cat.clone(), sel)
|
||||||
|
})
|
||||||
|
.filter(|(_, v)| !v.is_empty())
|
||||||
|
.collect();
|
||||||
|
let key = crate::model::cell::CellKey::new(coords);
|
||||||
|
vec![
|
||||||
|
Box::new(effect::SetCell(key, CellValue::Number(0.0))),
|
||||||
|
effect::mark_dirty(),
|
||||||
|
effect::set_status("Added new record row"),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Typewriter-style advance: move down, wrap to top of next column at bottom.
|
/// Typewriter-style advance: move down, wrap to top of next column at bottom.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct EnterAdvance {
|
pub struct EnterAdvance {
|
||||||
@ -1301,6 +1340,187 @@ impl Cmd for OpenItemAddAtCursor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Toggle expand/collapse of the category at the tree cursor.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ToggleCatExpand;
|
||||||
|
impl Cmd for ToggleCatExpand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"toggle-cat-expand"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
if let Some(cat_name) = ctx.cat_at_cursor() {
|
||||||
|
vec![Box::new(effect::ToggleCatExpand(cat_name))]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter to item: when on an item row, set the category to Page with the
|
||||||
|
/// item as the filter value.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FilterToItem;
|
||||||
|
impl Cmd for FilterToItem {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"filter-to-item"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
use crate::ui::cat_tree::CatTreeEntry;
|
||||||
|
match ctx.cat_tree_entry() {
|
||||||
|
Some(CatTreeEntry::Item {
|
||||||
|
cat_name,
|
||||||
|
item_name,
|
||||||
|
}) => {
|
||||||
|
vec![
|
||||||
|
Box::new(effect::SetAxis {
|
||||||
|
category: cat_name.clone(),
|
||||||
|
axis: crate::view::Axis::Page,
|
||||||
|
}),
|
||||||
|
Box::new(effect::SetPageSelection {
|
||||||
|
category: cat_name.clone(),
|
||||||
|
item: item_name.clone(),
|
||||||
|
}),
|
||||||
|
effect::set_status(format!("Filter: {cat_name} = {item_name}")),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Some(CatTreeEntry::Category { .. }) => {
|
||||||
|
// On a category header — toggle expand instead
|
||||||
|
ToggleCatExpand.execute(ctx)
|
||||||
|
}
|
||||||
|
None => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle pruning of empty rows/columns in the current view.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TogglePruneEmpty;
|
||||||
|
impl Cmd for TogglePruneEmpty {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"toggle-prune-empty"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
let currently_on = ctx.model.active_view().prune_empty;
|
||||||
|
vec![
|
||||||
|
Box::new(effect::TogglePruneEmpty),
|
||||||
|
effect::set_status(if currently_on {
|
||||||
|
"Showing all rows/columns"
|
||||||
|
} else {
|
||||||
|
"Hiding empty rows/columns"
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle between records mode (_Index on Row, _Dim on Column) and
|
||||||
|
/// pivot mode (auto-assigned axes). In records mode every cell is shown
|
||||||
|
/// as a flat row; in pivot mode the view is a cross-tab.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ToggleRecordsMode;
|
||||||
|
impl Cmd for ToggleRecordsMode {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"toggle-records-mode"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
use crate::view::Axis;
|
||||||
|
let view = ctx.model.active_view();
|
||||||
|
|
||||||
|
// Detect current state
|
||||||
|
let is_records = view
|
||||||
|
.category_axes
|
||||||
|
.get("_Index")
|
||||||
|
.copied()
|
||||||
|
== Some(Axis::Row)
|
||||||
|
&& view.category_axes.get("_Dim").copied() == Some(Axis::Column);
|
||||||
|
|
||||||
|
let mut effects: Vec<Box<dyn Effect>> = Vec::new();
|
||||||
|
|
||||||
|
if is_records {
|
||||||
|
// Switch back to pivot: auto-assign axes
|
||||||
|
// First regular category → Row, second → Column, rest → Page,
|
||||||
|
// virtuals/labels → None.
|
||||||
|
let mut row_done = false;
|
||||||
|
let mut col_done = false;
|
||||||
|
for (name, cat) in &ctx.model.categories {
|
||||||
|
let axis = if !cat.kind.is_regular() {
|
||||||
|
Axis::None
|
||||||
|
} else if !row_done {
|
||||||
|
row_done = true;
|
||||||
|
Axis::Row
|
||||||
|
} else if !col_done {
|
||||||
|
col_done = true;
|
||||||
|
Axis::Column
|
||||||
|
} else {
|
||||||
|
Axis::Page
|
||||||
|
};
|
||||||
|
effects.push(Box::new(effect::SetAxis {
|
||||||
|
category: name.clone(),
|
||||||
|
axis,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
effects.push(effect::set_status("Pivot mode"));
|
||||||
|
} else {
|
||||||
|
// Switch to records mode
|
||||||
|
effects.push(Box::new(effect::SetAxis {
|
||||||
|
category: "_Index".to_string(),
|
||||||
|
axis: Axis::Row,
|
||||||
|
}));
|
||||||
|
effects.push(Box::new(effect::SetAxis {
|
||||||
|
category: "_Dim".to_string(),
|
||||||
|
axis: Axis::Column,
|
||||||
|
}));
|
||||||
|
// Everything else → None
|
||||||
|
for name in ctx.model.categories.keys() {
|
||||||
|
if name != "_Index" && name != "_Dim" {
|
||||||
|
effects.push(Box::new(effect::SetAxis {
|
||||||
|
category: name.clone(),
|
||||||
|
axis: Axis::None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
effects.push(effect::set_status("Records mode"));
|
||||||
|
}
|
||||||
|
effects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete the category or item at the panel cursor.
|
||||||
|
/// On a category header → delete the whole category.
|
||||||
|
/// On an item row → delete just that item.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DeleteCategoryAtCursor;
|
||||||
|
impl Cmd for DeleteCategoryAtCursor {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"delete-category-at-cursor"
|
||||||
|
}
|
||||||
|
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||||
|
use crate::ui::cat_tree::CatTreeEntry;
|
||||||
|
match ctx.cat_tree_entry() {
|
||||||
|
Some(CatTreeEntry::Category { name, .. }) => {
|
||||||
|
vec![
|
||||||
|
Box::new(effect::RemoveCategory(name.clone())),
|
||||||
|
effect::mark_dirty(),
|
||||||
|
effect::set_status(format!("Deleted category '{name}'")),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Some(CatTreeEntry::Item {
|
||||||
|
cat_name,
|
||||||
|
item_name,
|
||||||
|
}) => {
|
||||||
|
vec![
|
||||||
|
Box::new(effect::RemoveItem {
|
||||||
|
category: cat_name.clone(),
|
||||||
|
item: item_name.clone(),
|
||||||
|
}),
|
||||||
|
effect::mark_dirty(),
|
||||||
|
effect::set_status(format!("Deleted item '{item_name}' from '{cat_name}'")),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
None => vec![effect::set_status("No category to delete")],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── View panel commands ─────────────────────────────────────────────────────
|
// ── View panel commands ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Switch to the view at the panel cursor and return to Normal mode.
|
/// Switch to the view at the panel cursor and return to Normal mode.
|
||||||
@ -2468,26 +2688,42 @@ pub fn default_registry() -> CmdRegistry {
|
|||||||
|
|
||||||
// ── Panel operations ─────────────────────────────────────────────────
|
// ── Panel operations ─────────────────────────────────────────────────
|
||||||
r.register(
|
r.register(
|
||||||
&TogglePanelAndFocus { panel: Panel::Formula, currently_open: false },
|
&TogglePanelAndFocus { panel: Panel::Formula, open: true, focused: true },
|
||||||
|args| {
|
|args| {
|
||||||
|
// Parse: toggle-panel-and-focus <panel> [open] [focused]
|
||||||
require_args("toggle-panel-and-focus", args, 1)?;
|
require_args("toggle-panel-and-focus", args, 1)?;
|
||||||
let panel = parse_panel(&args[0])?;
|
let panel = parse_panel(&args[0])?;
|
||||||
|
let open = args.get(1).map(|s| s == "true").unwrap_or(true);
|
||||||
|
let focused = args.get(2).map(|s| s == "true").unwrap_or(open);
|
||||||
Ok(Box::new(TogglePanelAndFocus {
|
Ok(Box::new(TogglePanelAndFocus {
|
||||||
panel,
|
panel,
|
||||||
currently_open: false,
|
open,
|
||||||
|
focused,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|args, ctx| {
|
|args, ctx| {
|
||||||
require_args("toggle-panel-and-focus", args, 1)?;
|
require_args("toggle-panel-and-focus", args, 1)?;
|
||||||
let panel = parse_panel(&args[0])?;
|
let panel = parse_panel(&args[0])?;
|
||||||
|
// Default interactive: if already open+focused → close, else open+focus
|
||||||
let currently_open = match panel {
|
let currently_open = match panel {
|
||||||
Panel::Formula => ctx.formula_panel_open,
|
Panel::Formula => ctx.formula_panel_open,
|
||||||
Panel::Category => ctx.category_panel_open,
|
Panel::Category => ctx.category_panel_open,
|
||||||
Panel::View => ctx.view_panel_open,
|
Panel::View => ctx.view_panel_open,
|
||||||
};
|
};
|
||||||
|
let currently_focused = match panel {
|
||||||
|
Panel::Formula => matches!(ctx.mode, AppMode::FormulaPanel | AppMode::FormulaEdit { .. }),
|
||||||
|
Panel::Category => matches!(ctx.mode, AppMode::CategoryPanel | AppMode::CategoryAdd { .. } | AppMode::ItemAdd { .. }),
|
||||||
|
Panel::View => matches!(ctx.mode, AppMode::ViewPanel),
|
||||||
|
};
|
||||||
|
let (open, focused) = if currently_open && currently_focused {
|
||||||
|
(false, false) // close
|
||||||
|
} else {
|
||||||
|
(true, true) // open + focus
|
||||||
|
};
|
||||||
Ok(Box::new(TogglePanelAndFocus {
|
Ok(Box::new(TogglePanelAndFocus {
|
||||||
panel,
|
panel,
|
||||||
currently_open,
|
open,
|
||||||
|
focused,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -2565,8 +2801,14 @@ pub fn default_registry() -> CmdRegistry {
|
|||||||
r.register_nullary(|| {
|
r.register_nullary(|| {
|
||||||
Box::new(DeleteFormulaAtCursor)
|
Box::new(DeleteFormulaAtCursor)
|
||||||
});
|
});
|
||||||
|
r.register_nullary(|| Box::new(AddRecordRow));
|
||||||
|
r.register_nullary(|| Box::new(TogglePruneEmpty));
|
||||||
|
r.register_nullary(|| Box::new(ToggleRecordsMode));
|
||||||
r.register_nullary(|| Box::new(CycleAxisAtCursor));
|
r.register_nullary(|| Box::new(CycleAxisAtCursor));
|
||||||
r.register_nullary(|| Box::new(OpenItemAddAtCursor));
|
r.register_nullary(|| Box::new(OpenItemAddAtCursor));
|
||||||
|
r.register_nullary(|| Box::new(DeleteCategoryAtCursor));
|
||||||
|
r.register_nullary(|| Box::new(ToggleCatExpand));
|
||||||
|
r.register_nullary(|| Box::new(FilterToItem));
|
||||||
r.register_nullary(|| Box::new(SwitchViewAtCursor));
|
r.register_nullary(|| Box::new(SwitchViewAtCursor));
|
||||||
r.register_nullary(|| Box::new(CreateAndSwitchView));
|
r.register_nullary(|| Box::new(CreateAndSwitchView));
|
||||||
r.register_nullary(|| Box::new(DeleteViewAtCursor));
|
r.register_nullary(|| Box::new(DeleteViewAtCursor));
|
||||||
|
|||||||
@ -354,9 +354,14 @@ impl KeymapSet {
|
|||||||
normal.bind(KeyCode::Char('z'), none, "toggle-group-under-cursor");
|
normal.bind(KeyCode::Char('z'), none, "toggle-group-under-cursor");
|
||||||
normal.bind(KeyCode::Char('H'), none, "hide-selected-row-item");
|
normal.bind(KeyCode::Char('H'), none, "hide-selected-row-item");
|
||||||
|
|
||||||
// Drill into aggregated cell / view history
|
// Drill into aggregated cell / view history / add row
|
||||||
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
|
normal.bind(KeyCode::Char('>'), none, "drill-into-cell");
|
||||||
normal.bind(KeyCode::Char('<'), none, "view-back");
|
normal.bind(KeyCode::Char('<'), none, "view-back");
|
||||||
|
normal.bind(KeyCode::Char('o'), none, "add-record-row");
|
||||||
|
|
||||||
|
// Records mode toggle and prune toggle
|
||||||
|
normal.bind(KeyCode::Char('R'), none, "toggle-records-mode");
|
||||||
|
normal.bind(KeyCode::Char('P'), none, "toggle-prune-empty");
|
||||||
|
|
||||||
// Tile select
|
// Tile select
|
||||||
normal.bind(KeyCode::Char('T'), none, "enter-tile-select");
|
normal.bind(KeyCode::Char('T'), none, "enter-tile-select");
|
||||||
|
|||||||
@ -117,6 +117,10 @@ impl Category {
|
|||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_item(&mut self, name: &str) {
|
||||||
|
self.items.shift_remove(name);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_item_in_group(
|
pub fn add_item_in_group(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
|
|||||||
@ -107,6 +107,47 @@ impl Model {
|
|||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove a category and all cells that reference it.
|
||||||
|
pub fn remove_category(&mut self, name: &str) {
|
||||||
|
if !self.categories.contains_key(name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.categories.shift_remove(name);
|
||||||
|
// Remove from all views
|
||||||
|
for view in self.views.values_mut() {
|
||||||
|
view.on_category_removed(name);
|
||||||
|
}
|
||||||
|
// Remove cells that have a coord in this category
|
||||||
|
let to_remove: Vec<CellKey> = self
|
||||||
|
.data
|
||||||
|
.iter_cells()
|
||||||
|
.filter(|(k, _)| k.get(name).is_some())
|
||||||
|
.map(|(k, _)| k)
|
||||||
|
.collect();
|
||||||
|
for k in to_remove {
|
||||||
|
self.data.remove(&k);
|
||||||
|
}
|
||||||
|
// Remove formulas targeting this category
|
||||||
|
self.formulas
|
||||||
|
.retain(|f| f.target_category != name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an item from a category and all cells that reference it.
|
||||||
|
pub fn remove_item(&mut self, cat_name: &str, item_name: &str) {
|
||||||
|
if let Some(cat) = self.categories.get_mut(cat_name) {
|
||||||
|
cat.remove_item(item_name);
|
||||||
|
}
|
||||||
|
let to_remove: Vec<CellKey> = self
|
||||||
|
.data
|
||||||
|
.iter_cells()
|
||||||
|
.filter(|(k, _)| k.get(cat_name) == Some(item_name))
|
||||||
|
.map(|(k, _)| k)
|
||||||
|
.collect();
|
||||||
|
for k in to_remove {
|
||||||
|
self.data.remove(&k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn category_mut(&mut self, name: &str) -> Option<&mut Category> {
|
pub fn category_mut(&mut self, name: &str) -> Option<&mut Category> {
|
||||||
self.categories.get_mut(name)
|
self.categories.get_mut(name)
|
||||||
}
|
}
|
||||||
@ -527,6 +568,31 @@ mod model_tests {
|
|||||||
assert_eq!(m.get_cell(&k4), Some(&CellValue::Number(40.0)));
|
assert_eq!(m.get_cell(&k4), Some(&CellValue::Number(40.0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_category_deletes_category_and_cells() {
|
||||||
|
let mut m = Model::new("Test");
|
||||||
|
m.add_category("Region").unwrap();
|
||||||
|
m.add_category("Product").unwrap();
|
||||||
|
m.category_mut("Region").unwrap().add_item("East");
|
||||||
|
m.category_mut("Product").unwrap().add_item("Shirts");
|
||||||
|
m.set_cell(
|
||||||
|
coord(&[("Region", "East"), ("Product", "Shirts")]),
|
||||||
|
CellValue::Number(42.0),
|
||||||
|
);
|
||||||
|
m.remove_category("Region");
|
||||||
|
assert!(m.category("Region").is_none());
|
||||||
|
// Cells referencing Region should be gone
|
||||||
|
assert_eq!(
|
||||||
|
m.data.iter_cells().count(),
|
||||||
|
0,
|
||||||
|
"all cells with Region coord should be removed"
|
||||||
|
);
|
||||||
|
// Views should no longer know about Region
|
||||||
|
// (axis_of would panic for unknown category, so check categories_on)
|
||||||
|
let v = m.active_view();
|
||||||
|
assert!(v.categories_on(crate::view::Axis::Row).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn create_view_copies_category_structure() {
|
fn create_view_copies_category_structure() {
|
||||||
let mut m = Model::new("Test");
|
let mut m = Model::new("Test");
|
||||||
|
|||||||
@ -90,6 +90,68 @@ impl Effect for RemoveFormula {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-enter edit mode by reading the cell value at the current cursor.
|
||||||
|
/// Used after commit+advance to continue data entry.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EnterEditAtCursor;
|
||||||
|
impl Effect for EnterEditAtCursor {
|
||||||
|
fn apply(&self, app: &mut App) {
|
||||||
|
let ctx = app.cmd_context(crossterm::event::KeyCode::Null, crossterm::event::KeyModifiers::NONE);
|
||||||
|
let value = if let Some(v) = &ctx.records_value {
|
||||||
|
v.clone()
|
||||||
|
} else {
|
||||||
|
ctx.cell_key
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|k| ctx.model.get_cell(k).cloned())
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
drop(ctx);
|
||||||
|
app.buffers.insert("edit".to_string(), value);
|
||||||
|
app.mode = AppMode::Editing {
|
||||||
|
buffer: String::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TogglePruneEmpty;
|
||||||
|
impl Effect for TogglePruneEmpty {
|
||||||
|
fn apply(&self, app: &mut App) {
|
||||||
|
let v = app.model.active_view_mut();
|
||||||
|
v.prune_empty = !v.prune_empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ToggleCatExpand(pub String);
|
||||||
|
impl Effect for ToggleCatExpand {
|
||||||
|
fn apply(&self, app: &mut App) {
|
||||||
|
if !app.expanded_cats.remove(&self.0) {
|
||||||
|
app.expanded_cats.insert(self.0.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RemoveItem {
|
||||||
|
pub category: String,
|
||||||
|
pub item: String,
|
||||||
|
}
|
||||||
|
impl Effect for RemoveItem {
|
||||||
|
fn apply(&self, app: &mut App) {
|
||||||
|
app.model.remove_item(&self.category, &self.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RemoveCategory(pub String);
|
||||||
|
impl Effect for RemoveCategory {
|
||||||
|
fn apply(&self, app: &mut App) {
|
||||||
|
app.model.remove_category(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── View mutations ───────────────────────────────────────────────────────────
|
// ── View mutations ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
Reference in New Issue
Block a user