diff --git a/Cargo.lock b/Cargo.lock index 098209e6..2cdb7a79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -628,6 +628,7 @@ dependencies = [ "console_error_panic_hook", "derive_enum_iter", "derive_enum_primitive", + "derive_more", "dotenv", "futures", "js-sys", @@ -2232,10 +2233,10 @@ dependencies = [ "lettre 0.10.3", "lettre_email", "libc", - "log", "openssl-sys", "serde", "toml", + "tracing", "uuid 1.3.0", ] @@ -3193,7 +3194,6 @@ dependencies = [ "js-sys", "rand 0.8.5", "serde", - "serde-wasm-bindgen", "serde_json", "uuid 1.3.0", "version_check 0.9.4", @@ -3418,12 +3418,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "static_assertions" version = "1.1.0" @@ -4227,7 +4221,6 @@ dependencies = [ "cfg-if 0.1.10", "libc", "memory_units", - "spin", "winapi", ] diff --git a/crates/bitque-data/Cargo.toml b/crates/bitque-data/Cargo.toml index 303aaff5..de65538d 100644 --- a/crates/bitque-data/Cargo.toml +++ b/crates/bitque-data/Cargo.toml @@ -22,7 +22,6 @@ chrono = { version = "*", features = ["serde"] } diesel = { version = "2.0.3", features = ["postgres", "numeric", "uuid", "r2d2"], optional = true } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } derive_enum_primitive = { workspace = true, optional = true } -#diesel-derive-enum = { path = "../derive_enum_sql", features = ["postgres"] } diesel-derive-more = { version = "1.1.3" } diesel-derive-newtype = { version = "2.0.0-rc.0", optional = true } serde = { version = "*" } diff --git a/crates/bitque-server/Cargo.toml b/crates/bitque-server/Cargo.toml index eec4cc92..420be41f 100644 --- a/crates/bitque-server/Cargo.toml +++ b/crates/bitque-server/Cargo.toml @@ -32,7 +32,7 @@ serde = { version = "*", features = ["derive"] } serde_json = { version = ">=0.8.0, <2.0" } tokio = { version = "1", features = ["full"] } toml = { version = "0.7.3" } -tracing = "0" +tracing = { version = "0" } tracing-subscriber = { version = "0", features = ['env-filter', 'thread_local', 'serde_json'] } web-actor = { workspace = true, features = ["local-storage"] } websocket-actor = { workspace = true } diff --git a/crates/mail-actor/Cargo.toml b/crates/mail-actor/Cargo.toml index 67588b62..71148528 100644 --- a/crates/mail-actor/Cargo.toml +++ b/crates/mail-actor/Cargo.toml @@ -20,8 +20,8 @@ futures = { version = "*" } lettre = { version = "0.10.0-rc.3" } lettre_email = { version = "*" } libc = { version = "0.2.0", default-features = false } -log = { version = "*" } openssl-sys = { version = "*", features = ["vendored"] } serde = { version = "*" } toml = { version = "*" } uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] } +tracing = { version = "0" } diff --git a/crates/mail-actor/src/invite.rs b/crates/mail-actor/src/invite.rs index d4d32d9b..1684cb1f 100644 --- a/crates/mail-actor/src/invite.rs +++ b/crates/mail-actor/src/invite.rs @@ -53,12 +53,12 @@ impl Handler for MailExecutor { .header(ContentType::TEXT_HTML) .body(html) .map_err(|e| { - log::error!("{:?}", e); + tracing::error!("{:?}", e); MailError::MalformedBody })?; transport.send(&mail).map(|_| ()).map_err(|e| { - log::error!("Mailer: {}", e); + tracing::error!("Mailer: {}", e); MailError::FailedToSendEmail }) } diff --git a/crates/mail-actor/src/welcome.rs b/crates/mail-actor/src/welcome.rs index 2798020f..4dd09083 100644 --- a/crates/mail-actor/src/welcome.rs +++ b/crates/mail-actor/src/welcome.rs @@ -45,7 +45,7 @@ impl Handler for MailExecutor { bind_token = msg.bind_token, ); if cfg!(debug_assetrions) { - log::info!("Sending email:\n{}", html); + tracing::info!("Sending email:\n{}", html); } let mail = lettre::Message::builder() @@ -55,12 +55,12 @@ impl Handler for MailExecutor { .header(ContentType::TEXT_HTML) .body(html) .map_err(|e| { - log::error!("{:?}", e); + tracing::error!("{:?}", e); MailError::MalformedBody })?; transport.send(&mail).map(|_| ()).map_err(|e| { - log::error!("{:?}", e); + tracing::error!("{:?}", e); MailError::FailedToSendEmail }) } diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 624c8fc5..a8bc9b90 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -22,7 +22,7 @@ derive_enum_primitive = { workspace = true } dotenv = { version = "*" } futures = { version = "0.3.6" } js-sys = { version = "*", default-features = false } -seed = { version = "0", features = ['serde-wasm-bindgen'] } +seed = { version = "0", features = [] } serde = { version = "1", features = ['derive'] } serde_json = { version = "*" } tracing = { version = "0.1.37" } @@ -31,9 +31,10 @@ tracing-subscriber-wasm = { version = "0.1.0" } uuid = { version = "1.3.0", features = ["serde"] } wasm-bindgen = { version = "*", features = ["enable-interning"] } wasm-bindgen-futures = { version = "*" } -wee_alloc = { version = "*", features = ["static_array_backend"] } +wee_alloc = { version = "*", features = [] } wasm-sockets = { version = "1", features = [] } strum = { version = "*" } +derive_more = { version = "*" } [dependencies.web-sys] version = "*" diff --git a/crates/web/src/components/events.rs b/crates/web/src/components/events.rs index 3a7b3df9..2c24c63c 100644 --- a/crates/web/src/components/events.rs +++ b/crates/web/src/components/events.rs @@ -131,6 +131,8 @@ pub fn on_click_change_page(href: String) -> EvHandler { ev.stop_propagation(); if let Ok(url) = seed::Url::from_str(href.as_str()) { + blur_active(); + url.go_and_push(); return resolve_page(url).map(crate::Msg::ChangePage); } @@ -191,3 +193,9 @@ pub fn on_event_change_text_value( ) }) } + +pub fn blur_active() { + if let Some(el) = seed::document().active_element() { + seed::to_html_el(&el).blur().ok(); + } +} diff --git a/crates/web/src/components/mod.rs b/crates/web/src/components/mod.rs index 6047e750..e952cd18 100644 --- a/crates/web/src/components/mod.rs +++ b/crates/web/src/components/mod.rs @@ -1,4 +1,4 @@ -mod events; +pub mod events; pub mod styled_avatar; pub mod styled_button; pub mod styled_checkbox; diff --git a/crates/web/src/components/styled_textarea.rs b/crates/web/src/components/styled_textarea.rs index e3a7d08d..8267f3ef 100644 --- a/crates/web/src/components/styled_textarea.rs +++ b/crates/web/src/components/styled_textarea.rs @@ -13,6 +13,7 @@ pub struct StyledTextarea<'l> { pub update_event: Ev, pub placeholder: &'l str, pub disable_auto_resize: bool, + pub textarea_handlers: Vec>, } impl<'l> Default for StyledTextarea<'l> { @@ -26,11 +27,17 @@ impl<'l> Default for StyledTextarea<'l> { update_event: Ev::Change, placeholder: "", disable_auto_resize: false, + textarea_handlers: vec![], } } } impl<'l> StyledTextarea<'l> { + pub fn with_textarea_handler(mut self, h: EventHandler) -> Self { + self.textarea_handlers.push(h); + self + } + // height = `calc( (${$0.value.split("\n").length}px * ( 15 * 1.4285 )) + 17px + // 2px)` where: // * 15 is font-size @@ -47,6 +54,7 @@ impl<'l> StyledTextarea<'l> { update_event, placeholder, disable_auto_resize, + textarea_handlers, } = self; let id = id.expect("Text area requires FieldId"); let mut style_list = Vec::with_capacity(3); @@ -107,6 +115,7 @@ impl<'l> StyledTextarea<'l> { At::Rows => if disable_auto_resize { "5" } else { "auto" }, At::Data => height ], + textarea_handlers, value, // resize_handler, text_input_handler, diff --git a/crates/web/src/components/styled_tip.rs b/crates/web/src/components/styled_tip.rs index edd651ed..5858ea39 100644 --- a/crates/web/src/components/styled_tip.rs +++ b/crates/web/src/components/styled_tip.rs @@ -2,21 +2,22 @@ use seed::prelude::*; use seed::*; use crate::model::Model; +use crate::shared::keys::BrowserKey; use crate::{BuildMsg, Msg}; -pub fn styled_tip(letter: char, model: &Model, builder: B) -> Node +pub fn styled_tip(letter: BrowserKey, model: &Model, builder: B) -> Node where B: BuildMsg + 'static, { model .key_triggers .borrow_mut() - .insert(letter, Box::new(builder)); + .insert(letter.clone(), Box::new(builder)); div![ C!["proTip"], strong![C!["strong"], "Pro tip: "], "press ", - span![C!["tipLetter", letter.to_string()], letter.to_string()], + span![C!["tipLetter", format!("{letter}")], format!("{letter}")], " to comment" ] } diff --git a/crates/web/src/main.rs b/crates/web/src/main.rs index 2077cd14..9d64a4af 100644 --- a/crates/web/src/main.rs +++ b/crates/web/src/main.rs @@ -7,22 +7,21 @@ pub use components::*; pub use fields::*; pub use images::*; use seed::prelude::*; -use web_sys::File; +use tracing::info; +use web_sys::{File, KeyboardEvent}; use crate::components::styled_date_time_input::StyledDateTimeChanged; use crate::components::styled_rte::RteMsg; use crate::components::styled_select::StyledSelectChanged; -use crate::components::styled_tooltip; use crate::components::styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant}; use crate::modals::DebugMsg; use crate::model::{ModalType, Model, Page}; use crate::pages::issues_and_filters::IssuesAndFiltersMsg; use crate::pages::sign_in_page::SignInMsg; use crate::shared::go_to_login; +use crate::shared::keys::BrowserKey; use crate::ws::{flush_queue, open_socket, read_incoming, send_ws_msg}; -// use crate::shared::styled_rte::RteMsg; - mod changes; mod components; mod fields; @@ -35,8 +34,8 @@ mod shared; pub mod validations; mod ws; -// #[global_allocator] -// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; #[derive(Debug)] #[repr(C)] @@ -65,6 +64,10 @@ pub enum OperationKind { pub trait BuildMsg: std::fmt::Debug { fn build(&self) -> Msg; + + fn sender_allowed(&self, tag_name: Option<&str>) -> bool { + tag_name == Some("BODY") + } } #[derive(Debug)] @@ -155,7 +158,7 @@ pub enum Msg { ResourceChanged(ResourceKind, OperationKind, Option), } -fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { +fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { if model.ws.is_none() { open_socket(model, orders); } @@ -215,7 +218,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { }; if cfg!(debug_assertions) { - tracing::info!("msg {:?}", msg); + info!("msg {:?}", msg); } match &msg { @@ -232,13 +235,13 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { model.page = *page; } Msg::ToggleTooltip(variant) => match variant { - styled_tooltip::TooltipVariant::About => { + TooltipVariant::About => { model.about_tooltip_visible = !model.about_tooltip_visible; } - styled_tooltip::TooltipVariant::Messages => { + TooltipVariant::Messages => { model.messages_tooltip_visible = !model.messages_tooltip_visible; } - styled_tooltip::TooltipVariant::CodeBuilder => {} + TooltipVariant::CodeBuilder => {} TooltipVariant::TableBuilder => {} TooltipVariant::DateTimeBuilder => {} }, @@ -270,7 +273,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { } } -fn view(model: &model::Model) -> Node { +fn view(model: &Model) -> Node { model.key_triggers.borrow_mut().clear(); match model.page { @@ -326,9 +329,8 @@ fn resolve_page(url: Url) -> Option { Some(page) } -// #[wasm_bindgen(start)] pub fn main() { - let app = seed::App::start("app", init, update, view); + let app = App::start("app", init, update, view); console_error_panic_hook::set_once(); { use tracing_subscriber_wasm::MakeConsoleWriter; @@ -340,7 +342,7 @@ pub fn main() { }; #[cfg(debug_assertions)] - crate::shared::on_event::keydown(move |ev| { + shared::on_event::keydown(move |ev| { let app = app.clone(); let event = seed::to_keyboard_event(&ev); @@ -362,7 +364,7 @@ pub fn main() { }; }); - crate::shared::on_event::keydown(move |_ev| { + shared::on_event::keydown(move |_ev| { let element = match seed::document().active_element() { Some(el) => el, _ => return, @@ -374,7 +376,7 @@ pub fn main() { if element.get_attribute("rows").as_deref() != Some("auto") { return; } - crate::components::styled_textarea::handle_resize(&element); + styled_textarea::handle_resize(&element); }); } @@ -389,27 +391,25 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { let key_triggers = model.key_triggers.clone(); let sender_clone = sender.clone(); - crate::shared::on_event::keypress(move |ev| { + shared::on_event::keydown(move |ev: KeyboardEvent| { let sender = sender_clone.clone(); let key_triggers = key_triggers.clone(); let event = seed::to_keyboard_event(&ev); - if seed::document() - .active_element() - .map(|el| el.tag_name() != "BODY") - .unwrap_or(true) - { - return; - } - ev.prevent_default(); - ev.stop_propagation(); + let active = seed::document().active_element().map(|el| el.tag_name()); - let key: String = event.key(); - let t = key_triggers.borrow(); - if let Some(b) = key.chars().next().and_then(|c| t.get(&c)) { + let Ok(key) = event.key().parse::() else { + return; + }; + if let Some(b) = key_triggers.borrow().get(&key) { + if !b.sender_allowed(active.as_deref()) { + return; + } + ev.prevent_default(); + ev.stop_propagation(); let msg = b.build(); sender.clone()(Some(msg)); - } + }; }); { @@ -435,7 +435,7 @@ fn init(url: Url, orders: &mut impl Orders) -> Model { #[inline(always)] fn authorize_or_redirect(model: &mut Model, orders: &mut impl Orders) { let pathname = seed::document().location().unwrap().pathname().unwrap(); - match crate::shared::read_auth_token() { + match shared::read_auth_token() { Ok(token) => { send_ws_msg( WsMsgSession::AuthorizeLoad(token).into(), diff --git a/crates/web/src/modals/epics_delete/view.rs b/crates/web/src/modals/epics_delete/view.rs index cba33db6..ae8f8724 100644 --- a/crates/web/src/modals/epics_delete/view.rs +++ b/crates/web/src/modals/epics_delete/view.rs @@ -5,6 +5,7 @@ use crate::components::styled_button::*; use crate::components::styled_confirm_modal::*; use crate::components::styled_icon::*; use crate::components::styled_modal::*; +use crate::events::blur_active; use crate::modals::epics_delete::Model; use crate::{model, Msg}; @@ -55,6 +56,8 @@ fn warning(model: &model::Model, modal: &Model) -> Node { on_click: Some(mouse_ev(Ev::Click, move |ev| { ev.stop_propagation(); ev.prevent_default(); + blur_active(); + Msg::ModalDropped })), variant: ButtonVariant::Secondary, diff --git a/crates/web/src/modals/issues_create/update.rs b/crates/web/src/modals/issues_create/update.rs index 37cc6e44..9dceee99 100644 --- a/crates/web/src/modals/issues_create/update.rs +++ b/crates/web/src/modals/issues_create/update.rs @@ -67,7 +67,11 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde }; } - Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleCreated, _) => { + Msg::ResourceChanged( + ResourceKind::Issue | ResourceKind::Epic, + OperationKind::SingleCreated, + _, + ) => { orders.skip().send_msg(Msg::ModalDropped); } diff --git a/crates/web/src/modals/issues_create/view.rs b/crates/web/src/modals/issues_create/view.rs index e49b9da0..6dbb798d 100644 --- a/crates/web/src/modals/issues_create/view.rs +++ b/crates/web/src/modals/issues_create/view.rs @@ -17,10 +17,29 @@ use crate::components::styled_textarea::StyledTextarea; use crate::modals::epic_field; use crate::modals::issues_create::{events, Model as AddIssueModal, Type}; use crate::model::Model; +use crate::shared::keys::{BrowserKey, UiKey}; use crate::shared::validate::Validator; -use crate::{FieldId, Msg}; +use crate::{BuildMsg, FieldId, Msg}; + +#[derive(Debug)] +pub struct CloseCreateIssueModal; + +impl BuildMsg for CloseCreateIssueModal { + fn build(&self) -> Msg { + Msg::ModalDropped + } + + fn sender_allowed(&self, tag_name: Option<&str>) -> bool { + matches!(tag_name, Some("BODY") | Some("TEXTAREA")) + } +} pub fn view(model: &Model, modal: &AddIssueModal) -> Node { + model.key_triggers.borrow_mut().insert( + BrowserKey::UiKey(UiKey::Escape), + Box::new(CloseCreateIssueModal), + ); + let issue_type = modal .type_state .values diff --git a/crates/web/src/modals/issues_edit/events.rs b/crates/web/src/modals/issues_edit/events.rs index 7c8f6bed..a8439966 100644 --- a/crates/web/src/modals/issues_edit/events.rs +++ b/crates/web/src/modals/issues_edit/events.rs @@ -1,7 +1,7 @@ use bitque_data::IssueId; use seed::prelude::*; -pub type EvHandler = seed::EventHandler; +pub type EvHandler = EventHandler; pub fn on_click_close_modal() -> EvHandler { mouse_ev(Ev::Click, |ev| { diff --git a/crates/web/src/modals/issues_edit/view.rs b/crates/web/src/modals/issues_edit/view.rs index cb956f48..a6788c9b 100644 --- a/crates/web/src/modals/issues_edit/view.rs +++ b/crates/web/src/modals/issues_edit/view.rs @@ -21,27 +21,50 @@ use crate::modals::epic_field; use crate::modals::issues_edit::Model as EditIssueModal; use crate::modals::time_tracking::time_tracking_field; use crate::model::Model; +use crate::shared::keys::{BrowserKey, UiKey}; use crate::shared::tracking_widget::tracking_link; use crate::{BuildMsg, EditIssueModalSection, FieldChange, FieldId, Msg}; mod comments; +#[derive(Debug)] +pub struct CloseAddComment; + +impl BuildMsg for CloseAddComment { + fn build(&self) -> Msg { + Msg::ModalChanged(FieldChange::ToggleCommentForm( + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), + false, + )) + } + + fn sender_allowed(&self, tag_name: Option<&str>) -> bool { + matches!(tag_name, Some("TEXTAREA")) + } +} + #[inline(always)] pub fn view(model: &Model, modal: &EditIssueModal) -> Node { - model + let Some(_) = model .issues_by_id - .get(&modal.id) - .map(|_issue| { - StyledModal { - variant: ModalVariant::Center, - width: Some(1014), - with_icon: false, - children: vec![details(model, modal)], - class_list: "", - } - .render() - }) - .unwrap_or(Node::Empty) + .get(&modal.id) else { + return Node::Empty; + }; + + { + let mut b = model.key_triggers.borrow_mut(); + b.insert(BrowserKey::UiKey(UiKey::Escape), Box::new(CloseIssueModal)); + b.insert(BrowserKey::UiKey(UiKey::Escape), Box::new(CloseAddComment)); + } + + StyledModal { + variant: ModalVariant::Center, + width: Some(1014), + with_icon: false, + children: vec![details(model, modal)], + class_list: "", + } + .render() } #[inline(always)] @@ -179,7 +202,7 @@ fn type_select_option(t: IssueType, text: &str) -> StyledSelectOption<'_> { text: Some(text), icon: Some( StyledIcon { - icon: t.into(), + icon: Icon::from(t), class_list: name, ..Default::default() } @@ -259,7 +282,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { div![ C!["right"], create_comment, - styled_tip('m', model, EnableCommentBuilder) + styled_tip(BrowserKey::Character('m'), model, EnableCommentBuilder) ] ], comments @@ -272,6 +295,7 @@ pub struct EnableCommentBuilder; impl BuildMsg for EnableCommentBuilder { fn build(&self) -> Msg { + tracing::info!("{self:?}"); Msg::ModalChanged(FieldChange::ToggleCommentForm( FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), true, @@ -279,6 +303,16 @@ impl BuildMsg for EnableCommentBuilder { } } +#[derive(Debug)] +pub struct CloseIssueModal; + +impl BuildMsg for CloseIssueModal { + fn build(&self) -> Msg { + tracing::info!("{self:?}"); + Msg::ModalDropped + } +} + #[inline(always)] fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { let EditIssueModal { diff --git a/crates/web/src/modals/issues_edit/view/comments.rs b/crates/web/src/modals/issues_edit/view/comments.rs index fdedefc1..e0161749 100644 --- a/crates/web/src/modals/issues_edit/view/comments.rs +++ b/crates/web/src/modals/issues_edit/view/comments.rs @@ -31,6 +31,12 @@ pub fn build_comment_form(form: &CommentForm) -> Vec> { placeholder: "Add a comment...", ..Default::default() } + .with_textarea_handler(ev("blur", |_| { + Msg::ModalChanged(FieldChange::ToggleCommentForm( + FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), + false, + )) + })) .render(); let submit = StyledButton { diff --git a/crates/web/src/model.rs b/crates/web/src/model.rs index 163cf961..c6afcbae 100644 --- a/crates/web/src/model.rs +++ b/crates/web/src/model.rs @@ -18,6 +18,7 @@ use crate::pages::sign_in_page::model::SignInPage; use crate::pages::sign_up_page::model::SignUpPage; use crate::pages::users_page::model::UsersPage; use crate::{BuildMsg, Msg}; +use crate::shared::keys::BrowserKey; pub trait IssueModal { fn epic_id_value(&self) -> Option; @@ -276,7 +277,7 @@ pub struct Model { pub epic_ids: Vec, pub epics_by_id: HashMap, - pub key_triggers: std::rc::Rc>>>, + pub key_triggers: std::rc::Rc>>>, pub distinct_key_up: crate::shared::on_event::Distinct, pub show_extras: bool, } diff --git a/crates/web/src/pages/issues_and_filters/view/issue_info.rs b/crates/web/src/pages/issues_and_filters/view/issue_info.rs index 8ec0a881..0b57af43 100644 --- a/crates/web/src/pages/issues_and_filters/view/issue_info.rs +++ b/crates/web/src/pages/issues_and_filters/view/issue_info.rs @@ -4,6 +4,7 @@ use seed::*; use crate::components::styled_icon::*; use crate::components::styled_link::*; +use crate::events::blur_active; use crate::model::Model; use crate::Msg; @@ -136,6 +137,8 @@ fn issue_entry( mouse_ev("click", move |ev| { ev.stop_propagation(); ev.prevent_default(); + + blur_active(); Msg::SetActiveIssue(Some(id)) }) }; diff --git a/crates/web/src/pages/project_page/events.rs b/crates/web/src/pages/project_page/events.rs index adc6ad53..cc79ec48 100644 --- a/crates/web/src/pages/project_page/events.rs +++ b/crates/web/src/pages/project_page/events.rs @@ -1,6 +1,7 @@ use bitque_data::{EpicId, UserId}; use seed::prelude::*; +use crate::events::blur_active; use crate::model::Page; use crate::{AvatarFilterActive, BoardPageChange, Msg, PageChanged}; @@ -66,7 +67,9 @@ pub fn on_click_edit_issue(issue_id: i32) -> EvHandler { ev(Ev::Click, move |ev| { ev.prevent_default(); ev.stop_propagation(); - seed::Url::new() + blur_active(); + + Url::new() .add_path_part("issues") .add_path_part(format!("{}", issue_id)) .go_and_push(); diff --git a/crates/web/src/pages/project_page/update.rs b/crates/web/src/pages/project_page/update.rs index 99cc8dcc..d034d43b 100644 --- a/crates/web/src/pages/project_page/update.rs +++ b/crates/web/src/pages/project_page/update.rs @@ -11,7 +11,7 @@ use crate::{ BoardPageChange, EditIssueModalSection, FieldId, Msg, OperationKind, PageChanged, ResourceKind, }; -pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { +pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { if model.user.is_none() { return; } @@ -110,7 +110,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } Msg::DeleteIssue(issue_id) => { send_ws_msg( - bitque_data::WsMsg::Issue(WsMsgIssue::IssueDelete(issue_id)), + WsMsg::Issue(WsMsgIssue::IssueDelete(issue_id)), model.ws.as_ref(), orders, ); diff --git a/crates/web/src/pages/project_page/view.rs b/crates/web/src/pages/project_page/view.rs index 3c0fc637..d2c824e3 100644 --- a/crates/web/src/pages/project_page/view.rs +++ b/crates/web/src/pages/project_page/view.rs @@ -3,14 +3,32 @@ use seed::*; use crate::components::styled_button::StyledButton; use crate::components::styled_icon::{Icon, StyledIcon}; -use crate::model::Model; +use crate::events::blur_active; +use crate::model::{ModalType, Model}; use crate::shared::inner_layout; -use crate::Msg; +use crate::shared::keys::BrowserKey; +use crate::{BuildMsg, Msg}; mod board; mod filters; +#[derive(Debug)] +struct CreateIssueShortcut(i32); + +impl BuildMsg for CreateIssueShortcut { + fn build(&self) -> Msg { + Msg::ModalOpened(ModalType::AddIssue(None)) + } +} + pub fn view(model: &Model) -> Node { + { + model + .key_triggers + .borrow_mut() + .insert(BrowserKey::Character('c'), Box::new(CreateIssueShortcut(0))); + } + let project_section = [ breadcrumbs(model), header(model), @@ -36,11 +54,21 @@ fn header(model: &Model) -> Node { if !model.show_extras { return Node::Empty; } + + let on_click = mouse_ev("click", |_| { + blur_active(); + }); + div![ id!["projectBoardHeader"], div![id!["boardName"], C!["headerChild"], "Kanban board"], a![ - attrs![At::Href => "https://gitlab.com/adrian.wozniak/bitque", At::Target => "__blank", At::Rel => "noreferrer noopener"], + attrs![ + At::Href => "https://gitlab.com/adrian.wozniak/bitque", + At::Target => "__blank", + At::Rel => "noreferrer noopener" + ], + on_click, StyledButton::secondary_with_text_and_icon( "Repository", StyledIcon::from(Icon::Github).render(), diff --git a/crates/web/src/pages/reports_page/view.rs b/crates/web/src/pages/reports_page/view.rs index ff9849bc..aabfa285 100644 --- a/crates/web/src/pages/reports_page/view.rs +++ b/crates/web/src/pages/reports_page/view.rs @@ -136,7 +136,7 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node SVG_DRAWABLE_HEIGHT as f64 - height, // reverse draw origin At::Width => piece_width, At::Height => height, - At::Style => "fill: rgb(255, 0, 0);", + At::Style => "fill: #d04437;", At::Title => format!("Number of issues: {}", num_issues), ] ], diff --git a/crates/web/src/shared/aside.rs b/crates/web/src/shared/aside.rs index 99f11a28..7d7e730c 100644 --- a/crates/web/src/shared/aside.rs +++ b/crates/web/src/shared/aside.rs @@ -4,6 +4,7 @@ use seed::prelude::*; use seed::*; use crate::components::styled_icon::{Icon, StyledIcon}; +use crate::events::blur_active; use crate::model::{Model, Page}; use crate::shared::divider; use crate::ws::enqueue_ws_msg; @@ -105,7 +106,9 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option) mouse_ev("click", move |ev| { ev.stop_propagation(); ev.prevent_default(); - seed::Url::new() + + blur_active(); + Url::new() .set_path(p.to_path().split('/').filter(|s| !s.is_empty())) .go_and_push(); Msg::ChangePage(p) diff --git a/crates/web/src/shared/keys.rs b/crates/web/src/shared/keys.rs new file mode 100644 index 00000000..fb5abaaa --- /dev/null +++ b/crates/web/src/shared/keys.rs @@ -0,0 +1,204 @@ +use std::str::FromStr; + +use derive_more::Display; + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Display)] +pub enum UiKey { + Accept, + Again, + Attn, + Cancel, + ContextMenu, + Escape, + Execute, + Find, + Help, + Pause, + Play, + Props, + Select, + ZoomIn, + ZoomOut, +} + +impl FromStr for UiKey { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "Accept" => Self::Accept, + "Again" => Self::Again, + "Attn" => Self::Attn, + "Cancel" => Self::Cancel, + "ContextMenu" => Self::ContextMenu, + "Escape" => Self::Escape, + "Execute" => Self::Execute, + "Find" => Self::Find, + "Help" => Self::Help, + "Pause" => Self::Pause, + "Play" => Self::Play, + "Props" => Self::Props, + "Select" => Self::Select, + "ZoomIn" => Self::ZoomIn, + "ZoomOut" => Self::ZoomOut, + _ => return Err(()), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Display)] +pub enum DeviceKey { + BrightnessDown, + BrightnessUp, + Eject, + LogOff, + Power, + PowerOff, + PrintScreen, + Hibernate, + Standby, + WakeUp, +} + +impl FromStr for DeviceKey { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "BrightnessDown" => Self::BrightnessDown, + "BrightnessUp" => Self::BrightnessUp, + "Eject" => Self::Eject, + "LogOff" => Self::LogOff, + "Power" => Self::Power, + "PowerOff" => Self::PowerOff, + "PrintScreen" => Self::PrintScreen, + "Hibernate" => Self::Hibernate, + "Standby" => Self::Standby, + "WakeUp" => Self::WakeUp, + _ => return Err(()), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Display)] +pub enum IMEICompositionKey { + AllCandidates, + Alphanumeric, + CodeInput, + Compose, + Convert, + Dead, + FinalMode, + GroupFirst, + GroupLast, + GroupNext, + GroupPrevious, + ModeChange, + NextCandidate, + NonConvert, + PreviousCandidate, + Process, + SingleCandidate, +} + +impl FromStr for IMEICompositionKey { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "AllCandidates" => Self::AllCandidates, + "Alphanumeric" => Self::Alphanumeric, + "CodeInput" => Self::CodeInput, + "Compose" => Self::Compose, + "Convert" => Self::Convert, + "Dead" => Self::Dead, + "FinalMode" => Self::FinalMode, + "GroupFirst" => Self::GroupFirst, + "GroupLast" => Self::GroupLast, + "GroupNext" => Self::GroupNext, + "GroupPrevious" => Self::GroupPrevious, + "ModeChange" => Self::ModeChange, + "NextCandidate" => Self::NextCandidate, + "NonConvert" => Self::NonConvert, + "PreviousCandidate" => Self::PreviousCandidate, + "Process" => Self::Process, + "SingleCandidate" => Self::SingleCandidate, + _ => return Err(()), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Display)] +pub enum GeneralFunctionKey { + F1, + F2, + F3, + F4, + F5, + F6, + F7, + F8, + F9, + F10, + F11, + F12, + Soft1, + Soft2, + Soft3, + Soft4, +} + +impl FromStr for GeneralFunctionKey { + type Err = (); + + fn from_str(s: &str) -> Result { + Ok(match s { + "F1 " => Self::F1, + "F2" => Self::F2, + "F3 " => Self::F3, + "F4" => Self::F4, + "F5 " => Self::F5, + "F6" => Self::F6, + "F7 " => Self::F7, + "F8" => Self::F8, + "F9 " => Self::F9, + "F10" => Self::F10, + "F11 " => Self::F11, + "F12" => Self::F12, + "Soft1 " => Self::Soft1, + "Soft2" => Self::Soft2, + "Soft3 " => Self::Soft3, + "Soft4" => Self::Soft4, + _ => return Err(()), + }) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Clone, Display)] +pub enum BrowserKey { + UiKey(UiKey), + DeviceKey(DeviceKey), + IMEICompositionKey(IMEICompositionKey), + GeneralFunctionKey(GeneralFunctionKey), + Character(char), + Unknown(String), +} + +impl FromStr for BrowserKey { + type Err = (); + + fn from_str(s: &str) -> Result { + s.parse::() + .map(Self::UiKey) + .or_else(|_| s.parse().map(Self::DeviceKey)) + .or_else(|_| s.parse().map(Self::IMEICompositionKey)) + .or_else(|_| s.parse().map(Self::GeneralFunctionKey)) + .or_else(|_| { + Ok(if s.len() == 1 { + Self::Character(s.chars().next().unwrap()) + } else { + Self::Unknown(s.into()) + }) + }) + } +} diff --git a/crates/web/src/shared/mod.rs b/crates/web/src/shared/mod.rs index ae274db3..b07687c4 100644 --- a/crates/web/src/shared/mod.rs +++ b/crates/web/src/shared/mod.rs @@ -6,6 +6,7 @@ use crate::{resolve_page, Msg}; pub mod aside; pub mod drag; +pub mod keys; pub mod navbar_left; pub mod on_event; pub mod tracking_widget; diff --git a/crates/web/src/shared/navbar_left.rs b/crates/web/src/shared/navbar_left.rs index c307770b..c6c18b95 100644 --- a/crates/web/src/shared/navbar_left.rs +++ b/crates/web/src/shared/navbar_left.rs @@ -8,6 +8,7 @@ use crate::components::styled_button::{ButtonVariant, StyledButton}; use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_tooltip; use crate::components::styled_tooltip::{StyledTooltip, TooltipVariant}; +use crate::events::blur_active; use crate::model::Model; use crate::shared::divider; use crate::ws::send_ws_msg; @@ -106,10 +107,16 @@ pub fn render(model: &Model) -> Vec> { let go_to_profile = mouse_ev("click", move |ev| { ev.stop_propagation(); ev.prevent_default(); - seed::Url::new().add_path_part("profile").go_and_push(); + + blur_active(); + Url::new().add_path_part("profile").go_and_push(); Msg::ChangePage(Page::Profile) }); + let on_click_logo = mouse_ev("click", |_| { + blur_active(); + }); + vec![ about_tooltip_popup(model), messages_tooltip_popup(model), @@ -118,7 +125,8 @@ pub fn render(model: &Model) -> Vec> { a![ C!["logoLink"], attrs![At::Href => "/"], - div![C!["styledLogo"], logo_svg] + on_click_logo, + div![C!["styledLogo"], logo_svg], ], issue_nav, div![ @@ -151,6 +159,12 @@ fn navbar_left_item( ) -> Node { let styled_icon = icon.into_nav_item_icon(); + let on_click = on_click.unwrap_or_else(|| { + mouse_ev("click", |_| { + blur_active(); + }) + }); + a![ C!["item"], attrs![At::Href => href.unwrap_or("#")], @@ -205,13 +219,19 @@ fn message_ui(model: &Model, message: &Message) -> Option> { empty![] } else { let link_icon = StyledIcon::from(Icon::Link).render(); + + let on_click_hyper = mouse_ev("click", |_| { + blur_active(); + }); + div![ C!["hyperlink"], a![ C!["styledLink"], attrs![At::Href => hyper_link], + on_click_hyper, link_icon, - hyper_link + hyper_link, ] ] }; @@ -303,7 +323,9 @@ fn about_tooltip_popup(model: &Model) -> Node { .render(); let on_click = mouse_ev(Ev::Click, |_| { - Msg::ToggleTooltip(styled_tooltip::TooltipVariant::About) + blur_active(); + + Msg::ToggleTooltip(TooltipVariant::About) }); let body = div![ on_click, diff --git a/crates/web/src/shared/on_event.rs b/crates/web/src/shared/on_event.rs index fa191fec..0f11c674 100644 --- a/crates/web/src/shared/on_event.rs +++ b/crates/web/src/shared/on_event.rs @@ -124,19 +124,12 @@ where }); } -pub fn keypress(cb: Cb) -where - Cb: FnMut(web_sys::KeyboardEvent) + 'static, -{ - on("keypress", cb); -} - pub fn on(event: &str, cb: Cb) where Cb: FnMut(web_sys::KeyboardEvent) + 'static, { let handler = Closure::wrap(Box::new(cb) as Box); - seed::window() + document() .add_event_listener_with_callback(event, handler.as_ref().unchecked_ref()) .expect("Failed to mount global key handler"); handler.forget(); @@ -147,7 +140,7 @@ pub async fn wait_frame() -> f64 { let handler = Closure::wrap(Box::new(move |f| { let _ = sender.unbounded_send(f); }) as Box); - let _ = seed::window().request_animation_frame(handler.as_ref().unchecked_ref()); + let _ = window().request_animation_frame(handler.as_ref().unchecked_ref()); handler.forget(); receiver.next().await.unwrap_or_default() }