Add initial reporter

This commit is contained in:
Adrian Wozniak 2020-04-08 08:58:02 +02:00
parent 673453e08e
commit 925773ddd7
10 changed files with 250 additions and 76 deletions

View File

@ -28,20 +28,6 @@
margin-left: 4px; 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 { .issueDetails > .topActions .styledSelect > .valueContainer > .value {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;

View File

@ -52,6 +52,10 @@
color: var(--textMedium); color: var(--textMedium);
} }
.styledSelect > .valueContainer > .placeholder {
color: var(--textLight);
}
.styledSelectTip { .styledSelectTip {
padding-top: 6px; padding-top: 6px;
color: var(--textMedium); color: var(--textMedium);
@ -134,3 +138,21 @@
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
/* issue type */
.styledSelect > .dropDown > .options > .option > .type {
display: flex;
align-items: center;
}
.styledSelect > .dropDown > .options > .option > .type > .styledIcon {
font-size: 18px;
}
.styledSelect > .dropDown > .options > .option > .type > .typeLabel {
padding: 0 5px 0 7px;
font-size: 15px
}
/* issue priority */

View File

@ -36,6 +36,7 @@ pub enum FieldId {
IssueTypeAddIssueModal, IssueTypeAddIssueModal,
SummaryAddIssueModal, SummaryAddIssueModal,
DescriptionAddIssueModal, DescriptionAddIssueModal,
ReporterAddIssueModal,
} }
impl std::fmt::Display for FieldId { impl std::fmt::Display for FieldId {
@ -47,6 +48,7 @@ impl std::fmt::Display for FieldId {
FieldId::IssueTypeAddIssueModal => f.write_str("issueTypeAddIssueModal"), FieldId::IssueTypeAddIssueModal => f.write_str("issueTypeAddIssueModal"),
FieldId::SummaryAddIssueModal => f.write_str("summaryAddIssueModal"), FieldId::SummaryAddIssueModal => f.write_str("summaryAddIssueModal"),
FieldId::DescriptionAddIssueModal => f.write_str("descriptionAddIssueModal"), FieldId::DescriptionAddIssueModal => f.write_str("descriptionAddIssueModal"),
FieldId::ReporterAddIssueModal => f.write_str("reporterAddIssueModal"),
} }
} }
} }

View File

