diff --git a/jirs-client/js/css/issue.css b/jirs-client/js/css/issue.css new file mode 100644 index 00000000..53951ac2 --- /dev/null +++ b/jirs-client/js/css/issue.css @@ -0,0 +1,38 @@ +.issueDetails > .content { + display: flex; + padding: 0 30px 60px; +} + +.issueDetails > .content > .left { + width: 65%; + padding-right: 50px; +} + +.issueDetails > .content > .right { + width: 35%; + padding-top: 5px; +} + +.issueDetails > .topActions { + display: flex; + justify-content: space-between; + padding: 21px 18px 0; +} + +.issueDetails > .topActions > .topActionsRight { + display: flex; + align-items: center; +} + +.issueDetails > .topActions > .topActionsRight > * { + margin-left: 4px; +} + +.issueDetails > .sectionTitle { + margin: 24px 0 5px; + text-transform: uppercase; + color: var(--textMedium); + font-size: 12.5px; + font-family: "CircularStdBold", serif; + font-weight: normal +} diff --git a/jirs-client/js/css/icon.css b/jirs-client/js/css/styledIcon.css similarity index 100% rename from jirs-client/js/css/icon.css rename to jirs-client/js/css/styledIcon.css diff --git a/jirs-client/js/css/styledSelect.css b/jirs-client/js/css/styledSelect.css new file mode 100644 index 00000000..cd539953 --- /dev/null +++ b/jirs-client/js/css/styledSelect.css @@ -0,0 +1,98 @@ +.styledSelect { + position: relative; + border-radius: 4px; + cursor: pointer; + font-size: 14px +} + +.styledSelect.normal { + width: 100%; + border: 1px solid var(--borderLightest); + background: var(--backgroundLightest); + transition: background 0.1s; +} + +.styledSelect.empty { + display: inline-block; +} + +.styledSelect:hover { + background: var(--backgroundLight); +} + +.styledSelect:focus { + outline: none; +} + +.styledSelect.normal:focus { + border: 1px solid var(--borderInputFocus); + box-shadow: 0 0 0 1px var(--borderInputFocus); + background: #fff; +} + +.styledSelect.invalid, .styledSelect.invalid:focus { + border: 1px solid var(--danger); + box-shadow: none; +} + +.styledSelect > .dropDownInput { + padding: 10px 14px 8px; + width: 100%; + border: none; + color: var(--textDarkest); + background: none; +} + +.styledSelect > .dropDownInput:focus { + outline: none; +} + +.styledSelect > .options { + max-height: 200px; + overflow-x: hidden; + overflow-y: auto; + -webkit-overflow-scrolling: touch; +} + +.styledSelect > .options::-webkit-scrollbar { + width: 8px; +} + +.styledSelect > .options::-webkit-scrollbar-track { + background: none; +} + +.styledSelect > .options::-webkit-scrollbar-thumb { + border-radius: 99px; + background: var(--backgroundMedium); +} + +.styledSelect > .options > .option { + padding: 8px 14px; + word-break: break-word; + cursor: pointer; +} + +.styledSelect > .options > .option:last-of-type { + margin-bottom: 8px; +} + +.styledSelect > .options > .option.jira-select-option-is-active { + background: var(--backgroundLightPrimary); +} + +.styledSelect > .noOptions { + padding: 5px 15px 15px; + color: var(--textLight); +} + +.styledSelect > .styledIcon { + position: absolute; + top: 4px; + right: 7px; + padding: 5px; + font-size: 16px; + color: var(--textMedium); + cursor: pointer; + user-select: none; +} diff --git a/jirs-client/js/styles.css b/jirs-client/js/styles.css index 811f8ec9..e1736073 100644 --- a/jirs-client/js/styles.css +++ b/jirs-client/js/styles.css @@ -4,10 +4,12 @@ @import "css/global.css"; @import "css/sidebar.css"; @import "css/aside.css"; -@import "css/icon.css"; +@import "css/styledIcon.css"; @import "css/shared.css"; @import "css/styledTooltip.css"; @import "css/styledAvatar.css"; +@import "css/styledSelect.css"; @import "css/app.css"; @import "css/modal.css"; +@import "css/issue.css"; @import "css/project.css"; diff --git a/jirs-client/src/lib.rs b/jirs-client/src/lib.rs index 4f6bde5c..0699f1c1 100644 --- a/jirs-client/src/lib.rs +++ b/jirs-client/src/lib.rs @@ -37,11 +37,13 @@ pub enum Msg { // dragging IssueDragStarted(IssueId), IssueDragStopped(IssueId), - IssueDragOver(f64, f64), IssueDropZone(IssueStatus), // issues IssueUpdateResult(FetchObject), + + // modals + CloseModal, } fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders) { @@ -52,6 +54,9 @@ 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); diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index f3686bb1..75953668 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -9,6 +9,11 @@ use crate::{IssueId, UserId, HOST_URL}; pub type ProjectId = i32; +#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] +pub enum ModalType { + EditIssue(IssueId), +} + #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] #[serde(rename_all = "kebab-case")] pub enum Page { @@ -47,12 +52,6 @@ pub struct UpdateProjectForm { pub fields: UpdateProjectPayload, } -#[derive(Serialize, Deserialize, Debug, Default)] -pub struct Point { - pub x: f64, - pub y: f64, -} - #[derive(Serialize, Deserialize, Debug)] pub struct ProjectPage { pub about_tooltip_visible: bool, @@ -61,7 +60,6 @@ pub struct ProjectPage { pub only_my_filter: bool, pub recently_updated_filter: bool, pub dragged_issue_id: Option, - pub drag_point: Point, } #[derive(Serialize, Deserialize, Debug)] @@ -77,6 +75,7 @@ pub struct Model { pub page: Page, pub host_url: String, pub project_page: ProjectPage, + pub modal: Option, } impl Default for Model { @@ -100,8 +99,8 @@ impl Default for Model { only_my_filter: false, recently_updated_filter: false, dragged_issue_id: None, - drag_point: Point::default(), }, + modal: None, } } } diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index 0204c91a..9acb8394 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -2,11 +2,13 @@ use seed::{prelude::*, *}; use jirs_data::*; -use crate::model::{Icon, Model, Page}; +use crate::model::{Icon, ModalType, Model, Page}; +use crate::shared::modal::{Modal, Variant as ModalVariant}; use crate::shared::styled_avatar::StyledAvatar; -use crate::shared::styled_button::{StyledButton, Variant}; +use crate::shared::styled_button::{StyledButton, Variant as ButtonVariant}; use crate::shared::styled_input::StyledInput; -use crate::shared::{drag_ev, inner_layout, ToNode}; +use crate::shared::styled_select::StyledSelect; +use crate::shared::{drag_ev, find_issue, inner_layout, ToNode}; use crate::Msg; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { @@ -19,6 +21,15 @@ 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)) => { + 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())); + model.modal = Some(ModalType::EditIssue(issue_id)); + } Msg::ToggleAboutTooltip => { model.project_page.about_tooltip_visible = !model.project_page.about_tooltip_visible; } @@ -58,10 +69,6 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order Msg::IssueDragStopped(_) => { model.project_page.dragged_issue_id = None; } - Msg::IssueDragOver(x, y) => { - model.project_page.drag_point.x = x; - model.project_page.drag_point.y = y; - } Msg::IssueDropZone(status) => { match ( model.project_page.dragged_issue_id.as_ref().cloned(), @@ -116,6 +123,25 @@ 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)) => { + if let Some(issue) = find_issue(model, issue_id) { + let details = issue_details(model, issue); + 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(), @@ -123,7 +149,7 @@ pub fn view(model: &Model) -> Node { project_board_lists(model), ]; - inner_layout(model, "projectPage", project_section) + inner_layout(model, "projectPage", project_section, modal) } fn breadcrumbs(model: &Model) -> Node { @@ -144,7 +170,7 @@ fn breadcrumbs(model: &Model) -> Node { fn header() -> Node { let button = StyledButton { - variant: Variant::Secondary, + variant: ButtonVariant::Secondary, icon_only: false, disabled: false, active: false, @@ -175,7 +201,7 @@ fn project_board_filters(model: &Model) -> Node { let project_page = &model.project_page; let only_my = StyledButton { - variant: Variant::Empty, + variant: ButtonVariant::Empty, icon_only: false, disabled: false, active: model.project_page.only_my_filter, @@ -186,7 +212,7 @@ fn project_board_filters(model: &Model) -> Node { .into_node(); let recently_updated = StyledButton { - variant: Variant::Empty, + variant: ButtonVariant::Empty, icon_only: false, disabled: false, active: model.project_page.recently_updated_filter, @@ -345,8 +371,10 @@ fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node "issueLink"], + attrs![At::Class => "issueLink"; At::Href => href], div![ attrs![At::Class => class_list.join(" "), At::Draggable => true], drag_started, @@ -363,3 +391,43 @@ fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node Node { + div![self.to_string()] + } +} + +fn issue_details(_model: &Model, _issue: &Issue) -> Node { + let issue_type_select = StyledSelect { + on_change: mouse_ev(Ev::Click, |_| Msg::NoOp), + variant: crate::shared::styled_select::Variant::Empty, + width: 150, + name: None, + placeholder: None, + valid: false, + is_multi: false, + allow_clear: true, + options: vec![IssueType::Story, IssueType::Task, IssueType::Bug], + } + .into_node(); + + div![ + attrs![At::Class => "issueDetails"], + div![ + attrs![At::Class => "topActions"], + issue_type_select, + div![attrs![At::Class => "topActionsRight"]], + ], + div![ + attrs![At::Class => "content"], + div![ + attrs![At::Class => "left"], + div![attrs![At::Class => "title"]], + div![attrs![At::Class => "description"]], + div![attrs![At::Class => "comments"]], + ], + div![attrs![At::Class => "right"]], + ], + ] +} diff --git a/jirs-client/src/project_settings.rs b/jirs-client/src/project_settings.rs index 0f926a07..6601c536 100644 --- a/jirs-client/src/project_settings.rs +++ b/jirs-client/src/project_settings.rs @@ -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) + inner_layout(model, "projectSettings", project_section, None) } diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index fe7b4966..2597dbde 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -1,8 +1,10 @@ use seed::{prelude::*, *}; use wasm_bindgen::JsCast; +use jirs_data::Issue; + use crate::model::{Icon, Model}; -use crate::Msg; +use crate::{IssueId, Msg}; pub mod aside; pub mod modal; @@ -10,8 +12,16 @@ pub mod navbar_left; pub mod styled_avatar; pub mod styled_button; pub mod styled_input; +pub mod styled_select; pub mod styled_tooltip; +pub fn find_issue(model: &Model, issue_id: IssueId) -> Option<&Issue> { + match model.project.as_ref() { + Some(p) => p.issues.iter().find(|issue| issue.id == issue_id), + _ => None, + } +} + pub trait ToNode { fn into_node(self) -> Node; } @@ -24,8 +34,18 @@ pub fn divider() -> Node { div![attrs![At::Class => "divider"], ""] } -pub fn inner_layout(model: &Model, page_name: &str, children: Vec>) -> Node { +pub fn inner_layout( + model: &Model, + page_name: &str, + children: Vec>, + modal: Option>, +) -> Node { + let modal_node = match modal { + Some(modal) => vec![modal], + _ => vec![], + }; article![ + modal_node, attrs![At::Class => "inner-layout"], id![page_name], navbar_left::render(model), diff --git a/jirs-client/src/shared/modal.rs b/jirs-client/src/shared/modal.rs index fc19c416..5f04a5fe 100644 --- a/jirs-client/src/shared/modal.rs +++ b/jirs-client/src/shared/modal.rs @@ -48,6 +48,7 @@ pub fn render(values: Modal) -> Node { with_icon, children, } = values; + let icon = if with_icon { let mut styled_icon = styled_icon(Icon::Close); styled_icon.add_class(variant.to_icon_class_name().to_string()); @@ -56,6 +57,12 @@ pub fn render(values: Modal) -> Node { empty![] }; + let close_handler = mouse_ev(Ev::Click, |_| Msg::CloseModal); + let body_handler = mouse_ev(Ev::Click, |ev| { + ev.stop_propagation(); + Msg::NoOp + }); + let clickable_class = format!("clickableOverlay {}", variant.to_class_name()); let styled_modal_class = format!("styledModal {}", variant.to_class_name()); let styled_modal_style = format!("max-width: {width}px", width = width); @@ -63,8 +70,10 @@ pub fn render(values: Modal) -> Node { attrs![At::Class => "modal"], div![ attrs![At::Class => clickable_class], + close_handler, div![ attrs![At::Class => styled_modal_class, At::Style => styled_modal_style], + body_handler, icon, children ] diff --git a/jirs-client/src/shared/styled_select.rs b/jirs-client/src/shared/styled_select.rs new file mode 100644 index 00000000..32086569 --- /dev/null +++ b/jirs-client/src/shared/styled_select.rs @@ -0,0 +1,93 @@ +use seed::{prelude::*, *}; + +use crate::shared::ToNode; +use crate::Msg; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Variant { + Empty, + Normal, +} + +pub struct StyledSelect +where + Child: ToNode, +{ + pub on_change: EventHandler, + pub variant: Variant, + pub width: usize, + pub name: Option, + pub placeholder: Option, + pub valid: bool, + pub is_multi: bool, + pub allow_clear: bool, + pub options: Vec, +} + +impl ToNode for StyledSelect +where + Child: ToNode, +{ + fn into_node(self) -> Node { + render(self) + } +} + +pub fn render(values: StyledSelect) -> Node +where + Child: ToNode, +{ + let StyledSelect { + on_change, + variant, + width, + name, + placeholder, + valid, + is_multi, + allow_clear, + options, + } = values; + + let select_style = format!("width: {width}px", width = width); + let mut select_class = vec!["styledSelect"]; + if !valid { + select_class.push("invalid"); + } + + let children: Vec> = options + .into_iter() + .map(|child| render_option(child.into_node())) + .collect(); + + let clear_icon = match allow_clear { + true => crate::shared::styled_icon(crate::model::Icon::Close), + false => empty![], + }; + + seed::div![ + on_change.clone(), + attrs![At::Class => "styledSelect", At::Style => select_style], + seed::input![ + attrs![ + At::Class => "dropDownInput", + At::Type => "text" + At::Placeholder => "Search" + At::AutoFocus => true, + ], + on_change, + ], + clear_icon, + seed::div![ + attrs![ + At::Class => "options", + ], + children + ], + seed::div![attrs![At::Class => "noOptions"], "No results"] + ] +} + +pub fn render_option(content: Node) -> Node { + seed::div![attrs![At::Class => "option"], content,] +} diff --git a/react-client/src/App/BaseStyles.js b/react-client/src/App/BaseStyles.js index c8a084ad..68ba99ed 100644 --- a/react-client/src/App/BaseStyles.js +++ b/react-client/src/App/BaseStyles.js @@ -49,7 +49,7 @@ export default createGlobalStyle` } h1, h2, h3, h4, h5, h6, strong { - ${font.bold} + font-family: "CircularStdBold"; font-weight: normal } button { diff --git a/react-client/src/Project/Board/IssueDetails/Comments/Create/ProTip/Styles.js b/react-client/src/Project/Board/IssueDetails/Comments/Create/ProTip/Styles.js index a3039fa7..b524d19f 100644 --- a/react-client/src/Project/Board/IssueDetails/Comments/Create/ProTip/Styles.js +++ b/react-client/src/Project/Board/IssueDetails/Comments/Create/ProTip/Styles.js @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { color, font } from 'shared/utils/styles'; +import { color } from 'shared/utils/styles'; export const Tip = styled.div` display: flex; @@ -22,6 +22,6 @@ export const TipLetter = styled.span` border-radius: 2px; color: ${color.textDarkest}; background: ${color.backgroundMedium}; - ${font.bold} + font-family: "CircularStdBold"; font-weight: normal font-size: 12px `; diff --git a/react-client/src/Project/Board/IssueDetails/Styles.js b/react-client/src/Project/Board/IssueDetails/Styles.js index 728616e5..dd0b7d7b 100644 --- a/react-client/src/Project/Board/IssueDetails/Styles.js +++ b/react-client/src/Project/Board/IssueDetails/Styles.js @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { color, font } from 'shared/utils/styles'; +import { color } from 'shared/utils/styles'; export const Content = styled.div` display: flex; @@ -36,5 +36,5 @@ export const SectionTitle = styled.div` text-transform: uppercase; color: ${color.textMedium}; font-size: 12.5px - ${font.bold} + font-family: "CircularStdBold"; font-weight: normal `; diff --git a/react-client/src/Project/Board/IssueDetails/Type/index.jsx b/react-client/src/Project/Board/IssueDetails/Type/index.jsx index 31227e65..142b5c68 100644 --- a/react-client/src/Project/Board/IssueDetails/Type/index.jsx +++ b/react-client/src/Project/Board/IssueDetails/Type/index.jsx @@ -1,18 +1,18 @@ -import React from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; -import { IssueType, IssueTypeCopy } from 'shared/constants/issues'; -import { IssueTypeIcon, Select } from 'shared/components'; +import { IssueType, IssueTypeCopy } from '../../../../shared/constants/issues'; +import { IssueTypeIcon, Select } from '../../../../shared/components'; -import { TypeButton, Type, TypeLabel } from './Styles'; +import { Type, TypeButton, TypeLabel } from './Styles'; const propTypes = { - issue: PropTypes.object.isRequired, - updateIssue: PropTypes.func.isRequired, + issue: PropTypes.object.isRequired, + updateIssue: PropTypes.func.isRequired, }; const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => ( -