Feature(Project board): Add text filter input

This commit is contained in:
Adrian Wozniak 2020-03-31 08:56:46 +02:00
parent 567f5e63c5
commit 55a8904210
18 changed files with 486 additions and 259 deletions

View File

@ -10,13 +10,13 @@ main {
min-height: 100%; min-height: 100%;
} }
article#inner-layout { article.inner-layout {
width: 100%; width: 100%;
} }
@media (min-width: 1240px) { @media (min-width: 1240px) {
article#inner-layout { /*article.inner-layout {*/
display: flex; /* display: flex;*/
justify-content: start; /* justify-content: start;*/
} /*}*/
} }

View File

@ -0,0 +1,50 @@
#projectPage {
padding: 25px 32px 50px calc(var(--appNavBarLeftWidth) + var(--secondarySideBarWidth) + 40px);
}
#projectPage > .breadcrumbsContainer {
color: var(--textMedium);
font-size: 15px;
}
#projectPage > .breadcrumbsContainer > .breadcrumbsDivider {
position: relative;
top: 2px;
margin: 0 10px;
font-size: 18px;
}
#projectPage > #projectBoardHeader {
margin-top: 6px;
display: flex;
justify-content: space-between;
}
#projectPage > #projectBoardHeader > #boardName {
font-size: 24px;
font-family: var(--font-medium);
font-weight: normal;
}
#projectPage > #projectBoardFilters {
display: flex;
align-items: center;
margin-top: 24px;
}
#projectPage > #projectBoardFilters > #searchInput {
margin-right: 18px;
width: 160px;
}
@media (max-width: 1100px) {
#projectPage {
padding: 25px 20px 50px calc(var(--appNavBarLeftWidth) + var(--secondarySideBarWidth) + 20px);
}
}
@media (max-width: 999px) {
#projectPage {
padding-left: calc(var(--appNavBarLeftWidth) + var(--secondarySideBarWidth) + 20px - var(--secondarySideBarWidth));
}
}

View File

