Feature(Project board): Add text filter input
This commit is contained in:
parent
567f5e63c5
commit
55a8904210
@ -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;*/
|
||||||
}
|
/*}*/
|
||||||
}
|
}
|
||||||
|
50
jirs-client/js/css/project.css
Normal file
50
jirs-client/js/css/project.css
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -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>) {
|
||||||
|
@ -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(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
66
jirs-client/src/shared/aside.rs
Normal file
66
jirs-client/src/shared/aside.rs
Normal 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],
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
@ -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,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
101
jirs-client/src/shared/navbar_left.rs
Normal file
101
jirs-client/src/shared/navbar_left.rs
Normal 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()
|
||||||
|
}
|
55
jirs-client/src/shared/styled_input.rs
Normal file
55
jirs-client/src/shared/styled_input.rs
Normal 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]
|
||||||
|
]
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user