Skip to content

Commit

Permalink
feat(datatrakWeb): RN-1331: Task comments UI (#5801)
Browse files Browse the repository at this point in the history
* Create task with comments

* View comments on task details

* Allow create comment on edit task

* Display comments count on table

* Show comment count on tasks tile on landing page

* Fix up types

* Order comments descending

* Update schemas.ts

* Move comment adding to central server

* Update schemas.ts

* Fix types

* build fixes

* Revert "Remove task comments endpoints"

This reverts commit 498b1be.

* Remove task comment create endpoint as no longer needed
  • Loading branch information
alexd-bes authored Jul 25, 2024
1 parent d0c82a2 commit b92b94f
Show file tree
Hide file tree
Showing 34 changed files with 383 additions and 90 deletions.
12 changes: 11 additions & 1 deletion packages/central-server/src/apiV2/tasks/CreateTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ export class CreateTask extends CreateHandler {
}

async createRecord() {
await this.insertRecord();
const { comment, ...data } = this.newRecordData;

return this.models.wrapInTransaction(async transactingModels => {
const task = await transactingModels.task.create(data);
if (comment) {
await task.addComment(comment, this.req.user.id);
}
return {
id: task.id,
};
});
}
}
14 changes: 13 additions & 1 deletion packages/central-server/src/apiV2/tasks/EditTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ export class EditTask extends EditHandler {
}

async editRecord() {
await this.updateRecord();
const { comment, ...updatedFields } = this.updatedFields;
return this.models.wrapInTransaction(async transactingModels => {
const originalTask = await transactingModels.task.findById(this.recordId);
let task = originalTask;
// Sometimes an update can just be a comment, so we don't want to update the task if there are no fields to update, because we would get an error
if (Object.keys(updatedFields).length > 0) {
task = await transactingModels.task.updateById(this.recordId, updatedFields);
}
if (comment) {
await originalTask.addComment(comment, this.req.user.id);
}
return task;
});
}
}
19 changes: 19 additions & 0 deletions packages/central-server/src/tests/apiV2/tasks/CreateTask.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,24 @@ describe('Permissions checker for CreateTask', async () => {
});
expect(result).to.have.keys('error');
});

it('Handles creating a task with a comment', async () => {
await app.grantAccess(BES_ADMIN_POLICY);
const { body: result } = await app.post('tasks', {
body: {
...BASE_TASK,
entity_id: facilities[0].id,
survey_id: surveys[0].survey.id,
comment: 'This is a comment',
},
});

expect(result.message).to.equal('Successfully created tasks');
const comment = await models.taskComment.findOne({
task_id: result.id,
message: 'This is a comment',
});
expect(comment).not.to.be.undefined;
});
});
});
47 changes: 47 additions & 0 deletions packages/central-server/src/tests/apiV2/tasks/EditTask.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,5 +207,52 @@ describe('Permissions checker for EditTask', async () => {
expect(result).to.have.keys('error');
expect(result.error).to.include('Need to have access to the new entity of the task');
});

it('Handles adding a comment when editing a task', async () => {
await app.grantAccess({
DL: ['Donor'],
TO: ['Donor'],
});
await app.put(`tasks/${tasks[1].id}`, {
body: {
survey_id: surveys[1].survey.id,
entity_id: facilities[1].id,
comment: 'This is a test comment',
},
});
const result = await models.task.find({
id: tasks[1].id,
});
expect(result[0].entity_id).to.equal(facilities[1].id);
expect(result[0].survey_id).to.equal(surveys[1].survey.id);

const comment = await models.taskComment.findOne({
task_id: tasks[1].id,
message: 'This is a test comment',
});
expect(comment).not.to.be.undefined;
});

it('Handles adding a comment when no other edits are made', async () => {
await app.grantAccess({
DL: ['Donor'],
TO: ['Donor'],
});
await app.put(`tasks/${tasks[1].id}`, {
body: {
comment: 'This is a test comment',
},
});
const result = await models.task.find({
id: tasks[1].id,
});
expect(result[0].entity_id).to.equal(tasks[1].entity_id);

const comment = await models.taskComment.findOne({
task_id: tasks[1].id,
message: 'This is a test comment',
});
expect(comment).not.to.be.undefined;
});
});
});
11 changes: 11 additions & 0 deletions packages/database/src/modelClasses/Task.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ export class TaskRecord extends DatabaseRecord {
async systemComments() {
return this.otherModels.taskComment.find({ task_id: this.id, type: 'system' });
}

async addComment(message, userId, type = 'user') {
const user = await this.otherModels.user.findById(userId);
return this.otherModels.taskComment.create({
message,
task_id: this.id,
user_id: userId,
user_name: user.full_name,
type,
});
}
}

