Remove JS garbage

This commit is contained in:
Adrian Woźniak 2020-04-20 16:43:51 +02:00
parent a16ddbcf2e
commit 761305fbbd
201 changed files with 0 additions and 67947 deletions

View File

@ -1,18 +0,0 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": 3
}
],
"@babel/react"
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-syntax-dynamic-import",
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}

View File

@ -1,45 +0,0 @@
{
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 8,
"ecmaFeatures": {
"jsx": true
}
},
"env": {
"browser": true,
"jest": true
},
"extends": ["airbnb", "prettier", "prettier/react"],
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"radix": 0,
"no-restricted-syntax": 0,
"no-await-in-loop": 0,
"no-console": 0,
"consistent-return": 0,
"no-param-reassign": [2, { "props": false }],
"no-return-assign": [2, "except-parens"],
"no-use-before-define": 0,
"import/prefer-default-export": 0,
"import/no-cycle": 0,
"react/no-array-index-key": 0,
"react/forbid-prop-types": 0,
"react/prop-types": [2, { "skipUndeclared": true }],
"react/jsx-fragments": [2, "element"],
"react/state-in-constructor": 0,
"react/jsx-props-no-spreading": 0,
"jsx-a11y/click-events-have-key-events": 0
},
"settings": {
// Allows us to lint absolute imports within codebase
"import/resolver": {
"node": {
"moduleDirectory": ["node_modules", "src/"]
}
}
}
}

View File

@ -1,2 +0,0 @@
node_modules
tmp

View File

@ -1,5 +0,0 @@
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -1,37 +0,0 @@
[
{
"test": ".*.tsx?$",
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": true,
"dynamicImport": true
}
}
},
{
"test": ".jsx?$",
"jsc": {
"target": "es2018",
"parser": {
"syntax": "ecmascript",
"jsx": true,
"dynamicImport": true,
"numericSeparator": true,
"classPrivateProperty": true,
"privateMethod": true,
"classProperty": true,
"functionBind": true,
"exportDefaultFrom": true,
"exportNamespaceFrom": true,
"decorators": true,
"decoratorsBeforeExport": true,
"nullishCoalescing": true,
"topLevelAwait": true,
"importMeta": true,
"optionalChaining": true
}
}
}
]

View File

