handle close Epic

This commit is contained in:
Adrian Woźniak 2023-04-05 15:03:00 +02:00
parent a8ff5101be
commit fc3052630a
29 changed files with 430 additions and 95 deletions

11
Cargo.lock generated
View File

@ -628,6 +628,7 @@ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"derive_enum_iter", "derive_enum_iter",
"derive_enum_primitive", "derive_enum_primitive",
"derive_more",
"dotenv", "dotenv",
"futures", "futures",
"js-sys", "js-sys",
@ -2232,10 +2233,10 @@ dependencies = [
"lettre 0.10.3", "lettre 0.10.3",
"lettre_email", "lettre_email",
"libc", "libc",
"log",
"openssl-sys", "openssl-sys",
"serde", "serde",
"toml", "toml",
"tracing",
"uuid 1.3.0", "uuid 1.3.0",
] ]
@ -3193,7 +3194,6 @@ dependencies = [
"js-sys", "js-sys",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde-wasm-bindgen",
"serde_json", "serde_json",
"uuid 1.3.0", "uuid 1.3.0",
"version_check 0.9.4", "version_check 0.9.4",
@ -3418,12 +3418,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "static_assertions" name = "static_assertions"
version = "1.1.0" version = "1.1.0"
@ -4227,7 +4221,6 @@ dependencies = [
"cfg-if 0.1.10", "cfg-if 0.1.10",
"libc", "libc",
"memory_units", "memory_units",
"spin",
"winapi", "winapi",
] ]

View File

@ -22,7 +22,6 @@ chrono = { version = "*", features = ["serde"] }
diesel = { version = "2.0.3", features = ["postgres", "numeric", "uuid", "r2d2"], optional = true } diesel = { version = "2.0.3", features = ["postgres", "numeric", "uuid", "r2d2"], optional = true }
diesel-derive-enum = { version = "2.0.1", features = ["postgres"] } diesel-derive-enum = { version = "2.0.1", features = ["postgres"] }
derive_enum_primitive = { workspace = true, optional = true } 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-more = { version = "1.1.3" }
diesel-derive-newtype = { version = "2.0.0-rc.0", optional = true } diesel-derive-newtype = { version = "2.0.0-rc.0", optional = true }
serde = { version = "*" } serde = { version = "*" }

View File

@ -32,7 +32,7 @@ serde = { version = "*", features = ["derive"] }
serde_json = { version = ">=0.8.0, <2.0" } serde_json = { version = ">=0.8.0, <2.0" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
toml = { version = "0.7.3" } toml = { version = "0.7.3" }
tracing = "0" tracing = { version = "0" }
tracing-subscriber = { version = "0", features = ['env-filter', 'thread_local', 'serde_json'] } tracing-subscriber = { version = "0", features = ['env-filter', 'thread_local', 'serde_json'] }
web-actor = { workspace = true, features = ["local-storage"] } web-actor = { workspace = true, features = ["local-storage"] }
websocket-actor = { workspace = true } websocket-actor = { workspace = true }

View File

@ -20,8 +20,8 @@ futures = { version = "*" }
lettre = { version = "0.10.0-rc.3" } lettre = { version = "0.10.0-rc.3" }
lettre_email = { version = "*" } lettre_email = { version = "*" }
libc = { version = "0.2.0", default-features = false } libc = { version = "0.2.0", default-features = false }
log = { version = "*" }
openssl-sys = { version = "*", features = ["vendored"] } openssl-sys = { version = "*", features = ["vendored"] }
serde = { version = "*" } serde = { version = "*" }
toml = { version = "*" } toml = { version = "*" }
uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] } uuid = { version = "1.3.0", features = ["serde", "v4", "v5"] }
tracing = { version = "0" }

View File

@ -53,12 +53,12 @@ impl Handler<Invite> for MailExecutor {
.header(ContentType::TEXT_HTML) .header(ContentType::TEXT_HTML)
.body(html) .body(html)
.map_err(|e| { .map_err(|e| {
log::error!("{:?}", e); tracing::error!("{:?}", e);
MailError::MalformedBody MailError::MalformedBody
})?; })?;
transport.send(&mail).map(|_| ()).map_err(|e| { transport.send(&mail).map(|_| ()).map_err(|e| {
log::error!("Mailer: {}", e); tracing::error!("Mailer: {}", e);
MailError::FailedToSendEmail MailError::FailedToSendEmail
}) })
} }

