Remove JS garbage
This commit is contained in:
parent
a16ddbcf2e
commit
761305fbbd
@ -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 }]
|
||||
]
|
||||
}
|
@ -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/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
react-client/.gitignore
vendored
2
react-client/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
tmp
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
@ -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. |
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8080",
|
||||
"viewportHeight": 800,
|
||||
"viewportWidth": 1440,
|
||||
"env": {
|
||||
"apiBaseUrl": "http://localhost:3000"
|
||||
}
|
||||
}
|
@ -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',
|
||||
},
|
||||
};
|
@ -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"]
|
||||
}
|
13088
react-client/package-lock.json
generated
13088
react-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
13
react-client/server.js
vendored
13
react-client/server.js
vendored
@ -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);
|
133
react-client/src/App/BaseStyles.js
vendored
133
react-client/src/App/BaseStyles.js
vendored
@ -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;
|
||||
}
|
||||
`;
|
152
react-client/src/App/NormalizeStyles.js
vendored
152
react-client/src/App/NormalizeStyles.js
vendored
@ -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;
|
||||
}
|
||||
`;
|
@ -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;
|
59
react-client/src/App/Toast/Styles.js
vendored
59
react-client/src/App/Toast/Styles.js
vendored
@ -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;
|
||||
`;
|
@ -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;
|
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 384 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 433 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 341 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 432 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 29 KiB |
Binary file not shown.
Binary file not shown.
@ -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;
|
||||
}
|
@ -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;
|
@ -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);
|
39
react-client/src/Auth/Styles.js
vendored
39
react-client/src/Auth/Styles.js
vendored
@ -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;
|
||||
`;
|
57
react-client/src/Project/Board/Filters/Styles.js
vendored
57
react-client/src/Project/Board/Filters/Styles.js
vendored
@ -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};
|
||||
}
|
||||
`;
|
@ -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;
|
14
react-client/src/Project/Board/Header/Styles.js
vendored
14
react-client/src/Project/Board/Header/Styles.js
vendored
@ -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;
|
||||
`;
|
@ -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;
|
@ -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
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
`;
|
@ -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;
|
@ -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
|
||||
`;
|
@ -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;
|
@ -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};
|
||||
}
|
||||
`;
|
@ -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);
|
@ -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
|
||||
`;
|
@ -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;
|
@ -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
|
||||
`;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
}
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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;
|
||||
`;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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
|
||||
`;
|
@ -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;
|
@ -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);
|
||||
}
|
||||
`}
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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;
|
||||
`;
|
@ -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;
|
@ -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
|
||||
`;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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;
|
@ -1,6 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Lists = styled.div`
|
||||
display: flex;
|
||||
margin: 26px -5px 0;
|
||||
`;
|
@ -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;
|
@ -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;
|
60
react-client/src/Project/IssueCreate/Styles.js
vendored
60
react-client/src/Project/IssueCreate/Styles.js
vendored
@ -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;
|
||||
`;
|
@ -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
97
react-client/src/Project/IssueSearch/Styles.js
vendored
97
react-client/src/Project/IssueSearch/Styles.js
vendored
@ -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
|
||||
`;
|
@ -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'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;
|
81
react-client/src/Project/NavbarLeft/Styles.js
vendored
81
react-client/src/Project/NavbarLeft/Styles.js
vendored
@ -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;
|
||||
}
|
||||
`;
|
@ -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;
|
@ -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;
|
||||
`;
|
@ -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
Loading…
Reference in New Issue
Block a user