@ -1,18 +0,0 @@
# Project structure 🏗
I've used this architecture on multiple larger projects in the past and it performed really well.
There are two special root folders in `src`: `App` and `shared` (described below). All other root folders in `src` (in our case only two: `Auth` and `Project`) should follow the structure of the routes. We can call these folders modules.
The main rule to follow: **Files from one module can only import from ancestor folders within the same module or from `src/shared`.** This makes the codebase easier to understand, and if you're fiddling with code in one module, you will never introduce a bug in another module.
<br>
| File or folder | Description |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src/index.jsx` | The entry file. This is where we import babel polyfills and render the App into the root DOM node. |
| `src/index.html` | The only HTML file in our App. All scripts and styles will be injected here by Webpack. |
| `src/App` | Main application routes, components that need to be mounted at all times regardless of current route, global css styles, fonts, etc. Basically anything considered global / ancestor of all modules. |
| `src/Auth` | Authentication module |
| `src/Project` | Project module |
| `src/shared` | Components, constants, utils, hooks, styles etc. that can be used anywhere in the codebase. Any module is allowed to import from shared. |

View File

@ -1,8 +0,0 @@
{
"baseUrl": "http://localhost:8080",
"viewportHeight": 800,
"viewportWidth": 1440,
"env": {
"apiBaseUrl": "http://localhost:3000"
}
}

View File

@ -1,9 +0,0 @@
module.exports = {
moduleFileExtensions: ['*', 'js', 'jsx'],
moduleDirectories: ['src', 'node_modules'],
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/jest/fileMock.js',
'\\.(css|scss|less)$': '<rootDir>/jest/styleMock.js',
},
};

View File

@ -1,10 +0,0 @@
// This config allows VSCode intellisense to work with absolute "src" imports and jsx files
{
"compilerOptions": {
"baseUrl": "./src",
"jsx": "react"
},
"include": [
"src/**/*",
"cypress/**/*.js", "./node_modules/cypress"]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,91 +0,0 @@
{
"name": "jira_client",
"version": "1.0.0",
"author": "Ivor Reic",
"license": "MIT",
"scripts": {
"start": "webpack-dev-server",
"start:production": "pm2 start --name 'jira_client' server.js",
"test:jest": "jest",
"test:cypress": "node_modules/.bin/cypress open",
"build": "rm -rf build && webpack --config webpack.config.production.js --progress",
"pre-commit": "lint-staged"
},
"devDependencies": {
"@babel/core": "^7.7.4",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-decorators": "^7.7.4",
"@babel/plugin-proposal-export-namespace-from": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/preset-env": "^7.7.4",
"@babel/preset-react": "^7.7.4",
"@cypress/webpack-preprocessor": "^4.1.1",
"@swc/core": "^1.1.36",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"css-loader": "^3.3.2",
"cypress": "^3.8.1",
"eslint": "^6.1.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-cypress": "^2.8.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react-hooks": "^1.7.0",
"file-loader": "^5.0.2",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.9.0",
"lint-staged": "^9.5.0",
"prettier": "^1.19.1",
"style-loader": "^1.0.1",
"swc-loader": "^0.1.8",
"url-loader": "^3.0.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
},
"dependencies": {
"@4tw/cypress-drag-drop": "^1.3.0",
"@types/axios": "^0.14.0",
"@types/react-redux": "^7.1.7",
"@types/redux": "^3.6.0",
"@types/redux-actions": "^2.6.1",
"@types/redux-saga": "^0.10.5",
"axios": "^0.19.2",
"color": "^3.1.2",
"compression": "^1.7.4",
"core-js": "^3.4.7",
"express": "^4.17.1",
"express-history-api-fallback": "^2.2.1",
"formik": "^2.1.1",
"history": "^4.10.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.15",
"moment": "^2.24.0",
"prop-types": "^15.7.2",
"query-string": "^6.9.0",
"quill": "^1.3.7",
"react": "^16.12.0",
"react-beautiful-dnd": "^12.2.0",
"react-content-loader": "^4.3.3",
"react-dom": "^16.12.0",
"react-redux": "^7.2.0",
"react-router-dom": "^5.1.2",
"react-textarea-autosize": "^7.1.2",
"react-transition-group": "^4.3.0",
"redux": "^4.0.5",
"redux-actions": "^2.6.5",
"redux-saga": "^1.1.3",
"regenerator-runtime": "^0.13.3",
"styled-components": "^4.4.1",
"sweet-pubsub": "^1.1.2"
},
"lint-staged": {
"*.{js,jsx}": [
"eslint --fix",
"prettier --write",
"git add"
]
}
}

View File

@ -1,13 +0,0 @@
const express = require('express');
const fallback = require('express-history-api-fallback');
const compression = require('compression');
const app = express();
app.use(compression());
app.use(express.static(`${__dirname}/build`));
app.use(fallback(`${__dirname}/build/index.html`));
app.listen(process.env.PORT || 8081);

View File

@ -1,133 +0,0 @@
import { createGlobalStyle } from 'styled-components';
import { color, font } from '../shared/utils/styles';
export default createGlobalStyle`
html, body, #root {
height: 100%;
min-height: 100%;
min-width: 768px;
}
body {
color: ${ color.textDarkest };
-webkit-tap-highlight-color: transparent;
line-height: 1.2;
font-size: 16px
${ font.regular };font-weight: normal;
}
#root {
display: flex;
flex-direction: column;
}
button,
input,
optgroup,
select,
textarea {
${ font.regular };font-weight: normal;
}
*, *:after, *:before, input[type="search"] {
box-sizing: border-box;
}
a {
color: inherit;
text-decoration: none;
}
ul {
list-style: none;
}
ul, li, ol, dd, h1, h2, h3, h4, h5, h6, p {
padding: 0;
margin: 0;
}
h1, h2, h3, h4, h5, h6, strong {
font-family: "CircularStdBold"; font-weight: normal
}
button {
background: none;
border: none;
}
/* Workaround for IE11 focus highlighting for select elements */
select::-ms-value {
background: none;
color: #42413d;
}
[role="button"], button, input, select, textarea {
outline: none;
&:focus {
outline: none;
}
&:disabled {
opacity: 1;
}
}
[role="button"], button, input, textarea {
appearance: none;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #000;
}
select::-ms-expand {
display: none;
}
select option {
color: ${ color.textDarkest };
}
p {
line-height: 1.4285;
a {
cursor: pointer;
color: ${ color.textLink };
${ font.medium };font-weight: normal;
&:hover, &:visited, &:active {
color: ${ color.textLink };
}
&:hover {
text-decoration: underline;
}
}
}
textarea {
line-height: 1.4285;
}
body, select {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
touch-action: manipulation;
}
::-webkit-input-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
:-moz-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
::-moz-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
:-ms-input-placeholder {
color: ${color.textLight} !important;
opacity: 1 !important;
}
`;

View File

@ -1,152 +0,0 @@
import { createGlobalStyle } from 'styled-components';
/** DO NOT ALTER THIS FILE. It is a copy of https://necolas.github.io/normalize.css/ */
export default createGlobalStyle`
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
main {
display: block;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
pre {
font-family: monospace, monospace;
font-size: 1em;
}
a {
background-color: transparent;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details {
display: block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none;
}
`;

View File

@ -1,20 +0,0 @@
import React from 'react';
import { Redirect, Route, Router, Switch } from 'react-router-dom';
import history from '../browserHistory';
import Project from '../Project';
import Authenticate from '../Auth/Authenticate';
import PageError from '../shared/components/PageError';
const Routes = () => (
<Router history={history}>
<Switch>
<Redirect exact from="/" to="/project"/>
<Route path="/authenticate" component={Authenticate}/>
<Route path="/project" component={Project}/>
<Route component={PageError}/>
</Switch>
</Router>
);
export default Routes;

View File

@ -1,59 +0,0 @@
import styled from 'styled-components';
import { color, font, mixin, zIndexValues } from '../../shared/utils/styles';
import { Icon } from '../../shared/components';
export const Container = styled.div`
z-index: ${zIndexValues.modal + 1};
position: fixed;
right: 30px;
top: 50px;
`;
export const StyledToast = styled.div`
position: relative;
margin-bottom: 5px;
width: 300px;
padding: 15px 20px;
border-radius: 3px;
color: #fff;
background: ${props => color[props.type]};
cursor: pointer;
transition: all 0.15s;
${mixin.clearfix}
transform: translateZ(0);
&.jira-toast-enter,
&.jira-toast-exit.jira-toast-exit-active {
opacity: 0;
right: -10px;
}
&.jira-toast-exit,
&.jira-toast-enter.jira-toast-enter-active {
opacity: 1;
right: 0;
}
`;
export const CloseIcon = styled(Icon)`
position: absolute;
top: 13px;
right: 14px;
font-size: 22px;
cursor: pointer;
color: #fff;
`;
export const Title = styled.div`
padding-right: 22px;
font-size: 15px
${ font.medium };font-weight: normal;
`;
export const Message = styled.div`
padding: 8px 10px 0 0;
white-space: pre-wrap;
font-size: 14px
${ font.medium };font-weight: normal;
`;

View File

@ -1,50 +0,0 @@
import React, { useState, useEffect } from 'react';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import pubsub from 'sweet-pubsub';
import { uniqueId } from 'lodash';
import { Container, StyledToast, CloseIcon, Title, Message } from './Styles';
const Toast = () => {
const [toasts, setToasts] = useState([]);
useEffect(() => {
const addToast = ({ type = 'success', title, message, duration = 5 }) => {
const id = uniqueId('toast-');
setToasts(currentToasts => [...currentToasts, { id, type, title, message }]);
if (duration) {
setTimeout(() => removeToast(id), duration * 1000);
}
};
pubsub.on('toast', addToast);
return () => {
pubsub.off('toast', addToast);
};
}, []);
const removeToast = id => {
setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id));
};
return (
<Container>
<TransitionGroup>
{toasts.map(toast => (
<CSSTransition key={toast.id} classNames="jira-toast" timeout={200}>
<StyledToast key={toast.id} type={toast.type} onClick={() => removeToast(toast.id)}>
<CloseIcon type="close" />
{toast.title && <Title>{toast.title}</Title>}
{toast.message && <Message>{toast.message}</Message>}
</StyledToast>
</CSSTransition>
))}
</TransitionGroup>
</Container>
);
};
export default Toast;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 384 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 433 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 341 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 432 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@ -1,35 +0,0 @@
@font-face {
font-family: 'CircularStdBlack';
src: url('./assets/fonts/CircularStd-Black.woff2') format('woff2'),
url('./assets/fonts/CircularStd-Black.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'CircularStdBold';
src: url('./assets/fonts/CircularStd-Bold.woff2') format('woff2'),
url('./assets/fonts/CircularStd-Bold.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'CircularStdMedium';
src: url('./assets/fonts/CircularStd-Medium.woff2') format('woff2'),
url('./assets/fonts/CircularStd-Medium.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'CircularStdBook';
src: url('./assets/fonts/CircularStd-Book.woff2') format('woff2'),
url('./assets/fonts/CircularStd-Book.woff') format('woff');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'jira';
src: url('./assets/fonts/jira.woff') format('truetype'),
url('./assets/fonts/jira.ttf') format('woff'), url('./assets/fonts/jira.svg#jira') format('svg');
font-weight: normal;
font-style: normal;
}

View File

@ -1,22 +0,0 @@
import React from 'react';
import { Provider } from 'react-redux';
import store from '../store';
import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
import Toast from './Toast';
import Routes from './Routes';
import './fontStyles.css';
const App = () => (
<Provider store={ store }>
<NormalizeStyles/>
<BaseStyles/>
<Toast/>
<Routes/>
</Provider>
);
export default App;

View File

@ -1,56 +0,0 @@
import React from 'react';
import { connect } from "react-redux";
import { Redirect } from 'react-router-dom';
import * as formActions from '../actions/forms';
import { getStoredAuthToken } from '../shared/utils/authToken';
import {
ActionButton,
Actions,
Divider,
FormElement,
Header,
SignIn,
SignInSection,
} from '../Project/IssueCreate/Styles';
const Authenticate = ({
onEmailChanged,
onPasswordChanged,
onSubmit,
}) => {
if (getStoredAuthToken()) return <Redirect to='/project'/>;
return (
<SignIn>
<SignInSection>
<form onSubmit={ onSubmit }>
<FormElement>
<Header>Zaloguj się na swoje konto</Header>
<input
name='email'
onChange={ onEmailChanged }
/>
<input
name='password'
onChange={ onPasswordChanged }
/>
<Divider/>
<Actions>
<ActionButton type="submit" variant="primary" isWorking={ false }>
Zaloguj
</ActionButton>
</Actions>
</FormElement>
</form>
</SignInSection>
</SignIn>
);
};
export default connect(null, {
onEmailChanged: formActions.emailChanged,
onPasswordChanged: formActions.passwordChanged,
onSubmit: formActions.signInSubmit,
})(Authenticate);

View File

@ -1,39 +0,0 @@
import styled from 'styled-components/dist/styled-components.esm';
import { color, font } from '../shared/utils/styles';
import { Button, Form } from '../shared/components';
export const FormElement = styled(Form.Element)`;
padding: 25px 40px 35px;
`;
export const FormHeading = styled.div`
padding-bottom: 15px;
${font.size(21)}
`;
export const SelectItem = styled.div`
display: flex;
align-items: center;
margin-right: 15px;
${ props => props.withBottomMargin && `margin-bottom: 5px;` }
`;
export const SelectItemLabel = styled.div`
padding: 0 3px 0 6px;
`;
export const Divider = styled.div`
margin-top: 22px;
border-top: 1px solid ${ color.borderLightest };
`;
export const Actions = styled.div`
display: flex;
justify-content: flex-end;
padding-top: 30px;
`;
export const ActionButton = styled(Button)`
margin-left: 10px;
`;

View File

@ -1,57 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../shared/utils/styles';
import { Avatar, Button, InputDebounced } from '../../../shared/components';
export const Filters = styled.div`
display: flex;
align-items: center;
margin-top: 24px;
`;
export const SearchInput = styled(InputDebounced)`
margin-right: 18px;
width: 160px;
`;
export const Avatars = styled.div`
display: flex;
flex-direction: row-reverse;
margin: 0 12px 0 2px;
`;
export const AvatarIsActiveBorder = styled.div`
display: inline-flex;
margin-left: -2px;
border-radius: 50%;
transition: transform 0.1s;
cursor: pointer;
user-select: none;;
${props => props.isActive && `box-shadow: 0 0 0 4px ${color.primary}`}
&:hover {
transform: translateY(-5px);
}
`;
export const StyledAvatar = styled(Avatar)`
box-shadow: 0 0 0 2px #fff;
`;
export const StyledButton = styled(Button)`
margin-left: 6px;
`;
export const ClearAll = styled.div`
height: 32px;
line-height: 32px;
margin-left: 15px;
padding-left: 12px;
border-left: 1px solid ${color.borderLightest};
color: ${color.textDark};
font-size: 14.5px
cursor: pointer;
user-select: none;
&:hover {
color: ${color.textMedium};
}
`;

View File

@ -1,58 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { xor } from 'lodash';
import { AvatarIsActiveBorder, Avatars, ClearAll, Filters, SearchInput, StyledAvatar, StyledButton, } from './Styles';
const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, mergeFilters }) => {
const { searchTerm, userIds, myOnly, recent } = filters;
const areFiltersCleared = !searchTerm && userIds.length === 0 && !myOnly && !recent;
return (
<Filters data-testid="board-filters">
<SearchInput
icon="search"
value={searchTerm}
onChange={value => mergeFilters({ searchTerm: value })}
/>
<Avatars>
{projectUsers.map(user => (
<AvatarIsActiveBorder key={user.id} isActive={userIds.includes(user.id)}>
<StyledAvatar
avatarUrl={user.avatarUrl}
name={user.name}
onClick={() => mergeFilters({ userIds: xor(userIds, [user.id]) })}
/>
</AvatarIsActiveBorder>
))}
</Avatars>
<StyledButton
variant="empty"
isActive={myOnly}
onClick={() => mergeFilters({ myOnly: !myOnly })}
>
Only My Issues
</StyledButton>
<StyledButton
variant="empty"
isActive={recent}
onClick={() => mergeFilters({ recent: !recent })}
>
Recently Updated
</StyledButton>
{!areFiltersCleared && (
<ClearAll onClick={() => mergeFilters(defaultFilters)}>Clear all</ClearAll>
)}
</Filters>
);
};
ProjectBoardFilters.propTypes = {
projectUsers: PropTypes.array.isRequired,
defaultFilters: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
mergeFilters: PropTypes.func.isRequired,
};
export default ProjectBoardFilters;

View File

@ -1,14 +0,0 @@
import styled from 'styled-components';
import { font } from '../../../shared/utils/styles';
export const Header = styled.div`
margin-top: 6px;
display: flex;
justify-content: space-between;
`;
export const BoardName = styled.div`
font-size: 24px
${ font.medium };font-weight: normal;
`;

View File

@ -1,16 +0,0 @@
import React from 'react';
import { Button } from '../../../shared/components';
import { BoardName, Header } from './Styles';
const ProjectBoardHeader = () => (
<Header id='projectHeader'>
<BoardName id='boardName'>Kanban board</BoardName>
<a href="https://github.com/oldboyxx/jira_clone" target="_blank" rel="noreferrer noopener">
<Button icon="github">Github Repo</Button>
</a>
</Header>
);
export default ProjectBoardHeader;

View File

@ -1,27 +0,0 @@
import styled, { css } from 'styled-components';
import { color } from '../../../../shared/utils/styles';
export const User = styled.div`
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
${props =>
props.isSelectValue &&
css`
margin: 0 10px ${props.withBottomMargin ? 5 : 0}px 0;
padding: 4px 8px;
border-radius: 4px;
background: ${color.backgroundLight};
transition: background 0.1s;
&:hover {
background: ${color.backgroundMedium};
}
`}
`;
export const Username = styled.div`
padding: 0 3px 0 8px;
font-size: 14.5px
`;

View File

@ -1,69 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Avatar, Icon, Select } from '../../../../shared/components';
import { SectionTitle } from '../Styles';
import { User, Username } from './Styles';
const ProjectBoardIssueDetailsAssigneesReporter = ({ issue, updateIssue, projectUsers }) => {
const getUserById = userId => projectUsers.find(user => user.id === userId);
const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name }));
return (
<Fragment>
<SectionTitle>Assignees</SectionTitle>
<Select
isMulti
variant="empty"
dropdownWidth={343}
placeholder="Unassigned"
name="assignees"
value={issue.userIds}
options={userOptions}
onChange={userIds => {
updateIssue({ userIds, users: userIds.map(getUserById) });
}}
renderValue={({ value: userId, removeOptionValue }) =>
renderUser(getUserById(userId), true, removeOptionValue)
}
renderOption={({ value: userId }) => renderUser(getUserById(userId), false)}
/>
<SectionTitle>Reporter</SectionTitle>
<Select
variant="empty"
dropdownWidth={343}
withClearValue={false}
name="reporter"
value={issue.reporterId}
options={userOptions}
onChange={userId => updateIssue({ reporterId: userId })}
renderValue={({ value: userId }) => renderUser(getUserById(userId), true)}
renderOption={({ value: userId }) => renderUser(getUserById(userId))}
/>
</Fragment>
);
};
const renderUser = (user, isSelectValue, removeOptionValue) => (
<User
key={user.id}
isSelectValue={isSelectValue}
withBottomMargin={!!removeOptionValue}
onClick={() => removeOptionValue && removeOptionValue()}
>
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={24} />
<Username>{user.name}</Username>
{removeOptionValue && <Icon type="close" top={1} />}
</User>
);
ProjectBoardIssueDetailsAssigneesReporter.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
projectUsers: PropTypes.array.isRequired,
};
export default ProjectBoardIssueDetailsAssigneesReporter;

View File

@ -1,12 +0,0 @@
import styled from 'styled-components';
import { Button } from '../../../../../shared/components';
export const Actions = styled.div`
display: flex;
padding-top: 10px;
`;
export const FormButton = styled(Button)`
margin-right: 6px;
`;

View File

@ -1,56 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Textarea } from '../../../../../shared/components';
import { Actions, FormButton } from './Styles';
class ProjectBoardIssueDetailsCommentsBodyForm extends React.Component {
state = { textArea: React.createRef() };
handleSubmit = () => {
if (this.state.textArea.current.value.trim()) {
this.props.onSubmit();
}
};
render() {
let {
value,
onChange,
isWorking,
onCancel,
} = this.props;
return (
<Fragment>
<Textarea
autoFocus
placeholder="Add a comment..."
value={ value }
onChange={ onChange }
ref={ this.state.textArea }
/>
<Actions>
<FormButton variant="primary" isWorking={ isWorking } onClick={ this.handleSubmit }>
Save
</FormButton>
<FormButton variant="empty" onClick={ onCancel }>
Cancel
</FormButton>
</Actions>
</Fragment>
);
}
}
ProjectBoardIssueDetailsCommentsBodyForm.propTypes = {
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
isWorking: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsCommentsBodyForm;

View File

@ -1,72 +0,0 @@
import styled from 'styled-components';
import { color, font } from '../../../../../shared/utils/styles';
import { Avatar } from '../../../../../shared/components';
export const Comment = styled.div`
position: relative;
margin-top: 25px;
font-size: 15px
`;
export const UserAvatar = styled(Avatar)`
position: absolute;
top: 0;
left: 0;
`;
export const Content = styled.div`
padding-left: 44px;
`;
export const Username = styled.div`
display: inline-block;
padding-right: 12px;
padding-bottom: 10px;
color: ${ color.textDark };
${ font.medium };
font-weight: normal;
`;
export const CreatedAt = styled.div`
display: inline-block;
padding-bottom: 10px;
color: ${color.textDark};
font-size: 14.5px
`;
export const Body = styled.p`
padding-bottom: 10px;
white-space: pre-wrap;
`;
export const EditLink = styled.div`
margin-right: 12px;
display: inline-block;
padding: 2px 0;
color: ${color.textMedium};
font-size: 14.5px;
cursor: pointer;
user-select: none;
&:hover {
text-decoration: underline;
}
`;
export const DeleteLink = styled.div`
display: inline-block;
padding: 2px 0;
color: ${color.textMedium};
font-size: 14.5px
cursor: pointer;
user-select: none;
&:hover {
text-decoration: underline;
}
&:before {
position: relative;
right: 6px;
content: '·';
display: inline-block;
}
`;

View File

@ -1,76 +0,0 @@
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import api from '../../../../../shared/utils/api';
import toast from '../../../../../shared/utils/toast';
import { formatDateTimeConversational } from '../../../../../shared/utils/dateTime';
import { ConfirmModal } from '../../../../../shared/components';
import BodyForm from '../BodyForm';
import { Body, Comment, Content, CreatedAt, DeleteLink, EditLink, UserAvatar, Username, } from './Styles';
const ProjectBoardIssueDetailsComment = ({ comment, fetchIssue }) => {
const [ isFormOpen, setFormOpen ] = useState(false);
const [ isUpdating, setUpdating ] = useState(false);
const [ body, setBody ] = useState(comment.body);
const handleCommentDelete = async () => {
try {
await api.delete(`/comments/${ comment.id }`);
await fetchIssue();
} catch (error) {
toast.error(error);
}
};
const handleCommentUpdate = async () => {
try {
setUpdating(true);
await api.put(`/comments/${ comment.id }`, { body });
await fetchIssue();
setUpdating(false);
setFormOpen(false);
} catch (error) {
toast.error(error);
}
};
return (
<Comment data-testid="issue-comment">
<UserAvatar name={ comment.user.name } avatarUrl={ comment.user.avatarUrl }/>
<Content>
<Username>{ comment.user.name }</Username>
<CreatedAt>{ formatDateTimeConversational(comment.createdAt) }</CreatedAt>
{ isFormOpen ? (
<BodyForm
value={ body }
onChange={ setBody }
isWorking={ isUpdating }
onSubmit={ handleCommentUpdate }
onCancel={ () => setFormOpen(false) }
/>
) : (
<Fragment>
<Body>{ comment.body }</Body>
<EditLink onClick={ () => setFormOpen(true) }>Edit</EditLink>
<ConfirmModal
title="Are you sure you want to delete this comment?"
message="Once you delete, it's gone for good."
confirmText="Delete comment"
onConfirm={ handleCommentDelete }
renderLink={ modal => <DeleteLink onClick={ modal.open }>Delete</DeleteLink> }
/>
</Fragment>
) }
</Content>
</Comment>
);
};
ProjectBoardIssueDetailsComment.propTypes = {
comment: PropTypes.object.isRequired,
fetchIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsComment;

View File

@ -1,27 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../../../../shared/utils/styles';
export const Tip = styled.div`
display: flex;
align-items: center;
padding-top: 8px;
color: ${color.textMedium};
font-size: 13px
strong {
padding-right: 4px;
}
`;
export const TipLetter = styled.span`
position: relative;
top: 1px;
display: inline-block;
margin: 0 4px;
padding: 0 4px;
border-radius: 2px;
color: ${color.textDarkest};
background: ${color.backgroundMedium};
font-family: "CircularStdBold"; font-weight: normal
font-size: 12px
`;

View File

@ -1,36 +0,0 @@
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { KeyCodes } from '../../../../../../shared/constants/keyCodes';
import { isFocusedElementEditable } from '../../../../../../shared/utils/browser';
import { Tip, TipLetter } from './Styles';
const ProjectBoardIssueDetailsCommentsCreateProTip = ({ setFormOpen }) => {
useEffect(() => {
const handleKeyDown = event => {
if (!isFocusedElementEditable() && event.keyCode === KeyCodes.M) {
event.preventDefault();
setFormOpen(true);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [setFormOpen]);
return (
<Tip>
<strong>Pro tip:</strong>press<TipLetter>M</TipLetter>to comment
</Tip>
);
};
ProjectBoardIssueDetailsCommentsCreateProTip.propTypes = {
setFormOpen: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsCommentsCreateProTip;

View File

@ -1,32 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../../../shared/utils/styles';
import { Avatar } from '../../../../../shared/components';
export const Create = styled.div`
position: relative;
margin-top: 25px;
font-size: 15px
`;
export const UserAvatar = styled(Avatar)`
position: absolute;
top: 0;
left: 0;
`;
export const Right = styled.div`
padding-left: 44px;
`;
export const FakeTextarea = styled.div`
padding: 12px 16px;
border-radius: 4px;
border: 1px solid ${color.borderLightest};
color: ${color.textLight};
cursor: pointer;
user-select: none;
&:hover {
border: 1px solid ${color.borderLight};
}
`;

View File

@ -1,73 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import api from '../../../../../shared/utils/api';
import toast from '../../../../../shared/utils/toast';
import { fetchCurrentUser } from "../../../../../actions/users";
import BodyForm from '../BodyForm';
import ProTip from './ProTip';
import { Create, FakeTextarea, Right, UserAvatar } from './Styles';
class ProjectBoardIssueDetailsCommentsCreate extends React.Component {
state = { isFormOpen: false, isCreating: false, body: '', currentUser: null };
setFormClosed = () => this.setState({ isFormOpen: false });
setFormOpened = () => this.setState({ isFormOpen: true });
setBody = body => this.setState({ body });
setFormOpen = isFormOpen => this.setState({ isFormOpen });
setCreatingTrue = () => this.setState({ isCreating: true });
componentDidMount() {
this.props.fetchCurrentUser({});
}
handleCommentCreate = async () => {
try {
this.setCreatingTrue();
await api.post(`/comments`, { body: this.state.body, issueId: this.props.issueId });
await this.props.fetchIssue();
this.setState({ isCreating: false, isFormOpen: false, body: '' });
} catch (error) {
this.setState({ isCreating: false });
toast.error(error);
}
};
render() {
const { body, isFormOpen, isCreating } = this.state;
const { currentUser } = this.props;
return (
<Create>
{ currentUser && <UserAvatar name={ currentUser.name } avatarUrl={ currentUser.avatarUrl }/> }
<Right>
{ isFormOpen ? (
<BodyForm
value={ body }
onChange={ this.setBody }
isWorking={ isCreating }
onSubmit={ this.handleCommentCreate }
onCancel={ this.setFormClosed }
/>
) : (
<Fragment>
<FakeTextarea onClick={ this.setFormOpened }>Add a comment...</FakeTextarea>
<ProTip setFormOpen={ this.setFormOpen }/>
</Fragment>
) }
</Right>
</Create>
);
}
}
ProjectBoardIssueDetailsCommentsCreate.propTypes = {
issueId: PropTypes.number.isRequired,
fetchIssue: PropTypes.func.isRequired,
fetchCurrentUser: PropTypes.func,
};
export default connect(({ users: { currentUser } }) => ({ currentUser }), { fetchCurrentUser })(ProjectBoardIssueDetailsCommentsCreate);

View File

@ -1,12 +0,0 @@
import styled from 'styled-components';
import { font } from '../../../../shared/utils/styles';
export const Comments = styled.div`
padding-top: 40px;
`;
export const Title = styled.div`
${ font.medium };font-weight: normal;
font-size: 15px
`;

View File

@ -1,26 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { sortByNewest } from '../../../../shared/utils/javascript';
import Create from './Create';
import Comment from './Comment';
import { Comments, Title } from './Styles';
const ProjectBoardIssueDetailsComments = ({ issue, fetchIssue }) => (
<Comments>
<Title>Comments</Title>
<Create issueId={issue.id} fetchIssue={fetchIssue}/>
{sortByNewest(issue.comments, 'createdAt').map(comment => (
<Comment key={comment.id} comment={comment} fetchIssue={fetchIssue}/>
))}
</Comments>
);
ProjectBoardIssueDetailsComments.propTypes = {
issue: PropTypes.object.isRequired,
fetchIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsComments;

View File

@ -1,12 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../../shared/utils/styles';
export const Dates = styled.div`
margin-top: 11px;
padding-top: 13px;
line-height: 22px;
border-top: 1px solid ${color.borderLightest};
color: ${color.textMedium};
font-size: 13px
`;

View File

@ -1,19 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { formatDateTimeConversational } from '../../../../shared/utils/dateTime';
import { Dates } from './Styles';
const ProjectBoardIssueDetailsDates = ({ issue }) => (
<Dates>
<div>Created at {formatDateTimeConversational(issue.createdAt)}</div>
<div>Updated at {formatDateTimeConversational(issue.updatedAt)}</div>
</Dates>
);
ProjectBoardIssueDetailsDates.propTypes = {
issue: PropTypes.object.isRequired,
};
export default ProjectBoardIssueDetailsDates;

View File

@ -1,38 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import api from '../../../shared/utils/api';
import toast from '../../../shared/utils/toast';
import { Button, ConfirmModal } from '../../../shared/components';
const ProjectBoardIssueDetailsDelete = ({ issue, fetchProject, modalClose }) => {
const handleIssueDelete = async () => {
try {
await api.delete(`/issues/${issue.id}`);
await fetchProject();
modalClose();
} catch (error) {
toast.error(error);
}
};
return (
<ConfirmModal
title="Are you sure you want to delete this issue?"
message="Once you delete, it's gone for good."
confirmText="Delete issue"
onConfirm={handleIssueDelete}
renderLink={modal => (
<Button icon="trash" iconSize={19} variant="empty" onClick={modal.open} />
)}
/>
);
};
ProjectBoardIssueDetailsDelete.propTypes = {
issue: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
modalClose: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsDelete;

View File

@ -1,31 +0,0 @@
import styled from 'styled-components';
import { color, font } from '../../../../shared/utils/styles';
export const Title = styled.div`
padding: 20px 0 6px;
font-size: 15px
${ font.medium };font-weight: normal;
`;
export const EmptyLabel = styled.div`
margin-left: -7px;
padding: 7px;
border-radius: 3px;
color: ${color.textMedium}
transition: background 0.1s;
font-size: 15px
cursor: pointer;
user-select: none;
&:hover {
background: ${color.backgroundLight};
}
`;
export const Actions = styled.div`
display: flex;
padding-top: 12px;
& > button {
margin-right: 6px;
}
`;

View File

@ -1,57 +0,0 @@
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { getTextContentsFromHtmlString } from '../../../../shared/utils/browser';
import { Button, TextEditedContent, TextEditor } from '../../../../shared/components';
import { Actions, EmptyLabel, Title } from './Styles';
const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => {
const [description, setDescription] = useState(issue.description);
const [isEditing, setEditing] = useState(false);
const handleUpdate = () => {
setEditing(false);
updateIssue({ description });
};
const isDescriptionEmpty = getTextContentsFromHtmlString(description).trim().length === 0;
return (
<Fragment>
<Title>Description</Title>
{isEditing ? (
<Fragment>
<TextEditor
placeholder="Describe the issue"
defaultValue={description}
onChange={setDescription}
/>
<Actions>
<Button variant="primary" onClick={handleUpdate}>
Save
</Button>
<Button variant="empty" onClick={() => setEditing(false)}>
Cancel
</Button>
</Actions>
</Fragment>
) : (
<Fragment>
{isDescriptionEmpty ? (
<EmptyLabel onClick={() => setEditing(true)}>Add a description...</EmptyLabel>
) : (
<TextEditedContent content={description} onClick={() => setEditing(true)} />
)}
</Fragment>
)}
</Fragment>
);
};
ProjectBoardIssueDetailsDescription.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsDescription;

View File

@ -1,47 +0,0 @@
import styled from 'styled-components';
import { color, font } from '../../../../shared/utils/styles';
export const TrackingLink = styled.div`
padding: 4px 4px 2px 0;
border-radius: 4px;
transition: background 0.1s;
cursor: pointer;
user-select: none;
&:hover {
background: ${color.backgroundLight};
}
`;
export const ModalContents = styled.div`
padding: 20px 25px 25px;
`;
export const ModalTitle = styled.div`
padding-bottom: 14px;
${ font.medium };
font-weight: normal;
font-size: 20px
`;
export const Inputs = styled.div`
display: flex;
margin: 20px -5px 30px;
`;
export const InputCont = styled.div`
margin: 0 5px;
width: 50%;
`;
export const InputLabel = styled.div`
padding-bottom: 5px;
color: ${ color.textMedium };
${ font.medium };font-weight: normal;;
font-size: 13px;
`;
export const Actions = styled.div`
display: flex;
justify-content: flex-end;
`;

View File

@ -1,39 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../../../shared/utils/styles';
import { Icon } from '../../../../../shared/components';
export const TrackingWidget = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const WatchIcon = styled(Icon)`
color: ${color.textMedium};
`;
export const Right = styled.div`
width: 90%;
`;
export const BarCont = styled.div`
height: 5px;
border-radius: 4px;
background: ${color.backgroundMedium};
`;
export const Bar = styled.div`
height: 5px;
border-radius: 4px;
background: ${color.primary};
transition: all 0.1s;
width: ${props => props.width}%;
`;
export const Values = styled.div`
display: flex;
justify-content: space-between;
padding-top: 3px;
font-size: 14.5px;
`;

View File

@ -1,53 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isNil } from 'lodash';
import { Bar, BarCont, Right, TrackingWidget, Values, WatchIcon } from './Styles';
const ProjectBoardIssueDetailsTrackingWidget = ({ issue }) => (
<TrackingWidget>
<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>
</TrackingWidget>
);
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>;
}
};
ProjectBoardIssueDetailsTrackingWidget.propTypes = {
issue: PropTypes.object.isRequired,
};
export default ProjectBoardIssueDetailsTrackingWidget;

View File

@ -1,67 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { isNil } from 'lodash';
import { Button, InputDebounced, Modal } from '../../../../shared/components';
import TrackingWidget from './TrackingWidget';
import { SectionTitle } from '../Styles';
import { Actions, InputCont, InputLabel, Inputs, ModalContents, ModalTitle, TrackingLink, } from './Styles';
const ProjectBoardIssueDetailsEstimateTracking = ({ issue, updateIssue }) => (
<Fragment>
<SectionTitle>Original Estimate (hours)</SectionTitle>
{renderHourInput('estimate', issue, updateIssue)}
<SectionTitle>Time Tracking</SectionTitle>
<Modal
testid="modal:tracking"
width={400}
renderLink={modal => (
<TrackingLink onClick={modal.open}>
<TrackingWidget issue={issue} />
</TrackingLink>
)}
renderContent={modal => (
<ModalContents>
<ModalTitle>Time tracking</ModalTitle>
<TrackingWidget issue={issue} />
<Inputs>
<InputCont>
<InputLabel>Time spent (hours)</InputLabel>
{renderHourInput('timeSpent', issue, updateIssue)}
</InputCont>
<InputCont>
<InputLabel>Time remaining (hours)</InputLabel>
{renderHourInput('timeRemaining', issue, updateIssue)}
</InputCont>
</Inputs>
<Actions>
<Button variant="primary" onClick={modal.close}>
Done
</Button>
</Actions>
</ModalContents>
)}
/>
</Fragment>
);
const renderHourInput = (fieldName, issue, updateIssue) => (
<InputDebounced
placeholder="Number"
filter={/^\d{0,6}$/}
value={isNil(issue[fieldName]) ? '' : issue[fieldName]}
onChange={stringValue => {
const value = stringValue.trim() ? Number(stringValue) : null;
updateIssue({ [fieldName]: value });
}}
/>
);
ProjectBoardIssueDetailsEstimateTracking.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsEstimateTracking;

View File

@ -1,31 +0,0 @@
import React from 'react';
import ContentLoader from 'react-content-loader';
const IssueDetailsLoader = () => (
<div style={{ padding: 40 }}>
<ContentLoader
height={260}
width={940}
speed={2}
primaryColor="#f3f3f3"
secondaryColor="#ecebeb"
>
<rect x="0" y="0" rx="3" ry="3" width="627" height="24" />
<rect x="0" y="29" rx="3" ry="3" width="506" height="24" />
<rect x="0" y="77" rx="3" ry="3" width="590" height="16" />
<rect x="0" y="100" rx="3" ry="3" width="627" height="16" />
<rect x="0" y="123" rx="3" ry="3" width="480" height="16" />
<rect x="0" y="187" rx="3" ry="3" width="370" height="16" />
<circle cx="18" cy="239" r="18" />
<rect x="46" y="217" rx="3" ry="3" width="548" height="42" />
<rect x="683" y="3" rx="3" ry="3" width="135" height="14" />
<rect x="683" y="33" rx="3" ry="3" width="251" height="24" />
<rect x="683" y="90" rx="3" ry="3" width="135" height="14" />
<rect x="683" y="120" rx="3" ry="3" width="251" height="24" />
<rect x="683" y="177" rx="3" ry="3" width="135" height="14" />
<rect x="683" y="207" rx="3" ry="3" width="251" height="24" />
</ContentLoader>
</div>
);
export default IssueDetailsLoader;

View File

@ -1,23 +0,0 @@
import styled, { css } from 'styled-components';
import { color } from '../../../../shared/utils/styles';
export const Priority = styled.div`
display: flex;
align-items: center;
${props =>
props.isValue &&
css`
padding: 3px 4px 3px 0px;
border-radius: 4px;
&:hover,
&:focus {
background: ${color.backgroundLight};
}
`}
`;
export const Label = styled.div`
padding: 0 3px 0 8px;
font-size: 14.5px
`;

View File

@ -1,42 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { IssuePriority, IssuePriorityCopy } from '../../../../shared/constants/issues';
import { IssuePriorityIcon, Select } from '../../../../shared/components';
import { SectionTitle } from '../Styles';
import { Label, Priority } from './Styles';
const ProjectBoardIssueDetailsPriority = ({ issue, updateIssue }) => (
<Fragment>
<SectionTitle>Priority</SectionTitle>
<Select
variant="empty"
withClearValue={false}
dropdownWidth={343}
name="priority"
value={issue.priority}
options={Object.values(IssuePriority).map(priority => ({
value: priority,
label: IssuePriorityCopy[priority],
}))}
onChange={priority => updateIssue({ priority })}
renderValue={({ value: priority }) => renderPriorityItem(priority, true)}
renderOption={({ value: priority }) => renderPriorityItem(priority)}
/>
</Fragment>
);
const renderPriorityItem = (priority, isValue) => (
<Priority isValue={isValue}>
<IssuePriorityIcon priority={priority} />
<Label>{IssuePriorityCopy[priority]}</Label>
</Priority>
);
ProjectBoardIssueDetailsPriority.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsPriority;

View File

@ -1,35 +0,0 @@
import styled, { css } from 'styled-components';
import { issueStatusBackgroundColors, issueStatusColors } from '../../../../shared/utils/styles';
export const Status = styled.div`
text-transform: uppercase;
transition: all 0.1s;
${props =>
css`
display: inline-flex;
align-items: center;
height: 24px;
padding: 0 8px;
border-radius: 4px;
cursor: pointer;
user-select: none;
color: ${issueStatusColors[props.color]};
background: ${issueStatusBackgroundColors[props.color]};
font-family: "CircularStdBold"; font-weight: normal
font-size: 12px
i {
margin-left: 4px;
}
`
}
${props =>
props.isValue &&
css`
padding: 0 12px;
height: 32px;
&:hover {
transform: scale(1.05);
}
`}
`;

View File

@ -1,42 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { IssueStatus, IssueStatusCopy } from '../../../../shared/constants/issues';
import { Icon, Select } from '../../../../shared/components';
import { SectionTitle } from '../Styles';
import { Status } from './Styles';
const ProjectBoardIssueDetailsStatus = ({ issue, updateIssue }) => (
<Fragment>
<SectionTitle>Status</SectionTitle>
<Select
variant="empty"
dropdownWidth={343}
withClearValue={false}
name="status"
value={issue.status}
options={Object.values(IssueStatus).map(status => ({
value: status,
label: IssueStatusCopy[status],
}))}
onChange={status => updateIssue({ status })}
renderValue={({ value: status }) => (
<Status isValue color={status}>
<div>{IssueStatusCopy[status]}</div>
<Icon type="chevron-down" size={18} />
</Status>
)}
renderOption={({ value: status }) => (
<Status color={status}>{IssueStatusCopy[status]}</Status>
)}
/>
</Fragment>
);
ProjectBoardIssueDetailsStatus.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsStatus;

View File

@ -1,41 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../shared/utils/styles';
export const Content = styled.div`
display: flex;
padding: 0 30px 60px;
`;
export const Left = styled.div`
width: 65%;
padding-right: 50px;
`;
export const Right = styled.div`
width: 35%;
padding-top: 5px;
`;
export const TopActions = styled.div`
display: flex;
justify-content: space-between;
padding: 21px 18px 0;
`;
export const TopActionsRight = styled.div`
display: flex;
align-items: center;
& > * {
margin-left: 4px;
}
`;
export const SectionTitle = styled.div`
margin: 24px 0 5px;
text-transform: uppercase;
color: ${color.textMedium};
font-size: 12.5px
font-family: "CircularStdBold";
font-weight: normal;
`;

View File

@ -1,33 +0,0 @@
import styled from 'styled-components';
import { color, font } from '../../../../shared/utils/styles';
import { Textarea } from '../../../../shared/components';
export const TitleTextarea = styled(Textarea)`
margin: 18px 0 0 -8px;
height: 44px;
width: 100%;
textarea {
padding: 7px 7px 8px;
line-height: 1.28;
border: none;
resize: none;
background: #fff;
border: 1px solid transparent;
box-shadow: 0 0 0 1px transparent;
transition: background 0.1s;
font-size: 24px
${font.medium};
font-weight: normal;
&:hover:not(:focus) {
background: ${color.backgroundLight};
}
}
`;
export const ErrorText = styled.div`
padding-top: 4px;
color: ${ color.danger };
font-size: 13px
${ font.medium };font-weight: normal;
`;

View File

@ -1,52 +0,0 @@
import React, { Fragment, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { KeyCodes } from '../../../../shared/constants/keyCodes';
import { generateErrors, is } from '../../../../shared/utils/validation';
import { ErrorText, TitleTextarea } from './Styles';
const ProjectBoardIssueDetailsTitle = ({ issue, updateIssue }) => {
const $titleInputRef = useRef();
const [error, setError] = useState(null);
const handleTitleChange = () => {
setError(null);
const title = $titleInputRef.current.value;
if (title === issue.title) return;
const errors = generateErrors({ title }, { title: [is.required(), is.maxLength(200)] });
if (errors.title) {
setError(errors.title);
} else {
updateIssue({ title });
}
};
return (
<Fragment>
<TitleTextarea
minRows={1}
placeholder="Short summary"
defaultValue={issue.title}
ref={$titleInputRef}
onBlur={handleTitleChange}
onKeyDown={event => {
if (event.keyCode === KeyCodes.ENTER) {
event.target.blur();
}
}}
/>
{error && <ErrorText>{error}</ErrorText>}
</Fragment>
);
};
ProjectBoardIssueDetailsTitle.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsTitle;

View File

@ -1,21 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../../shared/utils/styles';
import { Button } from '../../../../shared/components';
export const TypeButton = styled(Button)`
text-transform: uppercase;
letter-spacing: 0.5px;
color: ${color.textMedium};
font-size: 13px
`;
export const Type = styled.div`
display: flex;
align-items: center;
`;
export const TypeLabel = styled.div`
padding: 0 5px 0 7px;
font-size: 15px
`;

View File

@ -1,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { IssueType, IssueTypeCopy } from '../../../../shared/constants/issues';
import { IssueTypeIcon, Select } from '../../../../shared/components';
import { Type, TypeButton, TypeLabel } from './Styles';
const ProjectBoardIssueDetailsType = ({ issue, updateIssue }) => (
<Select
variant="empty"
dropdownWidth={ 150 }
withClearValue={ false }
name="type"
value={ issue.type }
options={ Object.values(IssueType).map(type => ({
value: type,
label: IssueTypeCopy[type],
})) }
onChange={ type => updateIssue({ type }) }
renderValue={ ({ value: type }) => (
<TypeButton variant="empty" icon={ <IssueTypeIcon type={ type }/> }>
{ `${ IssueTypeCopy[type] }-${ issue.id }` }
</TypeButton>
) }
renderOption={ ({ value: type }) => (
<Type key={ type } onClick={ () => updateIssue({ type }) }>
<IssueTypeIcon type={ type } top={ 1 }/>
<TypeLabel>{ IssueTypeCopy[type] }</TypeLabel>
</Type>
) }
/>
);
ProjectBoardIssueDetailsType.propTypes = {
issue: PropTypes.object.isRequired,
updateIssue: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetailsType;

View File

@ -1,92 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import api from '../../../shared/utils/api';
import useApi from '../../../shared/hooks/api';
import { AboutTooltip, Button, CopyLinkButton, PageError } from '../../../shared/components';
import Loader from './Loader';
import Type from './Type';
import Delete from './Delete';
import Title from './Title';
import Description from './Description';
import Comments from './Comments';
import Status from './Status';
import AssigneesReporter from './AssigneesReporter';
import Priority from './Priority';
import EstimateTracking from './EstimateTracking';
import Dates from './Dates';
import { Content, Left, Right, TopActions, TopActionsRight } from './Styles';
const ProjectBoardIssueDetails = ({
issueId,
projectUsers,
fetchProject,
updateLocalProjectIssues,
modalClose,
}) => {
const [{ data, error, setLocalData }, fetchIssue] = useApi.get(`/issues/${issueId}`);
if (!data) return <Loader />;
if (error) return <PageError />;
const { issue } = data;
const updateLocalIssueDetails = fields =>
setLocalData(currentData => ({ issue: { ...currentData.issue, ...fields } }));
const updateIssue = updatedFields => {
api.optimisticUpdate(`/issues/${issueId}`, {
updatedFields,
currentFields: issue,
setLocalData: fields => {
updateLocalIssueDetails(fields);
updateLocalProjectIssues(issue.id, fields);
},
});
};
return (
<Fragment>
<TopActions>
<Type issue={issue} updateIssue={updateIssue} />
<TopActionsRight>
<AboutTooltip
renderLink={linkProps => (
<Button icon="feedback" variant="empty" {...linkProps}>
Give feedback
</Button>
)}
/>
<CopyLinkButton variant="empty" />
<Delete issue={issue} fetchProject={fetchProject} modalClose={modalClose} />
<Button icon="close" iconSize={24} variant="empty" onClick={modalClose} />
</TopActionsRight>
</TopActions>
<Content>
<Left>
<Title issue={issue} updateIssue={updateIssue} />
<Description issue={issue} updateIssue={updateIssue} />
<Comments issue={issue} fetchIssue={fetchIssue} />
</Left>
<Right>
<Status issue={issue} updateIssue={updateIssue} />
<AssigneesReporter issue={issue} updateIssue={updateIssue} projectUsers={projectUsers} />
<Priority issue={issue} updateIssue={updateIssue} />
<EstimateTracking issue={issue} updateIssue={updateIssue} />
<Dates issue={issue} />
</Right>
</Content>
</Fragment>
);
};
ProjectBoardIssueDetails.propTypes = {
issueId: PropTypes.string.isRequired,
projectUsers: PropTypes.array.isRequired,
fetchProject: PropTypes.func.isRequired,
updateLocalProjectIssues: PropTypes.func.isRequired,
modalClose: PropTypes.func.isRequired,
};
export default ProjectBoardIssueDetails;

View File

@ -1,57 +0,0 @@
import styled, { css } from 'styled-components';
import { Link } from 'react-router-dom';
import { color } from '../../../../../shared/utils/styles';
import { Avatar } from '../../../../../shared/components';
export const IssueLink = styled(Link)`
display: block;
margin-bottom: 5px;
`;
export const Issue = styled.div`
padding: 10px;
border-radius: 3px;
background: #fff;
box-shadow: 0px 1px 2px 0px rgba(9, 30, 66, 0.25);
transition: background 0.1s;
cursor: pointer;
user-select: none;
@media (max-width: 1100px) {
padding: 10px 8px;
}
&:hover {
background: ${color.backgroundLight};
}
${props =>
props.isBeingDragged &&
css`
transform: rotate(3deg);
box-shadow: 5px 10px 30px 0px rgba(9, 30, 66, 0.15);
`}
`;
export const Title = styled.p`
padding-bottom: 11px;
font-size: 15px
@media (max-width: 1100px) {
font-size: 14.5px
}
`;
export const Bottom = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
export const Assignees = styled.div`
display: flex;
flex-direction: row-reverse;
margin-left: 2px;
`;
export const AssigneeAvatar = styled(Avatar)`
margin-left: -2px;
box-shadow: 0 0 0 2px #fff;
`;

View File

@ -1,57 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useRouteMatch } from 'react-router-dom';
import { Draggable } from 'react-beautiful-dnd';
import { IssuePriorityIcon, IssueTypeIcon } from '../../../../../shared/components';
import { AssigneeAvatar, Assignees, Bottom, Issue, IssueLink, Title } from './Styles';
const ProjectBoardListIssue = ({ projectUsers, issue, index }) => {
const match = useRouteMatch();
const assignees = issue.userIds.map(userId => projectUsers.find(user => user.id === userId));
return (
<Draggable draggableId={ issue.id.toString() } index={ index }>
{ (provided, snapshot) => (
<IssueLink
data-id="IssueLink"
to={ `${ match.url }/issues/${ issue.id }` }
ref={ provided.innerRef }
data-testid="list-issue"
{ ...provided.draggableProps }
{ ...provided.dragHandleProps }
>
<Issue data-id='Issue' isBeingDragged={ snapshot.isDragging && !snapshot.isDropAnimating }>
<Title>{ issue.title }</Title>
<Bottom>
<div>
<IssueTypeIcon type={ issue.type }/>
<IssuePriorityIcon priority={ issue.priority } top={ -1 } left={ 4 }/>
</div>
<Assignees>
{ assignees.map(user => (
<AssigneeAvatar
key={ user.id }
size={ 24 }
avatarUrl={ user.avatarUrl }
name={ user.name }
/>
)) }
</Assignees>
</Bottom>
</Issue>
</IssueLink>
) }
</Draggable>
);
};
ProjectBoardListIssue.propTypes = {
projectUsers: PropTypes.array.isRequired,
issue: PropTypes.object.isRequired,
index: PropTypes.number.isRequired,
};
export default ProjectBoardListIssue;

View File

@ -1,33 +0,0 @@
import styled from 'styled-components';
import { color } from '../../../../shared/utils/styles';
export const List = styled.div`
display: flex;
flex-direction: column;
margin: 0 5px;
min-height: 400px;
width: 25%;
border-radius: 3px;
background: ${color.backgroundLightest};
`;
export const Title = styled.div`
padding: 13px 10px 17px;
text-transform: uppercase;
color: ${color.textMedium};
font-size: 12.5px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
export const IssuesCount = styled.span`
text-transform: lowercase;
font-size: 13px;
`;
export const Issues = styled.div`
height: 100%;
padding: 0 5px;
`;

View File

@ -1,81 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { Droppable } from 'react-beautiful-dnd';
import { intersection } from 'lodash';
import { IssueStatusCopy } from '../../../../shared/constants/issues';
import Issue from './Issue';
import { Issues, IssuesCount, List, Title } from './Styles';
const ProjectBoardList = ({ status, project, filters, currentUserId }) => {
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
const filteredListIssues = getSortedListIssues(filteredIssues, status);
const allListIssues = getSortedListIssues(project.issues, status);
return (
<Droppable key={ status } droppableId={ status }>
{ provided => (
<List data-id='List'>
<Title data-id="Title">
{ `${ IssueStatusCopy[status] } ` }
<IssuesCount>{ formatIssuesCount(allListIssues, filteredListIssues) }</IssuesCount>
</Title>
<Issues
data-id="Issues"
{ ...provided.droppableProps }
ref={ provided.innerRef }
data-testid={ `board-list:${ status }` }
>
{ filteredListIssues.map((issue, index) => (
<Issue key={ issue.id } projectUsers={ project.users } issue={ issue } index={ index }/>
)) }
{ provided.placeholder }
</Issues>
</List>
) }
</Droppable>
);
};
const filterIssues = (projectIssues, filters, currentUserId) => {
const { searchTerm, userIds, myOnly, recent } = filters;
let issues = projectIssues;
if (searchTerm) {
issues = issues.filter(issue => issue.title.toLowerCase().includes(searchTerm.toLowerCase()));
}
if (userIds.length > 0) {
issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0);
}
if (myOnly && currentUserId) {
issues = issues.filter(issue => issue.userIds.includes(currentUserId));
}
if (recent) {
issues = issues.filter(issue => moment(issue.updatedAt).isAfter(moment().subtract(3, 'days')));
}
return issues;
};
const getSortedListIssues = (issues, status) =>
issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);
const formatIssuesCount = (allListIssues, filteredListIssues) => {
if (allListIssues.length !== filteredListIssues.length) {
return `${ filteredListIssues.length } of ${ allListIssues.length }`;
}
return allListIssues.length;
};
ProjectBoardList.propTypes = {
status: PropTypes.string.isRequired,
project: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
currentUserId: PropTypes.number,
};
ProjectBoardList.defaultProps = {
currentUserId: null,
};
export default ProjectBoardList;

