Improved code styling
This commit is contained in:
@@ -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;
|
|
||||||
`;
|
|
||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -36,4 +36,7 @@
|
|||||||
<glyph unicode="" glyph-name="component" d="M618.965 537.387l-106.965-61.867-297.003 171.819 107.136 61.227zM809.003 647.339l-104.789-60.629-296.277 170.88 82.517 47.147c4.779 2.731 9.899 4.48 15.147 5.333 9.301 1.451 18.987-0.128 27.904-5.291zM491.776-41.002c6.016-3.243 12.928-5.077 20.224-5.077 7.381 0 14.336 1.877 20.395 5.163 15.189 2.475 29.909 7.68 43.392 15.36l298.709 170.709c26.368 15.232 45.269 38.315 55.424 64.597 5.675 14.592 8.619 30.165 8.747 46.251v341.333c0 20.395-4.821 39.723-13.397 56.917-0.939 3.029-2.219 5.973-3.883 8.832-1.963 3.371-4.267 6.357-6.912 8.96-1.323 1.835-2.731 3.669-4.139 5.419-9.813 12.203-21.845 22.528-35.456 30.507l-299.051 170.88c-26.027 15.019-55.467 19.84-83.328 15.531-15.531-2.432-30.507-7.637-44.288-15.488l-136.491-77.995c-8.96-1.749-17.323-6.4-23.595-13.483l-138.624-79.232c-16.341-9.429-29.824-21.888-40.149-36.267-2.56-2.56-4.864-5.547-6.784-8.832-1.664-2.901-2.987-5.888-3.925-8.96-1.707-3.456-3.243-6.955-4.608-10.496-5.632-14.635-8.576-30.208-8.704-45.995v-341.632c0.043-30.293 10.581-58.197 28.331-80.128 9.813-12.203 21.845-22.528 35.456-30.507l299.051-170.88c13.824-7.979 28.587-13.099 43.605-15.445zM469.333 401.622v-340.949l-277.12 158.336c-4.736 2.773-8.832 6.315-12.16 10.411-5.931 7.381-9.387 16.512-9.387 26.581v318.379zM554.667 60.672v340.949l298.667 172.757v-318.379c-0.043-5.163-1.067-10.496-2.987-15.445-3.413-8.789-9.6-16.384-18.176-21.333z" />
|
<glyph unicode="" glyph-name="component" d="M618.965 537.387l-106.965-61.867-297.003 171.819 107.136 61.227zM809.003 647.339l-104.789-60.629-296.277 170.88 82.517 47.147c4.779 2.731 9.899 4.48 15.147 5.333 9.301 1.451 18.987-0.128 27.904-5.291zM491.776-41.002c6.016-3.243 12.928-5.077 20.224-5.077 7.381 0 14.336 1.877 20.395 5.163 15.189 2.475 29.909 7.68 43.392 15.36l298.709 170.709c26.368 15.232 45.269 38.315 55.424 64.597 5.675 14.592 8.619 30.165 8.747 46.251v341.333c0 20.395-4.821 39.723-13.397 56.917-0.939 3.029-2.219 5.973-3.883 8.832-1.963 3.371-4.267 6.357-6.912 8.96-1.323 1.835-2.731 3.669-4.139 5.419-9.813 12.203-21.845 22.528-35.456 30.507l-299.051 170.88c-26.027 15.019-55.467 19.84-83.328 15.531-15.531-2.432-30.507-7.637-44.288-15.488l-136.491-77.995c-8.96-1.749-17.323-6.4-23.595-13.483l-138.624-79.232c-16.341-9.429-29.824-21.888-40.149-36.267-2.56-2.56-4.864-5.547-6.784-8.832-1.664-2.901-2.987-5.888-3.925-8.96-1.707-3.456-3.243-6.955-4.608-10.496-5.632-14.635-8.576-30.208-8.704-45.995v-341.632c0.043-30.293 10.581-58.197 28.331-80.128 9.813-12.203 21.845-22.528 35.456-30.507l299.051-170.88c13.824-7.979 28.587-13.099 43.605-15.445zM469.333 401.622v-340.949l-277.12 158.336c-4.736 2.773-8.832 6.315-12.16 10.411-5.931 7.381-9.387 16.512-9.387 26.581v318.379zM554.667 60.672v340.949l298.667 172.757v-318.379c-0.043-5.163-1.067-10.496-2.987-15.445-3.413-8.789-9.6-16.384-18.176-21.333z" />
|
||||||
<glyph unicode="" glyph-name="reports" d="M725.333 640h153.003l-302.336-302.336-183.168 183.168c-16.683 16.683-43.691 16.683-60.331 0l-320-320c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l289.835 289.835 183.168-183.168c16.683-16.683 43.691-16.683 60.331 0l332.501 332.501v-153.003c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 5.803-1.152 11.307-3.243 16.341s-5.163 9.728-9.216 13.781c-0.043 0.043-0.043 0.043-0.085 0.085-3.925 3.925-8.619 7.083-13.781 9.216-5.035 2.091-10.539 3.243-16.341 3.243h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
<glyph unicode="" glyph-name="reports" d="M725.333 640h153.003l-302.336-302.336-183.168 183.168c-16.683 16.683-43.691 16.683-60.331 0l-320-320c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l289.835 289.835 183.168-183.168c16.683-16.683 43.691-16.683 60.331 0l332.501 332.501v-153.003c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 5.803-1.152 11.307-3.243 16.341s-5.163 9.728-9.216 13.781c-0.043 0.043-0.043 0.043-0.085 0.085-3.925 3.925-8.619 7.083-13.781 9.216-5.035 2.091-10.539 3.243-16.341 3.243h-256c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
||||||
<glyph unicode="" glyph-name="shipping" d="M640 298.667h-554.667v469.333h554.667v-170.667zM725.333 554.667h110.336l102.997-102.997v-153.003h-213.333zM298.667 149.334c0-17.664-7.125-33.621-18.731-45.269s-27.605-18.731-45.269-18.731-33.621 7.125-45.269 18.731-18.731 27.605-18.731 45.269 7.125 33.621 18.731 45.269 27.605 18.731 45.269 18.731 33.621-7.125 45.269-18.731 18.731-27.605 18.731-45.269zM938.667 149.334c0 22.912-5.163 44.587-14.379 64h57.045c23.552 0 42.667 19.115 42.667 42.667v213.333c0 10.923-4.181 21.845-12.501 30.165l-128 128c-7.723 7.723-18.389 12.501-30.165 12.501h-128v170.667c0 23.552-19.115 42.667-42.667 42.667h-640c-23.552 0-42.667-19.115-42.667-42.667v-554.667c0-23.552 19.115-42.667 42.667-42.667h57.045c-9.216-19.413-14.379-41.088-14.379-64 0-41.216 16.768-78.635 43.733-105.6s64.384-43.733 105.6-43.733 78.635 16.768 105.6 43.733 43.733 64.384 43.733 105.6c0 22.912-5.163 44.587-14.379 64h284.757c-9.216-19.413-14.379-41.088-14.379-64 0-41.216 16.768-78.635 43.733-105.6s64.384-43.733 105.6-43.733 78.635 16.768 105.6 43.733 43.733 64.384 43.733 105.6zM853.333 149.334c0-17.664-7.125-33.621-18.731-45.269s-27.605-18.731-45.269-18.731-33.621 7.125-45.269 18.731-18.731 27.605-18.731 45.269 7.125 33.621 18.731 45.269 27.605 18.731 45.269 18.731 33.621-7.125 45.269-18.731 18.731-27.605 18.731-45.269z" />
|
<glyph unicode="" glyph-name="shipping" d="M640 298.667h-554.667v469.333h554.667v-170.667zM725.333 554.667h110.336l102.997-102.997v-153.003h-213.333zM298.667 149.334c0-17.664-7.125-33.621-18.731-45.269s-27.605-18.731-45.269-18.731-33.621 7.125-45.269 18.731-18.731 27.605-18.731 45.269 7.125 33.621 18.731 45.269 27.605 18.731 45.269 18.731 33.621-7.125 45.269-18.731 18.731-27.605 18.731-45.269zM938.667 149.334c0 22.912-5.163 44.587-14.379 64h57.045c23.552 0 42.667 19.115 42.667 42.667v213.333c0 10.923-4.181 21.845-12.501 30.165l-128 128c-7.723 7.723-18.389 12.501-30.165 12.501h-128v170.667c0 23.552-19.115 42.667-42.667 42.667h-640c-23.552 0-42.667-19.115-42.667-42.667v-554.667c0-23.552 19.115-42.667 42.667-42.667h57.045c-9.216-19.413-14.379-41.088-14.379-64 0-41.216 16.768-78.635 43.733-105.6s64.384-43.733 105.6-43.733 78.635 16.768 105.6 43.733 43.733 64.384 43.733 105.6c0 22.912-5.163 44.587-14.379 64h284.757c-9.216-19.413-14.379-41.088-14.379-64 0-41.216 16.768-78.635 43.733-105.6s64.384-43.733 105.6-43.733 78.635 16.768 105.6 43.733 43.733 64.384 43.733 105.6zM853.333 149.334c0-17.664-7.125-33.621-18.731-45.269s-27.605-18.731-45.269-18.731-33.621 7.125-45.269 18.731-18.731 27.605-18.731 45.269 7.125 33.621 18.731 45.269 27.605 18.731 45.269 18.731 33.621-7.125 45.269-18.731 18.731-27.605 18.731-45.269z" />
|
||||||
|
<glyph unicode="" 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="" 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="" 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.
Binary file not shown.
@@ -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';
|
||||||
|
|||||||
@@ -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,17 +11,18 @@ 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);
|
history.push('/');
|
||||||
history.push('/');
|
} catch (error) {
|
||||||
} catch (error) {
|
toast.error(error);
|
||||||
toast.error(error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
createGuestAccount();
|
|
||||||
|
if (!getStoredAuthToken()) {
|
||||||
|
createGuestAccount();
|
||||||
|
}
|
||||||
}, [history]);
|
}, [history]);
|
||||||
|
|
||||||
return <PageLoader />;
|
return <PageLoader />;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 =>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,46 +1,49 @@
|
|||||||
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 {
|
`;
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
export const InputElement = styled.input`
|
||||||
padding: 0 7px;
|
height: 100%;
|
||||||
border-radius: 3px;
|
width: 100%;
|
||||||
border: 1px solid ${color.borderLightest};
|
padding: 0 7px;
|
||||||
background: ${color.backgroundLightest};
|
border-radius: 3px;
|
||||||
transition: background 0.1s;
|
border: 1px solid ${color.borderLightest};
|
||||||
${font.regular}
|
background: ${color.backgroundLightest};
|
||||||
${font.size(15)}
|
transition: background 0.1s;
|
||||||
&:hover {
|
${font.regular}
|
||||||
background: ${color.backgroundLight};
|
${font.size(15)}
|
||||||
}
|
${props => props.hasIcon && 'padding-left: 32px;'}
|
||||||
&:focus {
|
&:hover {
|
||||||
background: #fff;
|
background: ${color.backgroundLight};
|
||||||
border: 1px solid ${color.borderInputFocus};
|
}
|
||||||
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
&:focus {
|
||||||
}
|
background: #fff;
|
||||||
${props => props.icon && 'padding-left: 32px;'}
|
border: 1px solid ${color.borderInputFocus};
|
||||||
${props =>
|
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
||||||
props.invalid &&
|
}
|
||||||
css`
|
${props =>
|
||||||
&,
|
props.invalid &&
|
||||||
&:focus {
|
css`
|
||||||
border: 1px solid ${color.danger};
|
&,
|
||||||
box-shadow: none;
|
&:focus {
|
||||||
}
|
border: 1px solid ${color.danger};
|
||||||
`}
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
i {
|
`}
|
||||||
position: absolute;
|
`;
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
export const StyledIcon = styled(Icon)`
|
||||||
pointer-events: none;
|
position: absolute;
|
||||||
color: ${color.textMedium};
|
top: 8px;
|
||||||
}
|
left: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
color: ${color.textMedium};
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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 = () => (
|
||||||
|
|||||||
@@ -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),
|
{propsRenderOption ? propsRenderOption(option) : option.label}
|
||||||
};
|
</Option>
|
||||||
return (
|
);
|
||||||
<Option {...optionProps}>
|
|
||||||
{propsRenderOption ? propsRenderOption(option) : option.label}
|
|
||||||
</Option>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderCreatableOption = () => (
|
const renderCreatableOption = () => (
|
||||||
<Option
|
<Option
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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};
|
||||||
|
}
|
||||||
|
`}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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),
|
|
||||||
};
|
|
||||||
11
client/src/shared/hooks/api/index.js
Normal file
11
client/src/shared/hooks/api/index.js
Normal 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),
|
||||||
|
};
|
||||||
42
client/src/shared/hooks/api/mutation.js
Normal file
42
client/src/shared/hooks/api/mutation.js
Normal 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;
|
||||||
92
client/src/shared/hooks/api/query.js
Normal file
92
client/src/shared/hooks/api/query.js
Normal 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;
|
||||||
18
client/src/shared/hooks/mergeState.js
Normal file
18
client/src/shared/hooks/mergeState.js
Normal 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;
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
18
client/src/shared/utils/browser.js
Normal file
18
client/src/shared/utils/browser.js
Normal 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);
|
||||||
@@ -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);
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export const isFocusedElementEditable = () =>
|
|
||||||
!!document.activeElement.getAttribute('contenteditable') ||
|
|
||||||
['TEXTAREA', 'INPUT'].includes(document.activeElement.tagName);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export const getTextContentsFromHtmlString = html => {
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.innerHTML = html;
|
|
||||||
return el.textContent;
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -14,8 +14,4 @@ const error = err => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default { show, error, success };
|
||||||
show,
|
|
||||||
error,
|
|
||||||
success,
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user