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

453 lines
13 KiB
Rust
Raw Normal View History

2020-04-02 23:51:29 +02:00
use seed::{prelude::*, *};
2020-04-10 08:09:40 +02:00
use jirs_data::{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-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;
2020-04-02 23:51:29 +02:00
use crate::shared::styled_icon::{Icon, StyledIcon};
2020-04-10 08:09:40 +02:00
use crate::shared::styled_select::{SelectOption, StyledSelect, StyledSelectChange};
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;
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-10 08:09:40 +02:00
match msg {
Msg::StyledSelectChanged(
FieldId::IssueTypeEditModalTop,
StyledSelectChange::Changed(value),
) => {
modal.payload.issue_type = (*value).into();
}
Msg::StyledSelectChanged(
FieldId::StatusIssueEditModal,
StyledSelectChange::Changed(value),
) => {
modal.payload.status = (*value).into();
}
Msg::StyledSelectChanged(
FieldId::ReporterIssueEditModal,
StyledSelectChange::Changed(value),
) => {
modal.payload.reporter_id = *value as i32;
}
Msg::StyledSelectChanged(
FieldId::AssigneesIssueEditModal,
StyledSelectChange::Changed(value),
) => {
modal.payload.user_ids.push(*value as i32);
}
Msg::StyledSelectChanged(
FieldId::PriorityIssueEditModal,
StyledSelectChange::Changed(value),
) => {
modal.payload.priority = (*value).into();
}
Msg::InputChanged(FieldId::TitleIssueEditModal, value) => {
modal.payload.title = value.clone();
}
Msg::InputChanged(FieldId::DescriptionIssueEditModal, value) => {
modal.payload.description = Some(value.clone());
modal.payload.description_text = Some(value.clone());
}
Msg::ModalChanged(FieldChange::TabChanged(FieldId::DescriptionIssueEditModal, mode)) => {
modal.description_editor_mode = mode.clone();
}
_ => (),
}
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> {
let EditIssueModal {
id,
link_copied,
payload,
top_type_state,
status_state,
reporter_state: _,
assignees_state: _,
priority_state: _,
description_editor_mode,
} = modal;
let issue_id = id.clone();
2020-04-02 23:51:29 +02:00
2020-04-04 17:42:02 +02:00
let issue_type_select = StyledSelect::build(FieldId::IssueTypeEditModalTop)
.dropdown_width(150)
.name("type")
2020-04-10 08:09:40 +02:00
.text_filter(top_type_state.text_filter.as_str())
.opened(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(),
)
2020-04-10 08:09:40 +02:00
.selected(vec![IssueTypeTopOption(
issue_id,
payload.issue_type.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)
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();
2020-04-09 16:03:11 +02:00
// left
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::TitleIssueEditModal)
.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::DescriptionIssueEditModal)
.text(description_text)
.mode(description_editor_mode.clone())
.build()
.into_node();
let description_field = StyledField::build().input(description).build().into_node();
2020-04-09 16:03:11 +02:00
// right
let status = StyledSelect::build(FieldId::StatusIssueEditModal)
.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| IssueStatusOption(issue_id, opt))
.collect(),
)
2020-04-10 08:09:40 +02:00
.selected(vec![IssueStatusOption(issue_id, payload.status.clone())])
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::AssigneesIssueEditModal)
.name("assignees")
.opened(modal.assignees_state.opened)
.normal()
.multi()
.text_filter(modal.assignees_state.text_filter.as_str())
.options(model.users.iter().map(|user| UserOption(user)).collect())
.selected(
model
.users
.iter()
.filter(|user| payload.user_ids.contains(&user.id))
.map(|user| UserOption(user))
.collect(),
)
.build()
.into_node();
let assignees_field = StyledField::build()
.input(assignees)
.label("Assignees")
.build()
.into_node();
2020-04-09 16:03:11 +02:00
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-10 08:09:40 +02:00
description_field,
2020-04-02 23:51:29 +02:00
div![attrs![At::Class => "comments"]],
],
2020-04-10 08:09:40 +02:00
div![attrs![At::Class => "right"], status_field, assignees_field,],
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
}
}