View File

@ -1,6 +0,0 @@
import styled from 'styled-components';
export const Lists = styled.div`
display: flex;
margin: 26px -5px 0;
`;

View File

@ -1,96 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { DragDropContext } from 'react-beautiful-dnd';
import useCurrentUser from '../../../shared/hooks/currentUser';
import api from '../../../shared/utils/api';
import { insertItemIntoArray, moveItemWithinArray } from '../../../shared/utils/javascript';
import { IssueStatus } from '../../../shared/constants/issues';
import List from './List';
import { Lists } from './Styles';
const ProjectBoardLists = ({ project, filters, updateLocalProjectIssues }) => {
const { currentUserId } = useCurrentUser();
const handleIssueDrop = ({ draggableId, destination, source }) => {
if (!isPositionChanged(source, destination)) return;
const issueId = Number(draggableId);
api.optimisticUpdate(`/issues/${ issueId }`, {
updatedFields: {
status: destination.droppableId,
listPosition: calculateIssueListPosition(project.issues, destination, source, issueId),
},
currentFields: project.issues.find(({ id }) => id === issueId),
setLocalData: fields => updateLocalProjectIssues(issueId, fields),
});
};
return (
<DragDropContext onDragEnd={ handleIssueDrop }>
<Lists id='Lists'>
{ Object.values(IssueStatus).map(status => (
<List
data-name='List'
key={ status }
status={ status }
project={ project }
filters={ filters }
currentUserId={ currentUserId }
/>
)) }
</Lists>
</DragDropContext>
);
};
const isPositionChanged = (destination, source) => {
if (!destination) return false;
const isSameList = destination.droppableId === source.droppableId;
const isSamePosition = destination.index === source.index;
return !isSameList || !isSamePosition;
};
const calculateIssueListPosition = (...args) => {
const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args);
let position;
if (!prevIssue && !nextIssue) {
position = 1;
} else if (!prevIssue) {
position = nextIssue.listPosition - 1;
} else if (!nextIssue) {
position = prevIssue.listPosition + 1;
} else {
position = prevIssue.listPosition + (nextIssue.listPosition - prevIssue.listPosition) / 2;
}
return position;
};
const getAfterDropPrevNextIssue = (allIssues, destination, source, droppedIssueId) => {
const beforeDropDestinationIssues = getSortedListIssues(allIssues, destination.droppableId);
const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId);
const isSameList = destination.droppableId === source.droppableId;
const afterDropDestinationIssues = isSameList
? moveItemWithinArray(beforeDropDestinationIssues, droppedIssue, destination.index)
: insertItemIntoArray(beforeDropDestinationIssues, droppedIssue, destination.index);
return {
prevIssue: afterDropDestinationIssues[destination.index - 1],
nextIssue: afterDropDestinationIssues[destination.index + 1],
};
};
const getSortedListIssues = (issues, status) =>
issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);
ProjectBoardLists.propTypes = {
project: PropTypes.object.isRequired,
filters: PropTypes.object.isRequired,
updateLocalProjectIssues: PropTypes.func.isRequired,
};
export default ProjectBoardLists;