@ -37,7 +37,7 @@
padding: 0 9px; padding: 0 9px;
} }
.styledButton.primary { .styledButton.primary, .styledButton.primary > i {
color: #fff; color: #fff;
background: var(--primary); background: var(--primary);
font-family: var(--font-medium); font-family: var(--font-medium);
@ -55,17 +55,17 @@
filter: brightness(110%); filter: brightness(110%);
} }
.styledButton.success { .styledButton.success, .styledButton.success > i {
color: #fff; color: #fff;
background: var(--success); background: var(--success);
} }
.styledButton.danger { .styledButton.danger, .styledButton.danger > i {
color: #fff; color: #fff;
background: var(--danger); background: var(--danger);
} }
.styledButton.secondary { .styledButton.secondary, .styledButton.secondary > i {
color: var(--textDark); color: var(--textDark);
background: var(--secondary); background: var(--secondary);
font-family: var(--font-regular); font-family: var(--font-regular);
@ -85,7 +85,7 @@
background: var(--backgroundLightPrimary); background: var(--backgroundLightPrimary);
} }
.styledButton.empty { .styledButton.empty, .styledButton.empty > i {
background: #fff; background: #fff;
color: var(--textDark); color: var(--textDark);
font-family: var(--font-regular); font-family: var(--font-regular);
@ -104,3 +104,52 @@
color: var(--primary); color: var(--primary);
background: var(--backgroundLightPrimary); 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

@ -8,3 +8,4 @@
@import "css/shared.css"; @import "css/shared.css";
@import "css/styledTooltip.css"; @import "css/styledTooltip.css";
@import "css/app.css"; @import "css/app.css";
@import "css/project.css";

View File

@ -18,6 +18,9 @@ pub enum Msg {
CurrentUserResult(FetchObject<String>), CurrentUserResult(FetchObject<String>),
InternalFailure(String), InternalFailure(String),
ToggleAboutTooltip, ToggleAboutTooltip,
// project
ProjectTextFilterChanged(String),
} }
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>) {

View File

@ -47,6 +47,12 @@ pub struct UpdateProjectForm {
pub fields: UpdateProjectPayload, pub fields: UpdateProjectPayload,
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct ProjectPage {
pub about_tooltip_visible: bool,
pub text_filter: String,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Model { pub struct Model {
pub access_token: Option<Uuid>, pub access_token: Option<Uuid>,
@ -59,8 +65,7 @@ pub struct Model {
pub comments_by_project_id: HashMap<ProjectId, Vec<Comment>>, pub comments_by_project_id: HashMap<ProjectId, Vec<Comment>>,
pub page: Page, pub page: Page,
pub host_url: String, pub host_url: String,
// pub project_page: ProjectPage,
pub about_tooltip_visible: bool,
} }
impl Default for Model { impl Default for Model {
@ -77,7 +82,10 @@ impl Default for Model {
comments_by_project_id: Default::default(), comments_by_project_id: Default::default(),
page: Page::Project, page: Page::Project,
host_url, host_url,
about_tooltip_visible: false, project_page: ProjectPage {
about_tooltip_visible: false,
text_filter: "".to_string(),
},
} }
} }
} }

View File

@ -1,6 +1,8 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use crate::model::Page; use crate::model::{Icon, Model, Page};
use crate::shared::styled_button::{StyledButton, Variant};
use crate::shared::styled_input::StyledInput;
use crate::shared::{host_client, inner_layout}; use crate::shared::{host_client, inner_layout};
use crate::Msg; use crate::Msg;
@ -15,14 +17,64 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
.perform_cmd(crate::api::fetch_current_user(model.host_url.clone())); .perform_cmd(crate::api::fetch_current_user(model.host_url.clone()));
} }
Msg::ToggleAboutTooltip => { Msg::ToggleAboutTooltip => {
model.about_tooltip_visible = !model.about_tooltip_visible; model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible;
}
Msg::ProjectTextFilterChanged(text) => {
model.project_page.text_filter = text;
} }
_ => (), _ => (),
} }
} }
pub fn view(model: &crate::model::Model) -> Node<Msg> { pub fn view(model: &Model) -> Node<Msg> {
let project_section = section![id!["project-section"],]; let project_section = vec![breadcrumbs(model), header(), project_board_filters(model)];
inner_layout(model, project_section) inner_layout(model, "projectPage", project_section)
}
fn breadcrumbs(model: &Model) -> Node<Msg> {
let project_name = model
.project
.as_ref()
.map(|p| p.name.clone())
.unwrap_or_default();
div![
attrs![At::Class => "breadcrumbsContainer"],
span!["Projects"],
span![attrs![At::Class => "breadcrumbsDivider"], "/"],
span![project_name],
span![attrs![At::Class => "breadcrumbsDivider"], "/"],
span!["Kanban Board"]
]
}
fn header() -> Node<Msg> {
let button = StyledButton {
variant: Variant::Secondary,
icon_only: false,
disabled: false,
active: false,
text: Some("Github Repo".to_string()),
icon: Some(Icon::Github),
}
.into_node();
div![
id!["projectBoardHeader"],
div![id!["boardName"], "Kanban board"],
a![
attrs![At::Href => "https://gitlab.com/adrian.wozniak/jirs", At::Target => "__blank", At::Rel => "noreferrer noopener"],
button
]
]
}
fn project_board_filters(_model: &Model) -> Node<Msg> {
let search_input = StyledInput {
icon: Some(Icon::Search),
id: Some("searchInput".to_string()),
valid: true,
on_change: input_ev(Ev::Change, |value| Msg::ProjectTextFilterChanged(value)),
}
.into_node();
div![id!["projectBoardFilters"], search_input]
} }

View File

@ -1,11 +1,12 @@
use seed::{prelude::*, *};
use crate::shared::inner_layout; use crate::shared::inner_layout;
use crate::{model, Msg}; use crate::{model, Msg};
use seed::{prelude::*, *};
pub fn update(_msg: Msg, _model: &mut model::Model, _orders: &mut impl Orders<Msg>) {} pub fn update(_msg: Msg, _model: &mut model::Model, _orders: &mut impl Orders<Msg>) {}
pub fn view(model: &model::Model) -> Node<Msg> { pub fn view(model: &model::Model) -> Node<Msg> {
let project_section = section![id!["project-settings-section"],]; let project_section = vec![];
inner_layout(model, project_section) inner_layout(model, "projectSettings", project_section)
} }

