Implemented kanban board page with lists of issues

This commit is contained in:
ireic
2019-12-12 17:26:57 +01:00
parent 3143f66a0f
commit 73b4ff97b2
73 changed files with 1343 additions and 561 deletions

View File

@@ -1,8 +1,6 @@
import express from 'express';
import { User } from 'entities';
import { catchErrors } from 'errors';
import { createEntity } from 'utils/typeorm';
import { signToken } from 'utils/authToken';
import seedGuestUserEntities from 'database/seeds/guestUser';
@@ -10,9 +8,8 @@ const router = express.Router();
router.post(
'/authentication/guest',
catchErrors(async (req, res) => {
const user = await createEntity(User, req.body);
await seedGuestUserEntities(user);
catchErrors(async (_req, res) => {
const user = await seedGuestUserEntities();
res.respond({
authToken: signToken({ sub: user.id }),
});

View File

@@ -2,36 +2,20 @@ import express from 'express';
import { Project } from 'entities';
import { catchErrors } from 'errors';
import { findEntityOrThrow, updateEntity, deleteEntity, createEntity } from 'utils/typeorm';
import { findEntityOrThrow, updateEntity } from 'utils/typeorm';
const router = express.Router();
router.get(
'/projects',
catchErrors(async (_req, res) => {
const projects = await Project.find();
res.respond({ projects });
}),
);
router.get(
'/projects/:projectId',
'/project',
catchErrors(async (req, res) => {
const project = await findEntityOrThrow(Project, req.params.projectId, {
const project = await findEntityOrThrow(Project, req.currentUser.projectId, {
relations: ['users', 'issues', 'issues.comments'],
});
res.respond({ project });
}),
);
router.post(
'/projects',
catchErrors(async (req, res) => {
const project = await createEntity(Project, req.body);
res.respond({ project });
}),
);
router.put(
'/projects/:projectId',
catchErrors(async (req, res) => {
@@ -40,12 +24,4 @@ router.put(
}),
);
router.delete(
'/projects/:projectId',
catchErrors(async (req, res) => {
const project = await deleteEntity(Project, req.params.projectId);
res.respond({ project });
}),
);
export default router;

View File

@@ -0,0 +1,14 @@
import express from 'express';
import { catchErrors } from 'errors';
const router = express.Router();
router.get(
'/currentUser',
catchErrors((req, res) => {
res.respond({ currentUser: req.currentUser });
}),
);
export default router;

View File

@@ -4,6 +4,7 @@ import User from 'entities/User';
const generateUser = (data: Partial<User> = {}): Partial<User> => ({
name: faker.company.companyName(),
avatarUrl: faker.image.avatar(),
email: faker.internet.email(),
...data,
});

View File

@@ -1,20 +1,39 @@
import faker from 'faker';
import { sample } from 'lodash';
import { Comment, Issue, Project, User } from 'entities';
import { ProjectCategory } from 'constants/projects';
import { IssueType, IssueStatus, IssuePriority } from 'constants/issues';
import { createEntity } from 'utils/typeorm';
const seedProject = (user: User): Promise<Project> =>
const seedUsers = (): Promise<User[]> => {
const users = [
createEntity(User, {
email: 'greg@jira.guest',
name: 'Greg the Egg',
avatarUrl: faker.image.avatar(),
}),
createEntity(User, {
email: 'yoda@jira.guest',
name: 'Baby Yoda',
avatarUrl: faker.image.avatar(),
}),
];
return Promise.all(users);
};
const seedProject = (users: User[]): Promise<Project> =>
createEntity(Project, {
name: 'Project: Hello World',
name: 'singularity 1.0',
url: 'https://www.atlassian.com/software/jira',
description:
'Plan, track, and manage your agile and software development projects in Jira. Customize your workflow, collaborate, and release great software.',
category: ProjectCategory.SOFTWARE,
users: [user],
users,
});
const seedIssues = (project: Project): Promise<Issue[]> => {
const user = project.users[0];
const getRandomUser = (): User => sample(project.users) as User;
const issues = [
createEntity(Issue, {
title: 'This is an issue of type: Task.',
@@ -22,9 +41,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
status: IssueStatus.BACKLOG,
priority: IssuePriority.LOWEST,
estimate: 8,
reporterId: user.id,
reporterId: getRandomUser().id,
project,
users: [user],
users: [getRandomUser()],
}),
createEntity(Issue, {
title: "Click on an issue to see what's behind it.",
@@ -33,7 +52,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
priority: IssuePriority.LOW,
description: 'Nothing in particular.',
estimate: 40,
reporterId: user.id,
reporterId: getRandomUser().id,
project,
}),
createEntity(Issue, {
@@ -42,9 +61,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
status: IssueStatus.BACKLOG,
priority: IssuePriority.MEDIUM,
estimate: 15,
reporterId: user.id,
reporterId: getRandomUser().id,
project,
users: [user],
users: [getRandomUser()],
}),
createEntity(Issue, {
title: 'You can use markdown for issue descriptions.',
@@ -54,9 +73,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
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",
estimate: 4,
reporterId: user.id,
reporterId: getRandomUser().id,
project,
users: [user],
users: [getRandomUser()],
}),
createEntity(Issue, {
title: 'You must assign priority from lowest to highest to all issues.',
@@ -64,7 +83,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
status: IssueStatus.SELECTED,
priority: IssuePriority.HIGHEST,
estimate: 15,
reporterId: user.id,
reporterId: getRandomUser().id,
project,
}),
createEntity(Issue, {
@@ -73,9 +92,9 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
status: IssueStatus.SELECTED,
priority: IssuePriority.MEDIUM,
estimate: 55,
reporterId: user.id,
reporterId: getRandomUser().id,
project,
users: [user],
users: [getRandomUser()],
}),
createEntity(Issue, {
title: 'Try leaving a comment on this issue.',
@@ -83,7 +102,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
status: IssueStatus.SELECTED,
priority: IssuePriority.MEDIUM,
estimate: 12,
reporterId: user.id,
reporterId: getRandomUser().id,
project,
}),
];
@@ -97,10 +116,12 @@ const seedComments = (issue: Issue, user: User): Promise<Comment> =>
user,
});
const seedGuestUserEntities = async (user: User): Promise<void> => {
const project = await seedProject(user);
const seedGuestUserEntities = async (): Promise<User> => {
const users = await seedUsers();
const project = await seedProject(users);
const issues = await seedIssues(project);
await seedComments(issues[issues.length - 1], project.users[0]);
return users[0];
};
export default seedGuestUserEntities;

View File

@@ -41,16 +41,16 @@ class Issue extends BaseEntity {
@Column('varchar')
priority: IssuePriority;
@Column({ type: 'text', nullable: true })
@Column('text', { nullable: true })
description: string | null;
@Column({ type: 'integer', nullable: true })
@Column('integer', { nullable: true })
estimate: number | null;
@Column({ type: 'integer', nullable: true })
@Column('integer', { nullable: true })
timeSpent: number | null;
@Column({ type: 'integer', nullable: true })
@Column('integer', { nullable: true })
timeRemaining: number | null;
@CreateDateColumn({ type: 'timestamp' })

View File

@@ -6,8 +6,6 @@ import {
CreateDateColumn,
UpdateDateColumn,
OneToMany,
ManyToMany,
JoinTable,
} from 'typeorm';
import is from 'utils/validation';
@@ -32,7 +30,7 @@ class Project extends BaseEntity {
@Column('varchar', { nullable: true })
url: string | null;
@Column({ type: 'text', nullable: true })
@Column('text', { nullable: true })
description: string | null;
@Column('varchar')
@@ -50,11 +48,10 @@ class Project extends BaseEntity {
)
issues: Issue[];
@ManyToMany(
@OneToMany(
() => User,
user => user.projects,
user => user.project,
)
@JoinTable()
users: User[];
}

View File

@@ -7,6 +7,8 @@ import {
UpdateDateColumn,
OneToMany,
ManyToMany,
ManyToOne,
RelationId,
} from 'typeorm';
import is from 'utils/validation';
@@ -28,6 +30,9 @@ class User extends BaseEntity {
@Column('varchar')
email: string;
@Column('varchar', { length: 2000 })
avatarUrl: string;
@CreateDateColumn({ type: 'timestamp' })
createdAt: Date;
@@ -46,11 +51,14 @@ class User extends BaseEntity {
)
issues: Issue[];
@ManyToMany(
@ManyToOne(
() => Project,
project => project.users,
)
projects: Project[];
project: Project;
@RelationId((user: User) => user.project)
projectId: number;
}
export default User;

View File

@@ -17,5 +17,5 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
data: {},
};
res.status(errorData.status).send({ errors: [errorData] });
res.status(errorData.status).send({ error: errorData });
};

View File

@@ -9,6 +9,7 @@ import { authenticateUser } from 'middleware/authentication';
import authenticationRoutes from 'controllers/authentication';
import projectsRoutes from 'controllers/projects';
import issuesRoutes from 'controllers/issues';
import usersRoutes from 'controllers/users';
import { RouteNotFoundError } from 'errors';
import { errorHandler } from 'errors/errorHandler';
@@ -30,7 +31,7 @@ const initializeExpress = (): void => {
app.use((_req, res, next) => {
res.respond = (data): void => {
res.status(200).send({ data });
res.status(200).send(data);
};
next();
});
@@ -41,6 +42,7 @@ const initializeExpress = (): void => {
app.use('/', projectsRoutes);
app.use('/', issuesRoutes);
app.use('/', usersRoutes);
app.use((req, _res, next) => next(new RouteNotFoundError(req.originalUrl)));
app.use(errorHandler);

View File

@@ -1,7 +1,6 @@
import { Request } from 'express';
import { verifyToken } from 'utils/authToken';
import { findEntityOrThrow } from 'utils/typeorm';
import { catchErrors, InvalidTokenError } from 'errors';
import { User } from 'entities';
@@ -20,6 +19,10 @@ export const authenticateUser = catchErrors(async (req, _res, next) => {
if (!userId) {
throw new InvalidTokenError('Authentication token is invalid.');
}
req.currentUser = await findEntityOrThrow(User, userId);
const user = await User.findOne(userId);
if (!user) {
throw new InvalidTokenError('Authentication token is invalid: User not found.');
}
req.currentUser = user;
next();
});

View File

@@ -5,7 +5,7 @@ import { InvalidTokenError } from 'errors';
export const signToken = (payload: object, options?: SignOptions): string =>
jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '7 days',
expiresIn: '180 days',
...options,
});