feat: Pagination support for jumping 20 items at a time in all table views [#45]
This commit is contained in:
@@ -40,7 +40,7 @@ mod tests {
|
||||
title: "Sonarr Test".to_owned(),
|
||||
route: ActiveSonarrBlock::default().into(),
|
||||
help: format!(
|
||||
"<↑↓> scroll | ←→ change tab | {} ",
|
||||
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
|
||||
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
|
||||
),
|
||||
contextual_help: None,
|
||||
@@ -50,7 +50,7 @@ mod tests {
|
||||
title: "Radarr 1".to_owned(),
|
||||
route: ActiveRadarrBlock::default().into(),
|
||||
help: format!(
|
||||
"<↑↓> scroll | ←→ change tab | {} ",
|
||||
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
|
||||
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
|
||||
),
|
||||
contextual_help: None,
|
||||
@@ -60,7 +60,7 @@ mod tests {
|
||||
title: "Radarr Test".to_owned(),
|
||||
route: ActiveRadarrBlock::default().into(),
|
||||
help: format!(
|
||||
"<↑↓> scroll | ←→ change tab | {} ",
|
||||
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
|
||||
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
|
||||
),
|
||||
contextual_help: None,
|
||||
@@ -70,7 +70,7 @@ mod tests {
|
||||
title: "Sonarr 1".to_owned(),
|
||||
route: ActiveSonarrBlock::default().into(),
|
||||
help: format!(
|
||||
"<↑↓> scroll | ←→ change tab | {} ",
|
||||
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
|
||||
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
|
||||
),
|
||||
contextual_help: None,
|
||||
|
||||
@@ -14,6 +14,8 @@ generate_keybindings! {
|
||||
down,
|
||||
left,
|
||||
right,
|
||||
pg_down,
|
||||
pg_up,
|
||||
backspace,
|
||||
next_servarr,
|
||||
previous_servarr,
|
||||
@@ -74,6 +76,16 @@ pub const DEFAULT_KEYBINDINGS: KeyBindings = KeyBindings {
|
||||
alt: Some(Key::Char('l')),
|
||||
desc: "right",
|
||||
},
|
||||
pg_down: KeyBinding {
|
||||
key: Key::PgDown,
|
||||
alt: Some(Key::Ctrl('d')),
|
||||
desc: "page down",
|
||||
},
|
||||
pg_up: KeyBinding {
|
||||
key: Key::PgUp,
|
||||
alt: Some(Key::Ctrl('u')),
|
||||
desc: "page up",
|
||||
},
|
||||
backspace: KeyBinding {
|
||||
key: Key::Backspace,
|
||||
alt: Some(Key::Ctrl('h')),
|
||||
|
||||
@@ -13,6 +13,8 @@ mod test {
|
||||
#[case(DEFAULT_KEYBINDINGS.down, Key::Down, Some(Key::Char('j')), "down")]
|
||||
#[case(DEFAULT_KEYBINDINGS.left, Key::Left, Some(Key::Char('h')), "left")]
|
||||
#[case(DEFAULT_KEYBINDINGS.right, Key::Right, Some(Key::Char('l')), "right")]
|
||||
#[case(DEFAULT_KEYBINDINGS.pg_down, Key::PgDown, Some(Key::Ctrl('d')), "page down")]
|
||||
#[case(DEFAULT_KEYBINDINGS.pg_up, Key::PgUp, Some(Key::Ctrl('u')), "page up")]
|
||||
#[case(DEFAULT_KEYBINDINGS.backspace, Key::Backspace, Some(Key::Ctrl('h')), "backspace")]
|
||||
#[case(DEFAULT_KEYBINDINGS.next_servarr, Key::Tab, None, "next servarr")]
|
||||
#[case(DEFAULT_KEYBINDINGS.previous_servarr, Key::BackTab, None, "previous servarr")]
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ impl App<'_> {
|
||||
) -> Self {
|
||||
let mut server_tabs = Vec::new();
|
||||
let help = format!(
|
||||
"<↑↓> scroll | ←→ change tab | {} ",
|
||||
"<↑↓> scroll | <C-u/d> page up/down | ←→ change tab | {} ",
|
||||
build_context_clue_string(&SERVARR_CONTEXT_CLUES)
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ pub enum Key {
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
PgDown,
|
||||
PgUp,
|
||||
Enter,
|
||||
Esc,
|
||||
Backspace,
|
||||
@@ -35,6 +37,8 @@ impl Display for Key {
|
||||
Key::Down => write!(f, "<↓>"),
|
||||
Key::Left => write!(f, "<←>"),
|
||||
Key::Right => write!(f, "<→>"),
|
||||
Key::PgDown => write!(f, "<C-d>"),
|
||||
Key::PgUp => write!(f, "<C-u>"),
|
||||
Key::Enter => write!(f, "<enter>"),
|
||||
Key::Esc => write!(f, "<esc>"),
|
||||
Key::Backspace => write!(f, "<backspace>"),
|
||||
@@ -66,6 +70,14 @@ impl From<KeyEvent> for Key {
|
||||
code: KeyCode::Right,
|
||||
..
|
||||
} => Key::Right,
|
||||
KeyEvent {
|
||||
code: KeyCode::PageDown,
|
||||
..
|
||||
} => Key::PgDown,
|
||||
KeyEvent {
|
||||
code: KeyCode::PageUp,
|
||||
..
|
||||
} => Key::PgUp,
|
||||
KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
..
|
||||
|
||||
@@ -11,6 +11,8 @@ mod tests {
|
||||
#[case(Key::Down, "↓")]
|
||||
#[case(Key::Left, "←")]
|
||||
#[case(Key::Right, "→")]
|
||||
#[case(Key::PgDown, "C-d")]
|
||||
#[case(Key::PgUp, "C-u")]
|
||||
#[case(Key::Enter, "enter")]
|
||||
#[case(Key::Esc, "esc")]
|
||||
#[case(Key::Backspace, "backspace")]
|
||||
@@ -45,6 +47,16 @@ mod tests {
|
||||
assert_eq!(Key::from(KeyEvent::from(KeyCode::Right)), Key::Right);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_from_page_down() {
|
||||
assert_eq!(Key::from(KeyEvent::from(KeyCode::PageDown)), Key::PgDown);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_from_page_up() {
|
||||
assert_eq!(Key::from(KeyEvent::from(KeyCode::PageUp)), Key::PgUp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_from_backspace() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -44,6 +44,8 @@ macro_rules! handle_table_events {
|
||||
match $self.key {
|
||||
_ if $crate::matches_key!(up, $self.key, $self.ignore_special_keys()) => $self.[<handle_ $name _table_scroll_up>](config),
|
||||
_ if $crate::matches_key!(down, $self.key, $self.ignore_special_keys()) => $self.[<handle_ $name _table_scroll_down>](config),
|
||||
_ if $crate::matches_key!(pg_up, $self.key, $self.ignore_special_keys()) => $self.[<handle_ $name _table_page_up>](config),
|
||||
_ if $crate::matches_key!(pg_down, $self.key, $self.ignore_special_keys()) => $self.[<handle_ $name _table_page_down>](config),
|
||||
_ if $crate::matches_key!(home, $self.key) => $self.[<handle_ $name _table_home>](config),
|
||||
_ if $crate::matches_key!(end, $self.key) => $self.[<handle_ $name _table_end>](config),
|
||||
_ if $crate::matches_key!(left, $self.key, $self.ignore_special_keys())
|
||||
@@ -116,6 +118,30 @@ macro_rules! handle_table_events {
|
||||
}
|
||||
}
|
||||
|
||||
fn [<handle_ $name _table_page_down>](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool {
|
||||
use $crate::models::Paginated;
|
||||
|
||||
match $self.app.get_current_route() {
|
||||
_ if config.table_block == $self.app.get_current_route() => {
|
||||
$table.page_down();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn [<handle_ $name _table_page_up>](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool {
|
||||
use $crate::models::Paginated;
|
||||
|
||||
match $self.app.get_current_route() {
|
||||
_ if config.table_block == $self.app.get_current_route() => {
|
||||
$table.page_up();
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn [<handle_ $name _table_home>](&mut $self, config: $crate::handlers::table_handler::TableHandlingConfig<$row>) -> bool {
|
||||
use $crate::models::Scrollable;
|
||||
|
||||
|
||||
@@ -429,6 +429,48 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_pagination_scroll {
|
||||
use super::*;
|
||||
use crate::handlers::table_handler::table_handler_tests::tests::TableHandlerUnit;
|
||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||
use pretty_assertions::assert_str_eq;
|
||||
use rstest::rstest;
|
||||
use std::iter;
|
||||
|
||||
#[rstest]
|
||||
fn test_table_pagination_scroll(
|
||||
#[values(DEFAULT_KEYBINDINGS.pg_up.key, DEFAULT_KEYBINDINGS.pg_down.key)] key: Key,
|
||||
) {
|
||||
let mut app = App::test_default();
|
||||
app.push_navigation_stack(ActiveRadarrBlock::Movies.into());
|
||||
let mut curr = 0;
|
||||
let movies_vec = iter::repeat_with(|| {
|
||||
let tmp = curr;
|
||||
curr += 1;
|
||||
Movie {
|
||||
title: format!("Test {tmp}").into(),
|
||||
..Movie::default()
|
||||
}
|
||||
})
|
||||
.take(100)
|
||||
.collect();
|
||||
app.data.radarr_data.movies.set_items(movies_vec);
|
||||
TableHandlerUnit::new(key, &mut app, ActiveRadarrBlock::Movies, None).handle();
|
||||
|
||||
if key == Key::PgUp {
|
||||
assert_str_eq!(
|
||||
app.data.radarr_data.movies.current_selection().title.text,
|
||||
"Test 79"
|
||||
);
|
||||
} else {
|
||||
assert_str_eq!(
|
||||
app.data.radarr_data.movies.current_selection().title.text,
|
||||
"Test 20"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod test_handle_left_right_action {
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::sync::atomic::Ordering::SeqCst;
|
||||
|
||||
@@ -49,6 +49,11 @@ pub trait Scrollable {
|
||||
fn scroll_to_bottom(&mut self);
|
||||
}
|
||||
|
||||
pub trait Paginated {
|
||||
fn page_down(&mut self);
|
||||
fn page_up(&mut self);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ScrollableText {
|
||||
pub items: Vec<String>,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::models::stateful_list::StatefulList;
|
||||
use crate::models::{strip_non_search_characters, HorizontallyScrollableText, Scrollable};
|
||||
use crate::models::{
|
||||
strip_non_search_characters, HorizontallyScrollableText, Paginated, Scrollable,
|
||||
};
|
||||
use ratatui::widgets::TableState;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Debug;
|
||||
@@ -151,6 +153,79 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Paginated for StatefulTable<T>
|
||||
where
|
||||
T: Clone + PartialEq + Eq + Debug,
|
||||
{
|
||||
fn page_down(&mut self) {
|
||||
if let Some(filtered_items) = self.filtered_items.as_ref() {
|
||||
if filtered_items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.filtered_state.as_ref().unwrap().selected() {
|
||||
Some(i) => {
|
||||
self
|
||||
.filtered_state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.select(Some(i.saturating_add(20) % (filtered_items.len() - 1)));
|
||||
}
|
||||
None => self.filtered_state.as_mut().unwrap().select_first(),
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.state.selected() {
|
||||
Some(i) => {
|
||||
self
|
||||
.state
|
||||
.select(Some(i.saturating_add(20) % (self.items.len() - 1)));
|
||||
}
|
||||
None => self.state.select_first(),
|
||||
};
|
||||
}
|
||||
|
||||
fn page_up(&mut self) {
|
||||
if let Some(filtered_items) = self.filtered_items.as_ref() {
|
||||
if filtered_items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.filtered_state.as_ref().unwrap().selected() {
|
||||
Some(i) => {
|
||||
let len = filtered_items.len() - 1;
|
||||
self
|
||||
.filtered_state
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.select(Some((i + len - (20 % len)) % len));
|
||||
}
|
||||
None => self.filtered_state.as_mut().unwrap().select_last(),
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.state.selected() {
|
||||
Some(i) => {
|
||||
let len = self.items.len() - 1;
|
||||
self.state.select(Some((i + len - (20 % len)) % len));
|
||||
}
|
||||
None => self.state.select_last(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> StatefulTable<T>
|
||||
where
|
||||
T: Clone + PartialEq + Eq + Debug + Default,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::models::stateful_table::{SortOption, StatefulTable};
|
||||
use crate::models::Scrollable;
|
||||
use crate::models::{Paginated, Scrollable};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use ratatui::widgets::TableState;
|
||||
use std::iter;
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_scrolling_on_empty_table_performs_no_op() {
|
||||
@@ -190,6 +191,174 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_pagination_on_empty_table_performs_no_op() {
|
||||
let mut stateful_table: StatefulTable<String> = StatefulTable::default();
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), None);
|
||||
|
||||
stateful_table.page_down();
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), None);
|
||||
|
||||
stateful_table.page_up();
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_filtered_pagination_on_empty_table_performs_no_op() {
|
||||
let mut filtered_stateful_table: StatefulTable<String> = StatefulTable {
|
||||
filtered_items: Some(Vec::new()),
|
||||
filtered_state: Some(TableState::default()),
|
||||
..StatefulTable::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
filtered_stateful_table
|
||||
.filtered_state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.selected(),
|
||||
None
|
||||
);
|
||||
|
||||
filtered_stateful_table.page_down();
|
||||
|
||||
assert_eq!(
|
||||
filtered_stateful_table
|
||||
.filtered_state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.selected(),
|
||||
None
|
||||
);
|
||||
|
||||
filtered_stateful_table.page_up();
|
||||
|
||||
assert_eq!(
|
||||
filtered_stateful_table
|
||||
.filtered_state
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.selected(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_pagination() {
|
||||
let mut stateful_table = StatefulTable::default();
|
||||
let mut curr = 0;
|
||||
stateful_table.set_filtered_items(
|
||||
iter::repeat_with(|| {
|
||||
let tmp = curr;
|
||||
curr += 1;
|
||||
tmp
|
||||
})
|
||||
.take(100)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||
Some(0)
|
||||
);
|
||||
|
||||
stateful_table.page_down();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||
Some(20)
|
||||
);
|
||||
|
||||
stateful_table.page_up();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||
Some(0)
|
||||
);
|
||||
|
||||
stateful_table.page_up();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||
Some(stateful_table.filtered_items.as_ref().unwrap().len() - 21)
|
||||
);
|
||||
|
||||
stateful_table.page_down();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||
Some(0)
|
||||
);
|
||||
|
||||
stateful_table.scroll_down();
|
||||
stateful_table.page_up();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||
Some(stateful_table.filtered_items.as_ref().unwrap().len() - 20)
|
||||
);
|
||||
|
||||
stateful_table.scroll_down();
|
||||
stateful_table.page_down();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.filtered_state.as_ref().unwrap().selected(),
|
||||
Some(2)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_filtered_items_pagination() {
|
||||
let mut stateful_table = StatefulTable::default();
|
||||
let mut curr = 0;
|
||||
stateful_table.set_items(
|
||||
iter::repeat_with(|| {
|
||||
let tmp = curr;
|
||||
curr += 1;
|
||||
tmp
|
||||
})
|
||||
.take(100)
|
||||
.collect(),
|
||||
);
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), Some(0));
|
||||
|
||||
stateful_table.page_down();
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), Some(20));
|
||||
|
||||
stateful_table.page_up();
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), Some(0));
|
||||
|
||||
stateful_table.page_up();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.state.selected(),
|
||||
Some(stateful_table.items.len() - 21)
|
||||
);
|
||||
|
||||
stateful_table.page_down();
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), Some(0));
|
||||
|
||||
stateful_table.scroll_down();
|
||||
stateful_table.page_up();
|
||||
|
||||
assert_eq!(
|
||||
stateful_table.state.selected(),
|
||||
Some(stateful_table.items.len() - 20)
|
||||
);
|
||||
|
||||
stateful_table.scroll_down();
|
||||
stateful_table.page_down();
|
||||
|
||||
assert_eq!(stateful_table.state.selected(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stateful_table_set_items() {
|
||||
let items_vec = vec!["Test 1", "Test 2", "Test 3"];
|
||||
|
||||
Reference in New Issue
Block a user