Improved code styling
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { InputDebounced, Avatar, Button } from 'shared/components';
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { InputDebounced, Avatar, Button } from 'shared/components';
|
||||
|
||||
export const Filters = styled.div`
|
||||
display: flex;
|
||||
|
||||
@@ -16,14 +16,12 @@ const propTypes = {
|
||||
projectUsers: PropTypes.array.isRequired,
|
||||
defaultFilters: PropTypes.object.isRequired,
|
||||
filters: PropTypes.object.isRequired,
|
||||
setFilters: PropTypes.func.isRequired,
|
||||
mergeFilters: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters }) => {
|
||||
const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilters }) => {
|
||||
const { searchQuery, userIds, myOnly, recent } = filters;
|
||||
|
||||
const setFiltersMerge = newFilters => setFilters({ ...filters, ...newFilters });
|
||||
|
||||
const areFiltersCleared = !searchQuery && userIds.length === 0 && !myOnly && !recent;
|
||||
|
||||
return (
|
||||
@@ -31,7 +29,7 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters
|
||||
<SearchInput
|
||||
icon="search"
|
||||
value={searchQuery}
|
||||
onChange={value => setFiltersMerge({ searchQuery: value })}
|
||||
onChange={value => mergeFilters({ searchQuery: value })}
|
||||
/>
|
||||
<Avatars>
|
||||
{projectUsers.map(user => (
|
||||
@@ -39,27 +37,27 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters
|
||||
<StyledAvatar
|
||||
avatarUrl={user.avatarUrl}
|
||||
name={user.name}
|
||||
onClick={() => setFiltersMerge({ userIds: xor(userIds, [user.id]) })}
|
||||
onClick={() => mergeFilters({ userIds: xor(userIds, [user.id]) })}
|
||||
/>
|
||||
</AvatarIsActiveBorder>
|
||||
))}
|
||||
</Avatars>
|
||||
<StyledButton
|
||||
color="empty"
|
||||
variant="empty"
|
||||
isActive={myOnly}
|
||||
onClick={() => setFiltersMerge({ myOnly: !myOnly })}
|
||||
onClick={() => mergeFilters({ myOnly: !myOnly })}
|
||||
>
|
||||
Only My Issues
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
color="empty"
|
||||
variant="empty"
|
||||
isActive={recent}
|
||||
onClick={() => setFiltersMerge({ recent: !recent })}
|
||||
onClick={() => mergeFilters({ recent: !recent })}
|
||||
>
|
||||
Recently Updated
|
||||
</StyledButton>
|
||||
{!areFiltersCleared && (
|
||||
<ClearAll onClick={() => setFilters(defaultFilters)}>Clear all</ClearAll>
|
||||
<ClearAll onClick={() => mergeFilters(defaultFilters)}>Clear all</ClearAll>
|
||||
)}
|
||||
</Filters>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CopyLinkButton } from 'shared/components';
|
||||
|
||||
import { Breadcrumbs, Divider, Header, BoardName } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
@@ -17,6 +18,7 @@ const ProjectBoardHeader = ({ projectName }) => (
|
||||
<Divider>/</Divider>
|
||||
Kanban Board
|
||||
</Breadcrumbs>
|
||||
|
||||
<Header>
|
||||
<BoardName>Kanban board</BoardName>
|
||||
<CopyLinkButton />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Avatar } from 'shared/components';
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { Avatar } from 'shared/components';
|
||||
|
||||
export const IssueLink = styled(Link)`
|
||||
display: block;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useRouteMatch } from 'react-router-dom';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components';
|
||||
|
||||
import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import api from 'shared/utils/api';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript';
|
||||
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
|
||||
|
||||
import Issue from './Issue';
|
||||
import { Lists, List, Title, IssuesCount, Issues } from './Styles';
|
||||
|
||||
@@ -21,13 +22,8 @@ const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
|
||||
const [{ data: currentUserData }] = useApi.get('/currentUser');
|
||||
const currentUserId = get(currentUserData, 'currentUser.id');
|
||||
|
||||
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
|
||||
|
||||
const handleIssueDrop = async ({ draggableId, destination, source }) => {
|
||||
if (!destination) return;
|
||||
const isSameList = destination.droppableId === source.droppableId;
|
||||
const isSamePosition = destination.index === source.index;
|
||||
if (isSameList && isSamePosition) return;
|
||||
if (!isPositionChanged(source, destination)) return;
|
||||
|
||||
const issueId = Number(draggableId);
|
||||
|
||||
@@ -35,7 +31,7 @@ const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
|
||||
url: `/issues/${issueId}`,
|
||||
updatedFields: {
|
||||
status: destination.droppableId,
|
||||
listPosition: calculateListPosition(project.issues, destination, isSameList, issueId),
|
||||
listPosition: calculateListPosition(project.issues, destination, source, issueId),
|
||||
},
|
||||
currentFields: project.issues.find(({ id }) => id === issueId),
|
||||
setLocalData: fields => updateLocalIssuesArray(issueId, fields),
|
||||
@@ -43,21 +39,17 @@ const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
|
||||
};
|
||||
|
||||
const renderList = status => {
|
||||
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
|
||||
const filteredListIssues = getSortedListIssues(filteredIssues, status);
|
||||
const allListIssues = getSortedListIssues(project.issues, status);
|
||||
|
||||
const issuesCount =
|
||||
allListIssues.length !== filteredListIssues.length
|
||||
? `${filteredListIssues.length} of ${allListIssues.length}`
|
||||
: allListIssues.length;
|
||||
|
||||
return (
|
||||
<Droppable key={status} droppableId={status}>
|
||||
{provided => (
|
||||
<List>
|
||||
<Title>
|
||||
{`${IssueStatusCopy[status]} `}
|
||||
<IssuesCount>{issuesCount}</IssuesCount>
|
||||
<IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount>
|
||||
</Title>
|
||||
<Issues {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{filteredListIssues.map((issue, index) => (
|
||||
@@ -102,6 +94,20 @@ const filterIssues = (projectIssues, filters, currentUserId) => {
|
||||
const getSortedListIssues = (issues, status) =>
|
||||
issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);
|
||||
|
||||
const formatIssuesCount = (allListIssues, filteredListIssues) => {
|
||||
if (allListIssues.length !== filteredListIssues.length) {
|
||||
return `${filteredListIssues.length} of ${allListIssues.length}`;
|
||||
}
|
||||
return allListIssues.length;
|
||||
};
|
||||
|
||||
const isPositionChanged = (destination, source) => {
|
||||
if (!destination) return false;
|
||||
const isSameList = destination.droppableId === source.droppableId;
|
||||
const isSamePosition = destination.index === source.index;
|
||||
return !isSameList || !isSamePosition;
|
||||
};
|
||||
|
||||
const calculateListPosition = (...args) => {
|
||||
const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args);
|
||||
let position;
|
||||
@@ -118,9 +124,10 @@ const calculateListPosition = (...args) => {
|
||||
return position;
|
||||
};
|
||||
|
||||
const getAfterDropPrevNextIssue = (allIssues, destination, isSameList, droppedIssueId) => {
|
||||
const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueId) => {
|
||||
const destinationIssues = getSortedListIssues(allIssues, destination.droppableId);
|
||||
const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId);
|
||||
const isSameList = destination.droppableId === source.droppableId;
|
||||
|
||||
const afterDropDestinationIssues = isSameList
|
||||
? moveItemWithinArray(destinationIssues, droppedIssue, destination.index)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useMergeState from 'shared/hooks/mergeState';
|
||||
|
||||
import Header from './Header';
|
||||
import Filters from './Filters';
|
||||
import Lists from './Lists';
|
||||
@@ -18,7 +20,7 @@ const defaultFilters = {
|
||||
};
|
||||
|
||||
const ProjectBoard = ({ project, updateLocalIssuesArray }) => {
|
||||
const [filters, setFilters] = useState(defaultFilters);
|
||||
const [filters, mergeFilters] = useMergeState(defaultFilters);
|
||||
return (
|
||||
<>
|
||||
<Header projectName={project.name} />
|
||||
@@ -26,7 +28,7 @@ const ProjectBoard = ({ project, updateLocalIssuesArray }) => {
|
||||
projectUsers={project.users}
|
||||
defaultFilters={defaultFilters}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
mergeFilters={mergeFilters}
|
||||
/>
|
||||
<Lists project={project} filters={filters} updateLocalIssuesArray={updateLocalIssuesArray} />
|
||||
</>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import toast from 'shared/utils/toast';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { Form, IssueTypeIcon, Icon, Avatar, IssuePriorityIcon } from 'shared/components';
|
||||
|
||||
import {
|
||||
FormHeading,
|
||||
FormElement,
|
||||
@@ -152,10 +153,10 @@ const ProjectIssueCreateForm = ({ project, fetchProject, modalClose }) => {
|
||||
renderValue={renderPriority}
|
||||
/>
|
||||
<Actions>
|
||||
<ActionButton type="submit" color="primary" working={isCreating}>
|
||||
<ActionButton type="submit" variant="primary" working={isCreating}>
|
||||
Create Issue
|
||||
</ActionButton>
|
||||
<ActionButton color="empty" onClick={modalClose}>
|
||||
<ActionButton variant="empty" onClick={modalClose}>
|
||||
Cancel
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Textarea } from 'shared/components';
|
||||
|
||||
import { Actions, FormButton } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
@@ -20,6 +21,7 @@ const ProjectBoardIssueDetailsCommentsBodyForm = ({
|
||||
onCancel,
|
||||
}) => {
|
||||
const $textareaRef = useRef();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Textarea
|
||||
@@ -31,8 +33,8 @@ const ProjectBoardIssueDetailsCommentsBodyForm = ({
|
||||
/>
|
||||
<Actions>
|
||||
<FormButton
|
||||
color="primary"
|
||||
working={isWorking}
|
||||
variant="primary"
|
||||
isWorking={isWorking}
|
||||
onClick={() => {
|
||||
if ($textareaRef.current.value.trim()) {
|
||||
onSubmit();
|
||||
@@ -41,7 +43,7 @@ const ProjectBoardIssueDetailsCommentsBodyForm = ({
|
||||
>
|
||||
Save
|
||||
</FormButton>
|
||||
<FormButton color="empty" onClick={onCancel}>
|
||||
<FormButton variant="empty" onClick={onCancel}>
|
||||
Cancel
|
||||
</FormButton>
|
||||
</Actions>
|
||||
|
||||
@@ -5,6 +5,7 @@ import api from 'shared/utils/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import { formatDateTimeConversational } from 'shared/utils/dateTime';
|
||||
import { ConfirmModal } from 'shared/components';
|
||||
|
||||
import BodyForm from '../BodyForm';
|
||||
import {
|
||||
Comment,
|
||||
@@ -35,6 +36,7 @@ const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
|
||||
toast.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCommentUpdate = async () => {
|
||||
try {
|
||||
setUpdating(true);
|
||||
@@ -46,12 +48,14 @@ const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
|
||||
toast.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Comment>
|
||||
<UserAvatar name={comment.user.name} avatarUrl={comment.user.avatarUrl} />
|
||||
<Content>
|
||||
<Username>{comment.user.name}</Username>
|
||||
<CreatedAt>{formatDateTimeConversational(comment.createdAt)}</CreatedAt>
|
||||
|
||||
{isFormOpen ? (
|
||||
<BodyForm
|
||||
value={body}
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCodes } from 'shared/constants/keyCodes';
|
||||
import { isFocusedElementEditable } from 'shared/utils/dom';
|
||||
import { isFocusedElementEditable } from 'shared/utils/browser';
|
||||
|
||||
import { Tip, TipLetter } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
@@ -17,7 +18,9 @@ const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
|
||||
setFormOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import api from 'shared/utils/api';
|
||||
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 './Styles';
|
||||
|
||||
@@ -14,13 +14,14 @@ const ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => (
|
||||
<Comments>
|
||||
<Title>Comments</Title>
|
||||
<Create issueId={issue.id} fetchIssue={fetchIssue} />
|
||||
{sortByNewestFirst(issue.comments).map(comment => (
|
||||
|
||||
{sortByNewest(issue.comments).map(comment => (
|
||||
<Comment key={comment.id} comment={comment} fetchIssue={fetchIssue} />
|
||||
))}
|
||||
</Comments>
|
||||
);
|
||||
|
||||
const sortByNewestFirst = items => items.sort((a, b) => -a.createdAt.localeCompare(b.createdAt));
|
||||
const sortByNewest = items => items.sort((a, b) => -a.createdAt.localeCompare(b.createdAt));
|
||||
|
||||
ProjectBoardIssueDetailsComments.propTypes = propTypes;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatDateTimeConversational } from 'shared/utils/dateTime';
|
||||
|
||||
import { Dates } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
|
||||
@@ -21,13 +21,16 @@ const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) =>
|
||||
toast.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
title="Are you sure you want to delete this issue?"
|
||||
message="Once you delete, it's gone for good."
|
||||
confirmText="Delete issue"
|
||||
onConfirm={handleIssueDelete}
|
||||
renderLink={modal => <Button icon="trash" iconSize={19} color="empty" onClick={modal.open} />}
|
||||
renderLink={modal => (
|
||||
<Button icon="trash" iconSize={19} variant="empty" onClick={modal.open} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getTextContentsFromHtmlString } from 'shared/utils/html';
|
||||
import { getTextContentsFromHtmlString } from 'shared/utils/browser';
|
||||
import { TextEditor, TextEditedContent, Button } from 'shared/components';
|
||||
|
||||
import { Title, EmptyLabel, Actions } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
@@ -14,8 +15,15 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
const [value, setValue] = useState(issue.description);
|
||||
const [isEditing, setEditing] = useState(false);
|
||||
|
||||
const handleUpdate = () => {
|
||||
setEditing(false);
|
||||
updateIssue({ description: value });
|
||||
};
|
||||
|
||||
const isDescriptionEmpty = getTextContentsFromHtmlString(issue.description).trim().length === 0;
|
||||
|
||||
const renderPresentingMode = () =>
|
||||
isDescriptionEmpty(issue.description) ? (
|
||||
isDescriptionEmpty ? (
|
||||
<EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel>
|
||||
) : (
|
||||
<TextEditedContent content={issue.description} onClick={() => setEditing(true)} />
|
||||
@@ -25,21 +33,16 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
<>
|
||||
<TextEditor placeholder="Describe the issue" defaultValue={value} onChange={setValue} />
|
||||
<Actions>
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setEditing(false);
|
||||
updateIssue({ description: value });
|
||||
}}
|
||||
>
|
||||
<Button variant="primary" onClick={handleUpdate}>
|
||||
Save
|
||||
</Button>
|
||||
<Button color="empty" onClick={() => setEditing(false)}>
|
||||
<Button variant="empty" onClick={() => setEditing(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Actions>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Description</Title>
|
||||
@@ -48,9 +51,6 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isDescriptionEmpty = description =>
|
||||
getTextContentsFromHtmlString(description).trim().length === 0;
|
||||
|
||||
ProjectBoardIssueDetailsDescription.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsDescription;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Tooltip } from 'shared/components';
|
||||
|
||||
import feedbackImage from './assets/feedback.png';
|
||||
import { FeedbackDropdown, FeedbackImageCont, FeedbackImage, FeedbackParagraph } from './Styles';
|
||||
|
||||
@@ -9,7 +10,7 @@ const ProjectBoardIssueDetailsFeedback = () => (
|
||||
width={300}
|
||||
offset={{ top: -15 }}
|
||||
renderLink={linkProps => (
|
||||
<Button icon="feedback" color="empty" {...linkProps}>
|
||||
<Button icon="feedback" variant="empty" {...linkProps}>
|
||||
Give feedback
|
||||
</Button>
|
||||
)}
|
||||
@@ -18,19 +19,23 @@ const ProjectBoardIssueDetailsFeedback = () => (
|
||||
<FeedbackImageCont>
|
||||
<FeedbackImage src={feedbackImage} alt="Give feedback" />
|
||||
</FeedbackImageCont>
|
||||
|
||||
<FeedbackParagraph>
|
||||
This simplified Jira clone is built with React on the front-end and Node/TypeScript on the
|
||||
back-end.
|
||||
</FeedbackParagraph>
|
||||
|
||||
<FeedbackParagraph>
|
||||
{'Read more on our website or reach out via '}
|
||||
<a href="mailto:ivor@codetree.co">
|
||||
<strong>ivor@codetree.co</strong>
|
||||
</a>
|
||||
</FeedbackParagraph>
|
||||
|
||||
<a href="https://codetree.co/" target="_blank" rel="noreferrer noopener">
|
||||
<Button color="primary">Visit Website</Button>
|
||||
<Button variant="primary">Visit Website</Button>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener">
|
||||
<Button style={{ marginLeft: 10 }} icon="github">
|
||||
Github Repo
|
||||
|
||||
@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues';
|
||||
import { Select, IssuePriorityIcon } from 'shared/components';
|
||||
import { Priority, Label } from './Styles';
|
||||
|
||||
import { SectionTitle } from '../Styles';
|
||||
import { Priority, Label } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
@@ -18,6 +19,7 @@ const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => {
|
||||
<Label>{IssuePriorityCopy[priority]}</Label>
|
||||
</Priority>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionTitle>Priority</SectionTitle>
|
||||
|
||||
@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
|
||||
import { Select, Icon } from 'shared/components';
|
||||
import { Status } from './Styles';
|
||||
|
||||
import { SectionTitle } from '../Styles';
|
||||
import { Status } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCodes } from 'shared/constants/keyCodes';
|
||||
import { is, generateErrors } from 'shared/utils/validation';
|
||||
|
||||
import { TitleTextarea, ErrorText } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
|
||||
@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
|
||||
import { isNil } from 'lodash';
|
||||
|
||||
import { InputDebounced, Modal, Button } from 'shared/components';
|
||||
|
||||
import { SectionTitle } from '../Styles';
|
||||
import {
|
||||
TrackingLink,
|
||||
Tracking,
|
||||
@@ -18,7 +20,6 @@ import {
|
||||
InputLabel,
|
||||
Actions,
|
||||
} from './Styles';
|
||||
import { SectionTitle } from '../Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
@@ -38,52 +39,6 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
|
||||
/>
|
||||
);
|
||||
|
||||
const calculateTrackingBarWidth = () => {
|
||||
const { timeSpent, timeRemaining, estimate } = issue;
|
||||
|
||||
if (!timeSpent) {
|
||||
return 0;
|
||||
}
|
||||
if (isNil(timeRemaining) && isNil(estimate)) {
|
||||
return 100;
|
||||
}
|
||||
if (!isNil(timeRemaining)) {
|
||||
return (timeSpent / (timeSpent + timeRemaining)) * 100;
|
||||
}
|
||||
if (!isNil(estimate)) {
|
||||
return Math.min((timeSpent / estimate) * 100, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const renderRemainingOrEstimate = () => {
|
||||
const { timeRemaining, estimate } = issue;
|
||||
|
||||
if (isNil(timeRemaining) && isNil(estimate)) {
|
||||
return null;
|
||||
}
|
||||
if (!isNil(timeRemaining)) {
|
||||
return <div>{`${timeRemaining}h remaining`}</div>;
|
||||
}
|
||||
if (!isNil(estimate)) {
|
||||
return <div>{`${estimate}h estimated`}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const renderTrackingPreview = (onClick = () => {}) => (
|
||||
<Tracking onClick={onClick}>
|
||||
<WatchIcon type="stopwatch" size={26} top={-1} />
|
||||
<Right>
|
||||
<BarCont>
|
||||
<Bar width={calculateTrackingBarWidth()} />
|
||||
</BarCont>
|
||||
<Values>
|
||||
<div>{issue.timeSpent ? `${issue.timeSpent}h logged` : 'No time logged'}</div>
|
||||
{renderRemainingOrEstimate()}
|
||||
</Values>
|
||||
</Right>
|
||||
</Tracking>
|
||||
);
|
||||
|
||||
const renderEstimate = () => (
|
||||
<>
|
||||
<SectionTitle>Original Estimate (hours)</SectionTitle>
|
||||
@@ -96,11 +51,11 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
|
||||
<SectionTitle>Time Tracking</SectionTitle>
|
||||
<Modal
|
||||
width={400}
|
||||
renderLink={modal => <TrackingLink>{renderTrackingPreview(modal.open)}</TrackingLink>}
|
||||
renderLink={modal => <TrackingLink>{renderTrackingWidget(modal.open)}</TrackingLink>}
|
||||
renderContent={modal => (
|
||||
<ModalContents>
|
||||
<ModalTitle>Time tracking</ModalTitle>
|
||||
{renderTrackingPreview()}
|
||||
{renderTrackingWidget()}
|
||||
<Inputs>
|
||||
<InputCont>
|
||||
<InputLabel>Time spent (hours)</InputLabel>
|
||||
@@ -112,7 +67,7 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
|
||||
</InputCont>
|
||||
</Inputs>
|
||||
<Actions>
|
||||
<Button color="primary" onClick={modal.close}>
|
||||
<Button variant="primary" onClick={modal.close}>
|
||||
Done
|
||||
</Button>
|
||||
</Actions>
|
||||
@@ -122,6 +77,21 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
|
||||
</>
|
||||
);
|
||||
|
||||
const renderTrackingWidget = (onClick = () => {}) => (
|
||||
<Tracking onClick={onClick}>
|
||||
<WatchIcon type="stopwatch" size={26} top={-1} />
|
||||
<Right>
|
||||
<BarCont>
|
||||
<Bar width={calculateTrackingBarWidth(issue)} />
|
||||
</BarCont>
|
||||
<Values>
|
||||
<div>{issue.timeSpent ? `${issue.timeSpent}h logged` : 'No time logged'}</div>
|
||||
{renderRemainingOrEstimate(issue)}
|
||||
</Values>
|
||||
</Right>
|
||||
</Tracking>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderEstimate()}
|
||||
@@ -130,6 +100,33 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const calculateTrackingBarWidth = ({ timeSpent, timeRemaining, estimate }) => {
|
||||
if (!timeSpent) {
|
||||
return 0;
|
||||
}
|
||||
if (isNil(timeRemaining) && isNil(estimate)) {
|
||||
return 100;
|
||||
}
|
||||
if (!isNil(timeRemaining)) {
|
||||
return (timeSpent / (timeSpent + timeRemaining)) * 100;
|
||||
}
|
||||
if (!isNil(estimate)) {
|
||||
return Math.min((timeSpent / estimate) * 100, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const renderRemainingOrEstimate = ({ timeRemaining, estimate }) => {
|
||||
if (isNil(timeRemaining) && isNil(estimate)) {
|
||||
return null;
|
||||
}
|
||||
if (!isNil(timeRemaining)) {
|
||||
return <div>{`${timeRemaining}h remaining`}</div>;
|
||||
}
|
||||
if (!isNil(estimate)) {
|
||||
return <div>{`${estimate}h estimated`}</div>;
|
||||
}
|
||||
};
|
||||
|
||||
ProjectBoardIssueDetailsTracking.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsTracking;
|
||||
|
||||
@@ -3,6 +3,7 @@ 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 = {
|
||||
@@ -21,7 +22,7 @@ const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
|
||||
}))}
|
||||
onChange={type => updateIssue({ type })}
|
||||
renderValue={({ value: type }) => (
|
||||
<TypeButton color="empty" icon={<IssueTypeIcon type={type} />}>
|
||||
<TypeButton variant="empty" icon={<IssueTypeIcon type={type} />}>
|
||||
{`${type}-${issue.id}`}
|
||||
</TypeButton>
|
||||
)}
|
||||
|
||||
@@ -2,8 +2,9 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Avatar, Select, Icon } from 'shared/components';
|
||||
import { User, Username } from './Styles';
|
||||
|
||||
import { SectionTitle } from '../Styles';
|
||||
import { User, Username } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
|
||||
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import api from 'shared/utils/api';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { PageError, CopyLinkButton, Button } from 'shared/components';
|
||||
|
||||
import Loader from './Loader';
|
||||
import Type from './Type';
|
||||
import Feedback from './Feedback';
|
||||
@@ -61,9 +62,9 @@ const ProjectBoardIssueDetails = ({
|
||||
<Type issue={issue} updateIssue={updateIssue} />
|
||||
<TopActionsRight>
|
||||
<Feedback />
|
||||
<CopyLinkButton color="empty" />
|
||||
<CopyLinkButton variant="empty" />
|
||||
<Delete issue={issue} fetchProject={fetchProject} modalClose={modalClose} />
|
||||
<Button icon="close" iconSize={24} color="empty" onClick={modalClose} />
|
||||
<Button icon="close" iconSize={24} variant="empty" onClick={modalClose} />
|
||||
</TopActionsRight>
|
||||
</TopActions>
|
||||
<Content>
|
||||
|
||||
@@ -2,7 +2,7 @@ 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';
|
||||
import { Logo } from 'shared/components';
|
||||
|
||||
export const NavLeft = styled.aside`
|
||||
z-index: ${zIndexValues.navLeft};
|
||||
|
||||
@@ -2,6 +2,7 @@ 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 = () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import styled from 'styled-components';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles';
|
||||
@@ -51,11 +51,7 @@ export const LinkItem = styled(NavLink)`
|
||||
${props =>
|
||||
!props.implemented
|
||||
? `cursor: not-allowed;`
|
||||
: css`
|
||||
&:hover {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
`}
|
||||
: `&:hover { background: ${color.backgroundLight}; }`}
|
||||
i {
|
||||
margin-right: 15px;
|
||||
font-size: 20px;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { Icon, ProjectAvatar } from 'shared/components';
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
ProjectInfo,
|
||||
@@ -16,17 +18,19 @@ import {
|
||||
|
||||
const propTypes = {
|
||||
projectName: PropTypes.string.isRequired,
|
||||
matchPath: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const ProjectSidebar = ({ projectName, matchPath }) => {
|
||||
const ProjectSidebar = ({ projectName }) => {
|
||||
const match = useRouteMatch();
|
||||
|
||||
const renderLinkItem = (text, iconType, path = '') => (
|
||||
<LinkItem exact to={`${matchPath}${path}`} implemented={path}>
|
||||
<LinkItem exact to={`${match.path}${path}`} implemented={path}>
|
||||
<Icon type={iconType} />
|
||||
<LinkText>{text}</LinkText>
|
||||
{!path && <NotImplemented>Not implemented</NotImplemented>}
|
||||
</LinkItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
<ProjectInfo>
|
||||
@@ -36,6 +40,7 @@ const ProjectSidebar = ({ projectName, matchPath }) => {
|
||||
<ProjectCategory>Software project</ProjectCategory>
|
||||
</ProjectTexts>
|
||||
</ProjectInfo>
|
||||
|
||||
{renderLinkItem('Kanban Board', 'board', '/board')}
|
||||
{renderLinkItem('Reports', 'reports')}
|
||||
<Divider />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Route, Redirect, useRouteMatch, useHistory } from 'react-router-dom';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { updateArrayItemById } from 'shared/utils/javascript';
|
||||
import { PageLoader, PageError, Modal } from 'shared/components';
|
||||
|
||||
import NavbarLeft from './NavbarLeft';
|
||||
import Sidebar from './Sidebar';
|
||||
import Board from './Board';
|
||||
@@ -38,12 +39,24 @@ const Project = () => {
|
||||
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderIssueCreateModal = () => (
|
||||
<Modal
|
||||
isOpen
|
||||
width={800}
|
||||
onClose={() => history.push(`${match.url}/board`)}
|
||||
renderContent={modal => (
|
||||
<IssueCreateForm project={project} fetchProject={fetchProject} modalClose={modal.close} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderIssueDetailsModal = routeProps => (
|
||||
<Modal
|
||||
isOpen
|
||||
width={1040}
|
||||
withCloseIcon={false}
|
||||
onClose={() => history.push(match.url)}
|
||||
onClose={() => history.push(`${match.url}/board`)}
|
||||
renderContent={modal => (
|
||||
<IssueDetails
|
||||
issueId={routeProps.match.params.issueId}
|
||||
@@ -55,20 +68,11 @@ const Project = () => {
|
||||
)}
|
||||
/>
|
||||
);
|
||||
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} />
|
||||
<Sidebar projectName={project.name} />
|
||||
<Route path={`${match.path}/board`} render={renderBoard} />
|
||||
<Route path={`${match.path}/board/create-issue`} render={renderIssueCreateModal} />
|
||||
<Route path={`${match.path}/board/issue/:issueId`} render={renderIssueDetailsModal} />
|
||||
|
||||
Reference in New Issue
Block a user