Implemented first draft of issue modal
@@ -9,7 +9,9 @@ const router = express.Router();
|
|||||||
router.get(
|
router.get(
|
||||||
'/issues/:issueId',
|
'/issues/:issueId',
|
||||||
catchErrors(async (req, res) => {
|
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 });
|
res.respond({ issue });
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class Comment extends BaseEntity {
|
|||||||
@ManyToOne(
|
@ManyToOne(
|
||||||
() => Issue,
|
() => Issue,
|
||||||
issue => issue.comments,
|
issue => issue.comments,
|
||||||
|
{ onDelete: 'CASCADE' },
|
||||||
)
|
)
|
||||||
issue: Issue;
|
issue: Issue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Issue extends BaseEntity {
|
|||||||
type: [is.required(), is.oneOf(Object.values(IssueType))],
|
type: [is.required(), is.oneOf(Object.values(IssueType))],
|
||||||
status: [is.required(), is.oneOf(Object.values(IssueStatus))],
|
status: [is.required(), is.oneOf(Object.values(IssueStatus))],
|
||||||
priority: [is.required(), is.oneOf(Object.values(IssuePriority))],
|
priority: [is.required(), is.oneOf(Object.values(IssuePriority))],
|
||||||
description: is.maxLength(100000),
|
reporterId: is.required(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
|||||||
228
client/package-lock.json
generated
@@ -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": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
"integrity": "sha1-/qJhbcZ2spYmhrOvjb2+GAskTgU="
|
"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": {
|
"css-select": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz",
|
||||||
@@ -2627,6 +2660,12 @@
|
|||||||
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
|
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
|
||||||
"dev": true
|
"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": {
|
"csstype": {
|
||||||
"version": "2.6.7",
|
"version": "2.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz",
|
||||||
@@ -2679,7 +2718,6 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
|
||||||
"integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
|
"integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-arguments": "^1.0.4",
|
"is-arguments": "^1.0.4",
|
||||||
"is-date-object": "^1.0.1",
|
"is-date-object": "^1.0.1",
|
||||||
@@ -2709,7 +2747,6 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||||
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
|
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"object-keys": "^1.0.12"
|
"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": {
|
"extend-shallow": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
|
||||||
@@ -3824,6 +3866,11 @@
|
|||||||
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=",
|
||||||
"dev": true
|
"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": {
|
"fast-glob": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.1.1.tgz",
|
||||||
@@ -4784,8 +4831,7 @@
|
|||||||
"function-bind": {
|
"function-bind": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
|
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"functional-red-black-tree": {
|
"functional-red-black-tree": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -4943,7 +4989,6 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"function-bind": "^1.1.1"
|
"function-bind": "^1.1.1"
|
||||||
}
|
}
|
||||||
@@ -5266,6 +5311,15 @@
|
|||||||
"safer-buffer": ">= 2.1.2 < 3"
|
"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": {
|
"ieee754": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||||
@@ -5324,6 +5378,12 @@
|
|||||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
"dev": true
|
"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": {
|
"infer-owner": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
|
||||||
@@ -5496,8 +5556,7 @@
|
|||||||
"is-arguments": {
|
"is-arguments": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
|
||||||
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==",
|
"integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"is-arrayish": {
|
"is-arrayish": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
@@ -5549,8 +5608,7 @@
|
|||||||
"is-date-object": {
|
"is-date-object": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
|
||||||
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
|
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"is-descriptor": {
|
"is-descriptor": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.6",
|
||||||
@@ -5682,7 +5740,6 @@
|
|||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
|
||||||
"integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
|
"integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"has": "^1.0.1"
|
"has": "^1.0.1"
|
||||||
}
|
}
|
||||||
@@ -6910,14 +6967,12 @@
|
|||||||
"object-is": {
|
"object-is": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz",
|
||||||
"integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=",
|
"integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"object-keys": {
|
"object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
|
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"object-visit": {
|
"object-visit": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -7206,6 +7261,11 @@
|
|||||||
"no-case": "^2.2.0"
|
"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": {
|
"parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -7407,6 +7467,94 @@
|
|||||||
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
|
"integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=",
|
||||||
"dev": true
|
"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": {
|
"postcss-value-parser": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
|
"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==",
|
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
|
||||||
"dev": true
|
"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": {
|
"raf-schd": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz",
|
||||||
@@ -7651,6 +7829,11 @@
|
|||||||
"use-memo-one": "^1.1.1"
|
"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": {
|
"react-dom": {
|
||||||
"version": "16.12.0",
|
"version": "16.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
|
||||||
@@ -7912,7 +8095,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.2.0.tgz",
|
||||||
"integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==",
|
"integrity": "sha512-ztaw4M1VqgMwl9HlPpOuiYgItcHlunW0He2fE6eNfT6E/CF2FtYi9ofOYe4mKntstYk0Fyh/rDRBdS3AnxjlrA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"define-properties": "^1.1.2"
|
"define-properties": "^1.1.2"
|
||||||
}
|
}
|
||||||
@@ -8990,6 +9172,16 @@
|
|||||||
"integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
|
"integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
|
||||||
"dev": true
|
"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": {
|
"styled-components": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz",
|
||||||
@@ -9365,6 +9557,12 @@
|
|||||||
"set-value": "^2.0.1"
|
"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": {
|
"unique-filename": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.7.4",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
|
"css-loader": "^3.3.2",
|
||||||
"eslint": "^6.1.0",
|
"eslint": "^6.1.0",
|
||||||
"eslint-config-airbnb": "^18.0.1",
|
"eslint-config-airbnb": "^18.0.1",
|
||||||
"eslint-config-prettier": "^6.7.0",
|
"eslint-config-prettier": "^6.7.0",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"lint-staged": "^9.5.0",
|
"lint-staged": "^9.5.0",
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
|
"style-loader": "^1.0.1",
|
||||||
"url-loader": "^3.0.0",
|
"url-loader": "^3.0.0",
|
||||||
"webpack": "^4.41.2",
|
"webpack": "^4.41.2",
|
||||||
"webpack-cli": "^3.3.10",
|
"webpack-cli": "^3.3.10",
|
||||||
@@ -44,12 +46,15 @@
|
|||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^6.9.0",
|
"query-string": "^6.9.0",
|
||||||
|
"quill": "^1.3.7",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
"react-beautiful-dnd": "^12.2.0",
|
"react-beautiful-dnd": "^12.2.0",
|
||||||
|
"react-content-loader": "^4.3.3",
|
||||||
"react-dom": "^16.12.0",
|
"react-dom": "^16.12.0",
|
||||||
"react-router-dom": "^5.1.2",
|
"react-router-dom": "^5.1.2",
|
||||||
"react-textarea-autosize": "^7.1.2",
|
"react-textarea-autosize": "^7.1.2",
|
||||||
"react-transition-group": "^4.3.0",
|
"react-transition-group": "^4.3.0",
|
||||||
|
"regenerator-runtime": "^0.13.3",
|
||||||
"styled-components": "^4.4.1",
|
"styled-components": "^4.4.1",
|
||||||
"sweet-pubsub": "^1.1.2"
|
"sweet-pubsub": "^1.1.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import NormalizeStyles from './NormalizeStyles';
|
import NormalizeStyles from './NormalizeStyles';
|
||||||
import FontStyles from './FontStyles';
|
|
||||||
import BaseStyles from './BaseStyles';
|
import BaseStyles from './BaseStyles';
|
||||||
import Toast from './Toast';
|
import Toast from './Toast';
|
||||||
import Routes from './Routes';
|
import Routes from './Routes';
|
||||||
|
|
||||||
|
// We're importing css because @font-face in styled-components causes font files
|
||||||
|
// 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 = () => (
|
const App = () => (
|
||||||
<>
|
<>
|
||||||
<NormalizeStyles />
|
<NormalizeStyles />
|
||||||
<FontStyles />
|
|
||||||
<BaseStyles />
|
<BaseStyles />
|
||||||
<Toast />
|
<Toast />
|
||||||
<Routes />
|
<Routes />
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useHistory } from 'react-router-dom';
|
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 { getStoredAuthToken, storeAuthToken } from 'shared/utils/authToken';
|
||||||
import { PageLoader } from 'shared/components';
|
import { PageLoader } from 'shared/components';
|
||||||
|
|
||||||
const Authenticate = () => {
|
const Authenticate = () => {
|
||||||
const [{ data }, createGuestAccount] = useApi.post('/authentication/guest');
|
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!getStoredAuthToken()) {
|
const createGuestAccount = async () => {
|
||||||
createGuestAccount();
|
if (!getStoredAuthToken()) {
|
||||||
}
|
try {
|
||||||
}, [createGuestAccount]);
|
const { authToken } = await api.post('/authentication/guest');
|
||||||
|
storeAuthToken(authToken);
|
||||||
useEffect(() => {
|
history.push('/');
|
||||||
if (data && data.authToken) {
|
} catch (error) {
|
||||||
storeAuthToken(data.authToken);
|
toast.error(error);
|
||||||
history.push('/');
|
}
|
||||||
}
|
}
|
||||||
}, [data, history]);
|
};
|
||||||
|
createGuestAccount();
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
return <PageLoader />;
|
return <PageLoader />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export default createGlobalStyle`
|
|||||||
}
|
}
|
||||||
|
|
||||||
a, a:hover, a:visited, a:active {
|
a, a:hover, a:visited, a:active {
|
||||||
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +93,7 @@ export default createGlobalStyle`
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
line-height: 1.6;
|
line-height: 1.4285;
|
||||||
}
|
}
|
||||||
|
|
||||||
body, select {
|
body, select {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 341 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 432 KiB |
@@ -25,10 +25,10 @@
|
|||||||
<glyph unicode="" glyph-name="bug" d="M554 384.667v256h-84v-256h84zM554 212.667v86h-84v-86h84zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125z" />
|
<glyph unicode="" glyph-name="bug" d="M554 384.667v256h-84v-256h84zM554 212.667v86h-84v-86h84zM512 852.667q176 0 301-125t125-301-125-301-301-125-301 125-125 301 125 301 301 125z" />
|
||||||
<glyph unicode="" glyph-name="task" d="M426 212.667l384 384-60 62-324-324-152 152-60-60zM810 810.667q36 0 61-25t25-61v-596q0-36-25-61t-61-25h-596q-36 0-61 25t-25 61v596q0 36 25 61t61 25h596z" />
|
<glyph unicode="" glyph-name="task" d="M426 212.667l384 384-60 62-324-324-152 152-60-60zM810 810.667q36 0 61-25t25-61v-596q0-36-25-61t-61-25h-596q-36 0-61 25t-25 61v596q0 36 25 61t61 25h596z" />
|
||||||
<glyph unicode="" glyph-name="story" d="M726 810.667q34 0 59-26t25-60v-682l-298 128-298-128v682q0 34 25 60t59 26h428z" />
|
<glyph unicode="" glyph-name="story" d="M726 810.667q34 0 59-26t25-60v-682l-298 128-298-128v682q0 34 25 60t59 26h428z" />
|
||||||
<glyph unicode="" glyph-name="help-filled" d="M512 952.32c-271.462 0-491.52-220.058-491.52-491.52s220.058-491.52 491.52-491.52c271.411 0 491.52 220.058 491.52 491.571 0 271.411-220.109 491.469-491.52 491.469zM504.269 174.131h-2.611c-40.038 1.178-68.301 30.72-67.123 70.195 1.126 38.81 30.054 67.021 68.762 67.021l2.355-0.102c41.165-1.178 69.12-30.413 67.942-71.014-1.178-38.912-29.594-66.099-69.325-66.099zM672.768 508.518c-9.421-13.312-30.106-30.003-56.218-50.33l-28.774-19.814c-15.77-12.288-25.293-23.91-28.826-35.226-2.867-8.909-4.198-11.315-4.454-29.491v-4.608h-109.824l0.307 9.318c1.382 38.093 2.304 60.621 18.125 79.206 24.832 29.082 79.616 64.41 81.92 65.894 7.885 5.888 14.49 12.595 19.405 19.814 11.52 15.923 16.589 28.416 16.589 40.602 0 17.101-5.018 32.922-15.002 46.899-9.626 13.619-27.904 20.378-54.323 20.378-26.214 0-44.186-8.294-54.886-25.395-11.059-17.459-16.64-35.84-16.64-54.63v-4.71h-113.203l0.205 4.915c2.918 69.274 27.699 119.194 73.472 148.326 28.826 18.483 64.717 27.853 106.547 27.853 54.682 0 100.966-13.312 137.318-39.526 36.915-26.573 55.603-66.406 55.603-118.323-0.051-29.030-9.216-56.32-27.341-81.152z" />
|
<glyph unicode="" glyph-name="trash" d="M768 640v-554.667c0-11.776-4.736-22.4-12.501-30.165s-18.389-12.501-30.165-12.501h-426.667c-11.776 0-22.4 4.736-30.165 12.501s-12.501 18.389-12.501 30.165v554.667zM725.333 725.334v42.667c0 35.328-14.379 67.413-37.504 90.496s-55.168 37.504-90.496 37.504h-170.667c-35.328 0-67.413-14.379-90.496-37.504s-37.504-55.168-37.504-90.496v-42.667h-170.667c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667h42.667v-554.667c0-35.328 14.379-67.413 37.504-90.496s55.168-37.504 90.496-37.504h426.667c35.328 0 67.413 14.379 90.496 37.504s37.504 55.168 37.504 90.496v554.667h42.667c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667zM384 725.334v42.667c0 11.776 4.736 22.4 12.501 30.165s18.389 12.501 30.165 12.501h170.667c11.776 0 22.4-4.736 30.165-12.501s12.501-18.389 12.501-30.165v-42.667zM384 469.334v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667zM554.667 469.334v-256c0-23.552 19.115-42.667 42.667-42.667s42.667 19.115 42.667 42.667v256c0 23.552-19.115 42.667-42.667 42.667s-42.667-19.115-42.667-42.667z" />
|
||||||
<glyph unicode="" glyph-name="close" d="M225.835 652.502l225.835-225.835-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l225.835 225.835 225.835-225.835c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-225.835 225.835 225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-225.835-225.835-225.835 225.835c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331z" />
|
<glyph unicode="" glyph-name="close" d="M225.835 652.502l225.835-225.835-225.835-225.835c-16.683-16.683-16.683-43.691 0-60.331s43.691-16.683 60.331 0l225.835 225.835 225.835-225.835c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-225.835 225.835 225.835 225.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-225.835-225.835-225.835 225.835c-16.683 16.683-43.691 16.683-60.331 0s-16.683-43.691 0-60.331z" />
|
||||||
<glyph unicode="" glyph-name="stopwatch" d="M512 84.667q124 0 211 88t87 212-87 211-211 87-211-87-87-211 87-212 211-88zM812 622.667q34-44 59-113t25-125q0-158-112-271t-272-113-272 113-112 271 112 271 272 113q54 0 125-26t115-60l60 62q32-26 60-60zM470 340.667v256h84v-256h-84zM640 896.667v-86h-256v86h256z" />
|
<glyph unicode="" glyph-name="stopwatch" d="M512 84.667q124 0 211 88t87 212-87 211-211 87-211-87-87-211 87-212 211-88zM812 622.667q34-44 59-113t25-125q0-158-112-271t-272-113-272 113-112 271 112 271 272 113q54 0 125-26t115-60l60 62q32-26 60-60zM470 340.667v256h84v-256h-84zM640 896.667v-86h-256v86h256z" />
|
||||||
<glyph unicode="" glyph-name="feedback" d="M881.818 612.864c-81.101 188.723-211.558 332.288-277.555 305.51-112.077-45.619 66.765-264.397-483.686-488.090-47.565-19.405-59.597-96.666-39.68-142.95 19.866-46.182 84.89-92.211 132.454-72.909 8.243 3.379 38.451 13.107 38.451 13.107 33.946-45.619 69.478-18.586 82.125-47.514 15.155-34.816 48.077-110.49 59.29-136.192s36.608-49.51 55.040-42.496c18.381 7.014 80.998 30.822 104.96 39.885 23.962 9.114 29.645 30.515 22.323 47.309-7.885 18.176-40.243 23.501-49.51 44.698-9.216 21.094-39.373 88.986-48.026 110.387-11.776 29.082 13.261 52.787 49.664 56.525 250.573 26.214 297.421-128.614 382.72-93.901 65.894 26.88 52.48 218.061-28.57 406.63zM853.606 306.893c-14.694-5.888-113.306 71.782-176.282 218.47-63.027 146.586-55.091 280.576-40.448 286.566 14.643 5.888 110.848-87.91 173.824-234.496 63.027-146.586 57.549-264.55 42.906-270.541z" />
|
|
||||||
<glyph unicode="" glyph-name="menu" d="M128 384h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM128 640h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM128 128h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
<glyph unicode="" glyph-name="menu" d="M128 384h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM128 640h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667zM128 128h768c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667h-768c-23.552 0-42.667-19.115-42.667-42.667s19.115-42.667 42.667-42.667z" />
|
||||||
<glyph unicode="" glyph-name="arrow-left-circle" d="M981.333 426.667c0 129.579-52.565 246.997-137.472 331.861s-202.283 137.472-331.861 137.472-246.997-52.565-331.861-137.472-137.472-202.283-137.472-331.861 52.565-246.997 137.472-331.861 202.283-137.472 331.861-137.472 246.997 52.565 331.861 137.472 137.472 202.283 137.472 331.861zM896 426.667c0-106.069-42.923-201.984-112.469-271.531s-165.461-112.469-271.531-112.469-201.984 42.923-271.531 112.469-112.469 165.461-112.469 271.531 42.923 201.984 112.469 271.531 165.461 112.469 271.531 112.469 201.984-42.923 271.531-112.469 112.469-165.461 112.469-271.531zM682.667 469.334h-238.336l97.835 97.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-170.667-170.667c-4.096-4.096-7.168-8.789-9.259-13.824s-3.243-10.539-3.243-16.341c0-5.547 1.067-11.136 3.243-16.341 2.091-5.035 5.163-9.728 9.259-13.824l170.667-170.667c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-97.835 97.835h238.336c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667z" />
|
<glyph unicode="" glyph-name="arrow-left-circle" d="M981.333 426.667c0 129.579-52.565 246.997-137.472 331.861s-202.283 137.472-331.861 137.472-246.997-52.565-331.861-137.472-137.472-202.283-137.472-331.861 52.565-246.997 137.472-331.861 202.283-137.472 331.861-137.472 246.997 52.565 331.861 137.472 137.472 202.283 137.472 331.861zM896 426.667c0-106.069-42.923-201.984-112.469-271.531s-165.461-112.469-271.531-112.469-201.984 42.923-271.531 112.469-112.469 165.461-112.469 271.531 42.923 201.984 112.469 271.531 165.461 112.469 271.531 112.469 201.984-42.923 271.531-112.469 112.469-165.461 112.469-271.531zM682.667 469.334h-238.336l97.835 97.835c16.683 16.683 16.683 43.691 0 60.331s-43.691 16.683-60.331 0l-170.667-170.667c-4.096-4.096-7.168-8.789-9.259-13.824s-3.243-10.539-3.243-16.341c0-5.547 1.067-11.136 3.243-16.341 2.091-5.035 5.163-9.728 9.259-13.824l170.667-170.667c16.683-16.683 43.691-16.683 60.331 0s16.683 43.691 0 60.331l-97.835 97.835h238.336c23.552 0 42.667 19.115 42.667 42.667s-19.115 42.667-42.667 42.667z" />
|
||||||
|
<glyph unicode="" glyph-name="feedback" d="M979.755 841.856c1.835 6.443 2.133 13.397 0.64 20.309-1.024 4.821-2.901 9.557-5.589 13.867-2.731 4.352-6.187 8.107-10.155 11.179-4.992 3.84-10.624 6.4-16.469 7.723s-12.032 1.451-18.176 0.171c-1.792-0.384-3.627-0.896-5.376-1.493l-0.896-0.299-852.48-298.368c-10.752-3.755-19.925-11.776-24.917-22.955-9.557-21.547 0.128-46.763 21.675-56.32l369.024-164.011 164.011-369.024c4.608-10.368 13.355-18.901 24.875-22.955 22.229-7.765 46.592 3.925 54.357 26.197l298.368 852.437c0.427 1.152 0.811 2.304 1.152 3.499zM459.904 434.902l-258.901 115.029 575.275 201.387zM836.651 690.944l-201.387-575.275-115.029 258.901z" />
|
||||||
</font></defs></svg>
|
</font></defs></svg>
|
||||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 20 KiB |
35
client/src/components/App/fontStyles.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import styled from 'styled-components';
|
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';
|
import { color, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const Filters = styled.div`
|
export const Filters = styled.div`
|
||||||
@@ -9,7 +9,7 @@ export const Filters = styled.div`
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SearchInput = styled(Input)`
|
export const SearchInput = styled(InputDebounced)`
|
||||||
margin-right: 18px;
|
margin-right: 18px;
|
||||||
width: 160px;
|
width: 160px;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { xor, debounce } from 'lodash';
|
import { xor } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Filters,
|
Filters,
|
||||||
@@ -30,7 +30,8 @@ const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters
|
|||||||
<Filters>
|
<Filters>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
icon="search"
|
icon="search"
|
||||||
onChange={debounce(value => setFiltersMerge({ searchQuery: value }), 500)}
|
value={searchQuery}
|
||||||
|
onChange={value => setFiltersMerge({ searchQuery: value })}
|
||||||
/>
|
/>
|
||||||
<Avatars>
|
<Avatars>
|
||||||
{projectUsers.map(user => (
|
{projectUsers.map(user => (
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ const propTypes = {
|
|||||||
|
|
||||||
const ProjectBoardHeader = ({ projectName }) => {
|
const ProjectBoardHeader = ({ projectName }) => {
|
||||||
const [isLinkCopied, setLinkCopied] = useState(false);
|
const [isLinkCopied, setLinkCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleLinkCopy = () => {
|
||||||
|
setLinkCopied(true);
|
||||||
|
setTimeout(() => setLinkCopied(false), 2000);
|
||||||
|
copyToClipboard(window.location.href);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
@@ -22,14 +28,7 @@ const ProjectBoardHeader = ({ projectName }) => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<Header>
|
<Header>
|
||||||
<BoardName>Kanban board</BoardName>
|
<BoardName>Kanban board</BoardName>
|
||||||
<Button
|
<Button icon="link" onClick={handleLinkCopy}>
|
||||||
icon="link"
|
|
||||||
onClick={() => {
|
|
||||||
setLinkCopied(true);
|
|
||||||
setTimeout(() => setLinkCopied(false), 2000);
|
|
||||||
copyToClipboard(window.location.href);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isLinkCopied ? 'Link Copied' : 'Copy link'}
|
{isLinkCopied ? 'Link Copied' : 'Copy link'}
|
||||||
</Button>
|
</Button>
|
||||||
</Header>
|
</Header>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -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) ? (
|
||||||
|
<EmptyLabel onClick={() => setPresenting(false)}>Add a description...</EmptyLabel>
|
||||||
|
) : (
|
||||||
|
<TextEditedContent content={issue.description} onClick={() => setPresenting(false)} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderEditingMode = () => (
|
||||||
|
<>
|
||||||
|
<TextEditor
|
||||||
|
placeholder="Describe the issue"
|
||||||
|
defaultValue={issue.description}
|
||||||
|
getEditor={editor => ($editorRef.current = editor)}
|
||||||
|
/>
|
||||||
|
<Actions>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
onClick={() => {
|
||||||
|
setPresenting(true);
|
||||||
|
updateIssue({ description: $editorRef.current.getHTML() });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button color="empty" onClick={() => setPresenting(true)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DescriptionLabel>Description</DescriptionLabel>
|
||||||
|
{isPresenting ? renderPresentingMode() : renderEditingMode()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardIssueDetailsDescription.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardIssueDetailsDescription;
|
||||||
31
client/src/components/Project/Board/IssueDetails/Loader.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
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;
|
||||||
@@ -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;
|
||||||
|
`;
|
||||||
@@ -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 => (
|
||||||
|
<InputDebounced
|
||||||
|
placeholder="Number"
|
||||||
|
filter={/^\d{0,6}$/}
|
||||||
|
value={isNil(issue[fieldName]) ? '' : issue[fieldName]}
|
||||||
|
onChange={stringValue => {
|
||||||
|
const value = stringValue.trim() ? parseInt(stringValue) : null;
|
||||||
|
updateIssue({ [fieldName]: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStatus = () => (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Status</SectionTitle>
|
||||||
|
<Select
|
||||||
|
value={issue.status}
|
||||||
|
options={Object.values(IssueStatus).map(status => ({
|
||||||
|
value: status,
|
||||||
|
label: IssueStatusCopy[status],
|
||||||
|
}))}
|
||||||
|
onChange={status => updateIssue({ status })}
|
||||||
|
renderValue={({ value: status }) => (
|
||||||
|
<Status isLarge color={status}>
|
||||||
|
{IssueStatusCopy[status]}
|
||||||
|
</Status>
|
||||||
|
)}
|
||||||
|
renderOption={({ value: status, ...optionProps }) => (
|
||||||
|
<StatusOption {...optionProps}>
|
||||||
|
<Status color={status}>{IssueStatusCopy[status]}</Status>
|
||||||
|
</StatusOption>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderUserValue = (user, withBottomMargin, removeOptionValue) => (
|
||||||
|
<User
|
||||||
|
key={user.id}
|
||||||
|
isSelectValue
|
||||||
|
withBottomMargin={withBottomMargin}
|
||||||
|
onClick={() => removeOptionValue && removeOptionValue(user.id)}
|
||||||
|
>
|
||||||
|
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={24} />
|
||||||
|
<UserName>{user.name}</UserName>
|
||||||
|
{removeOptionValue && <Icon type="close" top={1} />}
|
||||||
|
</User>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderUserOption = user => (
|
||||||
|
<User key={user.id}>
|
||||||
|
<Avatar avatarUrl={user.avatarUrl} name={user.name} size={32} />
|
||||||
|
<UserName>{user.name}</UserName>
|
||||||
|
</User>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderAssignees = () => (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Assignees</SectionTitle>
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
placeholder="Unassigned"
|
||||||
|
value={issue.userIds}
|
||||||
|
options={userOptions}
|
||||||
|
onChange={userIds => {
|
||||||
|
updateIssue({ userIds, users: userIds.map(getUserById) });
|
||||||
|
}}
|
||||||
|
renderValue={({ value, removeOptionValue }) =>
|
||||||
|
renderUserValue(getUserById(value), true, removeOptionValue)
|
||||||
|
}
|
||||||
|
renderOption={({ value, ...optionProps }) => (
|
||||||
|
<UserOptionCont {...optionProps}>{renderUserOption(getUserById(value))}</UserOptionCont>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderReporter = () => (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Reporter</SectionTitle>
|
||||||
|
<Select
|
||||||
|
value={issue.reporterId}
|
||||||
|
options={userOptions}
|
||||||
|
onChange={userId => updateIssue({ reporterId: userId })}
|
||||||
|
renderValue={({ value }) => renderUserValue(getUserById(value), false)}
|
||||||
|
renderOption={({ value, ...optionProps }) => (
|
||||||
|
<UserOptionCont {...optionProps}>{renderUserOption(getUserById(value))}</UserOptionCont>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderEstimate = () => (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Original Estimate (hours)</SectionTitle>
|
||||||
|
{renderHourInput('estimate')}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPriorityItem = priority => (
|
||||||
|
<Priority color={priority}>
|
||||||
|
<IssuePriorityIcon priority={priority} />
|
||||||
|
<PriorityLabel>{IssuePriorityCopy[priority].toLowerCase()}</PriorityLabel>
|
||||||
|
</Priority>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPriority = () => (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Priority</SectionTitle>
|
||||||
|
<Select
|
||||||
|
value={issue.priority}
|
||||||
|
options={Object.values(IssuePriority).map(priority => ({
|
||||||
|
value: priority,
|
||||||
|
label: IssuePriorityCopy[priority],
|
||||||
|
}))}
|
||||||
|
onChange={priority => updateIssue({ priority })}
|
||||||
|
renderValue={({ value }) => renderPriorityItem(value)}
|
||||||
|
renderOption={({ value, ...optionProps }) => (
|
||||||
|
<PriorityOption {...optionProps}>{renderPriorityItem(value)}</PriorityOption>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const calculateTrackingBarWidth = () => {
|
||||||
|
const { timeSpent, timeRemaining, estimate } = issue;
|
||||||
|
|
||||||
|
if (!timeSpent) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (isNil(timeRemaining) && isNil(estimate)) {
|
||||||
|
return 100;
|
||||||
|
}
|
||||||
|
if (!isNil(timeRemaining)) {
|
||||||
|
return (timeSpent / (timeSpent + timeRemaining)) * 100;
|
||||||
|
}
|
||||||
|
if (!isNil(estimate)) {
|
||||||
|
return Math.min((timeSpent / estimate) * 100, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRemainingOrEstimate = () => {
|
||||||
|
const { timeRemaining, estimate } = issue;
|
||||||
|
|
||||||
|
if (isNil(timeRemaining) && isNil(estimate)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!isNil(timeRemaining)) {
|
||||||
|
return <div>{`${timeRemaining}h remaining`}</div>;
|
||||||
|
}
|
||||||
|
if (!isNil(estimate)) {
|
||||||
|
return <div>{`${estimate}h estimated`}</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTrackingPreview = (onClick = () => {}) => (
|
||||||
|
<Tracking onClick={onClick}>
|
||||||
|
<TrackingIcon type="stopwatch" size={26} top={-1} />
|
||||||
|
<TrackingRight>
|
||||||
|
<TrackingBarCont>
|
||||||
|
<TrackingBar width={calculateTrackingBarWidth()} />
|
||||||
|
</TrackingBarCont>
|
||||||
|
<TrackingValues>
|
||||||
|
<div>{issue.timeSpent ? `${issue.timeSpent}h logged` : 'No time logged'}</div>
|
||||||
|
{renderRemainingOrEstimate()}
|
||||||
|
</TrackingValues>
|
||||||
|
</TrackingRight>
|
||||||
|
</Tracking>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTracking = () => (
|
||||||
|
<>
|
||||||
|
<SectionTitle>Time Tracking</SectionTitle>
|
||||||
|
<Modal
|
||||||
|
width={400}
|
||||||
|
renderLink={modal => renderTrackingPreview(modal.open)}
|
||||||
|
renderContent={modal => (
|
||||||
|
<TrackingModalContents>
|
||||||
|
<TrackingModalTitle>Time tracking</TrackingModalTitle>
|
||||||
|
{renderTrackingPreview()}
|
||||||
|
<Inputs>
|
||||||
|
<InputCont>
|
||||||
|
<InputLabel>Time spent (hours)</InputLabel>
|
||||||
|
{renderHourInput('timeSpent')}
|
||||||
|
</InputCont>
|
||||||
|
<InputCont>
|
||||||
|
<InputLabel>Time remaining (hours)</InputLabel>
|
||||||
|
{renderHourInput('timeRemaining')}
|
||||||
|
</InputCont>
|
||||||
|
</Inputs>
|
||||||
|
<Actions>
|
||||||
|
<Button color="primary" onClick={modal.close}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</Actions>
|
||||||
|
</TrackingModalContents>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderStatus()}
|
||||||
|
{renderAssignees()}
|
||||||
|
{renderReporter()}
|
||||||
|
{renderEstimate()}
|
||||||
|
{renderPriority()}
|
||||||
|
{renderTracking()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardIssueDetailsRightActions.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardIssueDetailsRightActions;
|
||||||
16
client/src/components/Project/Board/IssueDetails/Styles.js
Normal file
@@ -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;
|
||||||
|
`;
|
||||||
@@ -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}
|
||||||
|
`;
|
||||||
@@ -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 (
|
||||||
|
<>
|
||||||
|
<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>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardIssueDetailsTitle.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardIssueDetailsTitle;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
After Width: | Height: | Size: 16 KiB |
@@ -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 = () => (
|
||||||
|
<Tooltip
|
||||||
|
width={150}
|
||||||
|
offset={{ top: -15 }}
|
||||||
|
renderLink={linkProps => (
|
||||||
|
<TypeButton {...linkProps} color="empty" icon={<IssueTypeIcon type={issue.type} />}>
|
||||||
|
{`${issue.type}-${issue.id}`}
|
||||||
|
</TypeButton>
|
||||||
|
)}
|
||||||
|
renderContent={() => (
|
||||||
|
<TypeDropdown>
|
||||||
|
<TypeTitle>Change issue type</TypeTitle>
|
||||||
|
{Object.values(IssueType).map(type => (
|
||||||
|
<Type key={type} onClick={() => updateIssue({ type })}>
|
||||||
|
<IssueTypeIcon type={type} top={1} />
|
||||||
|
<TypeLabel>{type}</TypeLabel>
|
||||||
|
</Type>
|
||||||
|
))}
|
||||||
|
</TypeDropdown>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFeedback = () => (
|
||||||
|
<Tooltip
|
||||||
|
width={300}
|
||||||
|
offset={{ top: -15 }}
|
||||||
|
renderLink={linkProps => (
|
||||||
|
<Button icon="feedback" color="empty" {...linkProps}>
|
||||||
|
Give feedback
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
renderContent={() => (
|
||||||
|
<FeedbackDropdown>
|
||||||
|
<FeedbackImageCont>
|
||||||
|
<FeedbackImage src={feedbackImage} alt="Give feedback" />
|
||||||
|
</FeedbackImageCont>
|
||||||
|
<FeedbackParagraph>
|
||||||
|
This simplified Jira clone is built with React on the front-end and Node/TypeScript on
|
||||||
|
the back-end.
|
||||||
|
</FeedbackParagraph>
|
||||||
|
<FeedbackParagraph>
|
||||||
|
Read more on our website or reach out via <strong>ivor@codetree.co</strong>
|
||||||
|
</FeedbackParagraph>
|
||||||
|
<a href="https://codetree.co/" target="_blank" rel="noreferrer noopener">
|
||||||
|
<Button color="primary">Visit Website</Button>
|
||||||
|
</a>
|
||||||
|
</FeedbackDropdown>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderDeleteIcon = () => (
|
||||||
|
<ConfirmModal
|
||||||
|
title="Are you sure you want to delete this issue?"
|
||||||
|
message="This action is permanent and can not be reversed."
|
||||||
|
confirmText="Delete issue"
|
||||||
|
onConfirm={handleIssueDelete}
|
||||||
|
renderLink={modal => <Button icon="trash" iconSize={19} color="empty" onClick={modal.open} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopActions>
|
||||||
|
{renderType()}
|
||||||
|
<Right>
|
||||||
|
{renderFeedback()}
|
||||||
|
<CopyLinkButton color="empty" />
|
||||||
|
{renderDeleteIcon()}
|
||||||
|
<Button icon="close" iconSize={24} color="empty" onClick={modalClose} />
|
||||||
|
</Right>
|
||||||
|
</TopActions>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardIssueDetailsTopActions.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardIssueDetailsTopActions;
|
||||||
74
client/src/components/Project/Board/IssueDetails/index.jsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import api from 'shared/utils/api';
|
||||||
|
import useApi from 'shared/hooks/api';
|
||||||
|
import { PageError } from 'shared/components';
|
||||||
|
import Loader from './Loader';
|
||||||
|
import TopActions from './TopActions';
|
||||||
|
import Title from './Title';
|
||||||
|
import Description from './Description';
|
||||||
|
import RightActions from './RightActions';
|
||||||
|
import { Content, Left, Right } from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
issueId: PropTypes.string.isRequired,
|
||||||
|
projectUsers: PropTypes.array.isRequired,
|
||||||
|
fetchProject: PropTypes.func.isRequired,
|
||||||
|
updateLocalIssuesArray: PropTypes.func.isRequired,
|
||||||
|
modalClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardIssueDetails = ({
|
||||||
|
issueId,
|
||||||
|
projectUsers,
|
||||||
|
fetchProject,
|
||||||
|
updateLocalIssuesArray,
|
||||||
|
modalClose,
|
||||||
|
}) => {
|
||||||
|
const [{ data, error, isLoading, setLocalData }] = useApi.get(`/issues/${issueId}`);
|
||||||
|
|
||||||
|
if (isLoading) return <Loader />;
|
||||||
|
if (error) return <PageError />;
|
||||||
|
|
||||||
|
const { issue } = data;
|
||||||
|
|
||||||
|
const updateLocalIssue = fields =>
|
||||||
|
setLocalData(currentData => ({ issue: { ...currentData.issue, ...fields } }));
|
||||||
|
|
||||||
|
const updateIssue = updatedFields => {
|
||||||
|
api.optimisticUpdate({
|
||||||
|
url: `/issues/${issueId}`,
|
||||||
|
updatedFields,
|
||||||
|
currentFields: issue,
|
||||||
|
setLocalData: fields => {
|
||||||
|
updateLocalIssue(fields);
|
||||||
|
updateLocalIssuesArray(issue.id, fields);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TopActions
|
||||||
|
issue={issue}
|
||||||
|
updateIssue={updateIssue}
|
||||||
|
fetchProject={fetchProject}
|
||||||
|
modalClose={modalClose}
|
||||||
|
/>
|
||||||
|
<Content>
|
||||||
|
<Left>
|
||||||
|
<Title issue={issue} updateIssue={updateIssue} />
|
||||||
|
<Description issue={issue} updateIssue={updateIssue} />
|
||||||
|
</Left>
|
||||||
|
<Right>
|
||||||
|
<RightActions issue={issue} updateIssue={updateIssue} projectUsers={projectUsers} />
|
||||||
|
</Right>
|
||||||
|
</Content>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardIssueDetails.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardIssueDetails;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Avatar, Icon } from 'shared/components';
|
import { Avatar } from 'shared/components';
|
||||||
import { color, issueTypeColors, issuePriorityColors, font, mixin } from 'shared/utils/styles';
|
import { color, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const IssueWrapper = styled.div`
|
export const IssueWrapper = styled(Link)`
|
||||||
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -36,19 +38,6 @@ export const Bottom = styled.div`
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TypeIcon = styled(Icon)`
|
|
||||||
font-size: 19px;
|
|
||||||
color: ${props => issueTypeColors[props.color]};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const PriorityIcon = styled(Icon)`
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
margin-left: 4px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${props => issuePriorityColors[props.color]};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Assignees = styled.div`
|
export const Assignees = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { useRouteMatch } from 'react-router-dom';
|
||||||
import { Draggable } from 'react-beautiful-dnd';
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
import { IssuePriority } from 'shared/constants/issues';
|
import { IssueTypeIcon, IssuePriorityIcon } from 'shared/components';
|
||||||
import {
|
import { IssueWrapper, Issue, Title, Bottom, Assignees, AssigneeAvatar } from './Styles';
|
||||||
IssueWrapper,
|
|
||||||
Issue,
|
|
||||||
Title,
|
|
||||||
Bottom,
|
|
||||||
TypeIcon,
|
|
||||||
PriorityIcon,
|
|
||||||
Assignees,
|
|
||||||
AssigneeAvatar,
|
|
||||||
} from './Styles';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
projectUsers: PropTypes.array.isRequired,
|
projectUsers: PropTypes.array.isRequired,
|
||||||
@@ -21,28 +13,26 @@ const propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProjectBoardListsIssue = ({ projectUsers, issue, index }) => {
|
const ProjectBoardListsIssue = ({ projectUsers, issue, index }) => {
|
||||||
|
const match = useRouteMatch();
|
||||||
|
|
||||||
const getUserById = userId => projectUsers.find(user => user.id === userId);
|
const getUserById = userId => projectUsers.find(user => user.id === userId);
|
||||||
|
|
||||||
const assignees = issue.userIds.map(getUserById);
|
const assignees = issue.userIds.map(getUserById);
|
||||||
|
|
||||||
const priorityIconType = [IssuePriority.LOW || IssuePriority.LOWEST].includes(issue.priority)
|
|
||||||
? 'arrow-down'
|
|
||||||
: 'arrow-up';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={issue.id.toString()} index={index}>
|
<Draggable draggableId={issue.id.toString()} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<IssueWrapper
|
<IssueWrapper
|
||||||
|
to={`${match.url}/${issue.id}`}
|
||||||
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
|
||||||
>
|
>
|
||||||
<Issue isBeingDragged={snapshot.isDragging && !snapshot.isDropAnimating}>
|
<Issue isBeingDragged={snapshot.isDragging && !snapshot.isDropAnimating}>
|
||||||
<Title>{issue.title}</Title>
|
<Title>{issue.title}</Title>
|
||||||
<Bottom>
|
<Bottom>
|
||||||
<div>
|
<div>
|
||||||
<TypeIcon type={issue.type} color={issue.type} />
|
<IssueTypeIcon type={issue.type} />
|
||||||
<PriorityIcon type={priorityIconType} color={issue.priority} />
|
<IssuePriorityIcon priority={issue.priority} top={-1} left={4} />
|
||||||
</div>
|
</div>
|
||||||
<Assignees>
|
<Assignees>
|
||||||
{assignees.map(user => (
|
{assignees.map(user => (
|
||||||
|
|||||||
@@ -2,64 +2,44 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
import { intersection } from 'lodash';
|
import { get, intersection } from 'lodash';
|
||||||
|
|
||||||
import api from 'shared/utils/api';
|
import api from 'shared/utils/api';
|
||||||
import {
|
import useApi from 'shared/hooks/api';
|
||||||
moveItemWithinArray,
|
import { moveItemWithinArray, insertItemIntoArray } from 'shared/utils/javascript';
|
||||||
insertItemIntoArray,
|
import { IssueStatus, IssueStatusCopy } from 'shared/constants/issues';
|
||||||
updateArrayItemById,
|
|
||||||
} from 'shared/utils/javascript';
|
|
||||||
import { IssueStatus } from 'shared/constants/issues';
|
|
||||||
import Issue from './Issue';
|
import Issue from './Issue';
|
||||||
import { Lists, List, Title, IssuesCount, Issues } from './Styles';
|
import { Lists, List, Title, IssuesCount, Issues } from './Styles';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
project: PropTypes.object.isRequired,
|
project: PropTypes.object.isRequired,
|
||||||
filters: PropTypes.object.isRequired,
|
filters: PropTypes.object.isRequired,
|
||||||
currentUserId: PropTypes.number,
|
updateLocalIssuesArray: PropTypes.func.isRequired,
|
||||||
setLocalProjectData: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const ProjectBoardLists = ({ project, filters, updateLocalIssuesArray }) => {
|
||||||
currentUserId: null,
|
const [{ data: currentUserData }] = useApi.get('/currentUser');
|
||||||
};
|
const currentUserId = get(currentUserData, 'currentUser.id');
|
||||||
|
|
||||||
const ProjectBoardLists = ({ project, filters, currentUserId, setLocalProjectData }) => {
|
|
||||||
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
|
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
|
||||||
|
|
||||||
const handleIssueDrop = ({ draggableId, destination, source }) => {
|
const handleIssueDrop = async ({ draggableId, destination, source }) => {
|
||||||
if (!destination) return;
|
if (!destination) return;
|
||||||
|
|
||||||
const isSameList = destination.droppableId === source.droppableId;
|
const isSameList = destination.droppableId === source.droppableId;
|
||||||
const isSamePosition = destination.index === source.index;
|
const isSamePosition = destination.index === source.index;
|
||||||
|
|
||||||
if (isSameList && isSamePosition) return;
|
if (isSameList && isSamePosition) return;
|
||||||
|
|
||||||
const issueId = parseInt(draggableId);
|
const issueId = parseInt(draggableId);
|
||||||
|
|
||||||
const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(
|
api.optimisticUpdate({
|
||||||
project.issues,
|
url: `/issues/${issueId}`,
|
||||||
destination,
|
updatedFields: {
|
||||||
isSameList,
|
status: destination.droppableId,
|
||||||
issueId,
|
listPosition: calculateListPosition(project.issues, destination, isSameList, issueId),
|
||||||
);
|
|
||||||
|
|
||||||
const afterDropListPosition = calculateListPosition(prevIssue, nextIssue);
|
|
||||||
|
|
||||||
const issueFieldsToUpdate = {
|
|
||||||
status: destination.droppableId,
|
|
||||||
listPosition: afterDropListPosition,
|
|
||||||
};
|
|
||||||
|
|
||||||
setLocalProjectData(data => ({
|
|
||||||
project: {
|
|
||||||
...data.project,
|
|
||||||
issues: updateArrayItemById(data.project.issues, issueId, issueFieldsToUpdate),
|
|
||||||
},
|
},
|
||||||
}));
|
currentFields: project.issues.find(({ id }) => id === issueId),
|
||||||
|
setLocalData: fields => updateLocalIssuesArray(issueId, fields),
|
||||||
api.put(`/issues/${issueId}`, issueFieldsToUpdate);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderList = status => {
|
const renderList = status => {
|
||||||
@@ -76,12 +56,12 @@ const ProjectBoardLists = ({ project, filters, currentUserId, setLocalProjectDat
|
|||||||
{provided => (
|
{provided => (
|
||||||
<List>
|
<List>
|
||||||
<Title>
|
<Title>
|
||||||
{`${issueStatusCopy[status]} `}
|
{`${IssueStatusCopy[status]} `}
|
||||||
<IssuesCount>{issuesCount}</IssuesCount>
|
<IssuesCount>{issuesCount}</IssuesCount>
|
||||||
</Title>
|
</Title>
|
||||||
<Issues {...provided.droppableProps} ref={provided.innerRef}>
|
<Issues {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
{filteredListIssues.map((issue, i) => (
|
{filteredListIssues.map((issue, index) => (
|
||||||
<Issue key={issue.id} projectUsers={project.users} issue={issue} index={i} />
|
<Issue key={issue.id} projectUsers={project.users} issue={issue} index={index} />
|
||||||
))}
|
))}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</Issues>
|
</Issues>
|
||||||
@@ -92,9 +72,11 @@ const ProjectBoardLists = ({ project, filters, currentUserId, setLocalProjectDat
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={handleIssueDrop}>
|
<>
|
||||||
<Lists>{Object.values(IssueStatus).map(renderList)}</Lists>
|
<DragDropContext onDragEnd={handleIssueDrop}>
|
||||||
</DragDropContext>
|
<Lists>{Object.values(IssueStatus).map(renderList)}</Lists>
|
||||||
|
</DragDropContext>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,7 +102,8 @@ const filterIssues = (projectIssues, filters, currentUserId) => {
|
|||||||
const getSortedListIssues = (issues, status) =>
|
const getSortedListIssues = (issues, status) =>
|
||||||
issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);
|
issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);
|
||||||
|
|
||||||
const calculateListPosition = (prevIssue, nextIssue) => {
|
const calculateListPosition = (...args) => {
|
||||||
|
const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(...args);
|
||||||
let position;
|
let position;
|
||||||
|
|
||||||
if (!prevIssue && !nextIssue) {
|
if (!prevIssue && !nextIssue) {
|
||||||
@@ -149,14 +132,6 @@ const getAfterDropPrevNextIssue = (allIssues, destination, isSameList, droppedIs
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueStatusCopy = {
|
|
||||||
[IssueStatus.BACKLOG]: 'Backlog',
|
|
||||||
[IssueStatus.SELECTED]: 'Selected for development',
|
|
||||||
[IssueStatus.INPROGRESS]: 'In progress',
|
|
||||||
[IssueStatus.DONE]: 'Done',
|
|
||||||
};
|
|
||||||
|
|
||||||
ProjectBoardLists.propTypes = propTypes;
|
ProjectBoardLists.propTypes = propTypes;
|
||||||
ProjectBoardLists.defaultProps = defaultProps;
|
|
||||||
|
|
||||||
export default ProjectBoardLists;
|
export default ProjectBoardLists;
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { get } from 'lodash';
|
import { Route, useRouteMatch, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import useApi from 'shared/hooks/api';
|
import { Modal } from 'shared/components';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
import Filters from './Filters';
|
import Filters from './Filters';
|
||||||
import Lists from './Lists';
|
import Lists from './Lists';
|
||||||
|
import IssueDetails from './IssueDetails';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
project: PropTypes.object.isRequired,
|
project: PropTypes.object.isRequired,
|
||||||
setLocalProjectData: PropTypes.func.isRequired,
|
fetchProject: PropTypes.func.isRequired,
|
||||||
|
updateLocalIssuesArray: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultFilters = { searchQuery: '', userIds: [], myOnly: false, recent: false };
|
const defaultFilters = {
|
||||||
|
searchQuery: '',
|
||||||
|
userIds: [],
|
||||||
|
myOnly: false,
|
||||||
|
recent: false,
|
||||||
|
};
|
||||||
|
|
||||||
const ProjectBoard = ({ project, setLocalProjectData }) => {
|
const ProjectBoard = ({ project, fetchProject, updateLocalIssuesArray }) => {
|
||||||
|
const match = useRouteMatch();
|
||||||
|
const history = useHistory();
|
||||||
const [filters, setFilters] = useState(defaultFilters);
|
const [filters, setFilters] = useState(defaultFilters);
|
||||||
|
|
||||||
const [{ data }] = useApi.get('/currentUser');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header projectName={project.name} />
|
<Header projectName={project.name} />
|
||||||
@@ -28,11 +35,26 @@ const ProjectBoard = ({ project, setLocalProjectData }) => {
|
|||||||
filters={filters}
|
filters={filters}
|
||||||
setFilters={setFilters}
|
setFilters={setFilters}
|
||||||
/>
|
/>
|
||||||
<Lists
|
<Lists project={project} filters={filters} updateLocalIssuesArray={updateLocalIssuesArray} />
|
||||||
project={project}
|
<Route
|
||||||
filters={filters}
|
path={`${match.path}/:issueId`}
|
||||||
currentUserId={get(data, 'currentUser.id')}
|
render={({ match: { params } }) => (
|
||||||
setLocalProjectData={setLocalProjectData}
|
<Modal
|
||||||
|
isOpen
|
||||||
|
width={1040}
|
||||||
|
withCloseIcon={false}
|
||||||
|
onClose={() => history.push(match.url)}
|
||||||
|
renderContent={modal => (
|
||||||
|
<IssueDetails
|
||||||
|
issueId={params.issueId}
|
||||||
|
projectUsers={project.users}
|
||||||
|
fetchProject={fetchProject}
|
||||||
|
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||||
|
modalClose={modal.close}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import {
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
projectName: PropTypes.string.isRequired,
|
projectName: PropTypes.string.isRequired,
|
||||||
|
matchPath: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectSidebar = ({ projectName }) => (
|
const ProjectSidebar = ({ projectName, matchPath }) => (
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<ProjectInfo>
|
<ProjectInfo>
|
||||||
<ProjectAvatar />
|
<ProjectAvatar />
|
||||||
@@ -25,15 +26,15 @@ const ProjectSidebar = ({ projectName }) => (
|
|||||||
<ProjectCategory>Software project</ProjectCategory>
|
<ProjectCategory>Software project</ProjectCategory>
|
||||||
</ProjectTexts>
|
</ProjectTexts>
|
||||||
</ProjectInfo>
|
</ProjectInfo>
|
||||||
<LinkItem to="/project/board">
|
<LinkItem to={`${matchPath}/board`}>
|
||||||
<Icon type="board" />
|
<Icon type="board" />
|
||||||
<LinkText>Kanban Board</LinkText>
|
<LinkText>Kanban Board</LinkText>
|
||||||
</LinkItem>
|
</LinkItem>
|
||||||
<LinkItem to="/project/issues">
|
<LinkItem to={`${matchPath}/issues`}>
|
||||||
<Icon type="issues" />
|
<Icon type="issues" />
|
||||||
<LinkText>Issues and filters</LinkText>
|
<LinkText>Issues and filters</LinkText>
|
||||||
</LinkItem>
|
</LinkItem>
|
||||||
<LinkItem to="/project/settings">
|
<LinkItem to={`${matchPath}/settings`}>
|
||||||
<Icon type="settings" />
|
<Icon type="settings" />
|
||||||
<LinkText>Project settings</LinkText>
|
<LinkText>Project settings</LinkText>
|
||||||
</LinkItem>
|
</LinkItem>
|
||||||
|
|||||||
@@ -1,23 +1,48 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Route, Redirect, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import useApi from 'shared/hooks/api';
|
import useApi from 'shared/hooks/api';
|
||||||
|
import { updateArrayItemById } from 'shared/utils/javascript';
|
||||||
import { PageLoader, PageError } from 'shared/components';
|
import { PageLoader, PageError } from 'shared/components';
|
||||||
import Sidebar from './Sidebar';
|
import Sidebar from './Sidebar';
|
||||||
import Board from './Board';
|
import Board from './Board';
|
||||||
import { ProjectPage } from './Styles';
|
import { ProjectPage } from './Styles';
|
||||||
|
|
||||||
const Project = () => {
|
const Project = () => {
|
||||||
const [{ data, error, setLocalData: setLocalProjectData }] = useApi.get('/project');
|
const match = useRouteMatch();
|
||||||
|
const [{ data, error, setLocalData }, fetchProject] = useApi.get('/project');
|
||||||
|
|
||||||
|
const updateLocalIssuesArray = (issueId, updatedFields) => {
|
||||||
|
setLocalData(currentData => ({
|
||||||
|
project: {
|
||||||
|
...currentData.project,
|
||||||
|
issues: updateArrayItemById(data.project.issues, issueId, updatedFields),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
if (!data) return <PageLoader />;
|
if (!data) return <PageLoader />;
|
||||||
if (error) return <PageError />;
|
if (error) return <PageError />;
|
||||||
|
|
||||||
const { project } = data;
|
const { project } = data;
|
||||||
|
|
||||||
|
const renderBoard = () => (
|
||||||
|
<Board
|
||||||
|
project={project}
|
||||||
|
fetchProject={fetchProject}
|
||||||
|
updateLocalIssuesArray={updateLocalIssuesArray}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const renderSettings = () => <h1>SETTINGS</h1>;
|
||||||
|
const renderIssues = () => <h1>ISSUES</h1>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectPage>
|
<ProjectPage>
|
||||||
<Sidebar projectName={project.name} />
|
<Sidebar projectName={project.name} matchPath={match.path} />
|
||||||
<Board project={project} setLocalProjectData={setLocalProjectData} />
|
<Route path={`${match.path}/board`} render={renderBoard} />
|
||||||
|
<Route path={`${match.path}/settings`} render={renderSettings} />
|
||||||
|
<Route path={`${match.path}/issues`} render={renderIssues} />
|
||||||
|
{match.isExact && <Redirect to={`${match.url}/board`} />}
|
||||||
</ProjectPage>
|
</ProjectPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'core-js/stable';
|
import 'core-js/stable';
|
||||||
|
import 'regenerator-runtime/runtime';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|||||||
@@ -26,6 +26,21 @@ export const StyledButton = styled.button`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const colored = css`
|
||||||
|
color: #fff;
|
||||||
|
background: ${props => color[props.color]};
|
||||||
|
${font.medium}
|
||||||
|
&:not(:disabled) {
|
||||||
|
&:hover {
|
||||||
|
background: ${props => mixin.lighten(color[props.color], 0.15)};
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
background: ${props => mixin.darken(color[props.color], 0.1)};
|
||||||
|
}
|
||||||
|
${props => props.isActive && `background: ${mixin.darken(color[props.color], 0.1)} !important;`}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const secondaryAndEmptyShared = css`
|
const secondaryAndEmptyShared = css`
|
||||||
color: ${color.textDark};
|
color: ${color.textDark};
|
||||||
${font.regular}
|
${font.regular}
|
||||||
@@ -35,36 +50,21 @@ const secondaryAndEmptyShared = css`
|
|||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
color: ${color.primary};
|
color: ${color.primary};
|
||||||
background: ${mixin.rgba(color.primary, 0.15)};
|
background: ${color.backgroundLightPrimary};
|
||||||
}
|
}
|
||||||
${props =>
|
${props =>
|
||||||
props.isActive &&
|
props.isActive &&
|
||||||
`
|
`
|
||||||
color: ${color.primary};
|
color: ${color.primary};
|
||||||
background: ${mixin.rgba(color.primary, 0.15)} !important;
|
background: ${color.backgroundLightPrimary} !important;
|
||||||
`}
|
`}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const buttonColors = {
|
const buttonColors = {
|
||||||
primary: css`
|
primary: colored,
|
||||||
color: #fff;
|
success: colored,
|
||||||
background: ${color.primary};
|
danger: colored,
|
||||||
${font.medium}
|
|
||||||
&:not(:disabled) {
|
|
||||||
&:hover {
|
|
||||||
background: ${mixin.lighten(color.primary, 0.15)};
|
|
||||||
}
|
|
||||||
&:active {
|
|
||||||
background: ${mixin.darken(color.primary, 0.1)};
|
|
||||||
}
|
|
||||||
${props =>
|
|
||||||
props.isActive &&
|
|
||||||
`
|
|
||||||
background: ${mixin.darken(color.primary, 0.1)} !important;
|
|
||||||
`}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
secondary: css`
|
secondary: css`
|
||||||
background: ${color.secondary};
|
background: ${color.secondary};
|
||||||
${secondaryAndEmptyShared};
|
${secondaryAndEmptyShared};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { color } from 'shared/utils/styles';
|
import { color } from 'shared/utils/styles';
|
||||||
@@ -8,8 +8,8 @@ import { StyledButton, StyledSpinner } from './Styles';
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
color: PropTypes.oneOf(['primary', 'secondary', 'empty']),
|
color: PropTypes.oneOf(['primary', 'secondary', 'empty', 'success', 'danger']),
|
||||||
icon: PropTypes.string,
|
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
||||||
iconSize: PropTypes.number,
|
iconSize: PropTypes.number,
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
working: PropTypes.bool,
|
working: PropTypes.bool,
|
||||||
@@ -27,44 +27,55 @@ const defaultProps = {
|
|||||||
onClick: () => {},
|
onClick: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Button = ({
|
const Button = forwardRef(
|
||||||
children,
|
(
|
||||||
color: propsColor,
|
{
|
||||||
icon,
|
children,
|
||||||
iconSize,
|
color: propsColor,
|
||||||
disabled,
|
icon,
|
||||||
working,
|
iconSize,
|
||||||
onClick = () => {},
|
disabled,
|
||||||
...buttonProps
|
working,
|
||||||
}) => (
|
onClick = () => {},
|
||||||
<StyledButton
|
...buttonProps
|
||||||
{...buttonProps}
|
},
|
||||||
onClick={() => {
|
ref,
|
||||||
|
) => {
|
||||||
|
const handleClick = () => {
|
||||||
if (!disabled && !working) {
|
if (!disabled && !working) {
|
||||||
onClick();
|
onClick();
|
||||||
}
|
}
|
||||||
}}
|
};
|
||||||
color={propsColor}
|
const renderSpinner = () => (
|
||||||
disabled={disabled || working}
|
|
||||||
working={working}
|
|
||||||
iconOnly={!children}
|
|
||||||
>
|
|
||||||
{working && (
|
|
||||||
<StyledSpinner
|
<StyledSpinner
|
||||||
iconOnly={!children}
|
iconOnly={!children}
|
||||||
size={26}
|
size={26}
|
||||||
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
{!working && icon && (
|
const renderIcon = () => (
|
||||||
<Icon
|
<Icon
|
||||||
type={icon}
|
type={icon}
|
||||||
size={iconSize}
|
size={iconSize}
|
||||||
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
color={propsColor === 'primary' ? '#fff' : color.textDark}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
<div>{children}</div>
|
return (
|
||||||
</StyledButton>
|
<StyledButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={handleClick}
|
||||||
|
color={propsColor}
|
||||||
|
disabled={disabled || working}
|
||||||
|
working={working}
|
||||||
|
iconOnly={!children}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{working && renderSpinner()}
|
||||||
|
{!working && icon && (typeof icon !== 'string' ? icon : renderIcon())}
|
||||||
|
<div>{children}</div>
|
||||||
|
</StyledButton>
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Button.propTypes = propTypes;
|
Button.propTypes = propTypes;
|
||||||
|
|||||||
@@ -11,21 +11,21 @@ export const StyledConfirmModal = styled(Modal)`
|
|||||||
|
|
||||||
export const Title = styled.div`
|
export const Title = styled.div`
|
||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
${font.bold}
|
${font.medium}
|
||||||
${font.size(24)}
|
${font.size(22)}
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Message = styled.p`
|
export const Message = styled.p`
|
||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
${font.size(16)}
|
${font.size(15)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const InputLabel = styled.div`
|
export const InputLabel = styled.div`
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
${font.bold}
|
${font.bold}
|
||||||
${font.size(16)}
|
${font.size(15)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledInput = styled(Input)`
|
export const StyledInput = styled(Input)`
|
||||||
@@ -33,6 +33,10 @@ export const StyledInput = styled(Input)`
|
|||||||
max-width: 220px;
|
max-width: 220px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Actions = styled.div`
|
||||||
|
display: flex;
|
||||||
|
`;
|
||||||
|
|
||||||
export const StyledButton = styled(Button)`
|
export const StyledButton = styled(Button)`
|
||||||
margin: 5px 20px 0 0;
|
margin: 5px 20px 0 0;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Message,
|
Message,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
StyledInput,
|
StyledInput,
|
||||||
|
Actions,
|
||||||
StyledButton,
|
StyledButton,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
|
||||||
@@ -76,17 +77,19 @@ const ConfirmModal = ({
|
|||||||
<br />
|
<br />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<StyledButton hollow onClick={modal.close}>
|
<Actions>
|
||||||
{cancelText}
|
<StyledButton hollow onClick={modal.close}>
|
||||||
</StyledButton>
|
{cancelText}
|
||||||
<StyledButton
|
</StyledButton>
|
||||||
color={type}
|
<StyledButton
|
||||||
disabled={confirmInput && !isConfirmEnabled}
|
color={type}
|
||||||
working={isWorking}
|
disabled={confirmInput && !isConfirmEnabled}
|
||||||
onClick={() => handleConfirm(modal)}
|
working={isWorking}
|
||||||
>
|
onClick={() => handleConfirm(modal)}
|
||||||
{confirmText}
|
>
|
||||||
</StyledButton>
|
{confirmText}
|
||||||
|
</StyledButton>
|
||||||
|
</Actions>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
21
client/src/shared/components/CopyLinkButton.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { copyToClipboard } from 'shared/utils/clipboard';
|
||||||
|
import { Button } from 'shared/components';
|
||||||
|
|
||||||
|
const CopyLinkButton = ({ ...otherProps }) => {
|
||||||
|
const [isLinkCopied, setLinkCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleLinkCopy = () => {
|
||||||
|
setLinkCopied(true);
|
||||||
|
setTimeout(() => setLinkCopied(false), 2000);
|
||||||
|
copyToClipboard(window.location.href);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Button icon="link" onClick={handleLinkCopy} {...otherProps}>
|
||||||
|
{isLinkCopied ? 'Link Copied' : 'Copy link'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CopyLinkButton;
|
||||||
@@ -14,7 +14,7 @@ export const Dropdown = styled.div`
|
|||||||
width: 270px;
|
width: 270px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
${mixin.boxShadowBorderMedium}
|
${mixin.boxShadowDropdown}
|
||||||
${props => (props.withTime ? withTimeStyles : '')}
|
${props => (props.withTime ? withTimeStyles : '')}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const codes = {
|
|||||||
[`issues`]: '\\e908',
|
[`issues`]: '\\e908',
|
||||||
[`settings`]: '\\e909',
|
[`settings`]: '\\e909',
|
||||||
[`close`]: '\\e913',
|
[`close`]: '\\e913',
|
||||||
[`help-filled`]: '\\e912',
|
[`feedback`]: '\\e918',
|
||||||
[`feedback`]: '\\e915',
|
[`trash`]: '\\e912',
|
||||||
};
|
};
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
|||||||
43
client/src/shared/components/InputDebounced.jsx
Normal file
@@ -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 (
|
||||||
|
<Input
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
onChange={newValue => {
|
||||||
|
setValue(newValue);
|
||||||
|
handleChange(newValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
InputDebounced.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default InputDebounced;
|
||||||
9
client/src/shared/components/IssuePriorityIcon/Styles.js
Normal file
@@ -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]};
|
||||||
|
`;
|
||||||
21
client/src/shared/components/IssuePriorityIcon/index.jsx
Normal file
@@ -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 <PriorityIcon type={iconType} color={priority} {...otherProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
IssuePriorityIcon.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default IssuePriorityIcon;
|
||||||
9
client/src/shared/components/IssueTypeIcon/Styles.js
Normal file
@@ -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]};
|
||||||
|
`;
|
||||||
16
client/src/shared/components/IssueTypeIcon/index.jsx
Normal file
@@ -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 }) => (
|
||||||
|
<TypeIcon type={type} color={type} {...otherProps} />
|
||||||
|
);
|
||||||
|
|
||||||
|
IssueTypeIcon.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default IssueTypeIcon;
|
||||||
@@ -41,14 +41,14 @@ export const StyledModal = styled.div`
|
|||||||
|
|
||||||
const modalStyles = {
|
const modalStyles = {
|
||||||
center: css`
|
center: css`
|
||||||
max-width: 600px;
|
max-width: ${props => props.width}px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
${mixin.boxShadowMedium}
|
${mixin.boxShadowMedium}
|
||||||
`,
|
`,
|
||||||
aside: css`
|
aside: css`
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
max-width: 500px;
|
max-width: ${props => props.width}px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
|
box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.15);
|
||||||
`,
|
`,
|
||||||
@@ -57,7 +57,7 @@ const modalStyles = {
|
|||||||
export const CloseIcon = styled(Icon)`
|
export const CloseIcon = styled(Icon)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
color: ${color.textDark};
|
color: ${color.textMedium};
|
||||||
${mixin.clickable}
|
${mixin.clickable}
|
||||||
${props => closeIconStyles[props.variant]}
|
${props => closeIconStyles[props.variant]}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { uniqueId as uniqueIncreasingIntegerId } from 'lodash';
|
|
||||||
|
|
||||||
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
import useOnEscapeKeyDown from 'shared/hooks/onEscapeKeyDown';
|
||||||
@@ -10,6 +9,8 @@ import { ScrollOverlay, ClickableOverlay, StyledModal, CloseIcon } from './Style
|
|||||||
const propTypes = {
|
const propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
variant: PropTypes.oneOf(['center', 'aside']),
|
variant: PropTypes.oneOf(['center', 'aside']),
|
||||||
|
width: PropTypes.number,
|
||||||
|
withCloseIcon: PropTypes.bool,
|
||||||
isOpen: PropTypes.bool,
|
isOpen: PropTypes.bool,
|
||||||
onClose: PropTypes.func,
|
onClose: PropTypes.func,
|
||||||
renderLink: PropTypes.func,
|
renderLink: PropTypes.func,
|
||||||
@@ -19,6 +20,8 @@ const propTypes = {
|
|||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: undefined,
|
className: undefined,
|
||||||
variant: 'center',
|
variant: 'center',
|
||||||
|
width: 600,
|
||||||
|
withCloseIcon: true,
|
||||||
isOpen: undefined,
|
isOpen: undefined,
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
renderLink: () => {},
|
renderLink: () => {},
|
||||||
@@ -27,6 +30,8 @@ const defaultProps = {
|
|||||||
const Modal = ({
|
const Modal = ({
|
||||||
className,
|
className,
|
||||||
variant,
|
variant,
|
||||||
|
width,
|
||||||
|
withCloseIcon,
|
||||||
isOpen: propsIsOpen,
|
isOpen: propsIsOpen,
|
||||||
onClose: tellParentToClose,
|
onClose: tellParentToClose,
|
||||||
renderLink,
|
renderLink,
|
||||||
@@ -37,12 +42,9 @@ const Modal = ({
|
|||||||
const isOpen = isControlled ? propsIsOpen : stateIsOpen;
|
const isOpen = isControlled ? propsIsOpen : stateIsOpen;
|
||||||
|
|
||||||
const $modalRef = useRef();
|
const $modalRef = useRef();
|
||||||
const modalIdRef = useRef(uniqueIncreasingIntegerId());
|
const $clickableOverlayRef = useRef();
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
const closeModal = useCallback(() => {
|
||||||
if (hasChildModal(modalIdRef.current)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isControlled) {
|
if (!isControlled) {
|
||||||
setStateOpen(false);
|
setStateOpen(false);
|
||||||
} else {
|
} else {
|
||||||
@@ -50,15 +52,15 @@ const Modal = ({
|
|||||||
}
|
}
|
||||||
}, [isControlled, tellParentToClose]);
|
}, [isControlled, tellParentToClose]);
|
||||||
|
|
||||||
useOnOutsideClick($modalRef, isOpen, closeModal);
|
useOnOutsideClick($modalRef, isOpen, closeModal, $clickableOverlayRef);
|
||||||
useOnEscapeKeyDown(isOpen, closeModal);
|
useOnEscapeKeyDown(isOpen, closeModal);
|
||||||
useEffect(setBodyScrollLock, [isOpen]);
|
useEffect(setBodyScrollLock, [isOpen]);
|
||||||
|
|
||||||
const renderModal = () => (
|
const renderModal = () => (
|
||||||
<ScrollOverlay data-jira-modal-id={modalIdRef.current}>
|
<ScrollOverlay data-jira-modal="true">
|
||||||
<ClickableOverlay variant={variant}>
|
<ClickableOverlay variant={variant} ref={$clickableOverlayRef}>
|
||||||
<StyledModal className={className} variant={variant} ref={$modalRef}>
|
<StyledModal className={className} variant={variant} width={width} ref={$modalRef}>
|
||||||
<CloseIcon type="close" variant={variant} onClick={closeModal} />
|
{withCloseIcon && <CloseIcon type="close" variant={variant} onClick={closeModal} />}
|
||||||
{renderContent({ close: closeModal })}
|
{renderContent({ close: closeModal })}
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
</ClickableOverlay>
|
</ClickableOverlay>
|
||||||
@@ -75,15 +77,8 @@ const Modal = ({
|
|||||||
|
|
||||||
const $root = document.getElementById('root');
|
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 setBodyScrollLock = () => {
|
||||||
const areAnyModalsOpen = getIdsOfAllOpenModals().length > 0;
|
const areAnyModalsOpen = !!document.querySelector('[data-jira-modal]');
|
||||||
document.body.style.overflow = areAnyModalsOpen ? 'hidden' : 'visible';
|
document.body.style.overflow = areAnyModalsOpen ? 'hidden' : 'visible';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
export default styled.div`
|
export default styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 200px;
|
padding: 200px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -16,12 +16,14 @@ const propTypes = {
|
|||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onCreate: PropTypes.func,
|
onCreate: PropTypes.func,
|
||||||
isMulti: PropTypes.bool,
|
isMulti: PropTypes.bool,
|
||||||
|
propsRenderOption: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
value: undefined,
|
value: undefined,
|
||||||
onCreate: undefined,
|
onCreate: undefined,
|
||||||
isMulti: false,
|
isMulti: false,
|
||||||
|
propsRenderOption: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectDropdown = ({
|
const SelectDropdown = ({
|
||||||
@@ -35,6 +37,7 @@ const SelectDropdown = ({
|
|||||||
onChange,
|
onChange,
|
||||||
onCreate,
|
onCreate,
|
||||||
isMulti,
|
isMulti,
|
||||||
|
propsRenderOption,
|
||||||
}) => {
|
}) => {
|
||||||
const [isCreatingOption, setCreatingOption] = useState(false);
|
const [isCreatingOption, setCreatingOption] = useState(false);
|
||||||
|
|
||||||
@@ -143,27 +146,33 @@ const SelectDropdown = ({
|
|||||||
.includes(searchValue.toLowerCase()),
|
.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
|
const filteredOptions = isMulti
|
||||||
? removeSelectedOptions(optionsFilteredBySearchValue)
|
? removeSelectedOptionsMulti(optionsFilteredBySearchValue)
|
||||||
: optionsFilteredBySearchValue;
|
: removeSelectedOptionsSingle(optionsFilteredBySearchValue);
|
||||||
|
|
||||||
const searchValueNotInOptions = !options.map(option => option.label).includes(searchValue);
|
const searchValueNotInOptions = !options.map(option => option.label).includes(searchValue);
|
||||||
const isOptionCreatable = onCreate && searchValue && searchValueNotInOptions;
|
const isOptionCreatable = onCreate && searchValue && searchValueNotInOptions;
|
||||||
|
|
||||||
const renderSelectableOption = (option, i) => (
|
const renderSelectableOption = (option, i) => {
|
||||||
<Option
|
const optionProps = {
|
||||||
key={option.value}
|
key: option.value,
|
||||||
className={i === 0 ? activeOptionClass : undefined}
|
value: option.value,
|
||||||
isSelected={option.value === value}
|
label: option.label,
|
||||||
data-select-option-value={option.value}
|
className: i === 0 ? activeOptionClass : undefined,
|
||||||
onMouseEnter={handleOptionMouseEnter}
|
isSelected: option.value === value,
|
||||||
onClick={() => selectOptionValue(option.value)}
|
'data-select-option-value': option.value,
|
||||||
>
|
onMouseEnter: handleOptionMouseEnter,
|
||||||
{option.label}
|
onClick: () => selectOptionValue(option.value),
|
||||||
</Option>
|
};
|
||||||
);
|
return propsRenderOption ? (
|
||||||
|
propsRenderOption(optionProps)
|
||||||
|
) : (
|
||||||
|
<Option {...optionProps}>{option.label}</Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderCreatableOption = () => (
|
const renderCreatableOption = () => (
|
||||||
<Option
|
<Option
|
||||||
|
|||||||
@@ -6,63 +6,54 @@ import Icon from 'shared/components/Icon';
|
|||||||
export const StyledSelect = styled.div`
|
export const StyledSelect = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
border: 1px solid ${color.borderLight};
|
border: 1px solid ${color.borderLightest};
|
||||||
background: #fff;
|
background: #fff;
|
||||||
${font.size(14)}
|
${font.size(14)}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 1px solid ${color.borderMedium};
|
background: #fff;
|
||||||
|
border: 1px solid ${color.borderInputFocus};
|
||||||
|
box-shadow: 0 0 0 1px ${color.borderInputFocus};
|
||||||
}
|
}
|
||||||
${props => (props.hasIcon ? 'padding-left: 25px;' : '')}
|
${props => props.invalid && `&, &:focus { border: 1px solid ${color.danger}; }`}
|
||||||
${props => (props.invalid ? `&, &:focus { border: 1px solid ${color.danger}; }` : '')}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const StyledIcon = styled(Icon)`
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
left: 14px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: ${color.textMedium};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ValueContainer = styled.div`
|
export const ValueContainer = styled.div`
|
||||||
min-height: 38px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 32px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
padding: 8px 5px 8px 10px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ChevronIcon = styled(Icon)`
|
export const ChevronIcon = styled(Icon)`
|
||||||
position: absolute;
|
margin-left: auto;
|
||||||
top: 10px;
|
|
||||||
right: 11px;
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: ${color.textMedium};
|
color: ${color.textMedium};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Placeholder = styled.div`
|
export const Placeholder = styled.div`
|
||||||
padding: 11px 0 0 15px;
|
|
||||||
color: ${color.textLight};
|
color: ${color.textLight};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ValueSingle = styled.div`
|
|
||||||
padding: 11px 0 0 15px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const ValueMulti = styled.div`
|
export const ValueMulti = styled.div`
|
||||||
padding: 15px 5px 10px 10px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-top: 5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ValueMultiItem = styled.div`
|
export const ValueMultiItem = styled.div`
|
||||||
margin: 0 5px 5px 0;
|
margin: 0 5px 5px 0;
|
||||||
${mixin.tag}
|
${mixin.tag()}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const AddMore = styled.div`
|
export const AddMore = styled.div`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 24px;
|
margin-bottom: 3px;
|
||||||
line-height: 22px;
|
padding: 3px 0;
|
||||||
padding-right: 5px;
|
${font.size(12.5)}
|
||||||
${font.size(12)}
|
|
||||||
${mixin.link()}
|
${mixin.link()}
|
||||||
i {
|
i {
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
@@ -77,12 +68,13 @@ export const Dropdown = styled.div`
|
|||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
${mixin.boxShadowBorderMedium}
|
${mixin.boxShadowDropdown}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DropdownInput = styled.input`
|
export const DropdownInput = styled.input`
|
||||||
padding: 10px 15px 8px;
|
padding: 10px 12px 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
color: ${color.textDarkest};
|
color: ${color.textDarkest};
|
||||||
@@ -118,7 +110,7 @@ export const Option = styled.div`
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
&.jira-select-option-is-active {
|
&.jira-select-option-is-active {
|
||||||
background: ${mixin.lighten(color.backgroundMedium, 0.05)};
|
background: ${color.backgroundLightPrimary};
|
||||||
}
|
}
|
||||||
${props => (props.isSelected ? selectedOptionStyles : '')}
|
${props => (props.isSelected ? selectedOptionStyles : '')}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -7,11 +7,9 @@ import Icon from 'shared/components/Icon';
|
|||||||
import Dropdown from './Dropdown';
|
import Dropdown from './Dropdown';
|
||||||
import {
|
import {
|
||||||
StyledSelect,
|
StyledSelect,
|
||||||
StyledIcon,
|
|
||||||
ValueContainer,
|
ValueContainer,
|
||||||
ChevronIcon,
|
ChevronIcon,
|
||||||
Placeholder,
|
Placeholder,
|
||||||
ValueSingle,
|
|
||||||
ValueMulti,
|
ValueMulti,
|
||||||
ValueMultiItem,
|
ValueMultiItem,
|
||||||
AddMore,
|
AddMore,
|
||||||
@@ -19,8 +17,7 @@ import {
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
icon: PropTypes.string,
|
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string, PropTypes.number]),
|
||||||
value: PropTypes.any,
|
|
||||||
defaultValue: PropTypes.any,
|
defaultValue: PropTypes.any,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
invalid: PropTypes.bool,
|
invalid: PropTypes.bool,
|
||||||
@@ -28,22 +25,24 @@ const propTypes = {
|
|||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
onCreate: PropTypes.func,
|
onCreate: PropTypes.func,
|
||||||
isMulti: PropTypes.bool,
|
isMulti: PropTypes.bool,
|
||||||
|
renderValue: PropTypes.func,
|
||||||
|
renderOption: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
className: undefined,
|
className: undefined,
|
||||||
icon: undefined,
|
|
||||||
value: undefined,
|
value: undefined,
|
||||||
defaultValue: undefined,
|
defaultValue: undefined,
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
invalid: false,
|
invalid: false,
|
||||||
onCreate: undefined,
|
onCreate: undefined,
|
||||||
isMulti: false,
|
isMulti: false,
|
||||||
|
renderValue: undefined,
|
||||||
|
renderOption: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Select = ({
|
const Select = ({
|
||||||
className,
|
className,
|
||||||
icon,
|
|
||||||
value: propsValue,
|
value: propsValue,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -52,6 +51,8 @@ const Select = ({
|
|||||||
onChange,
|
onChange,
|
||||||
onCreate,
|
onCreate,
|
||||||
isMulti,
|
isMulti,
|
||||||
|
renderValue: propsRenderValue,
|
||||||
|
renderOption: propsRenderOption,
|
||||||
}) => {
|
}) => {
|
||||||
const [stateValue, setStateValue] = useState(defaultValue || (isMulti ? [] : null));
|
const [stateValue, setStateValue] = useState(defaultValue || (isMulti ? [] : null));
|
||||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||||
@@ -79,11 +80,18 @@ const Select = ({
|
|||||||
|
|
||||||
useOnOutsideClick($selectRef, isDropdownOpen, deactivateDropdown);
|
useOnOutsideClick($selectRef, isDropdownOpen, deactivateDropdown);
|
||||||
|
|
||||||
|
const ensureValueType = newValue => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
return isMulti ? newValue.map(parseInt) : parseInt(newValue);
|
||||||
|
}
|
||||||
|
return newValue;
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = newValue => {
|
const handleChange = newValue => {
|
||||||
if (!isControlled) {
|
if (!isControlled) {
|
||||||
setStateValue(newValue);
|
setStateValue(ensureValueType(newValue));
|
||||||
}
|
}
|
||||||
onChange(newValue);
|
onChange(ensureValueType(newValue));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeOptionValue = optionValue => {
|
const removeOptionValue = optionValue => {
|
||||||
@@ -106,16 +114,21 @@ const Select = ({
|
|||||||
|
|
||||||
const isValueEmpty = isMulti ? !value.length : !getOption(value);
|
const isValueEmpty = isMulti ? !value.length : !getOption(value);
|
||||||
|
|
||||||
const renderSingleValue = () => <ValueSingle>{getOptionLabel(value)}</ValueSingle>;
|
const renderSingleValue = () =>
|
||||||
|
propsRenderValue ? propsRenderValue({ value }) : getOptionLabel(value);
|
||||||
|
|
||||||
const renderMultiValue = () => (
|
const renderMultiValue = () => (
|
||||||
<ValueMulti>
|
<ValueMulti>
|
||||||
{value.map(optionValue => (
|
{value.map(optionValue =>
|
||||||
<ValueMultiItem key={optionValue} onClick={() => removeOptionValue(optionValue)}>
|
propsRenderValue ? (
|
||||||
{getOptionLabel(optionValue)}
|
propsRenderValue({ value: optionValue, removeOptionValue })
|
||||||
<Icon type="close" />
|
) : (
|
||||||
</ValueMultiItem>
|
<ValueMultiItem key={optionValue} onClick={() => removeOptionValue(optionValue)}>
|
||||||
))}
|
{getOptionLabel(optionValue)}
|
||||||
|
<Icon type="close" />
|
||||||
|
</ValueMultiItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
<AddMore>
|
<AddMore>
|
||||||
<Icon type="plus" />
|
<Icon type="plus" />
|
||||||
Add more
|
Add more
|
||||||
@@ -128,16 +141,14 @@ const Select = ({
|
|||||||
className={className}
|
className={className}
|
||||||
ref={$selectRef}
|
ref={$selectRef}
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
hasIcon={!!icon}
|
|
||||||
onKeyDown={handleFocusedSelectKeydown}
|
onKeyDown={handleFocusedSelectKeydown}
|
||||||
invalid={invalid}
|
invalid={invalid}
|
||||||
>
|
>
|
||||||
<ValueContainer onClick={activateDropdown}>
|
<ValueContainer onClick={activateDropdown}>
|
||||||
{icon && <StyledIcon type={icon} />}
|
|
||||||
{(!isMulti || isValueEmpty) && <ChevronIcon type="chevron-down" />}
|
|
||||||
{isValueEmpty && <Placeholder>{placeholder}</Placeholder>}
|
{isValueEmpty && <Placeholder>{placeholder}</Placeholder>}
|
||||||
{!isValueEmpty && !isMulti && renderSingleValue()}
|
{!isValueEmpty && !isMulti && renderSingleValue()}
|
||||||
{!isValueEmpty && isMulti && renderMultiValue()}
|
{!isValueEmpty && isMulti && renderMultiValue()}
|
||||||
|
{(!isMulti || isValueEmpty) && <ChevronIcon type="chevron-down" top={1} />}
|
||||||
</ValueContainer>
|
</ValueContainer>
|
||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -152,6 +163,7 @@ const Select = ({
|
|||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onCreate={onCreate}
|
onCreate={onCreate}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
|
propsRenderOption={propsRenderOption}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</StyledSelect>
|
</StyledSelect>
|
||||||
|
|||||||
9
client/src/shared/components/TextEditedContent/Styles.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { font } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const Content = styled.div`
|
||||||
|
padding: 0 !important;
|
||||||
|
${font.size(15)}
|
||||||
|
${font.regular}
|
||||||
|
`;
|
||||||
21
client/src/shared/components/TextEditedContent/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/* eslint-disable react/no-danger */
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { Content } from './Styles';
|
||||||
|
|
||||||
|
import('quill/dist/quill.snow.css');
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
content: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextEditedContent = ({ content, ...otherProps }) => (
|
||||||
|
<div className="ql-snow">
|
||||||
|
<Content className="ql-editor" dangerouslySetInnerHTML={{ __html: content }} {...otherProps} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
TextEditedContent.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default TextEditedContent;
|
||||||
21
client/src/shared/components/TextEditor/Styles.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { color, font } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const EditorCont = styled.div`
|
||||||
|
.ql-toolbar.ql-snow {
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
border: 1px solid ${color.borderLightest};
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.ql-container.ql-snow {
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
border: 1px solid ${color.borderLightest};
|
||||||
|
border-top: none;
|
||||||
|
${font.size(15)}
|
||||||
|
${font.regular}
|
||||||
|
}
|
||||||
|
.ql-editor {
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
70
client/src/shared/components/TextEditor/index.jsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { useLayoutEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Quill from 'quill';
|
||||||
|
|
||||||
|
import { EditorCont } from './Styles';
|
||||||
|
|
||||||
|
import('quill/dist/quill.snow.css');
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
defaultValue: PropTypes.string,
|
||||||
|
getEditor: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
className: undefined,
|
||||||
|
placeholder: undefined,
|
||||||
|
defaultValue: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const TextEditor = ({ className, placeholder, defaultValue, getEditor, ...otherProps }) => {
|
||||||
|
const $editorContRef = useRef();
|
||||||
|
const $editorRef = useRef();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let editor = null;
|
||||||
|
|
||||||
|
const setup = async () => {
|
||||||
|
editor = new Quill($editorRef.current, { placeholder, ...editorConfig });
|
||||||
|
|
||||||
|
editor.clipboard.dangerouslyPasteHTML(0, defaultValue);
|
||||||
|
|
||||||
|
getEditor({
|
||||||
|
getHTML: () => $editorContRef.current.querySelector('.ql-editor').innerHTML,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
setup();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
editor = null;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditorCont className={className} ref={$editorContRef}>
|
||||||
|
<div ref={$editorRef} {...otherProps} />
|
||||||
|
</EditorCont>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editorConfig = {
|
||||||
|
theme: 'snow',
|
||||||
|
modules: {
|
||||||
|
toolbar: [
|
||||||
|
['bold', 'italic', 'underline', 'strike'],
|
||||||
|
['blockquote', 'code-block'],
|
||||||
|
[{ list: 'ordered' }, { list: 'bullet' }],
|
||||||
|
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
||||||
|
[{ color: [] }, { background: [] }],
|
||||||
|
['clean'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
TextEditor.propTypes = propTypes;
|
||||||
|
TextEditor.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
export default TextEditor;
|
||||||
@@ -25,7 +25,7 @@ const Textarea = forwardRef(({ className, invalid, onChange, ...textareaProps },
|
|||||||
<TextareaAutoSize
|
<TextareaAutoSize
|
||||||
{...textareaProps}
|
{...textareaProps}
|
||||||
onChange={event => onChange(event.target.value, event)}
|
onChange={event => onChange(event.target.value, event)}
|
||||||
ref={ref}
|
inputRef={ref}
|
||||||
/>
|
/>
|
||||||
</StyledTextarea>
|
</StyledTextarea>
|
||||||
));
|
));
|
||||||
|
|||||||
13
client/src/shared/components/Tooltip/Styles.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { zIndexValues, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const Tooltip = styled.div`
|
||||||
|
z-index: ${zIndexValues.modal + 1};
|
||||||
|
position: fixed;
|
||||||
|
width: ${props => props.width}px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
${mixin.hardwareAccelerate}
|
||||||
|
${mixin.boxShadowDropdown}
|
||||||
|
`;
|
||||||
110
client/src/shared/components/Tooltip/index.jsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
|
||||||
|
import { Tooltip } from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
||||||
|
offset: PropTypes.shape({
|
||||||
|
top: PropTypes.number,
|
||||||
|
left: PropTypes.number,
|
||||||
|
}),
|
||||||
|
width: PropTypes.number.isRequired,
|
||||||
|
renderLink: PropTypes.func.isRequired,
|
||||||
|
renderContent: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
className: undefined,
|
||||||
|
placement: 'bottom',
|
||||||
|
offset: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Modal = ({ className, placement, offset, width, renderLink, renderContent }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const $linkRef = useRef();
|
||||||
|
const $tooltipRef = useRef();
|
||||||
|
|
||||||
|
const openTooltip = () => setIsOpen(true);
|
||||||
|
const closeTooltip = () => setIsOpen(false);
|
||||||
|
|
||||||
|
useOnOutsideClick([$tooltipRef, $linkRef], isOpen, closeTooltip);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const setTooltipPosition = () => {
|
||||||
|
const { top, left } = calcPosition(offset, placement, $tooltipRef, $linkRef);
|
||||||
|
$tooltipRef.current.style.top = `${top}px`;
|
||||||
|
$tooltipRef.current.style.left = `${left}px`;
|
||||||
|
};
|
||||||
|
if (isOpen) {
|
||||||
|
setTooltipPosition();
|
||||||
|
window.addEventListener('resize', setTooltipPosition);
|
||||||
|
window.addEventListener('scroll', setTooltipPosition);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', setTooltipPosition);
|
||||||
|
window.removeEventListener('scroll', setTooltipPosition);
|
||||||
|
};
|
||||||
|
}, [isOpen, offset, placement]);
|
||||||
|
|
||||||
|
const renderTooltip = () => (
|
||||||
|
<Tooltip className={className} ref={$tooltipRef} width={width}>
|
||||||
|
{renderContent({ close: closeTooltip })}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderLink({ ref: $linkRef, onClick: isOpen ? closeTooltip : openTooltip })}
|
||||||
|
{isOpen && ReactDOM.createPortal(renderTooltip(), $root)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calcPosition = (offset, placement, $tooltipRef, $linkRef) => {
|
||||||
|
const margin = 20;
|
||||||
|
const finalOffset = { ...defaultProps.offset, ...offset };
|
||||||
|
|
||||||
|
const tooltipRect = $tooltipRef.current.getBoundingClientRect();
|
||||||
|
const linkRect = $linkRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const linkCenterY = linkRect.top + linkRect.height / 2;
|
||||||
|
const linkCenterX = linkRect.left + linkRect.width / 2;
|
||||||
|
|
||||||
|
const placements = {
|
||||||
|
top: {
|
||||||
|
top: linkRect.top - margin - tooltipRect.height,
|
||||||
|
left: linkCenterX - tooltipRect.width / 2,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
top: linkCenterY - tooltipRect.height / 2,
|
||||||
|
left: linkRect.right + margin,
|
||||||
|
},
|
||||||
|
bottom: {
|
||||||
|
top: linkRect.bottom + margin,
|
||||||
|
left: linkCenterX - tooltipRect.width / 2,
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
top: linkCenterY - tooltipRect.height / 2,
|
||||||
|
left: linkRect.left - margin - tooltipRect.width,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
top: placements[placement].top + finalOffset.top,
|
||||||
|
left: placements[placement].left + finalOffset.left,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const $root = document.getElementById('root');
|
||||||
|
|
||||||
|
Modal.propTypes = propTypes;
|
||||||
|
Modal.defaultProps = defaultProps;
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
export { default as Avatar } from './Avatar';
|
export { default as Avatar } from './Avatar';
|
||||||
export { default as Button } from './Button';
|
export { default as Button } from './Button';
|
||||||
export { default as ConfirmModal } from './ConfirmModal';
|
export { default as ConfirmModal } from './ConfirmModal';
|
||||||
|
export { default as CopyLinkButton } from './CopyLinkButton';
|
||||||
export { default as DatePicker } from './DatePicker';
|
export { default as DatePicker } from './DatePicker';
|
||||||
|
export { default as Tooltip } from './Tooltip';
|
||||||
export { default as Icon } from './Icon';
|
export { default as Icon } from './Icon';
|
||||||
export { default as Input } from './Input';
|
export { default as Input } from './Input';
|
||||||
|
export { default as InputDebounced } from './InputDebounced';
|
||||||
|
export { default as IssueTypeIcon } from './IssueTypeIcon';
|
||||||
|
export { default as IssuePriorityIcon } from './IssuePriorityIcon';
|
||||||
export { default as Logo } from './Logo';
|
export { default as Logo } from './Logo';
|
||||||
export { default as Modal } from './Modal';
|
export { default as Modal } from './Modal';
|
||||||
export { default as PageError } from './PageError';
|
export { default as PageError } from './PageError';
|
||||||
@@ -12,3 +17,5 @@ export { default as ProjectAvatar } from './ProjectAvatar';
|
|||||||
export { default as Select } from './Select';
|
export { default as Select } from './Select';
|
||||||
export { default as Spinner } from './Spinner';
|
export { default as Spinner } from './Spinner';
|
||||||
export { default as Textarea } from './Textarea';
|
export { default as Textarea } from './Textarea';
|
||||||
|
export { default as TextEditedContent } from './TextEditedContent';
|
||||||
|
export { default as TextEditor } from './TextEditor';
|
||||||
|
|||||||
@@ -18,3 +18,10 @@ export const IssuePriority = {
|
|||||||
LOW: '2',
|
LOW: '2',
|
||||||
LOWEST: '1',
|
LOWEST: '1',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IssueStatusCopy = {
|
||||||
|
[IssueStatus.BACKLOG]: 'Backlog',
|
||||||
|
[IssueStatus.SELECTED]: 'Selected for development',
|
||||||
|
[IssueStatus.INPROGRESS]: 'In progress',
|
||||||
|
[IssueStatus.DONE]: 'Done',
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,54 +1,49 @@
|
|||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
|
||||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import api from 'shared/utils/api';
|
import api from 'shared/utils/api';
|
||||||
import useDeepCompareMemoize from './deepCompareMemoize';
|
import useDeepCompareMemoize from './deepCompareMemoize';
|
||||||
|
|
||||||
const useApi = (method, url, paramsOrData = {}, { lazy = false } = {}) => {
|
const useApi = (method, url, variables = {}, { lazy = false } = {}) => {
|
||||||
const isCalledAutomatically = method === 'get' && !lazy;
|
const isCalledAutomatically = method === 'get' && !lazy;
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
data: null,
|
data: null,
|
||||||
error: null,
|
error: null,
|
||||||
isLoading: isCalledAutomatically,
|
isLoading: isCalledAutomatically,
|
||||||
variables: {},
|
additionalVariables: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const setLocalData = useCallback(
|
const setStateMerge = newState => setState(currentState => ({ ...currentState, ...newState }));
|
||||||
set => setState(currentState => ({ ...currentState, data: set(currentState.data) })),
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const updateState = newState => setState(currentState => ({ ...currentState, ...newState }));
|
|
||||||
|
|
||||||
const wasCalledRef = useRef(false);
|
const wasCalledRef = useRef(false);
|
||||||
|
const variablesMemoized = useDeepCompareMemoize(variables);
|
||||||
|
|
||||||
const paramsOrDataMemoized = useDeepCompareMemoize(paramsOrData);
|
|
||||||
const stateRef = useRef();
|
const stateRef = useRef();
|
||||||
stateRef.current = state;
|
stateRef.current = state;
|
||||||
|
|
||||||
const makeRequest = useCallback(
|
const makeRequest = useCallback(
|
||||||
(newVariables = {}) =>
|
(newVariables = {}) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const variables = { ...stateRef.current.variables, ...newVariables };
|
const additionalVariables = { ...stateRef.current.additionalVariables, ...newVariables };
|
||||||
|
|
||||||
if (!isCalledAutomatically || wasCalledRef.current) {
|
if (!isCalledAutomatically || wasCalledRef.current) {
|
||||||
updateState({ variables, isLoading: true });
|
setStateMerge({ additionalVariables, isLoading: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
api[method](url, { ...paramsOrDataMemoized, ...variables }).then(
|
api[method](url, { ...variablesMemoized, ...additionalVariables }).then(
|
||||||
data => {
|
data => {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
updateState({ data, error: null, isLoading: false });
|
setStateMerge({ data, error: null, isLoading: false });
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
updateState({ error, data: null, isLoading: false });
|
setStateMerge({ error, data: null, isLoading: false });
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
wasCalledRef.current = true;
|
wasCalledRef.current = true;
|
||||||
}),
|
}),
|
||||||
[method, paramsOrDataMemoized, isCalledAutomatically, url],
|
[method, variablesMemoized, isCalledAutomatically, url],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -57,17 +52,26 @@ const useApi = (method, url, paramsOrData = {}, { lazy = false } = {}) => {
|
|||||||
}
|
}
|
||||||
}, [makeRequest, isCalledAutomatically]);
|
}, [makeRequest, isCalledAutomatically]);
|
||||||
|
|
||||||
return [
|
const setLocalData = useCallback(
|
||||||
|
getUpdatedData =>
|
||||||
|
setState(currentState => ({ ...currentState, data: getUpdatedData(currentState.data) })),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = [
|
||||||
{
|
{
|
||||||
...state,
|
...state,
|
||||||
wasCalled: wasCalledRef.current,
|
wasCalled: wasCalledRef.current,
|
||||||
variables: { ...paramsOrDataMemoized, ...state.variables },
|
variables: { ...variablesMemoized, ...state.additionalVariables },
|
||||||
setLocalData,
|
setLocalData,
|
||||||
},
|
},
|
||||||
makeRequest,
|
makeRequest,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
export default {
|
export default {
|
||||||
get: (...args) => useApi('get', ...args),
|
get: (...args) => useApi('get', ...args),
|
||||||
post: (...args) => useApi('post', ...args),
|
post: (...args) => useApi('post', ...args),
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
const useDebounceValue = (value, delay) => {
|
|
||||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handler = setTimeout(() => {
|
|
||||||
setDebouncedValue(value);
|
|
||||||
}, delay);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
clearTimeout(handler);
|
|
||||||
};
|
|
||||||
}, [value, delay]);
|
|
||||||
|
|
||||||
return debouncedValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useDebounceValue;
|
|
||||||
@@ -1,30 +1,40 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
const useOnOutsideClick = ($elementRef, isListening, onOutsideClick) => {
|
import useDeepCompareMemoize from 'shared/hooks/deepCompareMemoize';
|
||||||
|
|
||||||
|
const useOnOutsideClick = (
|
||||||
|
$ignoredElementRefs,
|
||||||
|
isListening,
|
||||||
|
onOutsideClick,
|
||||||
|
$listeningElementRef = {},
|
||||||
|
) => {
|
||||||
const $mouseDownTargetRef = useRef();
|
const $mouseDownTargetRef = useRef();
|
||||||
|
const $ignoredElementRefsMemoized = useDeepCompareMemoize([$ignoredElementRefs].flat());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMouseDown = event => {
|
const handleMouseDown = event => {
|
||||||
$mouseDownTargetRef.current = event.target;
|
$mouseDownTargetRef.current = event.target;
|
||||||
};
|
};
|
||||||
const handleMouseUp = event => {
|
const handleMouseUp = event => {
|
||||||
if (
|
const noElementsContainTarget = $ignoredElementRefsMemoized.every(
|
||||||
event.button === 0 &&
|
$elementRef =>
|
||||||
!$elementRef.current.contains($mouseDownTargetRef.current) &&
|
!$elementRef.current.contains($mouseDownTargetRef.current) &&
|
||||||
!$elementRef.current.contains(event.target)
|
!$elementRef.current.contains(event.target),
|
||||||
) {
|
);
|
||||||
|
if (event.button === 0 && noElementsContainTarget) {
|
||||||
onOutsideClick();
|
onOutsideClick();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const $listeningElement = $listeningElementRef.current || document;
|
||||||
if (isListening) {
|
if (isListening) {
|
||||||
document.addEventListener('mousedown', handleMouseDown);
|
$listeningElement.addEventListener('mousedown', handleMouseDown);
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
$listeningElement.addEventListener('mouseup', handleMouseUp);
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleMouseDown);
|
$listeningElement.removeEventListener('mousedown', handleMouseDown);
|
||||||
document.removeEventListener('mouseup', handleMouseUp);
|
$listeningElement.removeEventListener('mouseup', handleMouseUp);
|
||||||
};
|
};
|
||||||
}, [$elementRef, isListening, onOutsideClick]);
|
}, [$ignoredElementRefsMemoized, $listeningElementRef, isListening, onOutsideClick]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useOnOutsideClick;
|
export default useOnOutsideClick;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import history from 'browserHistory';
|
import history from 'browserHistory';
|
||||||
|
import toast from 'shared/utils/toast';
|
||||||
import { objectToQueryString } from 'shared/utils/url';
|
import { objectToQueryString } from 'shared/utils/url';
|
||||||
import { getStoredAuthToken, removeStoredAuthToken } from 'shared/utils/authToken';
|
import { getStoredAuthToken, removeStoredAuthToken } from 'shared/utils/authToken';
|
||||||
|
|
||||||
@@ -17,14 +18,14 @@ const defaults = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const api = (method, url, paramsOrData) =>
|
const api = (method, url, variables) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
axios({
|
axios({
|
||||||
url: `${defaults.baseURL}${url}`,
|
url: `${defaults.baseURL}${url}`,
|
||||||
method,
|
method,
|
||||||
headers: defaults.headers(),
|
headers: defaults.headers(),
|
||||||
params: method === 'get' ? paramsOrData : undefined,
|
params: method === 'get' ? variables : undefined,
|
||||||
data: method !== 'get' ? paramsOrData : undefined,
|
data: method !== 'get' ? variables : undefined,
|
||||||
paramsSerializer: objectToQueryString,
|
paramsSerializer: objectToQueryString,
|
||||||
}).then(
|
}).then(
|
||||||
response => {
|
response => {
|
||||||
@@ -45,10 +46,21 @@ const api = (method, url, paramsOrData) =>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const optimisticUpdate = async ({ url, updatedFields, currentFields, setLocalData }) => {
|
||||||
|
try {
|
||||||
|
setLocalData(updatedFields);
|
||||||
|
await api('put', url, updatedFields);
|
||||||
|
} catch (error) {
|
||||||
|
setLocalData(currentFields);
|
||||||
|
toast.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
get: (...args) => api('get', ...args),
|
get: (...args) => api('get', ...args),
|
||||||
post: (...args) => api('post', ...args),
|
post: (...args) => api('post', ...args),
|
||||||
put: (...args) => api('put', ...args),
|
put: (...args) => api('put', ...args),
|
||||||
patch: (...args) => api('patch', ...args),
|
patch: (...args) => api('patch', ...args),
|
||||||
delete: (...args) => api('delete', ...args),
|
delete: (...args) => api('delete', ...args),
|
||||||
|
optimisticUpdate,
|
||||||
};
|
};
|
||||||
|
|||||||
5
client/src/shared/utils/html.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const getTextContentsFromHtmlString = html => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.innerHTML = html;
|
||||||
|
return el.textContent;
|
||||||
|
};
|
||||||
@@ -11,12 +11,12 @@ export const insertItemIntoArray = (arr, item, index) => {
|
|||||||
return arrClone;
|
return arrClone;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateArrayItemById = (arr, itemId, newFields) => {
|
export const updateArrayItemById = (arr, itemId, fields) => {
|
||||||
const arrClone = [...arr];
|
const arrClone = [...arr];
|
||||||
const item = arrClone.find(({ id }) => id === itemId);
|
const item = arrClone.find(({ id }) => id === itemId);
|
||||||
const itemIndex = arrClone.indexOf(item);
|
if (item) {
|
||||||
if (itemIndex > -1) {
|
const itemIndex = arrClone.indexOf(item);
|
||||||
arrClone.splice(itemIndex, 1, { ...item, ...newFields });
|
arrClone.splice(itemIndex, 1, { ...item, ...fields });
|
||||||
}
|
}
|
||||||
return arrClone;
|
return arrClone;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import Color from 'color';
|
import Color from 'color';
|
||||||
|
import { IssueType, IssueStatus, IssuePriority } from 'shared/constants/issues';
|
||||||
|
|
||||||
export const color = {
|
export const color = {
|
||||||
primary: '#0052cc', // Blue
|
primary: '#0052cc', // Blue
|
||||||
success: '#29A638', // green
|
success: '#0B875B', // green
|
||||||
danger: '#E13C3C', // red
|
danger: '#E13C3C', // red
|
||||||
warning: '#F89C1C', // orange
|
warning: '#F89C1C', // orange
|
||||||
secondary: '#F4F5F7', // light grey
|
secondary: '#F4F5F7', // light grey
|
||||||
@@ -17,6 +18,8 @@ export const color = {
|
|||||||
backgroundMedium: '#dfe1e6',
|
backgroundMedium: '#dfe1e6',
|
||||||
backgroundLight: '#ebecf0',
|
backgroundLight: '#ebecf0',
|
||||||
backgroundLightest: '#F4F5F7',
|
backgroundLightest: '#F4F5F7',
|
||||||
|
backgroundLightPrimary: '#D2E5FE',
|
||||||
|
backgroundLightSuccess: '#E4FCEF',
|
||||||
|
|
||||||
borderLightest: '#dfe1e6',
|
borderLightest: '#dfe1e6',
|
||||||
borderLight: '#C1C7D0',
|
borderLight: '#C1C7D0',
|
||||||
@@ -24,17 +27,31 @@ export const color = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const issueTypeColors = {
|
export const issueTypeColors = {
|
||||||
story: '#65BA43', // green
|
[IssueType.TASK]: '#4FADE6', // blue
|
||||||
bug: '#E44D42', // red
|
[IssueType.BUG]: '#E44D42', // red
|
||||||
task: '#4FADE6', // blue
|
[IssueType.STORY]: '#65BA43', // green
|
||||||
};
|
};
|
||||||
|
|
||||||
export const issuePriorityColors = {
|
export const issuePriorityColors = {
|
||||||
'5': '#CD1317', // red
|
[IssuePriority.HIGHEST]: '#CD1317', // red
|
||||||
'4': '#E9494A', // orange
|
[IssuePriority.HIGH]: '#E9494A', // orange
|
||||||
'3': '#E97F33', // orange
|
[IssuePriority.MEDIUM]: '#E97F33', // orange
|
||||||
'2': '#2D8738', // green
|
[IssuePriority.LOW]: '#2D8738', // green
|
||||||
'1': '#57A55A', // green
|
[IssuePriority.LOWEST]: '#57A55A', // green
|
||||||
|
};
|
||||||
|
|
||||||
|
export const issueStatusColors = {
|
||||||
|
[IssueStatus.BACKLOG]: color.textDark,
|
||||||
|
[IssueStatus.INPROGRESS]: '#fff',
|
||||||
|
[IssueStatus.SELECTED]: color.textDark,
|
||||||
|
[IssueStatus.DONE]: '#fff',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const issueStatusBackgroundColors = {
|
||||||
|
[IssueStatus.BACKLOG]: color.backgroundMedium,
|
||||||
|
[IssueStatus.INPROGRESS]: color.primary,
|
||||||
|
[IssueStatus.SELECTED]: color.backgroundMedium,
|
||||||
|
[IssueStatus.DONE]: color.success,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sizes = {
|
export const sizes = {
|
||||||
@@ -73,10 +90,8 @@ export const mixin = {
|
|||||||
boxShadowMedium: `
|
boxShadowMedium: `
|
||||||
box-shadow: 0 5px 10px 0 rgba(0,0,0,0.1);
|
box-shadow: 0 5px 10px 0 rgba(0,0,0,0.1);
|
||||||
`,
|
`,
|
||||||
boxShadowBorderMedium: `
|
boxShadowDropdown: `
|
||||||
box-shadow: 0 5px 10px 0 rgba(0,0,0,0.1);
|
box-shadow: rgba(9, 30, 66, 0.25) 0px 4px 8px -2px, rgba(9, 30, 66, 0.31) 0px 0px 1px;
|
||||||
border: 1px solid ${color.borderLight};
|
|
||||||
border-top: 1px solid ${color.borderLightest};
|
|
||||||
`,
|
`,
|
||||||
truncateText: `
|
truncateText: `
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -161,4 +176,20 @@ export const mixin = {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
tag: (background = color.backgroundLight, colorValue = color.textDarkest) => `
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 8px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: ${colorValue};
|
||||||
|
background: ${background};
|
||||||
|
${font.bold}
|
||||||
|
${font.size(11.5)}
|
||||||
|
i {
|
||||||
|
margin-left: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const is = {
|
|||||||
'Must be a valid URL',
|
'Must be a valid URL',
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNilOrEmptyString = value => value === undefined || value === null || value === '';
|
export const isNilOrEmptyString = value => value === undefined || value === null || value === '';
|
||||||
|
|
||||||
export const generateErrors = (fieldValues, fieldValidators) => {
|
export const generateErrors = (fieldValues, fieldValidators) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ module.exports = {
|
|||||||
exclude: /node_modules/,
|
exclude: /node_modules/,
|
||||||
use: ['babel-loader'],
|
use: ['babel-loader'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: ['style-loader', { loader: 'css-loader' }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.(jpe?g|png|gif|woff2?|eot|ttf|otf|svg)$/,
|
test: /\.(jpe?g|png|gif|woff2?|eot|ttf|otf|svg)$/,
|
||||||
use: [
|
use: [
|
||||||
|
|||||||