export class TaskModel extends DatabaseModel {
Expand Down
2 changes: 1 addition & 1 deletion packages/datatrak-web-server/src/routes/CreateTaskRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class CreateTaskRoute extends Route<CreateTaskRequest> {
public async buildResponse() {
const { models, body, ctx } = this.req;

const { surveyCode, entityId } = body;
const { surveyCode, entityId, comment } = body;

const survey = await models.survey.findOne({ code: surveyCode });
if (!survey) {
Expand Down
4 changes: 2 additions & 2 deletions packages/datatrak-web-server/src/routes/EditTaskRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';
import { formatTaskChanges } from '../utils';
import { DatatrakWebTaskChangeRequest } from '@tupaia/types';
import { DatatrakWebTaskChangeRequest, TaskCommentType } from '@tupaia/types';

export type EditTaskRequest = Request<
{ taskId: string },
Record<string, never>,
{ message: string },
Partial<DatatrakWebTaskChangeRequest.ReqBody>,
Record<string, never>
>;
Expand Down
10 changes: 9 additions & 1 deletion packages/datatrak-web-server/src/routes/TaskRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';
import { DatatrakWebTaskRequest } from '@tupaia/types';
import { TaskT, formatTaskResponse } from '../utils';
import camelcaseKeys from 'camelcase-keys';

export type TaskRequest = Request<
DatatrakWebTaskRequest.Params,
Expand Down Expand Up @@ -43,6 +44,13 @@ export class TaskRoute extends Route<TaskRequest> {
throw new Error(`Task with id ${taskId} not found`);
}

return formatTaskResponse(task);
const comments = await ctx.services.central.fetchResources(`tasks/${taskId}/taskComments`, {
sort: ['created_at DESC'],
});

return {
...formatTaskResponse(task),
comments: camelcaseKeys(comments, { deep: true }),
};
}
}
26 changes: 16 additions & 10 deletions packages/datatrak-web-server/src/routes/TasksRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';
import { parse } from 'cookie';
import { DatatrakWebTasksRequest, Task, TaskStatus } from '@tupaia/types';
import { DatatrakWebTasksRequest, TaskCommentType, TaskStatus } from '@tupaia/types';
import { RECORDS } from '@tupaia/database';
import { TaskT, formatTaskResponse } from '../utils';

Expand Down Expand Up @@ -33,13 +33,6 @@ const FIELDS = [

const DEFAULT_PAGE_SIZE = 20;

type SingleTask = Task & {
'survey.name': string;
'survey.code': string;
'entity.name': string;
'entity.country_code': string;
};

type FormattedFilters = Record<string, any>;

const EQUALITY_FILTERS = ['due_date', 'survey.project_id', 'task_status'];
Expand Down Expand Up @@ -142,7 +135,7 @@ export class TasksRoute extends Route<TasksRequest> {
}

public async buildResponse() {
const { ctx, query = {} } = this.req;
const { ctx, query = {}, models } = this.req;
const { pageSize = DEFAULT_PAGE_SIZE, sort, page = 0 } = query;

this.formatFilters();
Expand All @@ -161,7 +154,20 @@ export class TasksRoute extends Route<TasksRequest> {
page,
});

const formattedTasks = tasks.map((task: TaskT) => formatTaskResponse(task));
const formattedTasks = (await Promise.all(
tasks.map(async (task: TaskT) => {
const formattedTask = formatTaskResponse(task);
// Get comment count for each task
const commentsCount = await models.taskComment.count({
task_id: task.id,
type: TaskCommentType.user,
});
return {
...formattedTask,
commentsCount,
};
}),
)) as DatatrakWebTasksRequest.ResBody['tasks'];

// Get all task ids for pagination
const count = await this.queryForCount();
Expand Down
2 changes: 2 additions & 0 deletions packages/datatrak-web-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
PermissionGroupModel,
SurveyModel,
SurveyResponseModel,
TaskCommentModel,
TaskModel,
UserEntityPermissionModel,
UserModel,
Expand All @@ -29,4 +30,5 @@ export interface DatatrakWebServerModelRegistry extends ModelRegistry {
readonly task: TaskModel;
readonly permissionGroup: PermissionGroupModel;
readonly userEntityPermission: UserEntityPermissionModel;
readonly taskComment: TaskCommentModel;
}
2 changes: 2 additions & 0 deletions packages/datatrak-web-server/src/utils/formatTaskChanges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Input = Partial<DatatrakWebTaskChangeRequest.ReqBody> &

type Output = Partial<Omit<Task, 'due_date'>> & {
due_date?: string | null;
comment?: string;
};

export const formatTaskChanges = (task: Input) => {
Expand All @@ -36,6 +37,7 @@ export const formatTaskChanges = (task: Input) => {
const withoutTimezone = stripTimezoneFromDate(endOfDay);

taskDetails.due_date = withoutTimezone;
taskDetails.repeat_schedule = null;
}

return taskDetails;
Expand Down
27 changes: 21 additions & 6 deletions packages/datatrak-web-server/src/utils/formatTaskResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,29 @@
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import { DatatrakWebTaskRequest, Entity, Survey, Task } from '@tupaia/types';
import {
DatatrakWebTaskRequest,
DatatrakWebTasksRequest,
Entity,
KeysToCamelCase,
Survey,
Task,
TaskStatus,
} from '@tupaia/types';
import camelcaseKeys from 'camelcase-keys';

export type TaskT = Omit<Task, 'created_at'> & {
export type TaskT = Omit<Task, 'created_at' | 'repeat_schedule'> & {
'entity.name': Entity['name'];
'entity.country_code': string;
'survey.code': Survey['code'];
'survey.name': Survey['name'];
task_status: DatatrakWebTaskRequest.ResBody['taskStatus'];
task_status: TaskStatus | 'overdue' | 'repeating';
repeat_schedule?: Record<string, unknown> | null;
};

export const formatTaskResponse = (task: TaskT): DatatrakWebTaskRequest.ResBody => {
type FormattedTask = DatatrakWebTasksRequest.TaskResponse;

export const formatTaskResponse = (task: TaskT): FormattedTask => {
const {
entity_id: entityId,
'entity.name': entityName,
Expand All @@ -23,12 +34,12 @@ export const formatTaskResponse = (task: TaskT): DatatrakWebTaskRequest.ResBody
survey_id: surveyId,
'survey.name': surveyName,
task_status: taskStatus,
repeat_schedule: repeatSchedule,
...rest
} = task;

const formattedTask = {
...rest,
taskStatus,
entity: {
id: entityId,
name: entityName,
Expand All @@ -39,7 +50,11 @@ export const formatTaskResponse = (task: TaskT): DatatrakWebTaskRequest.ResBody
name: surveyName,
code: surveyCode,
},
taskStatus,
repeatSchedule,
};

return camelcaseKeys(formattedTask) as DatatrakWebTaskRequest.ResBody;
return camelcaseKeys(formattedTask, {
deep: true,
});
};
4 changes: 2 additions & 2 deletions packages/datatrak-web/src/api/queries/useTask.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
*/

import { useQuery } from 'react-query';
import { DatatrakWebTasksRequest } from '@tupaia/types';
import { DatatrakWebTaskRequest } from '@tupaia/types';
import { get } from '../api';

export const useTask = (taskId?: string) => {
return useQuery(
['tasks', taskId],
(): Promise<DatatrakWebTasksRequest.ResBody['tasks'][0]> => get(`tasks/${taskId}`),
(): Promise<DatatrakWebTaskRequest.ResBody> => get(`tasks/${taskId}`),
{
enabled: !!taskId,
},
Expand Down
26 changes: 26 additions & 0 deletions packages/datatrak-web/src/components/Icons/CommentIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React from 'react';
import { SvgIcon, SvgIconProps } from '@material-ui/core';

export const CommentIcon = (props: SvgIconProps) => {
return (
<SvgIcon
{...props}
width="14"
height="14"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12.445 1.55516C12.0897 1.19958 11.6077 1 11.1053 1H2.89474C2.39225 1 1.91029 1.19958 1.55496 1.55516C1.19964 1.91011 1 2.39201 1 2.89475V8.57904C1 9.08178 1.19964 9.56368 1.55496 9.91864C1.91029 10.2742 2.39225 10.4738 2.89474 10.4738H4.78947V12.3686C4.78947 12.4936 4.82654 12.6155 4.89595 12.7191C4.96536 12.8233 5.06396 12.9041 5.17934 12.9521C5.29473 12.9995 5.42174 13.0121 5.54421 12.9881C5.66673 12.9635 5.77922 12.9035 5.86758 12.8151L8.20884 10.4738H11.1053C11.6077 10.4738 12.0897 10.2742 12.445 9.91864C12.8004 9.56368 13 9.08178 13 8.57904V2.89475C13 2.39201 12.8004 1.91011 12.445 1.55516ZM12.3684 8.57904C12.3684 8.91377 12.2353 9.23525 11.9984 9.4721C11.7615 9.70895 11.4402 9.84221 11.1053 9.84221H7.94736L5.42105 12.3686V9.84221H2.89474C2.55974 9.84221 2.23846 9.70895 2.00156 9.4721C1.76465 9.23525 1.63158 8.91377 1.63158 8.57904V2.89475C1.63158 2.56002 1.76465 2.23853 2.00156 2.00169C2.23846 1.76484 2.55974 1.63159 2.89474 1.63159H11.1053C11.4402 1.63159 11.7615 1.76484 11.9984 2.00169C12.2353 2.23853 12.3684 2.56002 12.3684 2.89475V8.57904Z"
fill="#898989"
stroke="#898989"
strokeWidth="0.3"
/>
</SvgIcon>
);
};
1 change: 1 addition & 0 deletions packages/datatrak-web/src/components/Icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export { PinIcon } from './PinIcon';
export { ReportsIcon } from './ReportsIcon';
export { CopyIcon } from './CopyIcon';
export { TaskIcon } from './TaskIcon';
export { CommentIcon } from './CommentIcon';
Loading

0 comments on commit b92b94f

Please sign in to comment.