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

View File

@ -52,6 +52,10 @@
color: var(--textMedium);
}
.styledSelect > .valueContainer > .placeholder {
color: var(--textLight);
}
.styledSelectTip {
padding-top: 6px;
color: var(--textMedium);
@ -134,3 +138,21 @@
cursor: pointer;
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,
SummaryAddIssueModal,
DescriptionAddIssueModal,
ReporterAddIssueModal,
}
impl std::fmt::Display for FieldId {
@ -47,6 +48,7 @@ impl std::fmt::Display for FieldId {
FieldId::IssueTypeAddIssueModal => f.write_str("issueTypeAddIssueModal"),
FieldId::SummaryAddIssueModal => f.write_str("summaryAddIssueModal"),
FieldId::DescriptionAddIssueModal => f.write_str("descriptionAddIssueModal"),
FieldId::ReporterAddIssueModal => f.write_str("reporterAddIssueModal"),
}
}
}

View File

@ -1,7 +1,8 @@
use seed::{prelude::*, *};
use jirs_data::IssueType;
use jirs_data::{IssueType, User};
use crate::model::Page;
use crate::model::{AddIssueModal, ModalType, Model};
use crate::shared::styled_button::StyledButton;
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);
}
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)
.name("type")
.normal()
@ -89,7 +90,6 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.into_node();
let description = StyledTextarea::build()
.on_change(input_ev(Ev::Change, |_| Msg::NoOp))
.height(110)
.build(FieldId::DescriptionAddIssueModal)
.into_node();
@ -100,15 +100,48 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build()
.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()
.primary()
.text("Create Issue")
.add_class("action")
.add_class("submit")
.add_class("ActionButton")
.build()
.into_node();
let cancel = StyledButton::build()
.empty()
.add_class("action")
.add_class("cancel")
.add_class("actionButton")
.text("Cancel")
.build()
.into_node();
@ -120,12 +153,14 @@ pub fn view(_model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.add_field(crate::shared::divider())
.add_field(short_summary_field)
.add_field(description_field)
.add_field(reporter_field)
.add_field(actions)
.build()
.into_node();
StyledModal::build()
.width(800)
.add_class("add-issue")
.variant(ModalVariant::Center)
.children(vec![form])
.build()
@ -140,7 +175,7 @@ impl crate::shared::styled_select::SelectOption for IssueTypeOption {
let name = self.0.to_label().to_owned();
let icon = StyledIcon::build(self.0.into())
.add_class("issueTypeIcon".to_string())
.add_class("issueTypeIcon")
.build()
.into_node();
@ -174,3 +209,34 @@ impl crate::shared::styled_select::SelectOption for IssueTypeOption {
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 project_id: i32,
pub user_ids: Vec<i32>,
pub reporter_id: Option<i32>,
// modal fields
pub type_select_filter: String,
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)]

View File

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

View File

@ -1,12 +1,12 @@
use seed::{prelude::*, *};
use crate::shared::ToNode;
use crate::{FieldId, Msg};
use seed::{prelude::*, *};
#[derive(Debug)]
pub struct StyledTextarea {
id: FieldId,
height: usize,
on_change: Option<EventHandler<Msg>>,
}
impl ToNode for StyledTextarea {
@ -33,16 +33,10 @@ impl StyledTextareaBuilder {
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 {
StyledTextarea {
id,
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
// * 2 is border top + bottom
pub fn render(values: StyledTextarea) -> Node<Msg> {
let StyledTextarea {
id,
height,
on_change,
} = values;
let StyledTextarea { id, height } = values;
let mut style_list = vec![];
style_list.push(format!("min-height: {}px", height));
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;
let target = match event.target() {
@ -98,7 +85,7 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
Msg::NoOp
});
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);
div![

View File

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

View File

@ -1,4 +1,4 @@
import React from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import {
@ -7,18 +7,19 @@ import {
IssueStatus,
IssueType,
IssueTypeCopy,
} from '../../shared/constants/issues';
import toast from '../../shared/utils/toast';
import api from '../../shared/utils/api';
} from '../../shared/constants/issues';
import toast from '../../shared/utils/toast';
import api from '../../shared/utils/api';
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';
class ProjectIssueCreate extends React.Component {
state = {
isCreating: false, form: {
type: IssueType.TASK,
title: '',
type: IssueType.TASK,
title: '',
description: '',
reporterId: null,
userIds: [],
@ -68,7 +69,7 @@ class ProjectIssueCreate extends React.Component {
<FormHeading>
Create issue
</FormHeading>
<Form.Field.Select
<Field.Select
name="type"
label="Issue Type"
tip="Start typing to get a list of possible matches."
@ -77,19 +78,19 @@ class ProjectIssueCreate extends React.Component {
renderValue={ renderType }
/>
<Divider/>
<Form.Field.Input
<Field.Input
name="title"
label="Short Summary"
tip="Concisely summarize the issue in one or two sentences."
onChange={ this.onInputChange }
/>
<Form.Field.TextEditor
<Field.TextEditor
name="description"
label="Description"
tip="Describe the issue in as much detail as you'd like."
onChange={ this.onInputChange }
/>
<Form.Field.Select
<Field.Select
name="reporterId"
label="Reporter"
options={ userOptions(project) }
@ -97,7 +98,7 @@ class ProjectIssueCreate extends React.Component {
renderValue={ renderUser(project) }
onChange={ this.onInputChange }
/>
<Form.Field.Select
<Field.Select
isMulti
name="userIds"
label="Assignees"
@ -107,7 +108,7 @@ class ProjectIssueCreate extends React.Component {
renderOption={ renderUser(project) }
renderValue={ renderUser(project) }
/>
<Form.Field.Select
<Field.Select
name="priority"
label="Priority"
tip="Priority in relation to other issues."

View File

@ -1,22 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
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 { generateErrors, is } from 'shared/utils/validation';
import Field from './Field';
import FieldComponents from './Field';
const {
Input,
Select,
Textarea,
TextEditor,
DatePicker,
} = FieldComponents;
const propTypes = {
validate: PropTypes.func,
validations: PropTypes.object,
validateOnBlur: PropTypes.bool,
validate: PropTypes.func,
validations: PropTypes.object,
validateOnBlur: PropTypes.bool,
};
const defaultProps = {
validate: undefined,
validations: undefined,
validate: undefined,
validations: undefined,
validateOnBlur: false,
};
@ -25,20 +33,22 @@ const Form = ({ validate, validations, ...otherProps }) => (
{...otherProps}
validate={values => {
if (validate) {
return validate(values);
return validate(values);
}
if (validations) {
return generateErrors(values, validations);
}
return {};
}}
if (validations) {
return generateErrors(values, validations);
}
return {};
} }
/>
);
Form.Element = props => <FormikForm noValidate {...props} />;
export const Element = props => <FormikForm noValidate { ...props } />;
Form.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) => {
return (
Form.Element = Element;
export const Field = {
Input: ({ name, validate, ...props }) => (
<FormikField name={ name } validate={ validate }>
{ ({ field, form: { touched, errors, setFieldValue } }) => {
const onChange = value => {
@ -49,7 +59,7 @@ Form.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) =
};
return (
<FieldComponent
<Input
{ ...field }
{ ...props }
name={ name }
@ -59,18 +69,108 @@ Form.Field = mapValues(Field, FieldComponent => ({ name, validate, ...props }) =
)
} }
</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) =>
getFieldValues((key, defaultValue = '') => {
const value = get(data, key);
return value === undefined || value === null ? defaultValue : value;
});
getFieldValues((key, defaultValue = '') => {
const value = get(data, key);
return value === undefined || value === null ? defaultValue : value;
});
Form.handleAPIError = (error, form) => {
if (error.data.fields) {
form.setErrors(error.data.fields);
if (error.data.fields) {
form.setErrors(error.data.fields);
} else {
toast.error(error);
}