use seed::{prelude::*, *}; use jirs_data::*; use crate::api::send_ws_msg; use crate::model::{EditIssueModal, ModalType, Model}; use crate::shared::styled_avatar::StyledAvatar; use crate::shared::styled_button::StyledButton; use crate::shared::styled_editor::StyledEditor; use crate::shared::styled_field::StyledField; use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_input::StyledInput; use crate::shared::styled_select::{SelectOption, StyledSelect, StyledSelectChange}; use crate::shared::styled_textarea::StyledTextarea; use crate::shared::ToNode; use crate::{FieldChange, FieldId, IssueId, Msg}; pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { 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); match msg { Msg::WsMsg(WsMsg::IssueUpdated(issue)) => { modal.payload = issue.clone().into(); } Msg::StyledSelectChanged( FieldId::IssueTypeEditModalTop, StyledSelectChange::Changed(value), ) => { modal.payload.issue_type = (*value).into(); send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); } Msg::StyledSelectChanged( FieldId::StatusIssueEditModal, StyledSelectChange::Changed(value), ) => { modal.payload.status = (*value).into(); send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); } Msg::StyledSelectChanged( FieldId::ReporterIssueEditModal, StyledSelectChange::Changed(value), ) => { modal.payload.reporter_id = *value as i32; send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); } Msg::StyledSelectChanged( FieldId::AssigneesIssueEditModal, StyledSelectChange::Changed(value), ) => { modal.payload.user_ids.push(*value as i32); send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); } Msg::StyledSelectChanged( FieldId::AssigneesIssueEditModal, 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())); } Msg::StyledSelectChanged( FieldId::PriorityIssueEditModal, StyledSelectChange::Changed(value), ) => { modal.payload.priority = (*value).into(); send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); } Msg::InputChanged(FieldId::TitleIssueEditModal, value) => { modal.payload.title = value.clone(); send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); } Msg::InputChanged(FieldId::DescriptionIssueEditModal, value) => { modal.payload.description = Some(value.clone()); modal.payload.description_text = Some(value.clone()); send_ws_msg(WsMsg::IssueUpdateRequest(modal.id, modal.payload.clone())); } Msg::ModalChanged(FieldChange::TabChanged(FieldId::DescriptionIssueEditModal, mode)) => { modal.description_editor_mode = mode.clone(); } _ => (), } } pub fn view(model: &Model, modal: &EditIssueModal) -> Node { 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(); let issue_type_select = StyledSelect::build(FieldId::IssueTypeEditModalTop) .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| IssueTypeTopOption(issue_id, t)) .collect(), ) .selected(vec![IssueTypeTopOption( issue_id, payload.issue_type.clone(), )]) .build() .into_node(); 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::() .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(); 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)) }); let copy_button = StyledButton::build() .empty() .icon(Icon::Link) .on_click(click_handler) .children(vec![span![if *link_copied { "Link Copied" } else { "Copy link" }]]) .build() .into_node(); let delete_button = StyledButton::build() .empty() .icon(Icon::Trash.into_styled_builder().size(19).build()) .on_click(delete_confirmation_handler) .build() .into_node(); let close_button = StyledButton::build() .empty() .icon(Icon::Close.into_styled_builder().size(24).build()) .on_click(close_handler) .build() .into_node(); // left let title = StyledTextarea::build() .value(payload.title.as_str()) .add_class("textarea") .max_height(48) .height(0) .build(FieldId::TitleIssueEditModal) .into_node(); 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()) .update_on(Ev::Change) .build() .into_node(); let description_field = StyledField::build().input(description).build().into_node(); // right let status = StyledSelect::build(FieldId::StatusIssueEditModal) .name("status") .opened(status_state.opened) .normal() .text_filter(status_state.text_filter.as_str()) .options( IssueStatus::ordered() .into_iter() .map(|opt| IssueStatusOption(issue_id, opt)) .collect(), ) .selected(vec![IssueStatusOption(issue_id, payload.status.clone())]) .valid(true) .build() .into_node(); 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(); let reporter = StyledSelect::build(FieldId::ReporterIssueEditModal) .name("reporter") .opened(modal.reporter_state.opened) .normal() .text_filter(modal.reporter_state.text_filter.as_str()) .options(model.users.iter().map(|user| UserOption(user)).collect()) .selected( model .users .iter() .filter(|user| payload.reporter_id == user.id) .map(|user| UserOption(user)) .collect(), ) .build() .into_node(); let reporter_field = StyledField::build() .input(reporter) .label("Reporter") .build() .into_node(); let priority = StyledSelect::build(FieldId::PriorityIssueEditModal) .name("assignees") .opened(modal.priority_state.opened) .normal() .text_filter(modal.priority_state.text_filter.as_str()) .options( IssuePriority::ordered() .into_iter() .map(|p| IssuePriorityOption(p)) .collect(), ) .selected(vec![IssuePriorityOption(payload.priority.clone())]) .build() .into_node(); let priority_field = StyledField::build() .input(priority) .label("Priority") .build() .into_node(); let estimate = StyledInput::build(FieldId::EstimateIssueEditModal) .valid(true) .build() .into_node(); let estimate_field = StyledField::build() .input(estimate) .label("Original Estimate (hours)") .build() .into_node(); 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"], title, description_field, div![attrs![At::Class => "comments"]], ], div![ attrs![At::Class => "right"], status_field, assignees_field, reporter_field, priority_field, estimate_field ], ], ] } #[derive(PartialOrd, PartialEq, Debug)] pub struct IssueTypeTopOption(pub IssueId, pub IssueType); impl SelectOption for IssueTypeTopOption { fn into_option(self) -> Node { let name = self.1.to_label().to_owned(); let icon = StyledIcon::build(self.1.into()) .add_class("issueTypeIcon".to_string()) .build() .into_node(); div![ attrs![At::Class => "optionItem"], icon, div![attrs![At::Class => "optionLabel typeLabel"], name] ] } fn into_value(self) -> Node { 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() } } ///// #[derive(PartialOrd, PartialEq, Debug)] pub struct IssueStatusOption(pub IssueId, pub IssueStatus); impl SelectOption for IssueStatusOption { fn into_option(self) -> Node { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 } }