From ef8ae5161441da2b256736ebc89a2f79e3a474e2 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:09:50 +1200 Subject: [PATCH 1/8] Create survey_response_id column --- ...AddTaskSurveyResponseId-modifies-schema.js | 40 +++++++++++++++++++ packages/types/src/schemas/schemas.ts | 12 ++++++ packages/types/src/types/models.ts | 3 ++ 3 files changed, 55 insertions(+) create mode 100644 packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js diff --git a/packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js b/packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js new file mode 100644 index 0000000000..6e2b8d1ba8 --- /dev/null +++ b/packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js @@ -0,0 +1,40 @@ +'use strict'; + +var dbm; +var type; +var seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function (options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = async function (db) { + await db.addColumn('task', 'survey_response_id', { + type: 'text', + foreignKey: { + name: 'task_survey_response_id_fk', + table: 'survey_response', + mapping: 'id', + // Don't cascade delete, as we want to keep the task even if the survey response is deleted + rules: {}, + }, + ifNotExists: true, + }); + return db.runSql(` + CREATE INDEX task_survey_response_id_idx ON task USING btree (survey_response_id); + `); +}; + +exports.down = function (db) { + return db.removeColumn('task', 'survey_response_id'); +}; + +exports._meta = { + version: 1, +}; diff --git a/packages/types/src/schemas/schemas.ts b/packages/types/src/schemas/schemas.ts index c96fb458bb..ee732e663a 100644 --- a/packages/types/src/schemas/schemas.ts +++ b/packages/types/src/schemas/schemas.ts @@ -85732,6 +85732,9 @@ export const TaskSchema = { }, "survey_id": { "type": "string" + }, + "survey_response_id": { + "type": "string" } }, "additionalProperties": false, @@ -85774,6 +85777,9 @@ export const TaskCreateSchema = { }, "survey_id": { "type": "string" + }, + "survey_response_id": { + "type": "string" } }, "additionalProperties": false, @@ -85817,6 +85823,9 @@ export const TaskUpdateSchema = { }, "survey_id": { "type": "string" + }, + "survey_response_id": { + "type": "string" } }, "additionalProperties": false @@ -87895,6 +87904,9 @@ export const TaskResponseSchema = { ], "type": "string" }, + "surveyResponseId": { + "type": "string" + }, "assigneeName": { "type": "string" }, diff --git a/packages/types/src/types/models.ts b/packages/types/src/types/models.ts index 62e6c132f8..60ad113173 100644 --- a/packages/types/src/types/models.ts +++ b/packages/types/src/types/models.ts @@ -1542,6 +1542,7 @@ export interface Task { 'repeat_schedule'?: {} | null; 'status'?: TaskStatus | null; 'survey_id': string; + 'survey_response_id'?: string | null; } export interface TaskCreate { 'assignee_id'?: string | null; @@ -1551,6 +1552,7 @@ export interface TaskCreate { 'repeat_schedule'?: {} | null; 'status'?: TaskStatus | null; 'survey_id': string; + 'survey_response_id'?: string | null; } export interface TaskUpdate { 'assignee_id'?: string | null; @@ -1561,6 +1563,7 @@ export interface TaskUpdate { 'repeat_schedule'?: {} | null; 'status'?: TaskStatus | null; 'survey_id'?: string; + 'survey_response_id'?: string | null; } export interface TupaiaWebSession { 'access_policy': {}; From 8bbf28bc9b21fe86fd95d47b4d6bba0f83f4d9ed Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:39:57 +1200 Subject: [PATCH 2/8] Update change handler --- .../TaskCompletionHandler.test.js | 24 +++++++----- .../changeHandlers/TaskCompletionHandler.js | 38 +++++++++++++------ ...AddTaskSurveyResponseId-modifies-schema.js | 7 +++- 3 files changed, 46 insertions(+), 23 deletions(-) diff --git a/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js b/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js index 770a54b610..e6a36ec052 100644 --- a/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js +++ b/packages/database/src/__tests__/changeHandlers/TaskCompletionHandler.test.js @@ -44,6 +44,7 @@ describe('TaskCompletionHandler', () => { data_time: datetime, user_id: userId, survey_id: SURVEY.id, + id: generateId(), ...otherFields, }; }), @@ -52,11 +53,12 @@ describe('TaskCompletionHandler', () => { return surveyResponses.map(sr => sr.id); }; - const assertTaskStatus = async (taskId, expectedStatus) => { + const assertTaskStatus = async (taskId, expectedStatus, expectedSurveyResponseId) => { await models.database.waitForAllChangeHandlers(); const task = await models.task.findById(taskId); expect(task.status).toBe(expectedStatus); + expect(task.survey_response_id).toBe(expectedSurveyResponseId); }; let tonga; @@ -71,6 +73,7 @@ describe('TaskCompletionHandler', () => { created_at: '2024-07-08', status: 'to_do', due_date: '2024-07-25', + survey_response_id: null, }); await upsertDummyRecord(models.user, { id: userId }); }); @@ -82,23 +85,26 @@ describe('TaskCompletionHandler', () => { afterEach(async () => { taskCompletionHandler.stopListeningForChanges(); await models.surveyResponse.delete({ survey_id: SURVEY.id }); - await models.task.update({ id: task.id }, { status: 'to_do' }); + await models.task.update({ id: task.id }, { status: 'to_do', survey_response_id: null }); }); describe('creating a survey response', () => { - it('created response marks associated tasks as completed if created_time < data_time', async () => { - await createResponses([{ entity_id: tonga.id, date: '2024-07-20' }]); - await assertTaskStatus(task.id, 'completed'); + it('created response marks associated tasks as completed if created_time < data_time, and links survey response IDs to the task', async () => { + const responseIds = await createResponses([ + { entity_id: tonga.id, date: '2024-07-20' }, + { entity_id: tonga.id, date: '2024-07-21' }, + ]); + await assertTaskStatus(task.id, 'completed', responseIds[0]); }); it('created response marks associated tasks as completed if created_time === data_time', async () => { - await createResponses([{ entity_id: tonga.id, date: '2024-07-08' }]); - await assertTaskStatus(task.id, 'completed'); + const responseIds = await createResponses([{ entity_id: tonga.id, date: '2024-07-08' }]); + await assertTaskStatus(task.id, 'completed', responseIds[0]); }); it('created response does not mark associated tasks as completed if created_time > data_time', async () => { await createResponses([{ entity_id: tonga.id, date: '2021-07-08' }]); - await assertTaskStatus(task.id, 'to_do'); + await assertTaskStatus(task.id, 'to_do', null); }); }); @@ -106,7 +112,7 @@ describe('TaskCompletionHandler', () => { it('updating a survey response does not mark a task as completed', async () => { await createResponses([{ entity_id: tonga.id, date: '2021-07-20' }]); await models.surveyResponse.update({ entity_id: tonga.id }, { data_time: '2024-07-25' }); - await assertTaskStatus(task.id, 'to_do'); + await assertTaskStatus(task.id, 'to_do', null); }); }); }); diff --git a/packages/database/src/changeHandlers/TaskCompletionHandler.js b/packages/database/src/changeHandlers/TaskCompletionHandler.js index 3fe85d2b4a..0f6daeb931 100644 --- a/packages/database/src/changeHandlers/TaskCompletionHandler.js +++ b/packages/database/src/changeHandlers/TaskCompletionHandler.js @@ -31,9 +31,9 @@ export class TaskCompletionHandler extends ChangeHandler { } /** - * @private Fetches all tasks that have the same survey_id and entity_id as the survey responses, and have a created_at date that is less than or equal to the data_time of the survey response + * @private Fetches all tasks that have the same survey_id and entity_id as the survey responses, and have a created_at date that is less than or equal to the data_time of the survey response, and returns a map of survey response ids to task ids */ - async fetchTaskIdsToUpdate(surveyResponses) { + async fetchTasksForSurveyResponses(surveyResponses) { const surveyIdAndEntityIdPairs = getUniqueEntries( surveyResponses.map(surveyResponse => ({ surveyId: surveyResponse.survey_id, @@ -57,23 +57,37 @@ export class TaskCompletionHandler extends ChangeHandler { }, }); - return tasks.map(task => task.id); + const mappedTasksToSurveyResponses = {}; + + // map the task ids to the survey response ids + tasks.forEach(task => { + const { survey_id: surveyId, entity_id: entityId, created_at: createdAt, id } = task; + const matchingSurveyResponse = surveyResponses.find( + surveyResponse => + surveyResponse.survey_id === surveyId && + surveyResponse.entity_id === entityId && + surveyResponse.data_time >= createdAt, + ); + if (matchingSurveyResponse) { + mappedTasksToSurveyResponses[id] = matchingSurveyResponse.id; + } + }); + return mappedTasksToSurveyResponses; } async handleChanges(transactingModels, changedResponses) { // if there are no changed responses, we don't need to do anything if (changedResponses.length === 0) return; - const taskIdsToUpdate = await this.fetchTaskIdsToUpdate(changedResponses); + const tasksToUpdate = await this.fetchTasksForSurveyResponses(changedResponses); // if there are no tasks to update, we don't need to do anything - if (taskIdsToUpdate.length === 0) return; + if (Object.values(tasksToUpdate).length === 0) return; - // update the tasks to be completed - await transactingModels.task.update( - { - id: taskIdsToUpdate, - }, - { status: 'completed' }, - ); + for (const [taskId, surveyResponseId] of Object.entries(tasksToUpdate)) { + await this.models.task.updateById(taskId, { + status: 'completed', + survey_response_id: surveyResponseId, + }); + } } } diff --git a/packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js b/packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js index 6e2b8d1ba8..4360c0c9f8 100644 --- a/packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js +++ b/packages/database/src/migrations/20240715234341-AddTaskSurveyResponseId-modifies-schema.js @@ -21,8 +21,11 @@ exports.up = async function (db) { name: 'task_survey_response_id_fk', table: 'survey_response', mapping: 'id', - // Don't cascade delete, as we want to keep the task even if the survey response is deleted - rules: {}, + // Don't cascade delete, as we want to keep the task even if the survey response is deleted, just set as null + rules: { + onDelete: 'SET NULL', + onUpdate: 'CASCADE', + }, }, ifNotExists: true, }); From cd3ae75cf866ee0406122cf0040dedbb46cd6437 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:02:18 +1200 Subject: [PATCH 3/8] View completed survey --- .../src/routes/TaskRoute.ts | 1 + .../src/views/Tasks/TaskDetailsPage.tsx | 39 +++++++++++++------ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/datatrak-web-server/src/routes/TaskRoute.ts b/packages/datatrak-web-server/src/routes/TaskRoute.ts index 3c4ee61a53..af54de4782 100644 --- a/packages/datatrak-web-server/src/routes/TaskRoute.ts +++ b/packages/datatrak-web-server/src/routes/TaskRoute.ts @@ -28,6 +28,7 @@ const FIELDS = [ 'repeat_schedule', 'survey_id', 'entity_id', + 'survey_response_id', ]; export class TaskRoute extends Route { diff --git a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx index 8f17cbe7e5..0b557c6754 100644 --- a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx +++ b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx @@ -24,9 +24,6 @@ export const TaskDetailsPage = () => { const { taskId } = useParams(); const { data: task, isLoading } = useTask(taskId); - const showCompleteButton = - task && task.taskStatus !== TaskStatus.completed && task.taskStatus !== TaskStatus.cancelled; - const surveyUrl = task ? generatePath(ROUTES.SURVEY_SCREEN, { countryCode: task?.entity?.countryCode, @@ -36,17 +33,37 @@ export const TaskDetailsPage = () => { : ''; const from = useFromLocation(); + + const ButtonComponent = () => { + if (!task) return null; + switch (task.taskStatus) { + case TaskStatus.cancelled: { + return null; + } + case TaskStatus.completed: { + return ( + + ); + } + default: { + return ( + + ); + } + } + }; + return ( <> - {showCompleteButton && ( - - - - - )} + + + {task && } + {isLoading && } {task && } From b009a73f8265d6b3d8623066ff5726a9d34deada Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:15:09 +1200 Subject: [PATCH 4/8] Error handling --- .../src/views/Tasks/TaskDetailsPage.tsx | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx index 0b557c6754..215c5b7c43 100644 --- a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx +++ b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx @@ -3,11 +3,12 @@ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ -import React from 'react'; +import React, { useState } from 'react'; import { generatePath, useParams } from 'react-router-dom'; import styled from 'styled-components'; +import { Typography } from '@material-ui/core'; import { TaskStatus } from '@tupaia/types'; -import { SpinningLoader } from '@tupaia/ui-components'; +import { Modal, ModalCenteredContent, SpinningLoader } from '@tupaia/ui-components'; import { Button } from '../../components'; import { TaskDetails, TaskPageHeader, TaskActionsMenu } from '../../features'; import { useTask } from '../../api'; @@ -20,7 +21,32 @@ const ButtonWrapper = styled.div` flex: 1; `; +const ErrorModal = ({ isOpen, onClose }) => { + return ( + + + + This response has since been deleted in the admin panel. Please contact your system + administrator for further questions. + + + + ); +}; + export const TaskDetailsPage = () => { + const [errorModalOpen, setErrorModalOpen] = useState(false); const { taskId } = useParams(); const { data: task, isLoading } = useTask(taskId); @@ -36,25 +62,25 @@ export const TaskDetailsPage = () => { const ButtonComponent = () => { if (!task) return null; - switch (task.taskStatus) { - case TaskStatus.cancelled: { - return null; - } - case TaskStatus.completed: { + if (task.taskStatus === TaskStatus.cancelled) return null; + if (task.taskStatus === TaskStatus.completed) { + if (!task.surveyResponseId) return ( - ); - } - default: { - return ( - - ); - } + return ( + + ); } + return ( + + ); }; return ( @@ -67,6 +93,7 @@ export const TaskDetailsPage = () => { {isLoading && } {task && } + setErrorModalOpen(false)} /> ); }; From 0c261189a50ef4243689ee020ec6b71a69b4eca7 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Wed, 17 Jul 2024 09:21:20 +1200 Subject: [PATCH 5/8] Fix comment --- packages/database/src/changeHandlers/TaskCompletionHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/database/src/changeHandlers/TaskCompletionHandler.js b/packages/database/src/changeHandlers/TaskCompletionHandler.js index 22b8ba5a9f..391909c392 100644 --- a/packages/database/src/changeHandlers/TaskCompletionHandler.js +++ b/packages/database/src/changeHandlers/TaskCompletionHandler.js @@ -31,7 +31,7 @@ export class TaskCompletionHandler extends ChangeHandler { } /** - * @private Fetches all tasks that have the same survey_id and entity_id as the survey responses, and have a created_at date that is less than or equal to the data_time of the survey response, and returns a map of survey response ids to task ids + * @private Fetches all tasks that have the same survey_id and entity_id as the survey responses, and have a created_at date that is less than or equal to the data_time of the survey response */ async fetchTasksForSurveyResponses(surveyResponses) { const surveyIdAndEntityIdPairs = getUniqueEntries( From 96486682925cd19e93c5d3700165b73fec8f8697 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 18 Jul 2024 09:43:19 +1200 Subject: [PATCH 6/8] fix tasks button --- .../Tasks/TasksTable/ActionButton.tsx | 4 +- .../Tasks/TasksTable/TaskCompleteButton.tsx | 55 ------------------- 2 files changed, 3 insertions(+), 56 deletions(-) delete mode 100644 packages/datatrak-web/src/features/Tasks/TasksTable/TaskCompleteButton.tsx diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/ActionButton.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/ActionButton.tsx index e448cad31f..0bde4ab494 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/ActionButton.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/ActionButton.tsx @@ -51,10 +51,12 @@ export const ActionButton = ({ task }: ActionButtonProps) => { ); } - const surveyLink = generatePath(ROUTES.SURVEY, { + const path = generatePath(ROUTES.SURVEY, { surveyCode: survey.code, countryCode: entity.countryCode, }); + // Link needs to include page number because if the redirect happens, the "from" state is lost + const surveyLink = `${path}/1`; return ( { - const location = useLocation(); - if (!task) return null; - const { assigneeId, survey, entity, status } = task; - if (status === TaskStatus.cancelled || status === TaskStatus.completed) return null; - if (!assigneeId) { - return Assign; - } - - const surveyLink = generatePath(ROUTES.SURVEY, { - surveyCode: survey.code, - countryCode: entity.countryCode, - }); - return ( - - Complete - - ); -}; From fe4a45749d3341204b6e62a6fcb538c6bcdb0acd Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 18 Jul 2024 09:49:15 +1200 Subject: [PATCH 7/8] removeTaskFilterSetting on logout --- packages/datatrak-web/src/api/mutations/useLogout.ts | 8 ++++---- packages/datatrak-web/src/utils/index.ts | 6 +++++- packages/datatrak-web/src/utils/taskFilterSettings.ts | 4 ++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/datatrak-web/src/api/mutations/useLogout.ts b/packages/datatrak-web/src/api/mutations/useLogout.ts index 14466b4814..1b7d2cd93c 100644 --- a/packages/datatrak-web/src/api/mutations/useLogout.ts +++ b/packages/datatrak-web/src/api/mutations/useLogout.ts @@ -5,7 +5,7 @@ import { useMutation, useQueryClient } from 'react-query'; import { post } from '../api'; -import { setTaskFilterSetting } from '../../utils'; +import { removeTaskFilterSetting } from '../../utils'; export const useLogout = () => { const queryClient = useQueryClient(); @@ -13,9 +13,9 @@ export const useLogout = () => { return useMutation('logout', () => post('logout'), { onSuccess: () => { queryClient.invalidateQueries(); - setTaskFilterSetting('all_assignees_tasks', false); - setTaskFilterSetting('show_completed_tasks', false); - setTaskFilterSetting('show_cancelled_tasks', false); + removeTaskFilterSetting('all_assignees_tasks'); + removeTaskFilterSetting('show_completed_tasks'); + removeTaskFilterSetting('show_cancelled_tasks'); }, }); }; diff --git a/packages/datatrak-web/src/utils/index.ts b/packages/datatrak-web/src/utils/index.ts index 672d643364..d1005e21a3 100644 --- a/packages/datatrak-web/src/utils/index.ts +++ b/packages/datatrak-web/src/utils/index.ts @@ -9,4 +9,8 @@ export { useFromLocation } from './useFromLocation'; export * from './date'; export * from './detectDevice'; export { gaEvent } from './ga'; -export { setTaskFilterSetting, getTaskFilterSetting } from './taskFilterSettings'; +export { + setTaskFilterSetting, + getTaskFilterSetting, + removeTaskFilterSetting, +} from './taskFilterSettings'; diff --git a/packages/datatrak-web/src/utils/taskFilterSettings.ts b/packages/datatrak-web/src/utils/taskFilterSettings.ts index 6f145ceda7..049f870758 100644 --- a/packages/datatrak-web/src/utils/taskFilterSettings.ts +++ b/packages/datatrak-web/src/utils/taskFilterSettings.ts @@ -22,3 +22,7 @@ export const setTaskFilterSetting = (cookieName: TaskFilterType, value: boolean) ...(!isDev && { domain: '.tupaia.org' }), }); }; + +export const removeTaskFilterSetting = (cookieName: TaskFilterType): boolean => { + return Cookies.remove(cookieName); +}; From aec25ed746d2b8c7023b46c864196e0401b48264 Mon Sep 17 00:00:00 2001 From: alexd-bes <129009580+alexd-bes@users.noreply.github.com> Date: Fri, 19 Jul 2024 08:55:10 +1200 Subject: [PATCH 8/8] PR fixes --- .../src/views/Tasks/TaskDetailsPage.tsx | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx index 215c5b7c43..302359a6cd 100644 --- a/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx +++ b/packages/datatrak-web/src/views/Tasks/TaskDetailsPage.tsx @@ -14,6 +14,7 @@ import { TaskDetails, TaskPageHeader, TaskActionsMenu } from '../../features'; import { useTask } from '../../api'; import { ROUTES } from '../../constants'; import { useFromLocation } from '../../utils'; +import { Task } from '../../types'; const ButtonWrapper = styled.div` display: flex; @@ -45,10 +46,10 @@ const ErrorModal = ({ isOpen, onClose }) => { ); }; -export const TaskDetailsPage = () => { - const [errorModalOpen, setErrorModalOpen] = useState(false); - const { taskId } = useParams(); - const { data: task, isLoading } = useTask(taskId); +const ButtonComponent = ({ task, openErrorModal }: { task?: Task; openErrorModal: () => void }) => { + const from = useFromLocation(); + + if (!task) return null; const surveyUrl = task ? generatePath(ROUTES.SURVEY_SCREEN, { @@ -58,36 +59,37 @@ export const TaskDetailsPage = () => { }) : ''; - const from = useFromLocation(); - - const ButtonComponent = () => { - if (!task) return null; - if (task.taskStatus === TaskStatus.cancelled) return null; - if (task.taskStatus === TaskStatus.completed) { - if (!task.surveyResponseId) - return ( - - ); + if (task.taskStatus === TaskStatus.cancelled) return null; + if (task.taskStatus === TaskStatus.completed) { + if (!task.surveyResponseId) return ( - ); - } return ( - ); - }; + } + return ( + + ); +}; + +export const TaskDetailsPage = () => { + const [errorModalOpen, setErrorModalOpen] = useState(false); + const { taskId } = useParams(); + const { data: task, isLoading } = useTask(taskId); return ( <> - + setErrorModalOpen(true)} /> {task && }