View File

@ -0,0 +1,66 @@
use seed::{prelude::*, *};
use crate::model::{Icon, Model, Page};
use crate::shared::{divider, styled_icon};
use crate::Msg;
pub fn render(model: &Model) -> Node<Msg> {
let project_icon = Node::from_html(include_str!("../../static/project-avatar.svg"));
let project_info = match model.project.as_ref() {
Some(project) => li![
id!["projectInfo"],
project_icon,
div![
attrs![At::Class => "projectTexts";],
div![attrs![At::Class => "projectName";], project.name],
div![attrs![At::Class => "projectCategory";], project.category]
],
],
_ => li![
id!["projectInfo"],
div![
attrs![At::Class => "projectTexts";],
div![attrs![At::Class => "projectName";], ""],
div![attrs![At::Class => "projectCategory";], ""]
],
],
};
nav![
id!["sidebar"],
ul![
project_info,
sidebar_link_item(model, "Kanban Board", Icon::Board, Some(Page::Project)),
sidebar_link_item(
model,
"Project settings",
Icon::Settings,
Some(Page::ProjectSettings)
),
li![divider()],
sidebar_link_item(model, "Releases", Icon::Shipping, None),
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
sidebar_link_item(model, "Pages", Icon::Page, None),
sidebar_link_item(model, "Reports", Icon::Reports, None),
sidebar_link_item(model, "Components", Icon::Component, None),
]
]
}
fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>) -> Node<Msg> {
let path = page.map(|ref p| p.to_path()).unwrap_or_default();
let mut class_list = vec!["linkItem".to_string(), icon.to_string()];
if let None = page {
class_list.push("notAllowed".to_string());
};
if Some(model.page) == page {
class_list.push("active".to_string());
}
li![
attrs![At::Class => class_list.join(" ")],
a![
attrs![At::Href => path],
styled_icon(icon),
div![attrs![At::Class => "linkText"], name],
]
]
}

View File