View File

@ -1,72 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Route, useHistory, useRouteMatch } from 'react-router-dom';
import useMergeState from '../../shared/hooks/mergeState';
import { Breadcrumbs, Modal } from '../../shared/components';
import Header from './Header';
import Filters from './Filters';
import Lists from './Lists';
import IssueDetails from './IssueDetails';
const defaultFilters = {
searchTerm: '',
userIds: [],
myOnly: false,
recent: false,
};
const ProjectBoard = ({ project, fetchProject, updateLocalProjectIssues }) => {
const match = useRouteMatch();
const history = useHistory();
const [filters, mergeFilters] = useMergeState(defaultFilters);
return (
<Fragment>
<Breadcrumbs items={['Projects', project.name, 'Kanban Board']} />
<Header />
<Filters
projectUsers={project.users}
defaultFilters={defaultFilters}
filters={filters}
mergeFilters={mergeFilters}
/>
<Lists
project={project}
filters={filters}
updateLocalProjectIssues={updateLocalProjectIssues}
/>
<Route
path={`${match.path}/issues/:issueId`}
render={routeProps => (
<Modal
isOpen
testid="modal:issue-details"
width={1040}
withCloseIcon={false}
onClose={() => history.push(match.url)}
renderContent={modal => (
<IssueDetails
issueId={routeProps.match.params.issueId}
projectUsers={project.users}
fetchProject={fetchProject}
updateLocalProjectIssues={updateLocalProjectIssues}
modalClose={modal.close}
/>
)}
/>
)}
/>
</Fragment>
);
};
ProjectBoard.propTypes = {
project: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
updateLocalProjectIssues: PropTypes.func.isRequired,
};
export default ProjectBoard;

