From 801f07648d352a3d45ec2eaf16cd0fc3de164982 Mon Sep 17 00:00:00 2001 From: Adrian Wozniak Date: Wed, 1 Apr 2020 22:41:26 +0200 Subject: [PATCH] Better select --- jirs-client/js/css/issue.css | 21 ++++ jirs-client/js/css/styledSelect.css | 44 +++++++-- jirs-client/src/model.rs | 9 ++ jirs-client/src/project.rs | 58 +++++++++-- jirs-client/src/shared/mod.rs | 9 +- jirs-client/src/shared/navbar_left.rs | 2 + jirs-client/src/shared/styled_button.rs | 10 +- jirs-client/src/shared/styled_select.rs | 96 +++++++++++++------ jirs-data/src/lib.rs | 15 ++- .../Project/Board/IssueDetails/Type/index.jsx | 62 ++++++------ .../src/shared/components/Select/Styles.js | 4 +- 11 files changed, 238 insertions(+), 92 deletions(-) diff --git a/jirs-client/js/css/issue.css b/jirs-client/js/css/issue.css index 53951ac2..70eeb751 100644 --- a/jirs-client/js/css/issue.css +++ b/jirs-client/js/css/issue.css @@ -28,6 +28,27 @@ margin-left: 4px; } +.issueDetails > .topActions .styledSelect > .dropDown > .options > .option > .type { + display: flex; + align-items: center; +} + +.issueDetails > .topActions .styledSelect > .dropDown > .options > .option > .type > .styledIcon { + font-size: 18px; +} + +.issueDetails > .topActions .styledSelect > .dropDown > .options > .option > .type > .typeLabel { + padding: 0 5px 0 7px; + font-size: 15px +} + +.issueDetails > .topActions .styledSelect > .valueContainer > .value { + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--textMedium); + font-size: 13px +} + .issueDetails > .sectionTitle { margin: 24px 0 5px; text-transform: uppercase; diff --git a/jirs-client/js/css/styledSelect.css b/jirs-client/js/css/styledSelect.css index cd539953..ee98b364 100644 --- a/jirs-client/js/css/styledSelect.css +++ b/jirs-client/js/css/styledSelect.css @@ -35,7 +35,29 @@ box-shadow: none; } -.styledSelect > .dropDownInput { +.styledSelect > .valueContainer { + display: flex; + align-items: center; + width: 100%; +} + +.styledSelect > .valueContainer.normal { + min-height: 32px; + padding: 5px 5px 5px 10px; +} + +.styledSelect > .dropDown { + z-index: var(--dropdown); + position: absolute; + top: 100%; + left: 0; + border-radius: 0 0 4px 4px; + background: #fff; + box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px; + width: 100%; +} + +.styledSelect > .dropDown > .dropDownInput { padding: 10px 14px 8px; width: 100%; border: none; @@ -43,7 +65,7 @@ background: none; } -.styledSelect > .dropDownInput:focus { +.styledSelect > .dropDown > .dropDownInput:focus { outline: none; } @@ -54,34 +76,38 @@ -webkit-overflow-scrolling: touch; } -.styledSelect > .options::-webkit-scrollbar { +.styledSelect > .dropDown > .options::-webkit-scrollbar { width: 8px; } -.styledSelect > .options::-webkit-scrollbar-track { +.styledSelect > .dropDown > .options::-webkit-scrollbar-track { background: none; } -.styledSelect > .options::-webkit-scrollbar-thumb { +.styledSelect > .dropDown > .options::-webkit-scrollbar-thumb { border-radius: 99px; background: var(--backgroundMedium); } -.styledSelect > .options > .option { +.styledSelect > .dropDown > .options > .option { padding: 8px 14px; word-break: break-word; cursor: pointer; } -.styledSelect > .options > .option:last-of-type { +.styledSelect > .dropDown > .options > .option:last-of-type { margin-bottom: 8px; } -.styledSelect > .options > .option.jira-select-option-is-active { +.styledSelect > .dropDown > .options > .option.jira-select-option-is-active { background: var(--backgroundLightPrimary); } -.styledSelect > .noOptions { +.styledSelect > .dropDown > .options > .option:hover { + background: var(--backgroundLightPrimary); +} + +.styledSelect > .dropDown > .noOptions { padding: 5px 15px 15px; color: var(--textLight); } diff --git a/jirs-client/src/model.rs b/jirs-client/src/model.rs index 75953668..567420ec 100644 --- a/jirs-client/src/model.rs +++ b/jirs-client/src/model.rs @@ -142,6 +142,15 @@ pub enum Icon { ArrowRight, } +impl Icon { + pub fn to_color(self) -> Option { + match self { + Icon::Bug | Icon::Task | Icon::Story => Some(format!("var(--{})", self)), + _ => None, + } + } +} + impl std::fmt::Display for Icon { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let code = match self { diff --git a/jirs-client/src/project.rs b/jirs-client/src/project.rs index 9acb8394..0d20bcdc 100644 --- a/jirs-client/src/project.rs +++ b/jirs-client/src/project.rs @@ -9,7 +9,7 @@ use crate::shared::styled_button::{StyledButton, Variant as ButtonVariant}; use crate::shared::styled_input::StyledInput; use crate::shared::styled_select::StyledSelect; use crate::shared::{drag_ev, find_issue, inner_layout, ToNode}; -use crate::Msg; +use crate::{IssueId, Msg}; pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Orders) { match msg { @@ -177,6 +177,7 @@ fn header() -> Node { text: Some("Github Repo".to_string()), icon: Some(Icon::Github), on_click: None, + children: vec![], } .into_node(); div![ @@ -208,6 +209,7 @@ fn project_board_filters(model: &Model) -> Node { text: Some("Only My Issues".to_string()), icon: None, on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleOnlyMy)), + children: vec![], } .into_node(); @@ -219,6 +221,7 @@ fn project_board_filters(model: &Model) -> Node { text: Some("Recently Updated".to_string()), icon: None, on_click: Some(mouse_ev(Ev::Click, |_| Msg::ProjectToggleRecentlyUpdated)), + children: vec![], } .into_node(); @@ -392,23 +395,58 @@ fn project_issue(model: &Model, project: &FullProject, issue: &Issue) -> Node Node { - div![self.to_string()] +#[derive(PartialOrd, PartialEq, Debug)] +struct IssueTypeOption(IssueId, IssueType); + +impl crate::shared::styled_select::SelectOption for IssueTypeOption { + fn into_option(self) -> Node { + let name = self.1.to_label().to_owned(); + + let mut icon = crate::shared::styled_icon(self.1.into()); + icon.add_class("issueTypeIcon"); + + div![ + attrs![At::Class => "type"], + icon, + div![attrs![At::Class => "typeLabel"], name] + ] + } + + fn into_value(self) -> Node { + let issue_id = self.0; + let name = self.1.to_label().to_owned(); + + StyledButton { + variant: ButtonVariant::Empty, + icon_only: true, + disabled: false, + active: false, + text: None, + icon: Some(self.1.into()), + on_click: None, + children: vec![span![format!("{}-{}", name, issue_id)]], + } + .into_node() } } -fn issue_details(_model: &Model, _issue: &Issue) -> Node { +fn issue_details(_model: &Model, issue: &Issue) -> Node { + let issue_id = issue.id; let issue_type_select = StyledSelect { on_change: mouse_ev(Ev::Click, |_| Msg::NoOp), variant: crate::shared::styled_select::Variant::Empty, - width: 150, - name: None, + dropdown_width: Some(150), + name: Some("type".to_string()), placeholder: None, - valid: false, + valid: true, is_multi: false, - allow_clear: true, - options: vec![IssueType::Story, IssueType::Task, IssueType::Bug], + allow_clear: false, + options: vec![ + IssueTypeOption(issue_id, IssueType::Story), + IssueTypeOption(issue_id, IssueType::Task), + IssueTypeOption(issue_id, IssueType::Bug), + ], + selected: Some(IssueTypeOption(issue_id, IssueType::Story)), } .into_node(); diff --git a/jirs-client/src/shared/mod.rs b/jirs-client/src/shared/mod.rs index 2597dbde..597ecc3d 100644 --- a/jirs-client/src/shared/mod.rs +++ b/jirs-client/src/shared/mod.rs @@ -27,7 +27,14 @@ pub trait ToNode { } pub fn styled_icon(icon: Icon) -> Node { - i![attrs![At::Class => format!("styledIcon {}", icon)], ""] + let style = icon + .to_color() + .map(|s| format!("color: {}", s)) + .unwrap_or_default(); + i![ + attrs![At::Class => format!("styledIcon {}", icon), At::Style => style], + "" + ] } pub fn divider() -> Node { diff --git a/jirs-client/src/shared/navbar_left.rs b/jirs-client/src/shared/navbar_left.rs index 1b8b90ec..e87885c1 100644 --- a/jirs-client/src/shared/navbar_left.rs +++ b/jirs-client/src/shared/navbar_left.rs @@ -51,6 +51,7 @@ fn about_tooltip_popup(model: &Model) -> Node { icon_only: false, icon: None, on_click: None, + children: vec![], } .into_node(); let github_repo = StyledButton { @@ -61,6 +62,7 @@ fn about_tooltip_popup(model: &Model) -> Node { icon_only: false, icon: Some(Icon::Github), on_click: None, + children: vec![], } .into_node(); diff --git a/jirs-client/src/shared/styled_button.rs b/jirs-client/src/shared/styled_button.rs index 4aa6b38e..961ac934 100644 --- a/jirs-client/src/shared/styled_button.rs +++ b/jirs-client/src/shared/styled_button.rs @@ -34,6 +34,7 @@ pub struct StyledButton { pub text: Option, pub icon: Option, pub on_click: Option>, + pub children: Vec>, } impl ToNode for StyledButton { @@ -51,6 +52,7 @@ pub fn render(values: StyledButton) -> Node { active, icon, on_click, + children, } = values; let mut class_list = vec!["styledButton".to_string(), variant.to_string()]; if icon_only { @@ -72,7 +74,7 @@ pub fn render(values: StyledButton) -> Node { Some(i) => styled_icon(i), }; - button![ + seed::button![ attrs![ At::Class => class_list.join(" "), ], @@ -82,6 +84,10 @@ pub fn render(values: StyledButton) -> Node { false => vec![], }, icon_node, - span![attrs![At::Class => "text"], text.unwrap_or_default()], + span![ + attrs![At::Class => "text"], + text.unwrap_or_default(), + children + ], ] } diff --git a/jirs-client/src/shared/styled_select.rs b/jirs-client/src/shared/styled_select.rs index 32086569..22bd4935 100644 --- a/jirs-client/src/shared/styled_select.rs +++ b/jirs-client/src/shared/styled_select.rs @@ -9,24 +9,40 @@ pub enum Variant { Normal, } +impl std::fmt::Display for Variant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Variant::Empty => f.write_str("empty"), + Variant::Normal => f.write_str("normal"), + } + } +} + +pub trait SelectOption { + fn into_option(self) -> Node; + + fn into_value(self) -> Node; +} + pub struct StyledSelect where - Child: ToNode, + Child: SelectOption + PartialEq, { pub on_change: EventHandler, pub variant: Variant, - pub width: usize, + pub dropdown_width: Option, pub name: Option, pub placeholder: Option, pub valid: bool, pub is_multi: bool, pub allow_clear: bool, pub options: Vec, + pub selected: Option, } impl ToNode for StyledSelect where - Child: ToNode, + Child: SelectOption + PartialEq, { fn into_node(self) -> Node { render(self) @@ -35,59 +51,85 @@ where pub fn render(values: StyledSelect) -> Node where - Child: ToNode, + Child: SelectOption + PartialEq, { let StyledSelect { on_change, variant, - width, + dropdown_width, name, - placeholder, + placeholder: _, valid, - is_multi, + is_multi: _, allow_clear, options, + selected, } = values; - let select_style = format!("width: {width}px", width = width); - let mut select_class = vec!["styledSelect"]; + let dropdown_style = dropdown_width + .map(|n| format!("width: {}px", n)) + .unwrap_or_default(); + let mut select_class = vec!["styledSelect".to_string(), format!("{}", variant)]; if !valid { - select_class.push("invalid"); + select_class.push("invalid".to_string()); } let children: Vec> = options .into_iter() - .map(|child| render_option(child.into_node())) + .filter(|o| Some(o) != selected.as_ref()) + .map(|child| render_option(child.into_option())) .collect(); + let value = selected + .map(|m| render_value(m.into_value())) + .unwrap_or_else(|| empty![]); + 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, + let option_list = if children.is_empty() { + seed::div![attrs![At::Class => "noOptions"], "No results"] + } else { seed::div![ attrs![ At::Class => "options", ], children + ] + }; + + seed::div![ + on_change.clone(), + attrs![At::Class => select_class.join(" ")], + div![ + attrs![At::Class => format!("valueContainer {}", variant)], + value ], - seed::div![attrs![At::Class => "noOptions"], "No results"] + div![ + attrs![At::Class => "dropDown", At::Style => dropdown_style], + seed::input![ + attrs![ + At::Name => name.unwrap_or_default(), + At::Class => "dropDownInput", + At::Type => "text" + At::Placeholder => "Search" + At::AutoFocus => true, + ], + on_change, + ], + clear_icon, + option_list + ] ] } -pub fn render_option(content: Node) -> Node { - seed::div![attrs![At::Class => "option"], content,] +fn render_option(content: Node) -> Node { + div![attrs![At::Class => "option"], content] +} + +fn render_value(mut content: Node) -> Node { + content.add_class("value"); + content } diff --git a/jirs-data/src/lib.rs b/jirs-data/src/lib.rs index 74727a44..5ac60804 100644 --- a/jirs-data/src/lib.rs +++ b/jirs-data/src/lib.rs @@ -28,15 +28,12 @@ pub enum IssueType { Story, } -impl FromStr for IssueType { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "task" => Ok(IssueType::Task), - "bug" => Ok(IssueType::Bug), - "story" => Ok(IssueType::Story), - _ => Err(format!("Unknown type {:?}", s)), +impl IssueType { + pub fn to_label(&self) -> &str { + match self { + IssueType::Task => "Task", + IssueType::Bug => "Bug", + IssueType::Story => "Story", } } } diff --git a/react-client/src/Project/Board/IssueDetails/Type/index.jsx b/react-client/src/Project/Board/IssueDetails/Type/index.jsx index 142b5c68..8da895a9 100644 --- a/react-client/src/Project/Board/IssueDetails/Type/index.jsx +++ b/react-client/src/Project/Board/IssueDetails/Type/index.jsx @@ -1,42 +1,40 @@ -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 { IssueTypeIcon, Select } from '../../../../shared/components'; import { Type, TypeButton, TypeLabel } from './Styles'; -const propTypes = { - issue: PropTypes.object.isRequired, +const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => ( + ({ - value: type, - label: IssueTypeCopy[type], - }))} - onChange={type => updateIssue({ type })} - renderValue={({ value: type }) => ( - }> - {`${IssueTypeCopy[type]}-${issue.id}`} - - )} - renderOption={({ value: type }) => ( - updateIssue({ type })}> - - {IssueTypeCopy[type]} - - )} - /> -); - -ProjectBoardIssueDetailsType.propTypes = propTypes; - export default ProjectBoardIssueDetailsType; diff --git a/react-client/src/shared/components/Select/Styles.js b/react-client/src/shared/components/Select/Styles.js index a3f7c5f8..790be068 100644 --- a/react-client/src/shared/components/Select/Styles.js +++ b/react-client/src/shared/components/Select/Styles.js @@ -1,7 +1,7 @@ import styled, { css } from 'styled-components'; import { color, mixin, zIndexValues } from 'shared/utils/styles'; -import Icon from 'shared/components/Icon'; +import Icon from 'shared/components/Icon'; export const StyledSelect = styled.div` position: relative; @@ -96,7 +96,7 @@ export const Dropdown = styled.div` left: 0; border-radius: 0 0 4px 4px; background: #fff; - ${mixin.boxShadowDropdown} + box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px; ${props => (props.width ? `width: ${props.width}px;` : 'width: 100%;')} `;