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

361 lines
10 KiB
Rust
Raw Normal View History

2020-04-02 23:51:29 +02:00
use seed::{prelude::*, *};
2020-04-09 16:03:11 +02:00
use jirs_data::{Issue, IssuePriority, IssueStatus, IssueType, ToVec, User};
2020-04-02 23:51:29 +02:00
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-09 16:03:11 +02:00
use crate::shared::styled_field::StyledField;
2020-04-02 23:51:29 +02:00
use crate::shared::styled_icon::{Icon, StyledIcon};
2020-04-09 16:03:11 +02:00
use crate::shared::styled_select::{SelectOption, StyledSelect};
use crate::shared::styled_textarea::StyledTextarea;
2020-04-02 23:51:29 +02:00
use crate::shared::ToNode;
2020-04-03 16:15:56 +02:00
use crate::{FieldChange, FieldId, IssueId, 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-02 23:51:29 +02:00
pub fn view(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node<Msg> {
let issue_id = issue.id;
2020-04-04 17:42:02 +02:00
let issue_type_select = StyledSelect::build(FieldId::IssueTypeEditModalTop)
.dropdown_width(150)
.name("type")
2020-04-09 16:03:11 +02:00
.text_filter(modal.top_type_state.text_filter.as_str())
.opened(modal.top_type_state.opened)
2020-04-04 17:42:02 +02:00
.valid(true)
2020-04-09 16:03:11 +02:00
.options(
IssueType::ordered()
.into_iter()
.map(|t| IssueTypeTopOption(issue_id, t))
.collect(),
)
.selected(vec![IssueTypeTopOption(issue_id, modal.value.clone())])
2020-04-04 17:42:02 +02:00
.build()
.into_node();
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)
.children(vec![span![if modal.link_copied {
"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();
2020-04-09 16:03:11 +02:00
// left
let title = StyledTextarea::build()
.value(issue.title.as_str())
.add_class("textarea")
.max_height(48)
.height(0)
.build(FieldId::TitleIssueEditModal)
.into_node();
// right
let status = StyledSelect::build(FieldId::StatusIssueEditModal)
.name("status")
.opened(modal.status_state.opened)
.text_filter(modal.status_state.text_filter.as_str())
.options(
IssueStatus::ordered()
.into_iter()
.map(|opt| IssueStatusOption(issue_id, opt))
.collect(),
)
.selected(vec![IssueStatusOption(issue_id, issue.status.clone())])
.valid(true)
.build()
.into_node();
// let status_field = StyledField::build()
// .input(status)
// .label("Status")
// .build()
// .into_node();
2020-04-02 23:51:29 +02:00
div![
attrs![At::Class => "issueDetails"],
div![
attrs![At::Class => "topActions"],
issue_type_select,
div![
attrs![At::Class => "topActionsRight"],
copy_button,
delete_button,
close_button
],
],
div![
attrs![At::Class => "content"],
div![
attrs![At::Class => "left"],
2020-04-09 16:03:11 +02:00
title,
2020-04-02 23:51:29 +02:00
div![attrs![At::Class => "description"]],
div![attrs![At::Class => "comments"]],
],
2020-04-09 16:03:11 +02:00
div![attrs![At::Class => "right"], status],
2020-04-02 23:51:29 +02:00
],
]
}
2020-04-04 17:42:02 +02:00
#[derive(PartialOrd, PartialEq, Debug)]
2020-04-09 16:03:11 +02:00
pub struct IssueTypeTopOption(pub IssueId, pub IssueType);
2020-04-04 17:42:02 +02:00
2020-04-09 16:03:11 +02:00
impl SelectOption for IssueTypeTopOption {
2020-04-04 17:42:02 +02:00
fn into_option(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
let icon = StyledIcon::build(self.1.into())
.add_class("issueTypeIcon".to_string())
.build()
.into_node();
div![
2020-04-09 16:03:11 +02:00
attrs![At::Class => "optionItem"],
2020-04-04 17:42:02 +02:00
icon,
2020-04-09 16:03:11 +02:00
div![attrs![At::Class => "optionLabel typeLabel"], name]
2020-04-04 17:42:02 +02:00
]
}
fn into_value(self) -> Node<Msg> {
let issue_id = self.0;
let name = self.1.to_label().to_owned();
StyledButton::build()
.empty()
.children(vec![span![format!("{}-{}", name, issue_id)]])
.icon(StyledIcon::build(self.1.into()).build())
.build()
.into_node()
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.1
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.1.clone().into()
}
}
2020-04-09 16:03:11 +02:00
/////
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueStatusOption(pub IssueId, pub IssueStatus);
impl SelectOption for IssueStatusOption {
fn into_option(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
div![
attrs![At::Class => "type optionItem"],
div![attrs![At::Class => "typeLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = self.1.to_label().to_owned();
div![
attrs![At::Class => "selectItem"],
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
format!("{}", self.0)
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.1.clone().into()
}
}
#[derive(PartialOrd, PartialEq, Debug)]
pub struct IssueTypeOption(pub IssueType);
impl SelectOption for IssueTypeOption {
fn into_option(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let icon = StyledIcon::build(self.0.into())
.add_class("issueTypeIcon")
.build()
.into_node();
div![
attrs![At::Class => "type optionItem"],
icon,
div![attrs![At::Class => "typeLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = self.0.to_label().to_owned();
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
div![
attrs![At::Class => "selectItem"],
type_icon,
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.0.clone().into()
}
}
#[derive(Debug, PartialEq)]
pub struct IssuePriorityOption(IssuePriority);
impl SelectOption for IssuePriorityOption {
fn into_option(self) -> Node<Msg> {
let name = format!("{}", self.0);
let icon = StyledIcon::build(self.0.into())
.add_class("issuePriorityIcon")
.size(18)
.build()
.into_node();
div![
attrs![At::Class => format!("priority optionItem {}", name)],
icon,
div![attrs![At::Class => "priorityLabel optionLabel"], name]
]
}
fn into_value(self) -> Node<Msg> {
let name = format!("{}", self.0);
let type_icon = StyledIcon::build(self.0.into()).build().into_node();
div![
attrs![At::Class => format!("selectItem priority {}", name)],
type_icon,
div![attrs![At::Class => "selectItemLabel"], name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0
.to_string()
.to_lowercase()
.contains(&text_filter.to_lowercase())
}
fn to_value(&self) -> u32 {
self.0.clone().into()
}
}
#[derive(Debug, PartialEq)]
pub struct UserOption<'opt>(pub &'opt User);
impl<'opt> UserOption<'opt> {
fn avatar_node(&self) -> Node<Msg> {
let user = self.0;
StyledAvatar::build()
.avatar_url(user.avatar_url.as_ref().cloned().unwrap_or_default())
.size(20)
.name(user.name.as_str())
.build()
.into_node()
}
}
impl<'opt> SelectOption for UserOption<'opt> {
fn into_option(self) -> Node<Msg> {
let user = self.0;
let styled_avatar = self.avatar_node();
div![
attrs![At::Class => "user optionItem"],
styled_avatar,
div![attrs![At::Class => "typeLabel optionLabel"], user.name]
]
}
fn into_value(self) -> Node<Msg> {
let user = self.0;
let styled_avatar = self.avatar_node();
div![
attrs![At::Class => "selectItem"],
styled_avatar,
div![attrs![At::Class => "selectItemLabel"], user.name]
]
}
fn match_text_filter(&self, text_filter: &str) -> bool {
self.0.name.contains(text_filter)
}
fn to_value(&self) -> u32 {
self.0.id as u32
}
}