Implemented issue create modal, further polish
This commit is contained in:
@@ -1,38 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { Button } from 'shared/components';
|
||||
|
||||
export const TypeButton = styled(Button)`
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${color.textMedium};
|
||||
${font.size(13)}
|
||||
`;
|
||||
|
||||
export const TypeDropdown = styled.div`
|
||||
padding-bottom: 6px;
|
||||
`;
|
||||
|
||||
export const TypeTitle = styled.div`
|
||||
padding: 10px 0 7px 12px;
|
||||
text-transform: uppercase;
|
||||
color: ${color.textMedium};
|
||||
${font.size(12)}
|
||||
`;
|
||||
|
||||
export const Type = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 7px 12px;
|
||||
${mixin.clickable}
|
||||
&:hover {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const TypeLabel = styled.div`
|
||||
padding: 0 5px 0 7px;
|
||||
text-transform: capitalize;
|
||||
${font.size(15)}
|
||||
`;
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { IssueType } from 'shared/constants/issues';
|
||||
import { IssueTypeIcon, Tooltip } from 'shared/components';
|
||||
import { TypeButton, TypeDropdown, TypeTitle, Type, TypeLabel } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
|
||||
<Tooltip
|
||||
width={150}
|
||||
offset={{ top: -15 }}
|
||||
renderLink={linkProps => (
|
||||
<TypeButton {...linkProps} color="empty" icon={<IssueTypeIcon type={issue.type} />}>
|
||||
{`${issue.type}-${issue.id}`}
|
||||
</TypeButton>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<TypeDropdown>
|
||||
<TypeTitle>Change issue type</TypeTitle>
|
||||
{Object.values(IssueType).map(type => (
|
||||
<Type key={type} onClick={() => updateIssue({ type })}>
|
||||
<IssueTypeIcon type={type} top={1} />
|
||||
<TypeLabel>{type}</TypeLabel>
|
||||
</Type>
|
||||
))}
|
||||
</TypeDropdown>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsType.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsType;
|
||||
@@ -22,7 +22,7 @@ const ProjectBoardListsIssue = ({ projectUsers, issue, index }) => {
|
||||
<Draggable draggableId={issue.id.toString()} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<IssueLink
|
||||
to={`${match.url}/${issue.id}`}
|
||||
to={`${match.url}/issue/${issue.id}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route, useRouteMatch, useHistory } from 'react-router-dom';
|
||||
|
||||
import { Modal } from 'shared/components';
|
||||
import Header from './Header';
|
||||
import Filters from './Filters';
|
||||
import Lists from './Lists';
|
||||
import IssueDetails from './IssueDetails';
|
||||
|
||||
const propTypes = {
|
||||
project: PropTypes.object.isRequired,
|
||||
fetchProject: PropTypes.func.isRequired,
|
||||
updateLocalIssuesArray: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@@ -21,11 +17,8 @@ const defaultFilters = {
|
||||
recent: false,
|
||||
};
|
||||
|
||||
const ProjectBoard = ({ project, fetchProject, updateLocalIssuesArray }) => {
|
||||
const match = useRouteMatch();
|
||||
const history = useHistory();
|
||||
const ProjectBoard = ({ project, updateLocalIssuesArray }) => {
|
||||
const [filters, setFilters] = useState(defaultFilters);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header projectName={project.name} />
|
||||
@@ -36,26 +29,6 @@ const ProjectBoard = ({ project, fetchProject, updateLocalIssuesArray }) => {
|
||||
setFilters={setFilters}
|
||||
/>
|
||||
<Lists project={project} filters={filters} updateLocalIssuesArray={updateLocalIssuesArray} />
|
||||
<Route
|
||||
path={`${match.path}/:issueId`}
|
||||
render={({ match: { params } }) => (
|
||||
<Modal
|
||||
isOpen
|
||||
width={1040}
|
||||
withCloseIcon={false}
|
||||
onClose={() => history.push(match.url)}
|
||||
renderContent={modal => (
|
||||
<IssueDetails
|
||||
issueId={params.issueId}
|
||||
projectUsers={project.users}
|
||||
fetchProject={fetchProject}
|
||||
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||
modalClose={modal.close}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
39
client/src/Project/IssueCreateForm/Styles.js
Normal file
39
client/src/Project/IssueCreateForm/Styles.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Button, Form } from 'shared/components';
|
||||
|
||||
export const FormElement = styled(Form.Element)`
|
||||
padding: 20px 40px;
|
||||
`;
|
||||
|
||||
export const FormHeading = styled.div`
|
||||
padding-bottom: 15px;
|
||||
${font.size(20)}
|
||||
`;
|
||||
|
||||
export const SelectItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
${props => props.withBottomMargin && `margin-bottom: 5px;`}
|
||||
`;
|
||||
|
||||
export const SelectItemLabel = styled.div`
|
||||
padding: 0 3px 0 6px;
|
||||
`;
|
||||
|
||||
export const Divider = styled.div`
|
||||
margin-top: 22px;
|
||||
border-top: 1px solid ${color.borderLightest};
|
||||
`;
|
||||
|
||||
export const Actions = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 30px;
|
||||
`;
|
||||
|
||||
export const ActionButton = styled(Button)`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
169
client/src/Project/IssueCreateForm/index.jsx
Normal file
169
client/src/Project/IssueCreateForm/index.jsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
IssueType,
|
||||
IssueStatus,
|
||||
IssuePriority,
|
||||
IssueTypeCopy,
|
||||
IssuePriorityCopy,
|
||||
} from 'shared/constants/issues';
|
||||
import toast from 'shared/utils/toast';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { Form, IssueTypeIcon, Icon, Avatar, IssuePriorityIcon } from 'shared/components';
|
||||
import {
|
||||
FormHeading,
|
||||
FormElement,
|
||||
SelectItem,
|
||||
SelectItemLabel,
|
||||
Divider,
|
||||
Actions,
|
||||
ActionButton,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
project: PropTypes.object.isRequired,
|
||||
fetchProject: PropTypes.func.isRequired,
|
||||
modalClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectIssueCreateForm = ({ project, fetchProject, modalClose }) => {
|
||||
const [{ isCreating }, createIssue] = useApi.post('/issues');
|
||||
|
||||
const typeOptions = Object.values(IssueType).map(type => ({
|
||||
value: type,
|
||||
label: IssueTypeCopy[type],
|
||||
}));
|
||||
|
||||
const priorityOptions = Object.values(IssuePriority).map(priority => ({
|
||||
value: priority,
|
||||
label: IssuePriorityCopy[priority],
|
||||
}));
|
||||
|
||||
const userOptions = project.users.map(user => ({ value: user.id, label: user.name }));
|
||||
|
||||
const renderType = ({ value: type }) => (
|
||||
<SelectItem>
|
||||
<IssueTypeIcon type={type} top={1} />
|
||||
<SelectItemLabel>{IssueTypeCopy[type]}</SelectItemLabel>
|
||||
</SelectItem>
|
||||
);
|
||||
|
||||
const renderPriority = ({ value: priority }) => (
|
||||
<SelectItem>
|
||||
<IssuePriorityIcon priority={priority} top={1} />
|
||||
<SelectItemLabel>{IssuePriorityCopy[priority]}</SelectItemLabel>
|
||||
</SelectItem>
|
||||
);
|
||||
|
||||
const renderUser = ({ value: userId, removeOptionValue }) => {
|
||||
const user = project.users.find(({ id }) => id === userId);
|
||||
return (
|
||||
<SelectItem
|
||||
key={user.id}
|
||||
withBottomMargin={!!removeOptionValue}
|
||||
onClick={() => removeOptionValue && removeOptionValue()}
|
||||
>
|
||||
<Avatar size={20} avatarUrl={user.avatarUrl} name={user.name} />
|
||||
<SelectItemLabel>{user.name}</SelectItemLabel>
|
||||
{removeOptionValue && <Icon type="close" top={2} />}
|
||||
</SelectItem>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
initialValues={{
|
||||
status: IssueStatus.BACKLOG,
|
||||
type: IssueType.TASK,
|
||||
title: '',
|
||||
description: '',
|
||||
reporterId: null,
|
||||
userIds: [],
|
||||
priority: null,
|
||||
}}
|
||||
validations={{
|
||||
type: Form.is.required(),
|
||||
title: [Form.is.required(), Form.is.maxLength(200)],
|
||||
reporterId: Form.is.required(),
|
||||
priority: Form.is.required(),
|
||||
}}
|
||||
onSubmit={async (values, form) => {
|
||||
try {
|
||||
await createIssue({
|
||||
...values,
|
||||
projectId: project.id,
|
||||
users: values.userIds.map(id => ({ id })),
|
||||
});
|
||||
await fetchProject();
|
||||
modalClose();
|
||||
} catch (error) {
|
||||
if (error.data.fields) {
|
||||
form.setErrors(error.data.fields);
|
||||
} else {
|
||||
toast.error(error);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormElement>
|
||||
<FormHeading>Create issue</FormHeading>
|
||||
<Form.Field.Select
|
||||
name="type"
|
||||
label="Issue Type"
|
||||
tip="Start typing to get a list of possible matches."
|
||||
options={typeOptions}
|
||||
renderOption={renderType}
|
||||
renderValue={renderType}
|
||||
/>
|
||||
<Divider />
|
||||
<Form.Field.Input
|
||||
name="title"
|
||||
label="Short Summary"
|
||||
tip="Concisely summarize the issue in one or two sentences."
|
||||
/>
|
||||
<Form.Field.TextEditor
|
||||
name="description"
|
||||
label="Description"
|
||||
tip="Describe the issue in as much detail as you'd like."
|
||||
/>
|
||||
<Form.Field.Select
|
||||
name="reporterId"
|
||||
label="Reporter"
|
||||
options={userOptions}
|
||||
renderOption={renderUser}
|
||||
renderValue={renderUser}
|
||||
/>
|
||||
<Form.Field.Select
|
||||
isMulti
|
||||
name="userIds"
|
||||
label="Assignees"
|
||||
tio="People who are responsible for dealing with this issue."
|
||||
options={userOptions}
|
||||
renderOption={renderUser}
|
||||
renderValue={renderUser}
|
||||
/>
|
||||
<Form.Field.Select
|
||||
name="priority"
|
||||
label="Priority"
|
||||
tip="Priority in relation to other issues."
|
||||
options={priorityOptions}
|
||||
renderOption={renderPriority}
|
||||
renderValue={renderPriority}
|
||||
/>
|
||||
<Actions>
|
||||
<ActionButton type="submit" color="primary" working={isCreating}>
|
||||
Create Issue
|
||||
</ActionButton>
|
||||
<ActionButton color="empty" onClick={modalClose}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</FormElement>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectIssueCreateForm.propTypes = propTypes;
|
||||
|
||||
export default ProjectIssueCreateForm;
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCodes } from 'shared/constants/keyCodes';
|
||||
import { isFocusedElementEditable } from 'shared/utils/dom';
|
||||
import { Tip, TipLetter } from './Style';
|
||||
import { Tip, TipLetter } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
setFormOpen: PropTypes.func.isRequired,
|
||||
@@ -6,7 +6,7 @@ import useApi from 'shared/hooks/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import BodyForm from '../BodyForm';
|
||||
import ProTip from './ProTip';
|
||||
import { Create, UserAvatar, Right, FakeTextarea } from './Style';
|
||||
import { Create, UserAvatar, Right, FakeTextarea } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issueId: PropTypes.number.isRequired,
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getTextContentsFromHtmlString } from 'shared/utils/html';
|
||||
@@ -11,34 +11,30 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
const $editorRef = useRef();
|
||||
const [isPresenting, setPresenting] = useState(true);
|
||||
const [value, setValue] = useState(issue.description);
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
|
||||
const renderPresentingMode = () =>
|
||||
isDescriptionEmpty(issue.description) ? (
|
||||
<EmptyLabel onClick={() => setPresenting(false)}>Add a description...</EmptyLabel>
|
||||
<EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel>
|
||||
) : (
|
||||
<TextEditedContent content={issue.description} onClick={() => setPresenting(false)} />
|
||||
<TextEditedContent content={issue.description} onClick={() => setEditing(true)} />
|
||||
);
|
||||
|
||||
const renderEditingMode = () => (
|
||||
<>
|
||||
<TextEditor
|
||||
placeholder="Describe the issue"
|
||||
defaultValue={issue.description}
|
||||
getEditor={editor => ($editorRef.current = editor)}
|
||||
/>
|
||||
<TextEditor placeholder="Describe the issue" defaultValue={value} onChange={setValue} />
|
||||
<Actions>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setPresenting(true);
|
||||
updateIssue({ description: $editorRef.current.getHTML() });
|
||||
setEditing(false);
|
||||
updateIssue({ description: value });
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button color="empty" onClick={() => setPresenting(true)}>
|
||||
<Button color="empty" onClick={() => setEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Actions>
|
||||
@@ -47,7 +43,7 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
return (
|
||||
<>
|
||||
<Title>Description</Title>
|
||||
{isPresenting ? renderPresentingMode() : renderEditingMode()}
|
||||
{isEditing ? renderEditingMode() : renderPresentingMode()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
@@ -18,7 +18,6 @@ export const Priority = styled.div`
|
||||
`;
|
||||
|
||||
export const Label = styled.div`
|
||||
text-transform: capitalize;
|
||||
padding: 0 3px 0 8px;
|
||||
${font.size(14.5)}
|
||||
`;
|
||||
@@ -1,14 +1,11 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { invert } from 'lodash';
|
||||
|
||||
import { IssuePriority } from 'shared/constants/issues';
|
||||
import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues';
|
||||
import { Select, IssuePriorityIcon } from 'shared/components';
|
||||
import { Priority, Label } from './Styles';
|
||||
import { SectionTitle } from '../Styles';
|
||||
|
||||
const IssuePriorityCopy = invert(IssuePriority);
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
@@ -18,13 +15,14 @@ const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => {
|
||||
const renderPriorityItem = (priority, isValue) => (
|
||||
<Priority isValue={isValue}>
|
||||
<IssuePriorityIcon priority={priority} />
|
||||
<Label>{IssuePriorityCopy[priority].toLowerCase()}</Label>
|
||||
<Label>{IssuePriorityCopy[priority]}</Label>
|
||||
</Priority>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<SectionTitle>Priority</SectionTitle>
|
||||
<Select
|
||||
variant="empty"
|
||||
dropdownWidth={343}
|
||||
value={issue.priority}
|
||||
options={Object.values(IssuePriority).map(priority => ({
|
||||
@@ -15,6 +15,7 @@ const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
|
||||
<>
|
||||
<SectionTitle>Status</SectionTitle>
|
||||
<Select
|
||||
variant="empty"
|
||||
dropdownWidth={343}
|
||||
value={issue.status}
|
||||
options={Object.values(IssueStatus).map(status => ({
|
||||
21
client/src/Project/IssueDetails/Type/Styles.js
Normal file
21
client/src/Project/IssueDetails/Type/Styles.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
import { Button } from 'shared/components';
|
||||
|
||||
export const TypeButton = styled(Button)`
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: ${color.textMedium};
|
||||
${font.size(13)}
|
||||
`;
|
||||
|
||||
export const Type = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const TypeLabel = styled.div`
|
||||
padding: 0 5px 0 7px;
|
||||
${font.size(15)}
|
||||
`;
|
||||
39
client/src/Project/IssueDetails/Type/index.jsx
Normal file
39
client/src/Project/IssueDetails/Type/index.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { IssueType, IssueTypeCopy } from 'shared/constants/issues';
|
||||
import { IssueTypeIcon, Select } from 'shared/components';
|
||||
import { TypeButton, Type, TypeLabel } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
|
||||
<Select
|
||||
variant="empty"
|
||||
dropdownWidth={150}
|
||||
value={issue.type}
|
||||
options={Object.values(IssueType).map(type => ({
|
||||
value: type,
|
||||
label: IssueTypeCopy[type],
|
||||
}))}
|
||||
onChange={type => updateIssue({ type })}
|
||||
renderValue={({ value: type }) => (
|
||||
<TypeButton color="empty" icon={<IssueTypeIcon type={type} />}>
|
||||
{`${type}-${issue.id}`}
|
||||
</TypeButton>
|
||||
)}
|
||||
renderOption={({ value: type }) => (
|
||||
<Type key={type} onClick={() => updateIssue({ type })}>
|
||||
<IssueTypeIcon type={type} top={1} />
|
||||
<TypeLabel>{IssueTypeCopy[type]}</TypeLabel>
|
||||
</Type>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsType.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsType;
|
||||
@@ -3,7 +3,7 @@ import styled, { css } from 'styled-components';
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
|
||||
export const User = styled.div`
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${mixin.clickable}
|
||||
${props =>
|
||||
@@ -16,12 +16,12 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
||||
|
||||
const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name }));
|
||||
|
||||
const renderUserValue = (user, withBottomMargin, removeOptionValue) => (
|
||||
const renderUser = (user, isSelectValue, removeOptionValue) => (
|
||||
<User
|
||||
key={user.id}
|
||||
isSelectValue
|
||||
withBottomMargin={withBottomMargin}
|
||||
onClick={() => removeOptionValue && removeOptionValue(user.id)}
|
||||
isSelectValue={isSelectValue}
|
||||
withBottomMargin={!!removeOptionValue}
|
||||
onClick={() => removeOptionValue && removeOptionValue()}
|
||||
>
|
||||
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={24} />
|
||||
<Username>{user.name}</Username>
|
||||
@@ -29,18 +29,12 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
||||
</User>
|
||||
);
|
||||
|
||||
const renderUserOption = user => (
|
||||
<User key={user.id}>
|
||||
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={32} />
|
||||
<Username>{user.name}</Username>
|
||||
</User>
|
||||
);
|
||||
|
||||
const renderAssignees = () => (
|
||||
<>
|
||||
<SectionTitle>Assignees</SectionTitle>
|
||||
<Select
|
||||
isMulti
|
||||
variant="empty"
|
||||
dropdownWidth={343}
|
||||
placeholder="Unassigned"
|
||||
value={issue.userIds}
|
||||
@@ -49,9 +43,9 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
||||
updateIssue({ userIds, users: userIds.map(getUserById) });
|
||||
}}
|
||||
renderValue={({ value, removeOptionValue }) =>
|
||||
renderUserValue(getUserById(value), true, removeOptionValue)
|
||||
renderUser(getUserById(value), true, removeOptionValue)
|
||||
}
|
||||
renderOption={({ value }) => renderUserOption(getUserById(value))}
|
||||
renderOption={({ value }) => renderUser(getUserById(value), false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -60,12 +54,13 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
||||
<>
|
||||
<SectionTitle>Reporter</SectionTitle>
|
||||
<Select
|
||||
variant="empty"
|
||||
dropdownWidth={343}
|
||||
value={issue.reporterId}
|
||||
options={userOptions}
|
||||
onChange={userId => updateIssue({ reporterId: userId })}
|
||||
renderValue={({ value }) => renderUserValue(getUserById(value), false)}
|
||||
renderOption={({ value }) => renderUserOption(getUserById(value))}
|
||||
renderValue={({ value }) => renderUser(getUserById(value), true)}
|
||||
renderOption={({ value }) => renderUser(getUserById(value))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
79
client/src/Project/NavbarLeft/Styles.js
Normal file
79
client/src/Project/NavbarLeft/Styles.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import styled from 'styled-components';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { font, sizes, color, mixin, zIndexValues } from 'shared/utils/styles';
|
||||
import Logo from 'shared/components/Logo';
|
||||
|
||||
export const NavLeft = styled.aside`
|
||||
z-index: ${zIndexValues.navLeft};
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow-x: hidden;
|
||||
height: 100vh;
|
||||
width: ${sizes.appNavBarLeftWidth}px;
|
||||
background: ${color.backgroundDarkPrimary};
|
||||
transition: all 0.1s;
|
||||
${mixin.hardwareAccelerate}
|
||||
&:hover {
|
||||
width: 200px;
|
||||
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
`;
|
||||
|
||||
export const LogoLink = styled(NavLink)`
|
||||
display: block;
|
||||
position: relative;
|
||||
left: 0;
|
||||
margin: 20px 0 10px;
|
||||
transition: left 0.1s;
|
||||
`;
|
||||
|
||||
export const StyledLogo = styled(Logo)`
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 10px;
|
||||
${mixin.clickable}
|
||||
`;
|
||||
|
||||
export const Bottom = styled.div`
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const Item = styled.div`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 42px;
|
||||
line-height: 42px;
|
||||
padding-left: 64px;
|
||||
color: #deebff;
|
||||
transition: color 0.1s;
|
||||
${mixin.clickable}
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
i {
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ItemText = styled.div`
|
||||
position: relative;
|
||||
right: 12px;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.1s;
|
||||
transition-property: right, visibility, opacity;
|
||||
${font.bold}
|
||||
${font.size(12)}
|
||||
${NavLeft}:hover & {
|
||||
right: 0;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
34
client/src/Project/NavbarLeft/index.jsx
Normal file
34
client/src/Project/NavbarLeft/index.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Link, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { Icon } from 'shared/components';
|
||||
import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles';
|
||||
|
||||
const ProjectNavbarLeft = () => {
|
||||
const match = useRouteMatch();
|
||||
return (
|
||||
<NavLeft>
|
||||
<LogoLink to="/">
|
||||
<StyledLogo color="#fff" />
|
||||
</LogoLink>
|
||||
<Item>
|
||||
<Icon type="search" size={22} top={1} left={3} />
|
||||
<ItemText>Search issues</ItemText>
|
||||
</Item>
|
||||
<Link to={`${match.path}/board/create-issue`}>
|
||||
<Item>
|
||||
<Icon type="plus" size={27} />
|
||||
<ItemText>Create Issue</ItemText>
|
||||
</Item>
|
||||
</Link>
|
||||
<Bottom>
|
||||
<Item>
|
||||
<Icon type="help" size={25} />
|
||||
<ItemText>Help</ItemText>
|
||||
</Item>
|
||||
</Bottom>
|
||||
</NavLeft>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectNavbarLeft;
|
||||
@@ -79,7 +79,7 @@ export const NotImplemented = styled.div`
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 104%;
|
||||
left: 101%;
|
||||
width: 120px;
|
||||
padding: 3px 0 3px 8px;
|
||||
border-radius: 3px;
|
||||
|
||||
@@ -3,5 +3,5 @@ import styled from 'styled-components';
|
||||
import { sizes } from 'shared/utils/styles';
|
||||
|
||||
export const ProjectPage = styled.div`
|
||||
padding: 25px 32px 0 ${sizes.secondarySideBarWidth + 40}px;
|
||||
padding: 25px 32px 0 ${sizes.appNavBarLeftWidth + sizes.secondarySideBarWidth + 40}px;
|
||||
`;
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Route, Redirect, useRouteMatch } from 'react-router-dom';
|
||||
import { Route, Redirect, useRouteMatch, useHistory } from 'react-router-dom';
|
||||
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { updateArrayItemById } from 'shared/utils/javascript';
|
||||
import { PageLoader, PageError } from 'shared/components';
|
||||
import { PageLoader, PageError, Modal } from 'shared/components';
|
||||
import NavbarLeft from './NavbarLeft';
|
||||
import Sidebar from './Sidebar';
|
||||
import Board from './Board';
|
||||
import IssueDetails from './IssueDetails';
|
||||
import IssueCreateForm from './IssueCreateForm';
|
||||
import { ProjectPage } from './Styles';
|
||||
|
||||
const Project = () => {
|
||||
const match = useRouteMatch();
|
||||
const history = useHistory();
|
||||
|
||||
const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project');
|
||||
|
||||
const updateLocalIssuesArray = (issueId, updatedFields) => {
|
||||
@@ -33,15 +38,40 @@ const Project = () => {
|
||||
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||
/>
|
||||
);
|
||||
const renderSettings = () => <h1>SETTINGS</h1>;
|
||||
const renderIssues = () => <h1>ISSUES</h1>;
|
||||
|
||||
const renderIssueDetailsModal = routeProps => (
|
||||
<Modal
|
||||
isOpen
|
||||
width={1040}
|
||||
withCloseIcon={false}
|
||||
onClose={() => history.push(match.url)}
|
||||
renderContent={modal => (
|
||||
<IssueDetails
|
||||
issueId={routeProps.match.params.issueId}
|
||||
projectUsers={project.users}
|
||||
fetchProject={fetchProject}
|
||||
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||
modalClose={modal.close}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
const renderIssueCreateModal = () => (
|
||||
<Modal
|
||||
isOpen
|
||||
width={800}
|
||||
onClose={() => history.push(match.url)}
|
||||
renderContent={modal => (
|
||||
<IssueCreateForm project={project} fetchProject={fetchProject} modalClose={modal.close} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<ProjectPage>
|
||||
<NavbarLeft />
|
||||
<Sidebar projectName={project.name} matchPath={match.path} />
|
||||
<Route path={`${match.path}/board`} render={renderBoard} />
|
||||
<Route path={`${match.path}/settings`} render={renderSettings} />
|
||||
<Route path={`${match.path}/issues`} render={renderIssues} />
|
||||
<Route path={`${match.path}/board/create-issue`} render={renderIssueCreateModal} />
|
||||
<Route path={`${match.path}/board/issue/:issueId`} render={renderIssueDetailsModal} />
|
||||
{match.isExact && <Redirect to={`${match.url}/board`} />}
|
||||
</ProjectPage>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user