Improved code styling

This commit is contained in:
ireic
2019-12-24 16:39:03 +01:00
parent 4941261251
commit 3c705a6084
81 changed files with 671 additions and 583 deletions

View File

@@ -1,8 +0,0 @@
import styled from 'styled-components';
import { sizes } from 'shared/utils/styles';
export const Main = styled.main`
position: relative;
width: 100%;
padding-left: ${sizes.appNavBarLeftWidth}px;
`;

View File

@@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { Router, Switch, Route, Redirect } from 'react-router-dom'; import { Router, Switch, Route, Redirect } from 'react-router-dom';
import history from 'browserHistory';
import PageError from 'shared/components/PageError'; import history from 'browserHistory';
import Project from 'Project'; import Project from 'Project';
import Authenticate from './Authenticate'; import Authenticate from 'Auth/Authenticate';
import PageError from 'shared/components/PageError';
const Routes = () => ( const Routes = () => (
<Router history={history}> <Router history={history}>

View File

@@ -1,6 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { color, font, mixin, zIndexValues } from 'shared/utils/styles'; import { color, font, mixin, zIndexValues } from 'shared/utils/styles';
import { Icon } from 'shared/components';
export const Container = styled.div` export const Container = styled.div`
z-index: ${zIndexValues.modal + 1}; z-index: ${zIndexValues.modal + 1};
@@ -33,15 +34,15 @@ export const StyledToast = styled.div`
opacity: 1; opacity: 1;
right: 0; right: 0;
} }
`;
i { export const CloseIcon = styled(Icon)`
position: absolute; position: absolute;
top: 13px; top: 13px;
right: 14px; right: 14px;
font-size: 22px; font-size: 22px;
cursor: pointer; cursor: pointer;
color: #fff; color: #fff;
}
`; `;
export const Title = styled.div` export const Title = styled.div`

View File

@@ -3,15 +3,14 @@ import { CSSTransition, TransitionGroup } from 'react-transition-group';
import pubsub from 'sweet-pubsub'; import pubsub from 'sweet-pubsub';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { Icon } from 'shared/components'; import { Container, StyledToast, CloseIcon, Title, Message } from './Styles';
import { Container, StyledToast, Title, Message } from './Styles';
const Toast = () => { const Toast = () => {
const [toasts, setToasts] = useState([]); const [toasts, setToasts] = useState([]);
useEffect(() => { useEffect(() => {
const addToast = ({ type = 'success', title, message, duration = 5 }) => { const addToast = ({ type = 'success', title, message, duration = 5 }) => {
const id = uniqueId(); const id = uniqueId('toast-');
setToasts(currentToasts => [...currentToasts, { id, type, title, message }]); setToasts(currentToasts => [...currentToasts, { id, type, title, message }]);
@@ -19,7 +18,9 @@ const Toast = () => {
setTimeout(() => removeToast(id), duration * 1000); setTimeout(() => removeToast(id), duration * 1000);
} }
}; };
pubsub.on('toast', addToast); pubsub.on('toast', addToast);
return () => { return () => {
pubsub.off('toast', addToast); pubsub.off('toast', addToast);
}; };
@@ -35,7 +36,7 @@ const Toast = () => {
{toasts.map(toast => ( {toasts.map(toast => (
<CSSTransition key={toast.id} classNames="jira-toast" timeout={200}> <CSSTransition key={toast.id} classNames="jira-toast" timeout={200}>
<StyledToast key={toast.id} type={toast.type} onClick={() => removeToast(toast.id)}> <StyledToast key={toast.id} type={toast.type} onClick={() => removeToast(toast.id)}>
<Icon type="close" /> <CloseIcon type="close" />
{toast.title && <Title>{toast.title}</Title>} {toast.title && <Title>{toast.title}</Title>}
{toast.message && <Message>{toast.message}</Message>} {toast.message && <Message>{toast.message}</Message>}
</StyledToast> </StyledToast>

View File

@@ -36,4 +36,7 @@
<glyph unicode="&#xe91a;" 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="&#xe91a;" 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="&#xe91b;" 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="&#xe91b;" 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="&#xe91c;" 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" /> <glyph unicode="&#xe91c;" 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" />
<glyph unicode="&#xe91d;" glyph-name="calendar" d="M298.667 853.334v-42.667h-85.333c-35.328 0-67.413-14.379-90.496-37.504s-37.504-55.168-37.504-90.496v-597.333c0-35.328 14.379-67.413 37.504-90.496s55.168-37.504 90.496-37.504h597.333c35.328 0 67.413 14.379 90.496 37.504s37.504 55.168 37.504 90.496v597.333c0 35.328-14.379 67.413-37.504 90.496s-55.168 37.504-90.496 37.504h-85.333v42.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667v-42.667h-256v42.667c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667zM853.333 554.667h-682.667v128c0 11.776 4.736 22.4 12.501 30.165s18.389 12.501 30.165 12.501h85.333v-42.667c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v42.667h256v-42.667c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v42.667h85.333c11.776 0 22.4-4.736 30.165-12.501s12.501-18.389 12.501-30.165zM170.667 469.334h682.667v-384c0-11.776-4.736-22.4-12.501-30.165s-18.389-12.501-30.165-12.501h-597.333c-11.776 0-22.4 4.736-30.165 12.501s-12.501 18.389-12.501 30.165z" />
<glyph unicode="&#xe91e;" glyph-name="arrow-left" d="M542.165 158.166l-225.835 225.835h494.336c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-494.336l225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-298.667-298.667c-4.096-4.096-7.168-8.789-9.259-13.824-2.176-5.205-3.243-10.795-3.243-16.341 0-10.923 4.181-21.845 12.501-30.165l298.667-298.667c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331z" />
<glyph unicode="&#xe91f;" glyph-name="arrow-right" d="M481.835 695.168l225.835-225.835h-494.336c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h494.336l-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l298.667 298.667c3.925 3.925 7.083 8.619 9.259 13.824 4.309 10.453 4.309 22.229 0 32.683-2.091 5.035-5.163 9.728-9.259 13.824l-298.667 298.667c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331z" />
</font></defs></svg> </font></defs></svg>

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

View File

@@ -5,7 +5,7 @@ import BaseStyles from './BaseStyles';
import Toast from './Toast'; import Toast from './Toast';
import Routes from './Routes'; import Routes from './Routes';
// We're importing css because @font-face in styled-components causes font files // We're importing .css because @font-face in styled-components causes font files
// to be constantly re-requested from the server (which causes screen flicker) // to be constantly re-requested from the server (which causes screen flicker)
// https://github.com/styled-components/styled-components/issues/1593 // https://github.com/styled-components/styled-components/issues/1593
import './fontStyles.css'; import './fontStyles.css';

View File

@@ -1,8 +1,8 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import toast from 'shared/utils/toast';
import api from 'shared/utils/api'; import api from 'shared/utils/api';
import toast from 'shared/utils/toast';
import { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken'; import { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken';
import { PageLoader } from 'shared/components'; import { PageLoader } from 'shared/components';
@@ -11,7 +11,6 @@ const Authenticate = () => {
useEffect(() => { useEffect(() => {
const createGuestAccount = async () => { const createGuestAccount = async () => {
if (!getStoredAuthToken()) {
try { try {
const { authToken } = await api.post('/authentication/guest'); const { authToken } = await api.post('/authentication/guest');
storeAuthToken(authToken); storeAuthToken(authToken);
@@ -19,9 +18,11 @@ const Authenticate = () => {
} catch (error) { } catch (error) {
toast.error(error); toast.error(error);
} }
}
}; };
if (!getStoredAuthToken()) {
createGuestAccount(); createGuestAccount();
}
}, [history]); }, [history]);
return <PageLoader />; return <PageLoader />;

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { InputDebounced, Avatar, Button } from 'shared/components';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font, mixin } from 'shared/utils/styles';
import { InputDebounced, Avatar, Button } from 'shared/components';
export const Filters = styled.div` export const Filters = styled.div`
display: flex; display: flex;

View File

@@ -16,14 +16,12 @@ const propTypes = {
projectUsers: PropTypes.array.isRequired, projectUsers: PropTypes.array.isRequired,
defaultFilters: PropTypes.object.isRequired, defaultFilters: PropTypes.object.isRequired,
filters: 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 { searchQuery, userIds, myOnly, recent } = filters;
const setFiltersMerge = newFilters => setFilters({ ...filters, ...newFilters });
const areFiltersCleared = !searchQuery && userIds.length === 0 && !myOnly && !recent; const areFiltersCleared = !searchQuery && userIds.length === 0 && !myOnly && !recent;
return ( return (
@@ -31,7 +29,7 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters
<SearchInput <SearchInput
icon="search" icon="search"
value={searchQuery} value={searchQuery}
onChange={value => setFiltersMerge({ searchQuery: value })} onChange={value => mergeFilters({ searchQuery: value })}
/> />
<Avatars> <Avatars>
{projectUsers.map(user => ( {projectUsers.map(user => (
@@ -39,27 +37,27 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters
<StyledAvatar <StyledAvatar
avatarUrl={user.avatarUrl} avatarUrl={user.avatarUrl}
name={user.name} name={user.name}
onClick={() => setFiltersMerge({ userIds: xor(userIds, [user.id]) })} onClick={() => mergeFilters({ userIds: xor(userIds, [user.id]) })}
/> />
</AvatarIsActiveBorder> </AvatarIsActiveBorder>
))} ))}
</Avatars> </Avatars>
<StyledButton <StyledButton
color="empty" variant="empty"
isActive={myOnly} isActive={myOnly}
onClick={() => setFiltersMerge({ myOnly: !myOnly })} onClick={() => mergeFilters({ myOnly: !myOnly })}
> >
Only My Issues Only My Issues
</StyledButton> </StyledButton>
<StyledButton <StyledButton
color="empty" variant="empty"
isActive={recent} isActive={recent}
onClick={() => setFiltersMerge({ recent: !recent })} onClick={() => mergeFilters({ recent: !recent })}
> >
Recently Updated Recently Updated
</StyledButton> </StyledButton>
{!areFiltersCleared && ( {!areFiltersCleared && (
<ClearAll onClick={() => setFilters(defaultFilters)}>Clear all</ClearAll> <ClearAll onClick={() => mergeFilters(defaultFilters)}>Clear all</ClearAll>
)} )}
</Filters> </Filters>
); );

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CopyLinkButton } from 'shared/components'; import { CopyLinkButton } from 'shared/components';
import { Breadcrumbs, Divider, Header, BoardName } from './Styles'; import { Breadcrumbs, Divider, Header, BoardName } from './Styles';
const propTypes = { const propTypes = {
@@ -17,6 +18,7 @@ const ProjectBoardHeader = ({ projectName }) => (
<Divider>/</Divider> <Divider>/</Divider>
Kanban Board Kanban Board
</Breadcrumbs> </Breadcrumbs>
<Header> <Header>
<BoardName>Kanban board</BoardName> <BoardName>Kanban board</BoardName>
<CopyLinkButton /> <CopyLinkButton />

View File

@@ -1,8 +1,8 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Avatar } from 'shared/components';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font, mixin } from 'shared/utils/styles';
import { Avatar } from 'shared/components';
export const IssueLink = styled(Link)` export const IssueLink = styled(Link)`
display: block; display: block;

View File

@@ -4,6 +4,7 @@ import { useRouteMatch } from 'react-router-dom';
import { Draggable } from 'react-beautiful-dnd'; import { Draggable } from 'react-beautiful-dnd';
import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components'; import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components';
import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles'; import { IssueLink, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles';
const propTypes = { const propTypes = {

View File

@@ -8,6 +8,7 @@ import api from 'shared/utils/api';
import useApi from 'shared/hooks/api'; import useApi from 'shared/hooks/api';
import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript'; import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript';
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues'; import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
import Issue from './Issue'; import Issue from './Issue';
import { Lists, List, Title, IssuesCount, Issues } from './Styles'; import { Lists, List, Title, IssuesCount, Issues } from './Styles';
@@ -21,13 +22,8 @@ const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
const [{ data: currentUserData }] = useApi.get('/currentUser'); const [{ data: currentUserData }] = useApi.get('/currentUser');
const currentUserId = get(currentUserData, 'currentUser.id'); const currentUserId = get(currentUserData, 'currentUser.id');
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
const handleIssueDrop = async ({ draggableId, destination, source }) => { const handleIssueDrop = async ({ draggableId, destination, source }) => {
if (!destination) return; if (!isPositionChanged(source, destination)) return;
const isSameList = destination.droppableId === source.droppableId;
const isSamePosition = destination.index === source.index;
if (isSameList && isSamePosition) return;
const issueId = Number(draggableId); const issueId = Number(draggableId);
@@ -35,7 +31,7 @@ const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
url: `/issues/${issueId}`, url: `/issues/${issueId}`,
updatedFields: { updatedFields: {
status: destination.droppableId, status: destination.droppableId,
listPosition: calculateListPosition(project.issues, destination, isSameList, issueId), listPosition: calculateListPosition(project.issues, destination, source, issueId),
}, },
currentFields: project.issues.find(({ id }) => id === issueId), currentFields: project.issues.find(({ id }) => id === issueId),
setLocalData: fields => updateLocalIssuesArray(issueId, fields), setLocalData: fields => updateLocalIssuesArray(issueId, fields),
@@ -43,21 +39,17 @@ const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
}; };
const renderList = status => { const renderList = status => {
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
const filteredListIssues = getSortedListIssues(filteredIssues, status); const filteredListIssues = getSortedListIssues(filteredIssues, status);
const allListIssues = getSortedListIssues(project.issues, status); const allListIssues = getSortedListIssues(project.issues, status);
const issuesCount =
allListIssues.length !== filteredListIssues.length
? `${filteredListIssues.length} of ${allListIssues.length}`
: allListIssues.length;
return ( return (
<Droppable key={status} droppableId={status}> <Droppable key={status} droppableId={status}>
{provided => ( {provided => (
<List> <List>
<Title> <Title>
{`${IssueStatusCopy[status]} `} {`${IssueStatusCopy[status]} `}
<IssuesCount>{issuesCount}</IssuesCount> <IssuesCount>{formatIssuesCount(allListIssues, filteredListIssues)}</IssuesCount>
</Title> </Title>
<Issues {...provided.droppableProps} ref={provided.innerRef}> <Issues {...provided.droppableProps} ref={provided.innerRef}>
{filteredListIssues.map((issue, index) => ( {filteredListIssues.map((issue, index) => (
@@ -102,6 +94,20 @@ const filterIssues = (projectIssues, filters, currentUserId) => {
const getSortedListIssues = (issues, status) => const getSortedListIssues = (issues, status) =>
issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition); 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 calculateListPosition = (...args) => {
const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args); const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args);
let position; let position;
@@ -118,9 +124,10 @@ const calculateListPosition = (...args) => {
return position; return position;
}; };
const getAfterDropPrevNextIssue = (allIssues, destination, isSameList, droppedIssueId) => { const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueId) => {
const destinationIssues = getSortedListIssues(allIssues, destination.droppableId); const destinationIssues = getSortedListIssues(allIssues, destination.droppableId);
const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId); const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId);
const isSameList = destination.droppableId === source.droppableId;
const afterDropDestinationIssues = isSameList const afterDropDestinationIssues = isSameList
? moveItemWithinArray(destinationIssues, droppedIssue, destination.index) ? moveItemWithinArray(destinationIssues, droppedIssue, destination.index)

View File

@@ -1,6 +1,8 @@
import React, { useState } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useMergeState from 'shared/hooks/mergeState';
import Header from './Header'; import Header from './Header';
import Filters from './Filters'; import Filters from './Filters';
import Lists from './Lists'; import Lists from './Lists';
@@ -18,7 +20,7 @@ const defaultFilters = {
}; };
const ProjectBoard = ({ project, updateLocalIssuesArray }) => { const ProjectBoard = ({ project, updateLocalIssuesArray }) => {
const [filters, setFilters] = useState(defaultFilters); const [filters, mergeFilters] = useMergeState(defaultFilters);
return ( return (
<> <>
<Header projectName={project.name} /> <Header projectName={project.name} />
@@ -26,7 +28,7 @@ const ProjectBoard = ({ project, updateLocalIssuesArray }) => {
projectUsers={project.users} projectUsers={project.users}
defaultFilters={defaultFilters} defaultFilters={defaultFilters}
filters={filters} filters={filters}
setFilters={setFilters} mergeFilters={mergeFilters}
/> />
<Lists project={project} filters={filters} updateLocalIssuesArray={updateLocalIssuesArray} /> <Lists project={project} filters={filters} updateLocalIssuesArray={updateLocalIssuesArray} />
</> </>

View File

@@ -11,6 +11,7 @@ import {
import toast from 'shared/utils/toast'; import toast from 'shared/utils/toast';
import useApi from 'shared/hooks/api'; import useApi from 'shared/hooks/api';
import { Form, IssueTypeIcon, Icon, Avatar, IssuePriorityIcon } from 'shared/components'; import { Form, IssueTypeIcon, Icon, Avatar, IssuePriorityIcon } from 'shared/components';
import { import {
FormHeading, FormHeading,
FormElement, FormElement,
@@ -152,10 +153,10 @@ const ProjectIssueCreateForm = ({ project, fetchProject, modalClose }) => {
renderValue={renderPriority} renderValue={renderPriority}
/> />
<Actions> <Actions>
<ActionButton type="submit" color="primary" working={isCreating}> <ActionButton type="submit" variant="primary" working={isCreating}>
Create Issue Create Issue
</ActionButton> </ActionButton>
<ActionButton color="empty" onClick={modalClose}> <ActionButton variant="empty" onClick={modalClose}>
Cancel Cancel
</ActionButton> </ActionButton>
</Actions> </Actions>

View File

@@ -2,6 +2,7 @@ import React, { useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Textarea } from 'shared/components'; import { Textarea } from 'shared/components';
import { Actions, FormButton } from './Styles'; import { Actions, FormButton } from './Styles';
const propTypes = { const propTypes = {
@@ -20,6 +21,7 @@ const ProjectBoardIssueDetailsCommentsBodyForm = ({
onCancel, onCancel,
}) => { }) => {
const $textareaRef = useRef(); const $textareaRef = useRef();
return ( return (
<> <>
<Textarea <Textarea
@@ -31,8 +33,8 @@ const ProjectBoardIssueDetailsCommentsBodyForm = ({
/> />
<Actions> <Actions>
<FormButton <FormButton
color="primary" variant="primary"
working={isWorking} isWorking={isWorking}
onClick={() => { onClick={() => {
if ($textareaRef.current.value.trim()) { if ($textareaRef.current.value.trim()) {
onSubmit(); onSubmit();
@@ -41,7 +43,7 @@ const ProjectBoardIssueDetailsCommentsBodyForm = ({
> >
Save Save
</FormButton> </FormButton>
<FormButton color="empty" onClick={onCancel}> <FormButton variant="empty" onClick={onCancel}>
Cancel Cancel
</FormButton> </FormButton>
</Actions> </Actions>

View File

@@ -5,6 +5,7 @@ import api from 'shared/utils/api';
import toast from 'shared/utils/toast'; import toast from 'shared/utils/toast';
import { formatDateTimeConversational } from 'shared/utils/dateTime'; import { formatDateTimeConversational } from 'shared/utils/dateTime';
import { ConfirmModal } from 'shared/components'; import { ConfirmModal } from 'shared/components';
import BodyForm from '../BodyForm'; import BodyForm from '../BodyForm';
import { import {
Comment, Comment,
@@ -35,6 +36,7 @@ const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
toast.error(error); toast.error(error);
} }
}; };
const handleCommentUpdate = async () => { const handleCommentUpdate = async () => {
try { try {
setUpdating(true); setUpdating(true);
@@ -46,12 +48,14 @@ const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
toast.error(error); toast.error(error);
} }
}; };
return ( return (
<Comment> <Comment>
<UserAvatar name={comment.user.name} avatarUrl={comment.user.avatarUrl} /> <UserAvatar name={comment.user.name} avatarUrl={comment.user.avatarUrl} />
<Content> <Content>
<Username>{comment.user.name}</Username> <Username>{comment.user.name}</Username>
<CreatedAt>{formatDateTimeConversational(comment.createdAt)}</CreatedAt> <CreatedAt>{formatDateTimeConversational(comment.createdAt)}</CreatedAt>
{isFormOpen ? ( {isFormOpen ? (
<BodyForm <BodyForm
value={body} value={body}

View File

@@ -2,7 +2,8 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { KeyCodes } from 'shared/constants/keyCodes'; import { KeyCodes } from 'shared/constants/keyCodes';
import { isFocusedElementEditable } from 'shared/utils/dom'; import { isFocusedElementEditable } from 'shared/utils/browser';
import { Tip, TipLetter } from './Styles'; import { Tip, TipLetter } from './Styles';
const propTypes = { const propTypes = {
@@ -17,7 +18,9 @@ const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
setFormOpen(true); setFormOpen(true);
} }
}; };
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
return () => { return () => {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', handleKeyDown);
}; };

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import api from 'shared/utils/api'; import api from 'shared/utils/api';
import useApi from 'shared/hooks/api'; import useApi from 'shared/hooks/api';
import toast from 'shared/utils/toast'; import toast from 'shared/utils/toast';
import BodyForm from '../BodyForm'; import BodyForm from '../BodyForm';
import ProTip from './ProTip'; import ProTip from './ProTip';
import { Create, UserAvatar, Right, FakeTextarea } from './Styles'; import { Create, UserAvatar, Right, FakeTextarea } from './Styles';

View File

@@ -14,13 +14,14 @@ const ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => (
<Comments> <Comments>
<Title>Comments</Title> <Title>Comments</Title>
<Create issueId={issue.id} fetchIssue={fetchIssue} /> <Create issueId={issue.id} fetchIssue={fetchIssue} />
{sortByNewestFirst(issue.comments).map(comment => (
{sortByNewest(issue.comments).map(comment => (
<Comment key={comment.id} comment={comment} fetchIssue={fetchIssue} /> <Comment key={comment.id} comment={comment} fetchIssue={fetchIssue} />
))} ))}
</Comments> </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; ProjectBoardIssueDetailsComments.propTypes = propTypes;

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { formatDateTimeConversational } from 'shared/utils/dateTime'; import { formatDateTimeConversational } from 'shared/utils/dateTime';
import { Dates } from './Styles'; import { Dates } from './Styles';
const propTypes = { const propTypes = {

View File

@@ -21,13 +21,16 @@ const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) =>
toast.error(error); toast.error(error);
} }
}; };
return ( return (
<ConfirmModal <ConfirmModal
title="Are you sure you want to delete this issue?" title="Are you sure you want to delete this issue?"
message="Once you delete, it's gone for good." message="Once you delete, it's gone for good."
confirmText="Delete issue" confirmText="Delete issue"
onConfirm={handleIssueDelete} 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} />
)}
/> />
); );
}; };

View File

@@ -1,8 +1,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; 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 { TextEditor, TextEditedContent, Button } from 'shared/components';
import { Title, EmptyLabel, Actions } from './Styles'; import { Title, EmptyLabel, Actions } from './Styles';
const propTypes = { const propTypes = {
@@ -14,8 +15,15 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
const [value, setValue] = useState(issue.description); const [value, setValue] = useState(issue.description);
const [isEditing, setEditing] = useState(false); const [isEditing, setEditing] = useState(false);
const handleUpdate = () => {
setEditing(false);
updateIssue({ description: value });
};
const isDescriptionEmpty = getTextContentsFromHtmlString(issue.description).trim().length === 0;
const renderPresentingMode = () => const renderPresentingMode = () =>
isDescriptionEmpty(issue.description) ? ( isDescriptionEmpty ? (
<EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel> <EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel>
) : ( ) : (
<TextEditedContent content={issue.description} onClick={() => setEditing(true)} /> <TextEditedContent content={issue.description} onClick={() => setEditing(true)} />
@@ -25,21 +33,16 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
<> <>
<TextEditor placeholder="Describe the issue" defaultValue={value} onChange={setValue} /> <TextEditor placeholder="Describe the issue" defaultValue={value} onChange={setValue} />
<Actions> <Actions>
<Button <Button variant="primary" onClick={handleUpdate}>
color="primary"
onClick={() => {
setEditing(false);
updateIssue({ description: value });
}}
>
Save Save
</Button> </Button>
<Button color="empty" onClick={() => setEditing(false)}> <Button variant="empty" onClick={() => setEditing(false)}>
Cancel Cancel
</Button> </Button>
</Actions> </Actions>
</> </>
); );
return ( return (
<> <>
<Title>Description</Title> <Title>Description</Title>
@@ -48,9 +51,6 @@ const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
); );
}; };
const isDescriptionEmpty = description =>
getTextContentsFromHtmlString(description).trim().length === 0;
ProjectBoardIssueDetailsDescription.propTypes = propTypes; ProjectBoardIssueDetailsDescription.propTypes = propTypes;
export default ProjectBoardIssueDetailsDescription; export default ProjectBoardIssueDetailsDescription;

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Button, Tooltip } from 'shared/components'; import { Button, Tooltip } from 'shared/components';
import feedbackImage from './assets/feedback.png'; import feedbackImage from './assets/feedback.png';
import { FeedbackDropdown, FeedbackImageCont, FeedbackImage, FeedbackParagraph } from './Styles'; import { FeedbackDropdown, FeedbackImageCont, FeedbackImage, FeedbackParagraph } from './Styles';
@@ -9,7 +10,7 @@ const ProjectBoardIssueDetailsFeedback = () => (
width={300} width={300}
offset={{ top: -15 }} offset={{ top: -15 }}
renderLink={linkProps => ( renderLink={linkProps => (
<Button icon="feedback" color="empty" {...linkProps}> <Button icon="feedback" variant="empty" {...linkProps}>
Give feedback Give feedback
</Button> </Button>
)} )}
@@ -18,19 +19,23 @@ const ProjectBoardIssueDetailsFeedback = () => (
<FeedbackImageCont> <FeedbackImageCont>
<FeedbackImage src={feedbackImage} alt="Give feedback" /> <FeedbackImage src={feedbackImage} alt="Give feedback" />
</FeedbackImageCont> </FeedbackImageCont>
<FeedbackParagraph> <FeedbackParagraph>
This simplified Jira clone is built with React on the front-end and Node/TypeScript on the This simplified Jira clone is built with React on the front-end and Node/TypeScript on the
back-end. back-end.
</FeedbackParagraph> </FeedbackParagraph>
<FeedbackParagraph> <FeedbackParagraph>
{'Read more on our website or reach out via '} {'Read more on our website or reach out via '}
<a href="mailto:ivor@codetree.co"> <a href="mailto:ivor@codetree.co">
<strong>ivor@codetree.co</strong> <strong>ivor@codetree.co</strong>
</a> </a>
</FeedbackParagraph> </FeedbackParagraph>
<a href="https://codetree.co/" target="_blank" rel="noreferrer noopener"> <a href="https://codetree.co/" target="_blank" rel="noreferrer noopener">
<Button color="primary">Visit Website</Button> <Button variant="primary">Visit Website</Button>
</a> </a>
<a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener"> <a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener">
<Button style={{ marginLeft: 10 }} icon="github"> <Button style={{ marginLeft: 10 }} icon="github">
Github Repo Github Repo

View File

@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues'; import { IssuePriority, IssuePriorityCopy } from 'shared/constants/issues';
import { Select, IssuePriorityIcon } from 'shared/components'; import { Select, IssuePriorityIcon } from 'shared/components';
import { Priority, Label } from './Styles';
import { SectionTitle } from '../Styles'; import { SectionTitle } from '../Styles';
import { Priority, Label } from './Styles';
const propTypes = { const propTypes = {
issue: PropTypes.object.isRequired, issue: PropTypes.object.isRequired,
@@ -18,6 +19,7 @@ const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => {
<Label>{IssuePriorityCopy[priority]}</Label> <Label>{IssuePriorityCopy[priority]}</Label>
</Priority> </Priority>
); );
return ( return (
<> <>
<SectionTitle>Priority</SectionTitle> <SectionTitle>Priority</SectionTitle>

View File

@@ -3,8 +3,9 @@ import PropTypes from 'prop-types';
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues'; import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
import { Select, Icon } from 'shared/components'; import { Select, Icon } from 'shared/components';
import { Status } from './Styles';
import { SectionTitle } from '../Styles'; import { SectionTitle } from '../Styles';
import { Status } from './Styles';
const propTypes = { const propTypes = {
issue: PropTypes.object.isRequired, issue: PropTypes.object.isRequired,

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { KeyCodes } from 'shared/constants/keyCodes'; import { KeyCodes } from 'shared/constants/keyCodes';
import { is, generateErrors } from 'shared/utils/validation'; import { is, generateErrors } from 'shared/utils/validation';
import { TitleTextarea, ErrorText } from './Styles'; import { TitleTextarea, ErrorText } from './Styles';
const propTypes = { const propTypes = {

View File

@@ -3,6 +3,8 @@ import PropTypes from 'prop-types';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import { InputDebounced, Modal, Button } from 'shared/components'; import { InputDebounced, Modal, Button } from 'shared/components';
import { SectionTitle } from '../Styles';
import { import {
TrackingLink, TrackingLink,
Tracking, Tracking,
@@ -18,7 +20,6 @@ import {
InputLabel, InputLabel,
Actions, Actions,
} from './Styles'; } from './Styles';
import { SectionTitle } from '../Styles';
const propTypes = { const propTypes = {
issue: PropTypes.object.isRequired, 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 = () => ( const renderEstimate = () => (
<> <>
<SectionTitle>Original Estimate (hours)</SectionTitle> <SectionTitle>Original Estimate (hours)</SectionTitle>
@@ -96,11 +51,11 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
<SectionTitle>Time Tracking</SectionTitle> <SectionTitle>Time Tracking</SectionTitle>
<Modal <Modal
width={400} width={400}
renderLink={modal => <TrackingLink>{renderTrackingPreview(modal.open)}</TrackingLink>} renderLink={modal => <TrackingLink>{renderTrackingWidget(modal.open)}</TrackingLink>}
renderContent={modal => ( renderContent={modal => (
<ModalContents> <ModalContents>
<ModalTitle>Time tracking</ModalTitle> <ModalTitle>Time tracking</ModalTitle>
{renderTrackingPreview()} {renderTrackingWidget()}
<Inputs> <Inputs>
<InputCont> <InputCont>
<InputLabel>Time spent (hours)</InputLabel> <InputLabel>Time spent (hours)</InputLabel>
@@ -112,7 +67,7 @@ const ProjectBoardIssueDetailsTracking = ({ issue, updateIssue }) => {
</InputCont> </InputCont>
</Inputs> </Inputs>
<Actions> <Actions>
<Button color="primary" onClick={modal.close}> <Button variant="primary" onClick={modal.close}>
Done Done
</Button> </Button>
</Actions> </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 ( return (
<> <>
{renderEstimate()} {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; ProjectBoardIssueDetailsTracking.propTypes = propTypes;
export default ProjectBoardIssueDetailsTracking; export default ProjectBoardIssueDetailsTracking;

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { IssueType, IssueTypeCopy } from 'shared/constants/issues'; import { IssueType, IssueTypeCopy } from 'shared/constants/issues';
import { IssueTypeIcon, Select } from 'shared/components'; import { IssueTypeIcon, Select } from 'shared/components';
import { TypeButton, Type, TypeLabel } from './Styles'; import { TypeButton, Type, TypeLabel } from './Styles';
const propTypes = { const propTypes = {
@@ -21,7 +22,7 @@ const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
}))} }))}
onChange={type => updateIssue({ type })} onChange={type => updateIssue({ type })}
renderValue={({ value: type }) => ( renderValue={({ value: type }) => (
<TypeButton color="empty" icon={<IssueTypeIcon type={type} />}> <TypeButton variant="empty" icon={<IssueTypeIcon type={type} />}>
{`${type}-${issue.id}`} {`${type}-${issue.id}`}
</TypeButton> </TypeButton>
)} )}

View File

@@ -2,8 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Avatar, Select, Icon } from 'shared/components'; import { Avatar, Select, Icon } from 'shared/components';
import { User, Username } from './Styles';
import { SectionTitle } from '../Styles'; import { SectionTitle } from '../Styles';
import { User, Username } from './Styles';
const propTypes = { const propTypes = {
issue: PropTypes.object.isRequired, issue: PropTypes.object.isRequired,

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import api from 'shared/utils/api'; import api from 'shared/utils/api';
import useApi from 'shared/hooks/api'; import useApi from 'shared/hooks/api';
import { PageError, CopyLinkButton, Button } from 'shared/components'; import { PageError, CopyLinkButton, Button } from 'shared/components';
import Loader from './Loader'; import Loader from './Loader';
import Type from './Type'; import Type from './Type';
import Feedback from './Feedback'; import Feedback from './Feedback';
@@ -61,9 +62,9 @@ const ProjectBoardIssueDetails = ({
<Type issue={issue} updateIssue={updateIssue} /> <Type issue={issue} updateIssue={updateIssue} />
<TopActionsRight> <TopActionsRight>
<Feedback /> <Feedback />
<CopyLinkButton color="empty" /> <CopyLinkButton variant="empty" />
<Delete issue={issue} fetchProject={fetchProject} modalClose={modalClose} /> <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> </TopActionsRight>
</TopActions> </TopActions>
<Content> <Content>

View File

@@ -2,7 +2,7 @@ import styled from 'styled-components';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { font, sizes, color, mixin, zIndexValues } from 'shared/utils/styles'; 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` export const NavLeft = styled.aside`
z-index: ${zIndexValues.navLeft}; z-index: ${zIndexValues.navLeft};

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { Link, useRouteMatch } from 'react-router-dom'; import { Link, useRouteMatch } from 'react-router-dom';
import { Icon } from 'shared/components'; import { Icon } from 'shared/components';
import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles'; import { NavLeft, LogoLink, StyledLogo, Bottom, Item, ItemText } from './Styles';
const ProjectNavbarLeft = () => { const ProjectNavbarLeft = () => {

View File

@@ -1,4 +1,4 @@
import styled, { css } from 'styled-components'; import styled from 'styled-components';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
import { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles'; import { color, sizes, font, mixin, zIndexValues } from 'shared/utils/styles';
@@ -51,11 +51,7 @@ export const LinkItem = styled(NavLink)`
${props => ${props =>
!props.implemented !props.implemented
? `cursor: not-allowed;` ? `cursor: not-allowed;`
: css` : `&:hover { background: ${color.backgroundLight}; }`}
&:hover {
background: ${color.backgroundLight};
}
`}
i { i {
margin-right: 15px; margin-right: 15px;
font-size: 20px; font-size: 20px;

View File

@@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useRouteMatch } from 'react-router-dom';
import { Icon, ProjectAvatar } from 'shared/components'; import { Icon, ProjectAvatar } from 'shared/components';
import { import {
Sidebar, Sidebar,
ProjectInfo, ProjectInfo,
@@ -16,17 +18,19 @@ import {
const propTypes = { const propTypes = {
projectName: PropTypes.string.isRequired, projectName: PropTypes.string.isRequired,
matchPath: PropTypes.string.isRequired,
}; };
const ProjectSidebar = ({ projectName, matchPath }) => { const ProjectSidebar = ({ projectName }) => {
const match = useRouteMatch();
const renderLinkItem = (text, iconType, path = '') => ( const renderLinkItem = (text, iconType, path = '') => (
<LinkItem exact to={`${matchPath}${path}`} implemented={path}> <LinkItem exact to={`${match.path}${path}`} implemented={path}>
<Icon type={iconType} /> <Icon type={iconType} />
<LinkText>{text}</LinkText> <LinkText>{text}</LinkText>
{!path && <NotImplemented>Not implemented</NotImplemented>} {!path && <NotImplemented>Not implemented</NotImplemented>}
</LinkItem> </LinkItem>
); );
return ( return (
<Sidebar> <Sidebar>
<ProjectInfo> <ProjectInfo>
@@ -36,6 +40,7 @@ const ProjectSidebar = ({ projectName, matchPath }) => {
<ProjectCategory>Software project</ProjectCategory> <ProjectCategory>Software project</ProjectCategory>
</ProjectTexts> </ProjectTexts>
</ProjectInfo> </ProjectInfo>
{renderLinkItem('Kanban Board', 'board', '/board')} {renderLinkItem('Kanban Board', 'board', '/board')}
{renderLinkItem('Reports', 'reports')} {renderLinkItem('Reports', 'reports')}
<Divider /> <Divider />

View File

@@ -4,6 +4,7 @@ import { Route, Redirect, useRouteMatch, useHistory } from 'react-router-dom';
import useApi from 'shared/hooks/api'; import useApi from 'shared/hooks/api';
import { updateArrayItemById } from 'shared/utils/javascript'; import { updateArrayItemById } from 'shared/utils/javascript';
import { PageLoader, PageError, Modal } from 'shared/components'; import { PageLoader, PageError, Modal } from 'shared/components';
import NavbarLeft from './NavbarLeft'; import NavbarLeft from './NavbarLeft';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import Board from './Board'; import Board from './Board';
@@ -38,12 +39,24 @@ const Project = () => {
updateLocalIssuesArray={updateLocalIssuesArray} 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 => ( const renderIssueDetailsModal = routeProps => (
<Modal <Modal
isOpen isOpen
width={1040} width={1040}
withCloseIcon={false} withCloseIcon={false}
onClose={() => history.push(match.url)} onClose={() => history.push(`${match.url}/board`)}
renderContent={modal => ( renderContent={modal => (
<IssueDetails <IssueDetails
issueId={routeProps.match.params.issueId} 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 ( return (
<ProjectPage> <ProjectPage>
<NavbarLeft /> <NavbarLeft />
<Sidebar projectName={project.name} matchPath={match.path} /> <Sidebar projectName={project.name} />
<Route path={`${match.path}/board`} render={renderBoard} /> <Route path={`${match.path}/board`} render={renderBoard} />
<Route path={`${match.path}/board/create-issue`} render={renderIssueCreateModal} /> <Route path={`${match.path}/board/create-issue`} render={renderIssueCreateModal} />
<Route path={`${match.path}/board/issue/:issueId`} render={renderIssueDetailsModal} /> <Route path={`${match.path}/board/issue/:issueId`} render={renderIssueDetailsModal} />

View File

@@ -17,6 +17,17 @@ const defaultProps = {
size: 32, size: 32,
}; };
const Avatar = ({ className, avatarUrl, name, size, ...otherProps }) => {
if (avatarUrl) {
return <Image className={className} size={size} avatarUrl={avatarUrl} {...otherProps} />;
}
return (
<Letter className={className} size={size} color={getColorFromName(name)} {...otherProps}>
<span>{name.charAt(0)}</span>
</Letter>
);
};
const colors = [ const colors = [
'#DA7657', '#DA7657',
'#6ADA57', '#6ADA57',
@@ -30,17 +41,6 @@ const colors = [
const getColorFromName = name => colors[name.toLocaleLowerCase().charCodeAt(0) % colors.length]; const getColorFromName = name => colors[name.toLocaleLowerCase().charCodeAt(0) % colors.length];
const Avatar = ({ className, avatarUrl, name, size, ...otherProps }) => {
if (avatarUrl) {
return <Image className={className} size={size} avatarUrl={avatarUrl} {...otherProps} />;
}
return (
<Letter className={className} size={size} color={getColorFromName(name)} {...otherProps}>
<span>{name.charAt(0)}</span>
</Letter>
);
};
Avatar.propTypes = propTypes; Avatar.propTypes = propTypes;
Avatar.defaultProps = defaultProps; Avatar.defaultProps = defaultProps;

View File

@@ -1,7 +1,7 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import Spinner from 'shared/components/Spinner';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font, mixin } from 'shared/utils/styles';
import Spinner from 'shared/components/Spinner';
export const StyledButton = styled.button` export const StyledButton = styled.button`
display: inline-flex; display: inline-flex;
@@ -17,28 +17,29 @@ export const StyledButton = styled.button`
appearance: none; appearance: none;
${mixin.clickable} ${mixin.clickable}
${font.size(14.5)} ${font.size(14.5)}
${props => buttonColors[props.color]} ${props => buttonVariants[props.variant]}
&:disabled { &:disabled {
opacity: 0.6; opacity: 0.6;
cursor: default; cursor: default;
} }
i {
margin-right: ${props => (props.iconOnly ? 0 : 7)}px;
}
`; `;
const colored = css` const colored = css`
color: #fff; color: #fff;
background: ${props => color[props.color]}; background: ${props => color[props.variant]};
${font.medium} ${font.medium}
&:not(:disabled) { &:not(:disabled) {
&:hover { &:hover {
background: ${props => mixin.lighten(color[props.color], 0.15)}; background: ${props => mixin.lighten(color[props.variant], 0.15)};
} }
&:active { &:active {
background: ${props => mixin.darken(color[props.color], 0.1)}; background: ${props => mixin.darken(color[props.variant], 0.1)};
} }
${props => props.isActive && `background: ${mixin.darken(color[props.color], 0.1)} !important;`} ${props =>
props.isActive &&
css`
background: ${mixin.darken(color[props.variant], 0.1)} !important;
`}
} }
`; `;
@@ -55,14 +56,14 @@ const secondaryAndEmptyShared = css`
} }
${props => ${props =>
props.isActive && props.isActive &&
` css`
color: ${color.primary}; color: ${color.primary};
background: ${color.backgroundLightPrimary} !important; background: ${color.backgroundLightPrimary} !important;
`} `}
} }
`; `;
const buttonColors = { const buttonVariants = {
primary: colored, primary: colored,
success: colored, success: colored,
danger: colored, danger: colored,
@@ -79,5 +80,8 @@ const buttonColors = {
export const StyledSpinner = styled(Spinner)` export const StyledSpinner = styled(Spinner)`
position: relative; position: relative;
top: 1px; top: 1px;
margin-right: ${props => (props.iconOnly ? 0 : 7)}px; `;
export const Text = styled.div`
padding-left: ${props => (props.withPadding ? 7 : 0)}px;
`; `;

View File

@@ -3,81 +3,65 @@ import PropTypes from 'prop-types';
import { color } from 'shared/utils/styles'; import { color } from 'shared/utils/styles';
import Icon from 'shared/components/Icon'; import Icon from 'shared/components/Icon';
import { StyledButton, StyledSpinner } from './Styles';
import { StyledButton, StyledSpinner, Text } from './Styles';
const propTypes = { const propTypes = {
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
color: PropTypes.oneOf(['primary', 'secondary', 'empty', 'success', 'danger']), variant: PropTypes.oneOf(['primary', 'success', 'danger', 'secondary', 'empty']),
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
iconSize: PropTypes.number, iconSize: PropTypes.number,
disabled: PropTypes.bool, disabled: PropTypes.bool,
working: PropTypes.bool, isWorking: PropTypes.bool,
onClick: PropTypes.func, onClick: PropTypes.func,
}; };
const defaultProps = { const defaultProps = {
className: undefined, className: undefined,
children: undefined, children: undefined,
color: 'secondary', variant: 'secondary',
icon: undefined, icon: undefined,
iconSize: 18, iconSize: 18,
disabled: false, disabled: false,
working: false, isWorking: false,
onClick: () => {}, onClick: () => {},
}; };
const Button = forwardRef( const Button = forwardRef(
( ({ children, variant, icon, iconSize, disabled, isWorking, onClick, ...buttonProps }, ref) => {
{
children,
color: propsColor,
icon,
iconSize,
disabled,
working,
onClick = () => {},
...buttonProps
},
ref,
) => {
const handleClick = () => { const handleClick = () => {
if (!disabled && !working) { if (!disabled && !isWorking) {
onClick(); onClick();
} }
}; };
const renderSpinner = () => (
<StyledSpinner
iconOnly={!children}
size={26}
color={propsColor === 'primary' ? '#fff' : color.textDark}
/>
);
const renderIcon = () => (
<Icon
type={icon}
size={iconSize}
color={propsColor === 'primary' ? '#fff' : color.textDark}
/>
);
return ( return (
<StyledButton <StyledButton
{...buttonProps} {...buttonProps}
onClick={handleClick} onClick={handleClick}
color={propsColor} variant={variant}
disabled={disabled || working} disabled={disabled || isWorking}
working={working} isWorking={isWorking}
iconOnly={!children} iconOnly={!children}
ref={ref} ref={ref}
> >
{working && renderSpinner()} {isWorking && <StyledSpinner size={26} color={getIconColor(variant)} />}
{!working && icon && (typeof icon !== 'string' ? icon : renderIcon())}
<div>{children}</div> {!isWorking && icon && typeof icon === 'string' ? (
<Icon type={icon} size={iconSize} color={getIconColor(variant)} />
) : (
icon
)}
{children && <Text withPadding={isWorking || icon}>{children}</Text>}
</StyledButton> </StyledButton>
); );
}, },
); );
const getIconColor = variant =>
['secondary', 'empty'].includes(variant) ? color.textDark : '#fff';
Button.propTypes = propTypes; Button.propTypes = propTypes;
Button.defaultProps = defaultProps; Button.defaultProps = defaultProps;

View File

@@ -1,12 +1,11 @@
import styled from 'styled-components'; import styled from 'styled-components';
import Modal from 'shared/components/Modal';
import Input from 'shared/components/Input';
import Button from 'shared/components/Button';
import { font } from 'shared/utils/styles'; import { font } from 'shared/utils/styles';
import Modal from 'shared/components/Modal';
import Button from 'shared/components/Button';
export const StyledConfirmModal = styled(Modal)` export const StyledConfirmModal = styled(Modal)`
padding: 45px 50px 50px; padding: 35px 40px 40px;
`; `;
export const Title = styled.div` export const Title = styled.div`
@@ -22,17 +21,6 @@ export const Message = styled.p`
${font.size(15)} ${font.size(15)}
`; `;
export const InputLabel = styled.div`
padding-bottom: 12px;
${font.bold}
${font.size(15)}
`;
export const StyledInput = styled(Input)`
margin-bottom: 25px;
max-width: 220px;
`;
export const Actions = styled.div` export const Actions = styled.div`
display: flex; display: flex;
padding-top: 6px; padding-top: 6px;

View File

@@ -1,50 +1,38 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import { StyledConfirmModal, Title, Message, Actions, StyledButton } from './Styles';
StyledConfirmModal,
Title,
Message,
InputLabel,
StyledInput,
Actions,
StyledButton,
} from './Styles';
const propTypes = { const propTypes = {
className: PropTypes.string, className: PropTypes.string,
variant: PropTypes.oneOf(['primary', 'danger']),
title: PropTypes.string, title: PropTypes.string,
message: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), message: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
confirmText: PropTypes.string, confirmText: PropTypes.string,
cancelText: PropTypes.string, cancelText: PropTypes.string,
confirmInput: PropTypes.string,
type: PropTypes.oneOf(['primary', 'danger']),
onConfirm: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired,
renderLink: PropTypes.func.isRequired, renderLink: PropTypes.func.isRequired,
}; };
const defaultProps = { const defaultProps = {
className: undefined, className: undefined,
variant: 'primary',
title: 'Warning', title: 'Warning',
message: 'Are you sure you want to continue with this action?', message: 'Are you sure you want to continue with this action?',
confirmText: 'Confirm', confirmText: 'Confirm',
cancelText: 'Cancel', cancelText: 'Cancel',
confirmInput: null,
type: 'primary',
}; };
const ConfirmModal = ({ const ConfirmModal = ({
className, className,
variant,
title, title,
message, message,
confirmText, confirmText,
cancelText, cancelText,
confirmInput,
type,
onConfirm, onConfirm,
renderLink, renderLink,
}) => { }) => {
const [isConfirmEnabled, setConfirmEnabled] = useState(false);
const [isWorking, setWorking] = useState(false); const [isWorking, setWorking] = useState(false);
const handleConfirm = modal => { const handleConfirm = modal => {
@@ -57,31 +45,18 @@ const ConfirmModal = ({
}); });
}; };
const handleConfirmInputChange = value =>
setConfirmEnabled(value.trim().toLowerCase() === confirmInput.toLowerCase());
return ( return (
<StyledConfirmModal <StyledConfirmModal
suppressClassNameWarning
className={className} className={className}
afterClose={() => setConfirmEnabled(false)}
renderLink={renderLink} renderLink={renderLink}
renderContent={modal => ( renderContent={modal => (
<> <>
<Title>{title}</Title> <Title>{title}</Title>
{message && <Message>{message}</Message>} {message && <Message>{message}</Message>}
{confirmInput && (
<>
<InputLabel>{`Type ${confirmInput} below to confirm.`}</InputLabel>
<StyledInput onChange={handleConfirmInputChange} />
<br />
</>
)}
<Actions> <Actions>
<StyledButton <StyledButton
color={type} variant={variant}
disabled={confirmInput && !isConfirmEnabled} isWorking={isWorking}
working={isWorking}
onClick={() => handleConfirm(modal)} onClick={() => handleConfirm(modal)}
> >
{confirmText} {confirmText}

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { copyToClipboard } from 'shared/utils/clipboard'; import { copyToClipboard } from 'shared/utils/browser';
import { Button } from 'shared/components'; import { Button } from 'shared/components';
const CopyLinkButton = ({ ...buttonProps }) => { const CopyLinkButton = ({ ...buttonProps }) => {
@@ -11,6 +11,7 @@ const CopyLinkButton = ({ ...buttonProps }) => {
setTimeout(() => setLinkCopied(false), 2000); setTimeout(() => setLinkCopied(false), 2000);
copyToClipboard(window.location.href); copyToClipboard(window.location.href);
}; };
return ( return (
<Button icon="link" onClick={handleLinkCopy} {...buttonProps}> <Button icon="link" onClick={handleLinkCopy} {...buttonProps}>
{isLinkCopied ? 'Link Copied' : 'Copy link'} {isLinkCopied ? 'Link Copied' : 'Copy link'}

View File

@@ -5,6 +5,7 @@ import { times, range } from 'lodash';
import { formatDate, formatDateTimeForAPI } from 'shared/utils/dateTime'; import { formatDate, formatDateTimeForAPI } from 'shared/utils/dateTime';
import Icon from 'shared/components/Icon'; import Icon from 'shared/components/Icon';
import { import {
DateSection, DateSection,
YearSelect, YearSelect,
@@ -24,11 +25,11 @@ const propTypes = {
const defaultProps = { const defaultProps = {
withTime: true, withTime: true,
value: null, value: undefined,
}; };
const DatePickerDateSection = ({ withTime, value, onChange, setDropdownOpen }) => { const DatePickerDateSection = ({ withTime, value, onChange, setDropdownOpen }) => {
const [selectedMonth, setSelectedMonth] = useState(moment(value || undefined).startOf('month')); const [selectedMonth, setSelectedMonth] = useState(moment(value).startOf('month'));
const handleYearChange = year => { const handleYearChange = year => {
setSelectedMonth(moment(selectedMonth).set({ year: Number(year) })); setSelectedMonth(moment(selectedMonth).set({ year: Number(year) }));
@@ -53,47 +54,31 @@ const DatePickerDateSection = ({ withTime, value, onChange, setDropdownOpen }) =
} }
}; };
const generateYears = () => times(50, i => ({ label: `${i + 2010}`, value: `${i + 2010}` }));
const generateWeekDayNames = () => moment.weekdaysMin(true);
const generateFillerDaysBeforeMonthStart = () => {
const count = selectedMonth.diff(moment(selectedMonth).startOf('week'), 'days');
return range(count);
};
const generateMonthDays = () =>
times(selectedMonth.daysInMonth()).map(i => moment(selectedMonth).add(i, 'days'));
const generateFillerDaysAfterMonthEnd = () => {
const selectedMonthEnd = moment(selectedMonth).endOf('month');
const weekEnd = moment(selectedMonthEnd).endOf('week');
const count = weekEnd.diff(selectedMonthEnd, 'days');
return range(count);
};
return ( return (
<DateSection> <DateSection>
<SelectedMonthYear>{formatDate(selectedMonth, 'MMM YYYY')}</SelectedMonthYear> <SelectedMonthYear>{formatDate(selectedMonth, 'MMM YYYY')}</SelectedMonthYear>
<YearSelect onChange={event => handleYearChange(event.target.value)}> <YearSelect onChange={event => handleYearChange(event.target.value)}>
{[{ label: 'Year', value: '' }, ...generateYears()].map(option => ( {generateYearOptions().map(option => (
<option key={option.label} value={option.value}> <option key={option.label} value={option.value}>
{option.label} {option.label}
</option> </option>
))} ))}
</YearSelect> </YearSelect>
<PrevNextIcons> <PrevNextIcons>
<Icon type="arrow-left" onClick={() => handleMonthChange('subtract')} /> <Icon type="arrow-left" onClick={() => handleMonthChange('subtract')} />
<Icon type="arrow-right" onClick={() => handleMonthChange('add')} /> <Icon type="arrow-right" onClick={() => handleMonthChange('add')} />
</PrevNextIcons> </PrevNextIcons>
<Grid> <Grid>
{generateWeekDayNames().map(name => ( {generateWeekDayNames().map(name => (
<DayName key={name}>{name}</DayName> <DayName key={name}>{name}</DayName>
))} ))}
{generateFillerDaysBeforeMonthStart().map(i => ( {generateFillerDaysBeforeMonthStart(selectedMonth).map(i => (
<Day key={`before-${i}`} isFiller /> <Day key={`before-${i}`} isFiller />
))} ))}
{generateMonthDays().map(date => ( {generateMonthDays(selectedMonth).map(date => (
<Day <Day
key={date} key={date}
isToday={moment().isSame(date, 'day')} isToday={moment().isSame(date, 'day')}
@@ -103,7 +88,7 @@ const DatePickerDateSection = ({ withTime, value, onChange, setDropdownOpen }) =
{formatDate(date, 'D')} {formatDate(date, 'D')}
</Day> </Day>
))} ))}
{generateFillerDaysAfterMonthEnd().map(i => ( {generateFillerDaysAfterMonthEnd(selectedMonth).map(i => (
<Day key={`after-${i}`} isFiller /> <Day key={`after-${i}`} isFiller />
))} ))}
</Grid> </Grid>
@@ -111,6 +96,30 @@ const DatePickerDateSection = ({ withTime, value, onChange, setDropdownOpen }) =
); );
}; };
const currentYear = moment().year();
const generateYearOptions = () => [
{ label: 'Year', value: '' },
...times(50, i => ({ label: `${i + currentYear - 10}`, value: `${i + currentYear - 10}` })),
];
const generateWeekDayNames = () => moment.weekdaysMin(true);
const generateFillerDaysBeforeMonthStart = selectedMonth => {
const count = selectedMonth.diff(moment(selectedMonth).startOf('week'), 'days');
return range(count);
};
const generateMonthDays = selectedMonth =>
times(selectedMonth.daysInMonth()).map(i => moment(selectedMonth).add(i, 'days'));
const generateFillerDaysAfterMonthEnd = selectedMonth => {
const selectedMonthEnd = moment(selectedMonth).endOf('month');
const weekEnd = moment(selectedMonthEnd).endOf('week');
const count = weekEnd.diff(selectedMonthEnd, 'days');
return range(count);
};
DatePickerDateSection.propTypes = propTypes; DatePickerDateSection.propTypes = propTypes;
DatePickerDateSection.defaultProps = defaultProps; DatePickerDateSection.defaultProps = defaultProps;

View File

@@ -1,4 +1,4 @@
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { color, font, mixin, zIndexValues } from 'shared/utils/styles'; import { color, font, mixin, zIndexValues } from 'shared/utils/styles';
@@ -15,12 +15,12 @@ export const Dropdown = styled.div`
border-radius: 3px; border-radius: 3px;
background: #fff; background: #fff;
${mixin.boxShadowDropdown} ${mixin.boxShadowDropdown}
${props => (props.withTime ? withTimeStyles : '')} ${props =>
`; props.withTime &&
css`
const withTimeStyles = `
width: 360px; width: 360px;
padding-right: 90px; padding-right: 90px;
`}
`; `;
export const DateSection = styled.div` export const DateSection = styled.div`
@@ -78,9 +78,9 @@ export const Day = styled.div`
line-height: 30px; line-height: 30px;
border-radius: 3px; border-radius: 3px;
${font.size(15)} ${font.size(15)}
${props => (!props.isFiller ? hoverStyles : '')} ${props => !props.isFiller && hoverStyles}
${props => (props.isToday ? font.bold : '')} ${props => props.isToday && font.bold}
${props => (props.isSelected ? selectedStyles : '')} ${props => props.isSelected && selectedStyles}
`; `;
export const TimeSection = styled.div` export const TimeSection = styled.div`
@@ -97,20 +97,21 @@ export const TimeSection = styled.div`
export const Time = styled.div` export const Time = styled.div`
padding: 5px 0 5px 20px; padding: 5px 0 5px 20px;
${font.size(14)} ${font.size(14)}
${props => (!props.isFiller ? hoverStyles : '')} ${props => !props.isFiller && hoverStyles}
${props => (props.isSelected ? selectedStyles : '')} ${props => props.isSelected && selectedStyles}
`; `;
const hoverStyles = ` const hoverStyles = css`
${mixin.clickable} ${mixin.clickable}
&:hover { &:hover {
background: ${color.backgroundMedium}; background: ${color.backgroundMedium};
} }
`; `;
const selectedStyles = ` const selectedStyles = css`
color: #fff; color: #fff;
&:hover, & { &:hover,
& {
background: ${color.primary}; background: ${color.primary};
} }
`; `;

View File

@@ -4,6 +4,7 @@ import moment from 'moment';
import { range } from 'lodash'; import { range } from 'lodash';
import { formatDate, formatDateTimeForAPI } from 'shared/utils/dateTime'; import { formatDate, formatDateTimeForAPI } from 'shared/utils/dateTime';
import { TimeSection, Time } from './Styles'; import { TimeSection, Time } from './Styles';
const propTypes = { const propTypes = {
@@ -13,32 +14,20 @@ const propTypes = {
}; };
const defaultProps = { const defaultProps = {
value: null, value: undefined,
}; };
const DatePickerTimeSection = ({ value, onChange, setDropdownOpen }) => { const DatePickerTimeSection = ({ value, onChange, setDropdownOpen }) => {
const $sectionRef = useRef(); const $sectionRef = useRef();
const formattedTimeValue = formatDate(value, 'HH:mm');
useLayoutEffect(() => { useLayoutEffect(() => {
const scrollToSelectedTime = () => { scrollToSelectedTime($sectionRef.current, value);
if (!$sectionRef.current) return; }, [value]);
const $selectedTime = $sectionRef.current.querySelector(
`[data-time="${formattedTimeValue}"]`,
);
if (!$selectedTime) return;
$sectionRef.current.scrollTop = $selectedTime.offsetTop - 80;
};
scrollToSelectedTime();
}, [formattedTimeValue]);
const handleTimeChange = newTime => { const handleTimeChange = newTime => {
const [newHour, newMinute] = newTime.split(':'); const [newHour, newMinute] = newTime.split(':');
const existingDate = moment(value || undefined);
const existingDateWithNewTime = existingDate.set({ const existingDateWithNewTime = moment(value).set({
hour: Number(newHour), hour: Number(newHour),
minute: Number(newMinute), minute: Number(newMinute),
}); });
@@ -46,21 +35,13 @@ const DatePickerTimeSection = ({ value, onChange, setDropdownOpen }) => {
setDropdownOpen(false); setDropdownOpen(false);
}; };
const generateTimes = () =>
range(48).map(i => {
const hour = `${Math.floor(i / 2)}`;
const paddedHour = hour.length < 2 ? `0${hour}` : hour;
const minute = i % 2 === 0 ? '00' : '30';
return `${paddedHour}:${minute}`;
});
return ( return (
<TimeSection ref={$sectionRef}> <TimeSection ref={$sectionRef}>
{generateTimes().map(time => ( {generateTimes().map(time => (
<Time <Time
key={time} key={time}
data-time={time} data-time={time}
isSelected={time === formattedTimeValue} isSelected={time === formatTime(value)}
onClick={() => handleTimeChange(time)} onClick={() => handleTimeChange(time)}
> >
{time} {time}
@@ -70,6 +51,25 @@ const DatePickerTimeSection = ({ value, onChange, setDropdownOpen }) => {
); );
}; };
const formatTime = value => formatDate(value, 'HH:mm');
const scrollToSelectedTime = ($scrollCont, value) => {
if (!$scrollCont) return;
const $selectedTime = $scrollCont.querySelector(`[data-time="${formatTime(value)}"]`);
if (!$selectedTime) return;
$scrollCont.scrollTop = $selectedTime.offsetTop - 80;
};
const generateTimes = () =>
range(48).map(i => {
const hour = `${Math.floor(i / 2)}`;
const paddedHour = hour.length < 2 ? `0${hour}` : hour;
const minute = i % 2 === 0 ? '00' : '30';
return `${paddedHour}:${minute}`;
});
DatePickerTimeSection.propTypes = propTypes; DatePickerTimeSection.propTypes = propTypes;
DatePickerTimeSection.defaultProps = defaultProps; DatePickerTimeSection.defaultProps = defaultProps;

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import { formatDate, formatDateTime } from 'shared/utils/dateTime'; import { formatDate, formatDateTime } from 'shared/utils/dateTime';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import Input from 'shared/components/Input'; import Input from 'shared/components/Input';
import DateSection from './DateSection'; import DateSection from './DateSection';
import TimeSection from './TimeSection'; import TimeSection from './TimeSection';
import { StyledDatePicker, Dropdown } from './Styles'; import { StyledDatePicker, Dropdown } from './Styles';
@@ -18,7 +19,7 @@ const propTypes = {
const defaultProps = { const defaultProps = {
className: undefined, className: undefined,
withTime: true, withTime: true,
value: null, value: undefined,
}; };
const DatePicker = ({ className, withTime, value, onChange, ...inputProps }) => { const DatePicker = ({ className, withTime, value, onChange, ...inputProps }) => {
@@ -27,11 +28,6 @@ const DatePicker = ({ className, withTime, value, onChange, ...inputProps }) =>
useOnOutsideClick($containerRef, isDropdownOpen, () => setDropdownOpen(false)); useOnOutsideClick($containerRef, isDropdownOpen, () => setDropdownOpen(false));
const formatValueForInput = () => {
if (!value) return '';
return withTime ? formatDateTime(value) : formatDate(value);
};
return ( return (
<StyledDatePicker ref={$containerRef}> <StyledDatePicker ref={$containerRef}>
<Input <Input
@@ -39,7 +35,7 @@ const DatePicker = ({ className, withTime, value, onChange, ...inputProps }) =>
{...inputProps} {...inputProps}
className={className} className={className}
autoComplete="off" autoComplete="off"
value={formatValueForInput()} value={getFormattedInputValue(value, withTime)}
onClick={() => setDropdownOpen(true)} onClick={() => setDropdownOpen(true)}
/> />
{isDropdownOpen && ( {isDropdownOpen && (
@@ -59,6 +55,11 @@ const DatePicker = ({ className, withTime, value, onChange, ...inputProps }) =>
); );
}; };
const getFormattedInputValue = (value, withTime) => {
if (!value) return '';
return withTime ? formatDateTime(value) : formatDate(value);
};
DatePicker.propTypes = propTypes; DatePicker.propTypes = propTypes;
DatePicker.defaultProps = defaultProps; DatePicker.defaultProps = defaultProps;

View File

@@ -7,6 +7,7 @@ import Select from 'shared/components/Select';
import Textarea from 'shared/components/Textarea'; import Textarea from 'shared/components/Textarea';
import TextEditor from 'shared/components/TextEditor'; import TextEditor from 'shared/components/TextEditor';
import DatePicker from 'shared/components/DatePicker'; import DatePicker from 'shared/components/DatePicker';
import { StyledField, FieldLabel, FieldTip, FieldError } from './Styles'; import { StyledField, FieldLabel, FieldTip, FieldError } from './Styles';
const propTypes = { const propTypes = {
@@ -24,12 +25,12 @@ const defaultProps = {
}; };
const generateField = FormComponent => { const generateField = FormComponent => {
const FieldComponent = ({ className, label, tip, error, ...props }) => { const FieldComponent = ({ className, label, tip, error, ...otherProps }) => {
const fieldId = uniqueId('form-field-'); const fieldId = uniqueId('form-field-');
return ( return (
<StyledField className={className} hasLabel={!!label}> <StyledField className={className} hasLabel={!!label}>
{label && <FieldLabel htmlFor={fieldId}>{label}</FieldLabel>} {label && <FieldLabel htmlFor={fieldId}>{label}</FieldLabel>}
<FormComponent id={fieldId} invalid={!!error} {...props} /> <FormComponent id={fieldId} invalid={!!error} {...otherProps} />
{tip && <FieldTip>{tip}</FieldTip>} {tip && <FieldTip>{tip}</FieldTip>}
{error && <FieldError>{error}</FieldError>} {error && <FieldError>{error}</FieldError>}
</StyledField> </StyledField>

View File

@@ -4,6 +4,7 @@ import { Formik, Form as FormikForm, Field as FormikField } from 'formik';
import { get, mapValues } from 'lodash'; import { get, mapValues } from 'lodash';
import { is, generateErrors } from 'shared/utils/validation'; import { is, generateErrors } from 'shared/utils/validation';
import Field from './Field'; import Field from './Field';
const propTypes = { const propTypes = {

View File

@@ -1,6 +1,6 @@
import styled from 'styled-components'; import styled from 'styled-components';
export default styled.i` export const StyledIcon = styled.i`
display: inline-block; display: inline-block;
font-size: ${props => `${props.size}px`}; font-size: ${props => `${props.size}px`};
${props => ${props =>

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import StyledIcon from './Styles'; import { StyledIcon } from './Styles';
const codes = { const fontIconCodes = {
[`bug`]: '\\e90f', [`bug`]: '\\e90f',
[`stopwatch`]: '\\e914', [`stopwatch`]: '\\e914',
[`task`]: '\\e910', [`task`]: '\\e910',
@@ -33,11 +33,14 @@ const codes = {
[`component`]: '\\e91a', [`component`]: '\\e91a',
[`reports`]: '\\e91b', [`reports`]: '\\e91b',
[`page`]: '\\e919', [`page`]: '\\e919',
[`calendar`]: '\\e91d',
[`arrow-left`]: '\\e91e',
[`arrow-right`]: '\\e91f',
}; };
const propTypes = { const propTypes = {
type: PropTypes.oneOf(Object.keys(codes)).isRequired,
className: PropTypes.string, className: PropTypes.string,
type: PropTypes.oneOf(Object.keys(fontIconCodes)).isRequired,
size: PropTypes.number, size: PropTypes.number,
left: PropTypes.number, left: PropTypes.number,
top: PropTypes.number, top: PropTypes.number,
@@ -50,7 +53,7 @@ const defaultProps = {
top: 0, top: 0,
}; };
const Icon = ({ type, ...iconProps }) => <StyledIcon {...iconProps} code={codes[type]} />; const Icon = ({ type, ...iconProps }) => <StyledIcon {...iconProps} code={fontIconCodes[type]} />;
Icon.propTypes = propTypes; Icon.propTypes = propTypes;
Icon.defaultProps = defaultProps; Icon.defaultProps = defaultProps;

View File

@@ -1,13 +1,16 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color, font } from 'shared/utils/styles';
import Icon from 'shared/components/Icon';
export default styled.div` export const StyledInput = styled.div`
position: relative; position: relative;
display: inline-block; display: inline-block;
height: 32px; height: 32px;
width: 100%; width: 100%;
input { `;
export const InputElement = styled.input`
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 0 7px; padding: 0 7px;
@@ -17,6 +20,7 @@ export default styled.div`
transition: background 0.1s; transition: background 0.1s;
${font.regular} ${font.regular}
${font.size(15)} ${font.size(15)}
${props => props.hasIcon && 'padding-left: 32px;'}
&:hover { &:hover {
background: ${color.backgroundLight}; background: ${color.backgroundLight};
} }
@@ -25,7 +29,6 @@ export default styled.div`
border: 1px solid ${color.borderInputFocus}; border: 1px solid ${color.borderInputFocus};
box-shadow: 0 0 0 1px ${color.borderInputFocus}; box-shadow: 0 0 0 1px ${color.borderInputFocus};
} }
${props => props.icon && 'padding-left: 32px;'}
${props => ${props =>
props.invalid && props.invalid &&
css` css`
@@ -35,12 +38,12 @@ export default styled.div`
box-shadow: none; box-shadow: none;
} }
`} `}
} `;
i {
export const StyledIcon = styled(Icon)`
position: absolute; position: absolute;
top: 8px; top: 8px;
left: 8px; left: 8px;
pointer-events: none; pointer-events: none;
color: ${color.textMedium}; color: ${color.textMedium};
}
`; `;

View File

@@ -1,37 +1,37 @@
import React, { forwardRef } from 'react'; import React, { forwardRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Icon from 'shared/components/Icon'; import { StyledInput, InputElement, StyledIcon } from './Styles';
import StyledInput from './Styles';
const propTypes = { const propTypes = {
icon: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
invalid: PropTypes.bool,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
icon: PropTypes.string,
invalid: PropTypes.bool,
filter: PropTypes.instanceOf(RegExp), filter: PropTypes.instanceOf(RegExp),
onChange: PropTypes.func, onChange: PropTypes.func,
}; };
const defaultProps = { const defaultProps = {
icon: undefined,
className: undefined, className: undefined,
invalid: false,
value: undefined, value: undefined,
icon: undefined,
invalid: false,
filter: undefined, filter: undefined,
onChange: () => {}, onChange: () => {},
}; };
const Input = forwardRef(({ icon, className, invalid, filter, onChange, ...inputProps }, ref) => { const Input = forwardRef(({ icon, className, filter, onChange, ...inputProps }, ref) => {
const handleChange = event => { const handleChange = event => {
if (!filter || filter.test(event.target.value)) { if (!filter || filter.test(event.target.value)) {
onChange(event.target.value, event); onChange(event.target.value, event);
} }
}; };
return ( return (
<StyledInput className={className} icon={icon} invalid={invalid}> <StyledInput className={className}>
{icon && <Icon type={icon} size={15} />} {icon && <StyledIcon type={icon} size={15} />}
<input {...inputProps} onChange={handleChange} ref={ref} /> <InputElement {...inputProps} onChange={handleChange} hasIcon={!!icon} ref={ref} />
</StyledInput> </StyledInput>
); );
}); });

View File

@@ -1,7 +1,7 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Icon } from 'shared/components';
import { issuePriorityColors } from 'shared/utils/styles'; import { issuePriorityColors } from 'shared/utils/styles';
import { Icon } from 'shared/components';
export const PriorityIcon = styled(Icon)` export const PriorityIcon = styled(Icon)`
font-size: 18px; font-size: 18px;

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { IssuePriority } from 'shared/constants/issues'; import { IssuePriority } from 'shared/constants/issues';
import { PriorityIcon } from './Styles'; import { PriorityIcon } from './Styles';
const propTypes = { const propTypes = {

View File

@@ -1,7 +1,7 @@
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import Icon from 'shared/components/Icon';
import { color, mixin, zIndexValues } from 'shared/utils/styles'; import { color, mixin, zIndexValues } from 'shared/utils/styles';
import Icon from 'shared/components/Icon';
export const ScrollOverlay = styled.div` export const ScrollOverlay = styled.div`
z-index: ${zIndexValues.modal}; z-index: ${zIndexValues.modal};

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown'; import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
import { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Styles'; import { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Styles';
const propTypes = { const propTypes = {

View File

@@ -1,7 +1,8 @@
import styled from 'styled-components'; import styled from 'styled-components';
import { Icon } from 'shared/components';
import { color, font, mixin } from 'shared/utils/styles'; import { color, font, mixin } from 'shared/utils/styles';
import { Icon } from 'shared/components';
import imageBackground from './assets/background-forest.jpg'; import imageBackground from './assets/background-forest.jpg';
export const ErrorPage = styled.div` export const ErrorPage = styled.div`

View File

@@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner';
import StyledPageLoader from './Styles'; import StyledPageLoader from './Styles';
const PageLoader = () => ( const PageLoader = () => (

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import { KeyCodes } from 'shared/constants/keyCodes'; import { KeyCodes } from 'shared/constants/keyCodes';
import { ClearIcon, Dropdown, DropdownInput, Options, Option, OptionsNoResults } from './Styles'; import { ClearIcon, Dropdown, DropdownInput, Options, Option, OptionsNoResults } from './Styles';
const propTypes = { const propTypes = {
@@ -86,6 +87,7 @@ const SelectDropdown = ({
const handleInputEnterKeyDown = event => { const handleInputEnterKeyDown = event => {
event.preventDefault(); event.preventDefault();
const $active = getActiveOptionNode(); const $active = getActiveOptionNode();
if (!$active) return; if (!$active) return;
@@ -156,25 +158,20 @@ const SelectDropdown = ({
? removeSelectedOptionsMulti(optionsFilteredBySearchValue) ? removeSelectedOptionsMulti(optionsFilteredBySearchValue)
: removeSelectedOptionsSingle(optionsFilteredBySearchValue); : removeSelectedOptionsSingle(optionsFilteredBySearchValue);
const searchValueNotInOptions = !options.map(option => option.label).includes(searchValue); const isSearchValueInOptions = options.map(option => option.label).includes(searchValue);
const isOptionCreatable = onCreate && searchValue && searchValueNotInOptions; const isOptionCreatable = onCreate && searchValue && !isSearchValueInOptions;
const renderSelectableOption = (option, i) => { const renderSelectableOption = (option, i) => (
const optionProps = { <Option
key: option.value, key={option.value}
value: option.value, className={i === 0 ? activeOptionClass : undefined}
label: option.label, data-select-option-value={option.value}
className: i === 0 ? activeOptionClass : undefined, onMouseEnter={handleOptionMouseEnter}
'data-select-option-value': option.value, onClick={() => selectOptionValue(option.value)}
onMouseEnter: handleOptionMouseEnter, >
onClick: () => selectOptionValue(option.value),
};
return (
<Option {...optionProps}>
{propsRenderOption ? propsRenderOption(option) : option.label} {propsRenderOption ? propsRenderOption(option) : option.label}
</Option> </Option>
); );
};
const renderCreatableOption = () => ( const renderCreatableOption = () => (
<Option <Option

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { KeyCodes } from 'shared/constants/keyCodes'; import { KeyCodes } from 'shared/constants/keyCodes';
import Icon from 'shared/components/Icon'; import Icon from 'shared/components/Icon';
import Dropdown from './Dropdown'; import Dropdown from './Dropdown';
import { import {
StyledSelect, StyledSelect,

View File

@@ -1,11 +1,10 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import 'quill/dist/quill.snow.css';
import { Content } from './Styles'; import { Content } from './Styles';
import('quill/dist/quill.snow.css');
const propTypes = { const propTypes = {
content: PropTypes.string.isRequired, content: PropTypes.string.isRequired,
}; };

View File

@@ -1,11 +1,10 @@
import React, { useLayoutEffect, useRef } from 'react'; import React, { useLayoutEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Quill from 'quill'; import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import { EditorCont } from './Styles'; import { EditorCont } from './Styles';
import('quill/dist/quill.snow.css');
const propTypes = { const propTypes = {
className: PropTypes.string, className: PropTypes.string,
placeholder: PropTypes.string, placeholder: PropTypes.string,
@@ -37,29 +36,26 @@ const TextEditor = ({
}) => { }) => {
const $editorContRef = useRef(); const $editorContRef = useRef();
const $editorRef = useRef(); const $editorRef = useRef();
const quillRef = useRef();
const initialValueRef = useRef(defaultValue || alsoDefaultValue || ''); const initialValueRef = useRef(defaultValue || alsoDefaultValue || '');
useLayoutEffect(() => { useLayoutEffect(() => {
const setupQuill = () => { let quill = new Quill($editorRef.current, { placeholder, ...quillConfig });
quillRef.current = new Quill($editorRef.current, { placeholder, ...quillConfig });
};
const insertInitialValue = () => { const insertInitialValue = () => {
quillRef.current.clipboard.dangerouslyPasteHTML(0, initialValueRef.current); quill.clipboard.dangerouslyPasteHTML(0, initialValueRef.current);
}; };
const handleContentsChange = () => { const handleContentsChange = () => {
onChange(getHTMLValue()); onChange(getHTMLValue());
}; };
const getHTMLValue = () => $editorContRef.current.querySelector('.ql-editor').innerHTML; const getHTMLValue = () => $editorContRef.current.querySelector('.ql-editor').innerHTML;
setupQuill();
insertInitialValue(); insertInitialValue();
getEditor({ getValue: getHTMLValue }); getEditor({ getValue: getHTMLValue });
quillRef.current.on('text-change', handleContentsChange); quill.on('text-change', handleContentsChange);
return () => { return () => {
quillRef.current.off('text-change', handleContentsChange); quill.off('text-change', handleContentsChange);
quillRef.current = null; quill = null;
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View File

@@ -1,8 +1,8 @@
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { color, font } from 'shared/utils/styles'; import { color, font } from 'shared/utils/styles';
export default styled.div` export const StyledTextarea = styled.div`
display: inline-block; display: inline-block;
width: 100%; width: 100%;
textarea { textarea {
@@ -19,6 +19,13 @@ export default styled.div`
border: 1px solid ${color.borderInputFocus}; border: 1px solid ${color.borderInputFocus};
box-shadow: 0 0 0 1px ${color.borderInputFocus}; box-shadow: 0 0 0 1px ${color.borderInputFocus};
} }
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')} ${props =>
props.invalid &&
css`
&,
&:focus {
border: 1px solid ${color.danger};
}
`}
} }
`; `;

View File

@@ -2,7 +2,7 @@ import React, { forwardRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TextareaAutoSize from 'react-textarea-autosize'; import TextareaAutoSize from 'react-textarea-autosize';
import StyledTextarea from './Styles'; import { StyledTextarea } from './Styles';
const propTypes = { const propTypes = {
className: PropTypes.string, className: PropTypes.string,

View File

@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnOutsideClick from 'shared/hooks/onOutsideClick';
import { StyledTooltip } from './Styles'; import { StyledTooltip } from './Styles';
const propTypes = { const propTypes = {

View File

@@ -1,90 +0,0 @@
import { useState, useRef, useCallback, useEffect } from 'react';
import api from 'shared/utils/api';
import useDeepCompareMemoize from './deepCompareMemoize';
const useApi = (method, url, variables = {}, { lazy = false } = {}) => {
const isCalledAutomatically = method === 'get' && !lazy;
const [state, setState] = useState({
data: null,
error: null,
isWorking: isCalledAutomatically,
additionalVariables: {},
});
const setStateMerge = newState => setState(currentState => ({ ...currentState, ...newState }));
const wasCalledRef = useRef(false);
const variablesMemoized = useDeepCompareMemoize(variables);
const stateRef = useRef();
stateRef.current = state;
const makeRequest = useCallback(
(newVariables = {}) =>
new Promise((resolve, reject) => {
const additionalVariables = { ...stateRef.current.additionalVariables, ...newVariables };
if (!isCalledAutomatically || wasCalledRef.current) {
setStateMerge({ additionalVariables, isWorking: true });
}
api[method](url, { ...variablesMemoized, ...additionalVariables }).then(
data => {
resolve(data);
setStateMerge({ data, error: null, isWorking: false });
},
error => {
reject(error);
setStateMerge({ error, data: null, isWorking: false });
},
);
wasCalledRef.current = true;
}),
[method, variablesMemoized, isCalledAutomatically, url],
);
useEffect(() => {
if (isCalledAutomatically) {
makeRequest();
}
}, [makeRequest, isCalledAutomatically]);
const setLocalData = useCallback(
getUpdatedData =>
setState(currentState => ({ ...currentState, data: getUpdatedData(currentState.data) })),
[],
);
const result = [
{
...state,
[isWorkingAlias[method]]: state.isWorking,
wasCalled: wasCalledRef.current,
variables: { ...variablesMemoized, ...state.additionalVariables },
setLocalData,
},
makeRequest,
];
return result;
};
const isWorkingAlias = {
get: 'isLoading',
post: 'isCreating',
put: 'isUpdating',
patch: 'isUpdating',
delete: 'isDeleting',
};
/* eslint-disable react-hooks/rules-of-hooks */
export default {
get: (...args) => useApi('get', ...args),
post: (...args) => useApi('post', ...args),
put: (...args) => useApi('put', ...args),
patch: (...args) => useApi('patch', ...args),
delete: (...args) => useApi('delete', ...args),
};

View File

@@ -0,0 +1,11 @@
import useQuery from './query';
import useMutation from './mutation';
/* eslint-disable react-hooks/rules-of-hooks */
export default {
get: (...args) => useQuery(...args),
post: (...args) => useMutation('post', ...args),
put: (...args) => useMutation('put', ...args),
patch: (...args) => useMutation('patch', ...args),
delete: (...args) => useMutation('delete', ...args),
};

View File

@@ -0,0 +1,42 @@
import { useCallback } from 'react';
import api from 'shared/utils/api';
import useMergeState from 'shared/hooks/mergeState';
const useMutation = (method, url) => {
const [state, mergeState] = useMergeState({
data: null,
error: null,
isWorking: false,
});
const makeRequest = useCallback(
(variables = {}) =>
new Promise((resolve, reject) => {
mergeState({ isWorking: true });
api[method](url, variables).then(
data => {
resolve(data);
mergeState({ data, error: null, isWorking: false });
},
error => {
reject(error);
mergeState({ error, data: null, isWorking: false });
},
);
}),
[method, url, mergeState],
);
return [{ ...state, [isWorkingAlias[method]]: state.isWorking }, makeRequest];
};
const isWorkingAlias = {
post: 'isCreating',
put: 'isUpdating',
patch: 'isUpdating',
delete: 'isDeleting',
};
export default useMutation;

View File

@@ -0,0 +1,92 @@
import { useRef, useCallback, useEffect } from 'react';
import { isEqual } from 'lodash';
import api from 'shared/utils/api';
import useMergeState from 'shared/hooks/mergeState';
import useDeepCompareMemoize from 'shared/hooks/deepCompareMemoize';
const useQuery = (
url,
propsVariables = {},
{ lazy = false, cachePolicy = CachePolicy.CACHE_FIRST } = {},
) => {
const [state, mergeState] = useMergeState({
data: null,
error: null,
isLoading: !lazy,
wasCalled: !lazy,
variables: {},
});
const wasCalledRef = useRef(false);
const propsVariablesMemoized = useDeepCompareMemoize(propsVariables);
const stateRef = useRef();
stateRef.current = state;
const makeRequest = useCallback(
(newVariables = {}) => {
const variables = { ...stateRef.current.variables, ...newVariables };
const apiVariables = { ...propsVariablesMemoized, ...variables };
const isCacheAvailable = cache[url] && isEqual(cache[url].apiVariables, apiVariables);
const isCacheAvailableAndPermitted = isCacheAvailable && cachePolicy !== CachePolicy.NO_CACHE;
if (isCacheAvailableAndPermitted) {
mergeState({ data: cache[url].data, error: null, isLoading: false, variables });
if (cachePolicy === CachePolicy.CACHE_ONLY) {
return;
}
}
if (!isCacheAvailableAndPermitted && (lazy || wasCalledRef.current)) {
mergeState({ isLoading: true, variables });
}
api.get(url, apiVariables).then(
data => {
cache[url] = { apiVariables, data };
mergeState({ data, error: null, isLoading: false });
},
error => {
mergeState({ error, data: null, isLoading: false });
},
);
wasCalledRef.current = true;
},
[propsVariablesMemoized, cachePolicy, url, lazy, mergeState],
);
useEffect(() => {
if (!lazy || wasCalledRef.current) {
makeRequest();
}
}, [lazy, makeRequest]);
const setLocalData = useCallback(
getUpdatedData => mergeState(({ data }) => ({ data: getUpdatedData(data) })),
[mergeState],
);
return [
{
...state,
wasCalled: wasCalledRef.current,
variables: { ...propsVariablesMemoized, ...state.variables },
setLocalData,
},
makeRequest,
];
};
const cache = {};
const CachePolicy = {
CACHE_ONLY: 'cache-only',
CACHE_FIRST: 'cache-first',
NO_CACHE: 'no-cache',
};
export default useQuery;

View File

@@ -0,0 +1,18 @@
import { useState, useCallback } from 'react';
import { isFunction } from 'lodash';
const useMergeState = initialState => {
const [state, setState] = useState(initialState || {});
const mergeState = useCallback(newState => {
if (isFunction(newState)) {
setState(currentState => ({ ...currentState, ...newState(currentState) }));
} else {
setState(currentState => ({ ...currentState, ...newState }));
}
}, []);
return [state, mergeState];
};
export default useMergeState;

View File

@@ -9,9 +9,11 @@ const useOnEscapeKeyDown = (isListening, onEscapeKeyDown) => {
onEscapeKeyDown(); onEscapeKeyDown();
} }
}; };
if (isListening) { if (isListening) {
document.addEventListener('keydown', handleKeyDown); document.addEventListener('keydown', handleKeyDown);
} }
return () => { return () => {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', handleKeyDown);
}; };

View File

@@ -6,7 +6,7 @@ const useOnOutsideClick = (
$ignoredElementRefs, $ignoredElementRefs,
shouldListen, shouldListen,
onOutsideClick, onOutsideClick,
$listeningElementRef = {}, $listeningElementRef,
) => { ) => {
const $mouseDownTargetRef = useRef(); const $mouseDownTargetRef = useRef();
const $ignoredElementRefsMemoized = useDeepCompareMemoize([$ignoredElementRefs].flat()); const $ignoredElementRefsMemoized = useDeepCompareMemoize([$ignoredElementRefs].flat());
@@ -15,21 +15,25 @@ const useOnOutsideClick = (
const handleMouseDown = event => { const handleMouseDown = event => {
$mouseDownTargetRef.current = event.target; $mouseDownTargetRef.current = event.target;
}; };
const handleMouseUp = event => { const handleMouseUp = event => {
const noIgnoredElementsContainTarget = $ignoredElementRefsMemoized.every( const isAnyIgnoredElementParentOfTarget = $ignoredElementRefsMemoized.some(
$elementRef => $elementRef =>
!$elementRef.current.contains($mouseDownTargetRef.current) && $elementRef.current.contains($mouseDownTargetRef.current) ||
!$elementRef.current.contains(event.target), $elementRef.current.contains(event.target),
); );
if (event.button === 0 && noIgnoredElementsContainTarget) { if (event.button === 0 && !isAnyIgnoredElementParentOfTarget) {
onOutsideClick(); onOutsideClick();
} }
}; };
const $listeningElement = $listeningElementRef.current || document;
const $listeningElement = ($listeningElementRef || {}).current || document;
if (shouldListen) { if (shouldListen) {
$listeningElement.addEventListener('mousedown', handleMouseDown); $listeningElement.addEventListener('mousedown', handleMouseDown);
$listeningElement.addEventListener('mouseup', handleMouseUp); $listeningElement.addEventListener('mouseup', handleMouseUp);
} }
return () => { return () => {
$listeningElement.removeEventListener('mousedown', handleMouseDown); $listeningElement.removeEventListener('mousedown', handleMouseDown);
$listeningElement.removeEventListener('mouseup', handleMouseUp); $listeningElement.removeEventListener('mouseup', handleMouseUp);

View File

@@ -1,3 +1,5 @@
export const getStoredAuthToken = () => localStorage.getItem('authToken'); export const getStoredAuthToken = () => localStorage.getItem('authToken');
export const storeAuthToken = token => localStorage.setItem('authToken', token); export const storeAuthToken = token => localStorage.setItem('authToken', token);
export const removeStoredAuthToken = () => localStorage.removeItem('authToken'); export const removeStoredAuthToken = () => localStorage.removeItem('authToken');

View File

@@ -0,0 +1,18 @@
export const getTextContentsFromHtmlString = html => {
const el = document.createElement('div');
el.innerHTML = html;
return el.textContent;
};
export const copyToClipboard = value => {
const $textarea = document.createElement('textarea');
$textarea.value = value;
document.body.appendChild($textarea);
$textarea.select();
document.execCommand('copy');
document.body.removeChild($textarea);
};
export const isFocusedElementEditable = () =>
!!document.activeElement.getAttribute('contenteditable') ||
['TEXTAREA', 'INPUT'].includes(document.activeElement.tagName);

View File

@@ -1,8 +0,0 @@
export const copyToClipboard = value => {
const $textarea = document.createElement('textarea');
$textarea.value = value;
document.body.appendChild($textarea);
$textarea.select();
document.execCommand('copy');
document.body.removeChild($textarea);
};

View File

@@ -1,3 +0,0 @@
export const isFocusedElementEditable = () =>
!!document.activeElement.getAttribute('contenteditable') ||
['TEXTAREA', 'INPUT'].includes(document.activeElement.tagName);

View File

@@ -1,5 +0,0 @@
export const getTextContentsFromHtmlString = html => {
const el = document.createElement('div');
el.innerHTML = html;
return el.textContent;
};

View File

@@ -1,4 +1,6 @@
import { css } from 'styled-components';
import Color from 'color'; import Color from 'color';
import { IssueType, IssueStatus, IssuePriority } from 'shared/constants/issues'; import { IssueType, IssueStatus, IssuePriority } from 'shared/constants/issues';
export const color = { export const color = {
@@ -87,43 +89,32 @@ export const mixin = {
Color(colorValue) Color(colorValue)
.alpha(opacity) .alpha(opacity)
.string(), .string(),
boxShadowMedium: ` boxShadowMedium: css`
box-shadow: 0 5px 10px 0 rgba(0,0,0,0.1); box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.1);
`, `,
boxShadowDropdown: ` boxShadowDropdown: css`
box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px; box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px;
`, `,
truncateText: ` truncateText: css`
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
`, `,
clickable: ` clickable: css`
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
`, `,
hardwareAccelerate: ` hardwareAccelerate: css`
transform: translateZ(0); transform: translateZ(0);
`, `,
clearfix: ` cover: css`
*zoom: 1;
&:before,
&:after {
content: " ";
display: table;
}
&:after {
clear: both;
}
`,
cover: `
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
`, `,
placeholderColor: colorValue => ` placeholderColor: colorValue => css`
::-webkit-input-placeholder { ::-webkit-input-placeholder {
color: ${colorValue} !important; color: ${colorValue} !important;
opacity: 1 !important; opacity: 1 !important;
@@ -141,12 +132,15 @@ export const mixin = {
opacity: 1 !important; opacity: 1 !important;
} }
`, `,
scrollableY: ` scrollableY: css`
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
`, `,
customScrollbar: ({ width = 8, background = mixin.darken(color.backgroundMedium, 0.2) } = {}) => ` customScrollbar: ({
width = 8,
background = mixin.darken(color.backgroundMedium, 0.2),
} = {}) => css`
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: ${width}px; width: ${width}px;
} }
@@ -158,14 +152,14 @@ export const mixin = {
background: ${background}; background: ${background};
} }
`, `,
backgroundImage: imageURL => ` backgroundImage: imageURL => css`
background-image: url("${imageURL}"); background-image: url("${imageURL}");
background-position: 50% 50%; background-position: 50% 50%;
background-repeat: no-repeat; background-repeat: no-repeat;
background-size: cover; background-size: cover;
background-color: ${color.backgroundLight}; background-color: ${color.backgroundLight};
`, `,
link: (colorValue = color.textLink) => ` link: (colorValue = color.textLink) => css`
cursor: pointer; cursor: pointer;
color: ${colorValue}; color: ${colorValue};
${font.medium} ${font.medium}
@@ -176,7 +170,7 @@ export const mixin = {
text-decoration: underline; text-decoration: underline;
} }
`, `,
tag: (background = color.backgroundMedium, colorValue = color.textDarkest) => ` tag: (background = color.backgroundMedium, colorValue = color.textDarkest) => css`
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
height: 24px; height: 24px;

View File

@@ -14,8 +14,4 @@ const error = err => {
}); });
}; };
export default { export default { show, error, success };
show,
error,
success,
};