Skip to content

Commit

Permalink
MPDX-8373 Task Row (#1139)
Browse files Browse the repository at this point in the history
* Add hover to check box and show Selected Count

* Make spacing smaller around date and delete icon

* When editing tasks, stop autofilling assignee

* Add assignee, tags, responsive layout

* ContactTaskRow assignee, tags, responsive layout

* Add tooltip to comments

* Allow mobile users to tap to see tooltip
  • Loading branch information
caleballdrin authored Oct 24, 2024
1 parent ad47379 commit 0162a33
Show file tree
Hide file tree
Showing 15 changed files with 427 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@ const TasksPage: React.FC = () => {
isChecked={isRowChecked(task.id)}
useTopMargin={index === 0}
getContactHrefObject={getContactHrefObject}
contactDetailsOpen={contactDetailsOpen}
removeSelectedIds={deselectMultipleIds}
filterPanelOpen={filterPanelOpen}
/>
</Box>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ describe('ContactTaskRow', () => {
expect(await findByText(task.subject)).toBeVisible();

expect(
await findByText(`${task.user?.firstName} ${task.user?.lastName}`),
await findByText(
`${task.user?.firstName?.[0]}${task.user?.lastName?.[0]}`,
),
).toBeVisible();

expect(queryByTestId('loadingRow')).toBeNull();
Expand All @@ -101,8 +103,34 @@ describe('ContactTaskRow', () => {
mocks: {
startAt,
result: ResultEnum.None,
user: {
firstName: 'John',
lastName: 'Wayne',
},
},
});
const taskWithTags = gqlMock<TaskRowFragment>(TaskRowFragmentDoc, {
mocks: {
id: '123',
startAt,
result: ResultEnum.None,
tagList: ['testTag'],
user: null,
},
});

it('renders the assignee avatar', async () => {
const { findByText } = render(<Components task={task} />);

expect(await findByText('JW')).toBeVisible();
});

it('renders the tag icon', async () => {
const { findByTestId } = render(<Components task={taskWithTags} />);

expect(await findByTestId('tagIcon-123')).toBeVisible();
});

it('handles complete button click', async () => {
const { findByText, getByRole } = render(<Components task={task} />);

Expand Down Expand Up @@ -166,7 +194,7 @@ describe('ContactTaskRow', () => {

const { getByText } = render(<Components task={task} />);

expect(getByText(activity?.value)).toBeVisible();
expect(getByText(activity?.value.split(' - ')[1].trim())).toBeVisible();
},
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import React, { useState } from 'react';
import { Box, Checkbox, Skeleton, Typography } from '@mui/material';
import LocalOffer from '@mui/icons-material/LocalOffer';
import {
Avatar,
Box,
Checkbox,
Skeleton,
Tooltip,
Typography,
} from '@mui/material';
import { styled } from '@mui/material/styles';
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { StyledCheckbox } from 'src/components/Contacts/ContactRow/ContactRow';
import { TaskModalEnum } from 'src/components/Task/Modal/TaskModal';
import {
CommentTooltipText,
TooltipTypography,
} from 'src/components/Task/TaskRow/CommentTooltipText';
import { TaskActionPhase } from 'src/components/Task/TaskRow/TaskActionPhase';
import { TaskRowFragment } from 'src/components/Task/TaskRow/TaskRow.generated';
import { StarredItemIcon } from 'src/components/common/StarredItemIcon/StarredItemIcon';
import { usePhaseData } from 'src/hooks/usePhaseData';
import useTaskModal from 'src/hooks/useTaskModal';
import theme from 'src/theme';
import { getLocalizedTaskType } from 'src/utils/functions/getLocalizedTaskType';
import { DeleteTaskIconButton } from '../DeleteTaskIconButton/DeleteTaskIconButton';
import { StarTaskIconButton } from '../StarTaskIconButton/StarTaskIconButton';
import { TaskCommentsButton } from './TaskCommentsButton/TaskCommentsButton';
Expand All @@ -36,12 +49,6 @@ const TaskItemWrap = styled(Box)(({ theme }) => ({
height: '100%',
}));

const TaskType = styled(Typography)(({ theme }) => ({
fontSize: 14,
fontWeight: 700,
color: theme.palette.text.primary,
}));

const TaskDescription = styled(Typography)(({ theme }) => ({
fontSize: 14,
color: theme.palette.text.primary,
Expand All @@ -66,14 +73,6 @@ const SubjectWrap = styled(Box)(({}) => ({
},
}));

const AssigneeName = styled(Typography)(({ theme }) => ({
fontSize: 14,
color: theme.palette.text.primary,
margin: theme.spacing(1),
overflow: 'hidden',
textOverflow: 'ellipsis',
}));

const StarIconWrap = styled(Box)(({ theme }) => ({
margin: theme.spacing(1),
}));
Expand Down Expand Up @@ -156,12 +155,13 @@ export const ContactTaskRow: React.FC<ContactTaskRowProps> = ({
const dateToShow = completedAt ?? startAt;
const taskDate = (dateToShow && DateTime.fromISO(dateToShow)) || null;
const assigneeName = user ? `${user.firstName} ${user.lastName}` : '';
const tagList = !!task.tagList.length ? task.tagList.join(', ') : '';
const activityData = activityType ? activityTypes.get(activityType) : null;

return !hasBeenDeleted ? (
<TaskRowWrap isChecked={isChecked}>
<TaskItemWrap width={theme.spacing(20)} justifyContent="space-between">
<Checkbox
<StyledCheckbox
checked={isChecked}
color="secondary"
onChange={() => onTaskCheckToggle(task.id)}
Expand All @@ -177,30 +177,82 @@ export const ContactTaskRow: React.FC<ContactTaskRowProps> = ({
onClick={handleSubjectPressed}
onMouseEnter={() => preloadTaskModal(TaskModalEnum.Edit)}
>
<TaskType>
{activityData && `${activityData.phase} - `}
{getLocalizedTaskType(t, activityType)}
</TaskType>
<TaskDescription>{subject}</TaskDescription>
<TaskActionPhase
activityData={activityData}
activityType={activityType}
/>

<Tooltip title={subject} placement="top-start" arrow>
<TaskDescription>{subject}</TaskDescription>
</Tooltip>
</SubjectWrap>

<TaskItemWrap justifyContent="end" maxWidth={theme.spacing(45)}>
<AssigneeName noWrap>{assigneeName}</AssigneeName>
<Box width={theme.spacing(16)}>
<TaskDate isComplete={isComplete} taskDate={taskDate} />
{(assigneeName || tagList) && (
<Tooltip
title={
<>
{assigneeName && (
<TooltipTypography>
{t('Assignee: ') + assigneeName}
</TooltipTypography>
)}
{tagList && (
<TooltipTypography>{t('Tags: ') + tagList}</TooltipTypography>
)}
</>
}
placement="top"
arrow
enterTouchDelay={0}
>
{assigneeName ? (
<Avatar
data-testid={`assigneeAvatar-${task.id}`}
sx={{
width: 30,
height: 30,
}}
>
{(task?.user?.firstName?.[0] || '') +
task?.user?.lastName?.[0] || ''}
</Avatar>
) : (
<LocalOffer
data-testid={`tagIcon-${task.id}`}
sx={{ color: theme.palette.secondary.dark }}
/>
)}
</Tooltip>
)}
<Box>
<TaskDate isComplete={isComplete} taskDate={taskDate} small />
<Tooltip
title={
comments?.totalCount ? (
<CommentTooltipText comments={comments.nodes} />
) : null
}
placement="top"
arrow
>
<Box>
<TaskCommentsButton
isComplete={isComplete}
numberOfComments={comments?.totalCount}
onClick={handleCommentButtonPressed}
onMouseEnter={() => preloadTaskModal(TaskModalEnum.Comments)}
small
/>
</Box>
</Tooltip>
</Box>
<TaskCommentsButton
isComplete={isComplete}
numberOfComments={comments?.totalCount}
onClick={handleCommentButtonPressed}
onMouseEnter={() => preloadTaskModal(TaskModalEnum.Comments)}
detailsPage
/>

<DeleteTaskIconButton
accountListId={accountListId}
taskId={task.id}
onDeleteConfirm={handleDeleteConfirm}
small
/>
<StarTaskIconButton
accountListId={accountListId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const TaskRowWrap = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
margin: theme.spacing(1),
margin: theme.spacing(1, 0.5),
}));

const TaskCommentIcon = styled(CalendarToday, {
Expand All @@ -32,7 +32,7 @@ const DateText = styled(Typography, {
: isComplete
? theme.palette.text.secondary
: theme.palette.text.primary,
marginLeft: theme.spacing(1),
marginLeft: small ? theme.spacing(0.5) : theme.spacing(1),
}),
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import Add from '@mui/icons-material/Add';
import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline';
import { Box, Button, Checkbox, Divider, Typography } from '@mui/material';
import { Box, Button, Divider, Hidden, Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
Expand All @@ -18,6 +18,7 @@ import { TaskFilterSetInput } from 'src/graphql/types.generated';
import { useGetTaskIdsForMassSelectionQuery } from 'src/hooks/GetIdsForMassSelection.generated';
import { useMassSelection } from 'src/hooks/useMassSelection';
import useTaskModal from 'src/hooks/useTaskModal';
import { StyledCheckbox } from '../../ContactRow/ContactRow';
import { ContactTaskRow } from './ContactTaskRow/ContactTaskRow';
import {
useContactPhaseQuery,
Expand Down Expand Up @@ -220,7 +221,7 @@ export const ContactTasksTab: React.FC<ContactTasksTabProps> = ({
</HeaderRow>
<HeaderRow mb={1}>
<HeaderItemsWrap>
<Checkbox
<StyledCheckbox
checked={selectionType === ListHeaderCheckBoxState.Checked}
color="secondary"
indeterminate={selectionType === ListHeaderCheckBoxState.Partial}
Expand All @@ -232,6 +233,13 @@ export const ContactTasksTab: React.FC<ContactTasksTabProps> = ({
placeholder={t('Search Tasks')}
page={PageEnum.Task}
/>
{!!ids.length && (
<Hidden smDown>
<Typography ml={2}>
{t('{{count}} Selected', { count: ids.length })}
</Typography>
</Hidden>
)}
</HeaderItemsWrap>
<HeaderItemsWrap>
<PlaceholderActionBar />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,26 @@ import { useTranslation } from 'react-i18next';
import { DeletedItemIcon } from '../../../../common/DeleteItemIcon/DeleteItemIcon';
import { DeleteConfirmation } from '../../../../common/Modal/DeleteConfirmation/DeleteConfirmation';

const DeleteButton = styled(IconButton)(({ theme }) => ({
margin: theme.spacing(1),
const DeleteButton = styled(IconButton, {
shouldForwardProp: (prop) => prop !== 'small',
})<{ small?: boolean }>(({ small, theme }) => ({
margin: small ? theme.spacing(0.5) : theme.spacing(1),
}));

interface DeleteTaskIconButtonProps {
accountListId: string;
taskId: string;
onDeleteConfirm?: () => void;
removeSelectedIds?: (id: string[]) => void;
small?: boolean;
}

export const DeleteTaskIconButton: React.FC<DeleteTaskIconButtonProps> = ({
accountListId,
taskId,
onDeleteConfirm,
removeSelectedIds,
small = false,
}) => {
const { t } = useTranslation();

Expand All @@ -31,6 +35,7 @@ export const DeleteTaskIconButton: React.FC<DeleteTaskIconButtonProps> = ({
<DeleteButton
onClick={() => setRemoveDialogOpen(true)}
data-testid={`DeleteIconButton-${taskId}`}
small={small}
>
<DeletedItemIcon />
</DeleteButton>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Task/Modal/Form/TaskModalForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ describe('TaskModalForm', () => {
<TaskModalForm
accountListId={accountListId}
onClose={onClose}
task={mockTask}
task={null}
/>
</GqlMockedProvider>
</SnackbarProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Task/Modal/Form/TaskModalForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ const TaskModalForm = ({
nextAction: task.nextAction ?? null,
tagList: additionalTags ?? [],
contactIds: task.contacts.nodes.map(({ id }) => id),
userId: task.user?.id ?? session.data?.user.userID ?? null,
userId: task.user?.id ?? null,
notificationTimeBefore: task.notificationTimeBefore,
notificationType: task.notificationType,
notificationTimeUnit: task.notificationTimeUnit,
Expand Down
46 changes: 46 additions & 0 deletions src/components/Task/TaskRow/CommentTooltipText.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { render } from '@testing-library/react';
import { DateTime } from 'luxon';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider, gqlMock } from '__tests__/util/graphqlMocking';
import {
TaskModalCommentFragment,
TaskModalCommentFragmentDoc,
} from '../Modal/Comments/TaskListComments.generated';
import { CommentTooltipText } from './CommentTooltipText';

const accountListId = 'account-list-1';
const commentId = 'comment-1';
const router = {
query: { accountListId },
isReady: true,
};

const TestComponent: React.FC = () => {
const comment = gqlMock<TaskModalCommentFragment>(
TaskModalCommentFragmentDoc,
{
mocks: {
id: commentId,
body: 'Comment',
updatedAt: DateTime.local(2020, 1, 2).toISODate() ?? '',
},
},
);

return (
<TestRouter router={router}>
<GqlMockedProvider>
<CommentTooltipText comments={[comment]} />
</GqlMockedProvider>
</TestRouter>
);
};

describe('CommentTooltipText', () => {
it('should render', async () => {
const { findByText, getByText } = render(<TestComponent />);

expect(await findByText('Comment')).toBeInTheDocument();
expect(getByText('Jan 2, 2020')).toBeInTheDocument();
});
});
Loading

0 comments on commit 0162a33

Please sign in to comment.