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",
"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",
]

View File

@ -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 = "*" }

View File

@ -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 }

View File

@ -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" }

View File

@ -53,12 +53,12 @@ impl Handler<Invite> 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
})
}

View File

@ -45,7 +45,7 @@ impl Handler<Welcome> 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<Welcome> 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
})
}

View File

@ -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 = "*"

View File

@ -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();
}
}

View File

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

View File

@ -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<EventHandler<Msg>>,
}
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<Msg>) -> 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,

View File

@ -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<B>(letter: char, model: &Model, builder: B) -> Node<Msg>
pub fn styled_tip<B>(letter: BrowserKey, model: &Model, builder: B) -> Node<Msg>
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"
]
}

View File

@ -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<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() {
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) {
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<Msg>) {
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<Msg>) {
}
}
fn view(model: &model::Model) -> Node<Msg> {
fn view(model: &Model) -> Node<Msg> {
model.key_triggers.borrow_mut().clear();
match model.page {
@ -326,9 +329,8 @@ fn resolve_page(url: Url) -> Option<Page> {
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<Msg>) -> 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::<BrowserKey>() 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<Msg>) -> Model {
#[inline(always)]
fn authorize_or_redirect(model: &mut Model, orders: &mut impl Orders<Msg>) {
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(),

View File

@ -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<Msg> {
on_click: Some(mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
ev.prevent_default();
blur_active();
Msg::ModalDropped
})),
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);
}

View File

@ -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<Msg> {
model.key_triggers.borrow_mut().insert(
BrowserKey::UiKey(UiKey::Escape),
Box::new(CloseCreateIssueModal),
);
let issue_type = modal
.type_state
.values

View File

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

View File

@ -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<Msg> {
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<Msg> {
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<Msg> {
let EditIssueModal {

View File

@ -31,6 +31,12 @@ pub fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
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 {

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::users_page::model::UsersPage;
use crate::{BuildMsg, Msg};
use crate::shared::keys::BrowserKey;
pub trait IssueModal {
fn epic_id_value(&self) -> Option<u32>;
@ -276,7 +277,7 @@ pub struct Model {
pub epic_ids: Vec<EpicId>,
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 show_extras: bool,
}

View File

@ -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))
})
};

View File

@ -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();

View File

@ -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<Msg>) {
pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
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,
);

View File

@ -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<Msg> {
{
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<Msg> {
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(),

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::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),
]
],

View File

@ -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<Page>)
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)

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 drag;
pub mod keys;
pub mod navbar_left;
pub mod on_event;
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_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<Node<Msg>> {
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<Node<Msg>> {
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<Msg> {
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<Node<Msg>> {
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<Msg> {
.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,

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)
where
Cb: FnMut(web_sys::KeyboardEvent) + 'static,
{
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())
.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<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();
receiver.next().await.unwrap_or_default()
}