Add initial reporter
This commit is contained in:
parent
673453e08e
commit
925773ddd7
@ -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;
|
||||
|
@ -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 */
|
||||
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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![
|
||||
|
@ -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,
|
||||
|
@ -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."
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user