View File

@ -45,7 +45,7 @@ impl Handler<Welcome> for MailExecutor {
bind_token = msg.bind_token, bind_token = msg.bind_token,
); );
if cfg!(debug_assetrions) { if cfg!(debug_assetrions) {
log::info!("Sending email:\n{}", html); tracing::info!("Sending email:\n{}", html);
} }
let mail = lettre::Message::builder() let mail = lettre::Message::builder()
@ -55,12 +55,12 @@ impl Handler<Welcome> for MailExecutor {
.header(ContentType::TEXT_HTML) .header(ContentType::TEXT_HTML)
.body(html) .body(html)
.map_err(|e| { .map_err(|e| {
log::error!("{:?}", e); tracing::error!("{:?}", e);
MailError::MalformedBody MailError::MalformedBody
})?; })?;
transport.send(&mail).map(|_| ()).map_err(|e| { transport.send(&mail).map(|_| ()).map_err(|e| {
log::error!("{:?}", e); tracing::error!("{:?}", e);
MailError::FailedToSendEmail MailError::FailedToSendEmail
}) })
} }

View File

@ -22,7 +22,7 @@ derive_enum_primitive = { workspace = true }
dotenv = { version = "*" } dotenv = { version = "*" }
futures = { version = "0.3.6" } futures = { version = "0.3.6" }
js-sys = { version = "*", default-features = false } js-sys = { version = "*", default-features = false }
seed = { version = "0", features = ['serde-wasm-bindgen'] } seed = { version = "0", features = [] }
serde = { version = "1", features = ['derive'] } serde = { version = "1", features = ['derive'] }
serde_json = { version = "*" } serde_json = { version = "*" }
tracing = { version = "0.1.37" } tracing = { version = "0.1.37" }
@ -31,9 +31,10 @@ tracing-subscriber-wasm = { version = "0.1.0" }
uuid = { version = "1.3.0", features = ["serde"] } uuid = { version = "1.3.0", features = ["serde"] }
wasm-bindgen = { version = "*", features = ["enable-interning"] } wasm-bindgen = { version = "*", features = ["enable-interning"] }
wasm-bindgen-futures = { version = "*" } wasm-bindgen-futures = { version = "*" }
wee_alloc = { version = "*", features = ["static_array_backend"] } wee_alloc = { version = "*", features = [] }
wasm-sockets = { version = "1", features = [] } wasm-sockets = { version = "1", features = [] }
strum = { version = "*" } strum = { version = "*" }
derive_more = { version = "*" }
[dependencies.web-sys] [dependencies.web-sys]
version = "*" version = "*"

View File

@ -131,6 +131,8 @@ pub fn on_click_change_page(href: String) -> EvHandler {
ev.stop_propagation(); ev.stop_propagation();
if let Ok(url) = seed::Url::from_str(href.as_str()) { if let Ok(url) = seed::Url::from_str(href.as_str()) {
blur_active();
url.go_and_push(); url.go_and_push();
return resolve_page(url).map(crate::Msg::ChangePage); 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();
}
}

View File

@ -1,4 +1,4 @@
mod events; pub mod events;
pub mod styled_avatar; pub mod styled_avatar;
pub mod styled_button; pub mod styled_button;
pub mod styled_checkbox; pub mod styled_checkbox;

View File

@ -13,6 +13,7 @@ pub struct StyledTextarea<'l> {
pub update_event: Ev, pub update_event: Ev,
pub placeholder: &'l str, pub placeholder: &'l str,
pub disable_auto_resize: bool, pub disable_auto_resize: bool,
pub textarea_handlers: Vec<EventHandler<Msg>>,
} }
impl<'l> Default for StyledTextarea<'l> { impl<'l> Default for StyledTextarea<'l> {
@ -26,11 +27,17 @@ impl<'l> Default for StyledTextarea<'l> {
update_event: Ev::Change, update_event: Ev::Change,
placeholder: "", placeholder: "",
disable_auto_resize: false, disable_auto_resize: false,
textarea_handlers: vec![],
} }
} }
} }
impl<'l> StyledTextarea<'l> { impl<'l> StyledTextarea<'l> {
pub fn with_textarea_handler(mut self, h: EventHandler<Msg>) -> Self {
self.textarea_handlers.push(h);
self
}
// height = `calc( (${$0.value.split("\n").length}px * ( 15 * 1.4285 )) + 17px + // height = `calc( (${$0.value.split("\n").length}px * ( 15 * 1.4285 )) + 17px +
// 2px)` where: // 2px)` where:
// * 15 is font-size // * 15 is font-size
@ -47,6 +54,7 @@ impl<'l> StyledTextarea<'l> {
update_event, update_event,
placeholder, placeholder,
disable_auto_resize, disable_auto_resize,
textarea_handlers,
} = self; } = self;
let id = id.expect("Text area requires FieldId"); let id = id.expect("Text area requires FieldId");
let mut style_list = Vec::with_capacity(3); 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::Rows => if disable_auto_resize { "5" } else { "auto" },
At::Data => height At::Data => height
], ],
textarea_handlers,
value, value,
// resize_handler, // resize_handler,
text_input_handler, text_input_handler,

