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
|
|
|
|
}
|
|
|
|
}
|