From ffe158643f415e1bcc38a08816afe2db2eb3874c Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Tue, 25 Jun 2024 15:40:40 +1200 Subject: [PATCH 01/24] add back to tasks button --- .../src/api/mutations/useSubmitSurveyResponse.ts | 3 +++ .../Survey/Components/SurveySideMenu/SurveySideMenu.tsx | 4 +++- .../src/features/Survey/Screens/SurveySuccessScreen.tsx | 5 ++++- .../datatrak-web/src/features/Survey/SurveyLayout.tsx | 8 ++++++-- .../src/features/Tasks/TasksTable/TasksTable.tsx | 4 ++-- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts index 72b4b05ee8..22f9b51b55 100644 --- a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts +++ b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts @@ -12,6 +12,7 @@ import { ROUTES } from '../../constants'; import { getAllSurveyComponents, useSurveyForm } from '../../features'; import { useSurvey } from '../queries'; import { gaEvent, successToast } from '../../utils'; +import { useLocation } from 'react-router-dom'; type Answer = string | number | boolean | null | undefined; @@ -37,6 +38,7 @@ export const useSurveyResponseData = () => { export const useSubmitSurveyResponse = () => { const queryClient = useQueryClient(); + const location = useLocation(); const navigate = useNavigate(); const params = useParams(); const { resetForm } = useSurveyForm(); @@ -86,6 +88,7 @@ export const useSubmitSurveyResponse = () => { // include the survey response data in the location state, so that we can use it to generate QR codes navigate(generatePath(ROUTES.SURVEY_SUCCESS, params), { state: { + from: location?.state?.from, surveyResponse: JSON.stringify(data), }, }); diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx index 75254ad3db..f134c94ae2 100644 --- a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx +++ b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; import styled from 'styled-components'; -import { To, Link as RouterLink } from 'react-router-dom'; +import { To, Link as RouterLink, useLocation } from 'react-router-dom'; import { useFormContext } from 'react-hook-form'; import { Drawer as BaseDrawer, ListItem, List, ButtonProps } from '@material-ui/core'; import { useIsMobile } from '../../../../utils'; @@ -94,6 +94,7 @@ const Header = styled.div` export const SurveySideMenu = () => { const { getValues } = useFormContext(); + const location = useLocation(); const isMobile = useIsMobile(); const { sideMenuOpen, @@ -139,6 +140,7 @@ export const SurveySideMenu = () => {
  • { const navigate = useNavigate(); const { resetForm } = useSurveyForm(); const { data: survey } = useSurvey(params.surveyCode); + const from = useFromLocation(); + const returnTo = from === '/tasks' ? '/tasks' : '/'; const repeatSurvey = () => { resetForm(); @@ -109,7 +112,7 @@ export const SurveySuccessScreen = () => { Repeat Survey )} - diff --git a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx index e2dcb63419..0d65116c46 100644 --- a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx +++ b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { Outlet, generatePath, useNavigate, useParams } from 'react-router'; +import { useLocation, Outlet, generatePath, useNavigate, useParams } from 'react-router'; import { useFormContext } from 'react-hook-form'; import styled from 'styled-components'; import { Paper as MuiPaper } from '@material-ui/core'; @@ -71,6 +71,7 @@ const LoadingContainer = styled.div` */ export const SurveyLayout = () => { const navigate = useNavigate(); + const location = useLocation(); const params = useParams(); const { updateFormData, @@ -89,7 +90,9 @@ export const SurveyLayout = () => { const handleStep = (path, data) => { updateFormData({ ...formData, ...data }); - navigate(path); + navigate(path, { + state: location.state, + }); }; const onStepPrevious = () => { @@ -136,6 +139,7 @@ export const SurveyLayout = () => { }), { state: { + ...location.state, errors: stringifiedErrors, }, }, diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx index 90051ba714..663156e1ec 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx @@ -64,10 +64,10 @@ const ActionButton = (task: Task) => { return ( Complete From 0744bfd41f6168ddadfaeb6d0bd8032bfca9fc2d Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Tue, 25 Jun 2024 16:15:53 +1200 Subject: [PATCH 02/24] use useFromLocation --- .../src/api/mutations/useSubmitSurveyResponse.ts | 7 +++---- .../Survey/Components/SurveySideMenu/SurveySideMenu.tsx | 9 +++++---- .../src/features/Survey/Screens/SurveySuccessScreen.tsx | 2 +- .../datatrak-web/src/features/Survey/SurveyLayout.tsx | 9 +++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts index 22f9b51b55..ff1512694b 100644 --- a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts +++ b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts @@ -11,8 +11,7 @@ import { post, useCurrentUserContext, useEntityByCode } from '..'; import { ROUTES } from '../../constants'; import { getAllSurveyComponents, useSurveyForm } from '../../features'; import { useSurvey } from '../queries'; -import { gaEvent, successToast } from '../../utils'; -import { useLocation } from 'react-router-dom'; +import { gaEvent, successToast, useFromLocation } from '../../utils'; type Answer = string | number | boolean | null | undefined; @@ -38,7 +37,7 @@ export const useSurveyResponseData = () => { export const useSubmitSurveyResponse = () => { const queryClient = useQueryClient(); - const location = useLocation(); + const from = useFromLocation(); const navigate = useNavigate(); const params = useParams(); const { resetForm } = useSurveyForm(); @@ -88,7 +87,7 @@ export const useSubmitSurveyResponse = () => { // include the survey response data in the location state, so that we can use it to generate QR codes navigate(generatePath(ROUTES.SURVEY_SUCCESS, params), { state: { - from: location?.state?.from, + from: from, surveyResponse: JSON.stringify(data), }, }); diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx index f134c94ae2..977f08cc48 100644 --- a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx +++ b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx @@ -4,10 +4,10 @@ */ import React from 'react'; import styled from 'styled-components'; -import { To, Link as RouterLink, useLocation } from 'react-router-dom'; +import { To, Link as RouterLink } from 'react-router-dom'; import { useFormContext } from 'react-hook-form'; import { Drawer as BaseDrawer, ListItem, List, ButtonProps } from '@material-ui/core'; -import { useIsMobile } from '../../../../utils'; +import { useFromLocation, useIsMobile } from '../../../../utils'; import { getSurveyScreenNumber } from '../../utils'; import { useSurveyForm } from '../../SurveyContext'; import { SideMenuButton } from './SideMenuButton'; @@ -45,6 +45,7 @@ const SurveyMenuItem = styled(ListItem).attrs({ component: RouterLink, variant: 'text', color: 'default', + state: {}, })< ButtonProps & { to: To; @@ -94,7 +95,7 @@ const Header = styled.div` export const SurveySideMenu = () => { const { getValues } = useFormContext(); - const location = useLocation(); + const from = useFromLocation(); const isMobile = useIsMobile(); const { sideMenuOpen, @@ -140,7 +141,7 @@ export const SurveySideMenu = () => {
  • { const { resetForm } = useSurveyForm(); const { data: survey } = useSurvey(params.surveyCode); const from = useFromLocation(); - const returnTo = from === '/tasks' ? '/tasks' : '/'; + const returnTo = from ? from : '/'; const repeatSurvey = () => { resetForm(); diff --git a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx index 0d65116c46..d2c8a3c6d5 100644 --- a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx +++ b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { useLocation, Outlet, generatePath, useNavigate, useParams } from 'react-router'; +import { Outlet, generatePath, useNavigate, useParams } from 'react-router'; import { useFormContext } from 'react-hook-form'; import styled from 'styled-components'; import { Paper as MuiPaper } from '@material-ui/core'; @@ -15,6 +15,7 @@ import { SIDE_MENU_WIDTH, SurveySideMenu } from './Components'; import { ROUTES } from '../../constants'; import { useSubmitSurveyResponse } from '../../api/mutations'; import { getErrorsByScreen } from './utils'; +import { useFromLocation } from '../../utils'; const ScrollableLayout = styled.div<{ $sideMenuClosed?: boolean; @@ -71,7 +72,7 @@ const LoadingContainer = styled.div` */ export const SurveyLayout = () => { const navigate = useNavigate(); - const location = useLocation(); + const from = useFromLocation(); const params = useParams(); const { updateFormData, @@ -91,7 +92,7 @@ export const SurveyLayout = () => { const handleStep = (path, data) => { updateFormData({ ...formData, ...data }); navigate(path, { - state: location.state, + state: { from }, }); }; @@ -139,7 +140,7 @@ export const SurveyLayout = () => { }), { state: { - ...location.state, + from, errors: stringifiedErrors, }, }, From e9c87234aade3598ae5059964fcdd8bc965c3356 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Tue, 25 Jun 2024 16:32:01 +1200 Subject: [PATCH 03/24] update types --- .../src/api/mutations/useSubmitSurveyResponse.ts | 2 +- .../Survey/Components/SurveySideMenu/SurveySideMenu.tsx | 6 ++++-- packages/datatrak-web/src/features/Survey/SurveyLayout.tsx | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts index ff1512694b..b5152751d6 100644 --- a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts +++ b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts @@ -87,7 +87,7 @@ export const useSubmitSurveyResponse = () => { // include the survey response data in the location state, so that we can use it to generate QR codes navigate(generatePath(ROUTES.SURVEY_SUCCESS, params), { state: { - from: from, + ...(from && { from }), surveyResponse: JSON.stringify(data), }, }); diff --git a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx index 977f08cc48..9654dd0619 100644 --- a/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx +++ b/packages/datatrak-web/src/features/Survey/Components/SurveySideMenu/SurveySideMenu.tsx @@ -45,12 +45,14 @@ const SurveyMenuItem = styled(ListItem).attrs({ component: RouterLink, variant: 'text', color: 'default', - state: {}, })< ButtonProps & { to: To; $active?: boolean; $isInstructionOnly?: boolean; + state: { + from?: string | undefined; + }; } >` padding: 0.5rem; @@ -141,7 +143,7 @@ export const SurveySideMenu = () => {
  • { const handleStep = (path, data) => { updateFormData({ ...formData, ...data }); navigate(path, { - state: { from }, + state: { ...(from && { from }) }, }); }; @@ -140,7 +140,7 @@ export const SurveyLayout = () => { }), { state: { - from, + ...(from && { from }), errors: stringifiedErrors, }, }, From 8e1fceef9609fce365eb4165e799edb2baff9563 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Tue, 25 Jun 2024 16:44:44 +1200 Subject: [PATCH 04/24] Update SurveySuccessScreen.tsx --- .../Survey/Screens/SurveySuccessScreen.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/datatrak-web/src/features/Survey/Screens/SurveySuccessScreen.tsx b/packages/datatrak-web/src/features/Survey/Screens/SurveySuccessScreen.tsx index 4ac871c588..2b2dc36e28 100644 --- a/packages/datatrak-web/src/features/Survey/Screens/SurveySuccessScreen.tsx +++ b/packages/datatrak-web/src/features/Survey/Screens/SurveySuccessScreen.tsx @@ -70,15 +70,25 @@ const Button = styled(BaseButton)` } `; +const ReturnButton = () => { + const from = useFromLocation(); + return from === ROUTES.TASKS ? ( + + ) : ( + + ); +}; + export const SurveySuccessScreen = () => { const { isLoggedIn } = useCurrentUserContext(); const params = useParams(); const navigate = useNavigate(); const { resetForm } = useSurveyForm(); const { data: survey } = useSurvey(params.surveyCode); - const from = useFromLocation(); - const returnTo = from ? from : '/'; - const repeatSurvey = () => { resetForm(); const path = generatePath(ROUTES.SURVEY_SCREEN, { @@ -112,9 +122,7 @@ export const SurveySuccessScreen = () => { Repeat Survey )} - + )} From 93c4922739619caf39a1b7b2710e9e667bea8200 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 27 Jun 2024 17:06:32 +1200 Subject: [PATCH 05/24] save task endpoint --- .../datatrak-web-server/src/app/createApp.ts | 68 ++++++++++--------- .../src/routes/SaveTaskRoute.ts | 44 ++++++++++++ .../datatrak-web-server/src/routes/index.ts | 1 + 3 files changed, 82 insertions(+), 31 deletions(-) create mode 100644 packages/datatrak-web-server/src/routes/SaveTaskRoute.ts diff --git a/packages/datatrak-web-server/src/app/createApp.ts b/packages/datatrak-web-server/src/app/createApp.ts index 8227e79a5c..ff2606aa97 100644 --- a/packages/datatrak-web-server/src/app/createApp.ts +++ b/packages/datatrak-web-server/src/app/createApp.ts @@ -6,47 +6,49 @@ import { Request } from 'express'; import { TupaiaDatabase } from '@tupaia/database'; import { - OrchestratorApiBuilder, - handleWith, attachSessionIfAvailable, - SessionSwitchingAuthHandler, forwardRequest, + handleWith, + OrchestratorApiBuilder, + SessionSwitchingAuthHandler, } from '@tupaia/server-boilerplate'; import { getEnvVarOrDefault } from '@tupaia/utils'; import { DataTrakSessionModel } from '../models'; import { - UserRoute, - UserRequest, - SurveysRoute, - SurveysRequest, - SurveyResponsesRequest, - SurveyResponsesRoute, - ProjectsRoute, - ProjectsRequest, - SurveyRequest, - SurveyRoute, - SingleEntityRequest, - SingleEntityRoute, + ActivityFeedRequest, + ActivityFeedRoute, + EntitiesRequest, + EntitiesRoute, EntityDescendantsRequest, EntityDescendantsRoute, + GenerateLoginTokenRequest, + GenerateLoginTokenRoute, + LeaderboardRequest, + LeaderboardRoute, ProjectRequest, ProjectRoute, - SubmitSurveyResponseRoute, - SubmitSurveyResponseRequest, + ProjectsRequest, + ProjectsRoute, RecentSurveysRequest, RecentSurveysRoute, - LeaderboardRequest, - LeaderboardRoute, - ActivityFeedRequest, - ActivityFeedRoute, - SingleSurveyResponseRoute, + SaveTaskRequest, + SaveTaskRoute, + SingleEntityRequest, + SingleEntityRoute, SingleSurveyResponseRequest, - EntitiesRoute, - EntitiesRequest, - GenerateLoginTokenRoute, - GenerateLoginTokenRequest, + SingleSurveyResponseRoute, + SubmitSurveyResponseRequest, + SubmitSurveyResponseRoute, + SurveyRequest, + SurveyResponsesRequest, + SurveyResponsesRoute, + SurveyRoute, + SurveysRequest, + SurveysRoute, TasksRequest, TasksRoute, + UserRequest, + UserRoute, } from '../routes'; import { attachAccessPolicy } from './middleware'; @@ -63,11 +65,7 @@ export async function createApp() { .useAttachSession(attachSessionIfAvailable) .use('*', attachAccessPolicy) .attachApiClientToContext(authHandlerProvider) - .post( - 'submitSurveyResponse', - handleWith(SubmitSurveyResponseRoute), - ) - .post('generateLoginToken', handleWith(GenerateLoginTokenRoute)) + // Get Routes .get('getUser', handleWith(UserRoute)) .get('entity/:entityCode', handleWith(SingleEntityRoute)) .get('entityDescendants', handleWith(EntityDescendantsRoute)) @@ -82,6 +80,14 @@ export async function createApp() { .get('activityFeed', handleWith(ActivityFeedRoute)) .get('tasks', handleWith(TasksRoute)) .get('surveyResponse/:id', handleWith(SingleSurveyResponseRoute)) + // Post Routes + .post( + 'submitSurveyResponse', + handleWith(SubmitSurveyResponseRoute), + ) + .post('generateLoginToken', handleWith(GenerateLoginTokenRoute)) + // Put Routes + .put('tasks/:taskId', handleWith(SaveTaskRoute)) // Forward auth requests to web-config .use('signup', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider })) .use('resendEmail', forwardRequest(WEB_CONFIG_API_URL, { authHandlerProvider })) diff --git a/packages/datatrak-web-server/src/routes/SaveTaskRoute.ts b/packages/datatrak-web-server/src/routes/SaveTaskRoute.ts new file mode 100644 index 0000000000..460ee26186 --- /dev/null +++ b/packages/datatrak-web-server/src/routes/SaveTaskRoute.ts @@ -0,0 +1,44 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ +import { Request } from 'express'; +import { KeysToCamelCase, Task } from '@tupaia/types'; +import { Route } from '@tupaia/server-boilerplate'; + +// Todo: update api type with this +type TaskRequest = KeysToCamelCase; + +type Params = Record; +type ResBody = { + id: string; + message: string; +}; +type ReqBody = Record; +type ReqQuery = Record; + +export type SaveTaskRequest = Request; + +export class SaveTaskRoute extends Route { + public async buildResponse() { + const { status } = this.req.body; + const { taskId } = this.req.params; + const { task } = this.req.models; + + let result; + + // Update task if id exists + if (taskId) { + console.log('Update task', taskId, status); + await task.updateById(taskId, { status }); + } else { + // Todo: create new task record + console.log('create new task record'); + } + + return { + id: taskId, + message: 'Task saved successfully', + }; + } +} diff --git a/packages/datatrak-web-server/src/routes/index.ts b/packages/datatrak-web-server/src/routes/index.ts index 5d52969cae..b5f86b2162 100644 --- a/packages/datatrak-web-server/src/routes/index.ts +++ b/packages/datatrak-web-server/src/routes/index.ts @@ -22,3 +22,4 @@ export { ActivityFeedRequest, ActivityFeedRoute } from './ActivityFeedRoute'; export { EntitiesRequest, EntitiesRoute } from './EntitiesRoute'; export { GenerateLoginTokenRequest, GenerateLoginTokenRoute } from './GenerateLoginTokenRoute'; export { TasksRequest, TasksRoute } from './TasksRoute'; +export { SaveTaskRequest, SaveTaskRoute } from './SaveTaskRoute'; From 998c0e1ff8b91ea16a109f212b43399dd3eef090 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 27 Jun 2024 17:06:46 +1200 Subject: [PATCH 06/24] save task mutation --- .../datatrak-web/src/api/mutations/index.ts | 1 + .../src/api/mutations/useEditTask.ts | 29 +++++++++++++++ .../features/Tasks/TasksTable/TasksTable.tsx | 37 ++++++++++++++++--- 3 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 packages/datatrak-web/src/api/mutations/useEditTask.ts diff --git a/packages/datatrak-web/src/api/mutations/index.ts b/packages/datatrak-web/src/api/mutations/index.ts index 7bd41ae67c..e01e823e91 100644 --- a/packages/datatrak-web/src/api/mutations/index.ts +++ b/packages/datatrak-web/src/api/mutations/index.ts @@ -7,6 +7,7 @@ export { useLogin } from './useLogin'; export { useLogout } from './useLogout'; export { useRegister } from './useRegister'; export { useEditUser } from './useEditUser'; +export { useEditTask } from './useEditTask'; export { useResendVerificationEmail } from './useResendVerificationEmail'; export { useRequestProjectAccess } from './useRequestProjectAccess'; export { useSubmitSurveyResponse } from './useSubmitSurveyResponse'; diff --git a/packages/datatrak-web/src/api/mutations/useEditTask.ts b/packages/datatrak-web/src/api/mutations/useEditTask.ts new file mode 100644 index 0000000000..723b7d60ea --- /dev/null +++ b/packages/datatrak-web/src/api/mutations/useEditTask.ts @@ -0,0 +1,29 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd + */ +import { useMutation, useQueryClient } from 'react-query'; +import { Task } from '@tupaia/types'; +import { put } from '../api'; + +export const useEditTask = (onSuccess?: () => void) => { + const queryClient = useQueryClient(); + + return useMutation( + async (task: Task) => { + if (!task) return; + + console.log('task', task); + + const updates = Object.fromEntries(Object.entries(task).map(([key, value]) => [key, value])); + + await put(`tasks/${task.id}`, { data: updates }); + }, + { + onSuccess: () => { + queryClient.invalidateQueries('tasks'); + if (onSuccess) onSuccess(); + }, + }, + ); +}; diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx index 663156e1ec..5a59cbbc3a 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx @@ -5,12 +5,12 @@ import React from 'react'; import styled from 'styled-components'; -import { generatePath, useSearchParams, Link, useLocation } from 'react-router-dom'; -import { FilterableTable } from '@tupaia/ui-components'; +import { generatePath, Link, useLocation, useSearchParams } from 'react-router-dom'; +import { ActionsMenu, FilterableTable } from '@tupaia/ui-components'; import { DatatrakWebTasksRequest, TaskStatus } from '@tupaia/types'; import { TaskStatusType } from '../../../types'; import { Button } from '../../../components'; -import { useCurrentUserContext, useTasks } from '../../../api'; +import { useCurrentUserContext, useEditTask, useTasks } from '../../../api'; import { displayDate } from '../../../utils'; import { ROUTES } from '../../../constants'; import { StatusPill } from '../StatusPill'; @@ -25,7 +25,6 @@ const ActionButtonComponent = styled(Button).attrs({ })` padding-inline: 1.2rem; padding-block: 0.4rem; - width: 100%; .MuiButton-label { font-size: 0.75rem; line-height: normal; @@ -75,6 +74,33 @@ const ActionButton = (task: Task) => { ); }; +export const ActionsMenuButton = ({ id }) => { + const { mutate, isLoading } = useEditTask(); + + console.log('is loading', isLoading); + const cancelTask = () => { + console.log('cancel task', id); + + mutate({ id, status: TaskStatus.cancelled }); + }; + + const actions = [ + { + label: 'Cancel task', + action: cancelTask, + }, + ]; + + return ; +}; + +const Actions = row => ( +
    + + +
    +); + const COLUMNS = [ { // only the survey name can be resized @@ -124,7 +150,8 @@ const COLUMNS = [ }, { Header: '', - accessor: row => , + accessor: row => , + width: 190, id: 'actions', filterable: false, disableSortBy: true, From 71b98b248386330674a8a6280a490b8c539254ed Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Mon, 1 Jul 2024 11:20:11 +1200 Subject: [PATCH 07/24] task cancel modal --- .../src/api/mutations/useEditTask.ts | 15 ++- .../src/components/SmallModal.tsx | 7 +- .../Tasks/TasksTable/TaskActionsMenu.tsx | 71 ++++++++++++++ .../Tasks/TasksTable/TaskCompleteButton.tsx | 55 +++++++++++ .../features/Tasks/TasksTable/TasksTable.tsx | 95 ++++--------------- packages/datatrak-web/src/types/task.ts | 5 +- .../src/components/ActionsMenu.tsx | 30 ++++-- 7 files changed, 179 insertions(+), 99 deletions(-) create mode 100644 packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx create mode 100644 packages/datatrak-web/src/features/Tasks/TasksTable/TaskCompleteButton.tsx diff --git a/packages/datatrak-web/src/api/mutations/useEditTask.ts b/packages/datatrak-web/src/api/mutations/useEditTask.ts index 723b7d60ea..42fa44efb8 100644 --- a/packages/datatrak-web/src/api/mutations/useEditTask.ts +++ b/packages/datatrak-web/src/api/mutations/useEditTask.ts @@ -3,21 +3,18 @@ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ import { useMutation, useQueryClient } from 'react-query'; -import { Task } from '@tupaia/types'; +import { Task } from '../../types'; import { put } from '../api'; +type PartialTask = Partial; + export const useEditTask = (onSuccess?: () => void) => { const queryClient = useQueryClient(); - return useMutation( - async (task: Task) => { + return useMutation( + async (task: PartialTask) => { if (!task) return; - - console.log('task', task); - - const updates = Object.fromEntries(Object.entries(task).map(([key, value]) => [key, value])); - - await put(`tasks/${task.id}`, { data: updates }); + await put(`tasks/${task.id}`, { data: task }); }, { onSuccess: () => { diff --git a/packages/datatrak-web/src/components/SmallModal.tsx b/packages/datatrak-web/src/components/SmallModal.tsx index 72e5978ba7..0be2c9069b 100644 --- a/packages/datatrak-web/src/components/SmallModal.tsx +++ b/packages/datatrak-web/src/components/SmallModal.tsx @@ -12,6 +12,7 @@ import { Button, Modal } from '.'; const Wrapper = styled.div` width: 25rem; padding: 0 2rem 1rem 2rem; + text-wrap: initial; `; const ButtonWrapper = styled.div` @@ -54,6 +55,7 @@ interface ModalProps { primaryButton?: ButtonProps | null; secondaryButton?: ButtonProps | null; children?: ReactNode; + isLoading: boolean; } export const SmallModal = ({ @@ -63,6 +65,7 @@ export const SmallModal = ({ primaryButton, secondaryButton, children, + isLoading = false, }: ModalProps) => { return ( @@ -80,7 +83,9 @@ export const SmallModal = ({ )} {primaryButton && ( - {primaryButton.label} + + {primaryButton.label} + )} diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx new file mode 100644 index 0000000000..cf5de6aa80 --- /dev/null +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx @@ -0,0 +1,71 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import React, { useState } from 'react'; +import { IconButton, Typography } from '@material-ui/core'; +import { TaskStatus } from '@tupaia/types'; +import { ActionsMenu } from '@tupaia/ui-components'; +import styled from 'styled-components'; +import { useEditTask } from '../../../api'; +import { SmallModal } from '../../../components'; + +const MenuButton = styled(IconButton)` + &.MuiIconButton-root { + padding: 0.4rem; + margin-left: 0; + } +`; + +const CancelTaskModal = ({ isOpen, onClose, onCancelTask, isLoading }) => ( + + + Are you sure you would like to cancel this task? This cannot be undone. + + +); + +export const TaskActionsMenu = ({ id: taskId }) => { + const [isOpen, setIsOpen] = useState(false); + const onOpen = () => setIsOpen(true); + const onClose = () => setIsOpen(false); + + const { mutate, isLoading } = useEditTask(onClose); + + const onCancelTask = () => { + mutate({ id: taskId, status: TaskStatus.cancelled }); + }; + + const actions = [ + { + label: 'Cancel task', + action: onOpen, + }, + ]; + + return ( + <> + + + + ); +}; diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TaskCompleteButton.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TaskCompleteButton.tsx new file mode 100644 index 0000000000..6415a7ef9b --- /dev/null +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TaskCompleteButton.tsx @@ -0,0 +1,55 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ + +import React from 'react'; +import styled from 'styled-components'; +import { generatePath, Link, useLocation } from 'react-router-dom'; +import { TaskStatus } from '@tupaia/types'; +import { ROUTES } from '../../../constants'; +import { Button } from '../../../components'; +import { Task } from '../../../types'; + +const ActionButtonComponent = styled(Button).attrs({ + color: 'primary', + size: 'small', +})` + padding-inline: 1.2rem; + padding-block: 0.4rem; + .MuiButton-label { + font-size: 0.75rem; + line-height: normal; + } + .cell-content:has(&) { + padding-block: 0.2rem; + padding-inline-start: 1.5rem; + } +`; + +export const TaskCompleteButton = (task: Task) => { + 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 + + ); +}; diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx index 5a59cbbc3a..dc289412bf 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx @@ -5,35 +5,16 @@ import React from 'react'; import styled from 'styled-components'; -import { generatePath, Link, useLocation, useSearchParams } from 'react-router-dom'; -import { ActionsMenu, FilterableTable } from '@tupaia/ui-components'; -import { DatatrakWebTasksRequest, TaskStatus } from '@tupaia/types'; +import { useSearchParams } from 'react-router-dom'; +import { FilterableTable } from '@tupaia/ui-components'; import { TaskStatusType } from '../../../types'; -import { Button } from '../../../components'; -import { useCurrentUserContext, useEditTask, useTasks } from '../../../api'; +import { useCurrentUserContext, useTasks } from '../../../api'; import { displayDate } from '../../../utils'; -import { ROUTES } from '../../../constants'; import { StatusPill } from '../StatusPill'; import { StatusFilter } from './StatusFilter'; import { DueDateFilter } from './DueDateFilter'; - -type Task = DatatrakWebTasksRequest.ResBody['tasks'][0]; - -const ActionButtonComponent = styled(Button).attrs({ - color: 'primary', - size: 'small', -})` - padding-inline: 1.2rem; - padding-block: 0.4rem; - .MuiButton-label { - font-size: 0.75rem; - line-height: normal; - } - .cell-content:has(&) { - padding-block: 0.2rem; - padding-inline-start: 1.5rem; - } -`; +import { TaskActionsMenu } from './TaskActionsMenu'; +import { TaskCompleteButton } from './TaskCompleteButton'; const Container = styled.div` display: flex; @@ -47,59 +28,10 @@ const Container = styled.div` } `; -const ActionButton = (task: Task) => { - 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 - - ); -}; - -export const ActionsMenuButton = ({ id }) => { - const { mutate, isLoading } = useEditTask(); - - console.log('is loading', isLoading); - const cancelTask = () => { - console.log('cancel task', id); - - mutate({ id, status: TaskStatus.cancelled }); - }; - - const actions = [ - { - label: 'Cancel task', - action: cancelTask, - }, - ]; - - return ; -}; - -const Actions = row => ( -
    - - -
    -); +const ActionsCell = styled.div` + margin-top: 5px; + margin-bottom: 2px; +`; const COLUMNS = [ { @@ -150,8 +82,13 @@ const COLUMNS = [ }, { Header: '', - accessor: row => , - width: 190, + accessor: row => ( + + + + + ), + width: 180, id: 'actions', filterable: false, disableSortBy: true, diff --git a/packages/datatrak-web/src/types/task.ts b/packages/datatrak-web/src/types/task.ts index d0ad15daca..26a60a4af1 100644 --- a/packages/datatrak-web/src/types/task.ts +++ b/packages/datatrak-web/src/types/task.ts @@ -3,10 +3,13 @@ * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ -import { TaskStatus } from '@tupaia/types'; +import { DatatrakWebTasksRequest, TaskStatus } from '@tupaia/types'; + enum OtherTaskStatus { overdue = 'overdue', repeating = 'repeating', } export type TaskStatusType = TaskStatus | OtherTaskStatus; + +export type Task = DatatrakWebTasksRequest.ResBody['tasks'][0]; diff --git a/packages/ui-components/src/components/ActionsMenu.tsx b/packages/ui-components/src/components/ActionsMenu.tsx index 2d0724ec7e..3e53a03a96 100644 --- a/packages/ui-components/src/components/ActionsMenu.tsx +++ b/packages/ui-components/src/components/ActionsMenu.tsx @@ -5,20 +5,30 @@ import React from 'react'; import { + IconButton as MuiIconButton, ListItemIcon, - MenuItem as MuiMenuItem, Menu as MuiMenu, - IconButton, - Typography, + MenuItem as MuiMenuItem, Tooltip, + Typography, } from '@material-ui/core'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import styled from 'styled-components'; import { ActionsMenuOptionType } from '../types'; +const StyledMenu = styled(MuiMenu)` + .MuiPaper-root { + border: 1px solid ${props => props.theme.palette.divider}; + } + .MuiList-root { + padding: 0.2rem; + } +`; + const StyledMenuItem = styled(MuiMenuItem)` - padding-top: 0.625rem; - padding-bottom: 0.625rem; + padding: 0.3rem 0.3rem; + font-size: 0.75rem; + min-width: 5rem; `; const StyledMenuIcon = styled(MoreVertIcon)` @@ -40,6 +50,7 @@ type ActionMenuProps = { vertical?: 'top' | 'bottom'; horizontal?: 'left' | 'right'; }; + IconButton?: typeof MuiIconButton; }; export const ActionsMenu = ({ @@ -47,6 +58,7 @@ export const ActionsMenu = ({ includesIcons = false, anchorOrigin = {}, transformOrigin = {}, + IconButton = MuiIconButton, }: ActionMenuProps) => { const [anchorEl, setAnchorEl] = React.useState<(EventTarget & HTMLButtonElement) | null>(null); return ( @@ -54,7 +66,7 @@ export const ActionsMenu = ({ setAnchorEl(event.currentTarget)}> - setAnchorEl(null)} anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', + vertical: 'top', + horizontal: 'right', ...anchorOrigin, }} transformOrigin={{ horizontal: 'right', vertical: 'top', ...transformOrigin }} @@ -104,7 +116,7 @@ export const ActionsMenu = ({ ), )} - + ); }; From 2ce4e582415a29fd35db58f53664b423b86b686a Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Mon, 1 Jul 2024 11:39:59 +1200 Subject: [PATCH 08/24] update types --- .../src/routes/SaveTaskRoute.ts | 24 +++++++------------ .../src/components/SmallModal.tsx | 2 +- .../Tasks/TasksTable/TaskActionsMenu.tsx | 12 ++++++++-- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/packages/datatrak-web-server/src/routes/SaveTaskRoute.ts b/packages/datatrak-web-server/src/routes/SaveTaskRoute.ts index 460ee26186..33b6974bef 100644 --- a/packages/datatrak-web-server/src/routes/SaveTaskRoute.ts +++ b/packages/datatrak-web-server/src/routes/SaveTaskRoute.ts @@ -6,18 +6,15 @@ import { Request } from 'express'; import { KeysToCamelCase, Task } from '@tupaia/types'; import { Route } from '@tupaia/server-boilerplate'; -// Todo: update api type with this -type TaskRequest = KeysToCamelCase; - -type Params = Record; -type ResBody = { - id: string; - message: string; -}; -type ReqBody = Record; -type ReqQuery = Record; - -export type SaveTaskRequest = Request; +export type SaveTaskRequest = Request< + Record, + { + id: string; + message: string; + }, + KeysToCamelCase, + Record +>; export class SaveTaskRoute extends Route { public async buildResponse() { @@ -25,11 +22,8 @@ export class SaveTaskRoute extends Route { const { taskId } = this.req.params; const { task } = this.req.models; - let result; - // Update task if id exists if (taskId) { - console.log('Update task', taskId, status); await task.updateById(taskId, { status }); } else { // Todo: create new task record diff --git a/packages/datatrak-web/src/components/SmallModal.tsx b/packages/datatrak-web/src/components/SmallModal.tsx index 0be2c9069b..e537b78009 100644 --- a/packages/datatrak-web/src/components/SmallModal.tsx +++ b/packages/datatrak-web/src/components/SmallModal.tsx @@ -55,7 +55,7 @@ interface ModalProps { primaryButton?: ButtonProps | null; secondaryButton?: ButtonProps | null; children?: ReactNode; - isLoading: boolean; + isLoading?: boolean; } export const SmallModal = ({ diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx index cf5de6aa80..8e9655c1f5 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TaskActionsMenu.tsx @@ -10,6 +10,7 @@ import { ActionsMenu } from '@tupaia/ui-components'; import styled from 'styled-components'; import { useEditTask } from '../../../api'; import { SmallModal } from '../../../components'; +import { Task } from '../../../types'; const MenuButton = styled(IconButton)` &.MuiIconButton-root { @@ -18,7 +19,14 @@ const MenuButton = styled(IconButton)` } `; -const CancelTaskModal = ({ isOpen, onClose, onCancelTask, isLoading }) => ( +interface ModalProps { + isOpen: boolean; + onClose: () => void; + onCancelTask: () => void; + isLoading: boolean; +} + +const CancelTaskModal = ({ isOpen, onClose, onCancelTask, isLoading }: ModalProps) => ( ( ); -export const TaskActionsMenu = ({ id: taskId }) => { +export const TaskActionsMenu = ({ id: taskId }: Task) => { const [isOpen, setIsOpen] = useState(false); const onOpen = () => setIsOpen(true); const onClose = () => setIsOpen(false); From 5b9055bff302a9551ef346af512d3c12a483f4b8 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Mon, 1 Jul 2024 12:55:33 +1200 Subject: [PATCH 09/24] pass from location into useSubmitSurveyResponse --- .../src/api/mutations/useSubmitSurveyResponse.ts | 7 +++---- packages/datatrak-web/src/features/Survey/SurveyLayout.tsx | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts index b5152751d6..087dacf7d1 100644 --- a/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts +++ b/packages/datatrak-web/src/api/mutations/useSubmitSurveyResponse.ts @@ -11,7 +11,7 @@ import { post, useCurrentUserContext, useEntityByCode } from '..'; import { ROUTES } from '../../constants'; import { getAllSurveyComponents, useSurveyForm } from '../../features'; import { useSurvey } from '../queries'; -import { gaEvent, successToast, useFromLocation } from '../../utils'; +import { gaEvent, successToast } from '../../utils'; type Answer = string | number | boolean | null | undefined; @@ -35,9 +35,8 @@ export const useSurveyResponseData = () => { }; }; -export const useSubmitSurveyResponse = () => { +export const useSubmitSurveyResponse = (fromLocation: string | undefined) => { const queryClient = useQueryClient(); - const from = useFromLocation(); const navigate = useNavigate(); const params = useParams(); const { resetForm } = useSurveyForm(); @@ -87,7 +86,7 @@ export const useSubmitSurveyResponse = () => { // include the survey response data in the location state, so that we can use it to generate QR codes navigate(generatePath(ROUTES.SURVEY_SUCCESS, params), { state: { - ...(from && { from }), + ...(fromLocation && { from: fromLocation }), surveyResponse: JSON.stringify(data), }, }); diff --git a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx index 8edc07064e..eb1bef2c2d 100644 --- a/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx +++ b/packages/datatrak-web/src/features/Survey/SurveyLayout.tsx @@ -87,7 +87,7 @@ export const SurveyLayout = () => { } = useSurveyForm(); const { handleSubmit, getValues } = useFormContext(); const { mutate: submitSurveyResponse, isLoading: isSubmittingSurveyResponse } = - useSubmitSurveyResponse(); + useSubmitSurveyResponse(from); const handleStep = (path, data) => { updateFormData({ ...formData, ...data }); From 20d78bfbe242b849caa4c3e80feee77a22e6e5b2 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Mon, 1 Jul 2024 15:30:55 +1200 Subject: [PATCH 10/24] build ui --- .../Tasks/TasksTable/FilterToolbar.tsx | 85 +++++++++++++++++++ .../features/Tasks/TasksTable/TasksTable.tsx | 2 + 2 files changed, 87 insertions(+) create mode 100644 packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx new file mode 100644 index 0000000000..dec43b3ff9 --- /dev/null +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -0,0 +1,85 @@ +/* + * Tupaia + * Copyright (c) 2017 - 2023 Beyond Essential Systems Pty Ltd + */ +import React from 'react'; +import styled from 'styled-components'; +import { + FormGroup as MuiFormGroup, + FormControlLabel as MuiFormControlLabel, + Checkbox as MuiCheckbox, +} from '@material-ui/core'; + +const Container = styled.div` + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0.3rem 0.3rem 0.2rem; + border-bottom: 1px solid ${({ theme }) => theme.palette.divider}; +`; + +const FormGroup = styled(MuiFormGroup)` + display: flex; + flex-direction: row; +`; + +const FormControlLabel = styled(MuiFormControlLabel)` + .MuiFormControlLabel-label { + font-size: 0.75rem; + color: ${({ theme }) => theme.palette.text.secondary}; + } + + .MuiCheckbox-root { + padding-right: 3px; + } +`; + +const Checkbox = ({ name, value, label, onChange }) => { + return ( + + } + label={label} + /> + ); +}; + +export const FilterToolbar = () => { + const [state, setState] = React.useState({ + allAssignees: true, + allCompleted: false, + allCancelled: false, + }); + + const handleChange = event => { + setState({ ...state, [event.target.name]: event.target.checked }); + }; + + const { allAssignees, allCompleted, allCancelled } = state; + + return ( + + + + + + + + ); +}; diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx index dc289412bf..d776716d35 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx @@ -15,6 +15,7 @@ import { StatusFilter } from './StatusFilter'; import { DueDateFilter } from './DueDateFilter'; import { TaskActionsMenu } from './TaskActionsMenu'; import { TaskCompleteButton } from './TaskCompleteButton'; +import { FilterToolbar } from './FilterToolbar'; const Container = styled.div` display: flex; @@ -173,6 +174,7 @@ export const TasksTable = () => { return ( + Date: Mon, 1 Jul 2024 16:34:54 +1200 Subject: [PATCH 11/24] set up filtering --- .../Tasks/TasksTable/FilterToolbar.tsx | 43 +++++++++++++------ .../features/Tasks/TasksTable/TasksTable.tsx | 2 +- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index dec43b3ff9..801aaae25a 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -4,6 +4,7 @@ */ import React from 'react'; import styled from 'styled-components'; +import keyBy from 'lodash.keyby'; import { FormGroup as MuiFormGroup, FormControlLabel as MuiFormControlLabel, @@ -45,18 +46,32 @@ const Checkbox = ({ name, value, label, onChange }) => { ); }; -export const FilterToolbar = () => { - const [state, setState] = React.useState({ - allAssignees: true, - allCompleted: false, - allCancelled: false, - }); +const customFilters = { + allAssignees: (value, filters) => { + console.log('filter allAssignees', value); + return filters; + }, + allCompleted: (value, filters) => { + return value ? { ...filters, task_status: { id: 'task_status', value: 'to_do' } } : filters; + }, + allCancelled: (value, filters) => { + return value ? { ...filters, task_status: { id: 'task_status', value: 'to_do' } } : filters; + }, +}; +export const FilterToolbar = ({ onChangeFilters, filters }) => { + const filtersById = keyBy(filters, 'id'); + console.log('filters', filters, filtersById); const handleChange = event => { - setState({ ...state, [event.target.name]: event.target.checked }); + const { name: id, checked: value } = event.target; + const customFilter = customFilters[id]; + const updatedFilters = customFilter(value, filters); + console.log('updatedFilters', updatedFilters); + console.log('as array', Object.values(updatedFilters)); + onChangeFilters(Object.values(updatedFilters)); }; - const { allAssignees, allCompleted, allCancelled } = state; + const getValue = id => filtersById[id]?.value || false; return ( @@ -64,20 +79,20 @@ export const FilterToolbar = () => { diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx index d776716d35..cb24089c24 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx @@ -174,7 +174,7 @@ export const TasksTable = () => { return ( - + Date: Tue, 2 Jul 2024 14:09:05 +1200 Subject: [PATCH 12/24] testing filters --- packages/central-server/src/apiV2/tasks/GETTasks.js | 8 ++++++++ packages/datatrak-web/src/api/queries/useTasks.ts | 10 ++++++++++ .../src/features/Tasks/TasksTable/FilterToolbar.tsx | 12 +++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/central-server/src/apiV2/tasks/GETTasks.js b/packages/central-server/src/apiV2/tasks/GETTasks.js index a2b706c6ea..6737ab6c73 100644 --- a/packages/central-server/src/apiV2/tasks/GETTasks.js +++ b/packages/central-server/src/apiV2/tasks/GETTasks.js @@ -7,6 +7,7 @@ import { assertAnyPermissions, assertBESAdminAccess } from '../../permissions'; import { GETHandler } from '../GETHandler'; import { mergeMultiJoin } from '../utilities'; import { assertUserHasTaskPermissions, createTaskDBFilter } from './assertTaskPermissions'; +import { processColumnSelectorKeys } from '../GETHandler/helpers'; export class GETTasks extends GETHandler { permissionsFilteredInternally = true; @@ -25,6 +26,13 @@ export class GETTasks extends GETHandler { return super.findSingleRecord(projectId, options); } + getDbQueryCriteria() { + const { filter: filterString } = this.req.query; + const filter = filterString ? JSON.parse(filterString) : {}; + console.log('FILTER: ', filter); + return processColumnSelectorKeys(this.models, filter, this.recordType); + } + async getDbQueryOptions() { const { multiJoin, sort, ...restOfOptions } = await super.getDbQueryOptions(); diff --git a/packages/datatrak-web/src/api/queries/useTasks.ts b/packages/datatrak-web/src/api/queries/useTasks.ts index 2475ea8b23..21a1af76c2 100644 --- a/packages/datatrak-web/src/api/queries/useTasks.ts +++ b/packages/datatrak-web/src/api/queries/useTasks.ts @@ -24,6 +24,15 @@ export const useTasks = ( filters: Filter[] = [], sortBy?: SortBy[], ) => { + const baseFilters = [ + { + id: 'task_status', + value: { + comparisonValue: 'completed', + comparator: '!=', + }, + }, + ]; return useQuery( ['tasks', projectId, pageSize, page, filters, sortBy], (): Promise => @@ -37,6 +46,7 @@ export const useTasks = ( id: 'survey.project_id', value: projectId, }, + ...baseFilters, ], sort: sortBy?.map(({ id, desc }) => `${id} ${desc ? 'DESC' : 'ASC'}`) ?? [], }, diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index 801aaae25a..7df909d671 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -68,7 +68,17 @@ export const FilterToolbar = ({ onChangeFilters, filters }) => { const updatedFilters = customFilter(value, filters); console.log('updatedFilters', updatedFilters); console.log('as array', Object.values(updatedFilters)); - onChangeFilters(Object.values(updatedFilters)); + + const foo = [ + { + id: 'task_status', + value: { + comparisonValue: 'completed', + comparator: '!=', + }, + }, + ]; + onChangeFilters(foo); }; const getValue = id => filtersById[id]?.value || false; From 8e4bfc38f939115c04d84ad1b98e362709f6066a Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Tue, 2 Jul 2024 17:33:23 +1200 Subject: [PATCH 13/24] Update useTasks.ts --- .../datatrak-web/src/api/queries/useTasks.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/datatrak-web/src/api/queries/useTasks.ts b/packages/datatrak-web/src/api/queries/useTasks.ts index 21a1af76c2..126e10bc97 100644 --- a/packages/datatrak-web/src/api/queries/useTasks.ts +++ b/packages/datatrak-web/src/api/queries/useTasks.ts @@ -17,6 +17,20 @@ type SortBy = { desc: boolean; }; +const getProcessedFilters = (filters: Filter[], { projectId }) => { + const processedFilters = filters; + // Todo: task_status filter + // If one is already selected do nothing + // If none is selected, add one based on the table filter settings + return [ + ...filters, + { + id: 'survey.project_id', + value: projectId, + }, + ]; +}; + export const useTasks = ( projectId?: string, pageSize?: number, @@ -24,15 +38,8 @@ export const useTasks = ( filters: Filter[] = [], sortBy?: SortBy[], ) => { - const baseFilters = [ - { - id: 'task_status', - value: { - comparisonValue: 'completed', - comparator: '!=', - }, - }, - ]; + const processedFilters = getProcessedFilters(filters, { projectId }); + return useQuery( ['tasks', projectId, pageSize, page, filters, sortBy], (): Promise => @@ -40,14 +47,7 @@ export const useTasks = ( params: { pageSize, page, - filters: [ - ...filters, - { - id: 'survey.project_id', - value: projectId, - }, - ...baseFilters, - ], + filters: processedFilters, sort: sortBy?.map(({ id, desc }) => `${id} ${desc ? 'DESC' : 'ASC'}`) ?? [], }, enabled: !!projectId, From 073a95e09526269000231780d62c7a05690e45c9 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 4 Jul 2024 10:45:11 +1200 Subject: [PATCH 14/24] fix front end --- .../src/apiV2/tasks/GETTasks.js | 8 ---- .../src/routes/TasksRoute.ts | 8 ++++ .../datatrak-web/src/api/queries/useTasks.ts | 24 +++------- .../Tasks/TasksTable/FilterToolbar.tsx | 44 ++++--------------- 4 files changed, 24 insertions(+), 60 deletions(-) diff --git a/packages/central-server/src/apiV2/tasks/GETTasks.js b/packages/central-server/src/apiV2/tasks/GETTasks.js index 6737ab6c73..a2b706c6ea 100644 --- a/packages/central-server/src/apiV2/tasks/GETTasks.js +++ b/packages/central-server/src/apiV2/tasks/GETTasks.js @@ -7,7 +7,6 @@ import { assertAnyPermissions, assertBESAdminAccess } from '../../permissions'; import { GETHandler } from '../GETHandler'; import { mergeMultiJoin } from '../utilities'; import { assertUserHasTaskPermissions, createTaskDBFilter } from './assertTaskPermissions'; -import { processColumnSelectorKeys } from '../GETHandler/helpers'; export class GETTasks extends GETHandler { permissionsFilteredInternally = true; @@ -26,13 +25,6 @@ export class GETTasks extends GETHandler { return super.findSingleRecord(projectId, options); } - getDbQueryCriteria() { - const { filter: filterString } = this.req.query; - const filter = filterString ? JSON.parse(filterString) : {}; - console.log('FILTER: ', filter); - return processColumnSelectorKeys(this.models, filter, this.recordType); - } - async getDbQueryOptions() { const { multiJoin, sort, ...restOfOptions } = await super.getDbQueryOptions(); diff --git a/packages/datatrak-web-server/src/routes/TasksRoute.ts b/packages/datatrak-web-server/src/routes/TasksRoute.ts index febadd68be..83ebef5481 100644 --- a/packages/datatrak-web-server/src/routes/TasksRoute.ts +++ b/packages/datatrak-web-server/src/routes/TasksRoute.ts @@ -66,6 +66,7 @@ const queryForCount = async (filter: FormattedFilters, models: DatatrakWebServer }); }; +const CUSTOM_FILTERS = ['all_assignees', 'all_completed', 'all_cancelled']; const EQUALITY_FILTERS = ['due_date', 'survey.project_id', 'task_status']; const formatFilters = (filters: Record[]) => { @@ -73,6 +74,11 @@ const formatFilters = (filters: Record[]) => { filters.forEach(({ id, value }) => { if (value === '' || value === undefined || value === null) return; + + if (CUSTOM_FILTERS.includes(id)) { + console.log('ID', id); + return; + } if (EQUALITY_FILTERS.includes(id)) { formattedFilters[id] = value; return; @@ -99,6 +105,8 @@ export class TasksRoute extends Route { const { ctx, query = {}, models } = this.req; const { filters = [], pageSize = DEFAULT_PAGE_SIZE, sort, page = 0 } = query; + console.log('FILTERS', filters); + const formattedFilters = formatFilters(filters); const tasks = await ctx.services.central.fetchResources('tasks', { diff --git a/packages/datatrak-web/src/api/queries/useTasks.ts b/packages/datatrak-web/src/api/queries/useTasks.ts index 126e10bc97..ee55d7acd7 100644 --- a/packages/datatrak-web/src/api/queries/useTasks.ts +++ b/packages/datatrak-web/src/api/queries/useTasks.ts @@ -17,20 +17,6 @@ type SortBy = { desc: boolean; }; -const getProcessedFilters = (filters: Filter[], { projectId }) => { - const processedFilters = filters; - // Todo: task_status filter - // If one is already selected do nothing - // If none is selected, add one based on the table filter settings - return [ - ...filters, - { - id: 'survey.project_id', - value: projectId, - }, - ]; -}; - export const useTasks = ( projectId?: string, pageSize?: number, @@ -38,8 +24,6 @@ export const useTasks = ( filters: Filter[] = [], sortBy?: SortBy[], ) => { - const processedFilters = getProcessedFilters(filters, { projectId }); - return useQuery( ['tasks', projectId, pageSize, page, filters, sortBy], (): Promise => @@ -47,7 +31,13 @@ export const useTasks = ( params: { pageSize, page, - filters: processedFilters, + filters: [ + { + id: 'survey.project_id', + value: projectId, + }, + ...filters, + ], sort: sortBy?.map(({ id, desc }) => `${id} ${desc ? 'DESC' : 'ASC'}`) ?? [], }, enabled: !!projectId, diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index 7df909d671..8d9ea084e1 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -46,39 +46,13 @@ const Checkbox = ({ name, value, label, onChange }) => { ); }; -const customFilters = { - allAssignees: (value, filters) => { - console.log('filter allAssignees', value); - return filters; - }, - allCompleted: (value, filters) => { - return value ? { ...filters, task_status: { id: 'task_status', value: 'to_do' } } : filters; - }, - allCancelled: (value, filters) => { - return value ? { ...filters, task_status: { id: 'task_status', value: 'to_do' } } : filters; - }, -}; - export const FilterToolbar = ({ onChangeFilters, filters }) => { const filtersById = keyBy(filters, 'id'); - console.log('filters', filters, filtersById); const handleChange = event => { const { name: id, checked: value } = event.target; - const customFilter = customFilters[id]; - const updatedFilters = customFilter(value, filters); - console.log('updatedFilters', updatedFilters); - console.log('as array', Object.values(updatedFilters)); - - const foo = [ - { - id: 'task_status', - value: { - comparisonValue: 'completed', - comparator: '!=', - }, - }, - ]; - onChangeFilters(foo); + filtersById[id] = { id, value }; + const updatedFilters = Object.values(filtersById); + onChangeFilters(updatedFilters); }; const getValue = id => filtersById[id]?.value || false; @@ -87,21 +61,21 @@ export const FilterToolbar = ({ onChangeFilters, filters }) => { From 027f0d5aac917ce54f92739603895c8d136647dc Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 4 Jul 2024 14:22:12 +1200 Subject: [PATCH 15/24] filtering with cookies --- packages/datatrak-web-server/package.json | 1 + .../src/routes/TasksRoute.ts | 89 +++++++++++++++++-- .../Tasks/TasksTable/FilterToolbar.tsx | 52 ++++++++--- yarn.lock | 3 +- 4 files changed, 124 insertions(+), 21 deletions(-) diff --git a/packages/datatrak-web-server/package.json b/packages/datatrak-web-server/package.json index 043f3eb21f..809643a0d0 100644 --- a/packages/datatrak-web-server/package.json +++ b/packages/datatrak-web-server/package.json @@ -33,6 +33,7 @@ "@tupaia/types": "workspace:*", "@tupaia/utils": "workspace:*", "camelcase-keys": "^6.2.2", + "cookie": "^0.6.0", "express": "^4.19.2", "lodash.groupby": "^4.6.0", "lodash.keyby": "^4.6.0", diff --git a/packages/datatrak-web-server/src/routes/TasksRoute.ts b/packages/datatrak-web-server/src/routes/TasksRoute.ts index 83ebef5481..d6db61f95b 100644 --- a/packages/datatrak-web-server/src/routes/TasksRoute.ts +++ b/packages/datatrak-web-server/src/routes/TasksRoute.ts @@ -2,10 +2,12 @@ * Tupaia * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ - +// @ts-nocheck import { Request } from 'express'; import camelcaseKeys from 'camelcase-keys'; import { Route } from '@tupaia/server-boilerplate'; +import { keyBy } from 'lodash'; +import { parse } from 'cookie'; import { DatatrakWebTasksRequest, Task, TaskStatus } from '@tupaia/types'; import { RECORDS } from '@tupaia/database'; import { DatatrakWebServerModelRegistry } from '../types'; @@ -66,19 +68,85 @@ const queryForCount = async (filter: FormattedFilters, models: DatatrakWebServer }); }; -const CUSTOM_FILTERS = ['all_assignees', 'all_completed', 'all_cancelled']; +const CUSTOM_FILTERS = { + all_assignees: { + comparator: 'eq', + comparisonValue: true, + }, + all_completed: { + comparator: 'eq', + comparisonValue: true, + }, + all_cancelled: { + comparator: 'eq', + comparisonValue: true, + }, +}; + const EQUALITY_FILTERS = ['due_date', 'survey.project_id', 'task_status']; +const processFilterSettings = ( + filters: Record[], + cookieString: string | string[] | undefined, + userId: string, +) => { + if (typeof cookieString !== 'string') return filters; + const filtersById = keyBy(filters, 'id'); + + const cookies = parse(cookieString); + + if (!cookies['all_assignees'] || cookies['all_assignees'] === 'false') { + // add filter for assignee + filtersById['assignee_id'] = { id: 'assignee_id', value: userId }; + } + + if (!cookies['all_completed'] || cookies['all_completed'] === 'false') { + // add filter to remove completed tasks + filtersById['task_status'] = { + id: 'task_status', + value: { + comparator: 'NOT IN', + comparisonValue: ['completed'], + }, + }; + } + + if (!cookies['all_cancelled'] || cookies['all_cancelled'] === 'false') { + // add filter to remove cancelled tasks + filtersById['task_status'] = { + id: 'task_status', + value: { + comparator: 'NOT IN', + comparisonValue: ['cancelled'], + }, + }; + } + + if ( + (!cookies['all_completed'] || cookies['all_completed'] === 'false') && + (!cookies['all_cancelled'] || cookies['all_cancelled'] === 'false') + ) { + // add filter to remove completed and cancelled tasks + filtersById['task_status'] = { + id: 'task_status', + value: { + comparator: 'NOT IN', + comparisonValue: ['completed', 'cancelled'], + }, + }; + } + console.log('cookies', cookies); + const filtersToReturn = Object.values(filtersById); + console.log('filtersToReturn', filtersToReturn); + return filtersToReturn; +}; + const formatFilters = (filters: Record[]) => { let formattedFilters: FormattedFilters = {}; filters.forEach(({ id, value }) => { if (value === '' || value === undefined || value === null) return; - if (CUSTOM_FILTERS.includes(id)) { - console.log('ID', id); - return; - } if (EQUALITY_FILTERS.includes(id)) { formattedFilters[id] = value; return; @@ -102,12 +170,17 @@ const formatFilters = (filters: Record[]) => { }; export class TasksRoute extends Route { public async buildResponse() { - const { ctx, query = {}, models } = this.req; + const { ctx, query = {}, models, headers } = this.req; const { filters = [], pageSize = DEFAULT_PAGE_SIZE, sort, page = 0 } = query; + const { id: userId } = await ctx.services.central.getUser(); + console.log('userId', userId); + + const processedFilters = processFilterSettings(filters, headers.cookie, userId); console.log('FILTERS', filters); + const formattedFilters = formatFilters(processedFilters); - const formattedFilters = formatFilters(filters); + console.log(' --- formattedFilters ---', formattedFilters); const tasks = await ctx.services.central.fetchResources('tasks', { filter: formattedFilters, diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index 8d9ea084e1..87d5fef19a 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -4,7 +4,8 @@ */ import React from 'react'; import styled from 'styled-components'; -import keyBy from 'lodash.keyby'; +import { parse } from 'cookie'; +import { useQueryClient } from 'react-query'; import { FormGroup as MuiFormGroup, FormControlLabel as MuiFormControlLabel, @@ -46,36 +47,63 @@ const Checkbox = ({ name, value, label, onChange }) => { ); }; -export const FilterToolbar = ({ onChangeFilters, filters }) => { - const filtersById = keyBy(filters, 'id'); +const setCookie = (cookieName: string, value: boolean) => { + const date = new Date(); + date.setTime(date.getTime() + 24 * 60 * 60 * 1000); // 24 hours, in milliseconds + const expires = 'expires=' + date.toUTCString(); + document.cookie = `${cookieName}=${value};${expires};path=/`; +}; + +const getCookie = (cookieName: string) => { + // get the cookie + const cname = `${cookieName}=`; + const decodedCookie = decodeURIComponent(document.cookie); + + // split the cookie into an array + + const ca = decodedCookie.split(';'); + // return the value of the cookie if it exists, otherwise return undefined + return ( + ca + .find(cookie => cookie.trim().startsWith(cname)) + ?.trim() + .substring(cname.length) === 'true' + ); +}; + +const getFilterValue = name => { + return getCookie(name); +}; + +export const FilterToolbar = () => { + const queryClient = useQueryClient(); + const handleChange = event => { - const { name: id, checked: value } = event.target; - filtersById[id] = { id, value }; - const updatedFilters = Object.values(filtersById); - onChangeFilters(updatedFilters); + const { name, checked: value } = event.target; + console.log('CHANGE', name, value); + queryClient.invalidateQueries('tasks'); + setCookie(name, value); }; - const getValue = id => filtersById[id]?.value || false; - return ( diff --git a/yarn.lock b/yarn.lock index 88845f8c59..3c6ffeeee4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12041,6 +12041,7 @@ __metadata: "@types/lodash.keyby": ^4.6.0 "@types/lodash.sortby": ^4.6.0 camelcase-keys: ^6.2.2 + cookie: ^0.6.0 express: ^4.19.2 lodash.groupby: ^4.6.0 lodash.keyby: ^4.6.0 @@ -19328,7 +19329,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.6.0": +"cookie@npm:0.6.0, cookie@npm:^0.6.0": version: 0.6.0 resolution: "cookie@npm:0.6.0" checksum: f56a7d32a07db5458e79c726b77e3c2eff655c36792f2b6c58d351fb5f61531e5b1ab7f46987150136e366c65213cbe31729e02a3eaed630c3bf7334635fb410 From 17764047528296056903bf9ddf61021d209c898b Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 4 Jul 2024 15:39:46 +1200 Subject: [PATCH 16/24] update cookie handling --- .../src/routes/TasksRoute.ts | 30 ++++++-------- packages/datatrak-web/package.json | 1 + .../Tasks/TasksTable/FilterToolbar.tsx | 39 +++---------------- .../Tasks/TasksTable/StatusFilter.tsx | 23 ++++++++--- .../features/Tasks/TasksTable/TasksTable.tsx | 2 +- .../Tasks/utils/taskFilterSettings.ts | 15 +++++++ yarn.lock | 8 ++++ 7 files changed, 60 insertions(+), 58 deletions(-) create mode 100644 packages/datatrak-web/src/features/Tasks/utils/taskFilterSettings.ts diff --git a/packages/datatrak-web-server/src/routes/TasksRoute.ts b/packages/datatrak-web-server/src/routes/TasksRoute.ts index d6db61f95b..1233ca92e5 100644 --- a/packages/datatrak-web-server/src/routes/TasksRoute.ts +++ b/packages/datatrak-web-server/src/routes/TasksRoute.ts @@ -9,7 +9,7 @@ import { Route } from '@tupaia/server-boilerplate'; import { keyBy } from 'lodash'; import { parse } from 'cookie'; import { DatatrakWebTasksRequest, Task, TaskStatus } from '@tupaia/types'; -import { RECORDS } from '@tupaia/database'; +import { QUERY_CONJUNCTIONS, RECORDS } from '@tupaia/database'; import { DatatrakWebServerModelRegistry } from '../types'; export type TasksRequest = Request< @@ -68,21 +68,6 @@ const queryForCount = async (filter: FormattedFilters, models: DatatrakWebServer }); }; -const CUSTOM_FILTERS = { - all_assignees: { - comparator: 'eq', - comparisonValue: true, - }, - all_completed: { - comparator: 'eq', - comparisonValue: true, - }, - all_cancelled: { - comparator: 'eq', - comparisonValue: true, - }, -}; - const EQUALITY_FILTERS = ['due_date', 'survey.project_id', 'task_status']; const processFilterSettings = ( @@ -130,8 +115,17 @@ const processFilterSettings = ( filtersById['task_status'] = { id: 'task_status', value: { - comparator: 'NOT IN', - comparisonValue: ['completed', 'cancelled'], + comparator: '=', + comparisonValue: 'to_do', + }, + }; + filtersById[QUERY_CONJUNCTIONS.AND] = { + task_status: { + id: 'task_status', + value: { + comparator: 'NOT IN', + comparisonValue: ['completed', 'cancelled'], + }, }, }; } diff --git a/packages/datatrak-web/package.json b/packages/datatrak-web/package.json index be5744f9a8..3870219198 100644 --- a/packages/datatrak-web/package.json +++ b/packages/datatrak-web/package.json @@ -25,6 +25,7 @@ "bson-objectid": "^1.2.2", "date-fns": "^2.29.2", "downloadjs": "1.4.7", + "js-cookie": "^3.0.5", "leaflet": "^1.7.1", "lodash.throttle": "^4.1.1", "markdown-to-jsx": "^6.4.1", diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index 87d5fef19a..f4c7e3b39c 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -4,13 +4,13 @@ */ import React from 'react'; import styled from 'styled-components'; -import { parse } from 'cookie'; import { useQueryClient } from 'react-query'; import { FormGroup as MuiFormGroup, FormControlLabel as MuiFormControlLabel, Checkbox as MuiCheckbox, } from '@material-ui/core'; +import { getTaskFilterSetting, setTaskFilterSetting } from '../utils/taskFilterSettings.ts'; const Container = styled.div` display: flex; @@ -47,42 +47,13 @@ const Checkbox = ({ name, value, label, onChange }) => { ); }; -const setCookie = (cookieName: string, value: boolean) => { - const date = new Date(); - date.setTime(date.getTime() + 24 * 60 * 60 * 1000); // 24 hours, in milliseconds - const expires = 'expires=' + date.toUTCString(); - document.cookie = `${cookieName}=${value};${expires};path=/`; -}; - -const getCookie = (cookieName: string) => { - // get the cookie - const cname = `${cookieName}=`; - const decodedCookie = decodeURIComponent(document.cookie); - - // split the cookie into an array - - const ca = decodedCookie.split(';'); - // return the value of the cookie if it exists, otherwise return undefined - return ( - ca - .find(cookie => cookie.trim().startsWith(cname)) - ?.trim() - .substring(cname.length) === 'true' - ); -}; - -const getFilterValue = name => { - return getCookie(name); -}; - export const FilterToolbar = () => { const queryClient = useQueryClient(); const handleChange = event => { const { name, checked: value } = event.target; - console.log('CHANGE', name, value); + setTaskFilterSetting(name, value); queryClient.invalidateQueries('tasks'); - setCookie(name, value); }; return ( @@ -91,19 +62,19 @@ export const FilterToolbar = () => { diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx index b104e2634c..3c8f1a9bda 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx @@ -8,6 +8,7 @@ import styled from 'styled-components'; import { MenuItem as MuiMenuItem, Select } from '@material-ui/core'; import { STATUS_VALUES, StatusPill } from '../StatusPill'; import { TaskStatus } from '@tupaia/types'; +import { getTaskFilterSetting } from '../utils/taskFilterSettings.ts'; const PlaceholderText = styled.span` color: ${({ theme }) => theme.palette.text.secondary}; @@ -30,11 +31,23 @@ interface StatusFilterProps { } export const StatusFilter = ({ onChange, filter }: StatusFilterProps) => { - // TODO: Filter/include cancelled and completed statuses as part of RN_1343 - const options = Object.entries(STATUS_VALUES).map(([value, { label }]) => ({ - value, - label, - })); + const includeCompletedTasks = getTaskFilterSetting('all_completed'); + const includeCancelledTasks = getTaskFilterSetting('all_cancelled'); + + const options = Object.entries(STATUS_VALUES) + .filter(([value]) => { + if (!includeCompletedTasks && value === TaskStatus.completed) { + return false; + } + if (!includeCancelledTasks && value === TaskStatus.cancelled) { + return false; + } + return true; + }) + .map(([value, { label }]) => ({ + value, + label, + })); const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { onChange(event.target.value as string); diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx index cb24089c24..d776716d35 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/TasksTable.tsx @@ -174,7 +174,7 @@ export const TasksTable = () => { return ( - + { + return Cookies.get(cookieName) === 'true'; +}; + +export const setTaskFilterSetting = (cookieName: FilterType, value: boolean) => { + return Cookies.set(cookieName, value); +}; diff --git a/yarn.lock b/yarn.lock index 3c6ffeeee4..3dffbebecd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12076,6 +12076,7 @@ __metadata: bson-objectid: ^1.2.2 date-fns: ^2.29.2 downloadjs: 1.4.7 + js-cookie: ^3.0.5 leaflet: ^1.7.1 lodash.throttle: ^4.1.1 markdown-to-jsx: ^6.4.1 @@ -28658,6 +28659,13 @@ __metadata: languageName: node linkType: hard +"js-cookie@npm:^3.0.5": + version: 3.0.5 + resolution: "js-cookie@npm:3.0.5" + checksum: 2dbd2809c6180fbcf060c6957cb82dbb47edae0ead6bd71cbeedf448aa6b6923115003b995f7d3e3077bfe2cb76295ea6b584eb7196cca8ba0a09f389f64967a + languageName: node + linkType: hard + "js-git@npm:^0.7.8": version: 0.7.8 resolution: "js-git@npm:0.7.8" From 583cab502bf75e2c737b2a077dfe2575862d7a3a Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Thu, 4 Jul 2024 17:25:33 +1200 Subject: [PATCH 17/24] clean up types --- .../src/routes/TasksRoute.ts | 43 ++++++++++--------- .../datatrak-web/src/api/queries/useTasks.ts | 2 +- .../src/features/Tasks/StatusPill.tsx | 8 +++- .../Tasks/TasksTable/FilterToolbar.tsx | 1 + .../Tasks/TasksTable/StatusFilter.tsx | 27 ++++++------ 5 files changed, 46 insertions(+), 35 deletions(-) diff --git a/packages/datatrak-web-server/src/routes/TasksRoute.ts b/packages/datatrak-web-server/src/routes/TasksRoute.ts index 1233ca92e5..0a13bff175 100644 --- a/packages/datatrak-web-server/src/routes/TasksRoute.ts +++ b/packages/datatrak-web-server/src/routes/TasksRoute.ts @@ -2,7 +2,6 @@ * Tupaia * Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd */ -// @ts-nocheck import { Request } from 'express'; import camelcaseKeys from 'camelcase-keys'; import { Route } from '@tupaia/server-boilerplate'; @@ -70,22 +69,36 @@ const queryForCount = async (filter: FormattedFilters, models: DatatrakWebServer const EQUALITY_FILTERS = ['due_date', 'survey.project_id', 'task_status']; +const getFilterSettings = (cookieString: string) => { + const cookies = parse(cookieString); + return { + allAssignees: cookies['all_assignees'] === 'true', + allCompleted: cookies['all_completed'] === 'true', + allCancelled: cookies['all_cancelled'] === 'true', + }; +}; + +type Filter = Record; + const processFilterSettings = ( - filters: Record[], + filters: Filter[], cookieString: string | string[] | undefined, userId: string, ) => { - if (typeof cookieString !== 'string') return filters; + if (typeof cookieString !== 'string') { + return filters; + } + const filtersById = keyBy(filters, 'id'); - const cookies = parse(cookieString); + const cookies = getFilterSettings(cookieString); - if (!cookies['all_assignees'] || cookies['all_assignees'] === 'false') { + if (!cookies.allAssignees) { // add filter for assignee filtersById['assignee_id'] = { id: 'assignee_id', value: userId }; } - if (!cookies['all_completed'] || cookies['all_completed'] === 'false') { + if (!cookies.allCompleted) { // add filter to remove completed tasks filtersById['task_status'] = { id: 'task_status', @@ -96,7 +109,7 @@ const processFilterSettings = ( }; } - if (!cookies['all_cancelled'] || cookies['all_cancelled'] === 'false') { + if (!cookies.allCancelled) { // add filter to remove cancelled tasks filtersById['task_status'] = { id: 'task_status', @@ -107,11 +120,9 @@ const processFilterSettings = ( }; } - if ( - (!cookies['all_completed'] || cookies['all_completed'] === 'false') && - (!cookies['all_cancelled'] || cookies['all_cancelled'] === 'false') - ) { + if (!cookies.allCompleted && !cookies.allCancelled) { // add filter to remove completed and cancelled tasks + // Todo: conditionally add the QUERY_CONJUNCTIONS.AND depending on whether or not there is a task_status filter set filtersById['task_status'] = { id: 'task_status', value: { @@ -129,10 +140,7 @@ const processFilterSettings = ( }, }; } - console.log('cookies', cookies); - const filtersToReturn = Object.values(filtersById); - console.log('filtersToReturn', filtersToReturn); - return filtersToReturn; + return Object.values(filtersById); }; const formatFilters = (filters: Record[]) => { @@ -168,14 +176,9 @@ export class TasksRoute extends Route { const { filters = [], pageSize = DEFAULT_PAGE_SIZE, sort, page = 0 } = query; const { id: userId } = await ctx.services.central.getUser(); - console.log('userId', userId); - const processedFilters = processFilterSettings(filters, headers.cookie, userId); - console.log('FILTERS', filters); const formattedFilters = formatFilters(processedFilters); - console.log(' --- formattedFilters ---', formattedFilters); - const tasks = await ctx.services.central.fetchResources('tasks', { filter: formattedFilters, columns: FIELDS, diff --git a/packages/datatrak-web/src/api/queries/useTasks.ts b/packages/datatrak-web/src/api/queries/useTasks.ts index ee55d7acd7..2475ea8b23 100644 --- a/packages/datatrak-web/src/api/queries/useTasks.ts +++ b/packages/datatrak-web/src/api/queries/useTasks.ts @@ -32,11 +32,11 @@ export const useTasks = ( pageSize, page, filters: [ + ...filters, { id: 'survey.project_id', value: projectId, }, - ...filters, ], sort: sortBy?.map(({ id, desc }) => `${id} ${desc ? 'DESC' : 'ASC'}`) ?? [], }, diff --git a/packages/datatrak-web/src/features/Tasks/StatusPill.tsx b/packages/datatrak-web/src/features/Tasks/StatusPill.tsx index d7f3e36cd8..d9094a7d23 100644 --- a/packages/datatrak-web/src/features/Tasks/StatusPill.tsx +++ b/packages/datatrak-web/src/features/Tasks/StatusPill.tsx @@ -23,7 +23,13 @@ const Pill = styled.span<{ } `; -export const STATUS_VALUES = { +interface StatusValue { + label: string; + color: string; + // Define other properties if needed +} + +export const STATUS_VALUES: Record = { [TaskStatus.to_do]: { label: 'To do', color: '#1172D1', diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index f4c7e3b39c..0605676a73 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -53,6 +53,7 @@ export const FilterToolbar = () => { const handleChange = event => { const { name, checked: value } = event.target; setTaskFilterSetting(name, value); + // Clear the cache so that the task data is re-fetched queryClient.invalidateQueries('tasks'); }; diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx index 3c8f1a9bda..5bbf156572 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx @@ -34,20 +34,21 @@ export const StatusFilter = ({ onChange, filter }: StatusFilterProps) => { const includeCompletedTasks = getTaskFilterSetting('all_completed'); const includeCancelledTasks = getTaskFilterSetting('all_cancelled'); - const options = Object.entries(STATUS_VALUES) - .filter(([value]) => { - if (!includeCompletedTasks && value === TaskStatus.completed) { - return false; + const options: { value: string }[] = Object.keys(STATUS_VALUES).reduce( + (acc: { value: string }[], value) => { + if ( + (includeCompletedTasks && value === TaskStatus.completed) || + (includeCancelledTasks && value === TaskStatus.cancelled) + ) { + return acc; } - if (!includeCancelledTasks && value === TaskStatus.cancelled) { - return false; - } - return true; - }) - .map(([value, { label }]) => ({ - value, - label, - })); + acc.push({ + value: value, + }); + return acc; + }, + [], + ); const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { onChange(event.target.value as string); From bb56227179ae76f05c31d109ed98c0acca29b2a9 Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Fri, 5 Jul 2024 11:08:54 +1200 Subject: [PATCH 18/24] refactor tasks route --- .../src/routes/TasksRoute.ts | 214 ++++++++---------- .../src/features/Tasks/StatusPill.tsx | 8 +- .../Tasks/TasksTable/FilterToolbar.tsx | 65 +++--- .../Tasks/TasksTable/StatusFilter.tsx | 31 ++- .../Tasks/utils/taskFilterSettings.ts | 2 +- 5 files changed, 141 insertions(+), 179 deletions(-) diff --git a/packages/datatrak-web-server/src/routes/TasksRoute.ts b/packages/datatrak-web-server/src/routes/TasksRoute.ts index 0a13bff175..82601da6f2 100644 --- a/packages/datatrak-web-server/src/routes/TasksRoute.ts +++ b/packages/datatrak-web-server/src/routes/TasksRoute.ts @@ -5,11 +5,9 @@ import { Request } from 'express'; import camelcaseKeys from 'camelcase-keys'; import { Route } from '@tupaia/server-boilerplate'; -import { keyBy } from 'lodash'; import { parse } from 'cookie'; import { DatatrakWebTasksRequest, Task, TaskStatus } from '@tupaia/types'; import { QUERY_CONJUNCTIONS, RECORDS } from '@tupaia/database'; -import { DatatrakWebServerModelRegistry } from '../types'; export type TasksRequest = Request< DatatrakWebTasksRequest.Params, @@ -42,156 +40,132 @@ type SingleTask = Task & { 'entity.country_code': string; }; -type FormattedFilter = - | string - | { comparator: string; comparisonValue: string; castAs?: string } - | TaskStatus; - -type FormattedFilters = Record; - -const queryForCount = async (filter: FormattedFilters, models: DatatrakWebServerModelRegistry) => { - const filtersWithColumnSelectors = { ...filter }; - - // use column selectors for custom columns being used in filters - for (const [key, value] of Object.entries(filter)) { - if (key in models.task.customColumnSelectors) { - const colKey = - models.task.customColumnSelectors[key as keyof typeof models.task.customColumnSelectors](); - filtersWithColumnSelectors[colKey] = value; - delete filtersWithColumnSelectors[key]; - } - } - - return models.database.count(RECORDS.TASK, filtersWithColumnSelectors, { - multiJoin: models.task.DatabaseRecordClass.joins, - }); -}; +type FormattedFilters = Record; const EQUALITY_FILTERS = ['due_date', 'survey.project_id', 'task_status']; const getFilterSettings = (cookieString: string) => { const cookies = parse(cookieString); return { - allAssignees: cookies['all_assignees'] === 'true', - allCompleted: cookies['all_completed'] === 'true', - allCancelled: cookies['all_cancelled'] === 'true', + allAssignees: cookies['all_assignees_tasks'] === 'true', + allCompleted: cookies['all_completed_tasks'] === 'true', + allCancelled: cookies['all_cancelled_tasks'] === 'true', }; }; -type Filter = Record; - -const processFilterSettings = ( - filters: Filter[], - cookieString: string | string[] | undefined, - userId: string, -) => { - if (typeof cookieString !== 'string') { - return filters; +export class TasksRoute extends Route { + private filters: FormattedFilters = {}; + private formatFilters() { + const { query } = this.req; + const { filters = [] } = query; + + filters.forEach(({ id, value }) => { + if (value === '' || value === undefined || value === null) return; + + if (EQUALITY_FILTERS.includes(id)) { + this.filters[id] = value; + return; + } + + if (id === 'repeat_schedule') { + this.filters[id] = { + comparator: 'ilike', + comparisonValue: `${value}%`, + castAs: 'text', + }; + return; + } + this.filters[id] = { + comparator: 'ilike', + comparisonValue: `${value}%`, + }; + }); } + private async processFilterSettings() { + const cookieString = this.req.headers.cookie; + if (!cookieString) { + return; + } + const { id: userId } = await this.req.ctx.services.central.getUser(); + const cookies = getFilterSettings(cookieString); - const filtersById = keyBy(filters, 'id'); - - const cookies = getFilterSettings(cookieString); + if (!cookies.allAssignees) { + this.filters['assignee_id'] = userId; + } - if (!cookies.allAssignees) { - // add filter for assignee - filtersById['assignee_id'] = { id: 'assignee_id', value: userId }; - } + let taskStatusFilter = null; - if (!cookies.allCompleted) { - // add filter to remove completed tasks - filtersById['task_status'] = { - id: 'task_status', - value: { + if (!cookies.allCompleted) { + taskStatusFilter = { comparator: 'NOT IN', - comparisonValue: ['completed'], - }, - }; - } + comparisonValue: [TaskStatus.completed], + }; + } - if (!cookies.allCancelled) { - // add filter to remove cancelled tasks - filtersById['task_status'] = { - id: 'task_status', - value: { + if (!cookies.allCancelled) { + taskStatusFilter = { comparator: 'NOT IN', - comparisonValue: ['cancelled'], - }, - }; - } + comparisonValue: [TaskStatus.cancelled], + }; + } - if (!cookies.allCompleted && !cookies.allCancelled) { - // add filter to remove completed and cancelled tasks - // Todo: conditionally add the QUERY_CONJUNCTIONS.AND depending on whether or not there is a task_status filter set - filtersById['task_status'] = { - id: 'task_status', - value: { - comparator: '=', - comparisonValue: 'to_do', - }, - }; - filtersById[QUERY_CONJUNCTIONS.AND] = { - task_status: { - id: 'task_status', - value: { - comparator: 'NOT IN', - comparisonValue: ['completed', 'cancelled'], - }, - }, - }; - } - return Object.values(filtersById); -}; + if (!cookies.allCompleted && !cookies.allCancelled) { + taskStatusFilter = { + comparator: 'NOT IN', + comparisonValue: [TaskStatus.completed, TaskStatus.cancelled], + }; + } -const formatFilters = (filters: Record[]) => { - let formattedFilters: FormattedFilters = {}; + if (!taskStatusFilter) return; - filters.forEach(({ id, value }) => { - if (value === '' || value === undefined || value === null) return; + const isTaskFilter = 'task_status' in this.filters; - if (EQUALITY_FILTERS.includes(id)) { - formattedFilters[id] = value; - return; + if (isTaskFilter) { + this.filters[QUERY_CONJUNCTIONS.AND] = { + task_status: taskStatusFilter, + }; + } else { + this.filters['task_status'] = taskStatusFilter; } + } - if (id === 'repeat_schedule') { - formattedFilters[id] = { - comparator: 'ilike', - comparisonValue: `${value}%`, - castAs: 'text', - }; - return; + private async queryForCount() { + const { models } = this.req; + const filtersWithColumnSelectors = { ...this.filters }; + + // use column selectors for custom columns being used in filters + for (const [key, value] of Object.entries(this.filters)) { + if (key in models.task.customColumnSelectors) { + const colKey = + models.task.customColumnSelectors[ + key as keyof typeof models.task.customColumnSelectors + ](); + filtersWithColumnSelectors[colKey] = value; + delete filtersWithColumnSelectors[key]; + } } - formattedFilters[id] = { - comparator: 'ilike', - comparisonValue: `${value}%`, - }; - }); - return formattedFilters; -}; -export class TasksRoute extends Route { + return models.database.count(RECORDS.TASK, filtersWithColumnSelectors, { + multiJoin: models.task.DatabaseRecordClass.joins, + }); + } public async buildResponse() { - const { ctx, query = {}, models, headers } = this.req; - const { filters = [], pageSize = DEFAULT_PAGE_SIZE, sort, page = 0 } = query; + const { ctx, query = {}, models } = this.req; + const { pageSize = DEFAULT_PAGE_SIZE, sort, page = 0 } = query; + + this.formatFilters(); + await this.processFilterSettings(); - const { id: userId } = await ctx.services.central.getUser(); - const processedFilters = processFilterSettings(filters, headers.cookie, userId); - const formattedFilters = formatFilters(processedFilters); + console.log('=====', this.filters, '====='); const tasks = await ctx.services.central.fetchResources('tasks', { - filter: formattedFilters, + filter: this.filters, columns: FIELDS, sort, pageSize, page, }); - // Get all task ids for pagination - const count = await queryForCount(formattedFilters, models); - - const numberOfPages = Math.ceil(count / pageSize); - const formattedTasks = tasks.map((task: SingleTask) => { const { entity_id: entityId, @@ -217,6 +191,10 @@ export class TasksRoute extends Route { }; }); + // Get all task ids for pagination + const count = await this.queryForCount(); + const numberOfPages = Math.ceil(count / pageSize); + return { tasks: camelcaseKeys(formattedTasks, { deep: true }), count, diff --git a/packages/datatrak-web/src/features/Tasks/StatusPill.tsx b/packages/datatrak-web/src/features/Tasks/StatusPill.tsx index d9094a7d23..d7f3e36cd8 100644 --- a/packages/datatrak-web/src/features/Tasks/StatusPill.tsx +++ b/packages/datatrak-web/src/features/Tasks/StatusPill.tsx @@ -23,13 +23,7 @@ const Pill = styled.span<{ } `; -interface StatusValue { - label: string; - color: string; - // Define other properties if needed -} - -export const STATUS_VALUES: Record = { +export const STATUS_VALUES = { [TaskStatus.to_do]: { label: 'To do', color: '#1172D1', diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index 0605676a73..4f73023c52 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -10,7 +10,11 @@ import { FormControlLabel as MuiFormControlLabel, Checkbox as MuiCheckbox, } from '@material-ui/core'; -import { getTaskFilterSetting, setTaskFilterSetting } from '../utils/taskFilterSettings.ts'; +import { + FilterType, + getTaskFilterSetting, + setTaskFilterSetting, +} from '../utils/taskFilterSettings'; const Container = styled.div` display: flex; @@ -36,49 +40,38 @@ const FormControlLabel = styled(MuiFormControlLabel)` } `; -const Checkbox = ({ name, value, label, onChange }) => { - return ( - - } - label={label} - /> - ); -}; - -export const FilterToolbar = () => { +const Checkbox = ({ name, label }) => { const queryClient = useQueryClient(); - const handleChange = event => { + const onChange = (event: React.ChangeEvent<{ name: string; checked: boolean }>) => { const { name, checked: value } = event.target; - setTaskFilterSetting(name, value); + setTaskFilterSetting(name as FilterType, value); // Clear the cache so that the task data is re-fetched queryClient.invalidateQueries('tasks'); }; return ( - - - - - - - + } + label={label} + /> ); }; + +export const FilterToolbar = () => ( + + + + + + + +); diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx index 5bbf156572..171927508a 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx @@ -6,9 +6,9 @@ import React from 'react'; import styled from 'styled-components'; import { MenuItem as MuiMenuItem, Select } from '@material-ui/core'; -import { STATUS_VALUES, StatusPill } from '../StatusPill'; import { TaskStatus } from '@tupaia/types'; -import { getTaskFilterSetting } from '../utils/taskFilterSettings.ts'; +import { STATUS_VALUES, StatusPill } from '../StatusPill'; +import { FilterType, getTaskFilterSetting } from '../utils/taskFilterSettings'; const PlaceholderText = styled.span` color: ${({ theme }) => theme.palette.text.secondary}; @@ -27,28 +27,25 @@ const MenuItem = styled(MuiMenuItem)` interface StatusFilterProps { onChange: (value: string) => void; - filter: { value: string } | undefined; + filter: { value: FilterType } | undefined; } export const StatusFilter = ({ onChange, filter }: StatusFilterProps) => { - const includeCompletedTasks = getTaskFilterSetting('all_completed'); - const includeCancelledTasks = getTaskFilterSetting('all_cancelled'); + const includeCompletedTasks = getTaskFilterSetting('all_completed_tasks'); + const includeCancelledTasks = getTaskFilterSetting('all_cancelled_tasks'); - const options: { value: string }[] = Object.keys(STATUS_VALUES).reduce( - (acc: { value: string }[], value) => { + const options = Object.keys(STATUS_VALUES) + .filter(value => { + // Filter out completed and cancelled tasks if the user has disabled them if ( - (includeCompletedTasks && value === TaskStatus.completed) || - (includeCancelledTasks && value === TaskStatus.cancelled) + (!includeCompletedTasks && value === TaskStatus.completed) || + (!includeCancelledTasks && value === TaskStatus.cancelled) ) { - return acc; + return false; } - acc.push({ - value: value, - }); - return acc; - }, - [], - ); + return true; + }) + .map(value => ({ value })); const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { onChange(event.target.value as string); diff --git a/packages/datatrak-web/src/features/Tasks/utils/taskFilterSettings.ts b/packages/datatrak-web/src/features/Tasks/utils/taskFilterSettings.ts index 283a158b69..80c92d9595 100644 --- a/packages/datatrak-web/src/features/Tasks/utils/taskFilterSettings.ts +++ b/packages/datatrak-web/src/features/Tasks/utils/taskFilterSettings.ts @@ -4,7 +4,7 @@ */ import Cookies from 'js-cookie'; -type FilterType = 'all_assignees' | 'all_completed' | 'all_cancelled'; +export type FilterType = 'all_assignees_tasks' | 'all_completed_tasks' | 'all_cancelled_tasks'; export const getTaskFilterSetting = (cookieName: FilterType): boolean => { return Cookies.get(cookieName) === 'true'; From d23c5cf65398567116ca9b3ca7f65a08b569220b Mon Sep 17 00:00:00 2001 From: Tom Caiger Date: Fri, 5 Jul 2024 11:56:03 +1200 Subject: [PATCH 19/24] handle invalidFilterValues --- .../src/routes/TasksRoute.ts | 29 ++++++------------- .../Tasks/TasksTable/FilterToolbar.tsx | 8 ++--- .../Tasks/TasksTable/StatusFilter.tsx | 6 ++++ 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/packages/datatrak-web-server/src/routes/TasksRoute.ts b/packages/datatrak-web-server/src/routes/TasksRoute.ts index 82601da6f2..397708782f 100644 --- a/packages/datatrak-web-server/src/routes/TasksRoute.ts +++ b/packages/datatrak-web-server/src/routes/TasksRoute.ts @@ -7,7 +7,7 @@ import camelcaseKeys from 'camelcase-keys'; import { Route } from '@tupaia/server-boilerplate'; import { parse } from 'cookie'; import { DatatrakWebTasksRequest, Task, TaskStatus } from '@tupaia/types'; -import { QUERY_CONJUNCTIONS, RECORDS } from '@tupaia/database'; +import { RECORDS } from '@tupaia/database'; export type TasksRequest = Request< DatatrakWebTasksRequest.Params, @@ -86,47 +86,38 @@ export class TasksRoute extends Route { if (!cookieString) { return; } - const { id: userId } = await this.req.ctx.services.central.getUser(); const cookies = getFilterSettings(cookieString); if (!cookies.allAssignees) { + const { id: userId } = await this.req.ctx.services.central.getUser(); this.filters['assignee_id'] = userId; } - let taskStatusFilter = null; + // If the task status filter is already present, don't need to worry about allCompleted and allCancelled filters + if ('task_status' in this.filters) { + return; + } if (!cookies.allCompleted) { - taskStatusFilter = { + this.filters['task_status'] = { comparator: 'NOT IN', comparisonValue: [TaskStatus.completed], }; } if (!cookies.allCancelled) { - taskStatusFilter = { + this.filters['task_status'] = { comparator: 'NOT IN', comparisonValue: [TaskStatus.cancelled], }; } if (!cookies.allCompleted && !cookies.allCancelled) { - taskStatusFilter = { + this.filters['task_status'] = { comparator: 'NOT IN', comparisonValue: [TaskStatus.completed, TaskStatus.cancelled], }; } - - if (!taskStatusFilter) return; - - const isTaskFilter = 'task_status' in this.filters; - - if (isTaskFilter) { - this.filters[QUERY_CONJUNCTIONS.AND] = { - task_status: taskStatusFilter, - }; - } else { - this.filters['task_status'] = taskStatusFilter; - } } private async queryForCount() { @@ -156,8 +147,6 @@ export class TasksRoute extends Route { this.formatFilters(); await this.processFilterSettings(); - console.log('=====', this.filters, '====='); - const tasks = await ctx.services.central.fetchResources('tasks', { filter: this.filters, columns: FIELDS, diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx index 4f73023c52..ddf0cd4a0f 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/FilterToolbar.tsx @@ -40,7 +40,7 @@ const FormControlLabel = styled(MuiFormControlLabel)` } `; -const Checkbox = ({ name, label }) => { +const FilterCheckbox = ({ name, label }) => { const queryClient = useQueryClient(); const onChange = (event: React.ChangeEvent<{ name: string; checked: boolean }>) => { @@ -69,9 +69,9 @@ const Checkbox = ({ name, label }) => { export const FilterToolbar = () => ( - - - + + + ); diff --git a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx index 171927508a..0f2f2ffea6 100644 --- a/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx +++ b/packages/datatrak-web/src/features/Tasks/TasksTable/StatusFilter.tsx @@ -33,6 +33,7 @@ interface StatusFilterProps { export const StatusFilter = ({ onChange, filter }: StatusFilterProps) => { const includeCompletedTasks = getTaskFilterSetting('all_completed_tasks'); const includeCancelledTasks = getTaskFilterSetting('all_cancelled_tasks'); + const filterValue = filter?.value ?? ''; const options = Object.keys(STATUS_VALUES) .filter(value => { @@ -51,6 +52,11 @@ export const StatusFilter = ({ onChange, filter }: StatusFilterProps) => { onChange(event.target.value as string); }; + const invalidFilterValue = !options.find(option => option.value === filterValue); + if (invalidFilterValue && filter?.value) { + onChange(''); + } + return (