View File

@ -1,60 +0,0 @@
import styled from 'styled-components';
import { color } from '../../shared/utils/styles';
import { Button } from '../../shared/components';
export const SignIn = styled.article`
margin: 24px auto;
display: flex;
justify-content: center;
`;
export const SignInSection = styled.section`
padding: 32px 40px;
width: 400px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 10px;
`;
export const Header = styled.h5`
color: rgb(94, 108, 132);
font-size: 16px;
font-style: normal;
font-weight: 600;
letter-spacing: -0.048px;
line-height: 18.2833px;
`;
export const FormElement = styled.div`
padding: 25px 40px 35px;
`;
export const FormHeading = styled.div`
padding-bottom: 15px;
font-size: 21px;
`;
export const SelectItem = styled.div`
display: flex;
align-items: center;
margin-right: 15px;
${props => props.withBottomMargin && `margin-bottom: 5px;`}
`;
export const SelectItemLabel = styled.div`
padding: 0 3px 0 6px;
`;
export const Divider = styled.div`
margin-top: 22px;
border-top: 1px solid ${color.borderLightest};
`;
export const Actions = styled.div`
display: flex;
justify-content: flex-end;
padding-top: 30px;
`;
export const ActionButton = styled(Button)`
margin-left: 10px;
`;

View File

