Implemented kanban board page with lists of issues
This commit is contained in:
@@ -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 }),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
14
api/src/controllers/users.ts
Normal file
14
api/src/controllers/users.ts
Normal 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;
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user