Skip to content

Commit

Permalink
62 Toggle task completion (#64)
Browse files Browse the repository at this point in the history
* #62 update deps

* #62 toggle task completion state

* #62 tests

* #62 tests
  • Loading branch information
mwarman authored Jun 17, 2024
1 parent 604730d commit 15792f3
Show file tree
Hide file tree
Showing 12 changed files with 559 additions and 206 deletions.
370 changes: 185 additions & 185 deletions package-lock.json

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@
"test:ci": "vitest run --coverage --silent"
},
"dependencies": {
"@codesandbox/sandpack-react": "2.14.0",
"@codesandbox/sandpack-react": "2.14.2",
"@leanstacks/react-common": "1.0.0",
"@react-spring/web": "9.7.3",
"@tanstack/react-query": "5.40.1",
"@tanstack/react-query-devtools": "5.40.1",
"@tanstack/react-query": "5.45.0",
"@tanstack/react-query-devtools": "5.45.0",
"@tanstack/react-table": "8.17.3",
"axios": "1.7.2",
"classnames": "2.5.1",
Expand All @@ -40,21 +40,21 @@
"react-dom": "18.3.1",
"react-i18next": "14.1.2",
"react-router-dom": "6.23.1",
"tailwindcss": "3.4.3",
"uuid": "9.0.1",
"tailwindcss": "3.4.4",
"uuid": "10.0.0",
"yup": "1.4.0"
},
"devDependencies": {
"@testing-library/react": "16.0.0",
"@testing-library/user-event": "14.5.2",
"@types/lodash": "4.17.4",
"@types/lodash": "4.17.5",
"@types/qs": "6.9.15",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"@types/uuid": "9.0.8",
"@typescript-eslint/eslint-plugin": "7.12.0",
"@typescript-eslint/parser": "7.12.0",
"@vitejs/plugin-react": "4.3.0",
"@typescript-eslint/eslint-plugin": "7.13.0",
"@typescript-eslint/parser": "7.13.0",
"@vitejs/plugin-react": "4.3.1",
"@vitest/coverage-v8": "1.6.0",
"autoprefixer": "10.4.19",
"eslint": "8.57.0",
Expand All @@ -63,10 +63,10 @@
"jsdom": "24.1.0",
"msw": "2.3.1",
"postcss": "8.4.38",
"prettier": "3.3.0",
"prettier-plugin-tailwindcss": "0.6.1",
"prettier": "3.3.2",
"prettier-plugin-tailwindcss": "0.6.4",
"typescript": "5.4.5",
"vite": "5.2.12",
"vite": "5.3.1",
"vitest": "1.6.0"
}
}
84 changes: 84 additions & 0 deletions src/pages/UsersPage/api/__tests__/useUpdateTask.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it } from 'vitest';

import { renderHook, waitFor } from 'test/test-utils';
import { queryClient } from 'test/query-client';
import { todosFixture } from '__fixtures__/todos';
import { QueryKeys } from 'utils/constants';
import { Task } from '../useGetUserTasks';

import { useUpdateTask } from '../useUpdateTask';

describe('useUpdateTask', () => {
it('should update task', async () => {
// ARRANGE
const updatedTask = todosFixture[0];
let isSuccess = false;
const { result } = renderHook(() => useUpdateTask());
await waitFor(() => expect(result.current).not.toBeNull());

// ACT
result.current.mutate(
{ task: updatedTask },
{
onSuccess: () => {
isSuccess = true;
},
},
);
await waitFor(() => expect(result.current.isSuccess).toBe(true));

// ASSERT
expect(isSuccess).toBe(true);
});

it('should create cached data when none exists', async () => {
// ARRANGE
const updatedTask = todosFixture[0];
let isSuccess = false;
const { result } = renderHook(() => useUpdateTask());
await waitFor(() => expect(result.current).not.toBeNull());

// ACT
result.current.mutate(
{ task: updatedTask },
{
onSuccess: () => {
isSuccess = true;
},
},
);
await waitFor(() => expect(result.current.isSuccess).toBe(true));

// ASSERT
expect(isSuccess).toBe(true);
expect(queryClient.getQueryData([QueryKeys.Tasks, { userId: updatedTask.userId }])).toEqual([
updatedTask,
]);
});

it('should update cached data when exists', async () => {
// ARRANGE
const updatedTask = todosFixture[0];
queryClient.setQueryData([QueryKeys.Tasks, { userId: updatedTask.userId }], todosFixture);
let isSuccess = false;
const { result } = renderHook(() => useUpdateTask());
await waitFor(() => expect(result.current).not.toBeNull());

// ACT
result.current.mutate(
{ task: updatedTask },
{
onSuccess: () => {
isSuccess = true;
},
},
);
await waitFor(() => expect(result.current.isSuccess).toBe(true));

// ASSERT
expect(isSuccess).toBe(true);
expect(
queryClient.getQueryData<Task[]>([QueryKeys.Tasks, { userId: updatedTask.userId }])?.length,
).toEqual(todosFixture.length);
});
});
52 changes: 52 additions & 0 deletions src/pages/UsersPage/api/useUpdateTask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import reject from 'lodash/reject';