@ -1,7 +1,8 @@
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use jirs_data::IssueType; use jirs_data::{IssueType, User};
use crate::model::Page;
use crate::model::{AddIssueModal, ModalType, Model}; use crate::model::{AddIssueModal, ModalType, Model};
use crate::shared::styled_button::StyledButton; use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
@ -55,7 +56,7 @@ pub fn update(msg: &Msg, model: &mut crate::model::Model, _orders: &mut impl Ord
log!(modal); log!(modal);
} }
pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> { pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
let select_type = StyledSelect::build(FieldId::IssueTypeAddIssueModal) let select_type = StyledSelect::build(FieldId::IssueTypeAddIssueModal)
.name("type") .name("type")
.normal() .normal()
@ -89,7 +90,6 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.into_node(); .into_node();
let description = StyledTextarea::build() let description = StyledTextarea::build()
.on_change(input_ev(Ev::Change, |_| Msg::NoOp))
.height(110) .height(110)
.build(FieldId::DescriptionAddIssueModal) .build(FieldId::DescriptionAddIssueModal)
.into_node(); .into_node();
@ -100,15 +100,48 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let reporter_id = modal
.reporter_id
.or_else(|| model.user.as_ref().map(|u| u.id))
.unwrap_or_default();
let reporter = StyledSelect::build(FieldId::ReporterAddIssueModal)
.options(model.users.iter().map(|u| UserOption(u)).collect())
.selected(
model
.users
.iter()
.filter_map(|user| {
if user.id == reporter_id {
Some(UserOption(user))
} else {
None
}
})
.collect(),
)
.valid(true)
.build()
.into_node();
let reporter_field = StyledField::build()
.input(reporter)
.label("Reporter")
.tip("")
.build()
.into_node();
let submit = StyledButton::build() let submit = StyledButton::build()
.primary() .primary()
.text("Create Issue") .text("Create Issue")
.add_class("action") .add_class("action")
.add_class("submit")
.add_class("ActionButton")
.build() .build()
.into_node(); .into_node();
let cancel = StyledButton::build() let cancel = StyledButton::build()
.empty() .empty()
.add_class("action") .add_class("action")
.add_class("cancel")
.add_class("actionButton")
.text("Cancel") .text("Cancel")
.build() .build()
.into_node(); .into_node();
@ -120,12 +153,14 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.add_field(crate::shared::divider()) .add_field(crate::shared::divider())
.add_field(short_summary_field) .add_field(short_summary_field)
.add_field(description_field) .add_field(description_field)
.add_field(reporter_field)
.add_field(actions) .add_field(actions)
.build() .build()
.into_node(); .into_node();
StyledModal::build() StyledModal::build()
.width(800) .width(800)
.add_class("add-issue")
.variant(ModalVariant::Center) .variant(ModalVariant::Center)
.children(vec![form]) .children(vec![form])
.build() .build()
@ -140,7 +175,7 @@ impl crate::shared::styled_select::SelectOption for IssueTypeOption {
let name = self.0.to_label().to_owned(); let name = self.0.to_label().to_owned();
let icon = StyledIcon::build(self.0.into()) let icon = StyledIcon::build(self.0.into())
.add_class("issueTypeIcon".to_string()) .add_class("issueTypeIcon")
.build() .build()
.into_node(); .into_node();
@ -174,3 +209,34 @@ impl crate::shared::styled_select::SelectOption for IssueTypeOption {
self.0.clone().into() self.0.clone().into()
} }
} }
#[derive(Debug, PartialEq)]
pub struct UserOption<'opt>(pub &'opt User);
impl<'opt> crate::shared::styled_select::SelectOption for UserOption<'opt> {
fn into_option(self) -> Node<Msg> {
let user = self.0;
div![
attrs![At::Class => "type"],
div![attrs![At::Class => "typeLabel"], user.name]
]
}
fn into_value(self) -> Node<Msg> {
let user = self.0;
div![
attrs![At::Class => "selectItem"],
div![attrs![At::Class => "selectItemLabel"], user.name]
]
}
fn match_text_filter(&self, _text_filter: &str) -> bool {
false
}
fn to_value(&self) -> u32 {
self.0.id as u32
}
}

View File

@ -39,10 +39,17 @@ pub struct AddIssueModal {
pub time_remaining: Option<i32>, pub time_remaining: Option<i32>,
pub project_id: i32, pub project_id: i32,
pub user_ids: Vec<i32>, pub user_ids: Vec<i32>,
pub reporter_id: Option<i32>,
// modal fields // modal fields
pub type_select_filter: String, pub type_select_filter: String,
pub type_select_opened: bool, pub type_select_opened: bool,
pub reporter_select_filter: String,
pub reporter_select_opened: bool,
pub assignees_select_filter: String,
pub assignees_select_opened: bool,
} }
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)] #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialOrd, PartialEq)]

View File

@ -78,8 +78,11 @@ impl StyledModalBuilder {
self self
} }
pub fn add_class(mut self, name: String) -> Self { pub fn add_class<S>(mut self, name: S) -> Self
self.class_list.push(name); where
S: Into<String>,
{
self.class_list.push(name.into());
self self
} }

View File

