From 2bc2e5f73e6f10be3c41d6eedb5055c1cbcda113 Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Thu, 2 Apr 2020 19:32:40 +0200 Subject: [PATCH] Handle multiple modals, fix copy icon background --- jirs-client/js/css/shared.css | 150 ---------------------------- jirs-client/js/css/styledButton.css | 100 +++++++++++++++++++ jirs-client/js/css/styledInput.css | 48 +++++++++ jirs-client/js/styles.css | 2 + jirs-client/src/lib.rs | 7 +- jirs-client/src/modal/mod.rs | 115 +++++++++++++++++++++ jirs-client/src/model.rs | 4 +- jirs-client/src/project.rs | 106 ++------------------ jirs-client/src/project_settings.rs | 4 +- jirs-client/src/shared/mod.rs | 6 +- jirs-client/src/shared/modal.rs | 2 +- jirs-server/src/schema.rs | 9 +- 12 files changed, 286 insertions(+), 267 deletions(-) create mode 100644 jirs-client/js/css/styledButton.css create mode 100644 jirs-client/js/css/styledInput.css create mode 100644 jirs-client/src/modal/mod.rs diff --git a/jirs-client/js/css/shared.css b/jirs-client/js/css/shared.css index 8de8728d..7c7d5824 100644 --- a/jirs-client/js/css/shared.css +++ b/jirs-client/js/css/shared.css @@ -3,153 +3,3 @@ padding-top: 18px; border-top: 1px solid var(--borderLight); } - -.styledButton { - display: inline-flex; - align-items: center; - justify-content: center; - height: 32px; - vertical-align: middle; - line-height: 1; - white-space: nowrap; - border-radius: 3px; - transition: all 0.1s; - appearance: none; - cursor: pointer; - user-select: none; - font-size: 14.5px; -} - -.styledButton.withIcon > span.text { - margin-left: 7px; -} - -.styledButton:disabled { - opacity: 0.6; - cursor: default; -} - -.styledButton:not(.onlyIcon) { - padding: 0 12px; -} - -.styledButton.onlyIcon { - padding: 0 9px; -} - -.styledButton.primary, .styledButton.primary > i { - color: #fff; - background: var(--primary); - font-family: var(--font-medium); -} - -.styledButton.primary:not(:disabled):hover { - filter: brightness(115%); -} - -.styledButton.primary:not(:disabled):active { - filter: brightness(110%); -} - -.styledButton.primary:not(:disabled).isActive { - filter: brightness(110%); -} - -.styledButton.success, .styledButton.success > i { - color: #fff; - background: var(--success); -} - -.styledButton.danger, .styledButton.danger > i { - color: #fff; - background: var(--danger); -} - -.styledButton.secondary, .styledButton.secondary > i { - color: var(--textDark); - background: var(--secondary); - font-family: var(--font-regular); -} - -.styledButton.secondary:not(:disabled):hover { - background: var(--backgroundLight); -} - -.styledButton.secondary:not(:disabled):active { - color: var(--primary); - background: var(--backgroundLightPrimary); -} - -.styledButton.secondary:not(:disabled).isActive { - color: var(--primary); - background: var(--backgroundLightPrimary); -} - -.styledButton.empty, .styledButton.empty > i { - background: #fff; - color: var(--textDark); - font-family: var(--font-regular); -} - -.styledButton.empty:not(:disabled):hover { - background: var(--backgroundLight); -} - -.styledButton.empty:not(:disabled):active { - color: var(--primary); - background: var(--backgroundLightPrimary); -} - -.styledButton.empty:not(:disabled).isActive { - color: var(--primary); - background: var(--backgroundLightPrimary); -} - -.styledInput { - position: relative; - display: inline-block; - height: 32px; - width: 100%; -} - -.styledInput > .inputElement { - height: 100%; - width: 100%; - padding: 0 7px; - border-radius: 3px; - border: 1px solid var(--borderLightest); - color: var(--textDarkest); - background: var(--backgroundLightest); - transition: background 0.1s; - font-family: var(--font-regular); - font-size: 15px; -} - -.styledInput > .inputElement.withIcon { - padding-left: 32px; -} - -.styledInput > i.styledIcon { - font-size: 15px; - position: absolute; - top: 8px; - left: 8px; - pointer-events: none; - color: #5E6C84; -} - -.styledInput > .inputElement:hover { - background: var(--backgroundLight); -} - -.styledInput > .inputElement:focus { - background: #fff; - border: 1px solid var(--borderInputFocus); - box-shadow: 0 0 0 1px var(--borderInputFocus); -} - -.styledInput.invalid, -.styledInput.invalid:focus { - border: 1px solid var(--danger); - box-shadow: none; -} diff --git a/jirs-client/js/css/styledButton.css b/jirs-client/js/css/styledButton.css new file mode 100644 index 00000000..1ae69c11 --- /dev/null +++ b/jirs-client/js/css/styledButton.css @@ -0,0 +1,100 @@ +.styledButton { + display: inline-flex; + align-items: center; + justify-content: center; + height: 32px; + vertical-align: middle; + line-height: 1; + white-space: nowrap; + border-radius: 3px; + transition: all 0.1s; + appearance: none; + cursor: pointer; + user-select: none; + font-size: 14.5px; +} + +.styledButton.withIcon > span.text { + margin-left: 7px; +} + +.styledButton:disabled { + opacity: 0.6; + cursor: default; +} + +.styledButton:not(.onlyIcon) { + padding: 0 12px; +} + +.styledButton.onlyIcon { + padding: 0 9px; +} + +.styledButton.primary, .styledButton.primary > i { + color: #fff; + background: var(--primary); + font-family: var(--font-medium); +} + +.styledButton.primary:not(:disabled):hover { + filter: brightness(115%); +} + +.styledButton.primary:not(:disabled):active { + filter: brightness(110%); +} + +.styledButton.primary:not(:disabled).isActive { + filter: brightness(110%); +} + +.styledButton.success, .styledButton.success > i { + color: #fff; + background: var(--success); +} + +.styledButton.danger, .styledButton.danger > i { + color: #fff; + background: var(--danger); +} + +.styledButton.secondary, .styledButton.secondary > i { + color: var(--textDark); + background: var(--secondary); + font-family: var(--font-regular); +} + +.styledButton.secondary:not(:disabled):hover { + background: var(--backgroundLight); +} + +.styledButton.secondary:not(:disabled):active { + color: var(--primary); + background: var(--backgroundLightPrimary); +} + +.styledButton.secondary:not(:disabled).isActive { + color: var(--primary); + background: var(--backgroundLightPrimary); +} + +.styledButton.empty, .styledButton.empty > i { + background: #fff; + color: var(--textDark); + font-family: var(--font-regular); +} + +.styledButton.empty:not(:disabled):hover, .styledButton.empty:not(:disabled):hover > i { + background: var(--backgroundLight); +} + +.styledButton.empty:not(:disabled):active { + color: var(--primary); + background: var(--backgroundLightPrimary); +} + +.styledButton.empty:not(:disabled).isActive { + color: var(--primary); + background: var(--backgroundLightPrimary); +} diff --git a/jirs-client/js/css/styledInput.css b/jirs-client/js/css/styledInput.css new file mode 100644 index 00000000..96efca56 --- /dev/null +++ b/jirs-client/js/css/styledInput.css @@ -0,0 +1,48 @@ +.styledInput { + position: relative; + display: inline-block; + height: 32px; + width: 100%; +} + +.styledInput > .inputElement { + height: 100%; + width: 100%; + padding: 0 7px; + border-radius: 3px; + border: 1px solid var(--borderLightest); + color: var(--textDarkest); + background: var(--backgroundLightest); + transition: background 0.1s; + font-family: var(--font-regular); + font-size: 15px; +} + +.styledInput > .inputElement.withIcon { + padding-left: 32px; +} + +.styledInput > i.styledIcon { + font-size: 15px; + position: absolute; + top: 8px; + left: 8px; + pointer-events: none; + color: #5E6C84; +} + +.styledInput > .inputElement:hover { + background: var(--backgroundLight); +} + +.styledInput > .inputElement:focus { + background: #fff; + border: 1px solid var(--borderInputFocus); + box-shadow: 0 0 0 1px var(--borderInputFocus); +} + +.styledInput.invalid, +.styledInput.invalid:focus { + border: 1px solid var(--danger); + box-shadow: none; +} diff --git a/jirs-client/js/styles.css b/jirs-client/js/styles.css index e1736073..2e31dac6 100644 --- a/jirs-client/js/styles.css +++ b/jirs-client/js/styles.css @@ -9,6 +9,8 @@ @import "css/styledTooltip.css"; @import "css/styledAvatar.css"; @import "css/styledSelect.css"; +@import "css/styledButton.css"; +@import "css/styledInput.css"; @import "css/app.css"; @import "css/modal.css"; @import "css/issue.css"; diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 981c2467..5b68de67 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -9,6 +9,7 @@ use crate::shared::styled_select::StyledSelectChange; mod api; mod api_handlers; mod login; +mod modal; mod model; mod project; mod project_settings; @@ -51,7 +52,7 @@ pub enum Msg { IssueUpdateResult(FetchObject), // modals - CloseModal, + PopModal, } fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { @@ -62,12 +63,10 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { Msg::ChangePage(page) => { model.page = page; } - Msg::CloseModal => { - model.modal = None; - } _ => (), } crate::shared::update(&msg, model, orders); + crate::modal::update(&msg, model, orders); match model.page { Page::Project => project::update(msg, model, orders), Page::EditIssue(_id) => project::update(msg, model, orders), diff --git a/jirs-client/src/modal/mod.rs b/jirs-client/src/modal/mod.rs new file mode 100644 index 00000000..872fc158 --- /dev/null +++ b/jirs-client/src/modal/mod.rs @@ -0,0 +1,115 @@ +use crate::api::update_issue; +use crate::model::{EditIssueModal, ModalType, Page}; +use crate::project::issue_details; +use crate::shared::modal::{Modal, Variant as ModalVariant}; +use crate::shared::styled_select::StyledSelectChange; +use crate::shared::{find_issue, ToNode}; +use crate::{model, FieldId, Msg}; +use jirs_data::{Issue, IssueType, UpdateIssuePayload}; +use seed::{prelude::*, *}; + +pub fn update(msg: &Msg, model: &mut model::Model, orders: &mut impl Orders) { + match msg { + Msg::PopModal => match model.modals.pop() { + _ => (), + }, + Msg::ChangePage(Page::EditIssue(issue_id)) => { + let value = find_issue(model, *issue_id) + .map(|issue| issue.issue_type.clone()) + .unwrap_or_else(|| IssueType::Task); + model.modals.push(ModalType::EditIssue( + *issue_id, + EditIssueModal { + id: *issue_id, + top_select_opened: false, + top_select_filter: "".to_string(), + value, + link_copied: false, + }, + )); + } + + Msg::StyledSelectChanged(FieldId::IssueTypeEditModalTop, change) => { + match (change, model.modals.last_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(issue_id, modal)), + ) => { + modal.value = (*value).into(); + let project = match model.project.as_mut() { + Some(p) => p, + _ => return, + }; + let mut found: Option<&mut Issue> = None; + for issue in project.issues.iter_mut() { + if issue.id == *issue_id { + found = Some(issue); + break; + } + } + let issue = match found { + Some(i) => i, + _ => return, + }; + + let form = UpdateIssuePayload { + title: Some(issue.title.clone()), + issue_type: Some(modal.value.clone()), + status: Some(issue.status.clone()), + priority: Some(issue.priority.clone()), + list_position: Some(issue.list_position), + description: Some(issue.description.clone()), + description_text: Some(issue.description_text.clone()), + estimate: Some(issue.estimate.clone()), + time_spent: Some(issue.time_spent.clone()), + time_remaining: Some(issue.time_remaining.clone()), + project_id: Some(issue.project_id.clone()), + user_ids: Some(issue.user_ids.clone()), + }; + orders.skip().perform_cmd(update_issue( + model.host_url.clone(), + *issue_id, + form, + )); + } + _ => {} + } + } + _ => (), + } +} + +pub fn view(model: &model::Model) -> Node { + let modals: Vec> = model + .modals + .iter() + .map(|modal| match modal { + 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, + with_icon: false, + children: vec![details], + } + .into_node(); + modal + } else { + empty![] + } + } + _ => empty![], + }) + .collect(); + section![id!["modals"], modals] +} diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 5f411aa6..e52acd7f 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -84,7 +84,7 @@ pub struct Model { pub page: Page, pub host_url: String, pub project_page: ProjectPage, - pub modal: Option, + pub modals: Vec, } impl Default for Model { @@ -109,7 +109,7 @@ impl Default for Model { recently_updated_filter: false, dragged_issue_id: None, }, - modal: None, + modals: vec![], } } } diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index 2cd82101..f23a06ed 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -2,14 +2,12 @@ use seed::{prelude::*, *}; use jirs_data::*; -use crate::api::update_issue; -use crate::model::{EditIssueModal, Icon, ModalType, Model, Page}; -use crate::shared::modal::{Modal, Variant as ModalVariant}; +use crate::model::{EditIssueModal, Icon, Model, Page}; 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, StyledSelectChange}; -use crate::shared::{drag_ev, find_issue, inner_layout, ToNode}; +use crate::shared::styled_select::StyledSelect; +use crate::shared::{drag_ev, inner_layout, ToNode}; use crate::{FieldId, IssueId, Msg}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { @@ -22,26 +20,13 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order .skip() .perform_cmd(crate::api::fetch_current_user(model.host_url.clone())); } - Msg::ChangePage(Page::EditIssue(issue_id)) => { + Msg::ChangePage(Page::EditIssue(_issue_id)) => { orders .skip() .perform_cmd(crate::api::fetch_current_project(model.host_url.clone())); orders .skip() .perform_cmd(crate::api::fetch_current_user(model.host_url.clone())); - 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, - link_copied: false, - }, - )); } Msg::ToggleAboutTooltip => { model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible; @@ -131,61 +116,6 @@ 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(issue_id, modal)), - ) => { - modal.value = value.into(); - let project = match model.project.as_mut() { - Some(p) => p, - _ => return, - }; - let mut found: Option<&mut Issue> = None; - for issue in project.issues.iter_mut() { - if issue.id == *issue_id { - found = Some(issue); - break; - } - } - let issue = match found { - Some(i) => i, - _ => return, - }; - - let form = UpdateIssuePayload { - title: Some(issue.title.clone()), - issue_type: Some(modal.value.clone()), - status: Some(issue.status.clone()), - priority: Some(issue.priority.clone()), - list_position: Some(issue.list_position), - description: Some(issue.description.clone()), - description_text: Some(issue.description_text.clone()), - estimate: Some(issue.estimate.clone()), - time_spent: Some(issue.time_spent.clone()), - time_remaining: Some(issue.time_remaining.clone()), - project_id: Some(issue.project_id.clone()), - user_ids: Some(issue.user_ids.clone()), - }; - orders.skip().perform_cmd(update_issue( - model.host_url.clone(), - *issue_id, - form, - )); - } - _ => {} - } - } Msg::IssueUpdateResult(fetched) => { crate::api_handlers::update_issue_response(&fetched, model); } @@ -194,25 +124,6 @@ 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, 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, - with_icon: false, - children: vec![details], - } - .into_node(); - Some(modal) - } else { - None - } - } - _ => None, - }; - let project_section = vec![ breadcrumbs(model), header(), @@ -220,7 +131,12 @@ pub fn view(model: &Model) -> Node { project_board_lists(model), ]; - inner_layout(model, "projectPage", project_section, modal) + inner_layout( + model, + "projectPage", + project_section, + crate::modal::view(model), + ) } fn breadcrumbs(model: &Model) -> Node { @@ -512,7 +428,7 @@ impl crate::shared::styled_select::SelectOption for IssueTypeOption { } } -fn issue_details(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node { +pub fn issue_details(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node { let issue_id = issue.id; let issue_type_select = StyledSelect { diff --git a/jirs-client/src/project_settings.rs b/jirs-client/src/project_settings.rs index 6601c536..c1a5a9ca 100644 --- a/jirs-client/src/project_settings.rs +++ b/jirs-client/src/project_settings.rs @@ -1,4 +1,4 @@ -use seed::prelude::*; +use seed::{prelude::*, *}; use crate::shared::inner_layout; use crate::{model, Msg}; @@ -8,5 +8,5 @@ pub fn update(_msg: Msg, _model: &mut model::Model, _orders: &mut impl Orders Node { let project_section = vec![]; - inner_layout(model, "projectSettings", project_section, None) + inner_layout(model, "projectSettings", project_section, empty![]) } diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index 597ecc3d..d486a777 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -45,12 +45,8 @@ pub fn inner_layout( model: &Model, page_name: &str, children: Vec>, - modal: Option>, + modal_node: Node, ) -> Node { - let modal_node = match modal { - Some(modal) => vec![modal], - _ => vec![], - }; article![ modal_node, attrs![At::Class => "inner-layout"], diff --git a/jirs-client/src/shared/modal.rs b/jirs-client/src/shared/modal.rs index 5f04a5fe..ee234f88 100644 --- a/jirs-client/src/shared/modal.rs +++ b/jirs-client/src/shared/modal.rs @@ -57,7 +57,7 @@ pub fn render(values: Modal) -> Node { empty![] }; - let close_handler = mouse_ev(Ev::Click, |_| Msg::CloseModal); + let close_handler = mouse_ev(Ev::Click, |_| Msg::PopModal); let body_handler = mouse_ev(Ev::Click, |ev| { ev.stop_propagation(); Msg::NoOp diff --git a/jirs-server/src/schema.rs b/jirs-server/src/schema.rs index cf59d64d..00d1c0bf 100644 --- a/jirs-server/src/schema.rs +++ b/jirs-server/src/schema.rs @@ -349,11 +349,4 @@ joinable!(issues -> users (reporter_id)); joinable!(tokens -> users (user_id)); joinable!(users -> projects (project_id)); -allow_tables_to_appear_in_same_query!( - comments, - issue_assignees, - issues, - projects, - tokens, - users, -); +allow_tables_to_appear_in_same_query!(comments, issue_assignees, issues, projects, tokens, users,);