From 7803e0fc3da1f6be768804ed187e3d07fe4f4394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Wed, 13 Jan 2021 22:16:22 +0100 Subject: [PATCH] Multiple changes --- Cargo.lock | 25 +- actors/highlight-actor/Cargo.toml | 1 + actors/highlight-actor/src/lib.rs | 94 +++++- .../websocket-actor/src/handlers/comments.rs | 10 +- actors/websocket-actor/src/handlers/epics.rs | 10 +- actors/websocket-actor/src/handlers/hi.rs | 11 +- actors/websocket-actor/src/handlers/issues.rs | 6 +- jirs-client/js/css/project.css | 22 ++ jirs-client/js/css/styledIcon.css | 37 +++ jirs-client/src/lib.rs | 1 + jirs-client/src/modal/mod.rs | 23 +- .../src/modals/issues_create/update.rs | 11 +- jirs-client/src/modals/issues_edit/model.rs | 8 + jirs-client/src/modals/issues_edit/update.rs | 18 +- .../issues_edit/{view/mod.rs => view.rs} | 3 +- jirs-client/src/model.rs | 5 +- jirs-client/src/pages/profile_page/view.rs | 2 +- jirs-client/src/pages/project_page/model.rs | 28 +- jirs-client/src/pages/project_page/update.rs | 40 ++- jirs-client/src/pages/project_page/view.rs | 289 ++---------------- .../src/pages/project_page/view/board.rs | 181 +++++++++++ .../src/pages/project_page/view/filters.rs | 95 ++++++ .../src/pages/project_settings_page/view.rs | 4 +- jirs-client/src/pages/reports_page/view.rs | 2 +- jirs-client/src/pages/users_page/view.rs | 6 +- jirs-client/src/shared/drag.rs | 5 + jirs-client/src/shared/mod.rs | 2 +- jirs-client/src/shared/navbar_left.rs | 5 +- jirs-client/src/shared/styled_avatar.rs | 8 +- jirs-client/src/shared/styled_input.rs | 111 +++---- jirs-client/src/ws/issue.rs | 123 ++++---- shared/jirs-config/src/hi.rs | 11 +- 32 files changed, 725 insertions(+), 472 deletions(-) rename jirs-client/src/modals/issues_edit/{view/mod.rs => view.rs} (99%) create mode 100644 jirs-client/src/pages/project_page/view/board.rs create mode 100644 jirs-client/src/pages/project_page/view/filters.rs diff --git a/Cargo.lock b/Cargo.lock index 04b4e64d..a329b789 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -861,7 +861,7 @@ dependencies = [ "ansi_term", "atty", "bitflags", - "strsim", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", @@ -1731,6 +1731,7 @@ dependencies = [ "log", "pretty_env_logger", "serde", + "simsearch", "syntect", "toml", ] @@ -3394,6 +3395,16 @@ dependencies = [ "libc", ] +[[package]] +name = "simsearch" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b5ceaabc64e2d93a73aa29f8a7dd83dde9c02fdbee660c4551c7e659ce4185" +dependencies = [ + "strsim 0.10.0", + "triple_accel", +] + [[package]] name = "slab" version = "0.4.2" @@ -3487,6 +3498,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.4.0" @@ -3803,6 +3820,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "triple_accel" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622b09ce2fe2df4618636fb92176d205662f59803f39e70d1c333393082de96c" + [[package]] name = "trust-dns-proto" version = "0.18.0-alpha.2" diff --git a/actors/highlight-actor/Cargo.toml b/actors/highlight-actor/Cargo.toml index b2d893ca..651821da 100644 --- a/actors/highlight-actor/Cargo.toml +++ b/actors/highlight-actor/Cargo.toml @@ -17,6 +17,7 @@ serde = "*" bincode = "*" toml = { version = "*" } +simsearch = { version = "0.2" } actix = { version = "0.10.0" } flate2 = { version = "*" } diff --git a/actors/highlight-actor/src/lib.rs b/actors/highlight-actor/src/lib.rs index 87deb62d..979d4755 100644 --- a/actors/highlight-actor/src/lib.rs +++ b/actors/highlight-actor/src/lib.rs @@ -1,6 +1,7 @@ use { actix::{Actor, Handler, SyncContext}, jirs_data::HighlightedCode, + simsearch::SimSearch, std::sync::Arc, syntect::{ easy::HighlightLines, @@ -14,6 +15,20 @@ mod load; lazy_static::lazy_static! { pub static ref THEME_SET: Arc = Arc::new(load::integrated_themeset()); pub static ref SYNTAX_SET: Arc = Arc::new(load::integrated_syntaxset()); + pub static ref SIM_SEARCH: Arc> = Arc::new(create_search_engine()); +} + +fn create_search_engine() -> SimSearch { + let mut engine = SimSearch::new(); + for (idx, name) in SYNTAX_SET + .syntaxes() + .iter() + .map(|s| s.name.as_str()) + .enumerate() + { + engine.insert(idx, name); + } + engine } #[derive(Debug)] @@ -23,23 +38,53 @@ pub enum HighlightError { ResultUnserializable, } -fn hi<'l>(code: &'l str, lang: &'l str) -> Result, HighlightError> { - let set = SYNTAX_SET - .as_ref() - .find_syntax_by_name(lang) - .ok_or_else(|| HighlightError::UnknownLanguage)?; - let theme: &syntect::highlighting::Theme = THEME_SET - .as_ref() - .themes - .get("GitHub") - .ok_or_else(|| HighlightError::UnknownTheme)?; - - let mut hi = HighlightLines::new(set, theme); - Ok(hi.highlight(code, SYNTAX_SET.as_ref())) +#[derive(Debug)] +pub struct HighlightActor { + theme_set: Arc, + syntax_set: Arc, } -#[derive(Debug, Default)] -pub struct HighlightActor {} +impl Default for HighlightActor { + fn default() -> Self { + let theme_set = THEME_SET.clone(); + let syntax_set = SYNTAX_SET.clone(); + + Self { + theme_set, + syntax_set, + } + } +} + +impl HighlightActor { + fn hi<'l>( + &self, + code: &'l str, + lang: &'l str, + ) -> Result, HighlightError> { + let lang = SIM_SEARCH + .search(lang) + .first() + .and_then(|idx| self.syntax_set.syntaxes().get(*idx)) + .map(|st| st.name.as_str()) + .ok_or_else(|| HighlightError::UnknownLanguage)?; + + let set = self + .syntax_set + .as_ref() + .find_syntax_by_name(lang) + .ok_or_else(|| HighlightError::UnknownLanguage)?; + let theme: &syntect::highlighting::Theme = self + .theme_set + .as_ref() + .themes + .get("GitHub") + .ok_or_else(|| HighlightError::UnknownTheme)?; + + let mut hi = HighlightLines::new(set, theme); + Ok(hi.highlight(code, self.syntax_set.as_ref())) + } +} impl Actor for HighlightActor { type Context = SyncContext; @@ -56,7 +101,7 @@ impl Handler for HighlightActor { type Result = Result; fn handle(&mut self, msg: HighlightCode, _ctx: &mut Self::Context) -> Self::Result { - let res: Vec<(Style, &str)> = hi(&msg.code, &msg.lang)?; + let res: Vec<(Style, &str)> = self.hi(&msg.code, &msg.lang)?; Ok(HighlightedCode { parts: res @@ -117,3 +162,20 @@ impl Handler for HighlightActor { })) } } + +#[derive(actix::Message)] +#[rtype(result = "Result, HighlightError>")] +pub struct LoadSyntaxSet; + +impl Handler for HighlightActor { + type Result = Result, HighlightError>; + + fn handle(&mut self, _msg: LoadSyntaxSet, _ctx: &mut Self::Context) -> Self::Result { + Ok(self + .syntax_set + .syntaxes() + .iter() + .map(|s| s.name.clone()) + .collect()) + } +} diff --git a/actors/websocket-actor/src/handlers/comments.rs b/actors/websocket-actor/src/handlers/comments.rs index cdda53a2..a86b9f3e 100644 --- a/actors/websocket-actor/src/handlers/comments.rs +++ b/actors/websocket-actor/src/handlers/comments.rs @@ -1,8 +1,8 @@ -use futures::executor::block_on; - -use jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg}; - -use crate::{WebSocketActor, WsHandler, WsResult}; +use { + crate::{WebSocketActor, WsHandler, WsResult}, + futures::executor::block_on, + jirs_data::{CommentId, CreateCommentPayload, IssueId, UpdateCommentPayload, WsMsg}, +}; pub struct LoadIssueComments { pub issue_id: IssueId, diff --git a/actors/websocket-actor/src/handlers/epics.rs b/actors/websocket-actor/src/handlers/epics.rs index 392c0c89..8654d4ca 100644 --- a/actors/websocket-actor/src/handlers/epics.rs +++ b/actors/websocket-actor/src/handlers/epics.rs @@ -1,8 +1,8 @@ -use futures::executor::block_on; - -use jirs_data::{EpicId, NameString, UserProject, WsMsg}; - -use crate::{WebSocketActor, WsHandler, WsResult}; +use { + crate::{WebSocketActor, WsHandler, WsResult}, + futures::executor::block_on, + jirs_data::{EpicId, NameString, UserProject, WsMsg}, +}; pub struct LoadEpics; diff --git a/actors/websocket-actor/src/handlers/hi.rs b/actors/websocket-actor/src/handlers/hi.rs index 14faf98d..a2a816b9 100644 --- a/actors/websocket-actor/src/handlers/hi.rs +++ b/actors/websocket-actor/src/handlers/hi.rs @@ -1,9 +1,8 @@ -use futures::executor::block_on; - -use jirs_data::WsMsg; -use jirs_data::{Code, Lang}; - -use crate::{WebSocketActor, WsHandler, WsResult}; +use { + crate::{WebSocketActor, WsHandler, WsResult}, + futures::executor::block_on, + jirs_data::{Code, Lang, WsMsg}, +}; pub struct HighlightCode(pub Lang, pub Code); diff --git a/actors/websocket-actor/src/handlers/issues.rs b/actors/websocket-actor/src/handlers/issues.rs index fdf1f8b4..f9a827e5 100644 --- a/actors/websocket-actor/src/handlers/issues.rs +++ b/actors/websocket-actor/src/handlers/issues.rs @@ -50,17 +50,13 @@ impl WsHandler for WebSocketActor { msg.title = Some(s); } (IssueFieldId::Description, PayloadVariant::String(s)) => { - // let mut opts = comrak::ComrakOptions::default(); - // opts.render.github_pre_lang = true; - // let html = comrak::markdown_to_html(s.as_str(), &opts); - let html: String = { use pulldown_cmark::*; let parser = pulldown_cmark::Parser::new(s.as_str()); enum ParseState { Code(highlight_actor::TextHighlightCode), Other, - }; + } let mut state = ParseState::Other; let parser = parser.flat_map(|event| match event { diff --git a/jirs-client/js/css/project.css b/jirs-client/js/css/project.css index 823d5e9c..6a222da5 100644 --- a/jirs-client/js/css/project.css +++ b/jirs-client/js/css/project.css @@ -50,12 +50,21 @@ transition: transform 0.1s; cursor: pointer; user-select: none; + background: var(--backgroundMedium); + border-color: var(--backgroundLight) } #projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder.isActive { box-shadow: 0 0 0 4px var(--primary); } +#projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder > .letter { + width: 32px; + height: 32px; + font-size: 16px; + font-weight: bolder; +} + #projectPage > #projectBoardFilters > #avatars > .avatarIsActiveBorder:hover { transform: translateY(-5px); } @@ -134,6 +143,19 @@ #projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink { display: block; margin-bottom: 5px; + position: relative; +} + +#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink > .dragCover { + display: block; + position: absolute; + z-index: 1; + width: 100%; + height: 100%; +} + +#projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink > .dragCover:-moz-drag-over { + border: var(--borderInputFocus); } #projectPage > .rows > .row > .projectBoardLists > .list > .issues > .issueLink > .issue { diff --git a/jirs-client/js/css/styledIcon.css b/jirs-client/js/css/styledIcon.css index 95b615fc..57fd0930 100644 --- a/jirs-client/js/css/styledIcon.css +++ b/jirs-client/js/css/styledIcon.css @@ -384,3 +384,40 @@ i.styledIcon.double-left:before { i.styledIcon.double-right:before { content: "\ea7c"; } + + +i.styledIcon.task { + color: var(--task); +} + +i.styledIcon.bug { + color: var(--bug); +} + +i.styledIcon.story { + color: var(--story); +} + +i.styledIcon.epic { + color: var(--epic); +} + +i.styledIcon.highest { + color: var(--highest); +} + +i.styledIcon.high { + color: var(--high); +} + +i.styledIcon.medium { + color: var(--medium); +} + +i.styledIcon.low { + color: var(--low); +} + +i.styledIcon.lowest { + color: var(--lowest); +} diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index a0cb73fa..23123d4f 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -99,6 +99,7 @@ pub enum Msg { // inputs StrInputChanged(FieldId, String), + U32InputChanged(FieldId, u32), FileInputChanged(FieldId, Vec), // Rte(FieldId, RteMsg), diff --git a/jirs-client/src/modal/mod.rs b/jirs-client/src/modal/mod.rs index 35642354..9a9f5c6f 100644 --- a/jirs-client/src/modal/mod.rs +++ b/jirs-client/src/modal/mod.rs @@ -8,7 +8,7 @@ use { ToNode, }, ws::send_ws_msg, - FieldChange, FieldId, Msg, WebSocketChanged, + FieldChange, FieldId, Msg, OperationKind, ResourceKind, }, jirs_data::{TimeTracking, WsMsg}, seed::{prelude::*, *}, @@ -37,24 +37,19 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders model.modals.push(modal_type.as_ref().clone()); } - Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::ProjectIssuesLoaded(_issues))) => { + Msg::ResourceChanged(ResourceKind::Issue, OperationKind::ListLoaded, _) => { match model.page { Page::EditIssue(issue_id) if model.modals.is_empty() => { push_edit_modal(issue_id, model, orders) } + Page::AddIssue if model.modals.is_empty() => push_add_modal(model, orders), _ => (), } } - Msg::ChangePage(Page::EditIssue(issue_id)) => { - push_edit_modal(*issue_id, model, orders); - } + Msg::ChangePage(Page::EditIssue(issue_id)) => push_edit_modal(*issue_id, model, orders), - Msg::ChangePage(Page::AddIssue) => { - let mut modal = crate::modals::issues_create::Model::default(); - modal.project_id = model.project.as_ref().map(|p| p.id); - model.modals.push(ModalType::AddIssue(Box::new(modal))); - } + Msg::ChangePage(Page::AddIssue) => push_add_modal(model, orders), #[cfg(debug_assertions)] Msg::GlobalKeyDown { key, .. } if key.eq("#") => { @@ -123,6 +118,14 @@ pub fn view(model: &model::Model) -> Node { section![id!["modals"], modals] } +fn push_add_modal(model: &mut Model, _orders: &mut impl Orders) { + use crate::modals::issues_create::Model; + model.modals.push(ModalType::AddIssue(Box::new(Model { + project_id: model.project.as_ref().map(|p| p.id), + ..Model::default() + }))); +} + fn push_edit_modal(issue_id: i32, model: &mut Model, orders: &mut impl Orders) { let time_tracking_type = model .project diff --git a/jirs-client/src/modals/issues_create/update.rs b/jirs-client/src/modals/issues_create/update.rs index bead4eb2..de1daf45 100644 --- a/jirs-client/src/modals/issues_create/update.rs +++ b/jirs-client/src/modals/issues_create/update.rs @@ -3,7 +3,7 @@ use { model::{IssueModal, ModalType}, shared::styled_select::StyledSelectChanged, ws::send_ws_msg, - FieldId, Msg, WebSocketChanged, + FieldId, Msg, OperationKind, ResourceKind, }, jirs_data::{IssueFieldId, UserId, WsMsg}, seed::prelude::*, @@ -48,7 +48,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde time_remaining: modal.time_remaining, project_id: modal.project_id.unwrap_or(project_id), user_ids: modal.user_ids.clone(), - reporter_id: modal.reporter_id.unwrap_or_else(|| user_id), + reporter_id: modal.reporter_id.unwrap_or(user_id), epic_id: modal.epic_id, }; @@ -64,12 +64,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde }; } - Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueCreated(issue))) => { - model.issues.push(issue.clone()); - orders.skip().send_msg(Msg::ModalDropped); - } - - Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::EpicCreated(_))) => { + Msg::ResourceChanged(ResourceKind::Issue, OperationKind::SingleCreated, _) => { orders.skip().send_msg(Msg::ModalDropped); } diff --git a/jirs-client/src/modals/issues_edit/model.rs b/jirs-client/src/modals/issues_edit/model.rs index 7462210e..b1a916d5 100644 --- a/jirs-client/src/modals/issues_edit/model.rs +++ b/jirs-client/src/modals/issues_edit/model.rs @@ -34,6 +34,7 @@ pub struct Model { pub time_remaining: StyledInputState, pub time_remaining_select: StyledSelectState, + pub title_state: StyledInputState, pub description_state: StyledEditorState, // comments @@ -107,6 +108,11 @@ impl Model { .map(|n| vec![n as u32]) .unwrap_or_default(), ), + title_state: StyledInputState::new( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)), + issue.title.clone(), + ) + .with_min(Some(3)), description_state: StyledEditorState::new( Mode::View, issue.description_text.as_deref().unwrap_or(""), @@ -159,5 +165,7 @@ impl IssueModal for Model { self.time_remaining.update(msg); self.time_remaining_select.update(msg, orders); self.epic_name_state.update(msg, orders); + + self.title_state.update(msg); } } diff --git a/jirs-client/src/modals/issues_edit/update.rs b/jirs-client/src/modals/issues_edit/update.rs index 341471bb..6a1994df 100644 --- a/jirs-client/src/modals/issues_edit/update.rs +++ b/jirs-client/src/modals/issues_edit/update.rs @@ -25,6 +25,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { issue.description_text.clone().unwrap_or_default(); } } + + // type Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), StyledSelectChanged::Changed(Some(value)), @@ -40,6 +42,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } + + // issue status id Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)), StyledSelectChanged::Changed(Some(value)), @@ -55,6 +59,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } + + // reporter id Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)), StyledSelectChanged::Changed(Some(value)), @@ -70,6 +76,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } + + // assignees Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)), StyledSelectChanged::Changed(Some(value)), @@ -89,8 +97,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)), StyledSelectChanged::RemoveMulti(value), ) => { - let mut old = vec![]; - std::mem::swap(&mut old, &mut modal.payload.user_ids); + let old = std::mem::replace(&mut modal.payload.user_ids, vec![]); let dropped = *value as i32; for id in old.into_iter() { if id != dropped { @@ -107,6 +114,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } + + // priority Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)), StyledSelectChanged::Changed(Some(value)), @@ -122,6 +131,8 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } + + // Title Msg::StrInputChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Title)), value, @@ -136,7 +147,10 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { model.ws.as_ref(), orders, ); + orders.skip(); } + + // Description Msg::StrInputChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)), value, diff --git a/jirs-client/src/modals/issues_edit/view/mod.rs b/jirs-client/src/modals/issues_edit/view.rs similarity index 99% rename from jirs-client/src/modals/issues_edit/view/mod.rs rename to jirs-client/src/modals/issues_edit/view.rs index f6fd0854..68950356 100644 --- a/jirs-client/src/modals/issues_edit/view/mod.rs +++ b/jirs-client/src/modals/issues_edit/view.rs @@ -151,8 +151,7 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node { .add_input_class("issueSummary") .add_wrapper_class("issueSummary") .add_wrapper_class("textarea") - .value(payload.title.as_str()) - .valid(payload.title.len() >= 3) + .state(&modal.title_state) .build(FieldId::EditIssueModal(EditIssueModalSection::Issue( IssueFieldId::Title, ))) diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 03f2ff6b..8eb2aec0 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -62,7 +62,7 @@ impl Page { match self { Page::Project => "/board".to_string(), Page::EditIssue(id) => format!("/issues/{id}", id = id), - Page::AddIssue => "/add-issues".to_string(), + Page::AddIssue => "/add-issue".to_string(), Page::ProjectSettings => "/project-settings".to_string(), Page::SignIn => "/login".to_string(), Page::SignUp => "/register".to_string(), @@ -163,6 +163,8 @@ pub struct Model { pub user_projects: Vec, pub projects: Vec, pub epics: Vec, + + pub show_extras: bool, } impl Model { @@ -197,6 +199,7 @@ impl Model { projects: vec![], epics: vec![], issues_by_id: Default::default(), + show_extras: false, } } diff --git a/jirs-client/src/pages/profile_page/view.rs b/jirs-client/src/pages/profile_page/view.rs index 993897d0..c33b1330 100644 --- a/jirs-client/src/pages/profile_page/view.rs +++ b/jirs-client/src/pages/profile_page/view.rs @@ -76,7 +76,7 @@ pub fn view(model: &Model) -> Node { .add_field(submit_field) .build() .into_node(); - inner_layout(model, "profile", vec![content]) + inner_layout(model, "profile", &[content]) } fn build_current_project(model: &Model, page: &ProfilePage) -> Node { diff --git a/jirs-client/src/pages/project_page/model.rs b/jirs-client/src/pages/project_page/model.rs index 2bc2ceb3..e07b03b9 100644 --- a/jirs-client/src/pages/project_page/model.rs +++ b/jirs-client/src/pages/project_page/model.rs @@ -38,31 +38,35 @@ impl ProjectPage { let statuses = statuses.iter().map(|s| (s.id, s.name.as_str())); - let mut issues: Vec<&Issue> = { + let mut issues: Vec<&Issue> = issues.iter().collect(); + if self.recently_updated_filter { let mut m = HashMap::new(); let mut sorted = vec![]; - for issue in issues.iter() { + for issue in issues.into_iter() { sorted.push((issue.id, issue.updated_at)); m.insert(issue.id, issue); } sorted.sort_by(|(_, a_time), (_, b_time)| a_time.cmp(b_time)); - sorted + issues = sorted .into_iter() + .take(10) .flat_map(|(id, _)| m.remove(&id)) - .collect() - }; - if self.recently_updated_filter { - issues = issues[0..10].to_vec() + .collect(); + issues.sort_by(|a, b| a.list_position.cmp(&b.list_position)); } for epic in epics { - let mut per_epic_map = EpicIssuePerStatus::default(); - per_epic_map.epic_name = epic.map(|(_, name)| name).unwrap_or_default().to_string(); + let mut per_epic_map = EpicIssuePerStatus { + epic_name: epic.map(|(_, name)| name).unwrap_or_default().to_string(), + ..Default::default() + }; for (current_status_id, issue_status_name) in statuses.to_owned() { - let mut per_status_map = StatusIssueIds::default(); - per_status_map.status_id = current_status_id; - per_status_map.status_name = issue_status_name.to_string(); + let mut per_status_map = StatusIssueIds { + status_id: current_status_id, + status_name: issue_status_name.to_string(), + ..Default::default() + }; for issue in issues.iter() { if issue.epic_id == epic.map(|(id, _)| id) && issue_filter_status(issue, current_status_id) diff --git a/jirs-client/src/pages/project_page/update.rs b/jirs-client/src/pages/project_page/update.rs index 4c4e673f..b4571bd8 100644 --- a/jirs-client/src/pages/project_page/update.rs +++ b/jirs-client/src/pages/project_page/update.rs @@ -79,28 +79,58 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } Msg::StrInputChanged(FieldId::TextFilterBoard, text) => { project_page.text_filter = text; + project_page.rebuild_visible( + &model.epics, + &model.issue_statuses, + &model.issues, + &model.user, + ); } Msg::ProjectAvatarFilterChanged(user_id, active) => { if active { - project_page.active_avatar_filters = project_page - .active_avatar_filters - .iter() - .filter_map(|id| if *id != user_id { Some(*id) } else { None }) - .collect(); + project_page.active_avatar_filters = + std::mem::replace(&mut project_page.active_avatar_filters, vec![]) + .into_iter() + .filter(|id| *id != user_id) + .collect(); } else { project_page.active_avatar_filters.push(user_id); } + project_page.rebuild_visible( + &model.epics, + &model.issue_statuses, + &model.issues, + &model.user, + ); } Msg::ProjectToggleOnlyMy => { project_page.only_my_filter = !project_page.only_my_filter; + project_page.rebuild_visible( + &model.epics, + &model.issue_statuses, + &model.issues, + &model.user, + ); } Msg::ProjectToggleRecentlyUpdated => { project_page.recently_updated_filter = !project_page.recently_updated_filter; + project_page.rebuild_visible( + &model.epics, + &model.issue_statuses, + &model.issues, + &model.user, + ); } Msg::ProjectClearFilters => { project_page.active_avatar_filters = vec![]; project_page.recently_updated_filter = false; project_page.only_my_filter = false; + project_page.rebuild_visible( + &model.epics, + &model.issue_statuses, + &model.issues, + &model.user, + ); } Msg::PageChanged(PageChanged::Board(BoardPageChange::IssueDragStarted(issue_id))) => { crate::ws::issue::drag_started(issue_id, model) diff --git a/jirs-client/src/pages/project_page/view.rs b/jirs-client/src/pages/project_page/view.rs index f71cb1fa..120c8714 100644 --- a/jirs-client/src/pages/project_page/view.rs +++ b/jirs-client/src/pages/project_page/view.rs @@ -1,36 +1,31 @@ use { crate::{ - model::{Model, Page, PageContent}, - shared::{ - inner_layout, - styled_avatar::StyledAvatar, - styled_button::StyledButton, - styled_icon::{Icon, StyledIcon}, - styled_input::StyledInput, - ToNode, - }, - BoardPageChange, FieldId, Msg, PageChanged, + model::Model, + shared::{inner_layout, styled_button::StyledButton, styled_icon::Icon, ToNode}, + Msg, }, - jirs_data::*, seed::{prelude::*, *}, }; +mod board; +mod filters; + pub fn view(model: &Model) -> Node { - let project_section = vec![ + let project_section = [ breadcrumbs(model), - header(), - project_board_filters(model), - project_board_lists(model), + header(model), + filters::project_board_filters(model), + board::project_board_lists(model), ]; - inner_layout(model, "projectPage", project_section) + inner_layout(model, "projectPage", &project_section) } fn breadcrumbs(model: &Model) -> Node { let project_name = model .project .as_ref() - .map(|p| p.name.clone()) + .map(|p| p.name.as_str()) .unwrap_or_default(); div![ C!["breadcrumbsContainer"], @@ -42,10 +37,13 @@ fn breadcrumbs(model: &Model) -> Node { ] } -fn header() -> Node { +fn header(model: &Model) -> Node { + if !model.show_extras { + return Node::Empty; + } let button = StyledButton::build() .secondary() - .text("Github Repo") + .text("Repository") .icon(Icon::Github) .build() .into_node(); @@ -58,256 +56,3 @@ fn header() -> Node { ] ] } - -fn project_board_filters(model: &Model) -> Node { - let project_page = match &model.page_content { - PageContent::Project(page_content) => page_content, - _ => return empty![], - }; - - let search_input = StyledInput::build() - .icon(Icon::Search) - .valid(true) - .value(project_page.text_filter.as_str()) - .build(FieldId::TextFilterBoard) - .into_node(); - - let only_my = StyledButton::build() - .empty() - .active(project_page.only_my_filter) - .text("Only My Issues") - .add_class("filterChild") - .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)) - .build() - .into_node(); - - let recently_updated = StyledButton::build() - .empty() - .text("Recently Updated") - .add_class("filterChild") - .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)) - .build() - .into_node(); - - let clear_all = if project_page.only_my_filter - || project_page.recently_updated_filter - || !project_page.active_avatar_filters.is_empty() - { - seed::button![ - id!["clearAllFilters"], - C!["filterChild"], - "Clear all", - mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters), - ] - } else { - empty![] - }; - - div![ - id!["projectBoardFilters"], - search_input, - avatars_filters(model), - only_my, - recently_updated, - clear_all - ] -} - -fn avatars_filters(model: &Model) -> Node { - let project_page = match &model.page_content { - PageContent::Project(project_page) => project_page, - _ => return empty![], - }; - let active_avatar_filters = &project_page.active_avatar_filters; - let avatars: Vec> = model - .users - .iter() - .enumerate() - .map(|(idx, user)| { - let mut class_list = vec!["avatarIsActiveBorder"]; - let user_id = user.id; - let active = active_avatar_filters.contains(&user_id); - if active { - class_list.push("isActive"); - } - let styled_avatar = StyledAvatar::build() - .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) - .on_click(mouse_ev(Ev::Click, move |_| { - Msg::ProjectAvatarFilterChanged(user_id, active) - })) - .name(user.name.as_str()) - .user_index(idx) - .build() - .into_node(); - div![attrs![At::Class => class_list.join(" ")], styled_avatar] - }) - .collect(); - - div![id!["avatars"], C!["filterChild"], avatars] -} - -fn project_board_lists(model: &Model) -> Node { - let project_page = match &model.page_content { - PageContent::Project(project_page) => project_page, - _ => return empty![], - }; - let rows = project_page.visible_issues.iter().map(|per_epic| { - let columns: Vec> = per_epic - .per_status_issues - .iter() - .map(|per_status| { - let issues: Vec<&Issue> = per_status - .issue_ids - .iter() - .filter_map(|id| model.issues_by_id.get(id)) - .collect(); - project_issue_list( - model, - per_status.status_id, - &per_status.status_name, - issues.as_slice(), - ) - }) - .collect(); - div![ - C!["row"], - div![C!["epicName"], per_epic.epic_name.as_str()], - div![C!["projectBoardLists"], columns] - ] - }); - div![C!["rows"], rows] -} - -fn project_issue_list( - model: &Model, - status_id: IssueStatusId, - status_name: &str, - issues: &[&Issue], -) -> Node { - let issues: Vec> = issues - .iter() - .map(|issue| project_issue(model, issue)) - .collect(); - let drop_handler = { - let send_status = status_id; - drag_ev(Ev::Drop, move |ev| { - ev.prevent_default(); - Some(Msg::PageChanged(PageChanged::Board( - BoardPageChange::IssueDropZone(send_status), - ))) - }) - }; - - let drag_over_handler = { - let send_status = status_id; - drag_ev(Ev::DragOver, move |ev| { - ev.prevent_default(); - Some(Msg::PageChanged(PageChanged::Board( - BoardPageChange::IssueDragOverStatus(send_status), - ))) - }) - }; - - div![ - C!["list"], - div![C!["title"], status_name, div![C!["issuesCount"]]], - div![ - C!["issues"], - attrs![At::DropZone => "link"], - drop_handler, - drag_over_handler, - issues - ] - ] -} - -fn project_issue(model: &Model, issue: &Issue) -> Node { - let avatars: Vec> = issue - .user_ids - .iter() - .filter_map(|id| model.users_by_id.get(id)) - .map(|user| { - StyledAvatar::build() - .size(24) - .name(user.name.as_str()) - .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) - .user_index(0) - .build() - .into_node() - }) - .collect(); - - let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into()) - .with_color(issue.issue_type.to_str()) - .build() - .into_node(); - let priority_icon = { - let icon = match issue.priority { - IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown, - _ => Icon::ArrowUp, - }; - StyledIcon::build(icon) - .with_color(issue.priority.to_str()) - .build() - .into_node() - }; - - let issue_id = issue.id; - let drag_started = drag_ev(Ev::DragStart, move |_| { - Some(Msg::PageChanged(PageChanged::Board( - BoardPageChange::IssueDragStarted(issue_id), - ))) - }); - let drag_stopped = drag_ev(Ev::DragEnd, move |_| { - Some(Msg::PageChanged(PageChanged::Board( - BoardPageChange::IssueDragStopped(issue_id), - ))) - }); - let drag_over_handler = drag_ev(Ev::DragOver, move |ev| { - ev.prevent_default(); - ev.stop_propagation(); - Some(Msg::PageChanged(PageChanged::Board( - BoardPageChange::ExchangePosition(issue_id), - ))) - }); - - let drag_out = drag_ev(Ev::DragLeave, move |_| { - Some(Msg::PageChanged(PageChanged::Board( - BoardPageChange::DragLeave(issue_id), - ))) - }); - let on_click = mouse_ev("click", move |ev| { - ev.prevent_default(); - ev.stop_propagation(); - seed::Url::new() - .add_path_part("issues") - .add_path_part(format!("{}", issue_id)) - .go_and_push(); - Msg::ChangePage(Page::EditIssue(issue_id)) - }); - - let href = format!("/issues/{id}", id = issue_id); - - a![ - drag_started, - on_click, - C!["issueLink"], - attrs![At::Href => href], - div![ - C!["issue"], - attrs![At::Draggable => true], - drag_stopped, - drag_over_handler, - drag_out, - p![C!["title"], issue.title.as_str()], - div![ - C!["bottom"], - div![ - div![C!["issueTypeIcon"], issue_type_icon], - div![C!["issuePriorityIcon"], priority_icon] - ], - div![C!["assignees"], avatars,], - ] - ] - ] -} diff --git a/jirs-client/src/pages/project_page/view/board.rs b/jirs-client/src/pages/project_page/view/board.rs new file mode 100644 index 00000000..cb836b63 --- /dev/null +++ b/jirs-client/src/pages/project_page/view/board.rs @@ -0,0 +1,181 @@ +use { + crate::{ + model::PageContent, + shared::{styled_avatar::*, styled_icon::*, ToNode}, + BoardPageChange, Model, Msg, Page, PageChanged, + }, + jirs_data::*, + seed::{prelude::*, *}, +}; + +pub fn project_board_lists(model: &Model) -> Node { + let project_page = match &model.page_content { + PageContent::Project(project_page) => project_page, + _ => return empty![], + }; + let rows = project_page.visible_issues.iter().map(|per_epic| { + let columns: Vec> = per_epic + .per_status_issues + .iter() + .map(|per_status| { + let issues: Vec<&Issue> = per_status + .issue_ids + .iter() + .filter_map(|id| model.issues_by_id.get(id)) + .collect(); + project_issue_list( + model, + per_status.status_id, + &per_status.status_name, + issues.as_slice(), + ) + }) + .collect(); + div![ + C!["row"], + div![C!["epicName"], per_epic.epic_name.as_str()], + div![C!["projectBoardLists"], columns] + ] + }); + div![C!["rows"], rows] +} + +fn project_issue_list( + model: &Model, + status_id: IssueStatusId, + status_name: &str, + issues: &[&Issue], +) -> Node { + let issues: Vec> = issues + .iter() + .map(|issue| project_issue(model, issue)) + .collect(); + let drop_handler = { + let send_status = status_id; + drag_ev(Ev::Drop, move |ev| { + ev.prevent_default(); + Some(Msg::PageChanged(PageChanged::Board( + BoardPageChange::IssueDropZone(send_status), + ))) + }) + }; + + let drag_over_handler = { + let send_status = status_id; + drag_ev(Ev::DragOver, move |ev| { + ev.prevent_default(); + Some(Msg::PageChanged(PageChanged::Board( + BoardPageChange::IssueDragOverStatus(send_status), + ))) + }) + }; + + div![ + C!["list"], + div![C!["title"], status_name, div![C!["issuesCount"]]], + div![ + C!["issues"], + attrs![At::DropZone => "link"], + drop_handler, + drag_over_handler, + issues + ] + ] +} + +fn project_issue(model: &Model, issue: &Issue) -> Node { + let is_dragging = match &model.page_content { + PageContent::Project(project_page) => project_page.issue_drag.is_dragging(), + _ => false, + }; + let avatars: Vec> = issue + .user_ids + .iter() + .filter_map(|id| model.users_by_id.get(id)) + .map(|user| { + StyledAvatar::build() + .size(24) + .name(user.name.as_str()) + .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) + .user_index(0) + .build() + .into_node() + }) + .collect(); + + let issue_type_icon = StyledIcon::build(issue.issue_type.clone().into()) + .with_color(issue.issue_type.to_str()) + .build() + .into_node(); + + let priority_icon = { + let icon = match issue.priority { + IssuePriority::Low | IssuePriority::Lowest => Icon::ArrowDown, + _ => Icon::ArrowUp, + }; + StyledIcon::build(icon) + .add_class(issue.priority.to_str()) + .with_color(issue.priority.to_str()) + .build() + .into_node() + }; + + let issue_id = issue.id; + let drag_started = drag_ev(Ev::DragStart, move |_| { + Some(Msg::PageChanged(PageChanged::Board( + BoardPageChange::IssueDragStarted(issue_id), + ))) + }); + let drag_stopped = drag_ev(Ev::DragEnd, move |_| { + Some(Msg::PageChanged(PageChanged::Board( + BoardPageChange::IssueDragStopped(issue_id), + ))) + }); + let drag_over_handler = drag_ev(Ev::DragEnter, move |ev| { + ev.prevent_default(); + ev.stop_propagation(); + Some(Msg::PageChanged(PageChanged::Board( + BoardPageChange::ExchangePosition(issue_id), + ))) + }); + + let drag_out = drag_ev(Ev::DragLeave, move |_| { + Some(Msg::PageChanged(PageChanged::Board( + BoardPageChange::DragLeave(issue_id), + ))) + }); + let on_click = mouse_ev("click", move |ev| { + ev.prevent_default(); + ev.stop_propagation(); + seed::Url::new() + .add_path_part("issues") + .add_path_part(format!("{}", issue_id)) + .go_and_push(); + Msg::ChangePage(Page::EditIssue(issue_id)) + }); + + let href = format!("/issues/{id}", id = issue_id); + + a![ + drag_started, + on_click, + C!["issueLink"], + attrs![At::Href => href], + IF![is_dragging => div![C!["dragCover"], drag_over_handler]], + div![ + C!["issue"], + attrs![At::Draggable => true], + drag_stopped, + drag_out, + p![C!["title"], issue.title.as_str()], + div![ + C!["bottom"], + div![ + div![C!["issueTypeIcon"], issue_type_icon], + div![C!["issuePriorityIcon"], priority_icon] + ], + div![C!["assignees"], avatars,], + ] + ] + ] +} diff --git a/jirs-client/src/pages/project_page/view/filters.rs b/jirs-client/src/pages/project_page/view/filters.rs new file mode 100644 index 00000000..5c6a6264 --- /dev/null +++ b/jirs-client/src/pages/project_page/view/filters.rs @@ -0,0 +1,95 @@ +use { + crate::{ + model::PageContent, + shared::{styled_avatar::*, styled_button::*, styled_icon::*, styled_input::*, ToNode}, + FieldId, Model, Msg, + }, + seed::{prelude::*, *}, +}; + +pub fn project_board_filters(model: &Model) -> Node { + let project_page = match &model.page_content { + PageContent::Project(page_content) => page_content, + _ => return empty![], + }; + + let search_input = StyledInput::build() + .icon(Icon::Search) + .valid(true) + .value(project_page.text_filter.as_str()) + .build(FieldId::TextFilterBoard) + .into_node(); + + let only_my = StyledButton::build() + .empty() + .active(project_page.only_my_filter) + .text("Only My Issues") + .add_class("filterChild") + .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)) + .build() + .into_node(); + + let recently_updated = StyledButton::build() + .empty() + .text("Recently Updated") + .add_class("filterChild") + .on_click(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)) + .build() + .into_node(); + + let clear_all = if project_page.only_my_filter + || project_page.recently_updated_filter + || !project_page.active_avatar_filters.is_empty() + { + seed::button![ + id!["clearAllFilters"], + C!["filterChild"], + "Clear all", + mouse_ev(Ev::Click, |_| Msg::ProjectClearFilters), + ] + } else { + empty![] + }; + + div![ + id!["projectBoardFilters"], + search_input, + avatars_filters(model), + only_my, + recently_updated, + clear_all + ] +} + +pub fn avatars_filters(model: &Model) -> Node { + let project_page = match &model.page_content { + PageContent::Project(project_page) => project_page, + _ => return empty![], + }; + let active_avatar_filters = &project_page.active_avatar_filters; + let avatars: Vec> = model + .users + .iter() + .enumerate() + .map(|(idx, user)| { + let user_id = user.id; + let active = active_avatar_filters.contains(&user_id); + let styled_avatar = StyledAvatar::build() + .avatar_url(user.avatar_url.as_deref().unwrap_or_default()) + .on_click(mouse_ev(Ev::Click, move |_| { + Msg::ProjectAvatarFilterChanged(user_id, active) + })) + .name(user.name.as_str()) + .user_index(idx) + .build() + .into_node(); + div![ + if active { Some(C!["isActive"]) } else { None }, + C!["avatarIsActiveBorder"], + styled_avatar + ] + }) + .collect(); + + div![id!["avatars"], C!["filterChild"], avatars] +} diff --git a/jirs-client/src/pages/project_settings_page/view.rs b/jirs-client/src/pages/project_settings_page/view.rs index e83bc464..56121eeb 100644 --- a/jirs-client/src/pages/project_settings_page/view.rs +++ b/jirs-client/src/pages/project_settings_page/view.rs @@ -105,9 +105,9 @@ pub fn view(model: &model::Model) -> Node { .build() .into_node(); - let project_section = vec![div![C!["formContainer"], form]]; + let project_section = [div![C!["formContainer"], form]]; - inner_layout(model, "projectSettings", project_section) + inner_layout(model, "projectSettings", &project_section) } /// Build project name input with styled field wrapper diff --git a/jirs-client/src/pages/reports_page/view.rs b/jirs-client/src/pages/reports_page/view.rs index b136df1e..f6d97b9c 100644 --- a/jirs-client/src/pages/reports_page/view.rs +++ b/jirs-client/src/pages/reports_page/view.rs @@ -29,7 +29,7 @@ pub fn view(model: &Model) -> Node { let body = section![C!["top"], h1![C!["header"], "Reports"], graph, list]; - inner_layout(model, "reports", vec![body]) + inner_layout(model, "reports", &[body]) } fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node { diff --git a/jirs-client/src/pages/users_page/view.rs b/jirs-client/src/pages/users_page/view.rs index 634aede3..2d8e76a4 100644 --- a/jirs-client/src/pages/users_page/view.rs +++ b/jirs-client/src/pages/users_page/view.rs @@ -167,9 +167,5 @@ pub fn view(model: &Model) -> Node { ul![C!["invitationsList"], invitations], ]; - inner_layout( - model, - "users", - vec![form, users_section, invitations_section], - ) + inner_layout(model, "users", &[form, users_section, invitations_section]) } diff --git a/jirs-client/src/shared/drag.rs b/jirs-client/src/shared/drag.rs index b7eadd5a..3507bd3f 100644 --- a/jirs-client/src/shared/drag.rs +++ b/jirs-client/src/shared/drag.rs @@ -8,6 +8,11 @@ pub struct DragState { } impl DragState { + #[inline] + pub fn is_dragging(&self) -> bool { + self.dragged_id.is_some() + } + #[inline] pub fn mark_dirty(&mut self, id: i32) { self.dirty.insert(id); diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index ec3fed04..16e04e53 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -77,7 +77,7 @@ pub fn divider() -> Node { } #[inline] -pub fn inner_layout(model: &Model, page_name: &str, children: Vec>) -> Node { +pub fn inner_layout(model: &Model, page_name: &str, children: &[Node]) -> Node { let modal_node = crate::modal::view(model); article![ modal_node, diff --git a/jirs-client/src/shared/navbar_left.rs b/jirs-client/src/shared/navbar_left.rs index 964971e4..c07d8621 100644 --- a/jirs-client/src/shared/navbar_left.rs +++ b/jirs-client/src/shared/navbar_left.rs @@ -103,7 +103,10 @@ pub fn render(model: &Model) -> Vec> { C!["bottom"], navbar_left_item("Profile", user_icon, Some("/profile"), Some(go_to_profile)), messages, - about_tooltip(model, navbar_left_item("About", Icon::Help, None, None)), + IF![model.show_extras => about_tooltip( + model, + navbar_left_item("About", Icon::Help, None, None) + )], ], ], ] diff --git a/jirs-client/src/shared/styled_avatar.rs b/jirs-client/src/shared/styled_avatar.rs index 78c22910..0063d8c1 100644 --- a/jirs-client/src/shared/styled_avatar.rs +++ b/jirs-client/src/shared/styled_avatar.rs @@ -145,17 +145,13 @@ pub fn render(values: StyledAvatar) -> Node { ] } _ => { - let style = format!( - "{shared}; width: {size}px; height: {size}px; font-size: calc({size}px / 1.7);", - shared = shared_style, - size = size - ); div![ C!["styledAvatar letter"], class_list, attrs![ At::Class => format!("avatarColor{}", index + 1), - At::Style => style + At::Style => shared_style, + At::Title => name ], span![letter], on_click, diff --git a/jirs-client/src/shared/styled_input.rs b/jirs-client/src/shared/styled_input.rs index c5c68581..aa334c30 100644 --- a/jirs-client/src/shared/styled_input.rs +++ b/jirs-client/src/shared/styled_input.rs @@ -32,6 +32,8 @@ pub struct StyledInputState { id: FieldId, touched: bool, pub value: String, + pub min: Option, + pub max: Option, } impl StyledInputState { @@ -44,9 +46,23 @@ impl StyledInputState { id, touched: false, value: value.into(), + min: None, + max: None, } } + #[inline] + pub fn with_min(mut self, min: Option) -> Self { + self.min = min; + self + } + + #[inline] + pub fn with_max(mut self, max: Option) -> Self { + self.max = max; + self + } + #[inline] pub fn to_i32(&self) -> Option { self.value.parse::().ok() @@ -80,11 +96,11 @@ impl StyledInputState { } #[derive(Debug)] -pub struct StyledInput<'l> { +pub struct StyledInput<'l, 'm: 'l> { id: FieldId, icon: Option, valid: bool, - value: Option, + value: Option<&'m str>, input_type: Option<&'l str>, input_class_list: Vec<&'l str>, wrapper_class_list: Vec<&'l str>, @@ -93,9 +109,9 @@ pub struct StyledInput<'l> { input_handlers: Vec>, } -impl<'l> StyledInput<'l> { +impl<'l, 'm: 'l> StyledInput<'l, 'm> { #[inline] - pub fn build() -> StyledInputBuilder<'l> { + pub fn build() -> StyledInputBuilder<'l, 'm> { StyledInputBuilder { icon: None, valid: None, @@ -111,10 +127,10 @@ impl<'l> StyledInput<'l> { } #[derive(Debug)] -pub struct StyledInputBuilder<'l> { +pub struct StyledInputBuilder<'l, 'm: 'l> { icon: Option, valid: Option, - value: Option, + value: Option<&'m str>, input_type: Option<&'l str>, input_class_list: Vec<&'l str>, wrapper_class_list: Vec<&'l str>, @@ -123,7 +139,7 @@ pub struct StyledInputBuilder<'l> { input_handlers: Vec>, } -impl<'l> StyledInputBuilder<'l> { +impl<'l, 'm: 'l> StyledInputBuilder<'l, 'm> { #[inline] pub fn icon(mut self, icon: Icon) -> Self { self.icon = Some(icon); @@ -137,18 +153,27 @@ impl<'l> StyledInputBuilder<'l> { } #[inline] - pub fn value(mut self, v: S) -> Self - where - S: Into, - { - self.value = Some(v.into()); + pub fn value(mut self, v: &'m str) -> Self { + self.value = Some(v); self } #[inline] - pub fn state(self, state: &StyledInputState) -> Self { - self.value(state.value.as_str()) - .valid(!state.touched || !state.value.is_empty()) + pub fn state(self, state: &'m StyledInputState) -> Self { + self.value(&state.value.as_str()).valid( + match ( + state.touched, + state.value.as_str(), + state.min.as_ref(), + state.max.as_ref(), + ) { + (false, ..) => true, + (_, s, None, None) => !s.is_empty(), + (_, s, Some(min), None) => s.len() >= *min, + (_, s, None, Some(max)) => s.len() <= *max, + (_, s, Some(min), Some(max)) => s.len() >= *min && s.len() <= *max, + }, + ) } #[inline] @@ -182,7 +207,7 @@ impl<'l> StyledInputBuilder<'l> { } #[inline] - pub fn build(self, id: FieldId) -> StyledInput<'l> { + pub fn build(self, id: FieldId) -> StyledInput<'l, 'm> { StyledInput { id, icon: self.icon, @@ -198,7 +223,7 @@ impl<'l> StyledInputBuilder<'l> { } } -impl<'l> ToNode for StyledInput<'l> { +impl<'l, 'm: 'l> ToNode for StyledInput<'l, 'm> { #[inline] fn into_node(self) -> Node { render(self) @@ -212,70 +237,50 @@ pub fn render(values: StyledInput) -> Node { valid, value, input_type, - mut input_class_list, - mut wrapper_class_list, + input_class_list, + wrapper_class_list, variant, auto_focus, input_handlers, } = values; - wrapper_class_list.push(variant.to_str()); - if !valid { - wrapper_class_list.push("invalid"); - } - - input_class_list.push(variant.to_str()); - if icon.is_some() { - input_class_list.push("withIcon"); - } - - let icon = icon + let icon_node = icon .map(|icon| StyledIcon::build(icon).build().into_node()) .unwrap_or(Node::Empty); - let on_input = { + let on_change = { let field_id = id.clone(); - ev(Ev::Input, move |event| { + ev(Ev::Change, move |event| { event.stop_propagation(); let target = event.target().unwrap(); - let input = seed::to_input(&target); - let value = input.value(); - Msg::StrInputChanged(field_id, value) + Msg::StrInputChanged(field_id, seed::to_input(&target).value()) }) }; - let on_keyup = ev(Ev::KeyUp, move |event| { - event.stop_propagation(); - None as Option - }); - let on_click = ev(Ev::Click, move |event| { - event.stop_propagation(); - None as Option - }); div![ C!["styledInput"], + C![variant.to_str()], + if !valid { Some(C!["invalid"]) } else { None }, attrs!( - At::Class => wrapper_class_list.join(" "), - At::Class => format!("{}", id), + "class" => format!("{} {}", id, wrapper_class_list.join(" ")), ), - icon, - on_click, - on_keyup, + icon_node, seed::input![ C!["inputElement"], + icon.as_ref().map(|_| C!["withIcon"]), + C![variant.to_str()], attrs![ - At::Id => format!("{}", id), + "id" => format!("{}", id), At::Class => input_class_list.join(" "), - At::Value => value.unwrap_or_default(), - At::Type => input_type.unwrap_or("text"), - + "value" => value.unwrap_or_default(), + "type" => input_type.unwrap_or("text"), ], if auto_focus { vec![attrs![At::AutoFocus => true]] } else { vec![] }, - on_input, + on_change, input_handlers, ], ] diff --git a/jirs-client/src/ws/issue.rs b/jirs-client/src/ws/issue.rs index 602200e5..1bfbe020 100644 --- a/jirs-client/src/ws/issue.rs +++ b/jirs-client/src/ws/issue.rs @@ -15,67 +15,88 @@ pub fn drag_started(issue_id: IssueId, model: &mut Model) { project_page.issue_drag.drag(issue_id); } -pub fn exchange_position(issue_bellow_id: IssueId, model: &mut Model) { +pub fn exchange_position(below_id: IssueId, model: &mut Model) { let project_page = match &mut model.page_content { PageContent::Project(project_page) => project_page, _ => return, }; - if project_page.issue_drag.dragged_or_last(issue_bellow_id) { - return; - } let dragged_id = match project_page.issue_drag.dragged_id.as_ref().cloned() { Some(id) => id, _ => return error!("Nothing is dragged"), }; - - let mut below = None; - let mut dragged = None; - let mut issues = vec![]; - std::mem::swap(&mut issues, &mut model.issues); - - for issue in issues.into_iter() { - match issue.id { - id if id == issue_bellow_id => below = Some(issue), - id if id == dragged_id => dragged = Some(issue), - _ => model.issues.push(issue), - }; + if below_id == dragged_id { + return; } - - let mut below = match below { - Some(below) => below, - _ => return, - }; - let mut dragged = match dragged { - Some(issue) => issue, - _ => { - model.issues.push(below); - return; - } - }; - if dragged.issue_status_id != below.issue_status_id { - let mut issues = vec![]; - std::mem::swap(&mut issues, &mut model.issues); - for mut c in issues.into_iter() { - if c.issue_status_id == below.issue_status_id && c.list_position > below.list_position { - c.list_position += 1; - project_page.issue_drag.mark_dirty(c.id); - } - model.issues.push(c); - } - dragged.list_position = below.list_position + 1; - dragged.issue_status_id = below.issue_status_id; - } - std::mem::swap(&mut dragged.list_position, &mut below.list_position); - - project_page.issue_drag.mark_dirty(dragged.id); - project_page.issue_drag.mark_dirty(below.id); - - model.issues.push(below); - model.issues.push(dragged); - model + let below_idx = model .issues - .sort_by(|a, b| a.list_position.cmp(&b.list_position)); - project_page.issue_drag.last_id = Some(issue_bellow_id); + .iter() + .position(|issue| issue.id == below_id) + .unwrap_or(model.issues.len()); + let dragged = model + .issues + .iter() + .position(|issue| issue.id == dragged_id) + .map(|idx| model.issues.remove(idx)) + .unwrap(); + let epic_id = dragged.epic_id; + model.issues.insert(below_idx, dragged); + let changed: Vec<(IssueId, i32)> = model + .issues + .iter_mut() + .filter(|issue| issue.epic_id == epic_id) + .enumerate() + .map(|(idx, issue)| { + issue.list_position = idx as i32; + (issue.id, issue.list_position) + }) + .collect(); + for (id, pos) in changed { + if let Some(iss) = model.issues_by_id.get_mut(&id) { + iss.list_position = pos; + } + } + + // let dragged_pos = match model.issues_by_id.get(&dragged_id) { + // Some(i) => i.list_position, + // _ => return, + // }; + // let below_pos = match model.issues_by_id.get(&below_id) { + // Some(i) => i.list_position, + // _ => return, + // }; + // use seed::*; + // log!(format!( + // "exchange dragged {} {} below {} {}", + // dragged_id, dragged_pos, below_id, below_pos + // )); + // for issue in model.issues_by_id.values_mut() { + // if issue.id == below_id { + // issue.list_position = dragged_pos; + // } else if issue.id == dragged_id { + // issue.list_position = below_pos; + // } + // } + // + // for issue in model.issues.iter_mut() { + // if issue.id == below_id { + // issue.list_position = dragged_pos; + // } else if issue.id == dragged_id { + // issue.list_position = below_pos; + // } + // } + // model + // .issues + // .sort_by(|a, b| a.list_position.cmp(&b.list_position)); + if let PageContent::Project(project_page) = &mut model.page_content { + project_page.rebuild_visible( + &model.epics, + &model.issue_statuses, + &model.issues, + &model.user, + ); + project_page.issue_drag.mark_dirty(dragged_id); + project_page.issue_drag.mark_dirty(below_id); + } } pub fn sync(model: &mut Model, orders: &mut impl Orders) { diff --git a/shared/jirs-config/src/hi.rs b/shared/jirs-config/src/hi.rs index 0eb8a96f..93272554 100644 --- a/shared/jirs-config/src/hi.rs +++ b/shared/jirs-config/src/hi.rs @@ -3,15 +3,24 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] pub struct Configuration { pub concurrency: usize, + #[serde(default = "Configuration::default_theme")] + pub theme: String, } impl Default for Configuration { fn default() -> Self { - Self { concurrency: 2 } + Self { + concurrency: 2, + theme: Self::default_theme(), + } } } impl Configuration { crate::rw!("highlight.toml"); + + fn default_theme() -> String { + "Github".to_string() + } } crate::read!(Configuration);