import { QueryKeys } from 'utils/constants';
import { Task } from './useGetUserTasks';
import { useConfig } from 'hooks/useConfig';
import { useAxios } from 'hooks/useAxios';

/**
* The `useUpdateTask` mutation function variables.
*/
export type UpdateTaskVariables = {
task: Task;
};

/**
* An API hook which updates a single `Task`. Returns a `UseMutationResult`
* object whose `mutate` attribute is a function to update as `Task`.
*
* When successful, the hook updates cached `Task` query data.
* @returns Returns a `UseMutationResult`.
*/
export const useUpdateTask = () => {
const queryClient = useQueryClient();
const config = useConfig();
const axios = useAxios();

/**
* Update a `Task`.
* @param {UpdateTaskVariables} variables - The mutation function variables.
* @returns The updated `Task` object.
*/
const updateTask = async ({ task }: UpdateTaskVariables): Promise<Task> => {
const response = await axios.request({
method: 'put',
url: `${config.VITE_BASE_URL_API}/todos/${task.id}`,
data: task,
});
return response.data;
};

return useMutation({
mutationFn: updateTask,
onSuccess: (data, variables) => {
// update cached query data
queryClient.setQueryData<Task[]>(
[QueryKeys.Tasks, { userId: variables.task.userId }],
(cachedTasks) => (cachedTasks ? [...reject(cachedTasks, { id: data.id }), data] : [data]),
);
},
});
};
86 changes: 86 additions & 0 deletions src/pages/UsersPage/components/TaskCompleteToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
Button,
ButtonVariant,
PropsWithClassName,
PropsWithTestId,
} from '@leanstacks/react-common';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';

import { Task } from '../api/useGetUserTasks';
import { useUpdateTask } from '../api/useUpdateTask';
import { useToasts } from 'hooks/useToasts';
import Icon from 'components/Icon/Icon';

/**
* Propeties for the`TaskCompleteToggle` component.
* @param {Task} task - A Task object.
* @see {@link PropsWithClassName}
* @see {@link PropsWithTestId}
*/
interface TaskCompleteToggleProps extends PropsWithClassName, PropsWithTestId {
task: Task;
}

/**
* The `TaskCompleteToggle` component renders a `Button` which allows a user
* to toggle the value of the Task `complete` attribute.
* @param {TaskCompleteToggleProps} props - Component properties.
* @returns {JSX.Element} JSX
*/
const TaskCompleteToggle = ({
className,
task,
testId = 'toggle-task-complete',
}: TaskCompleteToggleProps): JSX.Element => {
const { t } = useTranslation();
const { mutate: updateTask } = useUpdateTask();
const { createToast } = useToasts();

const buttonTitle = task.completed
? t('task.markIncomplete', { ns: 'users' })
: t('task.markComplete', { ns: 'users' });

/**
* Actions to perform when the task complete toggle button is clicked.
*/
const handleButtonClick = () => {
updateTask(
{
task: {
...task,
completed: !task.completed,
},
},
{
onSuccess: (data) => {
createToast({
text: data.completed
? t('task.markedComplete', { ns: 'users' })
: t('task.markedIncomplete', { ns: 'users' }),
isAutoDismiss: true,
});
},
},
);
};

return (
<Button
className={classNames('!m-0 contents !border-none !p-0', className)}
variant={ButtonVariant.Text}
title={buttonTitle}
onClick={handleButtonClick}
data-testid={testId}
>
<Icon
name={task.completed ? 'task_alt' : 'circle'}
fill={0}
className={classNames('text-lg', { 'text-green-600': task.completed })}
testId={`${testId}-icon`}
/>
</Button>
);
};

export default TaskCompleteToggle;
10 changes: 2 additions & 8 deletions src/pages/UsersPage/components/UserTaskListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { PropsWithClassName, PropsWithTestId } from '@leanstacks/react-common';

import { Task } from '../api/useGetUserTasks';
import Icon from 'components/Icon/Icon';
import classNames from 'classnames';
import TaskCompleteToggle from './TaskCompleteToggle';

/**
* Properties for the `UserTaskListItem` React component.
Expand All @@ -28,12 +27,7 @@ const UserTaskListItem = ({
return (
<div className={className} data-testid={testId}>
<div key={task.id} className="flex items-center gap-4 py-0.5">
<Icon
name={task.completed ? 'task_alt' : 'circle'}
fill={0}
className={classNames('text-lg', { 'text-green-600': task.completed })}
testId={`${testId}-icon`}
/>
<TaskCompleteToggle task={task} testId={`${testId}-toggle-complete`} />
<div>{task.title}</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 15792f3

Please sign in to comment.