@ -2,170 +2,16 @@ use seed::fetch::{FetchObject, ResponseWithDataResult};
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::FullProjectResponse; use jirs_data::FullProjectResponse;
use styled_button::*;
use crate::model::{Icon, Model, Page}; use crate::model::{Icon, Model};
use crate::Msg; use crate::Msg;
pub mod aside;
pub mod navbar_left;
pub mod styled_button; pub mod styled_button;
pub mod styled_input;
pub mod styled_tooltip; pub mod styled_tooltip;
pub fn navbar_left(model: &Model) -> Vec<Node<Msg>> {
let logo_svg = Node::from_html(include_str!("../../static/logo.svg"));
vec![
about_tooltip_popup(model),
aside![
id!["navbar-left"],
a![
attrs![At::Class => "logoLink", At::Href => "/"],
div![attrs![At::Class => "styledLogo"], logo_svg]
],
navbar_left_item(model, "Search issues", Icon::Search),
navbar_left_item(model, "Create Issue", Icon::Plus),
div![
attrs![At::Class => "bottom"],
about_tooltip(model, navbar_left_item(model, "About", Icon::Help)),
],
],
]
}
fn navbar_left_item(_model: &Model, text: &str, logo: Icon) -> Node<Msg> {
div![
attrs![At::Class => "item"],
i![attrs![At::Class => format!("styledIcon {}", logo)]],
span![attrs![At::Class => "itemText"], text]
]
}
pub fn about_tooltip(model: &Model, children: Node<Msg>) -> Node<Msg> {
div![
attrs![At::Class => "aboutTooltip"],
ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
children
]
}
fn about_tooltip_popup(model: &Model) -> Node<Msg> {
styled_tooltip::StyledTooltip {
visible: model.about_tooltip_visible,
class_name: "aboutTooltipPopup".to_string(),
children: div![
ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
attrs![At::Class => "feedbackDropdown"],
div![
attrs![At::Class => "feedbackImageCont"],
img![attrs![At::Src => "/feedback.png", At::Class => "feedbackImage"]]
],
div![
attrs![At::Class => "feedbackParagraph"],
"This simplified Jira clone is built with Seed.rs on the front-end and Actix-Web on the back-end."
],
div![
attrs![At::Class => "feedbackParagraph"],
"Read more on my website or reach out via ",
a![
attrs![At::Href => "mailto:adrian.wozniak@ita-prog.pl"],
strong!["adrian.wozniak@ita-prog.pl"]
]
],
a![
attrs![
At::Href => "https://gitlab.com/adrian.wozniak/jirs",
At::Target => "_blank",
At::Rel => "noreferrer noopener",
],
StyledButton {
text: Some("Visit Website".to_string()),
variant: Variant::Primary,
disabled: false,
active: false,
icon_only: false,
icon: None,
}.into_node(),
],
a![
id!["about-github-button"],
attrs![
At::Href => "https://gitlab.com/adrian.wozniak/jirs",
At::Target => "_blank",
At::Rel => "noreferrer noopener",
],
StyledButton {
text: Some("Github Repo".to_string()),
variant: Variant::Secondary,
disabled: false,
active: false,
icon_only: false,
icon: Some(Icon::Github),
}.into_node()
]
],
}.into_node()
}
pub fn sidebar(model: &Model) -> Node<Msg> {
let project_icon = Node::from_html(include_str!("../../static/project-avatar.svg"));
let project_info = match model.project.as_ref() {
Some(project) => li![
id!["projectInfo"],
project_icon,
div![
attrs![At::Class => "projectTexts";],
div![attrs![At::Class => "projectName";], project.name],
div![attrs![At::Class => "projectCategory";], project.category]
],
],
_ => li![
id!["projectInfo"],
div![
attrs![At::Class => "projectTexts";],
div![attrs![At::Class => "projectName";], ""],
div![attrs![At::Class => "projectCategory";], ""]
],
],
};
nav![
id!["sidebar"],
ul![
project_info,
sidebar_link_item(model, "Kanban Board", Icon::Board, Some(Page::Project)),
sidebar_link_item(
model,
"Project settings",
Icon::Settings,
Some(Page::ProjectSettings)
),
li![divider()],
sidebar_link_item(model, "Releases", Icon::Shipping, None),
sidebar_link_item(model, "Issue and Filters", Icon::Issues, None),
sidebar_link_item(model, "Pages", Icon::Page, None),
sidebar_link_item(model, "Reports", Icon::Reports, None),
sidebar_link_item(model, "Components", Icon::Component, None),
]
]
}
fn sidebar_link_item(model: &Model, name: &str, icon: Icon, page: Option<Page>) -> Node<Msg> {
let path = page.map(|ref p| p.to_path()).unwrap_or_default();
let mut class_list = vec!["linkItem".to_string(), icon.to_string()];
let item_class = if let None = page {
class_list.push("notAllowed".to_string())
};
if Some(model.page) == page {
class_list.push("active".to_string());
}
li![
attrs![At::Class => class_list.join(" ")],
a![
attrs![At::Href => path],
styled_icon(icon),
div![attrs![At::Class => "linkText"], name],
]
]
}
pub fn styled_icon(icon: Icon) -> Node<Msg> { pub fn styled_icon(icon: Icon) -> Node<Msg> {
i![attrs![At::Class => format!("styledIcon {}", icon)], ""] i![attrs![At::Class => format!("styledIcon {}", icon)], ""]
} }
@ -174,11 +20,12 @@ pub fn divider() -> Node<Msg> {
div![attrs![At::Class => "divider"], ""] div![attrs![At::Class => "divider"], ""]
} }
pub fn inner_layout(model: &Model, children: Node<Msg>) -> Node<Msg> { pub fn inner_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
article![ article![
id!["inner-layout"], attrs![At::Class => "inner-layout"],
navbar_left(model), id![page_name],
sidebar(model), navbar_left::render(model),
aside::render(model),
children, children,
] ]
} }

View File

