Add modal and initial select

This commit is contained in:
Adrian Woźniak 2020-04-01 18:30:01 +02:00
parent 4a9ba8e2a3
commit 28d35026ef
23 changed files with 460 additions and 140 deletions

View File

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

View File

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

View File

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

View File

@ -37,11 +37,13 @@ pub enum Msg {
// dragging
IssueDragStarted(IssueId),
IssueDragStopped(IssueId),
IssueDragOver(f64, f64),
IssueDropZone(IssueStatus),
// issues
IssueUpdateResult(FetchObject<String>),
// modals
CloseModal,
}
fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
@ -52,6 +54,9 @@ fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
Msg::ChangePage(page) => {
model.page = page;
}
Msg::CloseModal => {
model.modal = None;
}
_ => (),
}
crate::shared::update(&msg, model, orders);

View File

@ -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<IssueId>,
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<ModalType>,
}
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,
}
}
}

View File

@ -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<Msg>) {
@ -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<Msg> {
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<Msg> {
project_board_lists(model),
];
inner_layout(model, "projectPage", project_section)
inner_layout(model, "projectPage", project_section, modal)
}
fn breadcrumbs(model: &Model) -> Node<Msg> {
@ -144,7 +170,7 @@ fn breadcrumbs(model: &Model) -> Node<Msg> {
fn header() -> Node<Msg> {
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<Msg> {
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<Msg> {
.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<Ms
class_list.push("hidden");
}
let href = format!("/issues/{id}", id = issue_id);
a![
attrs![At::Class => "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<Ms
]
]
}
impl ToNode for IssueType {
fn into_node(self) -> Node<Msg> {
div![self.to_string()]
}
}
fn issue_details(_model: &Model, _issue: &Issue) -> Node<Msg> {
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"]],
],
]
}

View File

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

View File

@ -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<Msg>;
}
@ -24,8 +34,18 @@ pub fn divider() -> Node<Msg> {
div![attrs![At::Class => "divider"], ""]
}
pub fn inner_layout(model: &Model, page_name: &str, children: Vec<Node<Msg>>) -> Node<Msg> {
pub fn inner_layout(
model: &Model,
page_name: &str,
children: Vec<Node<Msg>>,
modal: Option<Node<Msg>>,
) -> Node<Msg> {
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),

View File

@ -48,6 +48,7 @@ pub fn render(values: Modal) -> Node<Msg> {
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<Msg> {
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<Msg> {
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
]

View File

@ -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<Child>
where
Child: ToNode,
{
pub on_change: EventHandler<Msg>,
pub variant: Variant,
pub width: usize,
pub name: Option<String>,
pub placeholder: Option<String>,
pub valid: bool,
pub is_multi: bool,
pub allow_clear: bool,
pub options: Vec<Child>,
}
impl<Child> ToNode for StyledSelect<Child>
where
Child: ToNode,
{
fn into_node(self) -> Node<Msg> {
render(self)
}
}
pub fn render<Child>(values: StyledSelect<Child>) -> Node<Msg>
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<Node<Msg>> = 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<Msg>) -> Node<Msg> {
seed::div![attrs![At::Class => "option"], content,]
}

View File

@ -49,7 +49,7 @@ export default createGlobalStyle`
}
h1, h2, h3, h4, h5, h6, strong {
${font.bold}
font-family: "CircularStdBold"; font-weight: normal
}
button {

View File

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

View File

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

View File

@ -1,10 +1,10 @@
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,

View File

@ -1,7 +1,7 @@
import styled from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles';
import { InputDebounced, Spinner, Icon } from 'shared/components';
import { color, font } from 'shared/utils/styles';
import { Icon, InputDebounced, Spinner } from 'shared/components';
export const IssueSearch = styled.div`
padding: 25px 35px 60px;
@ -76,7 +76,7 @@ export const SectionTitle = styled.div`
padding-bottom: 12px;
text-transform: uppercase;
color: ${color.textMedium};
${font.bold}
font-family: "CircularStdBold"; font-weight: normal
font-size: 11.5px
`;

View File

@ -1,7 +1,7 @@
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
import { font, sizes, color, mixin, zIndexValues } from 'shared/utils/styles';
import { color, mixin, sizes, zIndexValues } from 'shared/utils/styles';
import { Logo } from 'shared/components';
export const NavLeft = styled.aside`
@ -71,7 +71,7 @@ export const ItemText = styled.div`
text-transform: uppercase;
transition: all 0.1s;
transition-property: right, visibility, opacity;
${font.bold}
font-family: "CircularStdBold"; font-weight: normal
font-size: 12px
${NavLeft}:hover & {
right: 0;

View File

@ -90,7 +90,7 @@ export const NotImplemented = styled.div`
background: ${color.backgroundMedium};
opacity: 0;
font-size: 11.5px;
${font.bold}
font-family: "CircularStdBold"; font-weight: normal
${LinkItem}:hover & {
opacity: 1;
}

View File

@ -31,7 +31,7 @@ export const DateSection = styled.div`
export const SelectedMonthYear = styled.div`
display: inline-block;
padding-left: 7px;
${font.bold}
font-family: "CircularStdBold"; font-weight: normal
font-size: 16px
`;

View File

@ -1,10 +1,10 @@
import React, { useState, useRef, useLayoutEffect } from 'react';
import React, { useLayoutEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { uniq } from 'lodash';
import { KeyCodes } from 'shared/constants/keyCodes';
import { KeyCodes } from '../../../shared/constants/keyCodes';
import { ClearIcon, Dropdown, DropdownInput, Options, Option, OptionsNoResults } from './Styles';
import { ClearIcon, Dropdown, DropdownInput, Option, Options, OptionsNoResults } from './Styles';
const propTypes = {
dropdownWidth: PropTypes.number,
@ -179,8 +179,8 @@ const SelectDropdown = ({
<DropdownInput
type="text"
placeholder="Search"
ref={$inputRef}
autoFocus
ref={$inputRef}
onKeyDown={handleInputKeyDown}
onChange={event => setSearchValue(event.target.value)}
/>

View File

@ -126,7 +126,7 @@ export const Options = styled.div`
max-height: 200px;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;;
-webkit-overflow-scrolling: touch;
${mixin.customScrollbar()};
`;

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useRef, useState } from 'react';
import PropTypes from 'prop-types';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
@ -6,49 +6,7 @@ import { KeyCodes } from 'shared/constants/keyCodes';
import Icon from 'shared/components/Icon';
import Dropdown from './Dropdown';
import {
StyledSelect,
ValueContainer,
ChevronIcon,
Placeholder,
ValueMulti,
ValueMultiItem,
AddMore,
} from './Styles';
const propTypes = {
className: PropTypes.string,
variant: PropTypes.oneOf(['normal', 'empty']),
dropdownWidth: PropTypes.number,
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
defaultValue: PropTypes.any,
placeholder: PropTypes.string,
invalid: PropTypes.bool,
options: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onCreate: PropTypes.func,
isMulti: PropTypes.bool,
withClearValue: PropTypes.bool,
renderValue: PropTypes.func,
renderOption: PropTypes.func,
};
const defaultProps = {
className: undefined,
variant: 'normal',
dropdownWidth: undefined,
name: undefined,
value: undefined,
defaultValue: undefined,
placeholder: 'Select',
invalid: false,
onCreate: undefined,
isMulti: false,
withClearValue: true,
renderValue: undefined,
renderOption: undefined,
};
import { AddMore, ChevronIcon, Placeholder, StyledSelect, ValueContainer, ValueMulti, ValueMultiItem, } from './Styles';
const Select = ({
className,
@ -203,7 +161,37 @@ const Select = ({
);
};
Select.propTypes = propTypes;
Select.defaultProps = defaultProps;
Select.propTypes = {
className: PropTypes.string,
variant: PropTypes.oneOf(['normal', 'empty']),
dropdownWidth: PropTypes.number,
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
defaultValue: PropTypes.any,
placeholder: PropTypes.string,
invalid: PropTypes.bool,
options: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired,
onCreate: PropTypes.func,
isMulti: PropTypes.bool,
withClearValue: PropTypes.bool,
renderValue: PropTypes.func,
renderOption: PropTypes.func,
};
Select.defaultProps = {
className: undefined,
variant: 'normal',
dropdownWidth: undefined,
name: undefined,
value: undefined,
defaultValue: undefined,
placeholder: 'Select',
invalid: false,
onCreate: undefined,
isMulti: false,
withClearValue: true,
renderValue: undefined,
renderOption: undefined,
};
export default Select;

View File

@ -1,7 +1,7 @@
import { css } from 'styled-components';
import Color from 'color';
import { IssueType, IssueStatus, IssuePriority } from 'shared/constants/issues';
import { IssuePriority, IssueStatus, IssueType } from 'shared/constants/issues';
export const color = {
primary: '#0052cc', // Blue
@ -174,7 +174,7 @@ export const mixin = {
user-select: none;
color: ${colorValue};
background: ${background};
${font.bold}
font-family: "CircularStdBold"; font-weight: normal
font-size: 12px
i {
margin-left: 4px;