web: add bulk read/unerad functionality
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use graphql_client::GraphQLQuery;
|
||||
use log::{error, info};
|
||||
use seed::{app::subs, prelude::*, *};
|
||||
@@ -224,6 +226,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
results: data.search.nodes,
|
||||
count: data.count as usize,
|
||||
pager: data.search.page_info,
|
||||
selected_threads: HashSet::new(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,6 +259,54 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
Msg::ShowThreadResult(bad) => {
|
||||
error!("show_thread_query error: {bad:#?}");
|
||||
}
|
||||
Msg::SelectionMarkAsRead => {
|
||||
if let Context::SearchResult {
|
||||
selected_threads, ..
|
||||
} = &mut model.context
|
||||
{
|
||||
let threads = selected_threads
|
||||
.iter()
|
||||
.map(|tid| format!("thread:{tid}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
orders
|
||||
.skip()
|
||||
.perform_cmd(async move { Msg::SetUnread(threads, false) });
|
||||
}
|
||||
}
|
||||
Msg::SelectionMarkAsUnread => {
|
||||
if let Context::SearchResult {
|
||||
selected_threads, ..
|
||||
} = &mut model.context
|
||||
{
|
||||
let threads = selected_threads
|
||||
.iter()
|
||||
.map(|tid| format!("thread:{tid}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
orders
|
||||
.skip()
|
||||
.perform_cmd(async move { Msg::SetUnread(threads, true) });
|
||||
}
|
||||
}
|
||||
Msg::SelectionAddThread(tid) => {
|
||||
if let Context::SearchResult {
|
||||
selected_threads, ..
|
||||
} = &mut model.context
|
||||
{
|
||||
selected_threads.insert(tid);
|
||||
info!("selected threads {selected_threads:?}");
|
||||
}
|
||||
}
|
||||
Msg::SelectionRemoveThread(tid) => {
|
||||
if let Context::SearchResult {
|
||||
selected_threads, ..
|
||||
} = &mut model.context
|
||||
{
|
||||
selected_threads.remove(&tid);
|
||||
info!("selected threads {selected_threads:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// `Model` describes our app state.
|
||||
@@ -287,6 +338,7 @@ pub enum Context {
|
||||
results: Vec<FrontPageQuerySearchNodes>,
|
||||
count: usize,
|
||||
pager: FrontPageQuerySearchPageInfo,
|
||||
selected_threads: HashSet<String>,
|
||||
},
|
||||
ThreadResult(ShowThreadQueryThread),
|
||||
}
|
||||
@@ -337,4 +389,9 @@ pub enum Msg {
|
||||
ShowThreadResult(
|
||||
Result<graphql_client::Response<graphql::show_thread_query::ResponseData>, gloo_net::Error>,
|
||||
),
|
||||
|
||||
SelectionMarkAsRead,
|
||||
SelectionMarkAsUnread,
|
||||
SelectionAddThread(String),
|
||||
SelectionRemoveThread(String),
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
|
||||
results,
|
||||
count,
|
||||
pager,
|
||||
} => view_search_results(&query, results.as_slice(), *count, pager),
|
||||
selected_threads,
|
||||
} => view_search_results(&query, results.as_slice(), *count, pager, selected_threads),
|
||||
};
|
||||
fn view_tag_li(display_name: &str, indent: usize, t: &Tag, search_unread: bool) -> Node<Msg> {
|
||||
let href = if search_unread {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use seed::{prelude::*, *};
|
||||
|
||||
use crate::{
|
||||
@@ -16,7 +18,8 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
|
||||
results,
|
||||
count,
|
||||
pager,
|
||||
} => search_results(&query, results.as_slice(), *count, pager),
|
||||
selected_threads,
|
||||
} => search_results(&query, results.as_slice(), *count, pager, selected_threads),
|
||||
};
|
||||
div![
|
||||
view_header(&model.query, &model.refreshing_state),
|
||||
@@ -30,6 +33,7 @@ fn search_results(
|
||||
results: &[FrontPageQuerySearchNodes],
|
||||
count: usize,
|
||||
pager: &FrontPageQuerySearchPageInfo,
|
||||
selected_threads: &HashSet<String>,
|
||||
) -> Node<Msg> {
|
||||
if query.is_empty() {
|
||||
set_title("all mail");
|
||||
@@ -38,20 +42,45 @@ fn search_results(
|
||||
}
|
||||
let rows = results.iter().map(|r| {
|
||||
let tid = r.thread.clone();
|
||||
let check_tid = r.thread.clone();
|
||||
let datetime = human_age(r.timestamp as i64);
|
||||
let unread_idx = r.tags.iter().position(|e| e == &"unread");
|
||||
let mut tags = r.tags.clone();
|
||||
if let Some(idx) = unread_idx {
|
||||
tags.remove(idx);
|
||||
};
|
||||
a![
|
||||
C!["has-text-light"],
|
||||
IF!(unread_idx.is_some() => C!["unread"]),
|
||||
attrs! {
|
||||
At::Href => urls::thread(&tid)
|
||||
},
|
||||
div![
|
||||
C!["row"],
|
||||
div![
|
||||
C!["row"],
|
||||
label![
|
||||
C!["b-checkbox", "checkbox", "is-large"],
|
||||
input![attrs! {
|
||||
At::Type=>"checkbox",
|
||||
At::Checked=>selected_threads.contains(&tid).as_at_value(),
|
||||
}],
|
||||
span![C!["check"]],
|
||||
ev(Ev::Input, move |e| {
|
||||
if let Some(input) = e
|
||||
.target()
|
||||
.as_ref()
|
||||
.expect("failed to get reference to target")
|
||||
.dyn_ref::<web_sys::HtmlInputElement>()
|
||||
{
|
||||
if input.checked() {
|
||||
Msg::SelectionAddThread(check_tid)
|
||||
} else {
|
||||
Msg::SelectionRemoveThread(check_tid)
|
||||
}
|
||||
} else {
|
||||
Msg::Noop
|
||||
}
|
||||
}),
|
||||
],
|
||||
a![
|
||||
C!["has-text-light", "summary"],
|
||||
IF!(unread_idx.is_some() => C!["unread"]),
|
||||
attrs! {
|
||||
At::Href => urls::thread(&tid)
|
||||
},
|
||||
div![C!["subject"], &r.subject],
|
||||
span![C!["from", "is-size-7"], pretty_authors(&r.authors)],
|
||||
div![
|
||||
@@ -61,10 +90,11 @@ fn search_results(
|
||||
]
|
||||
]
|
||||
});
|
||||
let show_bulk_edit = !selected_threads.is_empty();
|
||||
div![
|
||||
C!["search-results"],
|
||||
search_toolbar(count, pager),
|
||||
div![C!["index"], rows,],
|
||||
search_toolbar(count, pager),
|
||||
search_toolbar(count, pager, show_bulk_edit),
|
||||
div![C!["index"], rows],
|
||||
search_toolbar(count, pager, show_bulk_edit),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -112,15 +112,17 @@ fn view_search_results(
|
||||
results: &[FrontPageQuerySearchNodes],
|
||||
count: usize,
|
||||
pager: &FrontPageQuerySearchPageInfo,
|
||||
selected_threads: &HashSet<String>,
|
||||
) -> Node<Msg> {
|
||||
info!("pager {pager:?}");
|
||||
if query.is_empty() {
|
||||
set_title("all mail");
|
||||
} else {
|
||||
set_title(query);
|
||||
}
|
||||
let show_bulk_edit = !selected_threads.is_empty();
|
||||
let rows = results.iter().map(|r| {
|
||||
let tid = r.thread.clone();
|
||||
let check_tid = r.thread.clone();
|
||||
let datetime = human_age(r.timestamp as i64);
|
||||
let unread_idx = r.tags.iter().position(|e| e == &"unread");
|
||||
let mut tags = r.tags.clone();
|
||||
@@ -129,6 +131,31 @@ fn view_search_results(
|
||||
};
|
||||
tr![
|
||||
IF!(unread_idx.is_some() => C!["unread"]),
|
||||
td![label![
|
||||
C!["checkbox"],
|
||||
input![
|
||||
attrs! {
|
||||
At::Type=>"checkbox",
|
||||
At::Checked=>selected_threads.contains(&tid).as_at_value(),
|
||||
},
|
||||
ev(Ev::Input, move |e| {
|
||||
if let Some(input) = e
|
||||
.target()
|
||||
.as_ref()
|
||||
.expect("failed to get reference to target")
|
||||
.dyn_ref::<web_sys::HtmlInputElement>()
|
||||
{
|
||||
if input.checked() {
|
||||
Msg::SelectionAddThread(check_tid)
|
||||
} else {
|
||||
Msg::SelectionRemoveThread(check_tid)
|
||||
}
|
||||
} else {
|
||||
Msg::Noop
|
||||
}
|
||||
}),
|
||||
]
|
||||
]],
|
||||
td![
|
||||
C!["from"],
|
||||
pretty_authors(&r.authors),
|
||||
@@ -144,15 +171,6 @@ fn view_search_results(
|
||||
At::Href => urls::thread(&tid)
|
||||
},
|
||||
&r.subject,
|
||||
button![
|
||||
C!["mark-read-button", "button", "is-dark", "is-small"],
|
||||
"Read",
|
||||
ev(Ev::Click, move |e| {
|
||||
e.stop_propagation();
|
||||
e.prevent_default();
|
||||
Msg::SetUnread(format!("thread:{tid}"), false)
|
||||
}),
|
||||
]
|
||||
]
|
||||
],
|
||||
td![C!["date"], datetime]
|
||||
@@ -160,7 +178,7 @@ fn view_search_results(
|
||||
});
|
||||
|
||||
div![
|
||||
search_toolbar(count, pager),
|
||||
search_toolbar(count, pager, show_bulk_edit),
|
||||
table![
|
||||
C![
|
||||
"table",
|
||||
@@ -171,53 +189,83 @@ fn view_search_results(
|
||||
"is-striped",
|
||||
],
|
||||
thead![tr![
|
||||
th![C!["edit"], ""],
|
||||
th![C!["from"], "From"],
|
||||
th![C!["subject"], "Subject"],
|
||||
th![C!["date"], "Date"]
|
||||
]],
|
||||
tbody![rows]
|
||||
],
|
||||
search_toolbar(count, pager)
|
||||
search_toolbar(count, pager, show_bulk_edit)
|
||||
]
|
||||
}
|
||||
|
||||
fn search_toolbar(count: usize, pager: &FrontPageQuerySearchPageInfo) -> Node<Msg> {
|
||||
fn search_toolbar(
|
||||
count: usize,
|
||||
pager: &FrontPageQuerySearchPageInfo,
|
||||
show_bulk_edit: bool,
|
||||
) -> Node<Msg> {
|
||||
let start = pager
|
||||
.start_cursor
|
||||
.as_ref()
|
||||
.map(|i| i.parse().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
nav![
|
||||
C!["pagination"],
|
||||
a![
|
||||
C![
|
||||
"pagination-previous",
|
||||
"button",
|
||||
//IF!(!pager.has_previous_page => "is-static"),
|
||||
],
|
||||
IF!(!pager.has_previous_page => attrs!{ At::Disabled=>true }),
|
||||
"<",
|
||||
IF!(pager.has_previous_page => ev(Ev::Click, |_| Msg::PreviousPage)),
|
||||
],
|
||||
a![
|
||||
C![
|
||||
"pagination-next",
|
||||
"button",
|
||||
//IF!(!pager.has_next_page => "is-static")
|
||||
],
|
||||
IF!(!pager.has_next_page => attrs!{ At::Disabled=>true }),
|
||||
">",
|
||||
IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage))
|
||||
],
|
||||
ul![
|
||||
C!["pagination-list"],
|
||||
li![format!(
|
||||
"{} - {} of {}",
|
||||
start,
|
||||
count.min(start + SEARCH_RESULTS_PER_PAGE),
|
||||
count
|
||||
)],
|
||||
C!["level"],
|
||||
div![
|
||||
C!["level-left"],
|
||||
IF!(show_bulk_edit =>
|
||||
span![
|
||||
C!["level-item", "buttons"],
|
||||
button![
|
||||
C!["button"],
|
||||
attrs!{At::Title => "Mark as read"},
|
||||
span![C!["icon", "is-small"], i![C!["far", "fa-envelope-open"]]],
|
||||
ev(Ev::Click, |_| Msg::SelectionMarkAsRead),
|
||||
],
|
||||
button![
|
||||
C!["button"],
|
||||
attrs!{At::Title => "Mark as unread"},
|
||||
span![C!["icon", "is-small"], i![C!["far", "fa-envelope"]]],
|
||||
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread),
|
||||
],
|
||||
]),
|
||||
],
|
||||
div![
|
||||
C!["level-right"],
|
||||
nav![
|
||||
C!["level-item", "pagination"],
|
||||
a![
|
||||
C![
|
||||
"pagination-previous",
|
||||
"button",
|
||||
//IF!(!pager.has_previous_page => "is-static"),
|
||||
],
|
||||
IF!(!pager.has_previous_page => attrs!{ At::Disabled=>true }),
|
||||
"<",
|
||||
IF!(pager.has_previous_page => ev(Ev::Click, |_| Msg::PreviousPage)),
|
||||
],
|
||||
a![
|
||||
C![
|
||||
"pagination-next",
|
||||
"button",
|
||||
//IF!(!pager.has_next_page => "is-static")
|
||||
],
|
||||
IF!(!pager.has_next_page => attrs!{ At::Disabled=>true }),
|
||||
">",
|
||||
IF!(pager.has_next_page => ev(Ev::Click, |_| Msg::NextPage))
|
||||
],
|
||||
ul![
|
||||
C!["pagination-list"],
|
||||
li![format!(
|
||||
"{} - {} of {}",
|
||||
start,
|
||||
count.min(start + SEARCH_RESULTS_PER_PAGE),
|
||||
count
|
||||
)],
|
||||
],
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -558,7 +606,6 @@ fn view_footer(render_time_ms: u128) -> Node<Msg> {
|
||||
// `view` describes what to display.
|
||||
pub fn view(model: &Model) -> Node<Msg> {
|
||||
let start = Instant::now();
|
||||
info!("refreshing {:?}", model.refreshing_state);
|
||||
let win = seed::window();
|
||||
let w = win
|
||||
.inner_width()
|
||||
|
||||
@@ -15,7 +15,8 @@ pub(super) fn view(model: &Model) -> Node<Msg> {
|
||||
results,
|
||||
count,
|
||||
pager,
|
||||
} => view_search_results(&query, results.as_slice(), *count, pager),
|
||||
selected_threads,
|
||||
} => view_search_results(&query, results.as_slice(), *count, pager, selected_threads),
|
||||
};
|
||||
div![
|
||||
C!["main-content"],
|
||||
|
||||
Reference in New Issue
Block a user