@ -0,0 +1,101 @@
use seed::{prelude::*, *};
use crate::model::{Icon, Model};
use crate::shared::styled_button::{StyledButton, Variant};
use crate::shared::styled_tooltip;
use crate::Msg;
pub fn render(model: &Model) -> Vec<Node<Msg>> {
let logo_svg = Node::from_html(include_str!("../../static/logo.svg"));
vec![
about_tooltip_popup(model),
aside![
id!["navbar-left"],
a![
attrs![At::Class => "logoLink", At::Href => "/"],
div![attrs![At::Class => "styledLogo"], logo_svg]
],
navbar_left_item(model, "Search issues", Icon::Search),
navbar_left_item(model, "Create Issue", Icon::Plus),
div![
attrs![At::Class => "bottom"],
about_tooltip(model, navbar_left_item(model, "About", Icon::Help)),
],
],
]
}
fn navbar_left_item(_model: &Model, text: &str, logo: Icon) -> Node<Msg> {
div![
attrs![At::Class => "item"],
i![attrs![At::Class => format!("styledIcon {}", logo)]],
span![attrs![At::Class => "itemText"], text]
]
}
pub fn about_tooltip(_model: &Model, children: Node<Msg>) -> Node<Msg> {
div![
attrs![At::Class => "aboutTooltip"],
ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
children
]
}
fn about_tooltip_popup(model: &Model) -> Node<Msg> {
styled_tooltip::StyledTooltip {
visible: model.project_page.about_tooltip_visible,
class_name: "aboutTooltipPopup".to_string(),
children: div![
ev(Ev::Click, |_| Msg::ToggleAboutTooltip),
attrs![At::Class => "feedbackDropdown"],
div![
attrs![At::Class => "feedbackImageCont"],
img![attrs![At::Src => "/feedback.png", At::Class => "feedbackImage"]]
],
div![
attrs![At::Class => "feedbackParagraph"],
"This simplified Jira clone is built with Seed.rs on the front-end and Actix-Web on the back-end."
],
div![
attrs![At::Class => "feedbackParagraph"],
"Read more on my website or reach out via ",
a![
attrs![At::Href => "mailto:adrian.wozniak@ita-prog.pl"],
strong!["adrian.wozniak@ita-prog.pl"]
]
],
a![
attrs![
At::Href => "https://gitlab.com/adrian.wozniak/jirs",
At::Target => "_blank",
At::Rel => "noreferrer noopener",
],
StyledButton {
text: Some("Visit Website".to_string()),
variant: Variant::Primary,
disabled: false,
active: false,
icon_only: false,
icon: None,
}.into_node(),
],
a![
id!["about-github-button"],
attrs![
At::Href => "https://gitlab.com/adrian.wozniak/jirs",
At::Target => "_blank",
At::Rel => "noreferrer noopener",
],
StyledButton {
text: Some("Github Repo".to_string()),
variant: Variant::Secondary,
disabled: false,
active: false,
icon_only: false,
icon: Some(Icon::Github),
}.into_node()
]
],
}.into_node()
}

View File

@ -0,0 +1,55 @@
use seed::{prelude::*, *};
use crate::model::Icon;
use crate::shared::styled_icon;
use crate::Msg;
pub struct StyledInput {
pub id: Option<String>,
pub icon: Option<Icon>,
pub valid: bool,
pub on_change: EventHandler<Msg>,
}
impl Into<Node<Msg>> for StyledInput {
fn into(self) -> Node<Msg> {
render(self)
}
}
impl StyledInput {
pub fn into_node(self) -> Node<Msg> {
self.into()
}
}
pub fn render(values: StyledInput) -> Node<Msg> {
let StyledInput {
id,
icon,
valid,
on_change,
} = values;
let mut wrapper_class_list = vec!["styledInput"];
if !valid {
wrapper_class_list.push("invalid");
}
let mut input_class_list = vec!["inputElement"];
if icon.is_some() {
input_class_list.push("withIcon");
}
let icon = match icon {
Some(icon) => vec![styled_icon(icon)],
_ => vec![],
};
div![
id![id.unwrap_or_default()],
attrs!(At::Class => wrapper_class_list.join(" ")),
icon,
input![attrs![At::Class => input_class_list.join(" ")], on_change]
]
}

