Add pagination to search results.
Move to shared definition of json requests between client/server.
This commit is contained in:
parent
f16860dd09
commit
eba362a7f2
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -1185,6 +1185,7 @@ dependencies = [
|
|||||||
"seed",
|
"seed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"shared",
|
||||||
"wasm-bindgen-test",
|
"wasm-bindgen-test",
|
||||||
"wasm-timer",
|
"wasm-timer",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@ -2321,6 +2322,7 @@ dependencies = [
|
|||||||
"rocket_cors",
|
"rocket_cors",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"shared",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@ -2355,6 +2357,14 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shared"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"notmuch",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
|||||||
@ -4,6 +4,7 @@ members = [
|
|||||||
"server",
|
"server",
|
||||||
"notmuch",
|
"notmuch",
|
||||||
"procmail2notmuch",
|
"procmail2notmuch",
|
||||||
|
"shared"
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|||||||
@ -478,8 +478,19 @@ impl Notmuch {
|
|||||||
self.run_notmuch(std::iter::empty::<&str>())
|
self.run_notmuch(std::iter::empty::<&str>())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search(&self, query: &str) -> Result<SearchSummary, NotmuchError> {
|
pub fn search(
|
||||||
let res = self.run_notmuch(["search", "--format=json", "--limit=20", query])?;
|
&self,
|
||||||
|
query: &str,
|
||||||
|
offset: usize,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<SearchSummary, NotmuchError> {
|
||||||
|
let res = self.run_notmuch([
|
||||||
|
"search",
|
||||||
|
"--format=json",
|
||||||
|
&format!("--offset={offset}"),
|
||||||
|
&format!("--limit={limit}"),
|
||||||
|
query,
|
||||||
|
])?;
|
||||||
Ok(serde_json::from_slice(&res)?)
|
Ok(serde_json::from_slice(&res)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ edition = "2021"
|
|||||||
rocket = { version = "0.5.0-rc.2", features = [ "json" ] }
|
rocket = { version = "0.5.0-rc.2", features = [ "json" ] }
|
||||||
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }
|
rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" }
|
||||||
notmuch = { path = "../notmuch" }
|
notmuch = { path = "../notmuch" }
|
||||||
|
shared = { path = "../shared" }
|
||||||
serde_json = "1.0.87"
|
serde_json = "1.0.87"
|
||||||
thiserror = "1.0.37"
|
thiserror = "1.0.37"
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
|
|||||||
@ -22,17 +22,30 @@ fn hello() -> &'static str {
|
|||||||
async fn refresh(nm: &State<Notmuch>) -> Result<Json<String>, Debug<NotmuchError>> {
|
async fn refresh(nm: &State<Notmuch>) -> Result<Json<String>, Debug<NotmuchError>> {
|
||||||
Ok(Json(String::from_utf8_lossy(&nm.new()?).to_string()))
|
Ok(Json(String::from_utf8_lossy(&nm.new()?).to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/search")]
|
#[get("/search")]
|
||||||
async fn search_all(nm: &State<Notmuch>) -> Result<Json<SearchSummary>, Debug<NotmuchError>> {
|
async fn search_all(
|
||||||
search(nm, "*").await
|
nm: &State<Notmuch>,
|
||||||
|
) -> Result<Json<shared::SearchResult>, Debug<NotmuchError>> {
|
||||||
|
search(nm, "*", None, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/search/<query>")]
|
#[get("/search/<query>?<page>&<results_per_page>")]
|
||||||
async fn search(
|
async fn search(
|
||||||
nm: &State<Notmuch>,
|
nm: &State<Notmuch>,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<Json<SearchSummary>, Debug<NotmuchError>> {
|
page: Option<usize>,
|
||||||
let res = nm.search(query)?;
|
results_per_page: Option<usize>,
|
||||||
|
) -> Result<Json<shared::SearchResult>, Debug<NotmuchError>> {
|
||||||
|
let page = page.unwrap_or(0);
|
||||||
|
let results_per_page = results_per_page.unwrap_or(10);
|
||||||
|
let res = shared::SearchResult {
|
||||||
|
summary: nm.search(query, page * results_per_page, results_per_page)?,
|
||||||
|
query: query.to_string(),
|
||||||
|
page,
|
||||||
|
results_per_page,
|
||||||
|
total: nm.count(query)?,
|
||||||
|
};
|
||||||
Ok(Json(res))
|
Ok(Json(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
shared/Cargo.toml
Normal file
10
shared/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "shared"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
notmuch = { path = "../notmuch" }
|
||||||
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
11
shared/src/lib.rs
Normal file
11
shared/src/lib.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use notmuch::SearchSummary;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct SearchResult {
|
||||||
|
pub summary: SearchSummary,
|
||||||
|
pub query: String,
|
||||||
|
pub page: usize,
|
||||||
|
pub results_per_page: usize,
|
||||||
|
pub total: usize,
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ seed = "0.9.2"
|
|||||||
console_log = {git = "http://git-private.h.xinu.tv/wathiede/console_log.git"}
|
console_log = {git = "http://git-private.h.xinu.tv/wathiede/console_log.git"}
|
||||||
serde = { version = "1.0.147", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
notmuch = {path = "../notmuch"}
|
notmuch = {path = "../notmuch"}
|
||||||
|
shared = {path = "../shared"}
|
||||||
itertools = "0.10.5"
|
itertools = "0.10.5"
|
||||||
serde_json = { version = "1.0.93", features = ["unbounded_depth"] }
|
serde_json = { version = "1.0.93", features = ["unbounded_depth"] }
|
||||||
wasm-timer = "0.2.5"
|
wasm-timer = "0.2.5"
|
||||||
|
|||||||
@ -37,6 +37,7 @@ iframe {
|
|||||||
}
|
}
|
||||||
.footer {
|
.footer {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
|
color: #222;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -71,6 +72,12 @@ iframe {
|
|||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
input, .input {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
input::placeholder, .input::placeholder{
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
117
web/src/lib.rs
117
web/src/lib.rs
@ -9,7 +9,7 @@ use std::{
|
|||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use log::{debug, error, info, warn, Level};
|
use log::{debug, error, info, warn, Level};
|
||||||
use notmuch::{Content, Part, SearchSummary, Thread, ThreadNode, ThreadSet};
|
use notmuch::{Content, Part, Thread, ThreadNode, ThreadSet};
|
||||||
use seed::{prelude::*, *};
|
use seed::{prelude::*, *};
|
||||||
use serde::de::Deserialize;
|
use serde::de::Deserialize;
|
||||||
use wasm_timer::Instant;
|
use wasm_timer::Instant;
|
||||||
@ -57,7 +57,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
|
|||||||
// ------ ------
|
// ------ ------
|
||||||
enum Context {
|
enum Context {
|
||||||
None,
|
None,
|
||||||
Search(SearchSummary),
|
Search(shared::SearchResult),
|
||||||
Thread(ThreadSet),
|
Thread(ThreadSet),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,11 +86,13 @@ enum Msg {
|
|||||||
RefreshStart,
|
RefreshStart,
|
||||||
RefreshDone(Option<FetchError>),
|
RefreshDone(Option<FetchError>),
|
||||||
SearchRequest(String),
|
SearchRequest(String),
|
||||||
SearchResult(fetch::Result<SearchSummary>),
|
SearchResult(fetch::Result<shared::SearchResult>),
|
||||||
ShowRequest(String),
|
ShowRequest(String),
|
||||||
ShowResult(fetch::Result<ThreadSet>),
|
ShowResult(fetch::Result<ThreadSet>),
|
||||||
ShowPrettyRequest(String),
|
ShowPrettyRequest(String),
|
||||||
ShowPrettyResult(fetch::Result<ThreadSet>),
|
ShowPrettyResult(fetch::Result<ThreadSet>),
|
||||||
|
NextPage,
|
||||||
|
PreviousPage,
|
||||||
}
|
}
|
||||||
|
|
||||||
// `update` describes how to handle each `Msg`.
|
// `update` describes how to handle each `Msg`.
|
||||||
@ -155,10 +157,38 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Msg::ShowPrettyResult(Err(fetch_error)) => {
|
Msg::ShowPrettyResult(Err(fetch_error)) => {
|
||||||
error!("fetch failed {:?}", fetch_error);
|
error!("fetch failed {:?}", fetch_error);
|
||||||
}
|
}
|
||||||
|
Msg::NextPage => {
|
||||||
|
match &model.context {
|
||||||
|
Context::Search(sr) => {
|
||||||
|
orders.send_msg(Msg::SearchRequest(format!(
|
||||||
|
"{}?page={}&results_per_page={}",
|
||||||
|
Url::encode_uri_component(&sr.query),
|
||||||
|
sr.page + 1,
|
||||||
|
sr.results_per_page
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Context::Thread(_) => (), // do nothing (yet?)
|
||||||
|
Context::None => (), // do nothing (yet?)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Msg::PreviousPage => {
|
||||||
|
match &model.context {
|
||||||
|
Context::Search(sr) => {
|
||||||
|
orders.send_msg(Msg::SearchRequest(format!(
|
||||||
|
"{}?page={}&results_per_page={}",
|
||||||
|
Url::encode_uri_component(&sr.query),
|
||||||
|
sr.page.saturating_sub(1),
|
||||||
|
sr.results_per_page
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Context::Thread(_) => (), // do nothing (yet?)
|
||||||
|
Context::None => (), // do nothing (yet?)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn search_request(query: &str) -> fetch::Result<SearchSummary> {
|
async fn search_request(query: &str) -> fetch::Result<shared::SearchResult> {
|
||||||
Request::new(api::search(query))
|
Request::new(api::search(query))
|
||||||
.method(Method::Get)
|
.method(Method::Get)
|
||||||
.fetch()
|
.fetch()
|
||||||
@ -402,13 +432,14 @@ fn pretty_authors(authors: &str) -> impl Iterator<Item = Node<Msg>> + '_ {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_mobile_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> {
|
fn view_mobile_search_results(query: &str, search_results: &shared::SearchResult) -> Node<Msg> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
set_title("all mail");
|
set_title("all mail");
|
||||||
} else {
|
} else {
|
||||||
set_title(query);
|
set_title(query);
|
||||||
}
|
}
|
||||||
let rows = search_results.0.iter().map(|r| {
|
let summaries = &search_results.summary.0;
|
||||||
|
let rows = summaries.iter().map(|r| {
|
||||||
/*
|
/*
|
||||||
let tid = r.thread.clone();
|
let tid = r.thread.clone();
|
||||||
tr![
|
tr![
|
||||||
@ -437,16 +468,22 @@ fn view_mobile_search_results(query: &str, search_results: &SearchSummary) -> No
|
|||||||
hr![],
|
hr![],
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
div![h1!["Search results"], rows]
|
let first = search_results.page * search_results.results_per_page;
|
||||||
|
div![
|
||||||
|
h1!["Search results"],
|
||||||
|
view_search_pager(first, summaries.len(), search_results.total),
|
||||||
|
rows
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg> {
|
fn view_search_results(query: &str, search_results: &shared::SearchResult) -> Node<Msg> {
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
set_title("all mail");
|
set_title("all mail");
|
||||||
} else {
|
} else {
|
||||||
set_title(query);
|
set_title(query);
|
||||||
}
|
}
|
||||||
let rows = search_results.0.iter().map(|r| {
|
let summaries = &search_results.summary.0;
|
||||||
|
let rows = summaries.iter().map(|r| {
|
||||||
let tid = r.thread.clone();
|
let tid = r.thread.clone();
|
||||||
tr![
|
tr![
|
||||||
td![
|
td![
|
||||||
@ -466,22 +503,46 @@ fn view_search_results(query: &str, search_results: &SearchSummary) -> Node<Msg>
|
|||||||
td![C!["date"], &r.date_relative]
|
td![C!["date"], &r.date_relative]
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
div![table![
|
let first = search_results.page * search_results.results_per_page;
|
||||||
C![
|
div![
|
||||||
"table",
|
view_search_pager(first, summaries.len(), search_results.total),
|
||||||
"index",
|
table![
|
||||||
"is-fullwidth",
|
C![
|
||||||
"is-hoverable",
|
"table",
|
||||||
"is-narrow",
|
"index",
|
||||||
"is-striped",
|
"is-fullwidth",
|
||||||
|
"is-hoverable",
|
||||||
|
"is-narrow",
|
||||||
|
"is-striped",
|
||||||
|
],
|
||||||
|
thead![tr![
|
||||||
|
th![C!["from"], "From"],
|
||||||
|
th![C!["subject"], "Subject"],
|
||||||
|
th![C!["date"], "Date"]
|
||||||
|
]],
|
||||||
|
tbody![rows]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_search_pager(page: usize, count: usize, total: usize) -> Node<Msg> {
|
||||||
|
nav![
|
||||||
|
C!["pagination"],
|
||||||
|
a![
|
||||||
|
C!["pagination-previous", "button"],
|
||||||
|
"<",
|
||||||
|
ev(Ev::Click, |_| Msg::PreviousPage)
|
||||||
],
|
],
|
||||||
thead![tr![
|
a![
|
||||||
th![C!["from"], "From"],
|
C!["pagination-next", "button"],
|
||||||
th![C!["subject"], "Subject"],
|
">",
|
||||||
th![C!["date"], "Date"]
|
ev(Ev::Click, |_| Msg::NextPage)
|
||||||
]],
|
],
|
||||||
tbody![rows]
|
ul![
|
||||||
]]
|
C!["pagination-list"],
|
||||||
|
li![format!("{} - {} of {}", page, page + count, total)],
|
||||||
|
],
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
|
fn view_thread(thread_set: &ThreadSet) -> Node<Msg> {
|
||||||
@ -541,7 +602,7 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
|||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
let query = query.to_string();
|
let query = Url::decode_uri_component(query).unwrap_or("".to_string());
|
||||||
nav![
|
nav![
|
||||||
C!["navbar"],
|
C!["navbar"],
|
||||||
attrs! {At::Role=>"navigation"},
|
attrs! {At::Role=>"navigation"},
|
||||||
@ -574,10 +635,12 @@ fn view_header(query: &str, refresh_request: &RefreshingState) -> Node<Msg> {
|
|||||||
At::AutoFocus => true.as_at_value();
|
At::AutoFocus => true.as_at_value();
|
||||||
At::Value => query,
|
At::Value => query,
|
||||||
},
|
},
|
||||||
input_ev(Ev::Input, Msg::SearchRequest),
|
input_ev(Ev::Input, |q| Msg::SearchRequest(
|
||||||
|
Url::encode_uri_component(q)
|
||||||
|
)),
|
||||||
// Resend search on enter.
|
// Resend search on enter.
|
||||||
keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
|
keyboard_ev(Ev::KeyUp, move |e| if e.key_code() == 0x0d {
|
||||||
Msg::SearchRequest(query)
|
Msg::SearchRequest(Url::encode_uri_component(query))
|
||||||
} else {
|
} else {
|
||||||
Msg::Noop
|
Msg::Noop
|
||||||
}),
|
}),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user