bitque/jirs-client/src/modal/issue_details.rs

454 lines
15 KiB
Rust
Raw Normal View History

2020-04-02 23:51:29 +02:00
use seed::{prelude::*, *};
2020-04-10 22:33:07 +02:00
use jirs_data::*;
2020-04-02 23:51:29 +02:00
2020-04-10 22:33:07 +02:00
use crate::api::send_ws_msg;
2020-04-03 16:15:56 +02:00
use crate::model::{EditIssueModal, ModalType, Model};
2020-04-09 16:03:11 +02:00
use crate::shared::styled_avatar::StyledAvatar;
2020-04-02 23:51:29 +02:00
use crate::shared::styled_button::StyledButton;
2020-04-10 08:09:40 +02:00
use crate::shared::styled_editor::StyledEditor;
2020-04-09 16:03:11 +02:00
use crate::shared::styled_field::StyledField;
use crate::shared::styled_icon::Icon;
2020-04-10 22:33:07 +02:00
use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{StyledSelect, StyledSelectChange};
use crate::shared::styled_select_child::ToStyledSelectChild;
2020-04-09 16:03:11 +02:00
use crate::shared::styled_textarea::StyledTextarea;
2020-04-02 23:51:29 +02:00
use crate::shared::ToNode;
use crate::{EditIssueModalFieldId, FieldChange, FieldId, Msg};
2020-04-02 23:51:29 +02:00
2020-04-09 16:03:11 +02:00
pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let modal: &mut EditIssueModal = match model.modals.get_mut(0) {
Some(ModalType::EditIssue(_issue_id, modal)) => modal,
_ => return,
};
modal.top_type_state.update(msg, orders);
modal.status_state.update(msg, orders);
modal.reporter_state.update(msg, orders);
modal.assignees_state.update(msg, orders);
modal.priority_state.update(msg, orders);
2020-04-10 08:09:40 +02:00
match msg {
2020-04-10 22:33:07 +02:00
Msg::WsMsg(WsMsg::IssueUpdated(issue)) => {
modal.payload = issue.clone().into();
}
2020-04-10 08:09:40 +02:00
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::IssueType),
2020-04-10 08:09:40 +02:00
StyledSelectChange::Changed(value),
) => {
modal.payload.issue_type = (*value).into();
2020-04-10 22:33:07 +02:00
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
2020-04-10 08:09:40 +02:00
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::Status),
2020-04-10 08:09:40 +02:00
StyledSelectChange::Changed(value),
) => {
modal.payload.status = (*value).into();
2020-04-10 22:33:07 +02:00
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
2020-04-10 08:09:40 +02:00
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::Reporter),
2020-04-10 08:09:40 +02:00
StyledSelectChange::Changed(value),
) => {
modal.payload.reporter_id = *value as i32;
2020-04-10 22:33:07 +02:00
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
2020-04-10 08:09:40 +02:00
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::Assignees),
2020-04-10 08:09:40 +02:00
StyledSelectChange::Changed(value),
) => {
modal.payload.user_ids.push(*value as i32);
2020-04-10 22:33:07 +02:00
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::Assignees),
2020-04-10 22:33:07 +02:00
StyledSelectChange::RemoveMulti(value),
) => {
let mut old = vec![];
std::mem::swap(&mut old, &mut modal.payload.user_ids);
let dropped = *value as i32;
for id in old.into_iter() {
if id != dropped {
modal.payload.user_ids.push(id);
}
}
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
2020-04-10 08:09:40 +02:00
}
Msg::StyledSelectChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::Priority),
2020-04-10 08:09:40 +02:00
StyledSelectChange::Changed(value),
) => {
modal.payload.priority = (*value).into();
2020-04-10 22:33:07 +02:00
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
2020-04-10 08:09:40 +02:00
}
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Title), value) => {
2020-04-10 08:09:40 +02:00
modal.payload.title = value.clone();
2020-04-10 22:33:07 +02:00
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
2020-04-10 08:09:40 +02:00
}
Msg::InputChanged(FieldId::EditIssueModal(EditIssueModalFieldId::Description), value) => {
2020-04-10 08:09:40 +02:00
modal.payload.description = Some(value.clone());
modal.payload.description_text = Some(value.clone());
2020-04-10 22:33:07 +02:00
send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone()));
2020-04-10 08:09:40 +02:00
}
Msg::ModalChanged(FieldChange::TabChanged(
FieldId::EditIssueModal(EditIssueModalFieldId::Description),
mode,
)) => {
2020-04-10 08:09:40 +02:00
modal.description_editor_mode = mode.clone();
}
Msg::ModalChanged(FieldChange::ToggleCreateComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
flag,
)) => {
modal.creating_comment = *flag;
}
Msg::GlobalKeyDown { key, .. } if key.as_str() == "m" && !modal.creating_comment => {
orders
.skip()
.send_msg(Msg::ModalChanged(FieldChange::ToggleCreateComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
true,
)));
}
2020-04-10 08:09:40 +02:00
_ => (),
}
2020-04-09 16:03:11 +02:00
}
2020-04-10 08:09:40 +02:00
pub fn view(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
div![
attrs![At::Class => "issueDetails"],
top_modal_row(model, modal),
div![
attrs![At::Class => "content"],
left_modal_column(model, modal),
right_modal_column(model, modal),
],
]
}
fn top_modal_row(_model: &Model, modal: &EditIssueModal) -> Node<Msg> {
2020-04-10 08:09:40 +02:00
let EditIssueModal {
id,
payload,
top_type_state,
link_copied,
..
2020-04-10 08:09:40 +02:00
} = modal;
2020-04-02 23:51:29 +02:00
let issue_id = id.clone();
2020-04-02 23:51:29 +02:00
let click_handler = mouse_ev(Ev::Click, move |_| {
use wasm_bindgen::JsCast;
let link = format!("http://localhost:7000/issues/{id}", id = issue_id);
let el = match seed::html_document().create_element("textarea") {
Ok(el) => el
.dyn_ref::<web_sys::HtmlTextAreaElement>()
.unwrap()
.clone(),
_ => return Msg::NoOp,
};
seed::body().append_child(&el).unwrap();
el.set_text_content(Some(link.as_str()));
el.select();
el.set_selection_range(0, 9999).unwrap();
seed::html_document().exec_command("copy").unwrap();
seed::body().remove_child(&el).unwrap();
2020-04-03 16:15:56 +02:00
Msg::ModalChanged(FieldChange::LinkCopied(FieldId::CopyButtonLabel, true))
});
let close_handler = mouse_ev(Ev::Click, |_| Msg::ModalDropped);
let delete_confirmation_handler = mouse_ev(Ev::Click, move |_| {
Msg::ModalOpened(ModalType::DeleteIssueConfirm(issue_id))
2020-04-02 23:51:29 +02:00
});
let copy_button = StyledButton::build()
.empty()
.icon(Icon::Link)
.on_click(click_handler)
2020-04-10 08:09:40 +02:00
.children(vec![span![if *link_copied {
2020-04-02 23:51:29 +02:00
"Link Copied"
} else {
"Copy link"
}]])
.build()
.into_node();
2020-04-03 08:51:25 +02:00
let delete_button = StyledButton::build()
.empty()
.icon(Icon::Trash.into_styled_builder().size(19).build())
2020-04-03 16:15:56 +02:00
.on_click(delete_confirmation_handler)
2020-04-03 08:51:25 +02:00
.build()
.into_node();
2020-04-02 23:51:29 +02:00
let close_button = StyledButton::build()
.empty()
2020-04-03 08:51:25 +02:00
.icon(Icon::Close.into_styled_builder().size(24).build())
2020-04-03 14:40:21 +02:00
.on_click(close_handler)
2020-04-02 23:51:29 +02:00
.build()
.into_node();
let issue_type_select =
StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::IssueType))
.dropdown_width(150)
.name("type")
.text_filter(top_type_state.text_filter.as_str())
.opened(top_type_state.opened)
.valid(true)
.options(
IssueType::ordered()
.into_iter()
.map(|t| t.to_select_child().name("type"))
.collect(),
)
.selected(vec![{
let id = modal.id.clone();
let issue_type = &payload.issue_type;
issue_type
.to_select_child()
.name("type")
.text(format!("{} - {}", issue_type, id))
}])
.build()
.into_node();
div![
attrs![At::Class => "topActions"],
issue_type_select,
div![
attrs![At::Class => "topActionsRight"],
copy_button,
delete_button,
close_button
],
]
}
fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal {
payload,
description_editor_mode,
creating_comment,
..
} = modal;
2020-04-09 16:03:11 +02:00
let title = StyledTextarea::build()
2020-04-10 08:09:40 +02:00
.value(payload.title.as_str())
2020-04-09 16:03:11 +02:00
.add_class("textarea")
.max_height(48)
.height(0)
.build(FieldId::EditIssueModal(EditIssueModalFieldId::Title))
2020-04-09 16:03:11 +02:00
.into_node();
2020-04-10 08:09:40 +02:00
let description_text = payload.description.as_ref().cloned().unwrap_or_default();
let description =
StyledEditor::build(FieldId::EditIssueModal(EditIssueModalFieldId::Description))
.text(description_text)
.mode(description_editor_mode.clone())
.update_on(Ev::Change)
.build()
.into_node();
let description_field = StyledField::build().input(description).build().into_node();
let user_avatar = StyledAvatar::build()
.add_class("userAvatar")
.size(32)
.avatar_url(
model
.user
.as_ref()
.and_then(|u| u.avatar_url.clone())
.unwrap_or_default(),
)
2020-04-10 08:09:40 +02:00
.build()
.into_node();
let create_comment = if *creating_comment {
2020-04-12 16:01:31 +02:00
use crate::shared::styled_button::Variant as ButtonVariant;
let close_comment_form = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
Msg::ModalChanged(FieldChange::ToggleCreateComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
false,
))
});
let text_area = StyledTextarea::build()
.build(FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody))
.into_node();
2020-04-12 16:01:31 +02:00
let submit = StyledButton::build()
.variant(ButtonVariant::Primary)
.text("Save")
.build()
.into_node();
let cancel = StyledButton::build()
.variant(ButtonVariant::Empty)
.on_click(close_comment_form)
.text("Cancel")
.build()
.into_node();
vec![text_area, div![class!["actions"], submit, cancel]]
} else {
let creating_comment = *creating_comment;
let handler = mouse_ev(Ev::Click, move |ev| {
ev.stop_propagation();
Msg::ModalChanged(FieldChange::ToggleCreateComment(
FieldId::EditIssueModal(EditIssueModalFieldId::CommentBody),
!creating_comment,
))
});
2020-04-12 16:01:31 +02:00
vec![div![class!["fakeTextArea"], "Add a comment...", handler]]
};
div![
class!["left"],
title,
description_field,
div![
class!["comments"],
div![class!["title"], "Comments"],
div![
class!["create"],
user_avatar,
div![
class!["right"],
create_comment,
div![
class!["proTip"],
strong![class!["strong"], "Pro tip: "],
"press ",
span![class!["tipLetter"], "M"],
" to comment"
]
]
]
],
]
}
fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
let EditIssueModal {
payload,
status_state,
reporter_state,
assignees_state,
priority_state,
..
} = modal;
let status = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Status))
2020-04-09 16:03:11 +02:00
.name("status")
2020-04-10 08:09:40 +02:00
.opened(status_state.opened)
.normal()
.text_filter(status_state.text_filter.as_str())
2020-04-09 16:03:11 +02:00
.options(
IssueStatus::ordered()
.into_iter()
.map(|opt| opt.to_select_child().name("status"))
2020-04-09 16:03:11 +02:00
.collect(),
)
.selected(vec![payload.status.to_select_child().name("status")])
2020-04-09 16:03:11 +02:00
.valid(true)
.build()
.into_node();
2020-04-10 08:09:40 +02:00
let status_field = StyledField::build()
.input(status)
.label("Status")
.build()
.into_node();
let assignees = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Assignees))
2020-04-10 08:09:40 +02:00
.name("assignees")
.opened(assignees_state.opened)
.empty()
2020-04-10 08:09:40 +02:00
.multi()
.text_filter(assignees_state.text_filter.as_str())
.options(
model
.users
.iter()
.map(|user| user.to_select_child().name("assignees"))
.collect(),
)
2020-04-10 08:09:40 +02:00
.selected(
model
.users
.iter()
.filter(|user| payload.user_ids.contains(&user.id))
.map(|user| user.to_select_child().name("assignees"))
2020-04-10 08:09:40 +02:00
.collect(),
)
.build()
.into_node();
let assignees_field = StyledField::build()
.input(assignees)
.label("Assignees")
.build()
.into_node();
2020-04-09 16:03:11 +02:00
let reporter = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Reporter))
2020-04-10 22:33:07 +02:00
.name("reporter")
.opened(reporter_state.opened)
.empty()
.text_filter(reporter_state.text_filter.as_str())
.options(
model
.users
.iter()
.map(|user| user.to_select_child().name("reporter"))
.collect(),
)
2020-04-10 22:33:07 +02:00
.selected(
model
.users
.iter()
.filter(|user| payload.reporter_id == user.id)
.map(|user| user.to_select_child().name("reporter"))
2020-04-10 22:33:07 +02:00
.collect(),
)
.build()
.into_node();
let reporter_field = StyledField::build()
.input(reporter)
.label("Reporter")
.build()
.into_node();
let priority = StyledSelect::build(FieldId::EditIssueModal(EditIssueModalFieldId::Priority))
.name("priority")
.opened(priority_state.opened)
.empty()
.text_filter(priority_state.text_filter.as_str())
2020-04-10 22:33:07 +02:00
.options(
IssuePriority::ordered()
.into_iter()
.map(|p| p.to_select_child().name("priority"))
2020-04-10 22:33:07 +02:00
.collect(),
)
.selected(vec![payload.priority.to_select_child().name("priority")])
2020-04-10 22:33:07 +02:00
.build()
.into_node();
let priority_field = StyledField::build()
.input(priority)
.label("Priority")
.build()
.into_node();
let estimate = StyledInput::build(FieldId::EditIssueModal(EditIssueModalFieldId::Estimate))
2020-04-10 22:33:07 +02:00
.valid(true)
.build()
.into_node();
let estimate_field = StyledField::build()
.input(estimate)
.label("Original Estimate (hours)")
.build()
.into_node();
2020-04-02 23:51:29 +02:00
div![
attrs![At::Class => "right"],
status_field,
assignees_field,
reporter_field,
priority_field,
estimate_field
2020-04-02 23:51:29 +02:00
]
}