Implemented project settings page, search issues modal, general refactoring

This commit is contained in:
ireic
2019-12-27 15:25:23 +01:00
parent 3c705a6084
commit 7ceb18ee84
58 changed files with 738 additions and 193 deletions

5
api/package-lock.json generated
View File

@@ -3870,6 +3870,11 @@
"integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
"dev": true
},
"striptags": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/striptags/-/striptags-3.1.1.tgz",
"integrity": "sha1-yMPn/db7S7OjKjt1LltePjgJPr0="
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

View File

@@ -19,6 +19,7 @@
"module-alias": "^2.2.2",
"pg": "^7.14.0",
"reflect-metadata": "^0.1.13",
"striptags": "^3.1.1",
"typeorm": "^0.2.20"
},
"devDependencies": {

View File

@@ -6,6 +6,27 @@ import { updateEntity, deleteEntity, createEntity, findEntityOrThrow } from 'uti
const router = express.Router();
router.get(
'/issues',
catchErrors(async (req, res) => {
const { projectId } = req.currentUser;
const { searchTerm } = req.query;
let whereSQL = 'issue.projectId = :projectId';
if (searchTerm) {
whereSQL += ' AND (issue.title ILIKE :searchTerm OR issue.descriptionText ILIKE :searchTerm)';
}
const issues = await Issue.createQueryBuilder('issue')
.select()
.where(whereSQL, { projectId, searchTerm: `%${searchTerm}%` })
.getMany();
res.respond({ issues });
}),
);
router.get(
'/issues/:issueId',
catchErrors(async (req, res) => {
@@ -41,11 +62,11 @@ router.delete(
}),
);
const calculateListPosition = async (newIssue: Issue): Promise<number> => {
const issues = await Issue.find({
where: { projectId: newIssue.projectId, status: newIssue.status },
});
const calculateListPosition = async ({ projectId, status }: Issue): Promise<number> => {
const issues = await Issue.find({ projectId, status });
const listPositions = issues.map(({ listPosition }) => listPosition);
if (listPositions.length > 0) {
return Math.min(...listPositions) - 1;
}

View File

@@ -23,9 +23,9 @@ router.get(
);
router.put(
'/projects/:projectId',
'/project',
catchErrors(async (req, res) => {
const project = await updateEntity(Project, req.params.projectId, req.body);
const project = await updateEntity(Project, req.currentUser.projectId, req.body);
res.respond({ project });
}),
);

View File

@@ -11,6 +11,7 @@ const createDatabaseConnection = (): Promise<Connection> =>
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: Object.values(entities),
synchronize: true,
});
export default createDatabaseConnection;

View File

@@ -74,8 +74,7 @@ const seedIssues = (project: Project): Promise<Issue[]> => {
status: IssueStatus.BACKLOG,
priority: IssuePriority.HIGH,
listPosition: 4,
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",
description: '#### Colons can be used to align columns.',
estimate: 4,
reporterId: getRandomUser().id,
project,

View File

@@ -1,3 +1,4 @@
import striptags from 'striptags';
import {
BaseEntity,
Entity,
@@ -10,6 +11,8 @@ import {
ManyToMany,
JoinTable,
RelationId,
BeforeUpdate,
BeforeInsert,
} from 'typeorm';
import is from 'utils/validation';
@@ -48,6 +51,9 @@ class Issue extends BaseEntity {
@Column('text', { nullable: true })
description: string | null;
@Column('text', { nullable: true })
descriptionText: string | null;
@Column('integer', { nullable: true })
estimate: number | null;
@@ -90,6 +96,14 @@ class Issue extends BaseEntity {
@RelationId((issue: Issue) => issue.users)
userIds: number[];
@BeforeInsert()
@BeforeUpdate()
setDescriptionText = (): void => {
if (this.description) {
this.descriptionText = striptags(this.description);
}
};
}
export default Issue;

View File

@@ -17,7 +17,6 @@ class Project extends BaseEntity {
static validations = {
name: [is.required(), is.maxLength(100)],
url: is.url(),
description: is.maxLength(10000),
category: [is.required(), is.oneOf(Object.values(ProjectCategory))],
};

View File

@@ -8,7 +8,7 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
const isErrorSafeForClient = error instanceof CustomError;
const errorData = isErrorSafeForClient
const clientError = isErrorSafeForClient
? pick(error, ['message', 'code', 'status', 'data'])
: {
message: 'Something went wrong, please contact our support.',
@@ -17,5 +17,5 @@ export const errorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
data: {},
};
res.status(errorData.status).send({ error: errorData });
res.status(clientError.status).send({ error: clientError });
};

View File

@@ -4,12 +4,6 @@ import { verifyToken } from 'utils/authToken';
import { catchErrors, InvalidTokenError } from 'errors';
import { User } from 'entities';
const getAuthTokenFromRequest = (req: Request): string | null => {
const header = req.get('Authorization') || '';
const [bearer, token] = header.split(' ');
return bearer === 'Bearer' && token ? token : null;
};
export const authenticateUser = catchErrors(async (req, _res, next) => {
const token = getAuthTokenFromRequest(req);
if (!token) {
@@ -26,3 +20,9 @@ export const authenticateUser = catchErrors(async (req, _res, next) => {
req.currentUser = user;
next();
});
const getAuthTokenFromRequest = (req: Request): string | null => {
const header = req.get('Authorization') || '';
const [bearer, token] = header.split(' ');
return bearer === 'Bearer' && token ? token : null;
};

View File

@@ -48,6 +48,7 @@ export const generateErrors = (
Object.entries(fieldValidators).forEach(([fieldName, validators]) => {
[validators].flat().forEach(validator => {
const errorMessage = validator(fieldValues[fieldName], fieldValues);
if (errorMessage !== false && !fieldErrors[fieldName]) {
fieldErrors[fieldName] = errorMessage;
}