Handle multiple modals, fix copy icon background

This commit is contained in:
Adrian Wozniak 2020-04-02 19:32:40 +02:00
parent f778f5a5c5
commit 2bc2e5f73e
12 changed files with 286 additions and 267 deletions

View File

@ -3,153 +3,3 @@
padding-top: 18px; padding-top: 18px;
border-top: 1px solid var(--borderLight); 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;
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -9,6 +9,8 @@
@import "css/styledTooltip.css"; @import "css/styledTooltip.css";
@import "css/styledAvatar.css"; @import "css/styledAvatar.css";
@import "css/styledSelect.css"; @import "css/styledSelect.css";
@import "css/styledButton.css";
@import "css/styledInput.css";
@import "css/app.css"; @import "css/app.css";
@import "css/modal.css"; @import "css/modal.css";
@import "css/issue.css"; @import "css/issue.css";

View File

@ -9,6 +9,7 @@ use crate::shared::styled_select::StyledSelectChange;
mod api; mod api;
mod api_handlers; mod api_handlers;
mod login; mod login;
mod modal;
mod model; mod model;
mod project; mod project;
mod project_settings; mod project_settings;
@ -51,7 +52,7 @@ pub enum Msg {
IssueUpdateResult(FetchObject<String>), IssueUpdateResult(FetchObject<String>),
// modals // modals
CloseModal, PopModal,
} }
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
@ -62,12 +63,10 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
Msg::ChangePage(page) => { Msg::ChangePage(page) => {
model.page = page; model.page = page;
} }
Msg::CloseModal => {
model.modal = None;
}
_ => (), _ => (),
} }
crate::shared::update(&msg, model, orders); crate::shared::update(&msg, model, orders);
crate::modal::update(&msg, model, orders);
match model.page { match model.page {
Page::Project => project::update(msg, model, orders), Page::Project => project::update(msg, model, orders),
Page::EditIssue(_id) => project::update(msg, model, orders), Page::EditIssue(_id) => project::update(msg, model, orders),

View File

@ -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<Msg>) {
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<Msg> {
let modals: Vec<Node<Msg>> = 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]
}

View File

@ -84,7 +84,7 @@ pub struct Model {
pub page: Page, pub page: Page,
pub host_url: String, pub host_url: String,
pub project_page: ProjectPage, pub project_page: ProjectPage,
pub modal: Option<ModalType>, pub modals: Vec<ModalType>,
} }
impl Default for Model { impl Default for Model {
@ -109,7 +109,7 @@ impl Default for Model {
recently_updated_filter: false, recently_updated_filter: false,
dragged_issue_id: None, dragged_issue_id: None,
}, },
modal: None, modals: vec![],
} }
} }
} }

View File

@ -2,14 +2,12 @@ use seed::{prelude::*, *};
use jirs_data::*; use jirs_data::*;
use crate::api::update_issue; use crate::model::{EditIssueModal, Icon, 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_avatar::StyledAvatar;
use crate::shared::styled_button::{StyledButton, Variant as ButtonVariant}; use crate::shared::styled_button::{StyledButton, Variant as ButtonVariant};
use crate::shared::styled_input::StyledInput; use crate::shared::styled_input::StyledInput;
use crate::shared::styled_select::{StyledSelect, StyledSelectChange}; use crate::shared::styled_select::StyledSelect;
use crate::shared::{drag_ev, find_issue, inner_layout, ToNode}; use crate::shared::{drag_ev, inner_layout, ToNode};
use crate::{FieldId, IssueId, Msg}; use crate::{FieldId, IssueId, Msg};
pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders<Msg>) {
@ -22,26 +20,13 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
.skip() .skip()
.perform_cmd(crate::api::fetch_current_user(model.host_url.clone())); .perform_cmd(crate::api::fetch_current_user(model.host_url.clone()));
} }
Msg::ChangePage(Page::EditIssue(issue_id)) => { Msg::ChangePage(Page::EditIssue(_issue_id)) => {
orders orders
.skip() .skip()
.perform_cmd(crate::api::fetch_current_project(model.host_url.clone())); .perform_cmd(crate::api::fetch_current_project(model.host_url.clone()));
orders orders
.skip() .skip()
.perform_cmd(crate::api::fetch_current_user(model.host_url.clone())); .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 => { Msg::ToggleAboutTooltip => {
model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible; 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 :("), _ => 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) => { Msg::IssueUpdateResult(fetched) => {
crate::api_handlers::update_issue_response(&fetched, model); 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<Msg> { pub fn view(model: &Model) -> Node<Msg> {
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![ let project_section = vec![
breadcrumbs(model), breadcrumbs(model),
header(), header(),
@ -220,7 +131,12 @@ pub fn view(model: &Model) -> Node<Msg> {
project_board_lists(model), 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<Msg> { fn breadcrumbs(model: &Model) -> Node<Msg> {
@ -512,7 +428,7 @@ impl crate::shared::styled_select::SelectOption for IssueTypeOption {
} }
} }
fn issue_details(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node<Msg> { pub fn issue_details(_model: &Model, issue: &Issue, modal: &EditIssueModal) -> Node<Msg> {
let issue_id = issue.id; let issue_id = issue.id;
let issue_type_select = StyledSelect { let issue_type_select = StyledSelect {

View File

@ -1,4 +1,4 @@
use seed::prelude::*; use seed::{prelude::*, *};
use crate::shared::inner_layout; use crate::shared::inner_layout;
use crate::{model, Msg}; use crate::{model, Msg};
@ -8,5 +8,5 @@ pub fn update(_msg: Msg, _model: &mut model::Model, _orders: &mut impl Orders<Ms
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let project_section = vec![]; let project_section = vec![];
inner_layout(model, "projectSettings", project_section, None) inner_layout(model, "projectSettings", project_section, empty![])
} }

View File

@ -45,12 +45,8 @@ pub fn inner_layout(
model: &Model, model: &Model,
page_name: &str, page_name: &str,
children: Vec<Node<Msg>>, children: Vec<Node<Msg>>,
modal: Option<Node<Msg>>, modal_node: Node<Msg>,
) -> Node<Msg> { ) -> Node<Msg> {
let modal_node = match modal {
Some(modal) => vec![modal],
_ => vec![],
};
article![ article![
modal_node, modal_node,
attrs![At::Class => "inner-layout"], attrs![At::Class => "inner-layout"],

View File

@ -57,7 +57,7 @@ pub fn render(values: Modal) -> Node<Msg> {
empty![] 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| { let body_handler = mouse_ev(Ev::Click, |ev| {
ev.stop_propagation(); ev.stop_propagation();
Msg::NoOp Msg::NoOp

View File

@ -349,11 +349,4 @@ joinable!(issues -> users (reporter_id));
joinable!(tokens -> users (user_id)); joinable!(tokens -> users (user_id));
joinable!(users -> projects (project_id)); joinable!(users -> projects (project_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(comments, issue_assignees, issues, projects, tokens, users,);
comments,
issue_assignees,
issues,
projects,
tokens,
users,
);