handle close Epic
This commit is contained in:
parent
a8ff5101be
commit
fc3052630a
11
Cargo.lock
generated
11
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
|
@ -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 = "*" }
|
||||
|
@ -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 }
|
||||
|
@ -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" }
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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 = "*"
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
mod events;
|
||||
pub mod events;
|
||||
pub mod styled_avatar;
|
||||
pub mod styled_button;
|
||||
pub mod styled_checkbox;
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
let active = seed::document().active_element().map(|el| el.tag_name());
|
||||
|
||||
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 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();
|
||||
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(),
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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| {
|
||||
|
@ -21,17 +21,42 @@ 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| {
|
||||
.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),
|
||||
@ -40,8 +65,6 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
|
||||
class_list: "",
|
||||
}
|
||||
.render()
|
||||
})
|
||||
.unwrap_or(Node::Empty)
|
||||
}
|
||||
|
||||
#[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 {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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))
|
||||
})
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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(),
|
||||
|
@ -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),
|
||||
]
|
||||
],
|
||||
|
@ -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)
|
||||
|
204
crates/web/src/shared/keys.rs
Normal file
204
crates/web/src/shared/keys.rs
Normal 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())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user