From b3349278295d1ad65bf490442385632fa815bad3 Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Fri, 10 Apr 2020 22:33:07 +0200 Subject: [PATCH] Most of fields. Send on blur --- jirs-client/js/css/styledButton.css | 4 +- jirs-client/js/css/styledEditor.css | 21 ++++-- jirs-client/js/css/styledForm.css | 3 +- jirs-client/js/css/styledIcon.css | 1 + jirs-client/js/css/styledTextArea.css | 3 +- jirs-client/src/modal/issue_details.rs | 90 ++++++++++++++++++++++- jirs-client/src/modal/mod.rs | 4 +- jirs-client/src/shared/styled_editor.rs | 39 ++++++++-- jirs-client/src/shared/styled_icon.rs | 2 +- jirs-client/src/shared/styled_textarea.rs | 43 ++++++++--- jirs-data/src/lib.rs | 20 +++++ jirs-server/seed.sql | 8 +- jirs-server/src/ws/mod.rs | 6 +- 13 files changed, 205 insertions(+), 39 deletions(-) diff --git a/jirs-client/js/css/styledButton.css b/jirs-client/js/css/styledButton.css index 773ab67b..ebcab0e0 100644 --- a/jirs-client/js/css/styledButton.css +++ b/jirs-client/js/css/styledButton.css @@ -23,11 +23,11 @@ cursor: default; } -.styledButton:not(.onlyIcon) { +.styledButton:not(.iconOnly) { padding: 0 12px; } -.styledButton.onlyIcon { +.styledButton.iconOnly { padding: 0 9px; } diff --git a/jirs-client/js/css/styledEditor.css b/jirs-client/js/css/styledEditor.css index 99b3fd1d..54ea91a9 100644 --- a/jirs-client/js/css/styledEditor.css +++ b/jirs-client/js/css/styledEditor.css @@ -25,13 +25,23 @@ font-size: 14.5px; } +.styledEditor > .navbar:not(:hover) { + border-color: var(--backgroundLightest); + background-color: var(--borderLight); +} + +.styledEditor > .navbar.activeTab { + background-color: var(--backgroundLightest); + border-color: var(--borderLight); +} + .styledEditor > .navbar:hover { background: #fff; border: 1px solid var(--borderInputFocus); box-shadow: 0 0 0 1px var(--borderInputFocus); } -.styledEditor > .navbar.active { +.styledEditor > .navbar { border-color: var(--borderInputFocus); } @@ -48,14 +58,15 @@ display: none; } -.styledEditor > input.editorRadio:checked ~ .styledTextArea { - display: block; -} - .styledEditor > .view { grid-area: view; display: none; min-height: 40px; + padding-top: 15px; +} + +.styledEditor > input.editorRadio:checked ~ .styledTextArea { + display: block; } .styledEditor > input.viewRadio:checked ~ .view { diff --git a/jirs-client/js/css/styledForm.css b/jirs-client/js/css/styledForm.css index 9ad27183..1d0e868e 100644 --- a/jirs-client/js/css/styledForm.css +++ b/jirs-client/js/css/styledForm.css @@ -11,7 +11,8 @@ font-size: 21px; } -.styledForm > .formElement .selectItem { +/*.styledForm > .formElement*/ +.selectItem { display: flex; align-items: center; margin-right: 15px; diff --git a/jirs-client/js/css/styledIcon.css b/jirs-client/js/css/styledIcon.css index 3fdb7c15..5c4b1fd9 100644 --- a/jirs-client/js/css/styledIcon.css +++ b/jirs-client/js/css/styledIcon.css @@ -2,6 +2,7 @@ i.styledIcon { color: var(--primary); display: inline-block; font-size: 16px; + line-height: 1; } i.styledIcon.left { diff --git a/jirs-client/js/css/styledTextArea.css b/jirs-client/js/css/styledTextArea.css index 21c3d3a2..cd59d5f4 100644 --- a/jirs-client/js/css/styledTextArea.css +++ b/jirs-client/js/css/styledTextArea.css @@ -13,7 +13,8 @@ background: var(--backgroundLightest); font-family: var(--font-regular); font-weight: normal; - font-size: 15px + font-size: 15px; + resize: vertical; } .styledTextArea > textarea:focus { diff --git a/jirs-client/src/modal/issue_details.rs b/jirs-client/src/modal/issue_details.rs index 8648f004..02100983 100644 --- a/jirs-client/src/modal/issue_details.rs +++ b/jirs-client/src/modal/issue_details.rs @@ -1,13 +1,15 @@ use seed::{prelude::*, *}; -use jirs_data::{IssuePriority, IssueStatus, IssueType, ToVec, User}; +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; @@ -25,42 +27,66 @@ pub fn update(msg: &Msg, model: &mut Model, orders: &mut impl 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(); @@ -164,6 +190,7 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node { 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(); @@ -213,6 +240,58 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node { .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![ @@ -233,7 +312,14 @@ pub fn view(model: &Model, modal: &EditIssueModal) -> Node { description_field, div![attrs![At::Class => "comments"]], ], - div![attrs![At::Class => "right"], status_field, assignees_field,], + div![ + attrs![At::Class => "right"], + status_field, + assignees_field, + reporter_field, + priority_field, + estimate_field + ], ], ] } diff --git a/jirs-client/src/modal/mod.rs b/jirs-client/src/modal/mod.rs index 07c724a7..32045036 100644 --- a/jirs-client/src/modal/mod.rs +++ b/jirs-client/src/modal/mod.rs @@ -1,9 +1,7 @@ use seed::{prelude::*, *}; use jirs_data::UpdateIssuePayload; -use jirs_data::*; -use crate::api::send_ws_msg; use crate::model::{AddIssueModal, EditIssueModal, ModalType, Model, Page}; use crate::shared::styled_editor::Mode; use crate::shared::styled_modal::{StyledModal, Variant as ModalVariant}; @@ -37,7 +35,7 @@ pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders model.modals.push(modal_type.clone()); } - Msg::WsMsg(jirs_data::WsMsg::ProjectIssuesLoaded(issues)) => match model.page.clone() { + Msg::WsMsg(jirs_data::WsMsg::ProjectIssuesLoaded(_issues)) => match model.page.clone() { Page::EditIssue(issue_id) if model.modals.is_empty() => { push_edit_modal(&issue_id, model) } diff --git a/jirs-client/src/shared/styled_editor.rs b/jirs-client/src/shared/styled_editor.rs index 665a813f..4bbb8fcb 100644 --- a/jirs-client/src/shared/styled_editor.rs +++ b/jirs-client/src/shared/styled_editor.rs @@ -20,6 +20,7 @@ pub struct StyledEditor { id: FieldId, text: String, mode: Mode, + update_event: Ev, } impl StyledEditor { @@ -28,6 +29,7 @@ impl StyledEditor { id, text: String::new(), mode: Mode::Editor, + update_event: None, } } } @@ -37,6 +39,7 @@ pub struct StyledEditorBuilder { id: FieldId, text: String, mode: Mode, + update_event: Option, } impl StyledEditorBuilder { @@ -58,8 +61,14 @@ impl StyledEditorBuilder { id: self.id, text: self.text, mode: self.mode, + update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp), } } + + pub fn update_on(mut self, ev: Ev) -> Self { + self.update_event = Some(ev); + self + } } impl ToNode for StyledEditor { @@ -69,7 +78,12 @@ impl ToNode for StyledEditor { } pub fn render(values: StyledEditor) -> Node { - let StyledEditor { id, text, mode } = values; + let StyledEditor { + id, + text, + mode, + update_event, + } = values; let field_id = id.clone(); let on_editor_clicked = mouse_ev(Ev::Click, move |ev| { @@ -89,6 +103,7 @@ pub fn render(values: StyledEditor) -> Node { let text_area = StyledTextarea::build() .height(40) + .update_on(update_event) .value(text.as_str()) .build(id.clone()) .into_node(); @@ -129,11 +144,13 @@ pub fn render(values: StyledEditor) -> Node { Mode::View => ( seed::input![ id![editor_id.as_str()], - attrs![At::Type => "radio"; At::Name => name.as_str(); At::Class => "editorRadio";], + class!["editorRadio"], + attrs![At::Type => "radio"; At::Name => name.as_str();], ], seed::input![ id![view_id.as_str()], - attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Class => "viewRadio"; At::Checked => true], + class!["viewRadio"], + attrs![ At::Type => "radio"; At::Name => name.as_str(); At::Checked => true], ], ), }; @@ -141,18 +158,28 @@ pub fn render(values: StyledEditor) -> Node { div![ attrs![At::Class => "styledEditor"], label![ - attrs![At::Class => "navbar editorTab", At::For => editor_id.as_str()], + if mode == Mode::Editor { + class!["navbar editorTab activeTab"] + } else { + class!["navbar editorTab"] + }, + attrs![At::For => editor_id.as_str()], "Editor", on_editor_clicked ], label![ - attrs![At::Class => "navbar viewTab"; At::For => view_id.as_str()], + if mode == Mode::View { + class!["navbar viewTab activeTab"] + } else { + class!["navbar viewTab"] + }, + attrs![At::For => view_id.as_str()], "View", on_view_clicked ], editor_radio_node, text_area, view_radio_node, - div![attrs![At::Class => "view"], parsed_node] + div![attrs![At::Class => "view"], parsed_node], ] } diff --git a/jirs-client/src/shared/styled_icon.rs b/jirs-client/src/shared/styled_icon.rs index 925e0d22..f57d3796 100644 --- a/jirs-client/src/shared/styled_icon.rs +++ b/jirs-client/src/shared/styled_icon.rs @@ -206,7 +206,7 @@ pub fn render(values: StyledIcon) -> Node { } if let Some(size) = size { - style_list.push(format!("width: {s}px; height: {s}px", s = size)); + style_list.push(format!("font-size: {s}px", s = size)); } class_list.push(format!("styledIcon {}", icon)); diff --git a/jirs-client/src/shared/styled_textarea.rs b/jirs-client/src/shared/styled_textarea.rs index 50366c5b..6cd8a03a 100644 --- a/jirs-client/src/shared/styled_textarea.rs +++ b/jirs-client/src/shared/styled_textarea.rs @@ -10,6 +10,7 @@ pub struct StyledTextarea { max_height: usize, value: String, class_list: Vec, + update_event: Ev, } impl ToNode for StyledTextarea { @@ -31,6 +32,7 @@ pub struct StyledTextareaBuilder { on_change: Option>, value: String, class_list: Vec, + update_event: Option, } impl StyledTextareaBuilder { @@ -64,6 +66,11 @@ impl StyledTextareaBuilder { self } + pub fn update_on(mut self, ev: Ev) -> Self { + self.update_event = Some(ev); + self + } + #[inline] pub fn build(self, id: FieldId) -> StyledTextarea { StyledTextarea { @@ -72,6 +79,7 @@ impl StyledTextareaBuilder { height: self.height.unwrap_or(110), class_list: self.class_list, max_height: self.max_height.unwrap_or_default(), + update_event: self.update_event.unwrap_or_else(|| Ev::KeyUp), } } } @@ -96,11 +104,15 @@ pub fn render(values: StyledTextarea) -> Node { max_height, value, mut class_list, + update_event, } = values; let mut style_list = vec![]; - if height > 0 { - style_list.push(format!("min-height: {}px", height)); + + let min_height = get_min_height(value.as_str(), height as f64); + if min_height > 0f64 { + style_list.push(format!("min-height: {}px", min_height)); } + if max_height > 0 { style_list.push(format!("max-height: {}px", max_height)); } @@ -116,22 +128,17 @@ pub fn render(values: StyledTextarea) -> Node { }; let text_area = target.dyn_ref::().unwrap(); let value: String = text_area.value(); - let len = value.lines().count() as f64; - - let calc_height = (len * LETTER_HEIGHT) + ADDITIONAL_HEIGHT; - let height = if calc_height + ADDITIONAL_HEIGHT < height as f64 { - height as f64 - } else { - calc_height + ADDITIONAL_HEIGHT - }; + let min_height = get_min_height(value.as_str(), height as f64); text_area .style() - .set_css_text(format!("height: {height}px", height = height).as_str()); + .set_css_text(format!("height: {min_height}px", min_height = min_height).as_str()); Msg::NoOp }); handlers.push(resize_handler); - let text_input_handler = input_ev(Ev::KeyUp, move |value| Msg::InputChanged(id, value)); + let text_input_handler = input_ev(update_event.clone(), move |value| { + Msg::InputChanged(id, value) + }); handlers.push(text_input_handler); class_list.push("textAreaInput".to_string()); @@ -150,3 +157,15 @@ pub fn render(values: StyledTextarea) -> Node { ] ] } + +fn get_min_height(value: &str, min_height: f64) -> f64 { + let len = value.lines().count() as f64; + if value.chars().last() == Some('\n') {} + + let calc_height = (len * LETTER_HEIGHT) + ADDITIONAL_HEIGHT; + if calc_height + ADDITIONAL_HEIGHT < min_height { + min_height + } else { + calc_height + ADDITIONAL_HEIGHT + } +} diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 2ea952f4..26f1dd4a 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -401,6 +401,26 @@ pub struct UpdateIssuePayload { pub user_ids: Vec, } +impl From for UpdateIssuePayload { + fn from(issue: Issue) -> Self { + Self { + title: issue.title, + issue_type: issue.issue_type, + status: issue.status, + priority: issue.priority, + list_position: issue.list_position, + description: issue.description, + description_text: issue.description_text, + estimate: issue.estimate, + time_spent: issue.time_spent, + time_remaining: issue.time_remaining, + project_id: issue.project_id, + reporter_id: issue.reporter_id, + user_ids: issue.user_ids, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct CreateCommentPayload { pub user_id: Option, diff --git a/jirs-server/seed.sql b/jirs-server/seed.sql index db84a6fb..0bddc62d 100644 --- a/jirs-server/seed.sql +++ b/jirs-server/seed.sql @@ -1,5 +1,11 @@ insert into projects (name) values ('initial'), ('second'), ('third'); -insert into users (project_id, email, name, avatar_url) values (1, 'john@example.com', 'John Doe', 'http://cdn.onlinewebfonts.com/svg/img_553934.png'), (1, 'kate@exampe.com', 'Kate Snow', 'http://www.asthmamd.org/images/icon_user_6.png'); +insert into users (project_id, email, name, avatar_url) values ( + 1, 'john@example.com', 'John Doe', 'http://cdn.onlinewebfonts.com/svg/img_553934.png +), ( + 1, 'kate@exampe.com', 'Kate Snow', 'http://www.asthmamd.org/images/icon_user_6.png +), ( + 1, 'mike@example.com', 'Mike Keningham', 'https://cdn0.iconfinder.com/data/icons/user-pictures/100/matureman1-512.png' +); insert into tokens (user_id, access_token, refresh_token) values (1, uuid_generate_v4(), uuid_generate_v4() ); insert into issues( title, diff --git a/jirs-server/src/ws/mod.rs b/jirs-server/src/ws/mod.rs index 0ea835e5..61a41cbe 100644 --- a/jirs-server/src/ws/mod.rs +++ b/jirs-server/src/ws/mod.rs @@ -37,7 +37,7 @@ impl WebSocketActor { use futures::executor::block_on; if msg != WsMsg::Ping && msg != WsMsg::Pong { - info!("(2)incoming message: {:?}", msg); + info!("incoming message: {:?}", msg); } let msg = match msg { @@ -196,10 +196,6 @@ impl StreamHandler> for WebSocketActor { Ok(m) => m, _ => return, }; - if msg != WsMsg::Ping && msg != WsMsg::Pong { - info!("(1)incoming message: {:?}", msg); - } - let _x = 1; match self.handle_ws_msg(msg) { Ok(Some(msg)) => ctx.send_msg(msg), Err(e) => ctx.send_msg(e),