diff --git a/README.md b/README.md index 6057d87a..86279b78 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,19 @@ https://git.sr.ht/~tsumanu/jirs * Add personal settings to choose MDE (Markdown Editor) or RTE * Add issues and filters +##### Work Progress + +* [X] Add Epic +* [ ] Edit Epic +* [ ] Delete Epic +* [ ] Epic `starts` and `ends` date +* [X] Grouping by Epic +* [X] Basic Rich Text Editor +* [ ] Insert Code in Rich Text Editor +* [ ] Personal settings to choose MDE (Markdown Editor) or RTE +* [ ] Issues and filters view +* [ ] Issues and filters working filters + ## How to run it ### Config files diff --git a/jirs-client/src/modal/issues.rs b/jirs-client/src/modal/issues.rs new file mode 100644 index 00000000..f7e0aa97 --- /dev/null +++ b/jirs-client/src/modal/issues.rs @@ -0,0 +1,46 @@ +use seed::prelude::Node; + +use jirs_data::EpicId; + +use crate::{ + model::{IssueModal, Model}, + shared::{styled_field::StyledField, styled_select::StyledSelect, ToChild, ToNode}, + FieldId, Msg, +}; + +pub mod add_issue; +pub mod issue_details; + +pub fn epic_field(model: &Model, modal: &Modal, field_id: FieldId) -> Option> +where + Modal: IssueModal, +{ + if model.epics.is_empty() { + None + } else { + let selected = modal + .epic_id_value() + .and_then(|id| model.epics.iter().find(|epic| epic.id == id as EpicId)) + .map(|epic| vec![epic.to_child()]) + .unwrap_or_default(); + let input = StyledSelect::build(field_id) + .name("epic") + .selected(selected) + .options(model.epics.iter().map(|epic| epic.to_child()).collect()) + .normal() + .clearable() + .text_filter(modal.epic_state().text_filter.as_str()) + .opened(modal.epic_state().opened) + .valid(true) + .build() + .into_node(); + Some( + StyledField::build() + .label("Epic") + .tip("Feature group") + .input(input) + .build() + .into_node(), + ) + } +} diff --git a/jirs-client/src/modal/add_issue.rs b/jirs-client/src/modal/issues/add_issue.rs similarity index 85% rename from jirs-client/src/modal/add_issue.rs rename to jirs-client/src/modal/issues/add_issue.rs index f11805d6..ab272848 100644 --- a/jirs-client/src/modal/add_issue.rs +++ b/jirs-client/src/modal/issues/add_issue.rs @@ -1,21 +1,25 @@ use seed::{prelude::*, *}; -use jirs_data::{EpicId, IssueFieldId, WsMsg}; -use jirs_data::{IssuePriority, ToVec}; +use jirs_data::{IssueFieldId, IssuePriority, ToVec, UserId, WsMsg}; -use crate::model::{AddIssueModal, ModalType, Model}; -use crate::shared::styled_button::StyledButton; -use crate::shared::styled_field::StyledField; -use crate::shared::styled_form::StyledForm; -use crate::shared::styled_input::StyledInput; -use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant}; -use crate::shared::styled_select::StyledSelect; -use crate::shared::styled_select::StyledSelectChange; -use crate::shared::styled_select_child::{StyledSelectChild, StyledSelectChildBuilder}; -use crate::shared::styled_textarea::StyledTextarea; -use crate::shared::{ToChild, ToNode}; -use crate::ws::send_ws_msg; -use crate::{FieldId, Msg, WebSocketChanged}; +use crate::{ + modal::issues::epic_field, + model::{AddIssueModal, IssueModal, ModalType, Model}, + shared::{ + styled_button::StyledButton, + styled_field::StyledField, + styled_form::StyledForm, + styled_input::StyledInput, + styled_modal::{StyledModal, Variant as ModalVariant}, + styled_select::StyledSelect, + styled_select::StyledSelectChange, + styled_select_child::{StyledSelectChild, StyledSelectChildBuilder}, + styled_textarea::StyledTextarea, + ToChild, ToNode, + }, + ws::send_ws_msg, + FieldId, Msg, WebSocketChanged, +}; #[derive(Copy, Clone)] enum Type { @@ -114,12 +118,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde _ => return, }; - modal.title_state.update(msg); - modal.assignees_state.update(msg, orders); - modal.reporter_state.update(msg, orders); - modal.type_state.update(msg, orders); - modal.priority_state.update(msg, orders); - modal.epic_state.update(msg, orders); + modal.update_states(msg, orders); match msg { Msg::AddEpic => { @@ -182,15 +181,15 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde FieldId::AddIssueModal(IssueFieldId::Reporter), StyledSelectChange::Changed(id), ) => { - modal.reporter_id = Some(*id as i32); + modal.reporter_id = id.map(|n| n as UserId); } // AssigneesAddIssueModal Msg::StyledSelectChanged( FieldId::AddIssueModal(IssueFieldId::Assignees), - StyledSelectChange::Changed(id), + StyledSelectChange::Changed(Some(id)), ) => { - let id = *id as i32; + let id = *id as UserId; if !modal.user_ids.contains(&id) { modal.user_ids.push(id); } @@ -212,7 +211,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, orders: &mut impl Orde // IssuePriorityAddIssueModal Msg::StyledSelectChanged( FieldId::AddIssueModal(IssueFieldId::Priority), - StyledSelectChange::Changed(id), + StyledSelectChange::Changed(Some(id)), ) => { modal.priority = (*id).into(); } @@ -247,7 +246,7 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node { let reporter_field = reporter_field(model, modal); let assignees_field = assignees_field(model, modal); let issue_priority_field = issue_priority_field(modal); - let epic_field = epic_field(model, modal); + let epic_field = epic_field(model, modal, FieldId::AddIssueModal(IssueFieldId::Epic)); form.add_field(short_summary_field) .add_field(description_field) @@ -456,38 +455,6 @@ fn issue_priority_field(modal: &AddIssueModal) -> Node { .into_node() } -fn epic_field(model: &Model, modal: &AddIssueModal) -> Option> { - if model.epics.is_empty() { - None - } else { - let selected = modal - .epic_state - .values - .get(0) - .and_then(|id| model.epics.iter().find(|epic| epic.id == *id as EpicId)) - .map(|epic| vec![epic.to_child()]) - .unwrap_or_default(); - let input = StyledSelect::build(FieldId::AddIssueModal(IssueFieldId::Epic)) - .name("epic") - .selected(selected) - .options(model.epics.iter().map(|epic| epic.to_child()).collect()) - .normal() - .text_filter(modal.epic_state.text_filter.as_str()) - .opened(modal.epic_state.opened) - .valid(true) - .build() - .into_node(); - Some( - StyledField::build() - .label("Epic") - .tip("Feature group") - .input(input) - .build() - .into_node(), - ) - } -} - fn name_field(modal: &AddIssueModal) -> Node { let name = StyledInput::build(FieldId::AddIssueModal(IssueFieldId::Title)) .state(&modal.title_state) diff --git a/jirs-client/src/modal/issue_details.rs b/jirs-client/src/modal/issues/issue_details.rs similarity index 93% rename from jirs-client/src/modal/issue_details.rs rename to jirs-client/src/modal/issues/issue_details.rs index 08fd6fef..db4648cc 100644 --- a/jirs-client/src/modal/issue_details.rs +++ b/jirs-client/src/modal/issues/issue_details.rs @@ -2,37 +2,31 @@ use seed::{prelude::*, *}; use jirs_data::*; -use crate::modal::time_tracking::time_tracking_field; -use crate::model::{CommentForm, 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; -use crate::shared::styled_input::StyledInput; -use crate::shared::styled_select::{StyledSelect, StyledSelectChange}; -use crate::shared::styled_textarea::StyledTextarea; -use crate::shared::tracking_widget::tracking_link; -use crate::shared::{ToChild, ToNode}; -use crate::ws::send_ws_msg; -use crate::{EditIssueModalSection, FieldChange, FieldId, Msg, WebSocketChanged}; +use crate::{ + modal::{issues::epic_field, time_tracking::time_tracking_field}, + model::{CommentForm, EditIssueModal, IssueModal, ModalType, Model}, + shared::{ + styled_avatar::StyledAvatar, + styled_button::StyledButton, + styled_editor::StyledEditor, + styled_field::StyledField, + styled_icon::Icon, + styled_input::StyledInput, + styled_select::{StyledSelect, StyledSelectChange}, + styled_textarea::StyledTextarea, + tracking_widget::tracking_link, + ToChild, ToNode, + }, + ws::send_ws_msg, + EditIssueModalSection, FieldChange, FieldId, Msg, WebSocketChanged, +}; 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); - modal.estimate.update(msg); - modal.estimate_select.update(msg, orders); - modal.time_spent.update(msg); - modal.time_spent_select.update(msg, orders); - modal.time_remaining.update(msg); - modal.time_remaining_select.update(msg, orders); + modal.update_states(msg, orders); match msg { Msg::WebSocketChange(WebSocketChanged::WsMsg(WsMsg::IssueUpdated(issue))) => { @@ -40,7 +34,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Type)), - StyledSelectChange::Changed(value), + StyledSelectChange::Changed(Some(value)), ) => { modal.payload.issue_type = (*value).into(); send_ws_msg( @@ -55,7 +49,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::IssueStatusId)), - StyledSelectChange::Changed(value), + StyledSelectChange::Changed(Some(value)), ) => { modal.payload.issue_status_id = *value as IssueStatusId; send_ws_msg( @@ -70,7 +64,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Reporter)), - StyledSelectChange::Changed(value), + StyledSelectChange::Changed(Some(value)), ) => { modal.payload.reporter_id = *value as i32; send_ws_msg( @@ -85,7 +79,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Assignees)), - StyledSelectChange::Changed(value), + StyledSelectChange::Changed(Some(value)), ) => { modal.payload.user_ids.push(*value as i32); send_ws_msg( @@ -122,7 +116,7 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::StyledSelectChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)), - StyledSelectChange::Changed(value), + StyledSelectChange::Changed(Some(value)), ) => { modal.payload.priority = (*value).into(); send_ws_msg( @@ -267,6 +261,20 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl Orders) { orders, ); } + Msg::StyledSelectChanged( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)), + StyledSelectChange::Changed(v), + ) => { + send_ws_msg( + WsMsg::IssueUpdate( + modal.id, + IssueFieldId::Epic, + PayloadVariant::OptionI32(v.map(|n| n as EpicId).clone()), + ), + model.ws.as_ref(), + orders, + ); + } Msg::ModalChanged(FieldChange::TabChanged( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Description)), mode, @@ -792,6 +800,13 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { (empty![], empty![]) }; + let epic_field = epic_field( + model, + modal, + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)), + ) + .unwrap_or_else(|| empty![]); + div![ attrs![At::Class => "right"], status_field, @@ -800,5 +815,6 @@ fn right_modal_column(model: &Model, modal: &EditIssueModal) -> Node { priority_field, estimate_field, tracking_field, + epic_field, ] } diff --git a/jirs-client/src/modal/mod.rs b/jirs-client/src/modal/mod.rs index 432d7547..f627ca78 100644 --- a/jirs-client/src/modal/mod.rs +++ b/jirs-client/src/modal/mod.rs @@ -2,19 +2,24 @@ use seed::{prelude::*, *}; use jirs_data::{TimeTracking, WsMsg}; -use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page}; -use crate::shared::styled_confirm_modal::StyledConfirmModal; -use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant}; -use crate::shared::{find_issue, go_to_board, ToNode}; -use crate::ws::send_ws_msg; -use crate::{model, FieldChange, FieldId, Msg, WebSocketChanged}; +use crate::{ + modal::issues::*, + model::{self, AddIssueModal, EditIssueModal, ModalType, Model, Page}, + shared::{ + find_issue, go_to_board, + styled_confirm_modal::StyledConfirmModal, + styled_modal::{StyledModal, Variant as ModalVariant}, + ToNode, + }, + ws::send_ws_msg, + FieldChange, FieldId, Msg, WebSocketChanged, +}; -mod add_issue; mod confirm_delete_issue; #[cfg(debug_assertions)] mod debug_modal; mod delete_issue_status; -mod issue_details; +pub mod issues; pub mod time_tracking; pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders) { diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 2c625126..69af7503 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -1,6 +1,7 @@ use std::collections::hash_map::HashMap; use chrono::{prelude::*, NaiveDate}; +use seed::app::Orders; use seed::browser::web_socket::WebSocket; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -15,7 +16,14 @@ use crate::shared::styled_image_input::StyledImageInputState; use crate::shared::styled_input::StyledInputState; use crate::shared::styled_rte::StyledRteState; use crate::shared::styled_select::StyledSelectState; -use crate::{EditIssueModalSection, FieldId, ProjectFieldId /*HOST_URL*/}; +use crate::{EditIssueModalSection, FieldId, Msg, ProjectFieldId}; + +pub trait IssueModal { + fn epic_id_value(&self) -> Option; + fn epic_state(&self) -> &StyledSelectState; + + fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders); +} #[derive(Clone, Debug, PartialOrd, PartialEq)] pub enum ModalType { @@ -61,6 +69,7 @@ pub struct EditIssueModal { pub reporter_state: StyledSelectState, pub assignees_state: StyledSelectState, pub priority_state: StyledSelectState, + pub epic_state: StyledSelectState, pub estimate: StyledInputState, pub estimate_select: StyledSelectState, @@ -75,6 +84,31 @@ pub struct EditIssueModal { pub comment_form: CommentForm, } +impl IssueModal for EditIssueModal { + fn epic_id_value(&self) -> Option { + self.epic_state.values.get(0).cloned() + } + + fn epic_state(&self) -> &StyledSelectState { + &self.epic_state + } + + fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders) { + self.top_type_state.update(msg, orders); + self.status_state.update(msg, orders); + self.reporter_state.update(msg, orders); + self.assignees_state.update(msg, orders); + self.priority_state.update(msg, orders); + self.estimate.update(msg); + self.estimate_select.update(msg, orders); + self.time_spent.update(msg); + self.time_spent_select.update(msg, orders); + self.time_remaining.update(msg); + self.time_remaining_select.update(msg, orders); + self.epic_state.update(msg, orders); + } +} + impl EditIssueModal { pub fn new(issue: &Issue, time_tracking_type: TimeTracking) -> Self { Self { @@ -115,6 +149,14 @@ impl EditIssueModal { FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Priority)), vec![issue.priority.into()], ), + epic_state: StyledSelectState::new( + FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Epic)), + issue + .epic_id + .as_ref() + .map(|id| vec![*id as u32]) + .unwrap_or_default(), + ), estimate: StyledInputState::new( FieldId::EditIssueModal(EditIssueModalSection::Issue(IssueFieldId::Estimate)), value_for_time_tracking(&issue.estimate, &time_tracking_type), @@ -175,6 +217,25 @@ pub struct AddIssueModal { pub epic_state: StyledSelectState, } +impl IssueModal for AddIssueModal { + fn epic_id_value(&self) -> Option { + self.epic_state.values.get(0).cloned() + } + + fn epic_state(&self) -> &StyledSelectState { + &self.epic_state + } + + fn update_states(&mut self, msg: &Msg, orders: &mut impl Orders) { + self.title_state.update(msg); + self.assignees_state.update(msg, orders); + self.reporter_state.update(msg, orders); + self.type_state.update(msg, orders); + self.priority_state.update(msg, orders); + self.epic_state.update(msg, orders); + } +} + impl Default for AddIssueModal { fn default() -> Self { Self { diff --git a/jirs-client/src/profile/update.rs b/jirs-client/src/profile/update.rs index 97f9b5d0..0696e8c2 100644 --- a/jirs-client/src/profile/update.rs +++ b/jirs-client/src/profile/update.rs @@ -1,7 +1,7 @@ use seed::prelude::{Method, Orders, Request}; use web_sys::FormData; -use jirs_data::{UsersFieldId, WsMsg}; +use jirs_data::{ProjectId, UsersFieldId, WsMsg}; use crate::model::{Model, Page, PageContent, ProfilePage}; use crate::shared::styled_select::StyledSelectChange; @@ -69,12 +69,12 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order } Msg::StyledSelectChanged( FieldId::Profile(UsersFieldId::CurrentProject), - StyledSelectChange::Changed(id), + StyledSelectChange::Changed(Some(id)), ) => { if let Some(up) = model .user_projects .iter() - .find(|up| up.project_id == id as i32) + .find(|up| up.project_id == id as ProjectId) { send_ws_msg( WsMsg::UserProjectSetCurrent(up.id), diff --git a/jirs-client/src/project_settings/update.rs b/jirs-client/src/project_settings/update.rs index 9d22d27d..a12fb73c 100644 --- a/jirs-client/src/project_settings/update.rs +++ b/jirs-client/src/project_settings/update.rs @@ -68,7 +68,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::StyledSelectChanged( FieldId::ProjectSettings(ProjectFieldId::Category), - StyledSelectChange::Changed(value), + StyledSelectChange::Changed(Some(value)), ) => { let category = value.into(); page.payload.category = Some(category); diff --git a/jirs-client/src/shared/styled_icon.rs b/jirs-client/src/shared/styled_icon.rs index 28463fee..c7efeb93 100644 --- a/jirs-client/src/shared/styled_icon.rs +++ b/jirs-client/src/shared/styled_icon.rs @@ -242,6 +242,7 @@ pub struct StyledIconBuilder { size: Option, class_list: Vec, style_list: Vec, + on_click: Option>, } impl StyledIconBuilder { @@ -266,12 +267,18 @@ impl StyledIconBuilder { self } + pub fn on_click(mut self, on_click: EventHandler) -> Self { + self.on_click = Some(on_click); + self + } + pub fn build(self) -> StyledIcon { StyledIcon { icon: self.icon, size: self.size, class_list: self.class_list, style_list: self.style_list, + on_click: self.on_click, } } } @@ -281,6 +288,7 @@ pub struct StyledIcon { size: Option, class_list: Vec, style_list: Vec, + on_click: Option>, } impl StyledIcon { @@ -290,6 +298,7 @@ impl StyledIcon { size: None, class_list: vec![], style_list: vec![], + on_click: None, } } } @@ -306,6 +315,7 @@ pub fn render(values: StyledIcon) -> Node { size, mut class_list, mut style_list, + on_click, } = values; if let Some(s) = icon.to_color() { @@ -320,6 +330,7 @@ pub fn render(values: StyledIcon) -> Node { i![ attrs![At::Class => class_list.join(" "), At::Style => style_list.join(";")], + on_click, "" ] } diff --git a/jirs-client/src/shared/styled_select.rs b/jirs-client/src/shared/styled_select.rs index 9c29aa59..a9daa09f 100644 --- a/jirs-client/src/shared/styled_select.rs +++ b/jirs-client/src/shared/styled_select.rs @@ -9,7 +9,7 @@ use crate::{FieldId, Msg}; pub enum StyledSelectChange { Text(String), DropDownVisibility(bool), - Changed(u32), + Changed(Option), RemoveMulti(u32), } @@ -75,11 +75,16 @@ impl StyledSelectState { { self.text_filter = text.clone(); } - Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(v)) + Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(v))) if field_id == &self.field_id => { self.values = vec![*v]; } + Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None)) + if field_id == &self.field_id => + { + self.values.clear(); + } Msg::StyledSelectChanged(field_id, StyledSelectChange::RemoveMulti(v)) if field_id == &self.field_id => { @@ -104,11 +109,11 @@ pub struct StyledSelect { name: Option, valid: bool, is_multi: bool, - allow_clear: bool, options: Vec, selected: Vec, text_filter: String, opened: bool, + clearable: bool, } impl ToNode for StyledSelect { @@ -126,11 +131,11 @@ impl StyledSelect { name: None, valid: None, is_multi: None, - allow_clear: None, options: None, selected: None, text_filter: None, opened: None, + clearable: false, } } } @@ -143,11 +148,11 @@ pub struct StyledSelectBuilder { name: Option, valid: Option, is_multi: Option, - allow_clear: Option, options: Option>, selected: Option>, text_filter: Option, opened: Option, + clearable: bool, } impl StyledSelectBuilder { @@ -159,11 +164,11 @@ impl StyledSelectBuilder { name: self.name, valid: self.valid.unwrap_or(true), is_multi: self.is_multi.unwrap_or_default(), - allow_clear: self.allow_clear.unwrap_or_default(), options: self.options.unwrap_or_default(), selected: self.selected.unwrap_or_default(), text_filter: self.text_filter.unwrap_or_default(), opened: self.opened.unwrap_or_default(), + clearable: self.clearable, } } @@ -227,6 +232,11 @@ impl StyledSelectBuilder { self.is_multi = Some(true); self } + + pub fn clearable(mut self) -> Self { + self.clearable = true; + self + } } pub fn render(values: StyledSelect) -> Node { @@ -237,22 +247,26 @@ pub fn render(values: StyledSelect) -> Node { name, valid, is_multi, - allow_clear, options, selected, text_filter, opened, + clearable, } = values; - let field_id = id.clone(); - let on_text = input_ev(Ev::KeyUp, move |value| { - Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(value)) - }); + let on_text = { + let field_id = id.clone(); + input_ev(Ev::KeyUp, move |value| { + Msg::StyledSelectChanged(field_id, StyledSelectChange::Text(value)) + }) + }; - let field_id = id.clone(); - let visibility_handler = mouse_ev(Ev::Click, move |_| { - Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(!opened)) - }); + let on_handler = { + let field_id = id.clone(); + mouse_ev(Ev::Click, move |_| { + Msg::StyledSelectChanged(field_id, StyledSelectChange::DropDownVisibility(!opened)) + }) + }; let dropdown_style = dropdown_width .map(|n| format!("width: {}px;", n)) @@ -263,7 +277,19 @@ pub fn render(values: StyledSelect) -> Node { select_class.push("invalid".to_string()); } - let chevron_down = if (selected.is_empty() || !is_multi) && variant != Variant::Empty { + let action_icon = if clearable && !selected.is_empty() { + let field_id = id.clone(); + let on_click = mouse_ev(Ev::Click, move |ev| { + ev.stop_propagation(); + ev.prevent_default(); + Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(None)) + }); + StyledIcon::build(Icon::Close) + .add_class("chevronIcon") + .on_click(on_click) + .build() + .into_node() + } else if (selected.is_empty() || !is_multi) && variant != Variant::Empty { StyledIcon::build(Icon::ChevronDown) .add_class("chevronIcon") .build() @@ -281,12 +307,12 @@ pub fn render(values: StyledSelect) -> Node { let node = child.into_node(); let field_id = id.clone(); let on_change = mouse_ev(Ev::Click, move |_| { - Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(value)) + Msg::StyledSelectChanged(field_id, StyledSelectChange::Changed(Some(value))) }); div![ attrs![At::Class => "option"], on_change, - visibility_handler.clone(), + on_handler.clone(), node ] }) @@ -307,11 +333,6 @@ pub fn render(values: StyledSelect) -> Node { empty![] }; - let clear_icon = match (opened, allow_clear) { - (true, true) => StyledIcon::build(Icon::Close).build().into_node(), - _ => empty![], - }; - let option_list = match (opened, children.is_empty()) { (false, _) => empty![], (_, true) => seed::div![attrs![At::Class => "noOptions"], "No results"], @@ -347,15 +368,14 @@ pub fn render(values: StyledSelect) -> Node { }), div![ attrs![At::Class => format!("valueContainer {}", variant)], - visibility_handler, + on_handler, value, - chevron_down, + action_icon, ], div![ - class!["dropDown"], + C!["dropDown"], attrs![At::Style => dropdown_style.as_str()], text_input, - clear_icon, option_list ] ] diff --git a/jirs-client/src/users/update.rs b/jirs-client/src/users/update.rs index cc204b06..8812b6e1 100644 --- a/jirs-client/src/users/update.rs +++ b/jirs-client/src/users/update.rs @@ -74,7 +74,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { } Msg::StyledSelectChanged( FieldId::Users(UsersFieldId::UserRole), - StyledSelectChange::Changed(role), + StyledSelectChange::Changed(Some(role)), ) => { page.user_role = role.into(); } diff --git a/jirs-server/src/db/authorize_user.rs b/jirs-server/src/db/authorize_user.rs index be963ef6..76957719 100644 --- a/jirs-server/src/db/authorize_user.rs +++ b/jirs-server/src/db/authorize_user.rs @@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize}; use jirs_data::{Token, User}; -use crate::db::{DbExecutor, DbPool, SyncQuery}; -use crate::errors::ServiceErrors; +use crate::{ + db::{DbExecutor, DbPool, SyncQuery}, + errors::ServiceErrors, +}; #[derive(Serialize, Deserialize, Debug)] pub struct AuthorizeUser { diff --git a/jirs-server/src/db/comments.rs b/jirs-server/src/db/comments.rs index e21bbccd..fea2be66 100644 --- a/jirs-server/src/db/comments.rs +++ b/jirs-server/src/db/comments.rs @@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize}; use jirs_data::Comment; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; +use crate::{db::DbExecutor, errors::ServiceErrors}; #[derive(Serialize, Deserialize)] pub struct LoadIssueComments { diff --git a/jirs-server/src/db/epics.rs b/jirs-server/src/db/epics.rs index 49ce7052..21e36b5a 100644 --- a/jirs-server/src/db/epics.rs +++ b/jirs-server/src/db/epics.rs @@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize}; use jirs_data::Epic; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; +use crate::{db::DbExecutor, errors::ServiceErrors}; #[derive(Serialize, Deserialize)] pub struct LoadEpics { diff --git a/jirs-server/src/db/invitations.rs b/jirs-server/src/db/invitations.rs index 8aed5738..e2c72d86 100644 --- a/jirs-server/src/db/invitations.rs +++ b/jirs-server/src/db/invitations.rs @@ -8,10 +8,14 @@ use jirs_data::{ User, UserId, UserRole, UsernameString, }; -use crate::db::tokens::CreateBindToken; -use crate::db::users::{LookupUser, Register}; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; +use crate::{ + db::{ + tokens::CreateBindToken, + users::{LookupUser, Register}, + DbExecutor, + }, + errors::ServiceErrors, +}; pub struct ListInvitation { pub user_id: UserId, diff --git a/jirs-server/src/db/issue_assignees.rs b/jirs-server/src/db/issue_assignees.rs index f358a5f8..8d74ea39 100644 --- a/jirs-server/src/db/issue_assignees.rs +++ b/jirs-server/src/db/issue_assignees.rs @@ -5,8 +5,7 @@ use serde::{Deserialize, Serialize}; use jirs_data::IssueAssignee; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; +use crate::{db::DbExecutor, errors::ServiceErrors}; #[derive(Serialize, Deserialize)] pub struct LoadAssignees { diff --git a/jirs-server/src/db/issue_statuses.rs b/jirs-server/src/db/issue_statuses.rs index a67d6c6f..32b7dc68 100644 --- a/jirs-server/src/db/issue_statuses.rs +++ b/jirs-server/src/db/issue_statuses.rs @@ -4,8 +4,7 @@ use diesel::prelude::*; use jirs_data::{IssueStatus, IssueStatusId, Position, ProjectId, TitleString}; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; +use crate::{db::DbExecutor, errors::ServiceErrors}; pub struct LoadIssueStatuses { pub project_id: ProjectId, diff --git a/jirs-server/src/db/issues.rs b/jirs-server/src/db/issues.rs index 69b19d0f..b9eecc06 100644 --- a/jirs-server/src/db/issues.rs +++ b/jirs-server/src/db/issues.rs @@ -6,9 +6,7 @@ use serde::{Deserialize, Serialize}; use jirs_data::{IssuePriority, IssueStatusId, IssueType}; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; -use crate::models::Issue; +use crate::{db::DbExecutor, errors::ServiceErrors, models::Issue}; const FAILED_CONNECT_USER_AND_ISSUE: &str = "Failed to create connection between user and issue"; @@ -88,6 +86,7 @@ pub struct UpdateIssue { pub user_ids: Option>, pub reporter_id: Option, pub issue_status_id: Option, + pub epic_id: Option>, } impl Message for UpdateIssue { @@ -127,6 +126,7 @@ impl Handler for DbExecutor { .map(|project_id| dsl::project_id.eq(project_id)), msg.reporter_id .map(|reporter_id| dsl::reporter_id.eq(reporter_id)), + msg.epic_id.map(|epic_id| dsl::epic_id.eq(epic_id)), dsl::updated_at.eq(chrono::Utc::now().naive_utc()), )); debug!( diff --git a/jirs-server/src/db/messages.rs b/jirs-server/src/db/messages.rs index 0c701886..03f3fa8e 100644 --- a/jirs-server/src/db/messages.rs +++ b/jirs-server/src/db/messages.rs @@ -3,9 +3,13 @@ use diesel::prelude::*; use jirs_data::{BindToken, Message, MessageId, MessageType, User, UserId}; -use crate::db::users::{FindUser, LookupUser}; -use crate::db::DbExecutor; -use crate::errors::ServiceErrors; +use crate::{ + db::{ + users::{FindUser, LookupUser}, + DbExecutor, + }, + errors::ServiceErrors, +}; #[derive(Debug)] pub struct LoadMessages { diff --git a/jirs-server/src/db/projects.rs b/jirs-server/src/db/projects.rs index fa631b6c..43f8423c 100644 --- a/jirs-server/src/db/projects.rs +++ b/jirs-server/src/db/projects.rs @@ -3,7 +3,7 @@ use diesel::pg::Pg; use diesel::prelude::*; use serde::{Deserialize, Serialize}; -use jirs_data::{Project, ProjectCategory, TimeTracking, UserId}; +use jirs_data::{NameString, Project, ProjectCategory, ProjectId, TimeTracking, UserId}; use crate::db::DbExecutor; use crate::errors::ServiceErrors; @@ -11,7 +11,7 @@ use crate::schema::projects::all_columns; #[derive(Serialize, Deserialize)] pub struct LoadCurrentProject { - pub project_id: i32, + pub project_id: ProjectId, } impl Message for LoadCurrentProject { @@ -43,7 +43,7 @@ impl Handler for DbExecutor { #[derive(Serialize, Deserialize)] pub struct CreateProject { - pub name: String, + pub name: NameString, pub url: Option, pub description: Option, pub category: Option, @@ -82,8 +82,8 @@ impl Handler for DbExecutor { #[derive(Serialize, Deserialize)] pub struct UpdateProject { - pub project_id: i32, - pub name: Option, + pub project_id: ProjectId, + pub name: Option, pub url: Option, pub description: Option, pub category: Option, diff --git a/jirs-server/src/ws/issues.rs b/jirs-server/src/ws/issues.rs index 50ef6578..912254d6 100644 --- a/jirs-server/src/ws/issues.rs +++ b/jirs-server/src/ws/issues.rs @@ -60,6 +60,9 @@ impl WsHandler for WebSocketActor { (IssueFieldId::TimeRemaining, PayloadVariant::OptionI32(o)) => { msg.time_remaining = o; } + (IssueFieldId::Epic, PayloadVariant::OptionI32(o)) => { + msg.epic_id = Some(o); + } _ => (), };