View File

@ -2,15 +2,15 @@ import React from 'react';
import { Button } from 'shared/components'; import { Button } from 'shared/components';
import { Header, BoardName } from './Styles'; import { BoardName, Header } from './Styles';
const ProjectBoardHeader = () => ( const ProjectBoardHeader = () => (
<Header> <Header id='projectHeader'>
<BoardName>Kanban board</BoardName> <BoardName id='boardName'>Kanban board</BoardName>
<a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener"> <a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener">
<Button icon="github">Github Repo</Button> <Button icon="github">Github Repo</Button>
</a> </a>
</Header> </Header>
); );
export default ProjectBoardHeader; export default ProjectBoardHeader;

View File

@ -1,6 +1,6 @@
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Route, useRouteMatch, useHistory } from 'react-router-dom'; import { Route, useHistory, useRouteMatch } from 'react-router-dom';
import useMergeState from 'shared/hooks/mergeState'; import useMergeState from 'shared/hooks/mergeState';
import { Breadcrumbs, Modal } from 'shared/components'; import { Breadcrumbs, Modal } from 'shared/components';
@ -10,12 +10,6 @@ import Filters from './Filters';
import Lists from './Lists'; import Lists from './Lists';
import IssueDetails from './IssueDetails'; import IssueDetails from './IssueDetails';
const propTypes = {
project: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
updateLocalProjectIssues: PropTypes.func.isRequired,
};
const defaultFilters = { const defaultFilters = {
searchTerm: '', searchTerm: '',
userIds: [], userIds: [],
@ -69,6 +63,10 @@ const ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => {
); );
}; };
ProjectBoard.propTypes = propTypes; ProjectBoard.propTypes = {
project: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
updateLocalProjectIssues: PropTypes.func.isRequired,
};
export default ProjectBoard; export default ProjectBoard;

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Icon, AboutTooltip } from 'shared/components'; import { AboutTooltip, Icon } from 'shared/components';
import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles'; import { Bottom, Item, ItemText, LogoLink, NavLeft, StyledLogo } from './Styles';
const propTypes = { const propTypes = {
issueSearchModalOpen: PropTypes.func.isRequired, issueSearchModalOpen: PropTypes.func.isRequired,
@ -11,17 +11,17 @@ const propTypes = {
}; };
const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => ( const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (
<NavLeft> <NavLeft class={ 'NavLeft' }>
<LogoLink to="/"> <LogoLink to="/">
<StyledLogo color="#fff" /> <StyledLogo color="#fff"/>
</LogoLink> </LogoLink>
<Item onClick={issueSearchModalOpen}> <Item onClick={ issueSearchModalOpen }>
<Icon type="search" size={22} top={1} left={3} /> <Icon type="search" size={ 22 } top={ 1 } left={ 3 }/>
<ItemText>Search issues</ItemText> <ItemText>Search issues</ItemText>
</Item> </Item>
<Item onClick={issueCreateModalOpen}> <Item onClick={ issueCreateModalOpen }>
<Icon type="plus" size={27} /> <Icon type="plus" size={27} />
<ItemText>Create Issue</ItemText> <ItemText>Create Issue</ItemText>
</Item> </Item>

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Route, Redirect, useRouteMatch, useHistory } from 'react-router-dom'; import { Redirect, Route, useHistory, useRouteMatch } from 'react-router-dom';
import useApi from 'shared/hooks/api'; import useApi from 'shared/hooks/api';
import { updateArrayItemById } from 'shared/utils/javascript'; import { updateArrayItemById } from 'shared/utils/javascript';
import { createQueryParamModalHelpers } from 'shared/utils/queryParamModal'; import { createQueryParamModalHelpers } from 'shared/utils/queryParamModal';
import { PageLoader, PageError, Modal } from 'shared/components'; import { Modal, PageError, PageLoader } from 'shared/components';
import NavbarLeft from './NavbarLeft'; import NavbarLeft from './NavbarLeft';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
@ -38,17 +38,17 @@ const Project = () => {
}; };
return ( return (
<ProjectPage> <ProjectPage id={ 'ProjectPage' }>
<NavbarLeft <NavbarLeft
issueSearchModalOpen={issueSearchModalHelpers.open} issueSearchModalOpen={ issueSearchModalHelpers.open }
issueCreateModalOpen={issueCreateModalHelpers.open} issueCreateModalOpen={ issueCreateModalHelpers.open }
/> />
<Sidebar project={project} /> <Sidebar project={ project }/>
{issueSearchModalHelpers.isOpen() && ( { issueSearchModalHelpers.isOpen() && (
<Modal <Modal
isOpen isOpen
testid="modal:issue-search" testid="modal:issue-search"
variant="aside" variant="aside"
width={600} width={600}

View File

@ -3,21 +3,19 @@ import PropTypes from 'prop-types';
import { Container, Divider } from './Styles'; import { Container, Divider } from './Styles';
const propTypes = {
items: PropTypes.array.isRequired,
};
const Breadcrumbs = ({ items }) => ( const Breadcrumbs = ({ items }) => (
<Container> <Container id='Breadcrumbs'>
{items.map((item, index) => ( { items.map((item, index) => (
<Fragment key={item}> <Fragment key={ item }>
{index !== 0 && <Divider>/</Divider>} { index !== 0 && <Divider>/</Divider> }
{item} { item }
</Fragment> </Fragment>
))} )) }
</Container> </Container>
); );
Breadcrumbs.propTypes = propTypes; Breadcrumbs.propTypes = {
items: PropTypes.array.isRequired,
};
export default Breadcrumbs; export default Breadcrumbs;

View File

@ -6,28 +6,6 @@ import Icon from 'shared/components/Icon';
import { StyledButton, StyledSpinner, Text } from './Styles'; import { StyledButton, StyledSpinner, Text } from './Styles';
const propTypes = {
className: PropTypes.string,
children: PropTypes.node,
variant: PropTypes.oneOf(['primary', 'success', 'danger', 'secondary', 'empty']),
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
iconSize: PropTypes.number,
disabled: PropTypes.bool,
isWorking: PropTypes.bool,
onClick: PropTypes.func,
};
const defaultProps = {
className: undefined,
children: undefined,
variant: 'secondary',
icon: undefined,
iconSize: 18,
disabled: false,
isWorking: false,
onClick: () => {},
};
const Button = forwardRef( const Button = forwardRef(
({ children, variant, icon, iconSize, disabled, isWorking, onClick, ...buttonProps }, ref) => { ({ children, variant, icon, iconSize, disabled, isWorking, onClick, ...buttonProps }, ref) => {
const handleClick = () => { const handleClick = () => {
@ -52,17 +30,37 @@ const Button = forwardRef(
<Icon type={icon} size={iconSize} color={getIconColor(variant)} /> <Icon type={icon} size={iconSize} color={getIconColor(variant)} />
) : ( ) : (
icon icon
)} ) }
{children && <Text withPadding={isWorking || icon}>{children}</Text>} { children && <Text withPadding={ isWorking || icon }>{ children }</Text> }
</StyledButton> </StyledButton>
); );
}, },
); );
const getIconColor = variant => const getIconColor = variant =>
['secondary', 'empty'].includes(variant) ? color.textDark : '#fff'; [ 'secondary', 'empty' ].includes(variant) ? color.textDark : '#fff';
Button.propTypes = propTypes; Button.propTypes = {
Button.defaultProps = defaultProps; className: PropTypes.string,
children: PropTypes.node,
variant: PropTypes.oneOf([ 'primary', 'success', 'danger', 'secondary', 'empty' ]),
icon: PropTypes.oneOfType([ PropTypes.string, PropTypes.node ]),
iconSize: PropTypes.number,
disabled: PropTypes.bool,
isWorking: PropTypes.bool,
onClick: PropTypes.func,
};
Button.defaultProps = {
className: undefined,
children: undefined,
variant: 'secondary',
icon: undefined,
iconSize: 18,
disabled: false,
isWorking: false,
onClick: () => {
},
};
export default Button; export default Button;