Polished existing features, added items to sidebar navigation
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 432 KiB |
@@ -28,7 +28,12 @@
|
||||
<glyph unicode="" glyph-name="trash" d="M768 640v-554.667c0-11.776-4.736-22.4-12.501-30.165s-18.389-12.501-30.165-12.501h-426.667c-11.776 0-22.4 4.736-30.165 12.501s-12.501 18.389-12.501 30.165v554.667zM725.333 725.334v42.667c0 35.328-14.379 67.413-37.504 90.496s-55.168 37.504-90.496 37.504h-170.667c-35.328 0-67.413-14.379-90.496-37.504s-37.504-55.168-37.504-90.496v-42.667h-170.667c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h42.667v-554.667c0-35.328 14.379-67.413 37.504-90.496s55.168-37.504 90.496-37.504h426.667c35.328 0 67.413 14.379 90.496 37.504s37.504 55.168 37.504 90.496v554.667h42.667c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM384 725.334v42.667c0 11.776 4.736 22.4 12.501 30.165s18.389 12.501 30.165 12.501h170.667c11.776 0 22.4-4.736 30.165-12.501s12.501-18.389 12.501-30.165v-42.667zM384 469.334v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667zM554.667 469.334v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="close" d="M225.835 652.502l225.835-225.835-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l225.835 225.835 225.835-225.835c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-225.835 225.835 225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-225.835-225.835-225.835 225.835c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331z" />
|
||||
<glyph unicode="" glyph-name="stopwatch" d="M512 84.667q124 0 211 88t87 212-87 211-211 87-211-87-87-211 87-212 211-88zM812 622.667q34-44 59-113t25-125q0-158-112-271t-272-113-272 113-112 271 112 271 272 113q54 0 125-26t115-60l60 62q32-26 60-60zM470 340.667v256h84v-256h-84zM640 896.667v-86h-256v86h256z" />
|
||||
<glyph unicode="" glyph-name="github" horiz-adv-x="878" d="M438.857 877.714c242.286 0 438.857-196.571 438.857-438.857 0-193.714-125.714-358.286-300-416.571-22.286-4-30.286 9.714-30.286 21.143 0 14.286 0.571 61.714 0.571 120.571 0 41.143-13.714 67.429-29.714 81.143 97.714 10.857 200.571 48 200.571 216.571 0 48-17.143 86.857-45.143 117.714 4.571 11.429 19.429 56-4.571 116.571-36.571 11.429-120.571-45.143-120.571-45.143-34.857 9.714-72.571 14.857-109.714 14.857s-74.857-5.143-109.714-14.857c0 0-84 56.571-120.571 45.143-24-60.571-9.143-105.143-4.571-116.571-28-30.857-45.143-69.714-45.143-117.714 0-168 102.286-205.714 200-216.571-12.571-11.429-24-30.857-28-58.857-25.143-11.429-89.143-30.857-127.429 36.571-24 41.714-67.429 45.143-67.429 45.143-42.857 0.571-2.857-26.857-2.857-26.857 28.571-13.143 48.571-64 48.571-64 25.714-78.286 148-52 148-52 0-36.571 0.571-70.857 0.571-81.714 0-11.429-8-25.143-30.286-21.143-174.286 58.286-300 222.857-300 416.571 0 242.286 196.571 438.857 438.857 438.857zM166.286 247.428c1.143 2.286-0.571 5.143-4 6.857-3.429 1.143-6.286 0.571-7.429-1.143-1.143-2.286 0.571-5.143 4-6.857 2.857-1.714 6.286-1.143 7.429 1.143zM184 228c2.286 1.714 1.714 5.714-1.143 9.143-2.857 2.857-6.857 4-9.143 1.714-2.286-1.714-1.714-5.714 1.143-9.143 2.857-2.857 6.857-4 9.143-1.714zM201.143 202.286c2.857 2.286 2.857 6.857 0 10.857-2.286 4-6.857 5.714-9.714 3.429-2.857-1.714-2.857-6.286 0-10.286s7.429-5.714 9.714-4zM225.143 178.286c2.286 2.286 1.143 7.429-2.286 10.857-4 4-9.143 4.571-11.429 1.714-2.857-2.286-1.714-7.429 2.286-10.857 4-4 9.143-4.571 11.429-1.714zM257.714 164c1.143 3.429-2.286 7.429-7.429 9.143-4.571 1.143-9.714-0.571-10.857-4s2.286-7.429 7.429-8.571c4.571-1.714 9.714 0 10.857 3.429zM293.714 161.143c0 4-4.571 6.857-9.714 6.286-5.143 0-9.143-2.857-9.143-6.286 0-4 4-6.857 9.714-6.286 5.143 0 9.143 2.857 9.143 6.286zM326.857 166.857c-0.571 3.429-5.143 5.714-10.286 5.143-5.143-1.143-8.571-4.571-8-8.571 0.571-3.429 5.143-5.714 10.286-4.571s8.571 4.571 8 8z" />
|
||||
<glyph unicode="" glyph-name="menu" d="M128 384h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM128 640h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM128 128h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="arrow-left-circle" d="M981.333 426.667c0 129.579-52.565 246.997-137.472 331.861s-202.283 137.472-331.861 137.472-246.997-52.565-331.861-137.472-137.472-202.283-137.472-331.861 52.565-246.997 137.472-331.861 202.283-137.472 331.861-137.472 246.997 52.565 331.861 137.472 137.472 202.283 137.472 331.861zM896 426.667c0-106.069-42.923-201.984-112.469-271.531s-165.461-112.469-271.531-112.469-201.984 42.923-271.531 112.469-112.469 165.461-112.469 271.531 42.923 201.984 112.469 271.531 165.461 112.469 271.531 112.469 201.984-42.923 271.531-112.469 112.469-165.461 112.469-271.531zM682.667 469.334h-238.336l97.835 97.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-170.667-170.667c-4.096-4.096-7.168-8.789-9.259-13.824s-3.243-10.539-3.243-16.341c0-5.547 1.067-11.136 3.243-16.341 2.091-5.035 5.163-9.728 9.259-13.824l170.667-170.667c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-97.835 97.835h238.336c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667z" />
|
||||
<glyph unicode="" glyph-name="feedback" d="M979.755 841.856c1.835 6.443 2.133 13.397 0.64 20.309-1.024 4.821-2.901 9.557-5.589 13.867-2.731 4.352-6.187 8.107-10.155 11.179-4.992 3.84-10.624 6.4-16.469 7.723s-12.032 1.451-18.176 0.171c-1.792-0.384-3.627-0.896-5.376-1.493l-0.896-0.299-852.48-298.368c-10.752-3.755-19.925-11.776-24.917-22.955-9.557-21.547 0.128-46.763 21.675-56.32l369.024-164.011 164.011-369.024c4.608-10.368 13.355-18.901 24.875-22.955 22.229-7.765 46.592 3.925 54.357 26.197l298.368 852.437c0.427 1.152 0.811 2.304 1.152 3.499zM459.904 434.902l-258.901 115.029 575.275 201.387zM836.651 690.944l-201.387-575.275-115.029 258.901z" />
|
||||
<glyph unicode="" glyph-name="page" d="M597.333 896h-341.333c-35.328 0-67.413-14.379-90.496-37.504s-37.504-55.168-37.504-90.496v-682.667c0-35.328 14.379-67.413 37.504-90.496s55.168-37.504 90.496-37.504h512c35.328 0 67.413 14.379 90.496 37.504s37.504 55.168 37.504 90.496v512c0 11.776-4.779 22.443-12.501 30.165l-256 256c-4.096 4.096-8.789 7.168-13.824 9.259-5.205 2.176-10.795 3.243-16.341 3.243zM750.336 640h-110.336v110.336zM554.667 810.667v-213.333c0-23.552 19.115-42.667 42.667-42.667h213.333v-469.333c0-11.776-4.736-22.4-12.501-30.165s-18.389-12.501-30.165-12.501h-512c-11.776 0-22.4 4.736-30.165 12.501s-12.501 18.389-12.501 30.165v682.667c0 11.776 4.736 22.4 12.501 30.165s18.389 12.501 30.165 12.501zM682.667 426.667h-341.333c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h341.333c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM682.667 256h-341.333c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h341.333c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM426.667 597.334h-85.333c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h85.333c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667z" />
|
||||
<glyph unicode="" glyph-name="component" d="M618.965 537.387l-106.965-61.867-297.003 171.819 107.136 61.227zM809.003 647.339l-104.789-60.629-296.277 170.88 82.517 47.147c4.779 2.731 9.899 4.48 15.147 5.333 9.301 1.451 18.987-0.128 27.904-5.291zM491.776-41.002c6.016-3.243 12.928-5.077 20.224-5.077 7.381 0 14.336 1.877 20.395 5.163 15.189 2.475 29.909 7.68 43.392 15.36l298.709 170.709c26.368 15.232 45.269 38.315 55.424 64.597 5.675 14.592 8.619 30.165 8.747 46.251v341.333c0 20.395-4.821 39.723-13.397 56.917-0.939 3.029-2.219 5.973-3.883 8.832-1.963 3.371-4.267 6.357-6.912 8.96-1.323 1.835-2.731 3.669-4.139 5.419-9.813 12.203-21.845 22.528-35.456 30.507l-299.051 170.88c-26.027 15.019-55.467 19.84-83.328 15.531-15.531-2.432-30.507-7.637-44.288-15.488l-136.491-77.995c-8.96-1.749-17.323-6.4-23.595-13.483l-138.624-79.232c-16.341-9.429-29.824-21.888-40.149-36.267-2.56-2.56-4.864-5.547-6.784-8.832-1.664-2.901-2.987-5.888-3.925-8.96-1.707-3.456-3.243-6.955-4.608-10.496-5.632-14.635-8.576-30.208-8.704-45.995v-341.632c0.043-30.293 10.581-58.197 28.331-80.128 9.813-12.203 21.845-22.528 35.456-30.507l299.051-170.88c13.824-7.979 28.587-13.099 43.605-15.445zM469.333 401.622v-340.949l-277.12 158.336c-4.736 2.773-8.832 6.315-12.16 10.411-5.931 7.381-9.387 16.512-9.387 26.581v318.379zM554.667 60.672v340.949l298.667 172.757v-318.379c-0.043-5.163-1.067-10.496-2.987-15.445-3.413-8.789-9.6-16.384-18.176-21.333z" />
|
||||
<glyph unicode="" glyph-name="reports" d="M725.333 640h153.003l-302.336-302.336-183.168 183.168c-16.683 16.683-43.691 16.683-60.331 0l-320-320c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l289.835 289.835 183.168-183.168c16.683-16.683 43.691-16.683 60.331 0l332.501 332.501v-153.003c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 5.803-1.152 11.307-3.243 16.341s-5.163 9.728-9.216 13.781c-0.043 0.043-0.043 0.043-0.085 0.085-3.925 3.925-8.619 7.083-13.781 9.216-5.035 2.091-10.539 3.243-16.341 3.243h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
||||
<glyph unicode="" glyph-name="shipping" d="M640 298.667h-554.667v469.333h554.667v-170.667zM725.333 554.667h110.336l102.997-102.997v-153.003h-213.333zM298.667 149.334c0-17.664-7.125-33.621-18.731-45.269s-27.605-18.731-45.269-18.731-33.621 7.125-45.269 18.731-18.731 27.605-18.731 45.269 7.125 33.621 18.731 45.269 27.605 18.731 45.269 18.731 33.621-7.125 45.269-18.731 18.731-27.605 18.731-45.269zM938.667 149.334c0 22.912-5.163 44.587-14.379 64h57.045c23.552 0 42.667 19.115 42.667 42.667v213.333c0 10.923-4.181 21.845-12.501 30.165l-128 128c-7.723 7.723-18.389 12.501-30.165 12.501h-128v170.667c0 23.552-19.115 42.667-42.667 42.667h-640c-23.552 0-42.667-19.115-42.667-42.667v-554.667c0-23.552 19.115-42.667 42.667-42.667h57.045c-9.216-19.413-14.379-41.088-14.379-64 0-41.216 16.768-78.635 43.733-105.6s64.384-43.733 105.6-43.733 78.635 16.768 105.6 43.733 43.733 64.384 43.733 105.6c0 22.912-5.163 44.587-14.379 64h284.757c-9.216-19.413-14.379-41.088-14.379-64 0-41.216 16.768-78.635 43.733-105.6s64.384-43.733 105.6-43.733 78.635 16.768 105.6 43.733 43.733 64.384 43.733 105.6zM853.333 149.334c0-17.664-7.125-33.621-18.731-45.269s-27.605-18.731-45.269-18.731-33.621 7.125-45.269 18.731-18.731 27.605-18.731 45.269 7.125 33.621 18.731 45.269 27.605 18.731 45.269 18.731 33.621-7.125 45.269-18.731 18.731-27.605 18.731-45.269z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 27 KiB |
29
client/src/Project/Board/Header/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CopyLinkButton } from 'shared/components';
|
||||
import { Breadcrumbs, Divider, Header, BoardName } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
projectName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardHeader = ({ projectName }) => (
|
||||
<>
|
||||
<Breadcrumbs>
|
||||
Projects
|
||||
<Divider>/</Divider>
|
||||
{projectName}
|
||||
<Divider>/</Divider>
|
||||
Kanban Board
|
||||
</Breadcrumbs>
|
||||
<Header>
|
||||
<BoardName>Kanban board</BoardName>
|
||||
<CopyLinkButton />
|
||||
</Header>
|
||||
</>
|
||||
);
|
||||
|
||||
ProjectBoardHeader.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardHeader;
|
||||
@@ -0,0 +1,27 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
|
||||
export const Tip = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
color: ${color.textMedium};
|
||||
${font.size(13)}
|
||||
strong {
|
||||
padding-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TipLetter = styled.span`
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: inline-block;
|
||||
margin: 0 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
color: ${color.textDarkest};
|
||||
background: ${color.backgroundMedium};
|
||||
${font.bold}
|
||||
${font.size(12)}
|
||||
`;
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { KeyCodes } from 'shared/constants/keyCodes';
|
||||
import { isFocusedElementEditable } from 'shared/utils/dom';
|
||||
import { Tip, TipLetter } from './Style';
|
||||
|
||||
const propTypes = {
|
||||
setFormOpen: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = event => {
|
||||
if (!isFocusedElementEditable() && event.keyCode === KeyCodes.M) {
|
||||
event.preventDefault();
|
||||
setFormOpen(true);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [setFormOpen]);
|
||||
|
||||
return (
|
||||
<Tip>
|
||||
<strong>Pro tip:</strong>press<TipLetter>M</TipLetter>to comment
|
||||
</Tip>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectBoardIssueDetailsCommentsCreateProTip.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsCommentsCreateProTip;
|
||||
@@ -20,7 +20,7 @@ export const Right = styled.div`
|
||||
`;
|
||||
|
||||
export const FakeTextarea = styled.div`
|
||||
padding: 12px 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${color.borderLightest};
|
||||
color: ${color.textLight};
|
||||
@@ -29,27 +29,3 @@ export const FakeTextarea = styled.div`
|
||||
border: 1px solid ${color.borderLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tip = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
color: ${color.textMedium};
|
||||
${font.size(13)}
|
||||
strong {
|
||||
padding-right: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TipLetter = styled.span`
|
||||
position: relative;
|
||||
top: 1px;
|
||||
display: inline-block;
|
||||
margin: 0 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
color: ${color.textDarkest};
|
||||
background: ${color.backgroundMedium};
|
||||
${font.bold}
|
||||
${font.size(12)}
|
||||
`;
|
||||
@@ -5,7 +5,8 @@ import api from 'shared/utils/api';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import BodyForm from '../BodyForm';
|
||||
import { Create, UserAvatar, Right, FakeTextarea, Tip, TipLetter } from './Style';
|
||||
import ProTip from './ProTip';
|
||||
import { Create, UserAvatar, Right, FakeTextarea } from './Style';
|
||||
|
||||
const propTypes = {
|
||||
issueId: PropTypes.number.isRequired,
|
||||
@@ -32,6 +33,7 @@ const ProjectBoardIssueDetailsCommentsCreate = ({ issueId, fetchIssue }) => {
|
||||
toast.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Create>
|
||||
{currentUser && <UserAvatar name={currentUser.name} avatarUrl={currentUser.avatarUrl} />}
|
||||
@@ -47,9 +49,7 @@ const ProjectBoardIssueDetailsCommentsCreate = ({ issueId, fetchIssue }) => {
|
||||
) : (
|
||||
<>
|
||||
<FakeTextarea onClick={() => setFormOpen(true)}>Add a comment...</FakeTextarea>
|
||||
<Tip>
|
||||
<strong>Pro tip:</strong>press<TipLetter>M</TipLetter>to comment
|
||||
</Tip>
|
||||
<ProTip setFormOpen={setFormOpen} />
|
||||
</>
|
||||
)}
|
||||
</Right>
|
||||
@@ -6,7 +6,7 @@ import Comment from './Comment';
|
||||
import { Comments, Title } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.array.isRequired,
|
||||
issue: PropTypes.object.isRequired,
|
||||
fetchIssue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
12
client/src/Project/Board/IssueDetails/Dates/Styles.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
|
||||
export const Dates = styled.div`
|
||||
margin-top: 11px;
|
||||
padding-top: 13px;
|
||||
line-height: 22px;
|
||||
border-top: 1px solid ${color.borderLightest};
|
||||
color: ${color.textMedium};
|
||||
${font.size(13)}
|
||||
`;
|
||||
20
client/src/Project/Board/IssueDetails/Dates/index.jsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatDateTimeConversational } from 'shared/utils/dateTime';
|
||||
import { Dates } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsDates = ({ issue }) => (
|
||||
<Dates>
|
||||
<div>Created at {formatDateTimeConversational(issue.createdAt)}</div>
|
||||
<div>Updated at {formatDateTimeConversational(issue.updatedAt)}</div>
|
||||
</Dates>
|
||||
);
|
||||
|
||||
ProjectBoardIssueDetailsDates.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsDates;
|
||||
37
client/src/Project/Board/IssueDetails/Delete.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import api from 'shared/utils/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import { Button, ConfirmModal } from 'shared/components';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
fetchProject: PropTypes.func.isRequired,
|
||||
modalClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) => {
|
||||
const handleIssueDelete = async () => {
|
||||
try {
|
||||
await api.delete(`/issues/${issue.id}`);
|
||||
await fetchProject();
|
||||
modalClose();
|
||||
} catch (error) {
|
||||
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} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectBoardIssueDetailsDelete.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsDelete;
|
||||
@@ -14,9 +14,6 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
const $editorRef = useRef();
|
||||
const [isPresenting, setPresenting] = useState(true);
|
||||
|
||||
const isDescriptionEmpty = description =>
|
||||
getTextContentsFromHtmlString(description).trim().length === 0;
|
||||
|
||||
const renderPresentingMode = () =>
|
||||
isDescriptionEmpty(issue.description) ? (
|
||||
<EmptyLabel onClick={() => setPresenting(false)}>Add a description...</EmptyLabel>
|
||||
@@ -55,6 +52,9 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isDescriptionEmpty = description =>
|
||||
getTextContentsFromHtmlString(description).trim().length === 0;
|
||||
|
||||
ProjectBoardIssueDetailsDescription.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsDescription;
|
||||
23
client/src/Project/Board/IssueDetails/Feedback/Styles.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { font } from 'shared/utils/styles';
|
||||
|
||||
export const FeedbackDropdown = styled.div`
|
||||
padding: 16px 24px 24px;
|
||||
`;
|
||||
|
||||
export const FeedbackImageCont = styled.div`
|
||||
padding: 24px 56px 20px;
|
||||
`;
|
||||
|
||||
export const FeedbackImage = styled.img`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FeedbackParagraph = styled.p`
|
||||
margin-bottom: 12px;
|
||||
${font.size(15)}
|
||||
&:last-of-type {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
`;
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
44
client/src/Project/Board/IssueDetails/Feedback/index.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Button, Tooltip } from 'shared/components';
|
||||
import feedbackImage from './assets/feedback.png';
|
||||
import { FeedbackDropdown, FeedbackImageCont, FeedbackImage, FeedbackParagraph } from './Styles';
|
||||
|
||||
const ProjectBoardIssueDetailsFeedback = () => (
|
||||
<Tooltip
|
||||
width={300}
|
||||
offset={{ top: -15 }}
|
||||
renderLink={linkProps => (
|
||||
<Button icon="feedback" color="empty" {...linkProps}>
|
||||
Give feedback
|
||||
</Button>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<FeedbackDropdown>
|
||||
<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>
|
||||
</a>
|
||||
<a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener">
|
||||
<Button style={{ marginLeft: 10 }} icon="github">
|
||||
Github Repo
|
||||
</Button>
|
||||
</a>
|
||||
</FeedbackDropdown>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
export default ProjectBoardIssueDetailsFeedback;
|
||||
24
client/src/Project/Board/IssueDetails/Priority/Styles.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { color, font } from 'shared/utils/styles';
|
||||
|
||||
export const Priority = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${props =>
|
||||
props.isValue &&
|
||||
css`
|
||||
padding: 3px 4px 3px 0px;
|
||||
border-radius: 4px;
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const Label = styled.div`
|
||||
text-transform: capitalize;
|
||||
padding: 0 3px 0 8px;
|
||||
${font.size(14.5)}
|
||||
`;
|
||||
@@ -4,7 +4,7 @@ import { invert } from 'lodash';
|
||||
|
||||
import { IssuePriority } from 'shared/constants/issues';
|
||||
import { Select, IssuePriorityIcon } from 'shared/components';
|
||||
import { Priority, Option, Label } from './Styles';
|
||||
import { Priority, Label } from './Styles';
|
||||
import { SectionTitle } from '../Styles';
|
||||
|
||||
const IssuePriorityCopy = invert(IssuePriority);
|
||||
@@ -15,8 +15,8 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => {
|
||||
const renderPriorityItem = priority => (
|
||||
<Priority color={priority}>
|
||||
const renderPriorityItem = (priority, isValue) => (
|
||||
<Priority isValue={isValue}>
|
||||
<IssuePriorityIcon priority={priority} />
|
||||
<Label>{IssuePriorityCopy[priority].toLowerCase()}</Label>
|
||||
</Priority>
|
||||
@@ -25,16 +25,15 @@ const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => {
|
||||
<>
|
||||
<SectionTitle>Priority</SectionTitle>
|
||||
<Select
|
||||
dropdownWidth={343}
|
||||
value={issue.priority}
|
||||
options={Object.values(IssuePriority).map(priority => ({
|
||||
value: priority,
|
||||
label: IssuePriorityCopy[priority],
|
||||
}))}
|
||||
onChange={priority => updateIssue({ priority })}
|
||||
renderValue={({ value }) => renderPriorityItem(value)}
|
||||
renderOption={({ value, ...optionProps }) => (
|
||||
<Option {...optionProps}>{renderPriorityItem(value)}</Option>
|
||||
)}
|
||||
renderValue={({ value }) => renderPriorityItem(value, true)}
|
||||
renderOption={({ value }) => renderPriorityItem(value)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
18
client/src/Project/Board/IssueDetails/Status/Styles.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { issueStatusColors, issueStatusBackgroundColors, mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Status = styled.div`
|
||||
text-transform: uppercase;
|
||||
transition: all 0.1s;
|
||||
${props => mixin.tag(issueStatusBackgroundColors[props.color], issueStatusColors[props.color])}
|
||||
${props =>
|
||||
props.isValue &&
|
||||
css`
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
`}
|
||||
`;
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
|
||||
import { Select } from 'shared/components';
|
||||
import { Status, Option } from './Styles';
|
||||
import { Select, Icon } from 'shared/components';
|
||||
import { Status } from './Styles';
|
||||
import { SectionTitle } from '../Styles';
|
||||
|
||||
const propTypes = {
|
||||
@@ -15,6 +15,7 @@ const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
|
||||
<>
|
||||
<SectionTitle>Status</SectionTitle>
|
||||
<Select
|
||||
dropdownWidth={343}
|
||||
value={issue.status}
|
||||
options={Object.values(IssueStatus).map(status => ({
|
||||
value: status,
|
||||
@@ -22,14 +23,13 @@ const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
|
||||
}))}
|
||||
onChange={status => updateIssue({ status })}
|
||||
renderValue={({ value: status }) => (
|
||||
<Status isLarge color={status}>
|
||||
{IssueStatusCopy[status]}
|
||||
<Status isValue color={status}>
|
||||
<div>{IssueStatusCopy[status]}</div>
|
||||
<Icon type="chevron-down" size={18} />
|
||||
</Status>
|
||||
)}
|
||||
renderOption={({ value: status, ...optionProps }) => (
|
||||
<Option {...optionProps}>
|
||||
<Status color={status}>{IssueStatusCopy[status]}</Status>
|
||||
</Option>
|
||||
renderOption={({ value: status }) => (
|
||||
<Status color={status}>{IssueStatusCopy[status]}</Status>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
@@ -17,6 +17,20 @@ export const Right = styled.div`
|
||||
padding-top: 5px;
|
||||
`;
|
||||
|
||||
export const TopActions = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 21px 18px 0;
|
||||
`;
|
||||
|
||||
export const TopActionsRight = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SectionTitle = styled.div`
|
||||
margin: 24px 0 5px;
|
||||
text-transform: uppercase;
|
||||
@@ -3,12 +3,20 @@ import styled from 'styled-components';
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { Icon } from 'shared/components';
|
||||
|
||||
export const TrackingLink = styled.div`
|
||||
padding: 4px 4px 2px 0;
|
||||
border-radius: 4px;
|
||||
transition: background 0.1s;
|
||||
${mixin.clickable}
|
||||
&:hover {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Tracking = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 2px;
|
||||
${mixin.clickable};
|
||||
`;
|
||||
|
||||
export const WatchIcon = styled(Icon)`
|
||||
@@ -22,7 +30,7 @@ export const Right = styled.div`
|
||||
export const BarCont = styled.div`
|
||||
height: 5px;
|
||||
border-radius: 4px;
|
||||
background: ${color.backgroundLight};
|
||||
background: ${color.backgroundMedium};
|
||||
`;
|
||||
|
||||
export const Bar = styled.div`
|
||||
@@ -41,7 +49,7 @@ export const Values = styled.div`
|
||||
`;
|
||||
|
||||
export const ModalContents = styled.div`
|
||||
padding: 20px;
|
||||
padding: 20px 25px 25px;
|
||||
`;
|
||||
|
||||
export const ModalTitle = styled.div`
|
||||
@@ -4,6 +4,7 @@ import { isNil } from 'lodash';
|
||||
|
||||
import { InputDebounced, Modal, Button } from 'shared/components';
|
||||
import {
|
||||
TrackingLink,
|
||||
Tracking,
|
||||
WatchIcon,
|
||||
Right,
|
||||
@@ -31,7 +32,7 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
|
||||
filter={/^\d{0,6}$/}
|
||||
value={isNil(issue[fieldName]) ? '' : issue[fieldName]}
|
||||
onChange={stringValue => {
|
||||
const value = stringValue.trim() ? parseInt(stringValue) : null;
|
||||
const value = stringValue.trim() ? Number(stringValue) : null;
|
||||
updateIssue({ [fieldName]: value });
|
||||
}}
|
||||
/>
|
||||
@@ -95,7 +96,7 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
|
||||
<SectionTitle>Time Tracking</SectionTitle>
|
||||
<Modal
|
||||
width={400}
|
||||
renderLink={modal => renderTrackingPreview(modal.open)}
|
||||
renderLink={modal => <TrackingLink>{renderTrackingPreview(modal.open)}</TrackingLink>}
|
||||
renderContent={modal => (
|
||||
<ModalContents>
|
||||
<ModalTitle>Time tracking</ModalTitle>
|
||||
@@ -3,12 +3,6 @@ import styled from 'styled-components';
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
import { Button } from 'shared/components';
|
||||
|
||||
export const TopActions = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 21px 18px 0;
|
||||
`;
|
||||
|
||||
export const TypeButton = styled(Button)`
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -16,14 +10,6 @@ export const TypeButton = styled(Button)`
|
||||
${font.size(13)}
|
||||
`;
|
||||
|
||||
export const Right = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
& > * {
|
||||
margin-left: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TypeDropdown = styled.div`
|
||||
padding-bottom: 6px;
|
||||
`;
|
||||
@@ -50,23 +36,3 @@ export const TypeLabel = styled.div`
|
||||
text-transform: capitalize;
|
||||
${font.size(15)}
|
||||
`;
|
||||
|
||||
export const FeedbackDropdown = styled.div`
|
||||
padding: 16px 24px 24px;
|
||||
`;
|
||||
|
||||
export const FeedbackImageCont = styled.div`
|
||||
padding: 24px 56px 20px;
|
||||
`;
|
||||
|
||||
export const FeedbackImage = styled.img`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const FeedbackParagraph = styled.p`
|
||||
margin-bottom: 12px;
|
||||
${font.size(15)}
|
||||
&:last-of-type {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
`;
|
||||
38
client/src/Project/Board/IssueDetails/Type/index.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
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;
|
||||
@@ -13,6 +13,10 @@ export const User = styled.div`
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
background: ${color.backgroundLight};
|
||||
transition: background 0.1s;
|
||||
&:hover {
|
||||
background: ${color.backgroundMedium};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
@@ -20,11 +24,3 @@ export const Username = styled.div`
|
||||
padding: 0 3px 0 8px;
|
||||
${font.size(14.5)}
|
||||
`;
|
||||
|
||||
export const Option = styled.div`
|
||||
padding: 8px 12px 5px;
|
||||
${mixin.clickable}
|
||||
&.jira-select-option-is-active {
|
||||
background: ${color.backgroundLightPrimary};
|
||||
}
|
||||
`;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Avatar, Select, Icon } from 'shared/components';
|
||||
import { User, Username, Option } from './Styles';
|
||||
import { User, Username } from './Styles';
|
||||
import { SectionTitle } from '../Styles';
|
||||
|
||||
const propTypes = {
|
||||
@@ -12,7 +12,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) => {
|
||||
const getUserById = userId => projectUsers.find(user => user.id === parseInt(userId));
|
||||
const getUserById = userId => projectUsers.find(user => user.id === userId);
|
||||
|
||||
const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name }));
|
||||
|
||||
@@ -41,6 +41,7 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
||||
<SectionTitle>Assignees</SectionTitle>
|
||||
<Select
|
||||
isMulti
|
||||
dropdownWidth={343}
|
||||
placeholder="Unassigned"
|
||||
value={issue.userIds}
|
||||
options={userOptions}
|
||||
@@ -50,9 +51,7 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
||||
renderValue={({ value, removeOptionValue }) =>
|
||||
renderUserValue(getUserById(value), true, removeOptionValue)
|
||||
}
|
||||
renderOption={({ value, ...optionProps }) => (
|
||||
<Option {...optionProps}>{renderUserOption(getUserById(value))}</Option>
|
||||
)}
|
||||
renderOption={({ value }) => renderUserOption(getUserById(value))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -61,13 +60,12 @@ const ProjectBoardIssueDetailsUsers = ({ issue, updateIssue, projectUsers }) =>
|
||||
<>
|
||||
<SectionTitle>Reporter</SectionTitle>
|
||||
<Select
|
||||
dropdownWidth={343}
|
||||
value={issue.reporterId}
|
||||
options={userOptions}
|
||||
onChange={userId => updateIssue({ reporterId: userId })}
|
||||
renderValue={({ value }) => renderUserValue(getUserById(value), false)}
|
||||
renderOption={({ value, ...optionProps }) => (
|
||||
<Option {...optionProps}>{renderUserOption(getUserById(value))}</Option>
|
||||
)}
|
||||
renderOption={({ value }) => renderUserOption(getUserById(value))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -3,9 +3,11 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import api from 'shared/utils/api';
|
||||
import useApi from 'shared/hooks/api';
|
||||
import { PageError } from 'shared/components';
|
||||
import { PageError, CopyLinkButton, Button } from 'shared/components';
|
||||
import Loader from './Loader';
|
||||
import TopActions from './TopActions';
|
||||
import Type from './Type';
|
||||
import Feedback from './Feedback';
|
||||
import Delete from './Delete';
|
||||
import Title from './Title';
|
||||
import Description from './Description';
|
||||
import Comments from './Comments';
|
||||
@@ -13,7 +15,8 @@ import Status from './Status';
|
||||
import Users from './Users';
|
||||
import Priority from './Priority';
|
||||
import Tracking from './Tracking';
|
||||
import { Content, Left, Right } from './Styles';
|
||||
import Dates from './Dates';
|
||||
import { TopActions, TopActionsRight, Content, Left, Right } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issueId: PropTypes.string.isRequired,
|
||||
@@ -54,12 +57,15 @@ const ProjectBoardIssueDetails = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<TopActions
|
||||
issue={issue}
|
||||
updateIssue={updateIssue}
|
||||
fetchProject={fetchProject}
|
||||
modalClose={modalClose}
|
||||
/>
|
||||
<TopActions>
|
||||
<Type issue={issue} updateIssue={updateIssue} />
|
||||
<TopActionsRight>
|
||||
<Feedback />
|
||||
<CopyLinkButton color="empty" />
|
||||
<Delete issue={issue} fetchProject={fetchProject} modalClose={modalClose} />
|
||||
<Button icon="close" iconSize={24} color="empty" onClick={modalClose} />
|
||||
</TopActionsRight>
|
||||
</TopActions>
|
||||
<Content>
|
||||
<Left>
|
||||
<Title issue={issue} updateIssue={updateIssue} />
|
||||
@@ -71,6 +77,7 @@ const ProjectBoardIssueDetails = ({
|
||||
<Users issue={issue} updateIssue={updateIssue} projectUsers={projectUsers} />
|
||||
<Priority issue={issue} updateIssue={updateIssue} />
|
||||
<Tracking issue={issue} updateIssue={updateIssue} />
|
||||
<Dates issue={issue} />
|
||||
</Right>
|
||||
</Content>
|
||||
</>
|
||||
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
|
||||
import { Avatar } from 'shared/components';
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
|
||||
export const IssueWrapper = styled(Link)`
|
||||
export const IssueLink = styled(Link)`
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
@@ -4,7 +4,7 @@ import { useRouteMatch } from 'react-router-dom';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components';
|
||||
import { IssueWrapper, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles';
|
||||
import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
projectUsers: PropTypes.array.isRequired,
|
||||
@@ -21,7 +21,7 @@ const ProjectBoardListsIssue = ({ projectUsers, issue, index }) => {
|
||||
return (
|
||||
<Draggable draggableId={issue.id.toString()} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<IssueWrapper
|
||||
<IssueLink
|
||||
to={`${match.url}/${issue.id}`}
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
@@ -46,7 +46,7 @@ const ProjectBoardListsIssue = ({ projectUsers, issue, index }) => {
|
||||
</Assignees>
|
||||
</Bottom>
|
||||
</Issue>
|
||||
</IssueWrapper>
|
||||
</IssueLink>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
@@ -29,7 +29,7 @@ const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
|
||||
const isSamePosition = destination.index === source.index;
|
||||
if (isSameList && isSamePosition) return;
|
||||
|
||||
const issueId = parseInt(draggableId);
|
||||
const issueId = Number(draggableId);
|
||||
|
||||
api.optimisticUpdate({
|
||||
url: `/issues/${issueId}`,
|
||||
93
client/src/Project/Sidebar/Styles.js
Normal file
@@ -0,0 +1,93 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles';
|
||||
|
||||
export const Sidebar = styled.div`
|
||||
position: absolute;
|
||||
z-index: ${zIndexValues.navLeft - 1};
|
||||
top: 0;
|
||||
left: ${sizes.appNavBarLeftWidth}px;
|
||||
height: 100vh;
|
||||
width: 240px;
|
||||
padding: 0 16px;
|
||||
background: ${color.backgroundLightest};
|
||||
border-right: 1px solid ${color.borderLightest};
|
||||
`;
|
||||
|
||||
export const ProjectInfo = styled.div`
|
||||
display: flex;
|
||||
padding: 24px 4px;
|
||||
`;
|
||||
|
||||
export const ProjectTexts = styled.div`
|
||||
padding: 3px 0 0 10px;
|
||||
`;
|
||||
|
||||
export const ProjectName = styled.div`
|
||||
color: ${color.textDark};
|
||||
${font.size(15)};
|
||||
${font.medium};
|
||||
`;
|
||||
|
||||
export const ProjectCategory = styled.div`
|
||||
color: ${color.textMedium};
|
||||
${font.size(13)};
|
||||
`;
|
||||
|
||||
export const Divider = styled.div`
|
||||
margin-top: 17px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px solid ${color.borderLight};
|
||||
`;
|
||||
|
||||
export const LinkItem = styled(NavLink)`
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
border-radius: 3px;
|
||||
color: ${color.textDark};
|
||||
${mixin.clickable}
|
||||
${props =>
|
||||
!props.implemented
|
||||
? `cursor: not-allowed;`
|
||||
: css`
|
||||
&:hover {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
`}
|
||||
i {
|
||||
margin-right: 15px;
|
||||
font-size: 20px;
|
||||
color: ${color.textDarkest};
|
||||
}
|
||||
&.active {
|
||||
color: ${color.primary};
|
||||
background: ${color.backgroundLight};
|
||||
i {
|
||||
color: ${color.primary};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LinkText = styled.div`
|
||||
padding-top: 2px;
|
||||
${font.size(14.7)};
|
||||
`;
|
||||
|
||||
export const NotImplemented = styled.div`
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
left: 104%;
|
||||
width: 120px;
|
||||
padding: 3px 0 3px 8px;
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
background: #000;
|
||||
${font.size(12.5)};
|
||||
${font.medium}
|
||||
${LinkItem}:hover & {
|
||||
display: inline-block;
|
||||
}
|
||||
`;
|
||||
53
client/src/Project/Sidebar/index.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Icon, ProjectAvatar } from 'shared/components';
|
||||
import {
|
||||
Sidebar,
|
||||
ProjectInfo,
|
||||
ProjectTexts,
|
||||
ProjectName,
|
||||
ProjectCategory,
|
||||
Divider,
|
||||
LinkItem,
|
||||
LinkText,
|
||||
NotImplemented,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
projectName: PropTypes.string.isRequired,
|
||||
matchPath: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const ProjectSidebar = ({ projectName, matchPath }) => {
|
||||
const renderLinkItem = (text, iconType, path = '') => (
|
||||
<LinkItem exact to={`${matchPath}${path}`} implemented={path}>
|
||||
<Icon type={iconType} />
|
||||
<LinkText>{text}</LinkText>
|
||||
{!path && <NotImplemented>Not implemented</NotImplemented>}
|
||||
</LinkItem>
|
||||
);
|
||||
return (
|
||||
<Sidebar>
|
||||
<ProjectInfo>
|
||||
<ProjectAvatar />
|
||||
<ProjectTexts>
|
||||
<ProjectName>{projectName}</ProjectName>
|
||||
<ProjectCategory>Software project</ProjectCategory>
|
||||
</ProjectTexts>
|
||||
</ProjectInfo>
|
||||
{renderLinkItem('Kanban Board', 'board', '/board')}
|
||||
{renderLinkItem('Reports', 'reports')}
|
||||
<Divider />
|
||||
{renderLinkItem('Releases', 'shipping')}
|
||||
{renderLinkItem('Issues and filters', 'issues')}
|
||||
{renderLinkItem('Pages', 'page')}
|
||||
{renderLinkItem('Components', 'component')}
|
||||
{renderLinkItem('Project settings', 'settings')}
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectSidebar.propTypes = propTypes;
|
||||
|
||||
export default ProjectSidebar;
|
||||
@@ -1,41 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { copyToClipboard } from 'shared/utils/clipboard';
|
||||
import { Button } from 'shared/components';
|
||||
import { Breadcrumbs, Divider, Header, BoardName } from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
projectName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardHeader = ({ projectName }) => {
|
||||
const [isLinkCopied, setLinkCopied] = useState(false);
|
||||
|
||||
const handleLinkCopy = () => {
|
||||
setLinkCopied(true);
|
||||
setTimeout(() => setLinkCopied(false), 2000);
|
||||
copyToClipboard(window.location.href);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs>
|
||||
Projects
|
||||
<Divider>/</Divider>
|
||||
{projectName}
|
||||
<Divider>/</Divider>
|
||||
Kanban Board
|
||||
</Breadcrumbs>
|
||||
<Header>
|
||||
<BoardName>Kanban board</BoardName>
|
||||
<Button icon="link" onClick={handleLinkCopy}>
|
||||
{isLinkCopied ? 'Link Copied' : 'Copy link'}
|
||||
</Button>
|
||||
</Header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectBoardHeader.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardHeader;
|
||||
@@ -1,22 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, font, mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Priority = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Option = styled.div`
|
||||
padding: 8px 12px;
|
||||
${mixin.clickable}
|
||||
&.jira-select-option-is-active {
|
||||
background: ${color.backgroundLightPrimary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const Label = styled.div`
|
||||
text-transform: capitalize;
|
||||
padding: 0 3px 0 8px;
|
||||
${font.size(14.5)}
|
||||
`;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { color, issueStatusColors, issueStatusBackgroundColors, mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Status = styled.div`
|
||||
text-transform: uppercase;
|
||||
${props => mixin.tag(issueStatusBackgroundColors[props.color], issueStatusColors[props.color])}
|
||||
${props => props.isLarge && `padding: 9px 14px 8px;`}
|
||||
`;
|
||||
|
||||
export const Option = styled.div`
|
||||
padding: 8px 16px;
|
||||
&.jira-select-option-is-active {
|
||||
background: ${color.backgroundLightPrimary};
|
||||
}
|
||||
`;
|
||||
@@ -1,118 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import api from 'shared/utils/api';
|
||||
import toast from 'shared/utils/toast';
|
||||
import { IssueType } from 'shared/constants/issues';
|
||||
import { IssueTypeIcon, Button, CopyLinkButton, Tooltip, ConfirmModal } from 'shared/components';
|
||||
import feedbackImage from './assets/feedback.png';
|
||||
import {
|
||||
TopActions,
|
||||
TypeButton,
|
||||
Right,
|
||||
TypeDropdown,
|
||||
TypeTitle,
|
||||
Type,
|
||||
TypeLabel,
|
||||
FeedbackDropdown,
|
||||
FeedbackImageCont,
|
||||
FeedbackImage,
|
||||
FeedbackParagraph,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
issue: PropTypes.object.isRequired,
|
||||
updateIssue: PropTypes.func.isRequired,
|
||||
fetchProject: PropTypes.func.isRequired,
|
||||
modalClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const ProjectBoardIssueDetailsTopActions = ({ issue, updateIssue, fetchProject, modalClose }) => {
|
||||
const handleIssueDelete = async () => {
|
||||
try {
|
||||
await api.delete(`/issues/${issue.id}`);
|
||||
await fetchProject();
|
||||
modalClose();
|
||||
} catch (error) {
|
||||
toast.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderType = () => (
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderFeedback = () => (
|
||||
<Tooltip
|
||||
width={300}
|
||||
offset={{ top: -15 }}
|
||||
renderLink={linkProps => (
|
||||
<Button icon="feedback" color="empty" {...linkProps}>
|
||||
Give feedback
|
||||
</Button>
|
||||
)}
|
||||
renderContent={() => (
|
||||
<FeedbackDropdown>
|
||||
<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 <strong>ivor@codetree.co</strong>
|
||||
</FeedbackParagraph>
|
||||
<a href="https://codetree.co/" target="_blank" rel="noreferrer noopener">
|
||||
<Button color="primary">Visit Website</Button>
|
||||
</a>
|
||||
</FeedbackDropdown>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderDeleteIcon = () => (
|
||||
<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} />}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<TopActions>
|
||||
{renderType()}
|
||||
<Right>
|
||||
{renderFeedback()}
|
||||
<CopyLinkButton color="empty" />
|
||||
{renderDeleteIcon()}
|
||||
<Button icon="close" iconSize={24} color="empty" onClick={modalClose} />
|
||||
</Right>
|
||||
</TopActions>
|
||||
);
|
||||
};
|
||||
|
||||
ProjectBoardIssueDetailsTopActions.propTypes = propTypes;
|
||||
|
||||
export default ProjectBoardIssueDetailsTopActions;
|
||||
@@ -1,56 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { color, sizes, font, mixin } from 'shared/utils/styles';
|
||||
|
||||
export const Sidebar = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: ${sizes.appNavBarLeftWidth}px;
|
||||
height: 100vh;
|
||||
width: 240px;
|
||||
padding: 0 16px;
|
||||
background: ${color.backgroundLightest};
|
||||
border-right: 1px solid ${color.borderLightest};
|
||||
`;
|
||||
|
||||
export const ProjectInfo = styled.div`
|
||||
display: flex;
|
||||
padding: 24px 4px;
|
||||
`;
|
||||
|
||||
export const ProjectTexts = styled.div`
|
||||
padding: 3px 0 0 10px;
|
||||
`;
|
||||
|
||||
export const ProjectName = styled.div`
|
||||
color: ${color.textDark};
|
||||
${font.size(15)};
|
||||
${font.medium};
|
||||
`;
|
||||
|
||||
export const ProjectCategory = styled.div`
|
||||
color: ${color.textMedium};
|
||||
${font.size(13)};
|
||||
`;
|
||||
|
||||
export const LinkItem = styled(Link)`
|
||||
display: flex;
|
||||
padding: 8px 12px;
|
||||
border-radius: 3px;
|
||||
color: ${color.textDark};
|
||||
${mixin.clickable}
|
||||
&:hover {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
i {
|
||||
margin-right: 15px;
|
||||
font-size: 20px;
|
||||
color: ${color.textDarkest};
|
||||
}
|
||||
`;
|
||||
|
||||
export const LinkText = styled.div`
|
||||
padding-top: 2px;
|
||||
${font.size(15)};
|
||||
`;
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Icon, ProjectAvatar } from 'shared/components';
|
||||
import {
|
||||
Sidebar,
|
||||
ProjectInfo,
|
||||
ProjectTexts,
|
||||
ProjectName,
|
||||
ProjectCategory,
|
||||
LinkItem,
|
||||
LinkText,
|
||||
} from './Styles';
|
||||
|
||||
const propTypes = {
|
||||
projectName: PropTypes.string.isRequired,
|
||||
matchPath: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const ProjectSidebar = ({ projectName, matchPath }) => (
|
||||
<Sidebar>
|
||||
<ProjectInfo>
|
||||
<ProjectAvatar />
|
||||
<ProjectTexts>
|
||||
<ProjectName>{projectName}</ProjectName>
|
||||
<ProjectCategory>Software project</ProjectCategory>
|
||||
</ProjectTexts>
|
||||
</ProjectInfo>
|
||||
<LinkItem to={`${matchPath}/board`}>
|
||||
<Icon type="board" />
|
||||
<LinkText>Kanban Board</LinkText>
|
||||
</LinkItem>
|
||||
<LinkItem to={`${matchPath}/issues`}>
|
||||
<Icon type="issues" />
|
||||
<LinkText>Issues and filters</LinkText>
|
||||
</LinkItem>
|
||||
<LinkItem to={`${matchPath}/settings`}>
|
||||
<Icon type="settings" />
|
||||
<LinkText>Project settings</LinkText>
|
||||
</LinkItem>
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
ProjectSidebar.propTypes = propTypes;
|
||||
|
||||
export default ProjectSidebar;
|
||||
@@ -8,6 +8,7 @@ export const StyledButton = styled.button`
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
vertical-align: middle;
|
||||
line-height: 1;
|
||||
padding: 0 ${props => (props.iconOnly ? 9 : 12)}px;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -35,8 +35,9 @@ export const StyledInput = styled(Input)`
|
||||
|
||||
export const Actions = styled.div`
|
||||
display: flex;
|
||||
padding-top: 6px;
|
||||
`;
|
||||
|
||||
export const StyledButton = styled(Button)`
|
||||
margin: 6px 20px 0 0;
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
import { copyToClipboard } from 'shared/utils/clipboard';
|
||||
import { Button } from 'shared/components';
|
||||
|
||||
const CopyLinkButton = ({ ...otherProps }) => {
|
||||
const CopyLinkButton = ({ ...buttonProps }) => {
|
||||
const [isLinkCopied, setLinkCopied] = useState(false);
|
||||
|
||||
const handleLinkCopy = () => {
|
||||
@@ -12,7 +12,7 @@ const CopyLinkButton = ({ ...otherProps }) => {
|
||||
copyToClipboard(window.location.href);
|
||||
};
|
||||
return (
|
||||
<Button icon="link" onClick={handleLinkCopy} {...otherProps}>
|
||||
<Button icon="link" onClick={handleLinkCopy} {...buttonProps}>
|
||||
{isLinkCopied ? 'Link Copied' : 'Copy link'}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ const DatePickerDateSection = ({ withTime, value, onChange, setDropdownOpen }) =
|
||||
const [selectedMonth, setSelectedMonth] = useState(moment(value || undefined).startOf('month'));
|
||||
|
||||
const handleYearChange = year => {
|
||||
setSelectedMonth(moment(selectedMonth).set({ year: parseInt(year) }));
|
||||
setSelectedMonth(moment(selectedMonth).set({ year: Number(year) }));
|
||||
};
|
||||
|
||||
const handleMonthChange = addOrSubtract => {
|
||||
|
||||
@@ -39,8 +39,8 @@ const DatePickerTimeSection = ({ value, onChange, setDropdownOpen }) => {
|
||||
const existingDate = moment(value || undefined);
|
||||
|
||||
const existingDateWithNewTime = existingDate.set({
|
||||
hour: parseInt(newHour),
|
||||
minute: parseInt(newMinute),
|
||||
hour: Number(newHour),
|
||||
minute: Number(newMinute),
|
||||
});
|
||||
onChange(formatDateTimeForAPI(existingDateWithNewTime));
|
||||
setDropdownOpen(false);
|
||||
|
||||
@@ -28,6 +28,11 @@ const codes = {
|
||||
[`close`]: '\\e913',
|
||||
[`feedback`]: '\\e918',
|
||||
[`trash`]: '\\e912',
|
||||
[`github`]: '\\e915',
|
||||
[`shipping`]: '\\e91c',
|
||||
[`component`]: '\\e91a',
|
||||
[`reports`]: '\\e91b',
|
||||
[`page`]: '\\e919',
|
||||
};
|
||||
|
||||
const propTypes = {
|
||||
|
||||
@@ -14,8 +14,12 @@ export default styled.div`
|
||||
border-radius: 3px;
|
||||
border: 1px solid ${color.borderLightest};
|
||||
background: ${color.backgroundLightest};
|
||||
transition: background 0.1s;
|
||||
${font.regular}
|
||||
${font.size(15)}
|
||||
&:hover {
|
||||
background: ${color.backgroundLight};
|
||||
}
|
||||
&:focus {
|
||||
background: #fff;
|
||||
border: 1px solid ${color.borderInputFocus};
|
||||
|
||||
@@ -9,7 +9,7 @@ const propTypes = {
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
};
|
||||
|
||||
const InputDebounced = ({ onChange, value: propsValue, ...props }) => {
|
||||
const InputDebounced = ({ onChange, value: propsValue, ...inputProps }) => {
|
||||
const [value, setValue] = useState(propsValue);
|
||||
|
||||
const handleChange = useCallback(
|
||||
@@ -28,7 +28,7 @@ const InputDebounced = ({ onChange, value: propsValue, ...props }) => {
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
{...inputProps}
|
||||
value={value}
|
||||
onChange={newValue => {
|
||||
setValue(newValue);
|
||||
|
||||
@@ -9,7 +9,7 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const IssuePriorityIcon = ({ priority, ...otherProps }) => {
|
||||
const iconType = [IssuePriority.LOW || IssuePriority.LOWEST].includes(priority)
|
||||
const iconType = [IssuePriority.LOW, IssuePriority.LOWEST].includes(priority)
|
||||
? 'arrow-down'
|
||||
: 'arrow-up';
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const ScrollOverlay = styled.div`
|
||||
|
||||
export const ClickableOverlay = styled.div`
|
||||
min-height: 100%;
|
||||
background: ${mixin.rgba(color.textLight, 0.7)};
|
||||
background: rgba(9, 30, 66, 0.54);
|
||||
${props => clickOverlayStyles[props.variant]}
|
||||
`;
|
||||
|
||||
|
||||