diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 0699f1c1..981c2467 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -4,6 +4,7 @@ use seed::{prelude::*, *}; use jirs_data::IssueStatus; use crate::model::Page; +use crate::shared::styled_select::StyledSelectChange; mod api; mod api_handlers; @@ -18,9 +19,16 @@ pub type UserId = i32; pub type IssueId = i32; pub type AvatarFilterActive = bool; +#[derive(Clone, Debug)] +pub enum FieldId { + IssueTypeEditModalTop, +} + #[derive(Clone, Debug)] pub enum Msg { NoOp, + StyledSelectChanged(FieldId, StyledSelectChange), + ChangePage(model::Page), CurrentProjectResult(FetchObject), CurrentUserResult(FetchObject), diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 567420ec..6253f572 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -9,9 +9,17 @@ use crate::{IssueId, UserId, HOST_URL}; pub type ProjectId = i32; -#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq)] pub enum ModalType { - EditIssue(IssueId), + EditIssue(IssueId, EditIssueModal), +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, PartialEq)] +pub struct EditIssueModal { + pub id: i32, + pub top_select_opened: bool, + pub top_select_filter: String, + pub value: IssueType, } #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index 0d20bcdc..f1aca05a 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -2,14 +2,14 @@ use seed::{prelude::*, *}; use jirs_data::*; -use crate::model::{Icon, ModalType, Model, Page}; +use crate::model::{EditIssueModal, Icon, ModalType, Model, Page}; use crate::shared::modal::{Modal, Variant as ModalVariant}; use crate::shared::styled_avatar::StyledAvatar; use crate::shared::styled_button::{StyledButton, Variant as ButtonVariant}; use crate::shared::styled_input::StyledInput; -use crate::shared::styled_select::StyledSelect; +use crate::shared::styled_select::{StyledSelect, StyledSelectChange}; use crate::shared::{drag_ev, find_issue, inner_layout, ToNode}; -use crate::{IssueId, Msg}; +use crate::{FieldId, IssueId, Msg}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { match msg { @@ -28,7 +28,18 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order orders .skip() .perform_cmd(crate::api::fetch_current_user(model.host_url.clone())); - model.modal = Some(ModalType::EditIssue(issue_id)); + let value = find_issue(model, issue_id) + .map(|issue| issue.issue_type.clone()) + .unwrap_or_else(|| IssueType::Task); + model.modal = Some(ModalType::EditIssue( + issue_id, + EditIssueModal { + id: issue_id, + top_select_opened: false, + top_select_filter: "".to_string(), + value, + }, + )); } Msg::ToggleAboutTooltip => { model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible; @@ -115,6 +126,23 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order _ => error!("Drag stopped before drop :("), } } + Msg::StyledSelectChanged(FieldId::IssueTypeEditModalTop, change) => { + match (change, model.modal.as_mut()) { + (StyledSelectChange::Text(ref text), Some(ModalType::EditIssue(_, modal))) => { + modal.top_select_filter = text.clone(); + } + ( + StyledSelectChange::DropDownVisibility(flag), + Some(ModalType::EditIssue(_, modal)), + ) => { + modal.top_select_opened = flag; + } + (StyledSelectChange::Changed(value), Some(ModalType::EditIssue(_, modal))) => { + modal.value = value.into(); + } + _ => {} + } + } Msg::IssueUpdateResult(fetched) => { crate::api_handlers::update_issue_response(&fetched, model); } @@ -123,10 +151,10 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } pub fn view(model: &Model) -> Node { - let modal = match model.modal { - Some(ModalType::EditIssue(issue_id)) => { - if let Some(issue) = find_issue(model, issue_id) { - let details = issue_details(model, issue); + let modal = match &model.modal { + Some(ModalType::EditIssue(issue_id, modal)) => { + if let Some(issue) = find_issue(model, *issue_id) { + let details = issue_details(model, issue, &modal); let modal = Modal { variant: ModalVariant::Center, width: 1040, @@ -428,16 +456,30 @@ impl crate::shared::styled_select::SelectOption for IssueTypeOption { } .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() + } } -fn issue_details(_model: &Model, issue: &Issue) -> Node { +fn issue_details(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node { let issue_id = issue.id; + let issue_type_select = StyledSelect { - on_change: mouse_ev(Ev::Click, |_| Msg::NoOp), + id: FieldId::IssueTypeEditModalTop, variant: crate::shared::styled_select::Variant::Empty, dropdown_width: Some(150), name: Some("type".to_string()), placeholder: None, + text_filter: modal.top_select_filter.clone(), + opened: modal.top_select_opened, valid: true, is_multi: false, allow_clear: false, @@ -446,7 +488,7 @@ fn issue_details(_model: &Model, issue: &Issue) -> Node { IssueTypeOption(issue_id, IssueType::Task), IssueTypeOption(issue_id, IssueType::Bug), ], - selected: Some(IssueTypeOption(issue_id, IssueType::Story)), + selected: vec![IssueTypeOption(issue_id, modal.value.clone())], } .into_node(); diff --git a/jirs-client/src/shared/styled_select.rs b/jirs-client/src/shared/styled_select.rs index 22bd4935..c9267d25 100644 --- a/jirs-client/src/shared/styled_select.rs +++ b/jirs-client/src/shared/styled_select.rs @@ -1,7 +1,14 @@ use seed::{prelude::*, *}; use crate::shared::ToNode; -use crate::Msg; +use crate::{FieldId, Msg}; + +#[derive(Clone, Debug, PartialEq)] +pub enum StyledSelectChange { + Text(String), + DropDownVisibility(bool), + Changed(u32), +} #[derive(Copy, Clone, Debug, PartialEq)] pub enum Variant { @@ -22,13 +29,17 @@ pub trait SelectOption { fn into_option(self) -> Node; fn into_value(self) -> Node; + + fn match_text_filter(&self, text_filter: &str) -> bool; + + fn to_value(&self) -> u32; } pub struct StyledSelect where Child: SelectOption + PartialEq, { - pub on_change: EventHandler, + pub id: FieldId, pub variant: Variant, pub dropdown_width: Option, pub name: Option, @@ -37,7 +48,9 @@ where pub is_multi: bool, pub allow_clear: bool, pub options: Vec, - pub selected: Option, + pub selected: Vec, + pub text_filter: String, + pub opened: bool, } impl ToNode for StyledSelect @@ -54,7 +67,7 @@ where Child: SelectOption + PartialEq, { let StyledSelect { - on_change, + id, variant, dropdown_width, name, @@ -64,8 +77,22 @@ where allow_clear, options, selected, + text_filter, + opened, } = values; + let on_text = input_ev(Ev::KeyUp, |value| { + Msg::StyledSelectChanged( + FieldId::IssueTypeEditModalTop, + StyledSelectChange::Text(value), + ) + }); + + let field_id = id.clone(); + let visibility_handler = mouse_ev(Ev::Click, move |_| { + Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(!opened)) + }); + let dropdown_style = dropdown_width .map(|n| format!("width: {}px", n)) .unwrap_or_default(); @@ -76,57 +103,71 @@ where let children: Vec> = options .into_iter() - .filter(|o| Some(o) != selected.as_ref()) - .map(|child| render_option(child.into_option())) + .filter(|o| !selected.contains(&o) && o.match_text_filter(text_filter.as_str())) + .map(|child| { + let value = child.to_value(); + let field_id = id.clone(); + let on_change = mouse_ev(Ev::Click, move |_| { + Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(value)) + }); + div![ + attrs![At::Class => "option"], + on_change, + visibility_handler.clone(), + child.into_option() + ] + }) .collect(); - let value = selected - .map(|m| render_value(m.into_value())) - .unwrap_or_else(|| empty![]); + let value = selected.into_iter().map(|m| render_value(m.into_value())); - let clear_icon = match allow_clear { - true => crate::shared::styled_icon(crate::model::Icon::Close), - false => empty![], + let text_input = match opened { + true => seed::input![ + attrs![ + At::Name => name.unwrap_or_default(), + At::Class => "dropDownInput", + At::Type => "text" + At::Placeholder => "Search" + At::AutoFocus => true, + ], + on_text.clone(), + ], + _ => empty![], }; - let option_list = if children.is_empty() { - seed::div![attrs![At::Class => "noOptions"], "No results"] - } else { - seed::div![ - attrs![ - At::Class => "options", - ], - children - ] + let clear_icon = match (opened, allow_clear) { + (true, true) => crate::shared::styled_icon(crate::model::Icon::Close), + _ => empty![], + }; + + let option_list = match (opened, children.is_empty()) { + (false, _) => empty![], + (_, true) => seed::div![attrs![At::Class => "noOptions"], "No results"], + _ => seed::div![attrs![ At::Class => "options" ], children], }; seed::div![ - on_change.clone(), attrs![At::Class => select_class.join(" ")], div![ attrs![At::Class => format!("valueContainer {}", variant)], - value + visibility_handler, + value, ], div![ attrs![At::Class => "dropDown", At::Style => dropdown_style], - seed::input![ - attrs![ - At::Name => name.unwrap_or_default(), - At::Class => "dropDownInput", - At::Type => "text" - At::Placeholder => "Search" - At::AutoFocus => true, - ], - on_change, - ], + text_input, clear_icon, option_list ] ] } -fn render_option(content: Node) -> Node { - div![attrs![At::Class => "option"], content] +fn render_option( + content: Node, + on_change: EventHandler, + on_click: EventHandler, +) -> Node { + div![attrs![At::Class => "option"], on_change, on_click, content] } fn render_value(mut content: Node) -> Node { diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 5ac60804..d52e0be3 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -38,6 +38,27 @@ impl IssueType { } } +impl Into for IssueType { + fn into(self) -> u32 { + match self { + IssueType::Task => 1, + IssueType::Bug => 2, + IssueType::Story => 3, + } + } +} + +impl Into for u32 { + fn into(self) -> IssueType { + match self { + 1 => IssueType::Task, + 2 => IssueType::Bug, + 3 => IssueType::Story, + _ => IssueType::Task, + } + } +} + impl std::fmt::Display for IssueType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self {