Add transform epic into issue
This commit is contained in:
parent
8412a113e7
commit
39021a8643
@ -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
|
||||
|
@ -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<DescriptionString>,
|
||||
description_html => Option<DescriptionString>
|
||||
}
|
||||
|
||||
db_update! {
|
||||
|
@ -103,6 +103,18 @@ table! {
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
ends_at -> Nullable<Timestamp>,
|
||||
/// The `description` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Text>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description -> Nullable<Text>,
|
||||
/// The `description_html` column of the `epics` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Text>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
description_html -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<LoadEpics> for WebSocketActor {
|
||||
|
||||
pub struct CreateEpic {
|
||||
pub name: NameString,
|
||||
pub description: Option<DescriptionString>,
|
||||
pub description_html: Option<DescriptionString>,
|
||||
}
|
||||
|
||||
impl WsHandler<CreateEpic> 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<CreateEpic> for WebSocketActor {
|
||||
database_actor::epics::CreateEpic {
|
||||
user_id: *user_id,
|
||||
project_id: *project_id,
|
||||
description,
|
||||
description_html,
|
||||
name,
|
||||
}
|
||||
);
|
||||
@ -77,3 +86,47 @@ impl WsHandler<DeleteEpic> for WebSocketActor {
|
||||
Ok(Some(WsMsg::EpicDeleted(epic_id, n)))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TransformEpic {
|
||||
pub epic_id: EpicId,
|
||||
pub issue_type: IssueType,
|
||||
}
|
||||
|
||||
impl WsHandler<TransformEpic> 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)
|
||||
}
|
||||
}
|
||||
|
@ -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)?
|
||||
}
|
||||
|
@ -31,6 +31,46 @@ fn parse_meta(mut it: Peekable<IntoIter>) -> (Peekable<IntoIter>, Option<Attribu
|
||||
(it, attrs)
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
///
|
||||
/// Example:
|
||||
/// ```
|
||||
/// pub struct Issue {
|
||||
/// pub id: i32,
|
||||
/// pub name: String,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Execute)]
|
||||
/// #[db_exec(schema = "issues", result = "Issue", find = "issues.find(msg.id)")]
|
||||
/// pub struct FindOne {
|
||||
/// pub id: i32,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Execute)]
|
||||
/// #[db_exec(schema = "issues", result = "Issue", load = "issues")]
|
||||
/// pub struct LoadAll;
|
||||
///
|
||||
/// #[derive(Execute)]
|
||||
/// #[db_exec(schema = "issues", result = "usize", destroy = "diesel::delete(issues.find(msg.id)")]
|
||||
/// pub struct DeleteOne {
|
||||
/// pub id: i32
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Execute)]
|
||||
/// #[db_exec(schema = "issues", result = "Issue", destroy = "diesel::insert_into(issues).values(name.eq(msg.name))")]
|
||||
/// pub struct CreateOne {
|
||||
/// pub name: String
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Execute)]
|
||||
/// #[db_exec(schema = "issues", result = "Issue", destroy = "diesel::update(issues.find(msg.id)).set(name.eq(msg.name))")]
|
||||
/// pub struct UpdateOne {
|
||||
/// pub id: i32,
|
||||
/// pub name: String
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
#[proc_macro_derive(Execute, attributes(db_exec))]
|
||||
pub fn derive_enum_iter(item: TokenStream) -> TokenStream {
|
||||
let mut it = item.into_iter().peekable();
|
||||
|
@ -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<IntoIter>) -> Peekable<IntoIter> {
|
||||
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<IntoIter>, name: &str) -> Peekable<IntoIter> {
|
||||
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<IntoIter>) -> Result<String, String> {
|
||||
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()));
|
||||
// }
|
||||
// }
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Msg> {
|
||||
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<Msg>
|
||||
})
|
||||
};
|
||||
|
||||
a![
|
||||
C!["styledLink"],
|
||||
attrs![
|
||||
At::Class => class_list.join(" "),
|
||||
At::Href => href,
|
||||
],
|
||||
on_click,
|
||||
children,
|
||||
]
|
||||
}
|
||||
|
@ -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<Msg>) {
|
||||
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)
|
||||
}));
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
@ -69,7 +69,15 @@ fn transform_into_available(modal: &super::Model) -> Node<Msg> {
|
||||
.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]]
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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<Msg>) {
|
||||
if let Msg::ChangePage(Page::Reports) = msg {
|
||||
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()))
|
||||
}
|
||||
|
@ -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<Msg> {
|
||||
_ => 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<M
|
||||
+ (legend_margin_width + SVG_MARGIN_X as f64);
|
||||
let height = num_issues as f64 * piece_height;
|
||||
|
||||
let day = page.first_day.with_day0(day).unwrap();
|
||||
let day = page.first_day.clone().with_day0(day).unwrap();
|
||||
|
||||
let on_hover: EventHandler<Msg> = 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<Msg> = 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<Msg> = 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<M
|
||||
]
|
||||
}
|
||||
|
||||
fn issue_list(page: &ReportsPage, this_month_updated: &[&Issue]) -> Node<Msg> {
|
||||
let mut children: Vec<Node<Msg>> = vec![];
|
||||
for issue in this_month_updated {
|
||||
#[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<Msg> {
|
||||
let children: Vec<Node<Msg>> = this_month_updated
|
||||
.iter()
|
||||
.map(|issue| {
|
||||
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 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_status_id: _,
|
||||
..
|
||||
} = 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();
|
||||
children.push(li![
|
||||
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![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()],
|
||||
]);
|
||||
}
|
||||
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"],
|
||||
|
4
migrations/2021-01-18-220341_add_epics_fields/down.sql
Normal file
4
migrations/2021-01-18-220341_add_epics_fields/down.sql
Normal file
@ -0,0 +1,4 @@
|
||||
ALTER TABLE epics
|
||||
DROP COLUMN description;
|
||||
ALTER TABLE epics
|
||||
DROP COLUMN description_html;
|
4
migrations/2021-01-18-220341_add_epics_fields/up.sql
Normal file
4
migrations/2021-01-18-220341_add_epics_fields/up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
ALTER TABLE epics
|
||||
ADD COLUMN description TEXT;
|
||||
ALTER TABLE epics
|
||||
ADD COLUMN description_html TEXT;
|
@ -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<String>,
|
||||
pub description_text: Option<String>,
|
||||
pub list_position: ListPosition,
|
||||
pub description: Option<DescriptionString>,
|
||||
pub description_text: Option<DescriptionString>,
|
||||
pub estimate: Option<i32>,
|
||||
pub time_spent: Option<i32>,
|
||||
pub time_remaining: Option<i32>,
|
||||
@ -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<NaiveDateTime>,
|
||||
pub ends_at: Option<NaiveDateTime>,
|
||||
pub description: Option<DescriptionString>,
|
||||
pub description_html: Option<DescriptionString>,
|
||||
}
|
||||
|
||||
pub type FontStyle = u8;
|
||||
|
@ -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<Epic>),
|
||||
EpicCreate(NameString),
|
||||
EpicCreate(
|
||||
NameString,
|
||||
Option<DescriptionString>,
|
||||
Option<DescriptionString>,
|
||||
),
|
||||
EpicCreated(Epic),
|
||||
EpicUpdate(EpicId, NameString),
|
||||
EpicUpdated(Epic),
|
||||
EpicDelete(EpicId),
|
||||
EpicDeleted(EpicId, NumberOfDeleted),
|
||||
EpicTransform(EpicId, IssueType),
|
||||
|
||||
// highlight
|
||||
HighlightCode(Lang, Code),
|
||||
|
Loading…
Reference in New Issue
Block a user