@ -1,12 +1,12 @@
use seed::{prelude::*, *};
use crate::shared::ToNode; use crate::shared::ToNode;
use crate::{FieldId, Msg}; use crate::{FieldId, Msg};
use seed::{prelude::*, *};
#[derive(Debug)] #[derive(Debug)]
pub struct StyledTextarea { pub struct StyledTextarea {
id: FieldId, id: FieldId,
height: usize, height: usize,
on_change: Option<EventHandler<Msg>>,
} }
impl ToNode for StyledTextarea { impl ToNode for StyledTextarea {
@ -33,16 +33,10 @@ impl StyledTextareaBuilder {
self self
} }
pub fn on_change(mut self, on_change: EventHandler<Msg>) -> Self {
self.on_change = Some(on_change);
self
}
pub fn build(self, id: FieldId) -> StyledTextarea { pub fn build(self, id: FieldId) -> StyledTextarea {
StyledTextarea { StyledTextarea {
id, id,
height: self.height.unwrap_or(110), height: self.height.unwrap_or(110),
on_change: self.on_change,
} }
} }
} }
@ -61,20 +55,13 @@ const ADDITIONAL_HEIGHT: f64 = PADDING_TOP_BOTTOM + BORDER_TOP_BOTTOM;
// * 17 is padding top + bottom // * 17 is padding top + bottom
// * 2 is border top + bottom // * 2 is border top + bottom
pub fn render(values: StyledTextarea) -> Node<Msg> { pub fn render(values: StyledTextarea) -> Node<Msg> {
let StyledTextarea { let StyledTextarea { id, height } = values;
id,
height,
on_change,
} = values;
let mut style_list = vec![]; let mut style_list = vec![];
style_list.push(format!("min-height: {}px", height)); style_list.push(format!("min-height: {}px", height));
let mut handlers = vec![]; let mut handlers = vec![];
if let Some(handler) = on_change {
handlers.push(handler);
}
let resize_handler = ev(Ev::KeyPress, move |event| { let resize_handler = ev(Ev::KeyUp, move |event| {
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
let target = match event.target() { let target = match event.target() {
@ -98,7 +85,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
Msg::NoOp Msg::NoOp
}); });
handlers.push(resize_handler); handlers.push(resize_handler);
let text_input_handler = input_ev(Ev::KeyPress, move |value| Msg::InputChanged(id, value)); let text_input_handler = input_ev(Ev::KeyUp, move |value| Msg::InputChanged(id, value));
handlers.push(text_input_handler); handlers.push(text_input_handler);
div![ div![

View File

@ -306,7 +306,7 @@ pub struct Comment {
pub user: Option<User>, pub user: Option<User>,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct User { pub struct User {
pub id: i32, pub id: i32,

View File

@ -11,6 +11,7 @@ import {
import toast from '../../shared/utils/toast'; import toast from '../../shared/utils/toast';
import api from '../../shared/utils/api'; import api from '../../shared/utils/api';
import { Avatar, Form, Icon, IssuePriorityIcon, IssueTypeIcon } from '../../shared/components'; import { Avatar, Form, Icon, IssuePriorityIcon, IssueTypeIcon } from '../../shared/components';
import { Field } from "../../shared/components/Form";
import { ActionButton, Actions, Divider, FormElement, FormHeading, SelectItem, SelectItemLabel, } from './Styles'; import { ActionButton, Actions, Divider, FormElement, FormHeading, SelectItem, SelectItemLabel, } from './Styles';
@ -68,7 +69,7 @@ class ProjectIssueCreate extends React.Component {
<FormHeading> <FormHeading>
Create issue Create issue
</FormHeading> </FormHeading>
<Form.Field.Select <Field.Select
name="type" name="type"
label="Issue Type" label="Issue Type"
tip="Start typing to get a list of possible matches." tip="Start typing to get a list of possible matches."
@ -77,19 +78,19 @@ class ProjectIssueCreate extends React.Component {
renderValue={ renderType } renderValue={ renderType }
/> />
<Divider/> <Divider/>
<Form.Field.Input <Field.Input
name="title" name="title"
label="Short Summary" label="Short Summary"
tip="Concisely summarize the issue in one or two sentences." tip="Concisely summarize the issue in one or two sentences."
onChange={ this.onInputChange } onChange={ this.onInputChange }
/> />
<Form.Field.TextEditor <Field.TextEditor
name="description" name="description"
label="Description" label="Description"
tip="Describe the issue in as much detail as you'd like." tip="Describe the issue in as much detail as you'd like."
onChange={ this.onInputChange } onChange={ this.onInputChange }
/> />
<Form.Field.Select <Field.Select
name="reporterId" name="reporterId"
label="Reporter" label="Reporter"
options={ userOptions(project) } options={ userOptions(project) }
@ -97,7 +98,7 @@ class ProjectIssueCreate extends React.Component {
renderValue={ renderUser(project) } renderValue={ renderUser(project) }
onChange={ this.onInputChange } onChange={ this.onInputChange }
/> />
<Form.Field.Select <Field.Select
isMulti isMulti
name="userIds" name="userIds"
label="Assignees" label="Assignees"
@ -107,7 +108,7 @@ class ProjectIssueCreate extends React.Component {
renderOption={ renderUser(project) } renderOption={ renderUser(project) }
renderValue={ renderUser(project) } renderValue={ renderUser(project) }
/> />
<Form.Field.Select <Field.Select
name="priority" name="priority"
label="Priority" label="Priority"
tip="Priority in relation to other issues." tip="Priority in relation to other issues."

View File

@ -1,12 +1,20 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Field as FormikField, Form as FormikForm, Formik } from 'formik'; import { Field as FormikField, Form as FormikForm, Formik } from 'formik';
import { get, mapValues } from 'lodash'; import { get } from 'lodash';
import toast from 'shared/utils/toast'; import toast from 'shared/utils/toast';
import { generateErrors, is } from 'shared/utils/validation'; import { generateErrors, is } from 'shared/utils/validation';
import Field from './Field'; import FieldComponents from './Field';
const {
Input,
Select,
Textarea,
TextEditor,
DatePicker,
} = FieldComponents;
const propTypes = { const propTypes = {
validate: PropTypes.func, validate: PropTypes.func,
@ -31,14 +39,16 @@ const Form = ({ validate, validations, ...otherProps }) => (
return generateErrors(values, validations); return generateErrors(values, validations);
} }
return {}; return {};
}} } }
/> />
); );
Form.Element = props => <FormikForm noValidate {...props} />; export const Element = props => <FormikForm noValidate { ...props } />;
Form.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) => { Form.Element = Element;
return (
export const Field = {
Input: ({ name, validate, ...props }) => (
<FormikField name={ name } validate={ validate }> <FormikField name={ name } validate={ validate }>
{ ({ field, form: { touched, errors, setFieldValue } }) => { { ({ field, form: { touched, errors, setFieldValue } }) => {
const onChange = value => { const onChange = value => {
@ -49,7 +59,7 @@ Form.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) =
}; };
return ( return (
<FieldComponent <Input
{ ...field } { ...field }
{ ...props } { ...props }
name={ name } name={ name }
@ -59,8 +69,98 @@ Form.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) =
) )
} } } }
</FormikField> </FormikField>
),
Select: ({ name, validate, ...props }) => (
<FormikField name={ name } validate={ validate }>
{ ({ field, form: { touched, errors, setFieldValue } }) => {
const onChange = value => {
if (props.onChange) {
props.onChange(name, value)
}
setFieldValue(name, value)
};
return (
<Select
{ ...field }
{ ...props }
name={ name }
error={ get(touched, name) && get(errors, name) }
onChange={ onChange }
/>
) )
}); } }
</FormikField>
),
Textarea: ({ name, validate, ...props }) => (
<FormikField name={ name } validate={ validate }>
{ ({ field, form: { touched, errors, setFieldValue } }) => {
const onChange = value => {
if (props.onChange) {
props.onChange(name, value)
}
setFieldValue(name, value)
};
return (
<Textarea
{ ...field }
{ ...props }
name={ name }
error={ get(touched, name) && get(errors, name) }
onChange={ onChange }
/>
)
} }
</FormikField>
),
TextEditor: ({ name, validate, ...props }) => (
<FormikField name={ name } validate={ validate }>
{ ({ field, form: { touched, errors, setFieldValue } }) => {
const onChange = value => {
if (props.onChange) {
props.onChange(name, value)
}
setFieldValue(name, value)
};
return (
<TextEditor
{ ...field }
{ ...props }
name={ name }
error={ get(touched, name) && get(errors, name) }
onChange={ onChange }
/>
)
} }
</FormikField>
),
DatePicker: ({ name, validate, ...props }) => (
<FormikField name={ name } validate={ validate }>
{ ({ field, form: { touched, errors, setFieldValue } }) => {
const onChange = value => {
if (props.onChange) {
props.onChange(name, value)
}
setFieldValue(name, value)
};
return (
<DatePicker
{ ...field }
{ ...props }
name={ name }
error={ get(touched, name) && get(errors, name) }
onChange={ onChange }
/>
)
} }
</FormikField>
),
};
Form.Field = Field;
Form.initialValues = (data, getFieldValues) => Form.initialValues = (data, getFieldValues) =>
getFieldValues((key, defaultValue = '') => { getFieldValues((key, defaultValue = '') => {