use super::{Issue, List, ListOf, Page, Rows, Store};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PageAsk {
pub of: ListOf,
pub after: Option<String>,
}
impl PageAsk {
fn first(of: ListOf) -> Self {
Self { of, after: None }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Opening {
Cached,
Fetch(PageAsk),
Replace(PageAsk),
}
impl Store {
pub fn open_list(&mut self, list: List, of: ListOf) -> Opening {
let rows = self.listing_mut(list);
let current = rows.of().cloned();
if current.as_ref() == Some(&of) && rows.loaded() {
return Opening::Cached;
}
if current.as_ref().is_some_and(|c| c.same_owner(&of)) {
rows.set_loaded(false);
rows.set_of(of.clone());
Opening::Fetch(PageAsk::first(of))
} else {
rows.reset();
rows.set_of(of.clone());
Opening::Replace(PageAsk::first(of))
}
}
pub fn reload_list(&mut self, list: List) -> Option<PageAsk> {
let rows = self.listing_mut(list);
let of = rows.of()?.clone();
rows.set_loaded(false);
Some(PageAsk::first(of))
}
pub fn next_page(&mut self, list: List) -> Option<PageAsk> {
let rows = self.listing(list);
let info = rows.page_info();
if !info.has_next_page {
return None;
}
let cursor = info.end_cursor.clone()?;
let of = rows.of()?.clone();
self.requested
.cursors
.insert(cursor.clone())
.then_some(PageAsk {
of,
after: Some(cursor),
})
}
pub fn page_failed(&mut self, cursor: &str) {
self.requested.cursors.remove(cursor);
}
pub fn forget_pages(&mut self) {
self.requested.cursors.clear();
}
}
impl<T> Rows<T> {
pub fn accept_for(&mut self, of: &ListOf, page: Page<T>) -> bool {
if self.of.as_ref() != Some(of) {
return false;
}
self.accept(page);
true
}
}
impl Rows<Issue> {
pub fn accept_issues_for(&mut self, of: &ListOf, page: Page<Issue>) -> bool {
if self.of.as_ref() != Some(of) {
return false;
}
self.accept_issues(page);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::entity::{PageInfo, Preset, Project, TeamId};
use crate::core::store::IssueSource;
fn team_projects(team: &str) -> ListOf {
ListOf::TeamProjects(TeamId::from(team))
}
fn project(id: &str) -> Project {
serde_json::from_str(&format!(r#"{{"id":"{id}","name":"{id}"}}"#)).unwrap()
}
fn page(more: Option<&str>) -> Page<Project> {
let info = PageInfo {
has_next_page: more.is_some(),
end_cursor: more.map(Into::into),
};
Page::new(vec![project("p")], info, false)
}
#[test]
fn a_new_list_asks_for_its_first_page() {
let mut store = Store::default();
assert_eq!(
store.open_list(List::Projects, team_projects("t")),
Opening::Replace(PageAsk::first(team_projects("t")))
);
}
#[test]
fn a_loaded_list_asks_for_nothing() {
let mut store = Store::default();
store.open_list(List::Projects, team_projects("t"));
store.projects.accept_for(&team_projects("t"), page(None));
assert_eq!(
store.open_list(List::Projects, team_projects("t")),
Opening::Cached
);
}
#[test]
fn a_list_of_something_else_drops_the_old_rows() {
let mut store = Store::default();
store.open_list(List::Projects, team_projects("t"));
store.projects.accept_for(&team_projects("t"), page(None));
assert!(matches!(
store.open_list(List::Projects, team_projects("u")),
Opening::Replace(_)
));
assert!(store.projects.items.is_empty());
}
#[test]
fn another_slice_keeps_the_rows_until_its_page_lands() {
let mut store = Store::default();
let slice = |preset| ListOf::TeamIssues {
team_id: TeamId::from("t"),
preset,
};
let list = List::Issues(IssueSource::Team);
store.open_list(list, slice(Preset::Active));
store.issues[IssueSource::Team].items =
vec![serde_json::from_str(r#"{"id":"i","identifier":"T-1","title":"t"}"#).unwrap()];
assert!(matches!(
store.open_list(list, slice(Preset::Backlog)),
Opening::Fetch(_)
));
assert_eq!(store.issues[IssueSource::Team].items.len(), 1);
}
#[test]
fn the_next_page_is_asked_for_once() {
let mut store = Store::default();
store.open_list(List::Projects, team_projects("t"));
store
.projects
.accept_for(&team_projects("t"), page(Some("c1")));
assert_eq!(
store.next_page(List::Projects),
Some(PageAsk {
of: team_projects("t"),
after: Some("c1".into())
})
);
assert_eq!(store.next_page(List::Projects), None);
}
#[test]
fn a_failed_page_can_be_asked_for_again() {
let mut store = Store::default();
store.open_list(List::Projects, team_projects("t"));
store
.projects
.accept_for(&team_projects("t"), page(Some("c1")));
store.next_page(List::Projects);
store.page_failed("c1");
assert!(store.next_page(List::Projects).is_some());
}
#[test]
fn the_last_page_has_no_next() {
let mut store = Store::default();
store.open_list(List::Projects, team_projects("t"));
store.projects.accept_for(&team_projects("t"), page(None));
assert_eq!(store.next_page(List::Projects), None);
}
#[test]
fn a_page_for_a_list_left_behind_is_dropped() {
let mut store = Store::default();
store.open_list(List::Projects, team_projects("u"));
assert!(!store.projects.accept_for(&team_projects("t"), page(None)));
assert!(store.projects.items.is_empty());
}
#[test]
fn reloading_asks_for_the_first_page_again() {
let mut store = Store::default();
store.open_list(List::Projects, team_projects("t"));
store.projects.accept_for(&team_projects("t"), page(None));
assert_eq!(
store.reload_list(List::Projects),
Some(PageAsk::first(team_projects("t")))
);
assert!(!store.projects.loaded);
assert_eq!(store.projects.items.len(), 1);
}
}