add panel toggling and new command implementations
- Implemented a suite of new commands for panel visibility, editing, export prompts, search navigation, page cycling, and grid operations. - Updated tests to cover new command behavior. - Adjusted command context usage accordingly. Co-Authored-By: fiddlerwoaroof/git-smart-commit (gpt-oss:20b)
This commit is contained in:
@ -315,14 +315,479 @@ impl Cmd for EnterSearchMode {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Panel commands ──────────────────────────────────────────────────────
|
||||
|
||||
/// Toggle a panel's visibility; if it opens, focus it (enter its mode).
|
||||
#[derive(Debug)]
|
||||
pub struct SetPendingKey(pub char);
|
||||
impl Cmd for SetPendingKey {
|
||||
pub struct TogglePanelAndFocus(pub Panel);
|
||||
impl Cmd for TogglePanelAndFocus {
|
||||
fn name(&self) -> &str {
|
||||
"set-pending-key"
|
||||
"toggle-panel-and-focus"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let currently_open = match self.0 {
|
||||
Panel::Formula => ctx.formula_panel_open,
|
||||
Panel::Category => ctx.category_panel_open,
|
||||
Panel::View => ctx.view_panel_open,
|
||||
};
|
||||
let new_open = !currently_open;
|
||||
let mut effects: Vec<Box<dyn Effect>> = vec![Box::new(effect::SetPanelOpen {
|
||||
panel: self.0,
|
||||
open: new_open,
|
||||
})];
|
||||
if new_open {
|
||||
let mode = match self.0 {
|
||||
Panel::Formula => AppMode::FormulaPanel,
|
||||
Panel::Category => AppMode::CategoryPanel,
|
||||
Panel::View => AppMode::ViewPanel,
|
||||
};
|
||||
effects.push(effect::change_mode(mode));
|
||||
}
|
||||
effects
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle a panel's visibility without changing mode.
|
||||
#[derive(Debug)]
|
||||
pub struct TogglePanelVisibility(pub Panel);
|
||||
impl Cmd for TogglePanelVisibility {
|
||||
fn name(&self) -> &str {
|
||||
"toggle-panel-visibility"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let currently_open = match self.0 {
|
||||
Panel::Formula => ctx.formula_panel_open,
|
||||
Panel::Category => ctx.category_panel_open,
|
||||
Panel::View => ctx.view_panel_open,
|
||||
};
|
||||
vec![Box::new(effect::SetPanelOpen {
|
||||
panel: self.0,
|
||||
open: !currently_open,
|
||||
})]
|
||||
}
|
||||
}
|
||||
|
||||
/// Tab through open panels, entering the first open panel's mode.
|
||||
#[derive(Debug)]
|
||||
pub struct CyclePanelFocus;
|
||||
impl Cmd for CyclePanelFocus {
|
||||
fn name(&self) -> &str {
|
||||
"cycle-panel-focus"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
if ctx.formula_panel_open {
|
||||
vec![effect::change_mode(AppMode::FormulaPanel)]
|
||||
} else if ctx.category_panel_open {
|
||||
vec![effect::change_mode(AppMode::CategoryPanel)]
|
||||
} else if ctx.view_panel_open {
|
||||
vec![effect::change_mode(AppMode::ViewPanel)]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Editing entry ───────────────────────────────────────────────────────
|
||||
|
||||
/// Read the current cell value and enter Editing mode with it as the buffer.
|
||||
#[derive(Debug)]
|
||||
pub struct EnterEditMode;
|
||||
impl Cmd for EnterEditMode {
|
||||
fn name(&self) -> &str {
|
||||
"enter-edit-mode"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||
let (ri, ci) = ctx.selected;
|
||||
let current = layout
|
||||
.cell_key(ri, ci)
|
||||
.and_then(|k| ctx.model.get_cell(&k).cloned())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default();
|
||||
vec![effect::change_mode(AppMode::Editing { buffer: current })]
|
||||
}
|
||||
}
|
||||
|
||||
/// Typewriter-style advance: move down, wrap to top of next column at bottom.
|
||||
#[derive(Debug)]
|
||||
pub struct EnterAdvance;
|
||||
impl Cmd for EnterAdvance {
|
||||
fn name(&self) -> &str {
|
||||
"enter-advance"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||
let row_max = layout.row_count().saturating_sub(1);
|
||||
let col_max = layout.col_count().saturating_sub(1);
|
||||
let (r, c) = ctx.selected;
|
||||
let (nr, nc) = if r < row_max {
|
||||
(r + 1, c)
|
||||
} else if c < col_max {
|
||||
(0, c + 1)
|
||||
} else {
|
||||
(r, c) // already at bottom-right; stay
|
||||
};
|
||||
let mut effects: Vec<Box<dyn Effect>> = vec![effect::set_selected(nr, nc)];
|
||||
let mut row_offset = ctx.row_offset;
|
||||
let mut col_offset = ctx.col_offset;
|
||||
if nr < row_offset {
|
||||
row_offset = nr;
|
||||
}
|
||||
if nr >= row_offset + 20 {
|
||||
row_offset = nr.saturating_sub(19);
|
||||
}
|
||||
if nc < col_offset {
|
||||
col_offset = nc;
|
||||
}
|
||||
if nc >= col_offset + 8 {
|
||||
col_offset = nc.saturating_sub(7);
|
||||
}
|
||||
if row_offset != ctx.row_offset {
|
||||
effects.push(Box::new(effect::SetRowOffset(row_offset)));
|
||||
}
|
||||
if col_offset != ctx.col_offset {
|
||||
effects.push(Box::new(effect::SetColOffset(col_offset)));
|
||||
}
|
||||
effects
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter export prompt mode.
|
||||
#[derive(Debug)]
|
||||
pub struct EnterExportPrompt;
|
||||
impl Cmd for EnterExportPrompt {
|
||||
fn name(&self) -> &str {
|
||||
"enter-export-prompt"
|
||||
}
|
||||
fn execute(&self, _ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
vec![Box::new(effect::SetPendingKey(Some(self.0)))]
|
||||
vec![effect::change_mode(AppMode::ExportPrompt {
|
||||
buffer: String::new(),
|
||||
})]
|
||||
}
|
||||
}
|
||||
|
||||
// ── Search / navigation ─────────────────────────────────────────────────
|
||||
|
||||
/// Navigate to the next or previous search match.
|
||||
#[derive(Debug)]
|
||||
pub struct SearchNavigate(pub bool);
|
||||
impl Cmd for SearchNavigate {
|
||||
fn name(&self) -> &str {
|
||||
"search-navigate"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let query = ctx.search_query.to_lowercase();
|
||||
if query.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||
let (cur_row, cur_col) = ctx.selected;
|
||||
let total_rows = layout.row_count().max(1);
|
||||
let total_cols = layout.col_count().max(1);
|
||||
let total = total_rows * total_cols;
|
||||
let cur_flat = cur_row * total_cols + cur_col;
|
||||
|
||||
let matches: Vec<usize> = (0..total)
|
||||
.filter(|&flat| {
|
||||
let ri = flat / total_cols;
|
||||
let ci = flat % total_cols;
|
||||
let key = match layout.cell_key(ri, ci) {
|
||||
Some(k) => k,
|
||||
None => return false,
|
||||
};
|
||||
let s = match ctx.model.evaluate_aggregated(&key, &layout.none_cats) {
|
||||
Some(CellValue::Number(n)) => format!("{n}"),
|
||||
Some(CellValue::Text(t)) => t,
|
||||
None => String::new(),
|
||||
};
|
||||
s.to_lowercase().contains(&query)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if matches.is_empty() {
|
||||
return vec![effect::set_status(format!(
|
||||
"No matches for '{}'",
|
||||
ctx.search_query
|
||||
))];
|
||||
}
|
||||
|
||||
let target_flat = if self.0 {
|
||||
matches
|
||||
.iter()
|
||||
.find(|&&f| f > cur_flat)
|
||||
.or_else(|| matches.first())
|
||||
.copied()
|
||||
} else {
|
||||
matches
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|&&f| f < cur_flat)
|
||||
.or_else(|| matches.last())
|
||||
.copied()
|
||||
};
|
||||
|
||||
if let Some(flat) = target_flat {
|
||||
let ri = flat / total_cols;
|
||||
let ci = flat % total_cols;
|
||||
let mut effects: Vec<Box<dyn Effect>> = vec![effect::set_selected(ri, ci)];
|
||||
if ri < ctx.row_offset {
|
||||
effects.push(Box::new(effect::SetRowOffset(ri)));
|
||||
}
|
||||
if ci < ctx.col_offset {
|
||||
effects.push(Box::new(effect::SetColOffset(ci)));
|
||||
}
|
||||
effects.push(effect::set_status(format!(
|
||||
"Match {}/{} for '{}'",
|
||||
matches.iter().position(|&f| f == flat).unwrap_or(0) + 1,
|
||||
matches.len(),
|
||||
ctx.search_query,
|
||||
)));
|
||||
effects
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If search query is active, navigate backward; otherwise open CategoryAdd.
|
||||
#[derive(Debug)]
|
||||
pub struct SearchOrCategoryAdd;
|
||||
impl Cmd for SearchOrCategoryAdd {
|
||||
fn name(&self) -> &str {
|
||||
"search-or-category-add"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
if !ctx.search_query.is_empty() {
|
||||
SearchNavigate(false).execute(ctx)
|
||||
} else {
|
||||
vec![
|
||||
Box::new(effect::SetPanelOpen {
|
||||
panel: Panel::Category,
|
||||
open: true,
|
||||
}),
|
||||
effect::change_mode(AppMode::CategoryAdd {
|
||||
buffer: String::new(),
|
||||
}),
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Page navigation ─────────────────────────────────────────────────────
|
||||
|
||||
/// Advance to the next page (odometer-style cycling).
|
||||
#[derive(Debug)]
|
||||
pub struct PageNext;
|
||||
impl Cmd for PageNext {
|
||||
fn name(&self) -> &str {
|
||||
"page-next"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let data = page_cat_data(ctx);
|
||||
if data.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
|
||||
let mut carry = true;
|
||||
for i in (0..data.len()).rev() {
|
||||
if !carry {
|
||||
break;
|
||||
}
|
||||
indices[i] += 1;
|
||||
if indices[i] >= data[i].1.len() {
|
||||
indices[i] = 0;
|
||||
} else {
|
||||
carry = false;
|
||||
}
|
||||
}
|
||||
data.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (cat, items, _))| {
|
||||
Box::new(effect::SetPageSelection {
|
||||
category: cat.clone(),
|
||||
item: items[indices[i]].clone(),
|
||||
}) as Box<dyn Effect>
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Go to the previous page (odometer-style cycling).
|
||||
#[derive(Debug)]
|
||||
pub struct PagePrev;
|
||||
impl Cmd for PagePrev {
|
||||
fn name(&self) -> &str {
|
||||
"page-prev"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let data = page_cat_data(ctx);
|
||||
if data.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
let mut indices: Vec<usize> = data.iter().map(|(_, _, i)| *i).collect();
|
||||
let mut borrow = true;
|
||||
for i in (0..data.len()).rev() {
|
||||
if !borrow {
|
||||
break;
|
||||
}
|
||||
if indices[i] == 0 {
|
||||
indices[i] = data[i].1.len().saturating_sub(1);
|
||||
} else {
|
||||
indices[i] -= 1;
|
||||
borrow = false;
|
||||
}
|
||||
}
|
||||
data.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (cat, items, _))| {
|
||||
Box::new(effect::SetPageSelection {
|
||||
category: cat.clone(),
|
||||
item: items[indices[i]].clone(),
|
||||
}) as Box<dyn Effect>
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Gather (cat_name, items, current_idx) for page-axis categories.
|
||||
fn page_cat_data(ctx: &CmdContext) -> Vec<(String, Vec<String>, usize)> {
|
||||
let view = ctx.model.active_view();
|
||||
let page_cats: Vec<String> = view
|
||||
.categories_on(Axis::Page)
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
page_cats
|
||||
.into_iter()
|
||||
.filter_map(|cat| {
|
||||
let items: Vec<String> = ctx
|
||||
.model
|
||||
.category(&cat)
|
||||
.map(|c| {
|
||||
c.ordered_item_names()
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
if items.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let current = view
|
||||
.page_selection(&cat)
|
||||
.map(String::from)
|
||||
.or_else(|| items.first().cloned())
|
||||
.unwrap_or_default();
|
||||
let idx = items.iter().position(|i| *i == current).unwrap_or(0);
|
||||
Some((cat, items, idx))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ── Grid operations ─────────────────────────────────────────────────────
|
||||
|
||||
/// Toggle the row group collapse under the cursor.
|
||||
#[derive(Debug)]
|
||||
pub struct ToggleGroupUnderCursor;
|
||||
impl Cmd for ToggleGroupUnderCursor {
|
||||
fn name(&self) -> &str {
|
||||
"toggle-group-under-cursor"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||
let sel_row = ctx.selected.0;
|
||||
let Some((cat, group)) = layout.row_group_for(sel_row) else {
|
||||
return vec![];
|
||||
};
|
||||
vec![
|
||||
Box::new(effect::ToggleGroup {
|
||||
category: cat,
|
||||
group,
|
||||
}),
|
||||
effect::mark_dirty(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the column group collapse under the cursor.
|
||||
#[derive(Debug)]
|
||||
pub struct ToggleColGroupUnderCursor;
|
||||
impl Cmd for ToggleColGroupUnderCursor {
|
||||
fn name(&self) -> &str {
|
||||
"toggle-col-group-under-cursor"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||
let sel_col = ctx.selected.1;
|
||||
let Some((cat, group)) = layout.col_group_for(sel_col) else {
|
||||
return vec![];
|
||||
};
|
||||
// After toggling, col_count may shrink — clamp selection
|
||||
// We return ToggleGroup + MarkDirty; selection clamping will need
|
||||
// to happen in the effect or in a follow-up pass
|
||||
vec![
|
||||
Box::new(effect::ToggleGroup {
|
||||
category: cat,
|
||||
group,
|
||||
}),
|
||||
effect::mark_dirty(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide the row item at the cursor.
|
||||
#[derive(Debug)]
|
||||
pub struct HideSelectedRowItem;
|
||||
impl Cmd for HideSelectedRowItem {
|
||||
fn name(&self) -> &str {
|
||||
"hide-selected-row-item"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let layout = GridLayout::new(ctx.model, ctx.model.active_view());
|
||||
let Some(cat_name) = layout.row_cats.first().cloned() else {
|
||||
return vec![];
|
||||
};
|
||||
let sel_row = ctx.selected.0;
|
||||
let Some(items) = layout
|
||||
.row_items
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
if let AxisEntry::DataItem(v) = e {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.nth(sel_row)
|
||||
else {
|
||||
return vec![];
|
||||
};
|
||||
let item_name = items[0].clone();
|
||||
vec![
|
||||
Box::new(effect::HideItem {
|
||||
category: cat_name,
|
||||
item: item_name,
|
||||
}),
|
||||
effect::mark_dirty(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// Enter tile select mode.
|
||||
#[derive(Debug)]
|
||||
pub struct EnterTileSelect;
|
||||
impl Cmd for EnterTileSelect {
|
||||
fn name(&self) -> &str {
|
||||
"enter-tile-select"
|
||||
}
|
||||
fn execute(&self, ctx: &CmdContext) -> Vec<Box<dyn Effect>> {
|
||||
let count = ctx.model.category_names().len();
|
||||
if count > 0 {
|
||||
vec![effect::change_mode(AppMode::TileSelect { cat_idx: 0 })]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,9 +807,11 @@ mod tests {
|
||||
col_offset: view.col_offset,
|
||||
search_query: "",
|
||||
yanked: &None,
|
||||
pending_key: None,
|
||||
dirty: false,
|
||||
file_path_set: false,
|
||||
formula_panel_open: false,
|
||||
category_panel_open: false,
|
||||
view_panel_open: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -401,7 +868,10 @@ mod tests {
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 1);
|
||||
let dbg = format!("{:?}", effects[0]);
|
||||
assert!(dbg.contains("ChangeMode"), "Expected ChangeMode, got: {dbg}");
|
||||
assert!(
|
||||
dbg.contains("ChangeMode"),
|
||||
"Expected ChangeMode, got: {dbg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -431,4 +901,133 @@ mod tests {
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 2); // SetYanked + SetStatus
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_panel_and_focus_opens_and_enters_mode() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = TogglePanelAndFocus(effect::Panel::Formula);
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 2); // SetPanelOpen + ChangeMode
|
||||
let dbg = format!("{:?}", effects[1]);
|
||||
assert!(
|
||||
dbg.contains("FormulaPanel"),
|
||||
"Expected FormulaPanel mode, got: {dbg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_panel_and_focus_closes_when_open() {
|
||||
let m = two_cat_model();
|
||||
let mut ctx = make_ctx(&m);
|
||||
ctx.formula_panel_open = true;
|
||||
let cmd = TogglePanelAndFocus(effect::Panel::Formula);
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 1); // SetPanelOpen only, no mode change
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_advance_moves_down() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = EnterAdvance;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert!(!effects.is_empty());
|
||||
let dbg = format!("{:?}", effects[0]);
|
||||
assert!(
|
||||
dbg.contains("SetSelected(1, 0)"),
|
||||
"Expected row 1, got: {dbg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_navigate_with_empty_query_returns_nothing() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = SearchNavigate(true);
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert!(effects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_edit_mode_produces_editing_mode() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = EnterEditMode;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 1);
|
||||
let dbg = format!("{:?}", effects[0]);
|
||||
assert!(dbg.contains("Editing"), "Expected Editing mode, got: {dbg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_tile_select_with_categories() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = EnterTileSelect;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 1);
|
||||
let dbg = format!("{:?}", effects[0]);
|
||||
assert!(
|
||||
dbg.contains("TileSelect"),
|
||||
"Expected TileSelect mode, got: {dbg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_tile_select_no_categories() {
|
||||
let m = Model::new("Empty");
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = EnterTileSelect;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert!(effects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_group_under_cursor_returns_empty_without_groups() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = ToggleGroupUnderCursor;
|
||||
let effects = cmd.execute(&ctx);
|
||||
// No groups defined, so nothing to toggle
|
||||
assert!(effects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_or_category_add_without_query_opens_category_add() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = SearchOrCategoryAdd;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 2); // SetPanelOpen + ChangeMode
|
||||
let dbg = format!("{:?}", effects[1]);
|
||||
assert!(
|
||||
dbg.contains("CategoryAdd"),
|
||||
"Expected CategoryAdd, got: {dbg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_panel_focus_with_no_panels_open() {
|
||||
let m = two_cat_model();
|
||||
let ctx = make_ctx(&m);
|
||||
let cmd = CyclePanelFocus;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert!(effects.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_panel_focus_with_formula_panel_open() {
|
||||
let m = two_cat_model();
|
||||
let mut ctx = make_ctx(&m);
|
||||
ctx.formula_panel_open = true;
|
||||
let cmd = CyclePanelFocus;
|
||||
let effects = cmd.execute(&ctx);
|
||||
assert_eq!(effects.len(), 1);
|
||||
let dbg = format!("{:?}", effects[0]);
|
||||
assert!(
|
||||
dbg.contains("FormulaPanel"),
|
||||
"Expected FormulaPanel, got: {dbg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user