@ -1,183 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
IssuePriority,
IssuePriorityCopy,
IssueStatus,
IssueType,
IssueTypeCopy,
} from '../../shared/constants/issues';
import toast from '../../shared/utils/toast';
import api from '../../shared/utils/api';
import { Avatar, Form, Icon, IssuePriorityIcon, IssueTypeIcon } from '../../shared/components';
import { Field } from "../../shared/components/Form";
import { ActionButton, Actions, Divider, FormElement, FormHeading, SelectItem, SelectItemLabel, } from './Styles';
class ProjectIssueCreate extends React.Component {
state = {
isCreating: false, form: {
type: IssueType.TASK,
title: '',
description: '',
reporterId: null,
userIds: [],
priority: IssuePriority.MEDIUM,
}
};
onSubmit = async () => {
let { project, fetchProject, onCreate } = this.props;
this.setState({ isCreating: true });
try {
await api.post('/issues', {
...this.state.form,
status: IssueStatus.BACKLOG,
projectId: project.id,
});
await fetchProject();
toast.success('Issue has been successfully created.');
onCreate();
} catch (error) {
}
this.setState({ isCreating: false });
};
onInputChange = (field, value) => {
this.setState({
form: {
...this.state.form,
[field]: value,
}
});
};
render() {
let { project, modalClose } = this.props;
return (
<Form
enableReinitialize
initialValues={ this.state.form }
validations={ {} }
validate={ () => true }
onSubmit={ this.onSubmit }
>
<FormElement>
<FormHeading>
Create issue
</FormHeading>
<Field.Select
name="type"
label="Issue Type"
tip="Start typing to get a list of possible matches."
options={ typeOptions }
renderOption={ renderType }
renderValue={ renderType }
/>
<Divider/>
<Field.Input
name="title"
label="Short Summary"
tip="Concisely summarize the issue in one or two sentences."
onChange={ this.onInputChange }
/>
<Field.TextEditor
name="description"
label="Description"
tip="Describe the issue in as much detail as you'd like."
onChange={ this.onInputChange }
/>
<Field.Select
name="reporterId"
label="Reporter"
options={ userOptions(project) }
renderOption={ renderUser(project) }
renderValue={ renderUser(project) }
onChange={ this.onInputChange }
/>
<Field.Select
isMulti
name="userIds"
label="Assignees"
tio="People who are responsible for dealing with this issue."
onChange={ this.onInputChange }
options={ userOptions(project) }
renderOption={ renderUser(project) }
renderValue={ renderUser(project) }
/>
<Field.Select
name="priority"
label="Priority"
tip="Priority in relation to other issues."
options={ priorityOptions }
renderOption={ renderPriority }
renderValue={ renderPriority }
onChange={ this.onInputChange }
/>
<Actions>
<ActionButton type="submit" variant="primary" onClick={ this.onSubmit }>
Create Issue
</ActionButton>
<ActionButton type="button" variant="empty" onClick={ modalClose }>
Cancel
</ActionButton>
</Actions>
</FormElement>
</Form>
);
}
}
const typeOptions = Object.values(IssueType).map(type => ({
value: type,
label: IssueTypeCopy[type],
}));
const priorityOptions = Object.values(IssuePriority).map(priority => ({
value: priority,
label: IssuePriorityCopy[priority],
}));
const userOptions = project => project.users.map(user => ({ value: user.id, label: user.name }));
const renderType = ({ value: type }) => (
<SelectItem>
<IssueTypeIcon type={ type } top={ 1 }/>
<SelectItemLabel>{ IssueTypeCopy[type] }</SelectItemLabel>
</SelectItem>
);
const renderPriority = ({ value: priority }) => (
<SelectItem>
<IssuePriorityIcon priority={ priority } top={ 1 }/>
<SelectItemLabel>{ IssuePriorityCopy[priority] }</SelectItemLabel>
</SelectItem>
);
const renderUser = project => ({ value: userId, removeOptionValue }) => {
const user = project.users.find(({ id }) => id === userId);
return (
<SelectItem
key={ user.id }
withBottomMargin={ !!removeOptionValue }
onClick={ () => removeOptionValue && removeOptionValue() }
>
<Avatar size={ 20 } avatarUrl={ user.avatarUrl } name={ user.name }/>
<SelectItemLabel>{ user.name }</SelectItemLabel>
{ removeOptionValue && <Icon type="close" top={ 2 }/> }
</SelectItem>
);
};
ProjectIssueCreate.propTypes = {
project: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
onCreate: PropTypes.func.isRequired,
modalClose: PropTypes.func.isRequired,
};
export default ProjectIssueCreate;

