Implemented issue drag and drop
This commit is contained in:
@@ -3,6 +3,7 @@ import express from 'express';
|
|||||||
import { Project } from 'entities';
|
import { Project } from 'entities';
|
||||||
import { catchErrors } from 'errors';
|
import { catchErrors } from 'errors';
|
||||||
import { findEntityOrThrow, updateEntity } from 'utils/typeorm';
|
import { findEntityOrThrow, updateEntity } from 'utils/typeorm';
|
||||||
|
import { issuePartial } from 'serializers/issues';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
@@ -10,9 +11,14 @@ router.get(
|
|||||||
'/project',
|
'/project',
|
||||||
catchErrors(async (req, res) => {
|
catchErrors(async (req, res) => {
|
||||||
const project = await findEntityOrThrow(Project, req.currentUser.projectId, {
|
const project = await findEntityOrThrow(Project, req.currentUser.projectId, {
|
||||||
relations: ['users', 'issues', 'issues.comments'],
|
relations: ['users', 'issues'],
|
||||||
|
});
|
||||||
|
res.respond({
|
||||||
|
project: {
|
||||||
|
...project,
|
||||||
|
issues: project.issues.map(issuePartial),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
res.respond({ project });
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ const seedProjects = (users: User[]): Promise<Project[]> => {
|
|||||||
const seedIssues = (projects: Project[]): Promise<Issue[]> => {
|
const seedIssues = (projects: Project[]): Promise<Issue[]> => {
|
||||||
const issues = projects
|
const issues = projects
|
||||||
.map(project =>
|
.map(project =>
|
||||||
times(10, () =>
|
times(10, i =>
|
||||||
createEntity(
|
createEntity(
|
||||||
Issue,
|
Issue,
|
||||||
generateIssue({
|
generateIssue({
|
||||||
|
listPosition: i + 1,
|
||||||
reporterId: (sample(project.users) as User).id,
|
reporterId: (sample(project.users) as User).id,
|
||||||
project,
|
project,
|
||||||
users: [sample(project.users) as User],
|
users: [sample(project.users) as User],
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
type: IssueType.TASK,
|
type: IssueType.TASK,
|
||||||
status: IssueStatus.BACKLOG,
|
status: IssueStatus.BACKLOG,
|
||||||
priority: IssuePriority.LOWEST,
|
priority: IssuePriority.LOWEST,
|
||||||
|
listPosition: 1,
|
||||||
estimate: 8,
|
estimate: 8,
|
||||||
reporterId: getRandomUser().id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
@@ -50,6 +51,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
type: IssueType.TASK,
|
type: IssueType.TASK,
|
||||||
status: IssueStatus.BACKLOG,
|
status: IssueStatus.BACKLOG,
|
||||||
priority: IssuePriority.LOW,
|
priority: IssuePriority.LOW,
|
||||||
|
listPosition: 2,
|
||||||
description: 'Nothing in particular.',
|
description: 'Nothing in particular.',
|
||||||
estimate: 40,
|
estimate: 40,
|
||||||
reporterId: getRandomUser().id,
|
reporterId: getRandomUser().id,
|
||||||
@@ -60,6 +62,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
type: IssueType.BUG,
|
type: IssueType.BUG,
|
||||||
status: IssueStatus.BACKLOG,
|
status: IssueStatus.BACKLOG,
|
||||||
priority: IssuePriority.MEDIUM,
|
priority: IssuePriority.MEDIUM,
|
||||||
|
listPosition: 3,
|
||||||
estimate: 15,
|
estimate: 15,
|
||||||
reporterId: getRandomUser().id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
@@ -70,6 +73,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
type: IssueType.STORY,
|
type: IssueType.STORY,
|
||||||
status: IssueStatus.BACKLOG,
|
status: IssueStatus.BACKLOG,
|
||||||
priority: IssuePriority.HIGH,
|
priority: IssuePriority.HIGH,
|
||||||
|
listPosition: 4,
|
||||||
description:
|
description:
|
||||||
"#### Colons can be used to align columns.\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | |\n| col 2 is | centered | |\n| zebra stripes | are neat | |\n\nThe outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.\n\nMarkdown | Less | Pretty\n--- | --- | ---\n*Still* | `renders` | **nicely**\n1 | 2 | 3",
|
"#### Colons can be used to align columns.\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | |\n| col 2 is | centered | |\n| zebra stripes | are neat | |\n\nThe outer pipes (|) are optional, and you don't need to make the raw Markdown line up prettily. You can also use inline Markdown.\n\nMarkdown | Less | Pretty\n--- | --- | ---\n*Still* | `renders` | **nicely**\n1 | 2 | 3",
|
||||||
estimate: 4,
|
estimate: 4,
|
||||||
@@ -82,6 +86,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
type: IssueType.TASK,
|
type: IssueType.TASK,
|
||||||
status: IssueStatus.SELECTED,
|
status: IssueStatus.SELECTED,
|
||||||
priority: IssuePriority.HIGHEST,
|
priority: IssuePriority.HIGHEST,
|
||||||
|
listPosition: 5,
|
||||||
estimate: 15,
|
estimate: 15,
|
||||||
reporterId: getRandomUser().id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
@@ -91,6 +96,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
type: IssueType.STORY,
|
type: IssueType.STORY,
|
||||||
status: IssueStatus.SELECTED,
|
status: IssueStatus.SELECTED,
|
||||||
priority: IssuePriority.MEDIUM,
|
priority: IssuePriority.MEDIUM,
|
||||||
|
listPosition: 6,
|
||||||
estimate: 55,
|
estimate: 55,
|
||||||
reporterId: getRandomUser().id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
@@ -101,6 +107,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
|
|||||||
type: IssueType.TASK,
|
type: IssueType.TASK,
|
||||||
status: IssueStatus.SELECTED,
|
status: IssueStatus.SELECTED,
|
||||||
priority: IssuePriority.MEDIUM,
|
priority: IssuePriority.MEDIUM,
|
||||||
|
listPosition: 7,
|
||||||
estimate: 12,
|
estimate: 12,
|
||||||
reporterId: getRandomUser().id,
|
reporterId: getRandomUser().id,
|
||||||
project,
|
project,
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class Issue extends BaseEntity {
|
|||||||
@Column('varchar')
|
@Column('varchar')
|
||||||
priority: IssuePriority;
|
priority: IssuePriority;
|
||||||
|
|
||||||
|
@Column('double precision')
|
||||||
|
listPosition: number;
|
||||||
|
|
||||||
@Column('text', { nullable: true })
|
@Column('text', { nullable: true })
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
|
||||||
|
|||||||
16
api/src/serializers/issues.ts
Normal file
16
api/src/serializers/issues.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { pick } from 'lodash';
|
||||||
|
|
||||||
|
import { Issue } from 'entities';
|
||||||
|
|
||||||
|
export const issuePartial = (issue: Issue): Partial<Issue> =>
|
||||||
|
pick(issue, [
|
||||||
|
'id',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
'status',
|
||||||
|
'priority',
|
||||||
|
'listPosition',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'userIds',
|
||||||
|
]);
|
||||||
74
client/package-lock.json
generated
74
client/package-lock.json
generated
@@ -891,6 +891,22 @@
|
|||||||
"regenerator-runtime": "^0.13.2"
|
"regenerator-runtime": "^0.13.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@babel/runtime-corejs2": {
|
||||||
|
"version": "7.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.7.6.tgz",
|
||||||
|
"integrity": "sha512-QYp/8xdH8iMin3pH5gtT/rUuttVfIcOhWBC3wh9Eh/qs4jEe39+3DpCDLgWXhMQgiCTOH8mrLSvQ0OHOCcox9g==",
|
||||||
|
"requires": {
|
||||||
|
"core-js": "^2.6.5",
|
||||||
|
"regenerator-runtime": "^0.13.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"core-js": {
|
||||||
|
"version": "2.6.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||||
|
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/runtime-corejs3": {
|
"@babel/runtime-corejs3": {
|
||||||
"version": "7.7.4",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.7.4.tgz",
|
||||||
@@ -2570,6 +2586,14 @@
|
|||||||
"randomfill": "^1.0.3"
|
"randomfill": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"css-box-model": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-lri0br+jSNV0kkkiGEp9y9y3Njq2PmpqbeGWRFQJuZteZzY9iC9GZhQ8Y4WpPwM/2YocjHePxy14igJY7YKzkA==",
|
||||||
|
"requires": {
|
||||||
|
"tiny-invariant": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"css-color-keywords": {
|
"css-color-keywords": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"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",
|
||||||
@@ -5415,7 +5439,6 @@
|
|||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -7554,6 +7577,11 @@
|
|||||||
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
|
"integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"raf-schd": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-VhlMZmGy6A6hrkJWHLNTGl5gtgMUm+xfGza6wbwnE914yeQ5Ybm18vgM734RZhMgfw4tacUrWseGZlpUrrakEQ=="
|
||||||
|
},
|
||||||
"randombytes": {
|
"randombytes": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||||
@@ -7609,6 +7637,20 @@
|
|||||||
"prop-types": "^15.6.2"
|
"prop-types": "^15.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-beautiful-dnd": {
|
||||||
|
"version": "12.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-12.2.0.tgz",
|
||||||
|
"integrity": "sha512-s5UrOXNDgeEC+sx65IgbeFlqKKgK3c0UfbrJLWufP34WBheyu5kJ741DtJbsSgPKyNLkqfswpMYr0P8lRj42cA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime-corejs2": "^7.6.3",
|
||||||
|
"css-box-model": "^1.2.0",
|
||||||
|
"memoize-one": "^5.1.1",
|
||||||
|
"raf-schd": "^4.0.2",
|
||||||
|
"react-redux": "^7.1.1",
|
||||||
|
"redux": "^4.0.4",
|
||||||
|
"use-memo-one": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"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",
|
||||||
@@ -7625,6 +7667,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz",
|
||||||
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
|
"integrity": "sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q=="
|
||||||
},
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"version": "7.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.3.tgz",
|
||||||
|
"integrity": "sha512-uI1wca+ECG9RoVkWQFF4jDMqmaw0/qnvaSvOoL/GA4dNxf6LoV8sUAcNDvE5NWKs4hFpn0t6wswNQnY3f7HT3w==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"hoist-non-react-statics": "^3.3.0",
|
||||||
|
"invariant": "^2.2.4",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-is": "^16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-router": {
|
"react-router": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz",
|
||||||
@@ -7805,6 +7860,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"redux": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==",
|
||||||
|
"requires": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"symbol-observable": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"regenerate": {
|
"regenerate": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
||||||
@@ -8972,8 +9036,7 @@
|
|||||||
"symbol-observable": {
|
"symbol-observable": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||||
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
|
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
"version": "5.4.6",
|
"version": "5.4.6",
|
||||||
@@ -9438,6 +9501,11 @@
|
|||||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"use-memo-one": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-oFfsyun+bP7RX8X2AskHNTxu+R3QdE/RC5IefMbqptmACAA/gfol1KDD5KRzPsGMa62sWxGZw+Ui43u6x4ddoQ=="
|
||||||
|
},
|
||||||
"util": {
|
"util": {
|
||||||
"version": "0.11.1",
|
"version": "0.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^6.9.0",
|
"query-string": "^6.9.0",
|
||||||
"react": "^16.12.0",
|
"react": "^16.12.0",
|
||||||
|
"react-beautiful-dnd": "^12.2.0",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import { xor, debounce } from 'lodash';
|
||||||
import { intersection, xor } from 'lodash';
|
|
||||||
|
|
||||||
import useDebounceValue from 'shared/hooks/debounceValue';
|
|
||||||
import {
|
import {
|
||||||
Filters,
|
Filters,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
@@ -15,73 +13,53 @@ import {
|
|||||||
} from './Styles';
|
} from './Styles';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
project: PropTypes.object.isRequired,
|
projectUsers: PropTypes.array.isRequired,
|
||||||
currentUser: PropTypes.object.isRequired,
|
defaultFilters: PropTypes.object.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
filters: PropTypes.object.isRequired,
|
||||||
|
setFilters: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectBoardFilters = ({ project, currentUser, onChange }) => {
|
const ProjectBoardFilters = ({ projectUsers, defaultFilters, filters, setFilters }) => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const { searchQuery, userIds, myOnly, recent } = filters;
|
||||||
const [userIds, setUserIds] = useState([]);
|
|
||||||
const [myOnly, setMyOnly] = useState(false);
|
|
||||||
const [recent, setRecent] = useState(false);
|
|
||||||
const debouncedSearchQuery = useDebounceValue(searchQuery, 500);
|
|
||||||
|
|
||||||
const clearFilters = () => {
|
const setFiltersMerge = newFilters => setFilters({ ...filters, ...newFilters });
|
||||||
setSearchQuery('');
|
|
||||||
setUserIds([]);
|
|
||||||
setMyOnly(false);
|
|
||||||
setRecent(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const areFiltersCleared = !searchQuery && userIds.length === 0 && !myOnly && !recent;
|
const areFiltersCleared = !searchQuery && userIds.length === 0 && !myOnly && !recent;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const getFilteredIssues = () => {
|
|
||||||
let { issues } = project;
|
|
||||||
|
|
||||||
if (debouncedSearchQuery) {
|
|
||||||
issues = issues.filter(issue =>
|
|
||||||
issue.title.toLowerCase().includes(debouncedSearchQuery.toLowerCase()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (userIds.length > 0) {
|
|
||||||
issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0);
|
|
||||||
}
|
|
||||||
if (myOnly) {
|
|
||||||
issues = issues.filter(issue => issue.userIds.includes(currentUser.id));
|
|
||||||
}
|
|
||||||
if (recent) {
|
|
||||||
issues = issues.filter(issue =>
|
|
||||||
moment(issue.updatedAt).isAfter(moment().subtract(3, 'days')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return issues;
|
|
||||||
};
|
|
||||||
onChange(getFilteredIssues());
|
|
||||||
}, [project, currentUser, onChange, debouncedSearchQuery, userIds, myOnly, recent]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Filters>
|
<Filters>
|
||||||
<SearchInput icon="search" value={searchQuery} onChange={setSearchQuery} />
|
<SearchInput
|
||||||
|
icon="search"
|
||||||
|
onChange={debounce(value => setFiltersMerge({ searchQuery: value }), 500)}
|
||||||
|
/>
|
||||||
<Avatars>
|
<Avatars>
|
||||||
{project.users.map(user => (
|
{projectUsers.map(user => (
|
||||||
<AvatarIsActiveBorder key={user.id} isActive={userIds.includes(user.id)}>
|
<AvatarIsActiveBorder key={user.id} isActive={userIds.includes(user.id)}>
|
||||||
<StyledAvatar
|
<StyledAvatar
|
||||||
avatarUrl={user.avatarUrl}
|
avatarUrl={user.avatarUrl}
|
||||||
name={user.name}
|
name={user.name}
|
||||||
onClick={() => setUserIds(value => xor(value, [user.id]))}
|
onClick={() => setFiltersMerge({ userIds: xor(userIds, [user.id]) })}
|
||||||
/>
|
/>
|
||||||
</AvatarIsActiveBorder>
|
</AvatarIsActiveBorder>
|
||||||
))}
|
))}
|
||||||
</Avatars>
|
</Avatars>
|
||||||
<StyledButton color="empty" isActive={myOnly} onClick={() => setMyOnly(!myOnly)}>
|
<StyledButton
|
||||||
|
color="empty"
|
||||||
|
isActive={myOnly}
|
||||||
|
onClick={() => setFiltersMerge({ myOnly: !myOnly })}
|
||||||
|
>
|
||||||
Only My Issues
|
Only My Issues
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
<StyledButton color="empty" isActive={recent} onClick={() => setRecent(!recent)}>
|
<StyledButton
|
||||||
|
color="empty"
|
||||||
|
isActive={recent}
|
||||||
|
onClick={() => setFiltersMerge({ recent: !recent })}
|
||||||
|
>
|
||||||
Recently Updated
|
Recently Updated
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
{!areFiltersCleared && <ClearAll onClick={clearFilters}>Clear all</ClearAll>}
|
{!areFiltersCleared && (
|
||||||
|
<ClearAll onClick={() => setFilters(defaultFilters)}>Clear all</ClearAll>
|
||||||
|
)}
|
||||||
</Filters>
|
</Filters>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
61
client/src/components/Project/Board/Lists/Issue/Styles.js
Normal file
61
client/src/components/Project/Board/Lists/Issue/Styles.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
|
import { Avatar, Icon } from 'shared/components';
|
||||||
|
import { color, issueTypeColors, issuePriorityColors, font, mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
|
export const IssueWrapper = styled.div`
|
||||||
|
margin-bottom: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Issue = styled.div`
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0px 1px 2px 0px rgba(9, 30, 66, 0.25);
|
||||||
|
transition: background 0.1s;
|
||||||
|
${mixin.clickable}
|
||||||
|
&:hover {
|
||||||
|
background: ${color.backgroundLight};
|
||||||
|
}
|
||||||
|
${props =>
|
||||||
|
props.isBeingDragged &&
|
||||||
|
css`
|
||||||
|
transform: rotate(3deg);
|
||||||
|
box-shadow: 5px 10px 30px 0px rgba(9, 30, 66, 0.15);
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Title = styled.p`
|
||||||
|
padding-bottom: 11px;
|
||||||
|
${font.size(15)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Bottom = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
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`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
margin-left: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const AssigneeAvatar = styled(Avatar)`
|
||||||
|
margin-left: -2px;
|
||||||
|
box-shadow: 0 0 0 2px #fff;
|
||||||
|
`;
|
||||||
67
client/src/components/Project/Board/Lists/Issue/index.jsx
Normal file
67
client/src/components/Project/Board/Lists/Issue/index.jsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Draggable } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { IssuePriority } from 'shared/constants/issues';
|
||||||
|
import {
|
||||||
|
IssueWrapper,
|
||||||
|
Issue,
|
||||||
|
Title,
|
||||||
|
Bottom,
|
||||||
|
TypeIcon,
|
||||||
|
PriorityIcon,
|
||||||
|
Assignees,
|
||||||
|
AssigneeAvatar,
|
||||||
|
} from './Styles';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
projectUsers: PropTypes.array.isRequired,
|
||||||
|
issue: PropTypes.object.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardListsIssue = ({ projectUsers, issue, index }) => {
|
||||||
|
const getUserById = userId => projectUsers.find(user => user.id === userId);
|
||||||
|
|
||||||
|
const assignees = issue.userIds.map(getUserById);
|
||||||
|
|
||||||
|
const priorityIconType = [IssuePriority.LOW || IssuePriority.LOWEST].includes(issue.priority)
|
||||||
|
? 'arrow-down'
|
||||||
|
: 'arrow-up';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Draggable draggableId={issue.id.toString()} index={index}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<IssueWrapper
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
>
|
||||||
|
<Issue isBeingDragged={snapshot.isDragging && !snapshot.isDropAnimating}>
|
||||||
|
<Title>{issue.title}</Title>
|
||||||
|
<Bottom>
|
||||||
|
<div>
|
||||||
|
<TypeIcon type={issue.type} color={issue.type} />
|
||||||
|
<PriorityIcon type={priorityIconType} color={issue.priority} />
|
||||||
|
</div>
|
||||||
|
<Assignees>
|
||||||
|
{assignees.map(user => (
|
||||||
|
<AssigneeAvatar
|
||||||
|
key={user.id}
|
||||||
|
size={24}
|
||||||
|
avatarUrl={user.avatarUrl}
|
||||||
|
name={user.name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Assignees>
|
||||||
|
</Bottom>
|
||||||
|
</Issue>
|
||||||
|
</IssueWrapper>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProjectBoardListsIssue.propTypes = propTypes;
|
||||||
|
|
||||||
|
export default ProjectBoardListsIssue;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { Avatar, Icon } from 'shared/components';
|
import { color, font } from 'shared/utils/styles';
|
||||||
import { color, issueTypeColors, issuePriorityColors, font, mixin } from 'shared/utils/styles';
|
|
||||||
|
|
||||||
export const Lists = styled.div`
|
export const Lists = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -9,72 +8,28 @@ export const Lists = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const List = styled.div`
|
export const List = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
|
min-height: 400px;
|
||||||
width: 25%;
|
width: 25%;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: ${color.backgroundLightest};
|
background: ${color.backgroundLightest};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ListTitle = styled.div`
|
export const Title = styled.div`
|
||||||
padding: 13px 10px 17px;
|
padding: 13px 10px 17px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: ${color.textMedium};
|
color: ${color.textMedium};
|
||||||
${font.size(12.5)};
|
${font.size(12.5)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ListIssuesCount = styled.span`
|
export const IssuesCount = styled.span`
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
${font.size(13)};
|
${font.size(13)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Issues = styled.div`
|
export const Issues = styled.div`
|
||||||
|
height: 100%;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Issue = styled.div`
|
|
||||||
margin-bottom: 5px;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0px 1px 2px 0px rgba(9, 30, 66, 0.25);
|
|
||||||
transition: background 0.1s;
|
|
||||||
${mixin.clickable}
|
|
||||||
&:hover {
|
|
||||||
background: ${color.backgroundLight};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IssueTitle = styled.p`
|
|
||||||
padding-bottom: 11px;
|
|
||||||
${font.size(15)}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IssueBottom = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IssueTypeIcon = styled(Icon)`
|
|
||||||
font-size: 19px;
|
|
||||||
color: ${props => issueTypeColors[props.color]};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IssuePriorityIcon = styled(Icon)`
|
|
||||||
position: relative;
|
|
||||||
top: -1px;
|
|
||||||
margin-left: 4px;
|
|
||||||
font-size: 18px;
|
|
||||||
color: ${props => issuePriorityColors[props.color]};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IssueAssignees = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
margin-left: 2px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const IssueAssigneeAvatar = styled(Avatar)`
|
|
||||||
margin-left: -2px;
|
|
||||||
box-shadow: 0 0 0 2px #fff;
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,32 +1,70 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
||||||
|
import { intersection } from 'lodash';
|
||||||
|
|
||||||
import { IssueStatus, IssuePriority } from 'shared/constants/issues';
|
import api from 'shared/utils/api';
|
||||||
import {
|
import {
|
||||||
Lists,
|
moveItemWithinArray,
|
||||||
List,
|
insertItemIntoArray,
|
||||||
ListTitle,
|
updateArrayItemById,
|
||||||
ListIssuesCount,
|
} from 'shared/utils/javascript';
|
||||||
Issues,
|
import { IssueStatus } from 'shared/constants/issues';
|
||||||
Issue,
|
import Issue from './Issue';
|
||||||
IssueTitle,
|
import { Lists, List, Title, IssuesCount, Issues } from './Styles';
|
||||||
IssueBottom,
|
|
||||||
IssueTypeIcon,
|
|
||||||
IssuePriorityIcon,
|
|
||||||
IssueAssignees,
|
|
||||||
IssueAssigneeAvatar,
|
|
||||||
} from './Styles';
|
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
project: PropTypes.object.isRequired,
|
project: PropTypes.object.isRequired,
|
||||||
filteredIssues: PropTypes.array.isRequired,
|
filters: PropTypes.object.isRequired,
|
||||||
|
currentUserId: PropTypes.number,
|
||||||
|
setLocalProjectData: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectBoardLists = ({ project, filteredIssues }) => {
|
const defaultProps = {
|
||||||
|
currentUserId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectBoardLists = ({ project, filters, currentUserId, setLocalProjectData }) => {
|
||||||
|
const filteredIssues = filterIssues(project.issues, filters, currentUserId);
|
||||||
|
|
||||||
|
const handleIssueDrop = ({ draggableId, destination, source }) => {
|
||||||
|
if (!destination) return;
|
||||||
|
|
||||||
|
const isSameList = destination.droppableId === source.droppableId;
|
||||||
|
const isSamePosition = destination.index === source.index;
|
||||||
|
|
||||||
|
if (isSameList && isSamePosition) return;
|
||||||
|
|
||||||
|
const issueId = parseInt(draggableId);
|
||||||
|
|
||||||
|
const { prevIssue, nextIssue } = getAfterDropPrevNextIssue(
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
api.put(`/issues/${issueId}`, issueFieldsToUpdate);
|
||||||
|
};
|
||||||
|
|
||||||
const renderList = status => {
|
const renderList = status => {
|
||||||
const getListIssues = issues => issues.filter(issue => issue.status === status);
|
const filteredListIssues = getSortedListIssues(filteredIssues, status);
|
||||||
const allListIssues = getListIssues(project.issues);
|
const allListIssues = getSortedListIssues(project.issues, status);
|
||||||
const filteredListIssues = getListIssues(filteredIssues);
|
|
||||||
|
|
||||||
const issuesCount =
|
const issuesCount =
|
||||||
allListIssues.length !== filteredListIssues.length
|
allListIssues.length !== filteredListIssues.length
|
||||||
@@ -34,50 +72,81 @@ const ProjectBoardLists = ({ project, filteredIssues }) => {
|
|||||||
: allListIssues.length;
|
: allListIssues.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List key={status}>
|
<Droppable key={status} droppableId={status}>
|
||||||
<ListTitle>
|
{provided => (
|
||||||
{`${issueStatusCopy[status]} `}
|
<List>
|
||||||
<ListIssuesCount>{issuesCount}</ListIssuesCount>
|
<Title>
|
||||||
</ListTitle>
|
{`${issueStatusCopy[status]} `}
|
||||||
<Issues>{filteredListIssues.map(renderIssue)}</Issues>
|
<IssuesCount>{issuesCount}</IssuesCount>
|
||||||
</List>
|
</Title>
|
||||||
|
<Issues {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{filteredListIssues.map((issue, i) => (
|
||||||
|
<Issue key={issue.id} projectUsers={project.users} issue={issue} index={i} />
|
||||||
|
))}
|
||||||
|
{provided.placeholder}
|
||||||
|
</Issues>
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderIssue = issue => {
|
return (
|
||||||
const getUserById = userId => project.users.find(user => user.id === userId);
|
<DragDropContext onDragEnd={handleIssueDrop}>
|
||||||
const assignees = issue.userIds.map(getUserById);
|
<Lists>{Object.values(IssueStatus).map(renderList)}</Lists>
|
||||||
return (
|
</DragDropContext>
|
||||||
<Issue key={issue.id}>
|
);
|
||||||
<IssueTitle>{issue.title}</IssueTitle>
|
};
|
||||||
<IssueBottom>
|
|
||||||
<div>
|
|
||||||
<IssueTypeIcon type={issue.type} color={issue.type} />
|
|
||||||
<IssuePriorityIcon
|
|
||||||
type={
|
|
||||||
[IssuePriority.LOW || IssuePriority.LOWEST].includes(issue.priority)
|
|
||||||
? 'arrow-down'
|
|
||||||
: 'arrow-up'
|
|
||||||
}
|
|
||||||
color={issue.priority}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<IssueAssignees>
|
|
||||||
{assignees.map(user => (
|
|
||||||
<IssueAssigneeAvatar
|
|
||||||
key={user.id}
|
|
||||||
size={24}
|
|
||||||
avatarUrl={user.avatarUrl}
|
|
||||||
name={user.name}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</IssueAssignees>
|
|
||||||
</IssueBottom>
|
|
||||||
</Issue>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Lists>{Object.values(IssueStatus).map(renderList)}</Lists>;
|
const filterIssues = (projectIssues, filters, currentUserId) => {
|
||||||
|
let issues = projectIssues;
|
||||||
|
const { searchQuery, userIds, myOnly, recent } = filters;
|
||||||
|
|
||||||
|
if (searchQuery) {
|
||||||
|
issues = issues.filter(issue => issue.title.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
|
}
|
||||||
|
if (userIds.length > 0) {
|
||||||
|
issues = issues.filter(issue => intersection(issue.userIds, userIds).length > 0);
|
||||||
|
}
|
||||||
|
if (myOnly && currentUserId) {
|
||||||
|
issues = issues.filter(issue => issue.userIds.includes(currentUserId));
|
||||||
|
}
|
||||||
|
if (recent) {
|
||||||
|
issues = issues.filter(issue => moment(issue.updatedAt).isAfter(moment().subtract(3, 'days')));
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortedListIssues = (issues, status) =>
|
||||||
|
issues.filter(issue => issue.status === status).sort((a, b) => a.listPosition - b.listPosition);
|
||||||
|
|
||||||
|
const calculateListPosition = (prevIssue, nextIssue) => {
|
||||||
|
let position;
|
||||||
|
|
||||||
|
if (!prevIssue && !nextIssue) {
|
||||||
|
position = 1;
|
||||||
|
} else if (!prevIssue) {
|
||||||
|
position = nextIssue.listPosition - 1;
|
||||||
|
} else if (!nextIssue) {
|
||||||
|
position = prevIssue.listPosition + 1;
|
||||||
|
} else {
|
||||||
|
position = prevIssue.listPosition + (nextIssue.listPosition - prevIssue.listPosition) / 2;
|
||||||
|
}
|
||||||
|
return position;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAfterDropPrevNextIssue = (allIssues, destination, isSameList, droppedIssueId) => {
|
||||||
|
const destinationIssues = getSortedListIssues(allIssues, destination.droppableId);
|
||||||
|
const droppedIssue = allIssues.find(issue => issue.id === droppedIssueId);
|
||||||
|
|
||||||
|
const afterDropDestinationIssues = isSameList
|
||||||
|
? moveItemWithinArray(destinationIssues, droppedIssue, destination.index)
|
||||||
|
: insertItemIntoArray(destinationIssues, droppedIssue, destination.index);
|
||||||
|
|
||||||
|
return {
|
||||||
|
prevIssue: afterDropDestinationIssues[destination.index - 1],
|
||||||
|
nextIssue: afterDropDestinationIssues[destination.index + 1],
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueStatusCopy = {
|
const issueStatusCopy = {
|
||||||
@@ -88,5 +157,6 @@ const issueStatusCopy = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ProjectBoardLists.propTypes = propTypes;
|
ProjectBoardLists.propTypes = propTypes;
|
||||||
|
ProjectBoardLists.defaultProps = defaultProps;
|
||||||
|
|
||||||
export default ProjectBoardLists;
|
export default ProjectBoardLists;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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 useApi from 'shared/hooks/api';
|
import useApi from 'shared/hooks/api';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
@@ -8,21 +9,31 @@ import Lists from './Lists';
|
|||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
project: PropTypes.object.isRequired,
|
project: PropTypes.object.isRequired,
|
||||||
|
setLocalProjectData: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProjectBoard = ({ project }) => {
|
const defaultFilters = { searchQuery: '', userIds: [], myOnly: false, recent: false };
|
||||||
const [filteredIssues, setFilteredIssues] = useState([]);
|
|
||||||
|
const ProjectBoard = ({ project, setLocalProjectData }) => {
|
||||||
|
const [filters, setFilters] = useState(defaultFilters);
|
||||||
|
|
||||||
const [{ data }] = useApi.get('/currentUser');
|
const [{ data }] = useApi.get('/currentUser');
|
||||||
|
|
||||||
const { currentUser } = data || {};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header projectName={project.name} />
|
<Header projectName={project.name} />
|
||||||
{currentUser && (
|
<Filters
|
||||||
<Filters project={project} currentUser={currentUser} onChange={setFilteredIssues} />
|
projectUsers={project.users}
|
||||||
)}
|
defaultFilters={defaultFilters}
|
||||||
<Lists project={project} filteredIssues={filteredIssues} />
|
filters={filters}
|
||||||
|
setFilters={setFilters}
|
||||||
|
/>
|
||||||
|
<Lists
|
||||||
|
project={project}
|
||||||
|
filters={filters}
|
||||||
|
currentUserId={get(data, 'currentUser.id')}
|
||||||
|
setLocalProjectData={setLocalProjectData}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,16 +7,17 @@ import Board from './Board';
|
|||||||
import { ProjectPage } from './Styles';
|
import { ProjectPage } from './Styles';
|
||||||
|
|
||||||
const Project = () => {
|
const Project = () => {
|
||||||
const [{ data, error, isLoading }] = useApi.get('/project');
|
const [{ data, error, setLocalData: setLocalProjectData }] = useApi.get('/project');
|
||||||
|
|
||||||
if (isLoading) return <PageLoader />;
|
if (!data) return <PageLoader />;
|
||||||
if (error) return <PageError />;
|
if (error) return <PageError />;
|
||||||
|
|
||||||
const { project } = data;
|
const { project } = data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectPage>
|
<ProjectPage>
|
||||||
<Sidebar projectName={project.name} />
|
<Sidebar projectName={project.name} />
|
||||||
<Board project={project} />
|
<Board project={project} setLocalProjectData={setLocalProjectData} />
|
||||||
</ProjectPage>
|
</ProjectPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const Modal = ({
|
|||||||
const modalIdRef = useRef(uniqueIncreasingIntegerId());
|
const modalIdRef = useRef(uniqueIncreasingIntegerId());
|
||||||
|
|
||||||
const closeModal = useCallback(() => {
|
const closeModal = useCallback(() => {
|
||||||
if (shouldNotCloseBecauseHasOpenChildModal(modalIdRef.current)) {
|
if (hasChildModal(modalIdRef.current)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isControlled) {
|
if (!isControlled) {
|
||||||
@@ -80,8 +80,7 @@ const getIdsOfAllOpenModals = () => {
|
|||||||
return $modalNodes.map($node => parseInt($node.getAttribute('data-jira-modal-id')));
|
return $modalNodes.map($node => parseInt($node.getAttribute('data-jira-modal-id')));
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldNotCloseBecauseHasOpenChildModal = modalId =>
|
const hasChildModal = modalId => getIdsOfAllOpenModals().some(id => id > modalId);
|
||||||
getIdsOfAllOpenModals().some(id => id > modalId);
|
|
||||||
|
|
||||||
const setBodyScrollLock = () => {
|
const setBodyScrollLock = () => {
|
||||||
const areAnyModalsOpen = getIdsOfAllOpenModals().length > 0;
|
const areAnyModalsOpen = getIdsOfAllOpenModals().length > 0;
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ const useApi = (method, url, paramsOrData = {}, { lazy = false } = {}) => {
|
|||||||
variables: {},
|
variables: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const setLocalData = useCallback(
|
||||||
|
set => setState(currentState => ({ ...currentState, data: set(currentState.data) })),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const updateState = newState => setState(currentState => ({ ...currentState, ...newState }));
|
||||||
|
|
||||||
const wasCalledRef = useRef(false);
|
const wasCalledRef = useRef(false);
|
||||||
|
|
||||||
const paramsOrDataMemoized = useDeepCompareMemoize(paramsOrData);
|
const paramsOrDataMemoized = useDeepCompareMemoize(paramsOrData);
|
||||||
@@ -23,7 +29,6 @@ const useApi = (method, url, paramsOrData = {}, { lazy = false } = {}) => {
|
|||||||
const makeRequest = useCallback(
|
const makeRequest = useCallback(
|
||||||
(newVariables = {}) =>
|
(newVariables = {}) =>
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
const updateState = newState => setState({ ...stateRef.current, ...newState });
|
|
||||||
const variables = { ...stateRef.current.variables, ...newVariables };
|
const variables = { ...stateRef.current.variables, ...newVariables };
|
||||||
|
|
||||||
if (!isCalledAutomatically || wasCalledRef.current) {
|
if (!isCalledAutomatically || wasCalledRef.current) {
|
||||||
@@ -57,6 +62,7 @@ const useApi = (method, url, paramsOrData = {}, { lazy = false } = {}) => {
|
|||||||
...state,
|
...state,
|
||||||
wasCalled: wasCalledRef.current,
|
wasCalled: wasCalledRef.current,
|
||||||
variables: { ...paramsOrDataMemoized, ...state.variables },
|
variables: { ...paramsOrDataMemoized, ...state.variables },
|
||||||
|
setLocalData,
|
||||||
},
|
},
|
||||||
makeRequest,
|
makeRequest,
|
||||||
];
|
];
|
||||||
|
|||||||
22
client/src/shared/utils/javascript.js
Normal file
22
client/src/shared/utils/javascript.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export const moveItemWithinArray = (arr, item, newIndex) => {
|
||||||
|
const arrClone = [...arr];
|
||||||
|
const oldIndex = arrClone.indexOf(item);
|
||||||
|
arrClone.splice(newIndex, 0, arrClone.splice(oldIndex, 1)[0]);
|
||||||
|
return arrClone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertItemIntoArray = (arr, item, index) => {
|
||||||
|
const arrClone = [...arr];
|
||||||
|
arrClone.splice(index, 0, item);
|
||||||
|
return arrClone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateArrayItemById = (arr, itemId, newFields) => {
|
||||||
|
const arrClone = [...arr];
|
||||||
|
const item = arrClone.find(({ id }) => id === itemId);
|
||||||
|
const itemIndex = arrClone.indexOf(item);
|
||||||
|
if (itemIndex > -1) {
|
||||||
|
arrClone.splice(itemIndex, 1, { ...item, ...newFields });
|
||||||
|
}
|
||||||
|
return arrClone;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user