From 386694d28fcf99b26ae2c5f19acda964594f2710 Mon Sep 17 00:00:00 2001 From: ireic Date: Wed, 18 Dec 2019 03:48:42 +0100 Subject: [PATCH] Implemented first draft of issue modal --- api/src/controllers/issues.ts | 4 +- api/src/entities/Comment.ts | 1 + api/src/entities/Issue.ts | 2 +- client/package-lock.json | 228 ++++++++++++++- client/package.json | 5 + client/src/components/App/App.jsx | 7 +- client/src/components/App/Authenticate.jsx | 29 +- client/src/components/App/BaseStyles.js | 3 +- client/src/components/App/FontStyles.js | 53 ---- .../App}/assets/fonts/CircularStd-Black.eot | Bin .../App}/assets/fonts/CircularStd-Black.otf | Bin .../App}/assets/fonts/CircularStd-Black.svg | 0 .../App}/assets/fonts/CircularStd-Black.ttf | Bin .../App}/assets/fonts/CircularStd-Black.woff | Bin .../App}/assets/fonts/CircularStd-Black.woff2 | Bin .../App}/assets/fonts/CircularStd-Bold.eot | Bin .../App}/assets/fonts/CircularStd-Bold.otf | Bin .../App}/assets/fonts/CircularStd-Bold.svg | 0 .../App}/assets/fonts/CircularStd-Bold.ttf | Bin .../App}/assets/fonts/CircularStd-Bold.woff | Bin .../App}/assets/fonts/CircularStd-Bold.woff2 | Bin .../App}/assets/fonts/CircularStd-Book.eot | Bin .../App}/assets/fonts/CircularStd-Book.otf | Bin .../App}/assets/fonts/CircularStd-Book.svg | 0 .../App}/assets/fonts/CircularStd-Book.ttf | Bin .../App}/assets/fonts/CircularStd-Book.woff | Bin .../App}/assets/fonts/CircularStd-Book.woff2 | Bin .../App}/assets/fonts/CircularStd-Medium.eot | Bin .../App}/assets/fonts/CircularStd-Medium.otf | Bin .../App}/assets/fonts/CircularStd-Medium.svg | 0 .../App}/assets/fonts/CircularStd-Medium.ttf | Bin .../App}/assets/fonts/CircularStd-Medium.woff | Bin .../assets/fonts/CircularStd-Medium.woff2 | Bin .../App/assets/fonts}/jira.svg | 4 +- .../App/assets/fonts}/jira.ttf | Bin 6720 -> 6624 bytes .../App/assets/fonts}/jira.woff | Bin 6796 -> 6700 bytes client/src/components/App/fontStyles.css | 35 +++ .../Project/Board/Filters/Styles.js | 4 +- .../Project/Board/Filters/index.jsx | 5 +- .../components/Project/Board/Header/index.jsx | 15 +- .../Board/IssueDetails/Description/Styles.js | 30 ++ .../Board/IssueDetails/Description/index.jsx | 60 ++++ .../Project/Board/IssueDetails/Loader.jsx | 31 ++ .../Board/IssueDetails/RightActions/Styles.js | 146 ++++++++++ .../Board/IssueDetails/RightActions/index.jsx | 267 ++++++++++++++++++ .../Project/Board/IssueDetails/Styles.js | 16 ++ .../Board/IssueDetails/Title/Styles.js | 31 ++ .../Board/IssueDetails/Title/index.jsx | 53 ++++ .../Board/IssueDetails/TopActions/Styles.js | 72 +++++ .../TopActions/assets/feedback.png | Bin 0 -> 16825 bytes .../Board/IssueDetails/TopActions/index.jsx | 118 ++++++++ .../Project/Board/IssueDetails/index.jsx | 74 +++++ .../Project/Board/IssueModal/Styles.js | 0 .../Project/Board/IssueModal/index.jsx | 0 .../Project/Board/Lists/Issue/Styles.js | 21 +- .../Project/Board/Lists/Issue/index.jsx | 28 +- .../components/Project/Board/Lists/index.jsx | 79 ++---- client/src/components/Project/Board/index.jsx | 46 ++- .../src/components/Project/Sidebar/index.jsx | 9 +- client/src/components/Project/index.jsx | 31 +- client/src/index.jsx | 1 + client/src/shared/components/Button/Styles.js | 40 +-- client/src/shared/components/Button/index.jsx | 67 +++-- .../shared/components/ConfirmModal/Styles.js | 12 +- .../shared/components/ConfirmModal/index.jsx | 25 +- .../src/shared/components/CopyLinkButton.jsx | 21 ++ .../shared/components/DatePicker/Styles.js | 2 +- client/src/shared/components/Icon/index.jsx | 4 +- .../src/shared/components/InputDebounced.jsx | 43 +++ .../components/IssuePriorityIcon/Styles.js | 9 + .../components/IssuePriorityIcon/index.jsx | 21 ++ .../shared/components/IssueTypeIcon/Styles.js | 9 + .../shared/components/IssueTypeIcon/index.jsx | 16 ++ client/src/shared/components/Modal/Styles.js | 6 +- client/src/shared/components/Modal/index.jsx | 31 +- .../shared/components/PageLoader/Styles.js | 2 +- .../src/shared/components/Select/Dropdown.jsx | 39 ++- client/src/shared/components/Select/Styles.js | 54 ++-- client/src/shared/components/Select/index.jsx | 48 ++-- .../components/TextEditedContent/Styles.js | 9 + .../components/TextEditedContent/index.jsx | 21 ++ .../shared/components/TextEditor/Styles.js | 21 ++ .../shared/components/TextEditor/index.jsx | 70 +++++ .../src/shared/components/Textarea/index.jsx | 2 +- .../src/shared/components/Tooltip/Styles.js | 13 + .../src/shared/components/Tooltip/index.jsx | 110 ++++++++ client/src/shared/components/index.js | 7 + client/src/shared/constants/issues.js | 7 + client/src/shared/hooks/api.js | 38 +-- client/src/shared/hooks/debounceValue.js | 19 -- client/src/shared/hooks/onOutsideClick.js | 32 ++- client/src/shared/utils/api.js | 18 +- client/src/shared/utils/html.js | 5 + client/src/shared/utils/javascript.js | 8 +- client/src/shared/utils/styles.js | 57 +++- client/src/shared/utils/validation.js | 2 +- client/webpack.config.js | 4 + 97 files changed, 1972 insertions(+), 428 deletions(-) delete mode 100644 client/src/components/App/FontStyles.js rename client/src/{shared => components/App}/assets/fonts/CircularStd-Black.eot (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Black.otf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Black.svg (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Black.ttf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Black.woff (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Black.woff2 (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Bold.eot (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Bold.otf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Bold.svg (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Bold.ttf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Bold.woff (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Bold.woff2 (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Book.eot (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Book.otf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Book.svg (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Book.ttf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Book.woff (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Book.woff2 (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Medium.eot (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Medium.otf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Medium.svg (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Medium.ttf (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Medium.woff (100%) rename client/src/{shared => components/App}/assets/fonts/CircularStd-Medium.woff2 (100%) rename client/src/{shared/assets/icons => components/App/assets/fonts}/jira.svg (90%) rename client/src/{shared/assets/icons => components/App/assets/fonts}/jira.ttf (61%) rename client/src/{shared/assets/icons => components/App/assets/fonts}/jira.woff (61%) create mode 100644 client/src/components/App/fontStyles.css create mode 100644 client/src/components/Project/Board/IssueDetails/Description/Styles.js create mode 100644 client/src/components/Project/Board/IssueDetails/Description/index.jsx create mode 100644 client/src/components/Project/Board/IssueDetails/Loader.jsx create mode 100644 client/src/components/Project/Board/IssueDetails/RightActions/Styles.js create mode 100644 client/src/components/Project/Board/IssueDetails/RightActions/index.jsx create mode 100644 client/src/components/Project/Board/IssueDetails/Styles.js create mode 100644 client/src/components/Project/Board/IssueDetails/Title/Styles.js create mode 100644 client/src/components/Project/Board/IssueDetails/Title/index.jsx create mode 100644 client/src/components/Project/Board/IssueDetails/TopActions/Styles.js create mode 100644 client/src/components/Project/Board/IssueDetails/TopActions/assets/feedback.png create mode 100644 client/src/components/Project/Board/IssueDetails/TopActions/index.jsx create mode 100644 client/src/components/Project/Board/IssueDetails/index.jsx delete mode 100644 client/src/components/Project/Board/IssueModal/Styles.js delete mode 100644 client/src/components/Project/Board/IssueModal/index.jsx create mode 100644 client/src/shared/components/CopyLinkButton.jsx create mode 100644 client/src/shared/components/InputDebounced.jsx create mode 100644 client/src/shared/components/IssuePriorityIcon/Styles.js create mode 100644 client/src/shared/components/IssuePriorityIcon/index.jsx create mode 100644 client/src/shared/components/IssueTypeIcon/Styles.js create mode 100644 client/src/shared/components/IssueTypeIcon/index.jsx create mode 100644 client/src/shared/components/TextEditedContent/Styles.js create mode 100644 client/src/shared/components/TextEditedContent/index.jsx create mode 100644 client/src/shared/components/TextEditor/Styles.js create mode 100644 client/src/shared/components/TextEditor/index.jsx create mode 100644 client/src/shared/components/Tooltip/Styles.js create mode 100644 client/src/shared/components/Tooltip/index.jsx delete mode 100644 client/src/shared/hooks/debounceValue.js create mode 100644 client/src/shared/utils/html.js diff --git a/api/src/controllers/issues.ts b/api/src/controllers/issues.ts index 24ed695..bab172f 100644 --- a/api/src/controllers/issues.ts +++ b/api/src/controllers/issues.ts @@ -9,7 +9,9 @@ const router = express.Router(); router.get( '/issues/:issueId', catchErrors(async (req, res) => { - const issue = await findEntityOrThrow(Issue, req.params.issueId); + const issue = await findEntityOrThrow(Issue, req.params.issueId, { + relations: ['users', 'comments'], + }); res.respond({ issue }); }), ); diff --git a/api/src/entities/Comment.ts b/api/src/entities/Comment.ts index 963adfb..a96d193 100644 --- a/api/src/entities/Comment.ts +++ b/api/src/entities/Comment.ts @@ -42,6 +42,7 @@ class Comment extends BaseEntity { @ManyToOne( () => Issue, issue => issue.comments, + { onDelete: 'CASCADE' }, ) issue: Issue; } diff --git a/api/src/entities/Issue.ts b/api/src/entities/Issue.ts index acff764..4f30a81 100644 --- a/api/src/entities/Issue.ts +++ b/api/src/entities/Issue.ts @@ -23,7 +23,7 @@ class Issue extends BaseEntity { type: [is.required(), is.oneOf(Object.values(IssueType))], status: [is.required(), is.oneOf(Object.values(IssueStatus))], priority: [is.required(), is.oneOf(Object.values(IssuePriority))], - description: is.maxLength(100000), + reporterId: is.required(), }; @PrimaryGeneratedColumn() diff --git a/client/package-lock.json b/client/package-lock.json index 943806f..740e5c4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -2212,6 +2212,11 @@ } } }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -2599,6 +2604,34 @@ "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU=" }, + "css-loader": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.3.2.tgz", + "integrity": "sha512-4XSiURS+YEK2fQhmSaM1onnUm0VKWNf6WWBYjkp9YbSDGCBTVZ5XOM6Gkxo8tLgQlzkZOBJvk9trHlDk4gjEYg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.23", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.1.1", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.0.2", + "schema-utils": "^2.6.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + } + } + }, "css-select": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", @@ -2627,6 +2660,12 @@ "integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==", "dev": true }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, "csstype": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz", @@ -2679,7 +2718,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, "requires": { "is-arguments": "^1.0.4", "is-date-object": "^1.0.1", @@ -2709,7 +2747,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -3721,6 +3758,11 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -3824,6 +3866,11 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, "fast-glob": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.1.tgz", @@ -4784,8 +4831,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -4943,7 +4989,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -5266,6 +5311,15 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", @@ -5324,6 +5378,12 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, + "indexes-of": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", + "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", + "dev": true + }, "infer-owner": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", @@ -5496,8 +5556,7 @@ "is-arguments": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" }, "is-arrayish": { "version": "0.2.1", @@ -5549,8 +5608,7 @@ "is-date-object": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true + "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" }, "is-descriptor": { "version": "0.1.6", @@ -5682,7 +5740,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, "requires": { "has": "^1.0.1" } @@ -6910,14 +6967,12 @@ "object-is": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true + "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=" }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -7206,6 +7261,11 @@ "no-case": "^2.2.0" } }, + "parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7407,6 +7467,94 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.24.tgz", + "integrity": "sha512-Xl0XvdNWg+CblAXzNvbSOUvgJXwSjmbAKORqyw9V2AlHrm1js2gFw9y3jibBAhpKZi8b5JzJCVh/FyzPsTtgTA==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.2.tgz", + "integrity": "sha512-jM/V8eqM4oJ/22j0gx4jrp63GSvDH6v86OqyTHHUvk4/k1vceipZsaymiZ5PvocqZOl5SFHiFJqjs3la0wnfIQ==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.16", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.0.0" + }, + "dependencies": { + "postcss-value-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", + "dev": true + } + } + }, + "postcss-modules-scope": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.1.1.tgz", + "integrity": "sha512-OXRUPecnHCg8b9xWvldG/jUpRIGPNRka0r4D4j0ESUU2/5IOnpsjfPPmDprM3Ih8CgZ8FXjWqaniK5v4rWt3oQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz", + "integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "indexes-of": "^1.0.1", + "uniq": "^1.0.1" + } + }, "postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", @@ -7577,6 +7725,36 @@ "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", "dev": true }, + "quill": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz", + "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==", + "requires": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.2", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + }, + "dependencies": { + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=" + } + } + }, + "quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "requires": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + } + }, "raf-schd": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz", @@ -7651,6 +7829,11 @@ "use-memo-one": "^1.1.1" } }, + "react-content-loader": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/react-content-loader/-/react-content-loader-4.3.3.tgz", + "integrity": "sha512-9DdPa6rt013sO5Z2TBnRLExSBVPMwcFP9nxFKQXyQHo71/xR4nfiQqQvFFI92XQlahILYZPr+S/W5hWuRmbYjg==" + }, "react-dom": { "version": "16.12.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz", @@ -7912,7 +8095,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz", "integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==", - "dev": true, "requires": { "define-properties": "^1.1.2" } @@ -8990,6 +9172,16 @@ "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", "dev": true }, + "style-loader": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.0.1.tgz", + "integrity": "sha512-CnpEkSR1C+REjudiTWCv4+ssP7SCiuaQZJTZDWBRwTJoS90mdqkB8uOGMHKgVeUzpaU7IfLWoyQbvvs5Joj3Xw==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.0.1" + } + }, "styled-components": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", @@ -9365,6 +9557,12 @@ "set-value": "^2.0.1" } }, + "uniq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", + "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", + "dev": true + }, "unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", diff --git a/client/package.json b/client/package.json index 2b9c7c1..7465ca1 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@babel/preset-react": "^7.7.4", "babel-eslint": "^10.0.3", "babel-loader": "^8.0.6", + "css-loader": "^3.3.2", "eslint": "^6.1.0", "eslint-config-airbnb": "^18.0.1", "eslint-config-prettier": "^6.7.0", @@ -29,6 +30,7 @@ "html-webpack-plugin": "^3.2.0", "lint-staged": "^9.5.0", "prettier": "^1.19.1", + "style-loader": "^1.0.1", "url-loader": "^3.0.0", "webpack": "^4.41.2", "webpack-cli": "^3.3.10", @@ -44,12 +46,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-router-dom": "^5.1.2", "react-textarea-autosize": "^7.1.2", "react-transition-group": "^4.3.0", + "regenerator-runtime": "^0.13.3", "styled-components": "^4.4.1", "sweet-pubsub": "^1.1.2" }, diff --git a/client/src/components/App/App.jsx b/client/src/components/App/App.jsx index 51cea4d..7734795 100644 --- a/client/src/components/App/App.jsx +++ b/client/src/components/App/App.jsx @@ -1,15 +1,18 @@ import React from 'react'; import NormalizeStyles from './NormalizeStyles'; -import FontStyles from './FontStyles'; import BaseStyles from './BaseStyles'; import Toast from './Toast'; import Routes from './Routes'; +// We're importing css because @font-face in styled-components causes font files +// to be constantly re-requested from the server (which causes screen flicker) +// https://github.com/styled-components/styled-components/issues/1593 +import './fontStyles.css'; + const App = () => ( <> - diff --git a/client/src/components/App/Authenticate.jsx b/client/src/components/App/Authenticate.jsx index 5299c5d..a8fef30 100644 --- a/client/src/components/App/Authenticate.jsx +++ b/client/src/components/App/Authenticate.jsx @@ -1,27 +1,28 @@ import React, { useEffect } from 'react'; import { useHistory } from 'react-router-dom'; -import useApi from 'shared/hooks/api'; +import toast from 'shared/utils/toast'; +import api from 'shared/utils/api'; import { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken'; import { PageLoader } from 'shared/components'; const Authenticate = () => { - const [{ data }, createGuestAccount] = useApi.post('/authentication/guest'); - const history = useHistory(); useEffect(() => { - if (!getStoredAuthToken()) { - createGuestAccount(); - } - }, [createGuestAccount]); - - useEffect(() => { - if (data && data.authToken) { - storeAuthToken(data.authToken); - history.push('/'); - } - }, [data, history]); + const createGuestAccount = async () => { + if (!getStoredAuthToken()) { + try { + const { authToken } = await api.post('/authentication/guest'); + storeAuthToken(authToken); + history.push('/'); + } catch (error) { + toast.error(error); + } + } + }; + createGuestAccount(); + }, [history]); return ; }; diff --git a/client/src/components/App/BaseStyles.js b/client/src/components/App/BaseStyles.js index 07423b1..ca2969c 100644 --- a/client/src/components/App/BaseStyles.js +++ b/client/src/components/App/BaseStyles.js @@ -34,6 +34,7 @@ export default createGlobalStyle` } a, a:hover, a:visited, a:active { + color: inherit; text-decoration: none; } @@ -92,7 +93,7 @@ export default createGlobalStyle` } textarea { - line-height: 1.6; + line-height: 1.4285; } body, select { diff --git a/client/src/components/App/FontStyles.js b/client/src/components/App/FontStyles.js deleted file mode 100644 index b74d56e..0000000 --- a/client/src/components/App/FontStyles.js +++ /dev/null @@ -1,53 +0,0 @@ -import { createGlobalStyle } from 'styled-components'; - -import BlackWoff2 from 'shared/assets/fonts/CircularStd-Black.woff2'; -import BoldWoff2 from 'shared/assets/fonts/CircularStd-Bold.woff2'; -import MediumWoff2 from 'shared/assets/fonts/CircularStd-Medium.woff2'; -import BookWoff2 from 'shared/assets/fonts/CircularStd-Book.woff2'; -import BlackWoff from 'shared/assets/fonts/CircularStd-Black.woff'; -import BoldWoff from 'shared/assets/fonts/CircularStd-Bold.woff'; -import MediumWoff from 'shared/assets/fonts/CircularStd-Medium.woff'; -import BookWoff from 'shared/assets/fonts/CircularStd-Book.woff'; -import IconsSvg from 'shared/assets/icons/jira.svg'; -import IconsTtf from 'shared/assets/icons/jira.ttf'; -import IconsWoff from 'shared/assets/icons/jira.woff'; - -export default createGlobalStyle` - @font-face { - font-family: "CircularStdBlack"; - src: url("${BlackWoff2}") format("woff2"), - url("${BlackWoff}") format("woff"); - font-weight: normal; - font-style: normal; - } - @font-face { - font-family: "CircularStdBold"; - src: url("${BoldWoff2}") format("woff2"), - url("${BoldWoff}") format("woff"); - font-weight: normal; - font-style: normal; - } - @font-face { - font-family: "CircularStdMedium"; - src: url("${MediumWoff2}") format("woff2"), - url("${MediumWoff}") format("woff"); - font-weight: normal; - font-style: normal; - } - @font-face { - font-family: "CircularStdBook"; - src: url("${BookWoff2}") format("woff2"), - url("${BookWoff}") format("woff"); - font-weight: normal; - font-style: normal; - } - @font-face { - font-family: "jira"; - src: - url("${IconsTtf}") format("truetype"), - url("${IconsWoff}") format("woff"), - url("${IconsSvg}#jira") format("svg"); - font-weight: normal; - font-style: normal; - } -`; diff --git a/client/src/shared/assets/fonts/CircularStd-Black.eot b/client/src/components/App/assets/fonts/CircularStd-Black.eot similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Black.eot rename to client/src/components/App/assets/fonts/CircularStd-Black.eot diff --git a/client/src/shared/assets/fonts/CircularStd-Black.otf b/client/src/components/App/assets/fonts/CircularStd-Black.otf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Black.otf rename to client/src/components/App/assets/fonts/CircularStd-Black.otf diff --git a/client/src/shared/assets/fonts/CircularStd-Black.svg b/client/src/components/App/assets/fonts/CircularStd-Black.svg similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Black.svg rename to client/src/components/App/assets/fonts/CircularStd-Black.svg diff --git a/client/src/shared/assets/fonts/CircularStd-Black.ttf b/client/src/components/App/assets/fonts/CircularStd-Black.ttf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Black.ttf rename to client/src/components/App/assets/fonts/CircularStd-Black.ttf diff --git a/client/src/shared/assets/fonts/CircularStd-Black.woff b/client/src/components/App/assets/fonts/CircularStd-Black.woff similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Black.woff rename to client/src/components/App/assets/fonts/CircularStd-Black.woff diff --git a/client/src/shared/assets/fonts/CircularStd-Black.woff2 b/client/src/components/App/assets/fonts/CircularStd-Black.woff2 similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Black.woff2 rename to client/src/components/App/assets/fonts/CircularStd-Black.woff2 diff --git a/client/src/shared/assets/fonts/CircularStd-Bold.eot b/client/src/components/App/assets/fonts/CircularStd-Bold.eot similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Bold.eot rename to client/src/components/App/assets/fonts/CircularStd-Bold.eot diff --git a/client/src/shared/assets/fonts/CircularStd-Bold.otf b/client/src/components/App/assets/fonts/CircularStd-Bold.otf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Bold.otf rename to client/src/components/App/assets/fonts/CircularStd-Bold.otf diff --git a/client/src/shared/assets/fonts/CircularStd-Bold.svg b/client/src/components/App/assets/fonts/CircularStd-Bold.svg similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Bold.svg rename to client/src/components/App/assets/fonts/CircularStd-Bold.svg diff --git a/client/src/shared/assets/fonts/CircularStd-Bold.ttf b/client/src/components/App/assets/fonts/CircularStd-Bold.ttf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Bold.ttf rename to client/src/components/App/assets/fonts/CircularStd-Bold.ttf diff --git a/client/src/shared/assets/fonts/CircularStd-Bold.woff b/client/src/components/App/assets/fonts/CircularStd-Bold.woff similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Bold.woff rename to client/src/components/App/assets/fonts/CircularStd-Bold.woff diff --git a/client/src/shared/assets/fonts/CircularStd-Bold.woff2 b/client/src/components/App/assets/fonts/CircularStd-Bold.woff2 similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Bold.woff2 rename to client/src/components/App/assets/fonts/CircularStd-Bold.woff2 diff --git a/client/src/shared/assets/fonts/CircularStd-Book.eot b/client/src/components/App/assets/fonts/CircularStd-Book.eot similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Book.eot rename to client/src/components/App/assets/fonts/CircularStd-Book.eot diff --git a/client/src/shared/assets/fonts/CircularStd-Book.otf b/client/src/components/App/assets/fonts/CircularStd-Book.otf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Book.otf rename to client/src/components/App/assets/fonts/CircularStd-Book.otf diff --git a/client/src/shared/assets/fonts/CircularStd-Book.svg b/client/src/components/App/assets/fonts/CircularStd-Book.svg similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Book.svg rename to client/src/components/App/assets/fonts/CircularStd-Book.svg diff --git a/client/src/shared/assets/fonts/CircularStd-Book.ttf b/client/src/components/App/assets/fonts/CircularStd-Book.ttf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Book.ttf rename to client/src/components/App/assets/fonts/CircularStd-Book.ttf diff --git a/client/src/shared/assets/fonts/CircularStd-Book.woff b/client/src/components/App/assets/fonts/CircularStd-Book.woff similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Book.woff rename to client/src/components/App/assets/fonts/CircularStd-Book.woff diff --git a/client/src/shared/assets/fonts/CircularStd-Book.woff2 b/client/src/components/App/assets/fonts/CircularStd-Book.woff2 similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Book.woff2 rename to client/src/components/App/assets/fonts/CircularStd-Book.woff2 diff --git a/client/src/shared/assets/fonts/CircularStd-Medium.eot b/client/src/components/App/assets/fonts/CircularStd-Medium.eot similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Medium.eot rename to client/src/components/App/assets/fonts/CircularStd-Medium.eot diff --git a/client/src/shared/assets/fonts/CircularStd-Medium.otf b/client/src/components/App/assets/fonts/CircularStd-Medium.otf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Medium.otf rename to client/src/components/App/assets/fonts/CircularStd-Medium.otf diff --git a/client/src/shared/assets/fonts/CircularStd-Medium.svg b/client/src/components/App/assets/fonts/CircularStd-Medium.svg similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Medium.svg rename to client/src/components/App/assets/fonts/CircularStd-Medium.svg diff --git a/client/src/shared/assets/fonts/CircularStd-Medium.ttf b/client/src/components/App/assets/fonts/CircularStd-Medium.ttf similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Medium.ttf rename to client/src/components/App/assets/fonts/CircularStd-Medium.ttf diff --git a/client/src/shared/assets/fonts/CircularStd-Medium.woff b/client/src/components/App/assets/fonts/CircularStd-Medium.woff similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Medium.woff rename to client/src/components/App/assets/fonts/CircularStd-Medium.woff diff --git a/client/src/shared/assets/fonts/CircularStd-Medium.woff2 b/client/src/components/App/assets/fonts/CircularStd-Medium.woff2 similarity index 100% rename from client/src/shared/assets/fonts/CircularStd-Medium.woff2 rename to client/src/components/App/assets/fonts/CircularStd-Medium.woff2 diff --git a/client/src/shared/assets/icons/jira.svg b/client/src/components/App/assets/fonts/jira.svg similarity index 90% rename from client/src/shared/assets/icons/jira.svg rename to client/src/components/App/assets/fonts/jira.svg index d22007a..e13a8e4 100755 --- a/client/src/shared/assets/icons/jira.svg +++ b/client/src/components/App/assets/fonts/jira.svg @@ -25,10 +25,10 @@ - + - + \ No newline at end of file diff --git a/client/src/shared/assets/icons/jira.ttf b/client/src/components/App/assets/fonts/jira.ttf similarity index 61% rename from client/src/shared/assets/icons/jira.ttf rename to client/src/components/App/assets/fonts/jira.ttf index ba30d7aefffceeaf6d364bbf5c7b0928ff691e5d..c4c92ab09aa5ff5b1421f0f7fd900c9a8ee4b47c 100755 GIT binary patch delta 826 zcmYk4O=uHQ5XWcs?Y`Y@(k8o2vPr*e5^a&yq~xn=KNJsD1V6w=FNHSNwh7@^5-~9eF^WMH;c2fJvG9UoB zpaKj=u3nEPJSLBk@|J9FzK|<6rW?598%9V<=dx_U**OrzaEEebY_K7P1{+IcM z+->XTuipSxisG3(3F>?Hjd+>(Xuhzzwg~Pt@e|_W^2+?_+{LN$lpG|6Glkq*5u*5$ zc$|3fUaqi^KlN#sc!n;yRa_~p(vQ*#?L2*b0fWzQEP0O54H`%w(_;o3zDCe#I*!O9 zf<~ZC==`u`i%~T_czY4Mjj{q+r^c$R0v?!v%P<3X=@P@o)h>*NBSXV6OdH42DdE@{ z5pq99Bi!!`j8R&^8o*>Ykr|z&v}7VfiP3O6C3p-IBHE84O@s`JQDO2hmbssS$`!BP zx}kd&u42t+n6cii-ne1-s%9u;+HC<36k8~*gNm(ofde7a2!#wYR23fqQl(kbyu7`< znq+6~p15K9e5Miau|-m|MuOodVIzXb_cs$%Pm9T=DSYy8t zF(i@f+RlbsE}uKBoy#AVwtsxX4Wma(Ve*LFLcM4l`s7~Gv03+y)d?;K4LGJ{+{RUH zKsU!t%Veu=EenS2*bZT;Zm^)Ee}k}#C0Akz`>bV!n@COP*!W((BFpD3HXbe~d7;6> z#U<%(w(`qj&ib*pOmNi6C_W6c;Gq!-6^cS7Cl4ZcXtKUZ1v~KHnfH7DH?#BRQEDrx-=CV8 z00bJLL2&XpxCdBS4(aQIzEW&9pDh4jj`&Tk#&n~;kS%idmSV?0c@`EQ%oFf8_`gdqwdHF69mTn#-@O65FMi_JzVcxKfKtv*a5$CkdS6 zx-@Yyna{40!Ca%budOKV-^=Fbh&`jjZ*k?$e%vW66<6p!uJwZ{G%J%hlDI&27yYdO zB3-tx;cu9B)3%AyRt;$A?FjAfhG;Ng z8jkc2#4x3uOeOik!U)Gb7>&pQzdk}X-RQ+ccqlzQPPW8QnvBtKD#_y!9OBs?R4qqeTA9!XZP=GVNj?#=GFrunPhV9;x|0Ui>Ilh$FyX1lj_w};0eSQ#r6*SvneSBr-%Zfe#DXaNFe;vvD}HJ0j@Wkr!?w`$p7 z1K$#X?{{=fUHjpaaa+=NvBV%|e_=bU1pROd2B~f1IM8ks^=hC$qD17Vf?cS{x*_>` z(a_Vwm`tbeG#XB1JBR`k1c`|bNmM0dhzC_ir>Jy#aCt*?soHUwTHgHAY6|Da-WS*k)vM|gwcfGTQQzJ-pW!&3E delta 958 zcmYLIO>EO<7=B-WwH@2B^W)z%b`sT%OOF^6vS2!R}NJuawG$gLu1-9&UfbH}5_beZ~DyY{My z_SOlUoth!JOmEB4g;02I5B?quh$ExtFpP2V)E)r_xN{TrRG)z{c*0(=UC6>DM9yQP zhN55^b`EW;yuUt&^;)$OPGcA_e;VtR+AQYK)X>*8OdyGAn|_saZ+Ep4klr5?sN~_B zQhC2cmZl+y9IUq5n<1^ue!I2$U(#-^bGZORkL8OP=6(@#x);|N$6*ioxPE{WWG2PL zn3y$;Y%C^S~pXP$bMtr!2wvNIYljo zff)5Tyis#BfOo7~G`L|*X{3o{wfc~@>J$WF5Hev%Q$!PeTcoE%_F29deCYd`NjCEC z((=7=J(2jx7YmtwNmlm6q=Kd$Pp4&GD2k#lNO||nY-7Q53mI9CrP4W9(2cU^I(A$U z7!!D1FT1W&5*2RcXrr;D8*-{t`f6eEly=4+t7=T~C2RDwu6w+Y6-8H76Uk&VX%?8A z)~8b`RStuV`NK#5xyNNxb!Kv+UN4uMPKM)>iMhS|>gzv_09-d8;Z*cz5f*-UnK+wh z{0RybXg$=e8~>k!U+K%+-P*RlF%7E!PWY0rNLV6FZJlsRN?JN5eJdB_j}>j_Yv*VD E7sLUlJOBUy diff --git a/client/src/components/App/fontStyles.css b/client/src/components/App/fontStyles.css new file mode 100644 index 0000000..20ef0b2 --- /dev/null +++ b/client/src/components/App/fontStyles.css @@ -0,0 +1,35 @@ +@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; +} diff --git a/client/src/components/Project/Board/Filters/Styles.js b/client/src/components/Project/Board/Filters/Styles.js index b2f7433..65de5f4 100644 --- a/client/src/components/Project/Board/Filters/Styles.js +++ b/client/src/components/Project/Board/Filters/Styles.js @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { Input, Avatar, Button } from 'shared/components'; +import { InputDebounced, Avatar, Button } from 'shared/components'; import { color, font, mixin } from 'shared/utils/styles'; export const Filters = styled.div` @@ -9,7 +9,7 @@ export const Filters = styled.div` margin-top: 24px; `; -export const SearchInput = styled(Input)` +export const SearchInput = styled(InputDebounced)` margin-right: 18px; width: 160px; `; diff --git a/client/src/components/Project/Board/Filters/index.jsx b/client/src/components/Project/Board/Filters/index.jsx index b272496..db76dc4 100644 --- a/client/src/components/Project/Board/Filters/index.jsx +++ b/client/src/components/Project/Board/Filters/index.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { xor, debounce } from 'lodash'; +import { xor } from 'lodash'; import { Filters, @@ -30,7 +30,8 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters setFiltersMerge({ searchQuery: value }), 500)} + value={searchQuery} + onChange={value => setFiltersMerge({ searchQuery: value })} /> {projectUsers.map(user => ( diff --git a/client/src/components/Project/Board/Header/index.jsx b/client/src/components/Project/Board/Header/index.jsx index f9774f8..9323ddc 100644 --- a/client/src/components/Project/Board/Header/index.jsx +++ b/client/src/components/Project/Board/Header/index.jsx @@ -11,6 +11,12 @@ const propTypes = { const ProjectBoardHeader = ({ projectName }) => { const [isLinkCopied, setLinkCopied] = useState(false); + + const handleLinkCopy = () => { + setLinkCopied(true); + setTimeout(() => setLinkCopied(false), 2000); + copyToClipboard(window.location.href); + }; return ( <> @@ -22,14 +28,7 @@ const ProjectBoardHeader = ({ projectName }) => {
Kanban board -
diff --git a/client/src/components/Project/Board/IssueDetails/Description/Styles.js b/client/src/components/Project/Board/IssueDetails/Description/Styles.js new file mode 100644 index 0000000..38c4d9c --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/Description/Styles.js @@ -0,0 +1,30 @@ +import styled from 'styled-components'; + +import { color, font, mixin } from 'shared/utils/styles'; + +export const DescriptionLabel = styled.div` + padding: 20px 0 6px; + ${font.size(15)} + ${font.medium} +`; + +export const EmptyLabel = styled.div` + margin-left: -7px; + padding: 7px; + border-radius: 3px; + color: ${color.textMedium} + transition: background 0.1s; + ${font.size(15)} + ${mixin.clickable} + &:hover { + background: ${color.backgroundLight}; + } +`; + +export const Actions = styled.div` + display: flex; + padding-top: 12px; + & > button { + margin-right: 6px; + } +`; diff --git a/client/src/components/Project/Board/IssueDetails/Description/index.jsx b/client/src/components/Project/Board/IssueDetails/Description/index.jsx new file mode 100644 index 0000000..5388681 --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/Description/index.jsx @@ -0,0 +1,60 @@ +import React, { useRef, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { getTextContentsFromHtmlString } from 'shared/utils/html'; +import { TextEditor, TextEditedContent, Button } from 'shared/components'; +import { DescriptionLabel, EmptyLabel, Actions } from './Styles'; + +const propTypes = { + issue: PropTypes.object.isRequired, + updateIssue: PropTypes.func.isRequired, +}; + +const ProjectBoardIssueDetailsDescription = ({ issue, updateIssue }) => { + const $editorRef = useRef(); + const [isPresenting, setPresenting] = useState(true); + + const isDescriptionEmpty = description => + getTextContentsFromHtmlString(description).trim().length === 0; + + const renderPresentingMode = () => + isDescriptionEmpty(issue.description) ? ( + setPresenting(false)}>Add a description... + ) : ( + setPresenting(false)} /> + ); + + const renderEditingMode = () => ( + <> + ($editorRef.current = editor)} + /> + + + + + + ); + return ( + <> + Description + {isPresenting ? renderPresentingMode() : renderEditingMode()} + + ); +}; + +ProjectBoardIssueDetailsDescription.propTypes = propTypes; + +export default ProjectBoardIssueDetailsDescription; diff --git a/client/src/components/Project/Board/IssueDetails/Loader.jsx b/client/src/components/Project/Board/IssueDetails/Loader.jsx new file mode 100644 index 0000000..f8fed75 --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/Loader.jsx @@ -0,0 +1,31 @@ +import React from 'react'; +import ContentLoader from 'react-content-loader'; + +const IssueDetailsLoader = () => ( +
+ + + + + + + + + + + + + + + + +
+); + +export default IssueDetailsLoader; diff --git a/client/src/components/Project/Board/IssueDetails/RightActions/Styles.js b/client/src/components/Project/Board/IssueDetails/RightActions/Styles.js new file mode 100644 index 0000000..e751f50 --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/RightActions/Styles.js @@ -0,0 +1,146 @@ +import styled, { css } from 'styled-components'; + +import { + color, + issueStatusColors, + issueStatusBackgroundColors, + font, + mixin, +} from 'shared/utils/styles'; +import { Icon } from 'shared/components'; + +export const SectionTitle = styled.div` + margin: 24px 0 5px; + text-transform: uppercase; + color: ${color.textMedium}; + ${font.size(12.5)} + ${font.bold} +`; + +export const User = styled.div` + display: inline-flex; + align-items: center; + ${mixin.clickable} + ${props => + props.isSelectValue && + css` + margin: 0 10px ${props.withBottomMargin ? 5 : 0}px 0; + padding: 4px 8px; + border-radius: 4px; + background: ${color.backgroundLight}; + `} +`; + +export const UserName = styled.div` + padding: 0 3px 0 8px; + ${font.size(14.5)} +`; + +export const StatusOption = styled.div` + padding: 8px 16px; + &.jira-select-option-is-active { + background: ${color.backgroundLightPrimary}; + } +`; + +export const Status = styled.div` + text-transform: uppercase; + ${props => mixin.tag(issueStatusBackgroundColors[props.color], issueStatusColors[props.color])} + ${props => props.isLarge && `padding: 9px 14px 8px;`} +`; + +export const UserOptionCont = styled.div` + padding: 8px 12px 5px; + ${mixin.clickable} + &.jira-select-option-is-active { + background: ${color.backgroundLightPrimary}; + } +`; + +export const Priority = styled.div` + display: flex; + align-items: center; +`; + +export const PriorityOption = styled.div` + padding: 8px 12px; + ${mixin.clickable} + &.jira-select-option-is-active { + background: ${color.backgroundLightPrimary}; + } +`; + +export const PriorityLabel = styled.div` + text-transform: capitalize; + padding: 0 3px 0 8px; + ${font.size(14.5)} +`; + +export const Tracking = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 2px; + ${mixin.clickable}; +`; + +export const TrackingIcon = styled(Icon)` + color: ${color.textMedium}; +`; + +export const TrackingRight = styled.div` + width: 90%; +`; + +export const TrackingBarCont = styled.div` + height: 5px; + border-radius: 4px; + background: ${color.backgroundLight}; +`; + +export const TrackingBar = styled.div` + height: 5px; + border-radius: 4px; + background: ${color.primary}; + transition: all 0.1s; + width: ${props => props.width}%; +`; + +export const TrackingValues = styled.div` + display: flex; + justify-content: space-between; + padding-top: 3px; + ${font.size(14.5)}; +`; + +export const TrackingModalContents = styled.div` + padding: 20px; +`; + +export const TrackingModalTitle = styled.div` + padding-bottom: 14px; + ${font.medium} + ${font.size(20)} +`; + +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.size(13)}; +`; + +export const Actions = styled.div` + display: flex; + justify-content: flex-end; +`; diff --git a/client/src/components/Project/Board/IssueDetails/RightActions/index.jsx b/client/src/components/Project/Board/IssueDetails/RightActions/index.jsx new file mode 100644 index 0000000..00e92dc --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/RightActions/index.jsx @@ -0,0 +1,267 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { invert, isNil } from 'lodash'; + +import { IssueStatus, IssueStatusCopy, IssuePriority } from 'shared/constants/issues'; +import { + Avatar, + Select, + Icon, + InputDebounced, + IssuePriorityIcon, + Modal, + Button, +} from 'shared/components'; +import { + SectionTitle, + User, + UserName, + Status, + StatusOption, + UserOptionCont, + Priority, + PriorityOption, + PriorityLabel, + Tracking, + TrackingIcon, + TrackingRight, + TrackingBarCont, + TrackingBar, + TrackingValues, + TrackingModalContents, + TrackingModalTitle, + Inputs, + InputCont, + InputLabel, + Actions, +} from './Styles'; + +const IssuePriorityCopy = invert(IssuePriority); + +const propTypes = { + issue: PropTypes.object.isRequired, + updateIssue: PropTypes.func.isRequired, + projectUsers: PropTypes.array.isRequired, +}; + +const ProjectBoardIssueDetailsRightActions = ({ issue, updateIssue, projectUsers }) => { + const getUserById = userId => projectUsers.find(user => user.id === parseInt(userId)); + + const userOptions = projectUsers.map(user => ({ value: user.id, label: user.name })); + + const renderHourInput = fieldName => ( + { + const value = stringValue.trim() ? parseInt(stringValue) : null; + updateIssue({ [fieldName]: value }); + }} + /> + ); + + const renderStatus = () => ( + <> + Status + { + updateIssue({ userIds, users: userIds.map(getUserById) }); + }} + renderValue={({ value, removeOptionValue }) => + renderUserValue(getUserById(value), true, removeOptionValue) + } + renderOption={({ value, ...optionProps }) => ( + {renderUserOption(getUserById(value))} + )} + /> + + ); + + const renderReporter = () => ( + <> + Reporter + ({ + value: priority, + label: IssuePriorityCopy[priority], + }))} + onChange={priority => updateIssue({ priority })} + renderValue={({ value }) => renderPriorityItem(value)} + renderOption={({ value, ...optionProps }) => ( + {renderPriorityItem(value)} + )} + /> + + ); + + const calculateTrackingBarWidth = () => { + const { timeSpent, timeRemaining, estimate } = issue; + + if (!timeSpent) { + return 0; + } + if (isNil(timeRemaining) && isNil(estimate)) { + return 100; + } + if (!isNil(timeRemaining)) { + return (timeSpent / (timeSpent + timeRemaining)) * 100; + } + if (!isNil(estimate)) { + return Math.min((timeSpent / estimate) * 100, 100); + } + }; + + const renderRemainingOrEstimate = () => { + const { timeRemaining, estimate } = issue; + + if (isNil(timeRemaining) && isNil(estimate)) { + return null; + } + if (!isNil(timeRemaining)) { + return
{`${timeRemaining}h remaining`}
; + } + if (!isNil(estimate)) { + return
{`${estimate}h estimated`}
; + } + }; + + const renderTrackingPreview = (onClick = () => {}) => ( + + + + + + + +
{issue.timeSpent ? `${issue.timeSpent}h logged` : 'No time logged'}
+ {renderRemainingOrEstimate()} +
+
+
+ ); + + const renderTracking = () => ( + <> + Time Tracking + renderTrackingPreview(modal.open)} + renderContent={modal => ( + + Time tracking + {renderTrackingPreview()} + + + Time spent (hours) + {renderHourInput('timeSpent')} + + + Time remaining (hours) + {renderHourInput('timeRemaining')} + + + + + + + )} + /> + + ); + + return ( + <> + {renderStatus()} + {renderAssignees()} + {renderReporter()} + {renderEstimate()} + {renderPriority()} + {renderTracking()} + + ); +}; + +ProjectBoardIssueDetailsRightActions.propTypes = propTypes; + +export default ProjectBoardIssueDetailsRightActions; diff --git a/client/src/components/Project/Board/IssueDetails/Styles.js b/client/src/components/Project/Board/IssueDetails/Styles.js new file mode 100644 index 0000000..7504306 --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/Styles.js @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +export const Content = styled.div` + display: flex; + padding: 0 30px 30px; +`; + +export const Left = styled.div` + width: 65%; + padding-right: 50px; +`; + +export const Right = styled.div` + width: 35%; + padding-top: 5px; +`; diff --git a/client/src/components/Project/Board/IssueDetails/Title/Styles.js b/client/src/components/Project/Board/IssueDetails/Title/Styles.js new file mode 100644 index 0000000..8dda6b6 --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/Title/Styles.js @@ -0,0 +1,31 @@ +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 { + background: #fff; + border: none; + resize: none; + line-height: 1.28; + border: 1px solid transparent; + box-shadow: 0 0 0 1px transparent; + transition: background 0.1s; + ${font.size(24)} + ${font.medium} + &:hover:not(:focus) { + background: ${color.backgroundLight}; + } + } +`; + +export const ErrorText = styled.div` + padding-top: 4px; + color: ${color.danger}; + ${font.size(13)} + ${font.medium} +`; diff --git a/client/src/components/Project/Board/IssueDetails/Title/index.jsx b/client/src/components/Project/Board/IssueDetails/Title/index.jsx new file mode 100644 index 0000000..715209b --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/Title/index.jsx @@ -0,0 +1,53 @@ +import React, { useRef, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { KeyCodes } from 'shared/constants/keyCodes'; +import { is, generateErrors } from 'shared/utils/validation'; +import { TitleTextarea, ErrorText } from './Styles'; + +const propTypes = { + issue: PropTypes.object.isRequired, + updateIssue: PropTypes.func.isRequired, +}; + +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 ( + <> + { + if (event.keyCode === KeyCodes.ENTER) { + event.target.blur(); + } + }} + /> + {error && {error}} + + ); +}; + +ProjectBoardIssueDetailsTitle.propTypes = propTypes; + +export default ProjectBoardIssueDetailsTitle; diff --git a/client/src/components/Project/Board/IssueDetails/TopActions/Styles.js b/client/src/components/Project/Board/IssueDetails/TopActions/Styles.js new file mode 100644 index 0000000..9ded3f5 --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/TopActions/Styles.js @@ -0,0 +1,72 @@ +import styled from 'styled-components'; + +import { color, font, mixin } from 'shared/utils/styles'; +import { Button } from 'shared/components'; + +export const TopActions = styled.div` + display: flex; + justify-content: space-between; + padding: 21px 18px 0; +`; + +export const TypeButton = styled(Button)` + text-transform: uppercase; + letter-spacing: 0.5px; + color: ${color.textMedium}; + ${font.size(13)} +`; + +export const Right = styled.div` + display: flex; + align-items: center; + & > * { + margin-left: 4px; + } +`; + +export const TypeDropdown = styled.div` + padding-bottom: 6px; +`; + +export const TypeTitle = styled.div` + padding: 10px 0 7px 12px; + text-transform: uppercase; + color: ${color.textMedium}; + ${font.size(12)} +`; + +export const Type = styled.div` + display: flex; + align-items: center; + padding: 7px 12px; + ${mixin.clickable} + &:hover { + background: ${color.backgroundLight}; + } +`; + +export const TypeLabel = styled.div` + padding: 0 5px 0 7px; + text-transform: capitalize; + ${font.size(15)} +`; + +export const FeedbackDropdown = styled.div` + padding: 16px 24px 24px; +`; + +export const FeedbackImageCont = styled.div` + padding: 24px 56px 20px; +`; + +export const FeedbackImage = styled.img` + width: 100%; +`; + +export const FeedbackParagraph = styled.p` + margin-bottom: 12px; + ${font.size(15)} + &:last-of-type { + margin-bottom: 22px; + } +`; diff --git a/client/src/components/Project/Board/IssueDetails/TopActions/assets/feedback.png b/client/src/components/Project/Board/IssueDetails/TopActions/assets/feedback.png new file mode 100644 index 0000000000000000000000000000000000000000..dbaf66eba9a1f674195d0a524b6817471c38bf62 GIT binary patch literal 16825 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 R@KgW*002ovPDHLkV1gD&r2PN@ literal 0 HcmV?d00001 diff --git a/client/src/components/Project/Board/IssueDetails/TopActions/index.jsx b/client/src/components/Project/Board/IssueDetails/TopActions/index.jsx new file mode 100644 index 0000000..f909cd0 --- /dev/null +++ b/client/src/components/Project/Board/IssueDetails/TopActions/index.jsx @@ -0,0 +1,118 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import api from 'shared/utils/api'; +import toast from 'shared/utils/toast'; +import { IssueType } from 'shared/constants/issues'; +import { IssueTypeIcon, Button, CopyLinkButton, Tooltip, ConfirmModal } from 'shared/components'; +import feedbackImage from './assets/feedback.png'; +import { + TopActions, + TypeButton, + Right, + TypeDropdown, + TypeTitle, + Type, + TypeLabel, + FeedbackDropdown, + FeedbackImageCont, + FeedbackImage, + FeedbackParagraph, +} from './Styles'; + +const propTypes = { + issue: PropTypes.object.isRequired, + updateIssue: PropTypes.func.isRequired, + fetchProject: PropTypes.func.isRequired, + modalClose: PropTypes.func.isRequired, +}; + +const ProjectBoardIssueDetailsTopActions = ({ issue, updateIssue, fetchProject, modalClose }) => { + const handleIssueDelete = async () => { + try { + await api.delete(`/issues/${issue.id}`); + await fetchProject(); + modalClose(); + } catch (error) { + toast.error(error); + } + }; + + const renderType = () => ( + ( + }> + {`${issue.type}-${issue.id}`} + + )} + renderContent={() => ( + + Change issue type + {Object.values(IssueType).map(type => ( + updateIssue({ type })}> + + {type} + + ))} + + )} + /> + ); + + const renderFeedback = () => ( + ( + + )} + renderContent={() => ( + + + + + + This simplified Jira clone is built with React on the front-end and Node/TypeScript on + the back-end. + + + Read more on our website or reach out via ivor@codetree.co + + + + + + )} + /> + ); + + const renderDeleteIcon = () => ( + + ); +}; + +export default CopyLinkButton; diff --git a/client/src/shared/components/DatePicker/Styles.js b/client/src/shared/components/DatePicker/Styles.js index 470462e..6894e4e 100644 --- a/client/src/shared/components/DatePicker/Styles.js +++ b/client/src/shared/components/DatePicker/Styles.js @@ -14,7 +14,7 @@ export const Dropdown = styled.div` width: 270px; border-radius: 3px; background: #fff; - ${mixin.boxShadowBorderMedium} + ${mixin.boxShadowDropdown} ${props => (props.withTime ? withTimeStyles : '')} `; diff --git a/client/src/shared/components/Icon/index.jsx b/client/src/shared/components/Icon/index.jsx index 9c5e033..6c9530c 100644 --- a/client/src/shared/components/Icon/index.jsx +++ b/client/src/shared/components/Icon/index.jsx @@ -26,8 +26,8 @@ const codes = { [`issues`]: '\\e908', [`settings`]: '\\e909', [`close`]: '\\e913', - [`help-filled`]: '\\e912', - [`feedback`]: '\\e915', + [`feedback`]: '\\e918', + [`trash`]: '\\e912', }; const propTypes = { diff --git a/client/src/shared/components/InputDebounced.jsx b/client/src/shared/components/InputDebounced.jsx new file mode 100644 index 0000000..a4d5ac0 --- /dev/null +++ b/client/src/shared/components/InputDebounced.jsx @@ -0,0 +1,43 @@ +import React, { useState, useRef, useEffect, useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { debounce } from 'lodash'; + +import { Input } from 'shared/components'; + +const propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, +}; + +const InputDebounced = ({ onChange, value: propsValue, ...props }) => { + const [value, setValue] = useState(propsValue); + + const handleChange = useCallback( + debounce(newValue => onChange(newValue), 500), + [], + ); + + const valueRef = useRef(value); + valueRef.current = value; + + useEffect(() => { + if (propsValue !== valueRef.current) { + setValue(propsValue); + } + }, [propsValue]); + + return ( + { + setValue(newValue); + handleChange(newValue); + }} + /> + ); +}; + +InputDebounced.propTypes = propTypes; + +export default InputDebounced; diff --git a/client/src/shared/components/IssuePriorityIcon/Styles.js b/client/src/shared/components/IssuePriorityIcon/Styles.js new file mode 100644 index 0000000..4f31139 --- /dev/null +++ b/client/src/shared/components/IssuePriorityIcon/Styles.js @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +import { Icon } from 'shared/components'; +import { issuePriorityColors } from 'shared/utils/styles'; + +export const PriorityIcon = styled(Icon)` + font-size: 18px; + color: ${props => issuePriorityColors[props.color]}; +`; diff --git a/client/src/shared/components/IssuePriorityIcon/index.jsx b/client/src/shared/components/IssuePriorityIcon/index.jsx new file mode 100644 index 0000000..ac488ff --- /dev/null +++ b/client/src/shared/components/IssuePriorityIcon/index.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { IssuePriority } from 'shared/constants/issues'; +import { PriorityIcon } from './Styles'; + +const propTypes = { + priority: PropTypes.string.isRequired, +}; + +const IssuePriorityIcon = ({ priority, ...otherProps }) => { + const iconType = [IssuePriority.LOW || IssuePriority.LOWEST].includes(priority) + ? 'arrow-down' + : 'arrow-up'; + + return ; +}; + +IssuePriorityIcon.propTypes = propTypes; + +export default IssuePriorityIcon; diff --git a/client/src/shared/components/IssueTypeIcon/Styles.js b/client/src/shared/components/IssueTypeIcon/Styles.js new file mode 100644 index 0000000..25115c8 --- /dev/null +++ b/client/src/shared/components/IssueTypeIcon/Styles.js @@ -0,0 +1,9 @@ +import styled from 'styled-components'; + +import { Icon } from 'shared/components'; +import { issueTypeColors } from 'shared/utils/styles'; + +export const TypeIcon = styled(Icon)` + font-size: 18px; + color: ${props => issueTypeColors[props.color]}; +`; diff --git a/client/src/shared/components/IssueTypeIcon/index.jsx b/client/src/shared/components/IssueTypeIcon/index.jsx new file mode 100644 index 0000000..6fb4995 --- /dev/null +++ b/client/src/shared/components/IssueTypeIcon/index.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { TypeIcon } from './Styles'; + +const propTypes = { + type: PropTypes.string.isRequired, +}; + +const IssueTypeIcon = ({ type, ...otherProps }) => ( + +); + +IssueTypeIcon.propTypes = propTypes; + +export default IssueTypeIcon; diff --git a/client/src/shared/components/Modal/Styles.js b/client/src/shared/components/Modal/Styles.js index 38337fc..d212dac 100644 --- a/client/src/shared/components/Modal/Styles.js +++ b/client/src/shared/components/Modal/Styles.js @@ -41,14 +41,14 @@ export const StyledModal = styled.div` const modalStyles = { center: css` - max-width: 600px; + max-width: ${props => props.width}px; vertical-align: middle; text-align: left; ${mixin.boxShadowMedium} `, aside: css` min-height: 100vh; - max-width: 500px; + max-width: ${props => props.width}px; text-align: left; box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15); `, @@ -57,7 +57,7 @@ const modalStyles = { export const CloseIcon = styled(Icon)` position: absolute; font-size: 25px; - color: ${color.textDark}; + color: ${color.textMedium}; ${mixin.clickable} ${props => closeIconStyles[props.variant]} `; diff --git a/client/src/shared/components/Modal/index.jsx b/client/src/shared/components/Modal/index.jsx index a3dfc6b..5e652d5 100644 --- a/client/src/shared/components/Modal/index.jsx +++ b/client/src/shared/components/Modal/index.jsx @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect, useCallback } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; -import { uniqueId as uniqueIncreasingIntegerId } from 'lodash'; import useOnOutsideClick from 'shared/hooks/onOutsideClick'; import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown'; @@ -10,6 +9,8 @@ import { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Style const propTypes = { className: PropTypes.string, variant: PropTypes.oneOf(['center', 'aside']), + width: PropTypes.number, + withCloseIcon: PropTypes.bool, isOpen: PropTypes.bool, onClose: PropTypes.func, renderLink: PropTypes.func, @@ -19,6 +20,8 @@ const propTypes = { const defaultProps = { className: undefined, variant: 'center', + width: 600, + withCloseIcon: true, isOpen: undefined, onClose: () => {}, renderLink: () => {}, @@ -27,6 +30,8 @@ const defaultProps = { const Modal = ({ className, variant, + width, + withCloseIcon, isOpen: propsIsOpen, onClose: tellParentToClose, renderLink, @@ -37,12 +42,9 @@ const Modal = ({ const isOpen = isControlled ? propsIsOpen : stateIsOpen; const $modalRef = useRef(); - const modalIdRef = useRef(uniqueIncreasingIntegerId()); + const $clickableOverlayRef = useRef(); const closeModal = useCallback(() => { - if (hasChildModal(modalIdRef.current)) { - return; - } if (!isControlled) { setStateOpen(false); } else { @@ -50,15 +52,15 @@ const Modal = ({ } }, [isControlled, tellParentToClose]); - useOnOutsideClick($modalRef, isOpen, closeModal); + useOnOutsideClick($modalRef, isOpen, closeModal, $clickableOverlayRef); useOnEscapeKeyDown(isOpen, closeModal); useEffect(setBodyScrollLock, [isOpen]); const renderModal = () => ( - - - - + + + + {withCloseIcon && } {renderContent({ close: closeModal })} @@ -75,15 +77,8 @@ const Modal = ({ const $root = document.getElementById('root'); -const getIdsOfAllOpenModals = () => { - const $modalNodes = Array.from(document.querySelectorAll('[data-jira-modal-id]')); - return $modalNodes.map($node => parseInt($node.getAttribute('data-jira-modal-id'))); -}; - -const hasChildModal = modalId => getIdsOfAllOpenModals().some(id => id > modalId); - const setBodyScrollLock = () => { - const areAnyModalsOpen = getIdsOfAllOpenModals().length > 0; + const areAnyModalsOpen = !!document.querySelector('[data-jira-modal]'); document.body.style.overflow = areAnyModalsOpen ? 'hidden' : 'visible'; }; diff --git a/client/src/shared/components/PageLoader/Styles.js b/client/src/shared/components/PageLoader/Styles.js index 277972d..39db28f 100644 --- a/client/src/shared/components/PageLoader/Styles.js +++ b/client/src/shared/components/PageLoader/Styles.js @@ -2,6 +2,6 @@ import styled from 'styled-components'; export default styled.div` width: 100%; - padding-top: 200px; + padding: 200px 0; text-align: center; `; diff --git a/client/src/shared/components/Select/Dropdown.jsx b/client/src/shared/components/Select/Dropdown.jsx index 3941af6..f6617e4 100644 --- a/client/src/shared/components/Select/Dropdown.jsx +++ b/client/src/shared/components/Select/Dropdown.jsx @@ -16,12 +16,14 @@ const propTypes = { onChange: PropTypes.func.isRequired, onCreate: PropTypes.func, isMulti: PropTypes.bool, + propsRenderOption: PropTypes.func, }; const defaultProps = { value: undefined, onCreate: undefined, isMulti: false, + propsRenderOption: undefined, }; const SelectDropdown = ({ @@ -35,6 +37,7 @@ const SelectDropdown = ({ onChange, onCreate, isMulti, + propsRenderOption, }) => { const [isCreatingOption, setCreatingOption] = useState(false); @@ -143,27 +146,33 @@ const SelectDropdown = ({ .includes(searchValue.toLowerCase()), ); - const removeSelectedOptions = opts => opts.filter(option => !value.includes(option.value)); + const removeSelectedOptionsMulti = opts => opts.filter(option => !value.includes(option.value)); + const removeSelectedOptionsSingle = opts => opts.filter(option => value !== option.value); const filteredOptions = isMulti - ? removeSelectedOptions(optionsFilteredBySearchValue) - : optionsFilteredBySearchValue; + ? removeSelectedOptionsMulti(optionsFilteredBySearchValue) + : removeSelectedOptionsSingle(optionsFilteredBySearchValue); const searchValueNotInOptions = !options.map(option => option.label).includes(searchValue); const isOptionCreatable = onCreate && searchValue && searchValueNotInOptions; - const renderSelectableOption = (option, i) => ( - - ); + const renderSelectableOption = (option, i) => { + const optionProps = { + key: option.value, + value: option.value, + label: option.label, + className: i === 0 ? activeOptionClass : undefined, + isSelected: option.value === value, + 'data-select-option-value': option.value, + onMouseEnter: handleOptionMouseEnter, + onClick: () => selectOptionValue(option.value), + }; + return propsRenderOption ? ( + propsRenderOption(optionProps) + ) : ( + + ); + }; const renderCreatableOption = () => (