File diff suppressed because one or more lines are too long

View File

@ -1,97 +0,0 @@
import styled from 'styled-components';
import { color, font } from '../../shared/utils/styles';
import { Icon, InputDebounced, Spinner } from '../../shared/components';
export const IssueSearch = styled.div`
padding: 25px 35px 60px;
`;
export const SearchInputCont = styled.div`
position: relative;
padding-right: 30px;
margin-bottom: 40px;
`;
export const SearchInputDebounced = styled(InputDebounced)`
height: 40px;
input {
padding: 0 0 0 32px;
border: none;
border-bottom: 2px solid ${color.primary};
background: #fff;
font-size: 21px
&:focus,
&:hover {
box-shadow: none;
border: none;
border-bottom: 2px solid ${color.primary};
background: #fff;
}
}
`;
export const SearchIcon = styled(Icon)`
position: absolute;
top: 8px;
left: 0;
color: ${color.textMedium};
`;
export const SearchSpinner = styled(Spinner)`
position: absolute;
top: 5px;
right: 30px;
`;
export const Issue = styled.div`
display: flex;
align-items: center;
padding: 4px 10px;
border-radius: 4px;
transition: background 0.1s;
cursor: pointer;
user-select: none;
&:hover {
background: ${color.backgroundLight};
}
`;
export const IssueData = styled.div`
padding-left: 15px;
`;
export const IssueTitle = styled.div`
color: ${color.textDark};
font-size: 15px
`;
export const IssueTypeId = styled.div`
text-transform: uppercase;
color: ${color.textMedium};
font-size: 12.5px
`;
export const SectionTitle = styled.div`
padding-bottom: 12px;
text-transform: uppercase;
color: ${color.textMedium};
font-family: "CircularStdBold"; font-weight: normal
font-size: 11.5px
`;
export const NoResults = styled.div`
padding-top: 50px;
text-align: center;
`;
export const NoResultsTitle = styled.div`
padding-top: 30px;
${ font.medium };font-weight: normal;
font-size: 20px
`;
export const NoResultsTip = styled.div`
padding-top: 10px;
font-size: 15px
`;

