From 39021a8643a7d8a712ef853eb4c9305fc90a91e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Wo=C5=BAniak?= Date: Tue, 19 Jan 2021 21:57:48 +0100 Subject: [PATCH] Add transform epic into issue --- README.md | 9 +- actors/database-actor/src/epics.rs | 17 +- actors/database-actor/src/schema.rs | 12 ++ actors/websocket-actor/src/handlers/epics.rs | 57 ++++++- actors/websocket-actor/src/lib.rs | 18 ++- derive/derive_db_execute/src/lib.rs | 40 +++++ derive/derive_enum_iter/src/lib.rs | 70 ++++++--- jirs-client/js/css/reports.scss | 51 +++++- jirs-client/js/css/styledLink.scss | 15 ++ jirs-client/src/components/styled_link.rs | 22 +++ jirs-client/src/lib.rs | 4 +- jirs-client/src/modals/epics_edit/update.rs | 12 +- jirs-client/src/modals/epics_edit/view.rs | 10 +- .../src/modals/issues_create/update.rs | 2 +- jirs-client/src/pages/reports_page/update.rs | 19 ++- jirs-client/src/pages/reports_page/view.rs | 147 +++++++++++------- .../down.sql | 4 + .../2021-01-18-220341_add_epics_fields/up.sql | 4 + shared/jirs-data/src/lib.rs | 15 +- shared/jirs-data/src/msg.rs | 16 +- 20 files changed, 439 insertions(+), 105 deletions(-) create mode 100644 migrations/2021-01-18-220341_add_epics_fields/down.sql create mode 100644 migrations/2021-01-18-220341_add_epics_fields/up.sql diff --git a/README.md b/README.md index f0d595a2..0a2de49b 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,17 @@ https://git.sr.ht/~tsumanu/jirs * Fix S3 upload with upgraded version of `rusoto` * Remove Custom Elements * Replace CSS with SCSS +* Disable RTE until properly optimized ##### Work Progress * [X] Add Epic -* [ ] Edit Epic -* [ ] Delete Epic +* [X] Edit Epic +* [X] Delete Epic * [ ] Epic `starts` and `ends` date * [X] Grouping by Epic -* [X] Basic Rich Text Editor -* [X] Insert Code in Rich Text Editor +* [ ] Basic Rich Text Editor +* [ ] Insert Code in Rich Text Editor * [X] Code syntax * [ ] Personal settings to choose MDE (Markdown Editor) or RTE * [ ] Issues and filters view diff --git a/actors/database-actor/src/epics.rs b/actors/database-actor/src/epics.rs index 4d49401e..81eeb1a3 100644 --- a/actors/database-actor/src/epics.rs +++ b/actors/database-actor/src/epics.rs @@ -1,14 +1,21 @@ use { crate::{db_create, db_delete, db_load, db_update}, + derive_db_execute::Execute, diesel::prelude::*, - jirs_data::Epic, + jirs_data::{DescriptionString, Epic, EpicId, ProjectId}, }; +#[derive(Execute)] +#[db_exec(schema = "epics", result = "Epic", find = "epics.find(msg.epic_id)")] +pub struct FindEpic { + pub epic_id: EpicId, +} + db_load! { LoadEpics, msg => epics => epics.distinct_on(id).filter(project_id.eq(msg.project_id)), Epic, - project_id => i32 + project_id => ProjectId } db_create! { @@ -17,11 +24,15 @@ db_create! { name.eq(msg.name.as_str()), user_id.eq(msg.user_id), project_id.eq(msg.project_id), + msg.description.map(|d| description.eq(d)), + msg.description_html.map(|d| description_html.eq(d)), )), Epic, user_id => i32, project_id => i32, - name => String + name => String, + description => Option, + description_html => Option } db_update! { diff --git a/actors/database-actor/src/schema.rs b/actors/database-actor/src/schema.rs index 0adc596f..97663a0e 100644 --- a/actors/database-actor/src/schema.rs +++ b/actors/database-actor/src/schema.rs @@ -103,6 +103,18 @@ table! { /// /// (Automatically generated by Diesel.) ends_at -> Nullable, + /// The `description` column of the `epics` table. + /// + /// Its SQL type is `Nullable`. + /// + /// (Automatically generated by Diesel.) + description -> Nullable, + /// The `description_html` column of the `epics` table. + /// + /// Its SQL type is `Nullable`. + /// + /// (Automatically generated by Diesel.) + description_html -> Nullable, } } diff --git a/actors/websocket-actor/src/handlers/epics.rs b/actors/websocket-actor/src/handlers/epics.rs index 2300a115..69bb16d0 100644 --- a/actors/websocket-actor/src/handlers/epics.rs +++ b/actors/websocket-actor/src/handlers/epics.rs @@ -1,7 +1,8 @@ +use jirs_data::IssueType; use { crate::{db_or_debug_and_return, WebSocketActor, WsHandler, WsResult}, futures::executor::block_on, - jirs_data::{EpicId, NameString, UserProject, WsMsg}, + jirs_data::{DescriptionString, EpicId, NameString, UserProject, WsMsg}, }; pub struct LoadEpics; @@ -16,11 +17,17 @@ impl WsHandler for WebSocketActor { pub struct CreateEpic { pub name: NameString, + pub description: Option, + pub description_html: Option, } impl WsHandler for WebSocketActor { fn handle_msg(&mut self, msg: CreateEpic, _ctx: &mut Self::Context) -> WsResult { - let CreateEpic { name } = msg; + let CreateEpic { + name, + description, + description_html, + } = msg; let UserProject { user_id, project_id, @@ -31,6 +38,8 @@ impl WsHandler for WebSocketActor { database_actor::epics::CreateEpic { user_id: *user_id, project_id: *project_id, + description, + description_html, name, } ); @@ -77,3 +86,47 @@ impl WsHandler for WebSocketActor { Ok(Some(WsMsg::EpicDeleted(epic_id, n))) } } + +pub struct TransformEpic { + pub epic_id: EpicId, + pub issue_type: IssueType, +} + +impl WsHandler for WebSocketActor { + fn handle_msg(&mut self, msg: TransformEpic, _ctx: &mut Self::Context) -> WsResult { + let epic: jirs_data::Epic = db_or_debug_and_return!( + self, + database_actor::epics::FindEpic { + epic_id: msg.epic_id + } + ); + let issue: database_actor::models::Issue = db_or_debug_and_return!( + self, + database_actor::issues::CreateIssue { + title: epic.name, + issue_type: msg.issue_type, + issue_status_id: 0, + priority: Default::default(), + description: epic.description_html, + description_text: epic.description, + estimate: None, + time_spent: None, + time_remaining: None, + project_id: epic.project_id, + reporter_id: epic.user_id, + user_ids: vec![epic.user_id], + epic_id: None + } + ); + let n = db_or_debug_and_return!( + self, + database_actor::epics::DeleteEpic { + user_id: epic.user_id, + epic_id: epic.id + } + ); + self.broadcast(&WsMsg::EpicDeleted(msg.epic_id, n)); + self.broadcast(&WsMsg::IssueCreated(issue.into())); + Ok(None) + } +} diff --git a/actors/websocket-actor/src/lib.rs b/actors/websocket-actor/src/lib.rs index f0e5f5e6..27edf2e6 100644 --- a/actors/websocket-actor/src/lib.rs +++ b/actors/websocket-actor/src/lib.rs @@ -185,11 +185,27 @@ impl WebSocketActor { // epics WsMsg::EpicsLoad => self.handle_msg(epics::LoadEpics, ctx)?, - WsMsg::EpicCreate(name) => self.handle_msg(epics::CreateEpic { name }, ctx)?, + WsMsg::EpicCreate(name, description, description_html) => self.handle_msg( + epics::CreateEpic { + name, + description_html, + description, + }, + ctx, + )?, WsMsg::EpicUpdate(epic_id, name) => { self.handle_msg(epics::UpdateEpic { epic_id, name }, ctx)? } WsMsg::EpicDelete(epic_id) => self.handle_msg(epics::DeleteEpic { epic_id }, ctx)?, + WsMsg::EpicTransform(epic_id, issue_type) => self.handle_msg( + epics::TransformEpic { + epic_id, + issue_type, + }, + ctx, + )?, + + // hi WsMsg::HighlightCode(lang, code) => { self.handle_msg(hi::HighlightCode(lang, code), ctx)? } diff --git a/derive/derive_db_execute/src/lib.rs b/derive/derive_db_execute/src/lib.rs index 9e8fc818..117bb47f 100644 --- a/derive/derive_db_execute/src/lib.rs +++ b/derive/derive_db_execute/src/lib.rs @@ -31,6 +31,46 @@ fn parse_meta(mut it: Peekable) -> (Peekable, Option TokenStream { let mut it = item.into_iter().peekable(); diff --git a/derive/derive_enum_iter/src/lib.rs b/derive/derive_enum_iter/src/lib.rs index eea53547..8bb1270b 100644 --- a/derive/derive_enum_iter/src/lib.rs +++ b/derive/derive_enum_iter/src/lib.rs @@ -1,10 +1,11 @@ extern crate proc_macro; -use proc_macro::{TokenStream, TokenTree}; +use { + proc_macro::{token_stream::IntoIter, TokenStream, TokenTree}, + std::iter::Peekable, +}; -#[proc_macro_derive(EnumIter)] -pub fn derive_enum_iter(item: TokenStream) -> TokenStream { - let mut it = item.into_iter().peekable(); +fn skip_meta(mut it: Peekable) -> Peekable { while let Some(token) = it.peek() { if let TokenTree::Ident(_) = token { break; @@ -12,20 +13,21 @@ pub fn derive_enum_iter(item: TokenStream) -> TokenStream { it.next(); } } + it +} + +fn consume_ident(mut it: Peekable, name: &str) -> Peekable { if let Some(TokenTree::Ident(ident)) = it.next() { - if ident.to_string().as_str() != "pub" { - panic!("Expect to find keyword pub but was found {:?}", ident) + if ident.to_string().as_str() != name { + panic!("Expect to find keyword {} but was found {:?}", name, ident) } } else { - panic!("Expect to find keyword pub but nothing was found") - } - if let Some(TokenTree::Ident(ident)) = it.next() { - if ident.to_string().as_str() != "enum" { - panic!("Expect to find keyword struct but was found {:?}", ident) - } - } else { - panic!("Expect to find keyword struct but nothing was found") + panic!("Expect to find keyword {} but nothing was found", name) } + it +} + +pub(in crate) fn codegen(mut it: Peekable) -> Result { let name = it .next() .expect("Expect to struct name but nothing was found"); @@ -38,10 +40,10 @@ pub fn derive_enum_iter(item: TokenStream) -> TokenStream { } } } else { - panic!("Enum variants group expected"); + return Err("Enum variants group expected".to_string()); } if variants.is_empty() { - panic!("Enum cannot be empty") + return Err("Enum cannot be empty".to_string()); } let mut code = format!( @@ -72,16 +74,15 @@ impl std::iter::Iterator for {name}Iter {{ match idx { 0 => code.push_str( format!( - "None => Some({name}::{variant}),\n", + " None => Some({name}::{variant}),\n", variant = variant, name = name ) .as_str(), ), - _ if idx == variants.len() - 1 => code.push_str("_ => None,\n"), _ => code.push_str( format!( - "Some({name}::{last_variant}) => Some({name}::{variant}),\n", + " Some({name}::{last_variant}) => Some({name}::{variant}),\n", last_variant = last_variant, variant = variant, name = name, @@ -89,6 +90,9 @@ impl std::iter::Iterator for {name}Iter {{ .as_str(), ), } + if idx == variants.len() - 1 { + code.push_str(" _ => None,\n"); + } last_variant = variant.as_str(); } @@ -113,5 +117,33 @@ impl std::iter::IntoIterator for {name} {{ ) .as_str(), ); + Ok(code) +} + +#[proc_macro_derive(EnumIter)] +pub fn derive_enum_iter(item: TokenStream) -> TokenStream { + let mut it = item.into_iter().peekable(); + it = skip_meta(it); + it = consume_ident(it, "pub"); + it = consume_ident(it, "enum"); + + let code = codegen(it).unwrap(); code.parse().unwrap() } + +// #[cfg(test)] +// mod tests { +// use super::codegen; +// use proc_macro::TokenStream; +// use std::str::FromStr; +// +// #[test] +// fn empty_enum() { +// let it = TokenStream::from_str("enum A {}") +// .unwrap() +// .into_iter() +// .peekable(); +// let code = codegen(it); +// assert_eq!(code, Err("Enum cannot be empty".to_string())); +// } +// } diff --git a/jirs-client/js/css/reports.scss b/jirs-client/js/css/reports.scss index a55e7be2..2789ea22 100644 --- a/jirs-client/js/css/reports.scss +++ b/jirs-client/js/css/reports.scss @@ -17,8 +17,57 @@ } > .issue { + padding: { + top: 10px; + bottom: 10px; + } display: grid; - grid-template-columns: 32px 32px 240px auto 120px; + grid-template-columns: 124px 130px calc(100% - 250px); + grid-template-areas: "type number name" "priority desc desc" "updatedAt desc desc"; + + &:hover { + background-color: var(--issue-background-selected); + } + + .number { + grid-area: number; + > .styledLink { + line-height: 1; + > .styledIcon, > span { + line-height: 1; + } + } + } + + .type { + grid-area: type; + } + + .priority { + grid-area: priority; + } + + .name { + grid-area: name; + } + + .desc { + grid-area: desc; + margin-top: 10px; + font-size: 12px; + color: var(--textLight); + pre * { + font-size: 12px; + color: var(--textLight); + } + } + + .updatedAt { + grid-area: updatedAt; + align-self: end; + color: var(--textLight); + font-size: 12px; + } } > .issue.selected { diff --git a/jirs-client/js/css/styledLink.scss b/jirs-client/js/css/styledLink.scss index d6958e90..e360efc3 100644 --- a/jirs-client/js/css/styledLink.scss +++ b/jirs-client/js/css/styledLink.scss @@ -8,4 +8,19 @@ cursor: pointer; user-select: none; font-size: 14.5px; + + &.withIcon { + display: flex; + + > .styledIcon { + margin-right: 10px; + font-size: 14.5px; + line-height: 2; + } + + > span { + font-size: 14.5px; + line-height: 2; + } + } } diff --git a/jirs-client/src/components/styled_link.rs b/jirs-client/src/components/styled_link.rs index f55e471e..4e2ad67b 100644 --- a/jirs-client/src/components/styled_link.rs +++ b/jirs-client/src/components/styled_link.rs @@ -1,6 +1,7 @@ use { crate::{shared::ToNode, Msg}, seed::{prelude::*, *}, + std::str::FromStr, }; pub struct StyledLink<'l> { @@ -28,6 +29,11 @@ impl<'l> StyledLinkBuilder<'l> { self } + pub fn with_icon(self) -> Self { + self.add_child(crate::components::styled_icon::Icon::Link.into_node()) + .add_class("withIcon") + } + pub fn add_class(mut self, name: &'l str) -> Self { self.class_list.push(name); self @@ -64,12 +70,28 @@ pub fn render(values: StyledLink) -> Node { href, } = values; + let on_click = { + let href = href.to_string(); + mouse_ev("click", move |ev| { + if href.starts_with('/') { + ev.prevent_default(); + ev.stop_propagation(); + if let Ok(url) = seed::Url::from_str(href.as_str()) { + url.go_and_push(); + } + } + + None as Option + }) + }; + a![ C!["styledLink"], attrs![ At::Class => class_list.join(" "), At::Href => href, ], + on_click, children, ] } diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 67d13362..2023523b 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -111,6 +111,7 @@ pub enum Msg { AddEpic, DeleteEpic, UpdateEpic, + TransformEpic, // issue statuses DeleteIssueStatus(IssueStatusId), @@ -172,8 +173,7 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { WebSocketChanged::WebSocketMessageLoaded(v) => { match bincode::deserialize(v.as_slice()) { Ok(WsMsg::Ping | WsMsg::Pong) => { - orders.skip(); - orders.perform_cmd(cmds::timeout(300, || { + orders.skip().perform_cmd(cmds::timeout(300, || { Msg::WebSocketChange(WebSocketChanged::SendPing) })); } diff --git a/jirs-client/src/modals/epics_edit/update.rs b/jirs-client/src/modals/epics_edit/update.rs index 7ff261fc..b68d4ad1 100644 --- a/jirs-client/src/modals/epics_edit/update.rs +++ b/jirs-client/src/modals/epics_edit/update.rs @@ -1,6 +1,6 @@ use { crate::{send_ws_msg, FieldId, Msg, OperationKind, ResourceKind}, - jirs_data::{EpicFieldId, WsMsg}, + jirs_data::{EpicFieldId, IssueType, WsMsg}, seed::prelude::*, }; @@ -40,6 +40,16 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde orders, ); } + Msg::TransformEpic => { + let epic_id = modal.epic_id; + let issue_type: IssueType = modal.transform_into.value.into(); + send_ws_msg( + WsMsg::EpicTransform(epic_id, issue_type), + model.ws.as_ref(), + orders, + ); + orders.skip().send_msg(Msg::ModalDropped); + } _ => (), }; } diff --git a/jirs-client/src/modals/epics_edit/view.rs b/jirs-client/src/modals/epics_edit/view.rs index 1bc4afbb..bd098cf3 100644 --- a/jirs-client/src/modals/epics_edit/view.rs +++ b/jirs-client/src/modals/epics_edit/view.rs @@ -69,7 +69,15 @@ fn transform_into_available(modal: &super::Model) -> Node { .state(&modal.transform_into) .build(FieldId::EditEpic(EpicFieldId::TransformInto)) .into_node(); - let execute = StyledButton::build().text("Transform").build().into_node(); + let execute = StyledButton::build() + .on_click(mouse_ev("click", |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Msg::TransformEpic + })) + .text("Transform") + .build() + .into_node(); div![C!["transform available"], div![types], div![execute]] } diff --git a/jirs-client/src/modals/issues_create/update.rs b/jirs-client/src/modals/issues_create/update.rs index 79b56f88..ecb3b86a 100644 --- a/jirs-client/src/modals/issues_create/update.rs +++ b/jirs-client/src/modals/issues_create/update.rs @@ -21,7 +21,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde match msg { Msg::AddEpic => { send_ws_msg( - WsMsg::EpicCreate(modal.title_state.value.clone()), + WsMsg::EpicCreate(modal.title_state.value.clone(), None, None), model.ws.as_ref(), orders, ); diff --git a/jirs-client/src/pages/reports_page/update.rs b/jirs-client/src/pages/reports_page/update.rs index cc8d8c4a..69abede8 100644 --- a/jirs-client/src/pages/reports_page/update.rs +++ b/jirs-client/src/pages/reports_page/update.rs @@ -4,16 +4,21 @@ use { model::{Model, Page, PageContent}, pages::reports_page::model::ReportsPage, ws::board_load, - Msg, WebSocketChanged, + Msg, OperationKind, ResourceKind, }, - jirs_data::WsMsg, seed::prelude::*, }; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { - if let Msg::ChangePage(Page::Reports) = msg { - build_page_content(model); - } + match msg { + Msg::ChangePage(Page::Reports) => build_page_content(model), + Msg::ResourceChanged(ResourceKind::Auth, OperationKind::SingleLoaded, _) + if model.page == Page::Reports => + { + build_page_content(model); + } + _ => {} + }; let page = match &mut model.page_content { PageContent::Reports(page) => page, @@ -25,7 +30,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } match msg { Msg::UserChanged(Some(..)) - | Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::AuthorizeLoaded(..))) + | Msg::ResourceChanged(ResourceKind::Auth, OperationKind::SingleLoaded, _) | Msg::ChangePage(Page::Reports) => { board_load(model, orders); } @@ -39,6 +44,6 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } } -fn build_page_content(model: &mut Model) { +pub fn build_page_content(model: &mut Model) { model.page_content = PageContent::Reports(Box::new(ReportsPage::default())) } diff --git a/jirs-client/src/pages/reports_page/view.rs b/jirs-client/src/pages/reports_page/view.rs index b7c3f428..6da7557a 100644 --- a/jirs-client/src/pages/reports_page/view.rs +++ b/jirs-client/src/pages/reports_page/view.rs @@ -1,6 +1,6 @@ use { crate::{ - components::styled_icon::StyledIcon, + components::{styled_icon::StyledIcon, styled_link::*}, model::{Model, PageContent}, pages::reports_page::model::ReportsPage, shared::{inner_layout, ToNode}, @@ -24,9 +24,14 @@ pub fn view(model: &Model) -> Node { _ => return empty![], }; + let project_name = model + .project + .as_ref() + .map(|p| p.name.as_str()) + .unwrap_or_default(); let this_month_updated = this_month_updated(model, page); let graph = this_month_graph(page, &this_month_updated); - let list = issue_list(page, this_month_updated.as_slice()); + let list = issue_list(page, project_name, this_month_updated.as_slice()); let body = section![C!["top"], h1![C!["header"], "Reports"], graph, list]; @@ -91,26 +96,32 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node = mouse_ev(Ev::MouseEnter, move |_| { - Some(Msg::PageChanged(PageChanged::Reports( - ReportsPageChange::DayHovered(Some(day)), - ))) + let on_hover = mouse_ev(Ev::MouseEnter, move |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Msg::PageChanged(PageChanged::Reports(ReportsPageChange::DayHovered(Some( + day, + )))) }); - let on_blur: EventHandler = mouse_ev(Ev::MouseLeave, move |_| { - Some(Msg::PageChanged(PageChanged::Reports( - ReportsPageChange::DayHovered(None), - ))) + + let on_blur = mouse_ev(Ev::MouseLeave, move |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Msg::PageChanged(PageChanged::Reports(ReportsPageChange::DayHovered(None))) }); + let selected = page.selected_day; let current_date = day; - let on_click: EventHandler = mouse_ev(Ev::MouseLeave, move |_| { - Some(Msg::PageChanged(PageChanged::Reports( - ReportsPageChange::DaySelected(match selected { + let on_click = mouse_ev("click", move |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Msg::PageChanged(PageChanged::Reports(ReportsPageChange::DaySelected( + match selected { Some(_) => None, None => Some(current_date), - }), + }, ))) }); @@ -149,44 +160,76 @@ fn this_month_graph(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node Node { - let mut children: Vec> = vec![]; - for issue in this_month_updated { - let date = issue.updated_at.date(); - let day = date.format("%Y-%m-%d").to_string(); - let active_class = match (page.hovered_day.as_ref(), page.selected_day.as_ref()) { - (Some(d), _) if *d == date => "selected", - (_, Some(d)) if *d == date => "selected", - (Some(_), _) | (_, Some(_)) => "nonSelected", - _ => "", - }; - let Issue { - title, - issue_type, - priority, - description, - issue_status_id: _, - .. - } = issue; - let type_icon = StyledIcon::build(issue_type.clone().into()) - .build() - .into_node(); - let priority_icon = StyledIcon::build(priority.clone().into()) - .build() - .into_node(); - children.push(li![ - C!["issue"], - C![active_class], - span![C!["priority"], priority_icon], - span![C!["type"], type_icon], - span![C!["name"], title.as_str()], - span![ - C!["desc"], - description.as_ref().cloned().unwrap_or_default() - ], - span![C!["updatedAt"], day.as_str()], - ]); +#[derive(PartialEq)] +enum SelectionState { + Inactive, + Selected, + NotSelected, +} + +impl SelectionState { + fn to_str(&self) -> &str { + match self { + SelectionState::Inactive => "", + SelectionState::Selected => "selected", + SelectionState::NotSelected => "nonSelected", + } } +} + +fn issue_list(page: &ReportsPage, project_name: &str, this_month_updated: &[&Issue]) -> Node { + let children: Vec> = this_month_updated + .iter() + .map(|issue| { + let date = issue.updated_at.date(); + + let selection_state = match (page.hovered_day.as_ref(), page.selected_day.as_ref()) { + (Some(d), _) if *d == date => SelectionState::Selected, + (_, Some(d)) if *d == date => SelectionState::Selected, + (Some(_), _) | (_, Some(_)) => SelectionState::NotSelected, + _ => SelectionState::Inactive, + }; + + let Issue { + id, + title, + issue_type, + priority, + description, + .. + } = issue; + let day = date.format("%Y-%m-%d").to_string(); + + let type_icon = StyledIcon::build(issue_type.clone().into()) + .build() + .into_node(); + let priority_icon = StyledIcon::build(priority.clone().into()) + .build() + .into_node(); + let desc = Node::from_html( + description + .as_deref() + .unwrap_or_default() + ); + let link = StyledLink::build() + .with_icon() + .text(format!("{}-{}", project_name, id).as_str()) + .href(format!("/issues/{}", id).as_str()) + .build() + .into_node(); + + li![ + C!["issue"], + C![selection_state.to_str()], + div![C!["number"], link], + div![C!["type"], type_icon], + IF!( selection_state != SelectionState::NotSelected => div![C!["priority"], priority_icon]), + IF!( selection_state != SelectionState::NotSelected => div![C!["name"], title.as_str()]), + IF!( selection_state != SelectionState::NotSelected => div![C!["desc"], desc]), + IF!( selection_state != SelectionState::NotSelected => div![C!["updatedAt"], day.as_str()]), + ] + }) + .collect(); div![ C!["issueList"], h5![C!["issueListHeader"], "Issues this month"], diff --git a/migrations/2021-01-18-220341_add_epics_fields/down.sql b/migrations/2021-01-18-220341_add_epics_fields/down.sql new file mode 100644 index 00000000..32a86d1d --- /dev/null +++ b/migrations/2021-01-18-220341_add_epics_fields/down.sql @@ -0,0 +1,4 @@ +ALTER TABLE epics + DROP COLUMN description; +ALTER TABLE epics + DROP COLUMN description_html; diff --git a/migrations/2021-01-18-220341_add_epics_fields/up.sql b/migrations/2021-01-18-220341_add_epics_fields/up.sql new file mode 100644 index 00000000..55acd9da --- /dev/null +++ b/migrations/2021-01-18-220341_add_epics_fields/up.sql @@ -0,0 +1,4 @@ +ALTER TABLE epics + ADD COLUMN description TEXT; +ALTER TABLE epics + ADD COLUMN description_html TEXT; diff --git a/shared/jirs-data/src/lib.rs b/shared/jirs-data/src/lib.rs index 72ae02f2..b01a090c 100644 --- a/shared/jirs-data/src/lib.rs +++ b/shared/jirs-data/src/lib.rs @@ -40,6 +40,7 @@ pub type UsernameString = String; pub type TitleString = String; pub type NameString = String; pub type AvatarUrl = String; +pub type DescriptionString = String; pub type Code = String; pub type Lang = String; @@ -226,9 +227,9 @@ pub struct Issue { pub title: String, pub issue_type: IssueType, pub priority: IssuePriority, - pub list_position: i32, - pub description: Option, - pub description_text: Option, + pub list_position: ListPosition, + pub description: Option, + pub description_text: Option, pub estimate: Option, pub time_spent: Option, pub time_remaining: Option, @@ -256,12 +257,12 @@ pub struct IssueStatus { #[cfg_attr(feature = "backend", derive(Queryable))] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Invitation { - pub id: i32, + pub id: InvitationId, pub name: String, pub email: String, pub state: InvitationState, - pub project_id: i32, - pub invited_by_id: i32, + pub project_id: ProjectId, + pub invited_by_id: UserId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub bind_token: Uuid, @@ -373,6 +374,8 @@ pub struct Epic { pub updated_at: NaiveDateTime, pub starts_at: Option, pub ends_at: Option, + pub description: Option, + pub description_html: Option, } pub type FontStyle = u8; diff --git a/shared/jirs-data/src/msg.rs b/shared/jirs-data/src/msg.rs index 82b46a97..228c00bf 100644 --- a/shared/jirs-data/src/msg.rs +++ b/shared/jirs-data/src/msg.rs @@ -1,11 +1,12 @@ +use crate::DescriptionString; use { crate::{ AvatarUrl, BindToken, Code, Comment, CommentId, CreateCommentPayload, CreateIssuePayload, EmailString, Epic, EpicId, HighlightedCode, Invitation, InvitationId, InvitationToken, - Issue, IssueFieldId, IssueId, IssueStatus, IssueStatusId, Lang, ListPosition, Message, - MessageId, NameString, NumberOfDeleted, PayloadVariant, Position, Project, TitleString, - UpdateCommentPayload, UpdateProjectPayload, User, UserId, UserProject, UserProjectId, - UserRole, UsernameString, + Issue, IssueFieldId, IssueId, IssueStatus, IssueStatusId, IssueType, Lang, ListPosition, + Message, MessageId, NameString, NumberOfDeleted, PayloadVariant, Position, Project, + TitleString, UpdateCommentPayload, UpdateProjectPayload, User, UserId, UserProject, + UserProjectId, UserRole, UsernameString, }, serde::{Deserialize, Serialize}, uuid::Uuid, @@ -229,12 +230,17 @@ pub enum WsMsg { // epics EpicsLoad, EpicsLoaded(Vec), - EpicCreate(NameString), + EpicCreate( + NameString, + Option, + Option, + ), EpicCreated(Epic), EpicUpdate(EpicId, NameString), EpicUpdated(Epic), EpicDelete(EpicId), EpicDeleted(EpicId, NumberOfDeleted), + EpicTransform(EpicId, IssueType), // highlight HighlightCode(Lang, Code),