From bba12cb05fd994a2707c7958f8fb55d1566b796d Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Thu, 1 Aug 2024 17:30:40 +1200
Subject: [PATCH 1/9] WIP
---
.../src/features/Tasks/CommentsCount.tsx | 2 +-
.../Tasks/TaskDetails/TaskComments.tsx | 56 ++++++-
.../Tasks/TaskDetails/TaskDetails.tsx | 156 +++++++++---------
3 files changed, 123 insertions(+), 91 deletions(-)
diff --git a/packages/datatrak-web/src/features/Tasks/CommentsCount.tsx b/packages/datatrak-web/src/features/Tasks/CommentsCount.tsx
index 88d6ee137c..539e97afe3 100644
--- a/packages/datatrak-web/src/features/Tasks/CommentsCount.tsx
+++ b/packages/datatrak-web/src/features/Tasks/CommentsCount.tsx
@@ -6,8 +6,8 @@
import React from 'react';
import styled from 'styled-components';
import { Typography } from '@material-ui/core';
-import { CommentIcon } from '../../components';
import { Tooltip } from '@tupaia/ui-components';
+import { CommentIcon } from '../../components';
const CommentsCountWrapper = styled.div`
color: ${({ theme }) => theme.palette.text.secondary};
diff --git a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx
index 5dde3bb1c9..ae178ee33d 100644
--- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx
+++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx
@@ -5,20 +5,23 @@
import React from 'react';
import styled from 'styled-components';
+import { useForm } from 'react-hook-form';
import { Typography } from '@material-ui/core';
import { TaskCommentType } from '@tupaia/types';
+import { TextField } from '@tupaia/ui-components';
import { displayDateTime } from '../../../utils';
import { SingleTaskResponse } from '../../../types';
+import { TaskForm } from '../TaskForm';
+import { Button } from '../../../components';
-const Wrapper = styled.div`
+const TaskCommentsDisplayContainer = styled.div`
width: 100%;
border: 1px solid ${({ theme }) => theme.palette.divider};
background-color: ${({ theme }) => theme.palette.background.default};
- margin-block-end: 1.2rem;
padding: 1rem;
border-radius: 4px;
overflow-y: auto;
- height: 19rem;
+ flex: 1;
`;
const CommentContainer = styled.div`
@@ -28,12 +31,36 @@ const CommentContainer = styled.div`
}
`;
+const CommentsInput = styled(TextField).attrs({
+ multiline: true,
+ variant: 'outlined',
+ fullWidth: true,
+ rows: 4,
+})`
+ margin-block-end: 0;
+ height: 11rem;
+ .MuiOutlinedInput-inputMultiline {
+ padding-inline: 1rem;
+ }
+ .MuiInputBase-root {
+ height: 100%;
+ }
+`;
+
const Message = styled(Typography).attrs({
variant: 'body2',
})`
margin-block-start: 0.2rem;
`;
+const Form = styled(TaskForm)`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ flex: 1;
+ align-items: flex-end;
+`;
+
type Comments = SingleTaskResponse['comments'];
const SingleComment = ({ comment }: { comment: Comments[0] }) => {
@@ -51,11 +78,24 @@ const SingleComment = ({ comment }: { comment: Comments[0] }) => {
};
export const TaskComments = ({ comments }: { comments: Comments }) => {
+ const {
+ register,
+ handleSubmit,
+ formState: { isDirty },
+ } = useForm();
+
+ const onSubmit = data => {};
return (
-
- {comments.map((comment, index) => (
-
- ))}
-
+
);
};
diff --git a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
index 8e25a9d745..81347b50df 100644
--- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
+++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
@@ -62,18 +62,6 @@ const ItemWrapper = styled.div`
}
`;
-const CommentsInput = styled(TextField).attrs({
- multiline: true,
- variant: 'outlined',
- fullWidth: true,
- rows: 4,
-})`
- margin-block-end: 0;
- .MuiOutlinedInput-inputMultiline {
- padding-inline: 1rem;
- }
-`;
-
const ClearButton = styled(Button).attrs({
variant: 'text',
})`
@@ -88,6 +76,11 @@ const ButtonWrapper = styled.div`
`;
const Form = styled(TaskForm)`
+ display: flex;
+ flex-direction: column;
+`;
+
+const Wrapper = styled.div`
.loading-screen {
border: 1px solid ${({ theme }) => theme.palette.divider};
}
@@ -110,7 +103,6 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => {
control,
handleSubmit,
watch,
- register,
formState: { dirtyFields },
reset,
} = formContext;
@@ -142,83 +134,83 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => {
};
return (
-
+
);
};
From 55c92dc45f2cc57ffd8795cf7228f2a3a7b47dc3 Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 2 Aug 2024 08:37:18 +1200
Subject: [PATCH 2/9] UI comments
---
.../datatrak-web/src/api/mutations/index.ts | 1 +
.../src/api/mutations/useCreateTaskComment.ts | 33 +++++++++++++++++
.../Tasks/TaskDetails/TaskComments.tsx | 35 +++++++++++++------
3 files changed, 58 insertions(+), 11 deletions(-)
create mode 100644 packages/datatrak-web/src/api/mutations/useCreateTaskComment.ts
diff --git a/packages/datatrak-web/src/api/mutations/index.ts b/packages/datatrak-web/src/api/mutations/index.ts
index 8eb2efe988..b067d0ed25 100644
--- a/packages/datatrak-web/src/api/mutations/index.ts
+++ b/packages/datatrak-web/src/api/mutations/index.ts
@@ -18,3 +18,4 @@ export { useOneTimeLogin } from './useOneTimeLogin';
export * from './useExportSurveyResponses';
export { useTupaiaRedirect } from './useTupaiaRedirect';
export { useCreateTask } from './useCreateTask';
+export { useCreateTaskComment } from './useCreateTaskComment';
diff --git a/packages/datatrak-web/src/api/mutations/useCreateTaskComment.ts b/packages/datatrak-web/src/api/mutations/useCreateTaskComment.ts
new file mode 100644
index 0000000000..07fc4704d7
--- /dev/null
+++ b/packages/datatrak-web/src/api/mutations/useCreateTaskComment.ts
@@ -0,0 +1,33 @@
+/*
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import { useMutation, useQueryClient } from 'react-query';
+import { Task, TaskCommentType } from '@tupaia/types';
+import { post } from '../api';
+import { successToast } from '../../utils';
+
+export const useCreateTaskComment = (taskId?: Task['id'], onSuccess?: () => void) => {
+ const queryClient = useQueryClient();
+ return useMutation(
+ (comment: string) => {
+ return post(`tasks/${taskId}/taskComments`, {
+ data: {
+ message: comment,
+ type: TaskCommentType.user,
+ },
+ });
+ },
+ {
+ onSuccess: () => {
+ queryClient.invalidateQueries('tasks');
+ queryClient.invalidateQueries(['tasks', taskId]);
+ successToast('Comment added successfully');
+ if (onSuccess) {
+ onSuccess();
+ }
+ },
+ },
+ );
+};
diff --git a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx
index ae178ee33d..80e8907d07 100644
--- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx
+++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx
@@ -6,6 +6,7 @@
import React from 'react';
import styled from 'styled-components';
import { useForm } from 'react-hook-form';
+import { useParams } from 'react-router';
import { Typography } from '@material-ui/core';
import { TaskCommentType } from '@tupaia/types';
import { TextField } from '@tupaia/ui-components';
@@ -13,6 +14,7 @@ import { displayDateTime } from '../../../utils';
import { SingleTaskResponse } from '../../../types';
import { TaskForm } from '../TaskForm';
import { Button } from '../../../components';
+import { useCreateTaskComment } from '../../../api';
const TaskCommentsDisplayContainer = styled.div`
width: 100%;
@@ -22,6 +24,7 @@ const TaskCommentsDisplayContainer = styled.div`
border-radius: 4px;
overflow-y: auto;
flex: 1;
+ max-height: 18rem;
`;
const CommentContainer = styled.div`
@@ -35,15 +38,13 @@ const CommentsInput = styled(TextField).attrs({
multiline: true,
variant: 'outlined',
fullWidth: true,
- rows: 4,
+ rows: 5,
})`
- margin-block-end: 0;
- height: 11rem;
- .MuiOutlinedInput-inputMultiline {
+ margin-block: 1.2rem;
+ height: 9.5rem;
+ .MuiOutlinedInput-inputMultiline.MuiInputBase-input {
padding-inline: 1rem;
- }
- .MuiInputBase-root {
- height: 100%;
+ padding-block: 1rem;
}
`;
@@ -78,13 +79,25 @@ const SingleComment = ({ comment }: { comment: Comments[0] }) => {
};
export const TaskComments = ({ comments }: { comments: Comments }) => {
+ const { taskId } = useParams();
+
const {
register,
handleSubmit,
+ reset,
formState: { isDirty },
- } = useForm();
+ } = useForm({
+ defaultValues: {
+ comment: '',
+ },
+ });
+
+ const { mutate: createTaskComment, isLoading: isSaving } = useCreateTaskComment(taskId, reset);
+
+ const onSubmit = data => {
+ createTaskComment(data.comment);
+ };
- const onSubmit = data => {};
return (
);
From bc278511be9f817d53443caae177c3b78972b577 Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 2 Aug 2024 08:37:30 +1200
Subject: [PATCH 3/9] Remove comment handling on edit task
---
packages/central-server/src/apiV2/tasks/EditTask.js | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/packages/central-server/src/apiV2/tasks/EditTask.js b/packages/central-server/src/apiV2/tasks/EditTask.js
index db20be926c..cdd15ac638 100644
--- a/packages/central-server/src/apiV2/tasks/EditTask.js
+++ b/packages/central-server/src/apiV2/tasks/EditTask.js
@@ -15,17 +15,10 @@ export class EditTask extends EditHandler {
}
async editRecord() {
- 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);
- }
+ const task = await transactingModels.task.updateById(this.recordId, this.updatedFields);
+
await originalTask.addSystemCommentsOnUpdate(this.updatedFields, this.req.user.id);
return task;
});
From 24425eea24e1544c8cdfeb0bc680bb501af2a09c Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 2 Aug 2024 08:38:38 +1200
Subject: [PATCH 4/9] Create task comment endpoint
---
packages/central-server/src/apiV2/index.js | 3 +-
.../apiV2/taskComments/CreateTaskComment.js | 31 +++++++++++++++++++
.../src/apiV2/taskComments/index.js | 1 +
3 files changed, 34 insertions(+), 1 deletion(-)
create mode 100644 packages/central-server/src/apiV2/taskComments/CreateTaskComment.js
diff --git a/packages/central-server/src/apiV2/index.js b/packages/central-server/src/apiV2/index.js
index 9ec8dacfb8..1535c0158a 100644
--- a/packages/central-server/src/apiV2/index.js
+++ b/packages/central-server/src/apiV2/index.js
@@ -145,7 +145,7 @@ import {
} from './dashboardMailingListEntries';
import { EditEntityHierarchy, GETEntityHierarchy } from './entityHierarchy';
import { CreateTask, EditTask, GETTasks } from './tasks';
-import { GETTaskComments } from './taskComments';
+import { CreateTaskComment, GETTaskComments } from './taskComments';
// quick and dirty permission wrapper for open endpoints
const allowAnyone = routeHandler => (req, res, next) => {
@@ -318,6 +318,7 @@ apiV2.post('/surveys', multipartJson(), useRouteHandler(CreateSurvey));
apiV2.post('/dhisInstances', useRouteHandler(BESAdminCreateHandler));
apiV2.post('/supersetInstances', useRouteHandler(BESAdminCreateHandler));
apiV2.post('/tasks', useRouteHandler(CreateTask));
+apiV2.post('/tasks/:parentRecordId/taskComments', useRouteHandler(CreateTaskComment));
/**
* PUT routes
*/
diff --git a/packages/central-server/src/apiV2/taskComments/CreateTaskComment.js b/packages/central-server/src/apiV2/taskComments/CreateTaskComment.js
new file mode 100644
index 0000000000..dde298ab42
--- /dev/null
+++ b/packages/central-server/src/apiV2/taskComments/CreateTaskComment.js
@@ -0,0 +1,31 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+import { CreateHandler } from '../CreateHandler';
+import { assertAnyPermissions, assertBESAdminAccess } from '../../permissions';
+import { assertUserHasTaskPermissions } from '../tasks/assertTaskPermissions';
+/**
+ * Handles POST endpoints:
+ * - /tasks/:parentRecordId/taskComments
+ */
+
+export class CreateTaskComment extends CreateHandler {
+ async assertUserHasAccess() {
+ const createPermissionChecker = accessPolicy =>
+ assertUserHasTaskPermissions(accessPolicy, this.models, this.parentRecordId);
+
+ await this.assertPermissions(
+ assertAnyPermissions([assertBESAdminAccess, createPermissionChecker]),
+ );
+ }
+
+ async createRecord() {
+ return this.models.wrapInTransaction(async transactingModels => {
+ const task = await transactingModels.task.findById(this.parentRecordId);
+ const { message, type } = this.newRecordData;
+ const newComment = await task.addComment(message, this.req.user.id, type);
+ return { id: newComment.id };
+ });
+ }
+}
diff --git a/packages/central-server/src/apiV2/taskComments/index.js b/packages/central-server/src/apiV2/taskComments/index.js
index d5760663d3..e6c87c1220 100644
--- a/packages/central-server/src/apiV2/taskComments/index.js
+++ b/packages/central-server/src/apiV2/taskComments/index.js
@@ -4,3 +4,4 @@
*/
export { GETTaskComments } from './GETTaskComments';
+export { CreateTaskComment } from './CreateTaskComment';
From 971e670d46882219347cb6e98dff30226d5f771f Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 2 Aug 2024 08:53:43 +1200
Subject: [PATCH 5/9] Link to task from landing page, and back button
---
.../src/features/Tasks/TaskTile.tsx | 21 +++++++++++++++++--
.../src/views/LandingPage/TasksSection.tsx | 2 +-
.../src/views/Tasks/TaskDetailsPage.tsx | 16 ++++++++++++++
3 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/packages/datatrak-web/src/features/Tasks/TaskTile.tsx b/packages/datatrak-web/src/features/Tasks/TaskTile.tsx
index caf098d49a..46cd914832 100644
--- a/packages/datatrak-web/src/features/Tasks/TaskTile.tsx
+++ b/packages/datatrak-web/src/features/Tasks/TaskTile.tsx
@@ -12,10 +12,11 @@ import { ButtonLink } from '../../components';
import { StatusPill } from './StatusPill';
import { CommentsCount } from './CommentsCount';
-const TileContainer = styled.div`
+const TileContainer = styled(Link)`
display: flex;
text-align: left;
justify-content: space-between;
+ text-decoration: none;
border-radius: 10px;
border: 1px solid ${({ theme }) => theme.palette.divider};
width: 100%;
@@ -29,6 +30,14 @@ const TileContainer = styled.div`
.MuiButton-label {
font-size: 0.75rem;
}
+
+ &:hover {
+ background-color: ${({ theme }) => theme.palette.primaryHover};
+ border-color: ${({ theme }) => theme.palette.primary.main};
+ }
+ &:focus-within {
+ border-color: ${({ theme }) => theme.palette.primary.main};
+ }
`;
const TileTitle = styled.div`
@@ -69,8 +78,16 @@ export const TaskTile = ({ task }) => {
surveyCode: survey.code,
countryCode: entity.countryCode,
});
+ const taskLink = generatePath(ROUTES.TASK_DETAILS, {
+ taskId: task.id,
+ });
return (
-
+
{survey.name}
diff --git a/packages/datatrak-web/src/views/LandingPage/TasksSection.tsx b/packages/datatrak-web/src/views/LandingPage/TasksSection.tsx
index 552a89ad7d..5c4d4ab3b2 100644
--- a/packages/datatrak-web/src/views/LandingPage/TasksSection.tsx
+++ b/packages/datatrak-web/src/views/LandingPage/TasksSection.tsx
@@ -7,11 +7,11 @@ import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { FlexSpaceBetween, TextButton, Button as UIButton } from '@tupaia/ui-components';
-import { SectionHeading } from './SectionHeading';
import { useCurrentUserContext, useTasks } from '../../api';
import { NoTasksSection, TaskTile } from '../../features/Tasks';
import { ROUTES } from '../../constants';
import { LoadingTile } from '../../components';
+import { SectionHeading } from './SectionHeading';
const SectionContainer = styled.section`
grid-area: tasks;
diff --git a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx
index 08a0ff28e3..b94f8aca90 100644
--- a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx
+++ b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
import { generatePath, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { Typography } from '@material-ui/core';
+import { KeyboardArrowLeft } from '@material-ui/icons';
import { TaskStatus } from '@tupaia/types';
import { Modal, ModalCenteredContent, SpinningLoader } from '@tupaia/ui-components';
import { Button } from '../../components';
@@ -16,6 +17,17 @@ import { ROUTES } from '../../constants';
import { useFromLocation } from '../../utils';
import { SingleTaskResponse } from '../../types';
+const BackButton = styled(Button)`
+ position: absolute;
+ left: 0;
+ color: ${({ theme }) => theme.palette.text.primary};
+ padding: 0.2rem;
+ border-radius: 50%;
+ .MuiSvgIcon-root {
+ font-size: 2.5rem;
+ }
+`;
+
const ButtonWrapper = styled.div`
display: flex;
justify-content: space-between;
@@ -90,10 +102,14 @@ export const TaskDetailsPage = () => {
const [errorModalOpen, setErrorModalOpen] = useState(false);
const { taskId } = useParams();
const { data: task, isLoading } = useTask(taskId);
+ const from = useFromLocation();
return (
<>
+
+
+
setErrorModalOpen(true)} />
{task && }
From 9dcc563012c1068dfcf4acb6f171578df7686d99 Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 2 Aug 2024 09:00:37 +1200
Subject: [PATCH 6/9] CHange back icon
---
.../src/components/Icons/ArrowLeftIcon.tsx | 24 +++++++++++++++++++
.../src/components/Icons/index.ts | 1 +
.../Tasks/TaskDetails/TaskDetails.tsx | 13 +++++++---
.../src/views/Tasks/TaskDetailsPage.tsx | 10 ++++----
4 files changed, 40 insertions(+), 8 deletions(-)
create mode 100644 packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx
diff --git a/packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx b/packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx
new file mode 100644
index 0000000000..511288c7fa
--- /dev/null
+++ b/packages/datatrak-web/src/components/Icons/ArrowLeftIcon.tsx
@@ -0,0 +1,24 @@
+/*
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+import React from 'react';
+import { SvgIcon, SvgIconProps } from '@material-ui/core';
+
+export const ArrowLeftIcon = (props: SvgIconProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/packages/datatrak-web/src/components/Icons/index.ts b/packages/datatrak-web/src/components/Icons/index.ts
index a6db7c9f18..3841949d04 100644
--- a/packages/datatrak-web/src/components/Icons/index.ts
+++ b/packages/datatrak-web/src/components/Icons/index.ts
@@ -14,3 +14,4 @@ export { ReportsIcon } from './ReportsIcon';
export { CopyIcon } from './CopyIcon';
export { TaskIcon } from './TaskIcon';
export { CommentIcon } from './CommentIcon';
+export { ArrowLeftIcon } from './ArrowLeftIcon';
diff --git a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
index 81347b50df..602bcd607c 100644
--- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
+++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
@@ -9,7 +9,7 @@ import { useForm, Controller } from 'react-hook-form';
import styled from 'styled-components';
import { Paper } from '@material-ui/core';
import { TaskStatus } from '@tupaia/types';
-import { LoadingContainer, TextField } from '@tupaia/ui-components';
+import { LoadingContainer } from '@tupaia/ui-components';
import { useEditTask } from '../../../api';
import { Button } from '../../../components';
import { useFromLocation } from '../../../utils';
@@ -41,9 +41,16 @@ const MainColumn = styled.div`
justify-content: space-between;
flex: 1;
margin-block: 1.2rem;
+ border-color: ${({ theme }) => theme.palette.divider};
+ border-style: solid;
+ border-width: 1px 0;
+ padding-block: 1.2rem;
${({ theme }) => theme.breakpoints.up('md')} {
- width: 44%;
+ width: 50%;
margin-block: 0;
+ padding-inline: 1.2rem;
+ padding-block: 0;
+ border-width: 0 1px;
}
`;
@@ -52,7 +59,7 @@ const SideColumn = styled.div`
flex-direction: column;
justify-content: space-between;
${({ theme }) => theme.breakpoints.up('md')} {
- width: 28%;
+ width: 25%;
}
`;
diff --git a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx
index b94f8aca90..bf1a8889d3 100644
--- a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx
+++ b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx
@@ -7,10 +7,9 @@ import React, { useState } from 'react';
import { generatePath, useParams } from 'react-router-dom';
import styled from 'styled-components';
import { Typography } from '@material-ui/core';
-import { KeyboardArrowLeft } from '@material-ui/icons';
import { TaskStatus } from '@tupaia/types';
import { Modal, ModalCenteredContent, SpinningLoader } from '@tupaia/ui-components';
-import { Button } from '../../components';
+import { ArrowLeftIcon, Button } from '../../components';
import { TaskDetails, TaskPageHeader, TaskActionsMenu } from '../../features';
import { useTask } from '../../api';
import { ROUTES } from '../../constants';
@@ -20,11 +19,12 @@ import { SingleTaskResponse } from '../../types';
const BackButton = styled(Button)`
position: absolute;
left: 0;
+ min-width: 0;
color: ${({ theme }) => theme.palette.text.primary};
- padding: 0.2rem;
+ padding: 0.7rem;
border-radius: 50%;
.MuiSvgIcon-root {
- font-size: 2.5rem;
+ font-size: 1.3rem;
}
`;
@@ -108,7 +108,7 @@ export const TaskDetailsPage = () => {
<>
-
+
setErrorModalOpen(true)} />
From 6af53728638e3a130aedaef7e3b84746e228d343 Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 2 Aug 2024 09:08:56 +1200
Subject: [PATCH 7/9] Update tests
---
.../taskComments/CreateTaskComment.test.js | 169 ++++++++++++++++++
.../src/tests/apiV2/tasks/EditTask.test.js | 96 ----------
2 files changed, 169 insertions(+), 96 deletions(-)
create mode 100644 packages/central-server/src/tests/apiV2/taskComments/CreateTaskComment.test.js
diff --git a/packages/central-server/src/tests/apiV2/taskComments/CreateTaskComment.test.js b/packages/central-server/src/tests/apiV2/taskComments/CreateTaskComment.test.js
new file mode 100644
index 0000000000..9bcb8e2822
--- /dev/null
+++ b/packages/central-server/src/tests/apiV2/taskComments/CreateTaskComment.test.js
@@ -0,0 +1,169 @@
+/**
+ * Tupaia
+ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
+ */
+
+import { expect } from 'chai';
+import {
+ buildAndInsertSurveys,
+ findOrCreateDummyCountryEntity,
+ findOrCreateDummyRecord,
+ generateId,
+} from '@tupaia/database';
+import { TestableApp, resetTestData } from '../../testUtilities';
+import { BES_ADMIN_PERMISSION_GROUP } from '../../../permissions';
+
+describe('Permissions checker for CreateTaskComment', async () => {
+ const BES_ADMIN_POLICY = {
+ DL: [BES_ADMIN_PERMISSION_GROUP],
+ };
+
+ const DEFAULT_POLICY = {
+ DL: ['Donor'],
+ TO: ['Donor'],
+ };
+
+ const app = new TestableApp();
+ const { models } = app;
+ let tasks;
+
+ before(async () => {
+ const { country: tongaCountry } = await findOrCreateDummyCountryEntity(models, {
+ code: 'TO',
+ name: 'Tonga',
+ });
+
+ const { country: dlCountry } = await findOrCreateDummyCountryEntity(models, {
+ code: 'DL',
+ name: 'Demo Land',
+ });
+
+ const donorPermission = await findOrCreateDummyRecord(models.permissionGroup, {
+ name: 'Donor',
+ });
+ const BESAdminPermission = await findOrCreateDummyRecord(models.permissionGroup, {
+ name: 'Admin',
+ });
+
+ const facilities = [
+ {
+ id: generateId(),
+ code: 'TEST_FACILITY_1',
+ name: 'Test Facility 1',
+ country_code: tongaCountry.code,
+ },
+ {
+ id: generateId(),
+ code: 'TEST_FACILITY_2',
+ name: 'Test Facility 2',
+ country_code: dlCountry.code,
+ },
+ ];
+
+ await Promise.all(facilities.map(facility => findOrCreateDummyRecord(models.entity, facility)));
+
+ const surveys = await buildAndInsertSurveys(models, [
+ {
+ code: 'TEST_SURVEY_1',
+ name: 'Test Survey 1',
+ permission_group_id: BESAdminPermission.id,
+ country_ids: [tongaCountry.id, dlCountry.id],
+ },
+ {
+ code: 'TEST_SURVEY_2',
+ name: 'Test Survey 2',
+ permission_group_id: donorPermission.id,
+ country_ids: [tongaCountry.id, dlCountry.id],
+ },
+ ]);
+
+ const assignee = {
+ id: generateId(),
+ first_name: 'Minnie',
+ last_name: 'Mouse',
+ };
+ await findOrCreateDummyRecord(models.user, assignee);
+
+ const dueDate = new Date('2021-12-31');
+
+ tasks = [
+ {
+ id: generateId(),
+ survey_id: surveys[0].survey.id,
+ entity_id: facilities[0].id,
+ due_date: dueDate,
+ status: 'to_do',
+ repeat_schedule: null,
+ },
+ {
+ id: generateId(),
+ survey_id: surveys[1].survey.id,
+ entity_id: facilities[1].id,
+ assignee_id: assignee.id,
+ due_date: null,
+ repeat_schedule: '{}',
+ status: null,
+ },
+ ];
+
+ await Promise.all(
+ tasks.map(task =>
+ findOrCreateDummyRecord(
+ models.task,
+ {
+ 'task.id': task.id,
+ },
+ task,
+ ),
+ ),
+ );
+ });
+
+ afterEach(async () => {
+ await models.taskComment.delete({ task_id: tasks[0].id });
+ await models.taskComment.delete({ task_id: tasks[1].id });
+ app.revokeAccess();
+ });
+
+ after(async () => {
+ await resetTestData();
+ });
+
+ describe('POST /tasks/:id/taskComments', async () => {
+ it('Sufficient permissions: Successfully creates a task comment when the user has BES Admin permissions', async () => {
+ await app.grantAccess(BES_ADMIN_POLICY);
+ await app.post(`tasks/${tasks[0].id}/taskComments`, {
+ body: {
+ message: 'This is a test comment',
+ type: 'user',
+ },
+ });
+ const comment = await models.taskComment.findOne({ task_id: tasks[0].id });
+ expect(comment.message).to.equal('This is a test comment');
+ });
+
+ it('Sufficient permissions: Successfully creates a task comment when user has access to the task', async () => {
+ await app.grantAccess(DEFAULT_POLICY);
+ await app.post(`tasks/${tasks[1].id}/taskComments`, {
+ body: {
+ message: 'This is a test comment',
+ type: 'user',
+ },
+ });
+ const comment = await models.taskComment.findOne({ task_id: tasks[1].id });
+ expect(comment.message).to.equal('This is a test comment');
+ });
+
+ it('Insufficient permissions: throws an error if trying to create a comment for a task the user does not have permissions for', async () => {
+ await app.grantAccess(DEFAULT_POLICY);
+ const { body: result } = await app.post(`tasks/${tasks[0].id}/taskComments`, {
+ body: {
+ message: 'This is a test comment',
+ type: 'user',
+ },
+ });
+
+ expect(result).to.have.keys('error');
+ });
+ });
+});
diff --git a/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js b/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js
index 8f1855e7cd..592e54e215 100644
--- a/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js
+++ b/packages/central-server/src/tests/apiV2/tasks/EditTask.test.js
@@ -210,55 +210,6 @@ describe('Permissions checker for EditTask', async () => {
});
});
- describe('User generated comments', async () => {
- 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.null;
- });
-
- 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.null;
- });
- });
-
describe('System generated comments', () => {
it('Adds a comment when the due date changes on a task', async () => {
await app.grantAccess({
@@ -391,52 +342,5 @@ describe('Permissions checker for EditTask', async () => {
expect(comment).not.to.be.null;
});
});
-
- 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;
- });
});
});
From 2ddc2d8171394692b85914bd17e59819cf721dec Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Fri, 2 Aug 2024 09:19:40 +1200
Subject: [PATCH 8/9] Don't navigate back on successful task save
---
.../src/api/mutations/useEditTask.ts | 2 ++
.../Tasks/TaskDetails/TaskDetails.tsx | 32 +++++++++++--------
2 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/packages/datatrak-web/src/api/mutations/useEditTask.ts b/packages/datatrak-web/src/api/mutations/useEditTask.ts
index 89c2d73a92..4fb206477c 100644
--- a/packages/datatrak-web/src/api/mutations/useEditTask.ts
+++ b/packages/datatrak-web/src/api/mutations/useEditTask.ts
@@ -6,6 +6,7 @@
import { useMutation, useQueryClient } from 'react-query';
import { Task } from '@tupaia/types';
import { put } from '../api';
+import { successToast } from '../../utils';
type PartialTask = Partial;
@@ -21,6 +22,7 @@ export const useEditTask = (taskId?: Task['id'], onSuccess?: () => void) => {
onSuccess: () => {
queryClient.invalidateQueries('tasks');
queryClient.invalidateQueries(['tasks', taskId]);
+ successToast('Task updated successfully');
if (onSuccess) onSuccess();
},
},
diff --git a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
index 602bcd607c..865e1709bd 100644
--- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
+++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
@@ -3,8 +3,7 @@
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
-import React from 'react';
-import { useNavigate } from 'react-router';
+import React, { useEffect, useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import styled from 'styled-components';
import { Paper } from '@material-ui/core';
@@ -12,13 +11,11 @@ import { TaskStatus } from '@tupaia/types';
import { LoadingContainer } from '@tupaia/ui-components';
import { useEditTask } from '../../../api';
import { Button } from '../../../components';
-import { useFromLocation } from '../../../utils';
import { SingleTaskResponse } from '../../../types';
import { RepeatScheduleInput } from '../RepeatScheduleInput';
import { DueDatePicker } from '../DueDatePicker';
import { AssigneeInput } from '../AssigneeInput';
import { TaskForm } from '../TaskForm';
-import { ROUTES } from '../../../constants';
import { TaskMetadata } from './TaskMetadata';
import { TaskComments } from './TaskComments';
@@ -94,14 +91,12 @@ const Wrapper = styled.div`
`;
export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => {
- const navigate = useNavigate();
- const backLink = useFromLocation();
-
- const defaultValues = {
+ const [defaultValues, setDefaultValues] = useState({
due_date: task.dueDate ?? null,
repeat_schedule: task.repeatSchedule?.frequency ?? null,
assignee_id: task.assigneeId ?? null,
- };
+ });
+
const formContext = useForm({
mode: 'onChange',
defaultValues,
@@ -114,11 +109,7 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => {
reset,
} = formContext;
- const navigateBack = () => {
- navigate(backLink || ROUTES.TASKS);
- };
-
- const { mutate: editTask, isLoading: isSaving } = useEditTask(task.id, navigateBack);
+ const { mutate: editTask, isLoading: isSaving } = useEditTask(task.id);
const isDirty = Object.keys(dirtyFields).length > 0;
@@ -126,6 +117,19 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => {
reset();
};
+ // Reset form when task changes, i.e after task is saved and the task is re-fetched
+ useEffect(() => {
+ const newDefaultValues = {
+ due_date: task.dueDate ?? null,
+ repeat_schedule: task.repeatSchedule?.frequency ?? null,
+ assignee_id: task.assigneeId ?? null,
+ };
+
+ setDefaultValues(newDefaultValues);
+
+ reset(newDefaultValues);
+ }, [JSON.stringify(task)]);
+
const canEditFields =
task.taskStatus !== TaskStatus.completed && task.taskStatus !== TaskStatus.cancelled;
From a9cca75286b9f1df19c1f104728b1fe5faea2254 Mon Sep 17 00:00:00 2001
From: alexd-bes <129009580+alexd-bes@users.noreply.github.com>
Date: Mon, 5 Aug 2024 12:08:47 +1200
Subject: [PATCH 9/9] Remove double default values
---
.../src/features/Tasks/TaskDetails/TaskDetails.tsx | 3 ---
1 file changed, 3 deletions(-)
diff --git a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
index 865e1709bd..2e4c29c368 100644
--- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
+++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx
@@ -157,7 +157,6 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => {
(
{
(
{
(