View File

@ -1,99 +0,0 @@
import React, { Fragment, useState } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { get } from 'lodash';
import useApi from '../../shared/hooks/api';
import { sortByNewest } from '../../shared/utils/javascript';
import { IssueTypeIcon } from '../../shared/components';
import NoResultsSVG from './NoResultsSvg';
import {
Issue,
IssueData,
IssueSearch,
IssueTitle,
IssueTypeId,
NoResults,
NoResultsTip,
NoResultsTitle,
SearchIcon,
SearchInputCont,
SearchInputDebounced,
SearchSpinner,
SectionTitle,
} from './Styles';
const ProjectIssueSearch = ({ project }) => {
const [isSearchTermEmpty, setIsSearchTermEmpty] = useState(true);
const [{ data, isLoading }, fetchIssues] = useApi.get('/issues', {}, { lazy: true });
const matchingIssues = get(data, 'issues', []);
const recentIssues = sortByNewest(project.issues, 'createdAt').slice(0, 10);
const handleSearchChange = value => {
const searchTerm = value.trim();
setIsSearchTermEmpty(!searchTerm);
if (searchTerm) {
fetchIssues({ searchTerm });
}
};
return (
<IssueSearch>
<SearchInputCont>
<SearchInputDebounced
autoFocus
placeholder="Search issues by summary, description..."
onChange={handleSearchChange}
/>
<SearchIcon type="search" size={22} />
{isLoading && <SearchSpinner />}
</SearchInputCont>
{isSearchTermEmpty && recentIssues.length > 0 && (
<Fragment>
<SectionTitle>Recent Issues</SectionTitle>
{recentIssues.map(renderIssue)}
</Fragment>
)}
{!isSearchTermEmpty && matchingIssues.length > 0 && (
<Fragment>
<SectionTitle>Matching Issues</SectionTitle>
{matchingIssues.map(renderIssue)}
</Fragment>
)}
{!isSearchTermEmpty && !isLoading && matchingIssues.length === 0 && (
<NoResults>
<NoResultsSVG />
<NoResultsTitle>We couldn&apos;t find anything matching your search</NoResultsTitle>
<NoResultsTip>Try again with a different term.</NoResultsTip>
</NoResults>
)}
</IssueSearch>
);
};
const renderIssue = issue => (
<Link key={issue.id} to={`/project/board/issues/${issue.id}`}>
<Issue>
<IssueTypeIcon type={issue.type} size={25} />
<IssueData>
<IssueTitle>{issue.title}</IssueTitle>
<IssueTypeId>{`${issue.type}-${issue.id}`}</IssueTypeId>
</IssueData>
</Issue>
</Link>
);
ProjectIssueSearch.propTypes = {
project: PropTypes.object.isRequired,
};
export default ProjectIssueSearch;

View File

@ -1,81 +0,0 @@
import styled from 'styled-components';
import { NavLink } from 'react-router-dom';
import { color, sizes, zIndexValues } from '../../shared/utils/styles';
import { Logo } from '../../shared/components';
export const NavLeft = styled.aside`
z-index: ${zIndexValues.navLeft};
position: fixed;
top: 0;
left: 0;
overflow-x: hidden;
height: 100vh;
width: ${sizes.appNavBarLeftWidth}px;
background: ${color.backgroundDarkPrimary};
transition: all 0.1s;
transform: translateZ(0);
&:hover {
width: 200px;
box-shadow: 0 0 50px 0 rgba(0, 0, 0, 0.6);
}
`;
export const LogoLink = styled(NavLink)`
display: block;
position: relative;
left: 0;
margin: 20px 0 10px;
transition: left 0.1s;
`;
export const StyledLogo = styled(Logo)`
display: inline-block;
margin-left: 8px;
padding: 10px;
cursor: pointer;
user-select: none;
`;
export const Bottom = styled.div`
position: absolute;
bottom: 20px;
left: 0;
width: 100%;
`;
export const Item = styled.div`
position: relative;
width: 100%;
height: 42px;
line-height: 42px;
padding-left: 64px;
color: #deebff;
transition: color 0.1s;
cursor: pointer;
user-select: none;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
i {
position: absolute;
left: 18px;
}
`;
export const ItemText = styled.div`
position: relative;
right: 12px;
visibility: hidden;
opacity: 0;
text-transform: uppercase;
transition: all 0.1s;
transition-property: right, visibility, opacity;
font-family: "CircularStdBold"; font-weight: normal
font-size: 12px
${NavLeft}:hover & {
right: 0;
visibility: visible;
opacity: 1;
}
`;

View File

@ -1,44 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { AboutTooltip, Icon } from '../../shared/components';
import { Bottom, Item, ItemText, LogoLink, NavLeft, StyledLogo } from './Styles';
const ProjectNavbarLeft = ({ issueSearchModalOpen, issueCreateModalOpen }) => (
<NavLeft class={ 'NavLeft' }>
<LogoLink to="/">
<StyledLogo color="#fff"/>
</LogoLink>
<Item onClick={ issueSearchModalOpen }>
<Icon type="search" size={ 22 } top={ 1 } left={ 3 }/>
<ItemText>Search issues</ItemText>
</Item>
<Item onClick={ issueCreateModalOpen }>
<Icon type="plus" size={27} />
<ItemText>Create Issue</ItemText>
</Item>
<Bottom>
<AboutTooltip
placement="right"
offset={{ top: -218 }}
renderLink={linkProps => (
<Item {...linkProps}>
<Icon type="help" size={25} />
<ItemText>About</ItemText>
</Item>
)}
/>
</Bottom>
</NavLeft>
);
ProjectNavbarLeft.propTypes = {
issueSearchModalOpen: PropTypes.func.isRequired,
issueCreateModalOpen: PropTypes.func.isRequired,
};
export default ProjectNavbarLeft;

View File

@ -1,24 +0,0 @@
import styled from 'styled-components';
import { font } from '../../shared/utils/styles';
import { Button, Form } from '../../shared/components';
export const FormCont = styled.div`
display: flex;
justify-content: center;
`;
export const FormElement = styled(Form.Element)`
width: 100%;
max-width: 640px;
`;
export const FormHeading = styled.h1`
padding: 6px 0 15px;
font-size: 24px
${ font.medium };font-weight: normal;; font-weight: normal;
`;
export const ActionButton = styled(Button)`
margin-top: 30px;
`;

View File

@ -1,106 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from "react-redux";
import { ProjectCategory, ProjectCategoryCopy } from '../../shared/constants/projects';
import toast from '../../shared/utils/toast';
import api from '../../shared/utils/api';
import { Breadcrumbs, Form } from '../../shared/components';
import {
updateProjectFormFieldChanged,
updateProjectFormRequest,
updateProjectFormSuccess,
} from '../../actions/forms';
import { ActionButton, FormCont, FormElement, FormHeading } from './Styles';
class ProjectSettings extends React.Component {
state = {
isUpdating: false, form: {
name: '',
url: '',
category: '',
description: '',
}
};
componentDidMount() {
this.props.updateProjectFormFieldChanged(this.props.project);
}
onSubmit = async () => {
this.setState({ isUpdating: true });
try {
await api.put(`/project/${ this.props.project.id }`, this.state.form);
await this.props.fetchProject();
toast.success('Changes have been saved successfully.');
} catch (error) {
}
this.setState({ isUpdating: false });
};
onChange = (field, value) => this.props.updateProject({ [field]: value });
render() {
let { updateProject: project } = this.props;
if (!project.id) return <></>;
return (
<Form
initialValues={ project }
validations={ {
name: [ Form.is.required(), Form.is.maxLength(100) ],
url: Form.is.url(),
category: Form.is.required(),
} }
onSubmit={ this.onSubmit }
>
<FormCont>
<FormElement>
<Breadcrumbs items={ [ 'Projects', project.name, 'Project Details' ] }/>
<FormHeading>Project Details</FormHeading>
<Form.Field.Input name="name" label="Name" onChange={ this.onChange }/>
<Form.Field.Input name="url" label="URL" onChange={ this.onChange }/>
<Form.Field.TextEditor
name="description"
label="Description"
tip="Describe the project in as much detail as you'd like."
onChange={ this.onChange }
/>
<Form.Field.Select
name="category"
label="Project Category"
options={ categoryOptions }
onChange={ this.onChange }
/>
<ActionButton type="submit" variant="primary" isWorking={ this.state.isUpdating }>
Save changes
</ActionButton>
</FormElement>
</FormCont>
</Form>
);
}
}
const categoryOptions = Object.values(ProjectCategory).map(category => ({
value: category,
label: ProjectCategoryCopy[category],
}));
ProjectSettings.propTypes = {
project: PropTypes.object.isRequired,
fetchProject: PropTypes.func.isRequired,
};
const mapStateToProps = ({ forms: { updateProject } }) => ({ updateProject });
const mapDispatchToProps = ({
updateProjectFormFieldChanged,
updateProjectFormRequest,
updateProjectFormSuccess,
});
export default connect(mapStateToProps, mapDispatchToProps)(ProjectSettings);

Some files were not shown because too many files have changed in this diff Show More