Compare commits

..

2 Commits

Author SHA1 Message Date
2b6cb6ec6e chore: Release
All checks were successful
Continuous integration / Check (push) Successful in 42s
Continuous integration / Test Suite (push) Successful in 44s
Continuous integration / Trunk (push) Successful in 39s
Continuous integration / Rustfmt (push) Successful in 58s
Continuous integration / build (push) Successful in 53s
Continuous integration / Disallow unused dependencies (push) Successful in 2m37s
2025-02-22 17:24:31 -08:00
0cba3a624c web: add de/select all checkbox with tristate 2025-02-22 17:24:18 -08:00
6 changed files with 72 additions and 49 deletions

10
Cargo.lock generated
View File

@ -2965,7 +2965,7 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-notmuch" name = "letterbox-notmuch"
version = "0.8.2" version = "0.8.3"
dependencies = [ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
"log", "log",
@ -2980,14 +2980,14 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-procmail2notmuch" name = "letterbox-procmail2notmuch"
version = "0.8.2" version = "0.8.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
] ]
[[package]] [[package]]
name = "letterbox-server" name = "letterbox-server"
version = "0.8.2" version = "0.8.3"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"anyhow", "anyhow",
@ -3030,7 +3030,7 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-shared" name = "letterbox-shared"
version = "0.8.2" version = "0.8.3"
dependencies = [ dependencies = [
"build-info", "build-info",
"letterbox-notmuch", "letterbox-notmuch",
@ -3039,7 +3039,7 @@ dependencies = [
[[package]] [[package]]
name = "letterbox-web" name = "letterbox-web"
version = "0.8.2" version = "0.8.3"
dependencies = [ dependencies = [
"build-info", "build-info",
"build-info-build", "build-info-build",

View File

@ -8,7 +8,7 @@ authors = ["Bill Thiede <git@xinu.tv>"]
edition = "2021" edition = "2021"
license = "UNLICENSED" license = "UNLICENSED"
publish = ["xinu"] publish = ["xinu"]
version = "0.8.2" version = "0.8.3"
repository = "https://git.z.xinu.tv/wathiede/letterbox" repository = "https://git.z.xinu.tv/wathiede/letterbox"
[profile.dev] [profile.dev]

View File

@ -48,8 +48,8 @@ urlencoding = "2.1.3"
#xtracing = { path = "../../xtracing" } #xtracing = { path = "../../xtracing" }
#xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" } #xtracing = { git = "http://git-private.h.xinu.tv/wathiede/xtracing.git" }
xtracing = { version = "0.3.0", registry = "xinu" } xtracing = { version = "0.3.0", registry = "xinu" }
letterbox-notmuch = { version = "0.8.2", path = "../notmuch", registry = "xinu" } letterbox-notmuch = { version = "0.8.3", path = "../notmuch", registry = "xinu" }
letterbox-shared = { version = "0.8.2", path = "../shared", registry = "xinu" } letterbox-shared = { version = "0.8.3", path = "../shared", registry = "xinu" }
[build-dependencies] [build-dependencies]
build-info-build = "0.0.39" build-info-build = "0.0.39"

View File

@ -12,5 +12,5 @@ version.workspace = true
[dependencies] [dependencies]
build-info = "0.0.39" build-info = "0.0.39"
letterbox-notmuch = { version = "0.8.2", path = "../notmuch", registry = "xinu" } letterbox-notmuch = { version = "0.8.3", path = "../notmuch", registry = "xinu" }
serde = { version = "1.0.147", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] }

View File

@ -33,8 +33,8 @@ wasm-bindgen = "=0.2.100"
uuid = { version = "1.13.1", features = [ uuid = { version = "1.13.1", features = [
"js", "js",
] } # direct dep to set js feature, prevents Rng issues ] } # direct dep to set js feature, prevents Rng issues
letterbox-shared = { version = "0.8.2", path = "../shared", registry = "xinu" } letterbox-shared = { version = "0.8.3", path = "../shared", registry = "xinu" }
letterbox-notmuch = { version = "0.8.2", path = "../notmuch", registry = "xinu" } letterbox-notmuch = { version = "0.8.3", path = "../notmuch", registry = "xinu" }
seed_hooks = { version = "0.4.0", registry = "xinu" } seed_hooks = { version = "0.4.0", registry = "xinu" }
[package.metadata.wasm-pack.profile.release] [package.metadata.wasm-pack.profile.release]

View File

@ -7,7 +7,7 @@ use letterbox_shared::compute_color;
use log::{debug, error, info}; use log::{debug, error, info};
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use seed_hooks::{state_access::CloneState, topo, use_state, StateAccessEventHandlers}; use seed_hooks::{state_access::CloneState, topo, use_state, StateAccessEventHandlers};
use web_sys::HtmlElement; use web_sys::{HtmlElement, HtmlInputElement};
use crate::{ use crate::{
api::urls, api::urls,
@ -60,6 +60,7 @@ mod tw_classes {
"accent-green-600", "accent-green-600",
"appearance-none", "appearance-none",
"checked:appearance-auto", "checked:appearance-auto",
"indeterminate:appearance-auto",
"rounded", "rounded",
"border", "border",
"border-neutral-500", "border-neutral-500",
@ -135,7 +136,6 @@ fn search_results(
tags.remove(idx); tags.remove(idx);
}; };
let is_unread = unread_idx.is_some(); let is_unread = unread_idx.is_some();
// TODO: add check-all button, and tristate indicator
div![ div![
C![ C![
"flex", "flex",
@ -189,11 +189,12 @@ fn search_results(
] ]
}); });
let show_bulk_edit = !selected_threads.is_empty(); let show_bulk_edit = !selected_threads.is_empty();
let all_selected = selected_threads.len() == results.len();
div![ div![
C!["flex", "flex-col", "flex-auto", "p-4"], C!["flex", "flex-col", "flex-auto", "p-4"],
search_toolbar(count, pager, show_bulk_edit), search_toolbar(count, pager, show_bulk_edit, all_selected),
div![rows], div![rows],
search_toolbar(count, pager, show_bulk_edit), search_toolbar(count, pager, show_bulk_edit, all_selected),
] ]
} }
@ -310,47 +311,71 @@ fn human_age(timestamp: i64) -> String {
datetime datetime
} }
#[topo::nested]
fn search_toolbar( fn search_toolbar(
count: usize, count: usize,
pager: &FrontPageQuerySearchPageInfo, pager: &FrontPageQuerySearchPageInfo,
show_bulk_edit: bool, show_bulk_edit: bool,
all_selected: bool,
) -> Node<Msg> { ) -> Node<Msg> {
let indeterminate = show_bulk_edit && !all_selected;
let tristate_el: ElRef<HtmlInputElement> = use_state(|| Default::default()).get();
let tri = el_ref(&tristate_el);
if let Some(tri) = tri.get() {
info!(
"setting tristate to {indeterminate}, current {}",
tri.indeterminate()
);
tri.set_indeterminate(indeterminate);
}
nav![ nav![
C!["p-4", "flex", "w-full", "justify-between"], C!["py-4", "flex", "w-full", "justify-between"],
div![ div![
C!["gap-2", "flex"], C!["gap-2", "flex", IF!(!show_bulk_edit => "invisible")],
IF!(show_bulk_edit =>
div![ div![
button![ C!["flex", "items-center", "mr-4"],
C![&tw_classes::BUTTON, "rounded-r-none"], input![
attrs!{At::Title => "Mark as read"}, tri,
span![i![C!["far", "fa-envelope-open"]]], C![&tw_classes::CHECKBOX],
span![C!["pl-2", "hidden", "md:inline"], "Read"], attrs! {
ev(Ev::Click, |_| Msg::SelectionMarkAsRead) At::Type=>"checkbox",
], At::Checked=>all_selected,
button![ }
C![&tw_classes::BUTTON, "rounded-l-none"], ],
attrs!{At::Title => "Mark as unread"}, ev(Ev::Input, move |_| {
span![i![C!["far", "fa-envelope"]]], if all_selected {
span![C!["pl-2", "hidden", "md:inline"], "Unread"], Msg::SelectionSetNone
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread) } else {
] Msg::SelectionSetAll
]), }
IF!(show_bulk_edit => }),
],
div![ div![
button![ button![
C![&tw_classes::BUTTON, "text-red-500"], C![&tw_classes::BUTTON, "rounded-r-none"],
attrs!{At::Title => "Mark as spam"}, attrs! {At::Title => "Mark as read"},
span![i![C!["far", "fa-hand"]]], span![i![C!["far", "fa-envelope-open"]]],
span![C!["pl-2", "hidden", "md:inline"], "Spam"], span![C!["pl-2", "hidden", "md:inline"], "Read"],
ev(Ev::Click, |_| ev(Ev::Click, |_| Msg::SelectionMarkAsRead)
Msg::MultiMsg(vec![ ],
Msg::SelectionAddTag("Spam".to_string()), button![
Msg::SelectionMarkAsRead C![&tw_classes::BUTTON, "rounded-l-none"],
]) attrs! {At::Title => "Mark as unread"},
) span![i![C!["far", "fa-envelope"]]],
span![C!["pl-2", "hidden", "md:inline"], "Unread"],
ev(Ev::Click, |_| Msg::SelectionMarkAsUnread)
] ]
]), ],
div![button![
C![&tw_classes::BUTTON, "text-red-500"],
attrs! {At::Title => "Mark as spam"},
span![i![C!["far", "fa-hand"]]],
span![C!["pl-2", "hidden", "md:inline"], "Spam"],
ev(Ev::Click, |_| Msg::MultiMsg(vec![
Msg::SelectionAddTag("Spam".to_string()),
Msg::SelectionMarkAsRead
]))
]],
], ],
div![ div![
C!["flex", "gap-2", "items-center"], C!["flex", "gap-2", "items-center"],
@ -877,6 +902,7 @@ fn view_header(
false false
}; };
let query = Url::decode_uri_component(query).unwrap_or("".to_string()); let query = Url::decode_uri_component(query).unwrap_or("".to_string());
nav![ nav![
C!["flex", "px-4", "pt-4", "overflow-hidden"], C!["flex", "px-4", "pt-4", "overflow-hidden"],
a![ a![
@ -1021,9 +1047,6 @@ pub fn tags(model: &Model) -> Node<Msg> {
} else { } else {
a.name.cmp(&b.name) a.name.cmp(&b.name)
}; };
if a.name.starts_with('@') || b.name.starts_with('@') {
info!("a {} < b {} = {r:?}", a.name, b.name,);
}
return r; return r;
}); });
let tags_open = use_state(|| false); let tags_open = use_state(|| false);