View File

@ -2,21 +2,22 @@ use seed::prelude::*;
use seed::*; use seed::*;
use crate::model::Model; use crate::model::Model;
use crate::shared::keys::BrowserKey;
use crate::{BuildMsg, Msg}; use crate::{BuildMsg, Msg};
pub fn styled_tip<B>(letter: char, model: &Model, builder: B) -> Node<Msg> pub fn styled_tip<B>(letter: BrowserKey, model: &Model, builder: B) -> Node<Msg>
where where
B: BuildMsg + 'static, B: BuildMsg + 'static,
{ {
model model
.key_triggers .key_triggers
.borrow_mut() .borrow_mut()
.insert(letter, Box::new(builder)); .insert(letter.clone(), Box::new(builder));
div![ div![
C!["proTip"], C!["proTip"],
strong![C!["strong"], "Pro tip: "], strong![C!["strong"], "Pro tip: "],
"press ", "press ",
span![C!["tipLetter", letter.to_string()], letter.to_string()], span![C!["tipLetter", format!("{letter}")], format!("{letter}")],
" to comment" " to comment"
] ]
} }

View File

@ -7,22 +7,21 @@ pub use components::*;
pub use fields::*; pub use fields::*;
pub use images::*; pub use images::*;
use seed::prelude::*; 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_date_time_input::StyledDateTimeChanged;
use crate::components::styled_rte::RteMsg; use crate::components::styled_rte::RteMsg;
use crate::components::styled_select::StyledSelectChanged; use crate::components::styled_select::StyledSelectChanged;
use crate::components::styled_tooltip;
use crate::components::styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant}; use crate::components::styled_tooltip::{TooltipVariant as StyledTooltip, TooltipVariant};
use crate::modals::DebugMsg; use crate::modals::DebugMsg;
use crate::model::{ModalType, Model, Page}; use crate::model::{ModalType, Model, Page};
use crate::pages::issues_and_filters::IssuesAndFiltersMsg; use crate::pages::issues_and_filters::IssuesAndFiltersMsg;
use crate::pages::sign_in_page::SignInMsg; use crate::pages::sign_in_page::SignInMsg;
use crate::shared::go_to_login; 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::ws::{flush_queue, open_socket, read_incoming, send_ws_msg};
// use crate::shared::styled_rte::RteMsg;
mod changes; mod changes;
mod components; mod components;
mod fields; mod fields;
@ -35,8 +34,8 @@ mod shared;
pub mod validations; pub mod validations;
mod ws; mod ws;
// #[global_allocator] #[global_allocator]
// static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[derive(Debug)] #[derive(Debug)]
#[repr(C)] #[repr(C)]
@ -65,6 +64,10 @@ pub enum OperationKind {
pub trait BuildMsg: std::fmt::Debug { pub trait BuildMsg: std::fmt::Debug {
fn build(&self) -> Msg; fn build(&self) -> Msg;
fn sender_allowed(&self, tag_name: Option<&str>) -> bool {
tag_name == Some("BODY")
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -155,7 +158,7 @@ pub enum Msg {
ResourceChanged(ResourceKind, OperationKind, Option<i32>), ResourceChanged(ResourceKind, OperationKind, Option<i32>),
} }
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
if model.ws.is_none() { if model.ws.is_none() {
open_socket(model, orders); open_socket(model, orders);
} }
@ -215,7 +218,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
}; };
if cfg!(debug_assertions) { if cfg!(debug_assertions) {
tracing::info!("msg {:?}", msg); info!("msg {:?}", msg);
} }
match &msg { match &msg {
@ -232,13 +235,13 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
model.page = *page; model.page = *page;
} }
Msg::ToggleTooltip(variant) => match variant { Msg::ToggleTooltip(variant) => match variant {
styled_tooltip::TooltipVariant::About => { TooltipVariant::About => {
model.about_tooltip_visible = !model.about_tooltip_visible; model.about_tooltip_visible = !model.about_tooltip_visible;
} }
styled_tooltip::TooltipVariant::Messages => { TooltipVariant::Messages => {
model.messages_tooltip_visible = !model.messages_tooltip_visible; model.messages_tooltip_visible = !model.messages_tooltip_visible;
} }
styled_tooltip::TooltipVariant::CodeBuilder => {} TooltipVariant::CodeBuilder => {}
TooltipVariant::TableBuilder => {} TooltipVariant::TableBuilder => {}
TooltipVariant::DateTimeBuilder => {} TooltipVariant::DateTimeBuilder => {}
}, },
@ -270,7 +273,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
} }
} }
fn view(model: &model::Model) -> Node<Msg> { fn view(model: &Model) -> Node<Msg> {
model.key_triggers.borrow_mut().clear(); model.key_triggers.borrow_mut().clear();
match model.page { match model.page {
@ -326,9 +329,8 @@ fn resolve_page(url: Url) -> Option<Page> {
Some(page) Some(page)
} }
// #[wasm_bindgen(start)]
pub fn main() { 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(); console_error_panic_hook::set_once();
{ {
use tracing_subscriber_wasm::MakeConsoleWriter; use tracing_subscriber_wasm::MakeConsoleWriter;
@ -340,7 +342,7 @@ pub fn main() {
}; };
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
crate::shared::on_event::keydown(move |ev| { shared::on_event::keydown(move |ev| {
let app = app.clone(); let app = app.clone();
let event = seed::to_keyboard_event(&ev); 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() { let element = match seed::document().active_element() {
Some(el) => el, Some(el) => el,
_ => return, _ => return,
@ -374,7 +376,7 @@ pub fn main() {
if element.get_attribute("rows").as_deref() != Some("auto") { if element.get_attribute("rows").as_deref() != Some("auto") {
return; 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<Msg>) -> Model {
let key_triggers = model.key_triggers.clone(); let key_triggers = model.key_triggers.clone();
let sender_clone = sender.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 sender = sender_clone.clone();
let key_triggers = key_triggers.clone(); let key_triggers = key_triggers.clone();
let event = seed::to_keyboard_event(&ev); let event = seed::to_keyboard_event(&ev);
if seed::document() let active = seed::document().active_element().map(|el| el.tag_name());
.active_element()
.map(|el| el.tag_name() != "BODY") let Ok(key) = event.key().parse::<BrowserKey>() else {
.unwrap_or(true) return;
{ };
if let Some(b) = key_triggers.borrow().get(&key) {
if !b.sender_allowed(active.as_deref()) {
return; return;
} }
ev.prevent_default(); ev.prevent_default();
ev.stop_propagation(); ev.stop_propagation();
let key: String = event.key();
let t = key_triggers.borrow();
if let Some(b) = key.chars().next().and_then(|c| t.get(&c)) {
let msg = b.build(); let msg = b.build();
sender.clone()(Some(msg)); sender.clone()(Some(msg));
} };
}); });
{ {
@ -435,7 +435,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
#[inline(always)] #[inline(always)]
fn authorize_or_redirect(model: &mut Model, orders: &mut impl Orders<Msg>) { fn authorize_or_redirect(model: &mut Model, orders: &mut impl Orders<Msg>) {
let pathname = seed::document().location().unwrap().pathname().unwrap(); let pathname = seed::document().location().unwrap().pathname().unwrap();
match crate::shared::read_auth_token() { match shared::read_auth_token() {
Ok(token) => { Ok(token) => {
send_ws_msg( send_ws_msg(
WsMsgSession::AuthorizeLoad(token).into(), WsMsgSession::AuthorizeLoad(token).into(),

View File

@ -5,6 +5,7 @@ use crate::components::styled_button::*;
use crate::components::styled_confirm_modal::*; use crate::components::styled_confirm_modal::*;
use crate::components::styled_icon::*; use crate::components::styled_icon::*;
use crate::components::styled_modal::*; use crate::components::styled_modal::*;
use crate::events::blur_active;
use crate::modals::epics_delete::Model; use crate::modals::epics_delete::Model;
use crate::{model, Msg}; use crate::{model, Msg};
@ -55,6 +56,8 @@ fn warning(model: &model::Model, modal: &Model) -> Node<Msg> {
on_click: Some(mouse_ev(Ev::Click, move |ev| { on_click: Some(mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
blur_active();
Msg::ModalDropped Msg::ModalDropped
})), })),
variant: ButtonVariant::Secondary, variant: ButtonVariant::Secondary,

View File

@ -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); orders.skip().send_msg(Msg::ModalDropped);
} }

View File

@ -17,10 +17,29 @@ use crate::components::styled_textarea::StyledTextarea;
use crate::modals::epic_field; use crate::modals::epic_field;
use crate::modals::issues_create::{events, Model as AddIssueModal, Type}; use crate::modals::issues_create::{events, Model as AddIssueModal, Type};
use crate::model::Model; use crate::model::Model;
use crate::shared::keys::{BrowserKey, UiKey};
use crate::shared::validate::Validator; 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<Msg> { pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
model.key_triggers.borrow_mut().insert(
BrowserKey::UiKey(UiKey::Escape),
Box::new(CloseCreateIssueModal),
);
let issue_type = modal let issue_type = modal
.type_state .type_state
.values .values

View File

@ -1,7 +1,7 @@
use bitque_data::IssueId; use bitque_data::IssueId;
use seed::prelude::*; use seed::prelude::*;
pub type EvHandler = seed::EventHandler<crate::Msg>; pub type EvHandler = EventHandler<crate::Msg>;
pub fn on_click_close_modal() -> EvHandler { pub fn on_click_close_modal() -> EvHandler {
mouse_ev(Ev::Click, |ev| { mouse_ev(Ev::Click, |ev| {

View File

@ -21,17 +21,42 @@ use crate::modals::epic_field;
use crate::modals::issues_edit::Model as EditIssueModal; use crate::modals::issues_edit::Model as EditIssueModal;
use crate::modals::time_tracking::time_tracking_field; use crate::modals::time_tracking::time_tracking_field;
use crate::model::Model; use crate::model::Model;
use crate::shared::keys::{BrowserKey, UiKey};
use crate::shared::tracking_widget::tracking_link; use crate::shared::tracking_widget::tracking_link;
use crate::{BuildMsg, EditIssueModalSection, FieldChange, FieldId, Msg}; use crate::{BuildMsg, EditIssueModalSection, FieldChange, FieldId, Msg};
mod comments; 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)] #[inline(always)]
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> { pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
model let Some(_) = model
.issues_by_id .issues_by_id
.get(&modal.id) .get(&modal.id) else {
.map(|_issue| { 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 { StyledModal {
variant: ModalVariant::Center, variant: ModalVariant::Center,
width: Some(1014), width: Some(1014),
@ -40,8 +65,6 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
class_list: "", class_list: "",
} }
.render() .render()
})
.unwrap_or(Node::Empty)
} }
#[inline(always)] #[inline(always)]
@ -179,7 +202,7 @@ fn type_select_option(t: IssueType, text: &str) -> StyledSelectOption<'_> {
text: Some(text), text: Some(text),
icon: Some( icon: Some(
StyledIcon { StyledIcon {
icon: t.into(), icon: Icon::from(t),
class_list: name, class_list: name,
..Default::default() ..Default::default()
} }
@ -259,7 +282,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
div![ div![
C!["right"], C!["right"],
create_comment, create_comment,
styled_tip('m', model, EnableCommentBuilder) styled_tip(BrowserKey::Character('m'), model, EnableCommentBuilder)
] ]
], ],
comments comments
@ -272,6 +295,7 @@ pub struct EnableCommentBuilder;
impl BuildMsg for EnableCommentBuilder { impl BuildMsg for EnableCommentBuilder {
fn build(&self) -> Msg { fn build(&self) -> Msg {
tracing::info!("{self:?}");
Msg::ModalChanged(FieldChange::ToggleCommentForm( Msg::ModalChanged(FieldChange::ToggleCommentForm(
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)), FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
true, 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)] #[inline(always)]
fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> { fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal { let EditIssueModal {

View File

@ -31,6 +31,12 @@ pub fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
placeholder: "Add a comment...", placeholder: "Add a comment...",
..Default::default() ..Default::default()
} }
.with_textarea_handler(ev("blur", |_| {
Msg::ModalChanged(FieldChange::ToggleCommentForm(
FieldId::EditIssueModal(EditIssueModalSection::Comment(CommentFieldId::Body)),
false,
))
}))
.render(); .render();
let submit = StyledButton { let submit = StyledButton {

View File

@ -18,6 +18,7 @@ use crate::pages::sign_in_page::model::SignInPage;
use crate::pages::sign_up_page::model::SignUpPage; use crate::pages::sign_up_page::model::SignUpPage;
use crate::pages::users_page::model::UsersPage; use crate::pages::users_page::model::UsersPage;
use crate::{BuildMsg, Msg}; use crate::{BuildMsg, Msg};
use crate::shared::keys::BrowserKey;
pub trait IssueModal { pub trait IssueModal {
fn epic_id_value(&self) -> Option<u32>; fn epic_id_value(&self) -> Option<u32>;
@ -276,7 +277,7 @@ pub struct Model {
pub epic_ids: Vec<EpicId>, pub epic_ids: Vec<EpicId>,
pub epics_by_id: HashMap<EpicId, Epic>, pub epics_by_id: HashMap<EpicId, Epic>,
pub key_triggers: std::rc::Rc<std::cell::RefCell<HashMap<char, Box<dyn BuildMsg>>>>, pub key_triggers: std::rc::Rc<std::cell::RefCell<HashMap<BrowserKey, Box<dyn BuildMsg>>>>,
pub distinct_key_up: crate::shared::on_event::Distinct, pub distinct_key_up: crate::shared::on_event::Distinct,
pub show_extras: bool, pub show_extras: bool,
} }

View File

@ -4,6 +4,7 @@ use seed::*;
use crate::components::styled_icon::*; use crate::components::styled_icon::*;
use crate::components::styled_link::*; use crate::components::styled_link::*;
use crate::events::blur_active;
use crate::model::Model; use crate::model::Model;
use crate::Msg; use crate::Msg;
@ -136,6 +137,8 @@ fn issue_entry(
mouse_ev("click", move |ev| { mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
blur_active();
Msg::SetActiveIssue(Some(id)) Msg::SetActiveIssue(Some(id))
}) })
}; };

View File

@ -1,6 +1,7 @@
use bitque_data::{EpicId, UserId}; use bitque_data::{EpicId, UserId};
use seed::prelude::*; use seed::prelude::*;
use crate::events::blur_active;
use crate::model::Page; use crate::model::Page;
use crate::{AvatarFilterActive, BoardPageChange, Msg, PageChanged}; 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(Ev::Click, move |ev| {
ev.prevent_default(); ev.prevent_default();
ev.stop_propagation(); ev.stop_propagation();
seed::Url::new() blur_active();
Url::new()
.add_path_part("issues") .add_path_part("issues")
.add_path_part(format!("{}", issue_id)) .add_path_part(format!("{}", issue_id))
.go_and_push(); .go_and_push();

View File

@ -11,7 +11,7 @@ use crate::{
BoardPageChange, EditIssueModalSection, FieldId, Msg, OperationKind, PageChanged, ResourceKind, BoardPageChange, EditIssueModalSection, FieldId, Msg, OperationKind, PageChanged, ResourceKind,
}; };
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
if model.user.is_none() { if model.user.is_none() {
return; return;
} }
@ -110,7 +110,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
} }
Msg::DeleteIssue(issue_id) => { Msg::DeleteIssue(issue_id) => {
send_ws_msg( send_ws_msg(
bitque_data::WsMsg::Issue(WsMsgIssue::IssueDelete(issue_id)), WsMsg::Issue(WsMsgIssue::IssueDelete(issue_id)),
model.ws.as_ref(), model.ws.as_ref(),
orders, orders,
); );

View File

@ -3,14 +3,32 @@ use seed::*;
use crate::components::styled_button::StyledButton; use crate::components::styled_button::StyledButton;
use crate::components::styled_icon::{Icon, StyledIcon}; 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::shared::inner_layout;
use crate::Msg; use crate::shared::keys::BrowserKey;
use crate::{BuildMsg, Msg};
mod board; mod board;
mod filters; 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<Msg> { pub fn view(model: &Model) -> Node<Msg> {
{
model
.key_triggers
.borrow_mut()
.insert(BrowserKey::Character('c'), Box::new(CreateIssueShortcut(0)));
}
let project_section = [ let project_section = [
breadcrumbs(model), breadcrumbs(model),
header(model), header(model),
@ -36,11 +54,21 @@ fn header(model: &Model) -> Node<Msg> {
if !model.show_extras { if !model.show_extras {
return Node::Empty; return Node::Empty;
} }
let on_click = mouse_ev("click", |_| {
blur_active();
});
div![ div![
id!["projectBoardHeader"], id!["projectBoardHeader"],
div![id!["boardName"], C!["headerChild"], "Kanban board"], div![id!["boardName"], C!["headerChild"], "Kanban board"],
a![ 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( StyledButton::secondary_with_text_and_icon(
"Repository", "Repository",
StyledIcon::from(Icon::Github).render(), StyledIcon::from(Icon::Github).render(),

View File

@ -136,7 +136,7 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<M
At::Y => SVG_DRAWABLE_HEIGHT as f64 - height, // reverse draw origin At::Y => SVG_DRAWABLE_HEIGHT as f64 - height, // reverse draw origin
At::Width => piece_width, At::Width => piece_width,
At::Height => height, At::Height => height,
At::Style => "fill: rgb(255, 0, 0);", At::Style => "fill: #d04437;",
At::Title => format!("Number of issues: {}", num_issues), At::Title => format!("Number of issues: {}", num_issues),
] ]
], ],

View File

@ -4,6 +4,7 @@ use seed::prelude::*;
use seed::*; use seed::*;
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::events::blur_active;
use crate::model::{Model, Page}; use crate::model::{Model, Page};
use crate::shared::divider; use crate::shared::divider;
use crate::ws::enqueue_ws_msg; use crate::ws::enqueue_ws_msg;
@ -105,7 +106,9 @@ fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>)
mouse_ev("click", move |ev| { mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); ev.prevent_default();
seed::Url::new()
blur_active();
Url::new()
.set_path(p.to_path().split('/').filter(|s| !s.is_empty())) .set_path(p.to_path().split('/').filter(|s| !s.is_empty()))
.go_and_push(); .go_and_push();
Msg::ChangePage(p) Msg::ChangePage(p)

View File

@ -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<Self, Self::Err> {
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<Self, Self::Err> {
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<Self, Self::Err> {
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<Self, Self::Err> {
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<Self, Self::Err> {
s.parse::<UiKey>()
.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())
})
})
}
}

View File

@ -6,6 +6,7 @@ use crate::{resolve_page, Msg};
pub mod aside; pub mod aside;
pub mod drag; pub mod drag;
pub mod keys;
pub mod navbar_left; pub mod navbar_left;
pub mod on_event; pub mod on_event;
pub mod tracking_widget; pub mod tracking_widget;

View File

@ -8,6 +8,7 @@ use crate::components::styled_button::{ButtonVariant, StyledButton};
use crate::components::styled_icon::{Icon, StyledIcon}; use crate::components::styled_icon::{Icon, StyledIcon};
use crate::components::styled_tooltip; use crate::components::styled_tooltip;
use crate::components::styled_tooltip::{StyledTooltip, TooltipVariant}; use crate::components::styled_tooltip::{StyledTooltip, TooltipVariant};
use crate::events::blur_active;
use crate::model::Model; use crate::model::Model;
use crate::shared::divider; use crate::shared::divider;
use crate::ws::send_ws_msg; use crate::ws::send_ws_msg;
@ -106,10 +107,16 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
let go_to_profile = mouse_ev("click", move |ev| { let go_to_profile = mouse_ev("click", move |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.prevent_default(); 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) Msg::ChangePage(Page::Profile)
}); });
let on_click_logo = mouse_ev("click", |_| {
blur_active();
});
vec![ vec![
about_tooltip_popup(model), about_tooltip_popup(model),
messages_tooltip_popup(model), messages_tooltip_popup(model),
@ -118,7 +125,8 @@ pub fn render(model: &Model) -> Vec<Node<Msg>> {
a![ a![
C!["logoLink"], C!["logoLink"],
attrs![At::Href => "/"], attrs![At::Href => "/"],
div![C!["styledLogo"], logo_svg] on_click_logo,
div![C!["styledLogo"], logo_svg],
], ],
issue_nav, issue_nav,
div![ div![
@ -151,6 +159,12 @@ fn navbar_left_item(
) -> Node<Msg> { ) -> Node<Msg> {
let styled_icon = icon.into_nav_item_icon(); let styled_icon = icon.into_nav_item_icon();
let on_click = on_click.unwrap_or_else(|| {
mouse_ev("click", |_| {
blur_active();
})
});
a![ a![
C!["item"], C!["item"],
attrs![At::Href => href.unwrap_or("#")], attrs![At::Href => href.unwrap_or("#")],
@ -205,13 +219,19 @@ fn message_ui(model: &Model, message: &Message) -> Option<Node<Msg>> {
empty![] empty![]
} else { } else {
let link_icon = StyledIcon::from(Icon::Link).render(); let link_icon = StyledIcon::from(Icon::Link).render();
let on_click_hyper = mouse_ev("click", |_| {
blur_active();
});
div![ div![
C!["hyperlink"], C!["hyperlink"],
a![ a![
C!["styledLink"], C!["styledLink"],
attrs![At::Href => hyper_link], attrs![At::Href => hyper_link],
on_click_hyper,
link_icon, link_icon,
hyper_link hyper_link,
] ]
] ]
}; };
@ -303,7 +323,9 @@ fn about_tooltip_popup(model: &Model) -> Node<Msg> {
.render(); .render();
let on_click = mouse_ev(Ev::Click, |_| { let on_click = mouse_ev(Ev::Click, |_| {
Msg::ToggleTooltip(styled_tooltip::TooltipVariant::About) blur_active();
Msg::ToggleTooltip(TooltipVariant::About)
}); });
let body = div![ let body = div![
on_click, on_click,

View File

@ -124,19 +124,12 @@ where
}); });
} }
pub fn keypress<Cb>(cb: Cb)
where
Cb: FnMut(web_sys::KeyboardEvent) + 'static,
{
on("keypress", cb);
}
pub fn on<Cb>(event: &str, cb: Cb) pub fn on<Cb>(event: &str, cb: Cb)
where where
Cb: FnMut(web_sys::KeyboardEvent) + 'static, Cb: FnMut(web_sys::KeyboardEvent) + 'static,
{ {
let handler = Closure::wrap(Box::new(cb) as Box<dyn FnMut(_)>); let handler = Closure::wrap(Box::new(cb) as Box<dyn FnMut(_)>);
seed::window() document()
.add_event_listener_with_callback(event, handler.as_ref().unchecked_ref()) .add_event_listener_with_callback(event, handler.as_ref().unchecked_ref())
.expect("Failed to mount global key handler"); .expect("Failed to mount global key handler");
handler.forget(); handler.forget();
@ -147,7 +140,7 @@ pub async fn wait_frame() -> f64 {
let handler = Closure::wrap(Box::new(move |f| { let handler = Closure::wrap(Box::new(move |f| {
let _ = sender.unbounded_send(f); let _ = sender.unbounded_send(f);
}) as Box<dyn FnMut(f64)>); }) as Box<dyn FnMut(f64)>);
let _ = seed::window().request_animation_frame(handler.as_ref().unchecked_ref()); let _ = window().request_animation_frame(handler.as_ref().unchecked_ref());
handler.forget(); handler.forget();
receiver.next().await.unwrap_or_default() receiver.next().await.unwrap_or_default()
} }