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'; 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; }); 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; - }); }); }); 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/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/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/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..80e8907d07 100644 --- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx +++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskComments.tsx @@ -5,20 +5,26 @@ 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'; import { displayDateTime } from '../../../utils'; import { SingleTaskResponse } from '../../../types'; +import { TaskForm } from '../TaskForm'; +import { Button } from '../../../components'; +import { useCreateTaskComment } from '../../../api'; -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; + max-height: 18rem; `; const CommentContainer = styled.div` @@ -28,12 +34,34 @@ const CommentContainer = styled.div` } `; +const CommentsInput = styled(TextField).attrs({ + multiline: true, + variant: 'outlined', + fullWidth: true, + rows: 5, +})` + margin-block: 1.2rem; + height: 9.5rem; + .MuiOutlinedInput-inputMultiline.MuiInputBase-input { + padding-inline: 1rem; + padding-block: 1rem; + } +`; + 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 +79,36 @@ const SingleComment = ({ comment }: { comment: Comments[0] }) => { }; export const TaskComments = ({ comments }: { comments: Comments }) => { + const { taskId } = useParams(); + + const { + register, + handleSubmit, + reset, + formState: { isDirty }, + } = useForm({ + defaultValues: { + comment: '', + }, + }); + + const { mutate: createTaskComment, isLoading: isSaving } = useCreateTaskComment(taskId, reset); + + const onSubmit = data => { + createTaskComment(data.comment); + }; + return ( - - {comments.map((comment, index) => ( - - ))} - +
+ + {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..2e4c29c368 100644 --- a/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx +++ b/packages/datatrak-web/src/features/Tasks/TaskDetails/TaskDetails.tsx @@ -3,22 +3,19 @@ * 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'; 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'; 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'; @@ -41,9 +38,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 +56,7 @@ const SideColumn = styled.div` flex-direction: column; justify-content: space-between; ${({ theme }) => theme.breakpoints.up('md')} { - width: 28%; + width: 25%; } `; @@ -62,18 +66,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,20 +80,23 @@ 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}; } `; 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, @@ -110,16 +105,11 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => { control, handleSubmit, watch, - register, formState: { dirtyFields }, 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; @@ -127,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; @@ -142,83 +145,80 @@ export const TaskDetails = ({ task }: { task: SingleTaskResponse }) => { }; return ( -
+ - - - - - ( - - )} - /> - - - - ( - - )} - /> - - - ( - - )} - /> - + + + + + + ( + + )} + /> + + + + ( + + )} + /> + + + ( + + )} + /> + + + + Clear changes + + + + - - - - - Clear changes - - - - + - +
); }; 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..bf1a8889d3 100644 --- a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx +++ b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx @@ -9,13 +9,25 @@ import styled from 'styled-components'; import { Typography } from '@material-ui/core'; 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'; import { useFromLocation } from '../../utils'; import { SingleTaskResponse } from '../../types'; +const BackButton = styled(Button)` + position: absolute; + left: 0; + min-width: 0; + color: ${({ theme }) => theme.palette.text.primary}; + padding: 0.7rem; + border-radius: 50%; + .MuiSvgIcon-root { + font-size: 1.3rem; + } +`; + 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 && }