mirror of
https://github.com/ershisan99/it-incubator-todolist-ts-17-live-2024-08-17.git
synced 2026-02-05 05:12:08 +00:00
init
This commit is contained in:
38
src/features/TodolistsList/Todolist/Task/Task.tsx
Normal file
38
src/features/TodolistsList/Todolist/Task/Task.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { ChangeEvent, useCallback } from 'react'
|
||||
import { Checkbox, IconButton } from '@mui/material'
|
||||
import { EditableSpan } from '../../../../components/EditableSpan/EditableSpan'
|
||||
import { Delete } from '@mui/icons-material'
|
||||
import { TaskStatuses, TaskType } from '../../../../api/todolists-api'
|
||||
|
||||
type TaskPropsType = {
|
||||
task: TaskType
|
||||
todolistId: string
|
||||
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
|
||||
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
|
||||
removeTask: (taskId: string, todolistId: string) => void
|
||||
}
|
||||
export const Task = React.memo((props: TaskPropsType) => {
|
||||
const onClickHandler = useCallback(() => props.removeTask(props.task.id, props.todolistId), [props.task.id, props.todolistId]);
|
||||
|
||||
const onChangeHandler = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
let newIsDoneValue = e.currentTarget.checked
|
||||
props.changeTaskStatus(props.task.id, newIsDoneValue ? TaskStatuses.Completed : TaskStatuses.New, props.todolistId)
|
||||
}, [props.task.id, props.todolistId]);
|
||||
|
||||
const onTitleChangeHandler = useCallback((newValue: string) => {
|
||||
props.changeTaskTitle(props.task.id, newValue, props.todolistId)
|
||||
}, [props.task.id, props.todolistId]);
|
||||
|
||||
return <div key={props.task.id} className={props.task.status === TaskStatuses.Completed ? 'is-done' : ''}>
|
||||
<Checkbox
|
||||
checked={props.task.status === TaskStatuses.Completed}
|
||||
color="primary"
|
||||
onChange={onChangeHandler}
|
||||
/>
|
||||
|
||||
<EditableSpan value={props.task.title} onChange={onTitleChangeHandler}/>
|
||||
<IconButton onClick={onClickHandler}>
|
||||
<Delete/>
|
||||
</IconButton>
|
||||
</div>
|
||||
})
|
||||
96
src/features/TodolistsList/Todolist/Todolist.tsx
Normal file
96
src/features/TodolistsList/Todolist/Todolist.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { AddItemForm } from '../../../components/AddItemForm/AddItemForm'
|
||||
import { EditableSpan } from '../../../components/EditableSpan/EditableSpan'
|
||||
import { Task } from './Task/Task'
|
||||
import { TaskStatuses, TaskType } from '../../../api/todolists-api'
|
||||
import { FilterValuesType, TodolistDomainType } from '../todolists-reducer'
|
||||
import { fetchTasksTC } from '../tasks-reducer'
|
||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||
import { Button, IconButton } from '@mui/material'
|
||||
import { Delete } from '@mui/icons-material'
|
||||
|
||||
type PropsType = {
|
||||
todolist: TodolistDomainType
|
||||
tasks: Array<TaskType>
|
||||
changeFilter: (value: FilterValuesType, todolistId: string) => void
|
||||
addTask: (title: string, todolistId: string) => void
|
||||
changeTaskStatus: (id: string, status: TaskStatuses, todolistId: string) => void
|
||||
changeTaskTitle: (taskId: string, newTitle: string, todolistId: string) => void
|
||||
removeTask: (taskId: string, todolistId: string) => void
|
||||
removeTodolist: (id: string) => void
|
||||
changeTodolistTitle: (id: string, newTitle: string) => void
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
export const Todolist = React.memo(function ({demo = false, ...props}: PropsType) {
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (demo) {
|
||||
return
|
||||
}
|
||||
const thunk = fetchTasksTC(props.todolist.id)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const addTask = useCallback((title: string) => {
|
||||
props.addTask(title, props.todolist.id)
|
||||
}, [props.addTask, props.todolist.id])
|
||||
|
||||
const removeTodolist = () => {
|
||||
props.removeTodolist(props.todolist.id)
|
||||
}
|
||||
const changeTodolistTitle = useCallback((title: string) => {
|
||||
props.changeTodolistTitle(props.todolist.id, title)
|
||||
}, [props.todolist.id, props.changeTodolistTitle])
|
||||
|
||||
const onAllClickHandler = useCallback(() => props.changeFilter('all', props.todolist.id), [props.todolist.id, props.changeFilter])
|
||||
const onActiveClickHandler = useCallback(() => props.changeFilter('active', props.todolist.id), [props.todolist.id, props.changeFilter])
|
||||
const onCompletedClickHandler = useCallback(() => props.changeFilter('completed', props.todolist.id), [props.todolist.id, props.changeFilter])
|
||||
|
||||
|
||||
let tasksForTodolist = props.tasks
|
||||
|
||||
if (props.todolist.filter === 'active') {
|
||||
tasksForTodolist = props.tasks.filter(t => t.status === TaskStatuses.New)
|
||||
}
|
||||
if (props.todolist.filter === 'completed') {
|
||||
tasksForTodolist = props.tasks.filter(t => t.status === TaskStatuses.Completed)
|
||||
}
|
||||
|
||||
return <div>
|
||||
<h3><EditableSpan value={props.todolist.title} onChange={changeTodolistTitle}/>
|
||||
<IconButton onClick={removeTodolist} disabled={props.todolist.entityStatus === 'loading'}>
|
||||
<Delete/>
|
||||
</IconButton>
|
||||
</h3>
|
||||
<AddItemForm addItem={addTask} disabled={props.todolist.entityStatus === 'loading'}/>
|
||||
<div>
|
||||
{
|
||||
tasksForTodolist.map(t => <Task key={t.id} task={t} todolistId={props.todolist.id}
|
||||
removeTask={props.removeTask}
|
||||
changeTaskTitle={props.changeTaskTitle}
|
||||
changeTaskStatus={props.changeTaskStatus}
|
||||
/>)
|
||||
}
|
||||
</div>
|
||||
<div style={{paddingTop: '10px'}}>
|
||||
<Button variant={props.todolist.filter === 'all' ? 'outlined' : 'text'}
|
||||
onClick={onAllClickHandler}
|
||||
color={'inherit'}
|
||||
>All
|
||||
</Button>
|
||||
<Button variant={props.todolist.filter === 'active' ? 'outlined' : 'text'}
|
||||
onClick={onActiveClickHandler}
|
||||
color={'primary'}>Active
|
||||
</Button>
|
||||
<Button variant={props.todolist.filter === 'completed' ? 'outlined' : 'text'}
|
||||
onClick={onCompletedClickHandler}
|
||||
color={'secondary'}>Completed
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
})
|
||||
|
||||
|
||||
113
src/features/TodolistsList/TodolistsList.tsx
Normal file
113
src/features/TodolistsList/TodolistsList.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { AppRootStateType } from '../../app/store'
|
||||
import {
|
||||
addTodolistTC,
|
||||
changeTodolistFilterAC,
|
||||
changeTodolistTitleTC,
|
||||
fetchTodolistsTC,
|
||||
FilterValuesType,
|
||||
removeTodolistTC,
|
||||
TodolistDomainType
|
||||
} from './todolists-reducer'
|
||||
import { addTaskTC, removeTaskTC, TasksStateType, updateTaskTC } from './tasks-reducer'
|
||||
import { TaskStatuses } from '../../api/todolists-api'
|
||||
import { Grid, Paper } from '@mui/material'
|
||||
import { AddItemForm } from '../../components/AddItemForm/AddItemForm'
|
||||
import { Todolist } from './Todolist/Todolist'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { useAppDispatch } from '../../hooks/useAppDispatch';
|
||||
|
||||
type PropsType = {
|
||||
demo?: boolean
|
||||
}
|
||||
|
||||
export const TodolistsList: React.FC<PropsType> = ({demo = false}) => {
|
||||
const todolists = useSelector<AppRootStateType, Array<TodolistDomainType>>(state => state.todolists)
|
||||
const tasks = useSelector<AppRootStateType, TasksStateType>(state => state.tasks)
|
||||
const isLoggedIn = useSelector<AppRootStateType, boolean>(state => state.auth.isLoggedIn)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (demo || !isLoggedIn) {
|
||||
return;
|
||||
}
|
||||
const thunk = fetchTodolistsTC()
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const removeTask = useCallback(function (id: string, todolistId: string) {
|
||||
const thunk = removeTaskTC(id, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const addTask = useCallback(function (title: string, todolistId: string) {
|
||||
const thunk = addTaskTC(title, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeStatus = useCallback(function (id: string, status: TaskStatuses, todolistId: string) {
|
||||
const thunk = updateTaskTC(id, {status}, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeTaskTitle = useCallback(function (id: string, newTitle: string, todolistId: string) {
|
||||
const thunk = updateTaskTC(id, {title: newTitle}, todolistId)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeFilter = useCallback(function (value: FilterValuesType, todolistId: string) {
|
||||
const action = changeTodolistFilterAC(todolistId, value)
|
||||
dispatch(action)
|
||||
}, [])
|
||||
|
||||
const removeTodolist = useCallback(function (id: string) {
|
||||
const thunk = removeTodolistTC(id)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const changeTodolistTitle = useCallback(function (id: string, title: string) {
|
||||
const thunk = changeTodolistTitleTC(id, title)
|
||||
dispatch(thunk)
|
||||
}, [])
|
||||
|
||||
const addTodolist = useCallback((title: string) => {
|
||||
const thunk = addTodolistTC(title)
|
||||
dispatch(thunk)
|
||||
}, [dispatch])
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <Navigate to={"/login"} />
|
||||
}
|
||||
|
||||
return <>
|
||||
<Grid container style={{padding: '20px'}}>
|
||||
<AddItemForm addItem={addTodolist}/>
|
||||
</Grid>
|
||||
<Grid container spacing={3}>
|
||||
{
|
||||
todolists.map(tl => {
|
||||
let allTodolistTasks = tasks[tl.id]
|
||||
|
||||
return <Grid item key={tl.id}>
|
||||
<Paper style={{padding: '10px'}}>
|
||||
<Todolist
|
||||
todolist={tl}
|
||||
tasks={allTodolistTasks}
|
||||
removeTask={removeTask}
|
||||
changeFilter={changeFilter}
|
||||
addTask={addTask}
|
||||
changeTaskStatus={changeStatus}
|
||||
removeTodolist={removeTodolist}
|
||||
changeTaskTitle={changeTaskTitle}
|
||||
changeTodolistTitle={changeTodolistTitle}
|
||||
demo={demo}
|
||||
/>
|
||||
</Paper>
|
||||
</Grid>
|
||||
})
|
||||
}
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
133
src/features/TodolistsList/tasks-reducer.test.ts
Normal file
133
src/features/TodolistsList/tasks-reducer.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { addTaskAC, removeTaskAC, setTasksAC, tasksReducer, TasksStateType, updateTaskAC } from './tasks-reducer'
|
||||
|
||||
import {addTodolistAC, removeTodolistAC, setTodolistsAC} from './todolists-reducer'
|
||||
import {TaskPriorities, TaskStatuses} from '../../api/todolists-api'
|
||||
|
||||
let startState: TasksStateType = {};
|
||||
beforeEach(() => {
|
||||
startState = {
|
||||
"todolistId1": [
|
||||
{ id: "1", title: "CSS", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "2", title: "JS", status: TaskStatuses.Completed, todoListId: "todolistId1", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "3", title: "React", status: TaskStatuses.New, todoListId: "todolistId1", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
|
||||
],
|
||||
"todolistId2": [
|
||||
{ id: "1", title: "bread", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "2", title: "milk", status: TaskStatuses.Completed, todoListId: "todolistId2", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low },
|
||||
{ id: "3", title: "tea", status: TaskStatuses.New, todoListId: "todolistId2", description: '',
|
||||
startDate: '', deadline: '', addedDate: '', order: 0, priority: TaskPriorities.Low }
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
test('correct task should be deleted from correct array', () => {
|
||||
const action = removeTaskAC("2", "todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"].length).toBe(3);
|
||||
expect(endState["todolistId2"].length).toBe(2);
|
||||
expect(endState["todolistId2"].every(t => t.id != "2")).toBeTruthy();
|
||||
});
|
||||
test('correct task should be added to correct array', () => {
|
||||
//const action = addTaskAC("juce", "todolistId2");
|
||||
const action = addTaskAC({
|
||||
todoListId: "todolistId2",
|
||||
title: "juce",
|
||||
status: TaskStatuses.New,
|
||||
addedDate: "",
|
||||
deadline: "",
|
||||
description: "",
|
||||
order: 0,
|
||||
priority: 0,
|
||||
startDate: "",
|
||||
id: "id exists"
|
||||
});
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"].length).toBe(3);
|
||||
expect(endState["todolistId2"].length).toBe(4);
|
||||
expect(endState["todolistId2"][0].id).toBeDefined();
|
||||
expect(endState["todolistId2"][0].title).toBe("juce");
|
||||
expect(endState["todolistId2"][0].status).toBe(TaskStatuses.New);
|
||||
});
|
||||
test('status of specified task should be changed', () => {
|
||||
const action = updateTaskAC("2", {status: TaskStatuses.New}, "todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"][1].status).toBe(TaskStatuses.Completed);
|
||||
expect(endState["todolistId2"][1].status).toBe(TaskStatuses.New);
|
||||
});
|
||||
test('title of specified task should be changed', () => {
|
||||
const action = updateTaskAC("2", {title: "yogurt"}, "todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
expect(endState["todolistId1"][1].title).toBe("JS");
|
||||
expect(endState["todolistId2"][1].title).toBe("yogurt");
|
||||
expect(endState["todolistId2"][0].title).toBe("bread");
|
||||
});
|
||||
test('new array should be added when new todolist is added', () => {
|
||||
const action = addTodolistAC({
|
||||
id: "blabla",
|
||||
title: "new todolist",
|
||||
order: 0,
|
||||
addedDate: ''
|
||||
});
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
|
||||
const keys = Object.keys(endState);
|
||||
const newKey = keys.find(k => k != "todolistId1" && k != "todolistId2");
|
||||
if (!newKey) {
|
||||
throw Error("new key should be added")
|
||||
}
|
||||
|
||||
expect(keys.length).toBe(3);
|
||||
expect(endState[newKey]).toEqual([]);
|
||||
});
|
||||
test('propertry with todolistId should be deleted', () => {
|
||||
const action = removeTodolistAC("todolistId2");
|
||||
|
||||
const endState = tasksReducer(startState, action)
|
||||
|
||||
const keys = Object.keys(endState);
|
||||
|
||||
expect(keys.length).toBe(1);
|
||||
expect(endState["todolistId2"]).not.toBeDefined();
|
||||
});
|
||||
|
||||
test('empty arrays should be added when we set todolists', () => {
|
||||
const action = setTodolistsAC([
|
||||
{id: "1", title: "title 1", order: 0, addedDate: ""},
|
||||
{id: "2", title: "title 2", order: 0, addedDate: ""}
|
||||
])
|
||||
|
||||
const endState = tasksReducer({}, action)
|
||||
|
||||
const keys = Object.keys(endState)
|
||||
|
||||
expect(keys.length).toBe(2)
|
||||
expect(endState['1']).toBeDefined()
|
||||
expect(endState['2']).toBeDefined()
|
||||
})
|
||||
test('tasks should be added for todolist', () => {
|
||||
const action = setTasksAC(startState["todolistId1"], "todolistId1");
|
||||
|
||||
const endState = tasksReducer({
|
||||
"todolistId2": [],
|
||||
"todolistId1": []
|
||||
}, action)
|
||||
|
||||
expect(endState["todolistId1"].length).toBe(3)
|
||||
expect(endState["todolistId2"].length).toBe(0)
|
||||
})
|
||||
|
||||
140
src/features/TodolistsList/tasks-reducer.ts
Normal file
140
src/features/TodolistsList/tasks-reducer.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import {AddTodolistActionType, RemoveTodolistActionType, SetTodolistsActionType} from './todolists-reducer'
|
||||
import {TaskPriorities, TaskStatuses, TaskType, todolistsAPI, UpdateTaskModelType} from '../../api/todolists-api'
|
||||
import {Dispatch} from 'redux'
|
||||
import {AppRootStateType} from '../../app/store'
|
||||
import {setAppErrorAC, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
||||
import {handleServerAppError, handleServerNetworkError} from '../../utils/error-utils'
|
||||
|
||||
const initialState: TasksStateType = {}
|
||||
|
||||
export const tasksReducer = (state: TasksStateType = initialState, action: ActionsType): TasksStateType => {
|
||||
switch (action.type) {
|
||||
case 'REMOVE-TASK':
|
||||
return {...state, [action.todolistId]: state[action.todolistId].filter(t => t.id != action.taskId)}
|
||||
case 'ADD-TASK':
|
||||
return {...state, [action.task.todoListId]: [action.task, ...state[action.task.todoListId]]}
|
||||
case 'UPDATE-TASK':
|
||||
return {
|
||||
...state,
|
||||
[action.todolistId]: state[action.todolistId]
|
||||
.map(t => t.id === action.taskId ? {...t, ...action.model} : t)
|
||||
}
|
||||
case 'ADD-TODOLIST':
|
||||
return {...state, [action.todolist.id]: []}
|
||||
case 'REMOVE-TODOLIST':
|
||||
const copyState = {...state}
|
||||
delete copyState[action.id]
|
||||
return copyState
|
||||
case 'SET-TODOLISTS': {
|
||||
const copyState = {...state}
|
||||
action.todolists.forEach(tl => {
|
||||
copyState[tl.id] = []
|
||||
})
|
||||
return copyState
|
||||
}
|
||||
case 'SET-TASKS':
|
||||
return {...state, [action.todolistId]: action.tasks}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
export const removeTaskAC = (taskId: string, todolistId: string) =>
|
||||
({type: 'REMOVE-TASK', taskId, todolistId} as const)
|
||||
export const addTaskAC = (task: TaskType) =>
|
||||
({type: 'ADD-TASK', task} as const)
|
||||
export const updateTaskAC = (taskId: string, model: UpdateDomainTaskModelType, todolistId: string) =>
|
||||
({type: 'UPDATE-TASK', model, todolistId, taskId} as const)
|
||||
export const setTasksAC = (tasks: Array<TaskType>, todolistId: string) =>
|
||||
({type: 'SET-TASKS', tasks, todolistId} as const)
|
||||
|
||||
// thunks
|
||||
export const fetchTasksTC = (todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppStatusActionType>) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.getTasks(todolistId)
|
||||
.then((res) => {
|
||||
const tasks = res.data.items
|
||||
dispatch(setTasksAC(tasks, todolistId))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
}
|
||||
export const removeTaskTC = (taskId: string, todolistId: string) => (dispatch: Dispatch<ActionsType>) => {
|
||||
todolistsAPI.deleteTask(todolistId, taskId)
|
||||
.then(res => {
|
||||
const action = removeTaskAC(taskId, todolistId)
|
||||
dispatch(action)
|
||||
})
|
||||
}
|
||||
export const addTaskTC = (title: string, todolistId: string) => (dispatch: Dispatch<ActionsType | SetAppErrorActionType | SetAppStatusActionType>) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.createTask(todolistId, title)
|
||||
.then(res => {
|
||||
if (res.data.resultCode === 0) {
|
||||
const task = res.data.data.item
|
||||
const action = addTaskAC(task)
|
||||
dispatch(action)
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
} else {
|
||||
handleServerAppError(res.data, dispatch);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleServerNetworkError(error, dispatch)
|
||||
})
|
||||
}
|
||||
export const updateTaskTC = (taskId: string, domainModel: UpdateDomainTaskModelType, todolistId: string) =>
|
||||
(dispatch: ThunkDispatch, getState: () => AppRootStateType) => {
|
||||
const state = getState()
|
||||
const task = state.tasks[todolistId].find(t => t.id === taskId)
|
||||
if (!task) {
|
||||
//throw new Error("task not found in the state");
|
||||
console.warn('task not found in the state')
|
||||
return
|
||||
}
|
||||
|
||||
const apiModel: UpdateTaskModelType = {
|
||||
deadline: task.deadline,
|
||||
description: task.description,
|
||||
priority: task.priority,
|
||||
startDate: task.startDate,
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
...domainModel
|
||||
}
|
||||
|
||||
todolistsAPI.updateTask(todolistId, taskId, apiModel)
|
||||
.then(res => {
|
||||
if (res.data.resultCode === 0) {
|
||||
const action = updateTaskAC(taskId, domainModel, todolistId)
|
||||
dispatch(action)
|
||||
} else {
|
||||
handleServerAppError(res.data, dispatch);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
handleServerNetworkError(error, dispatch);
|
||||
})
|
||||
}
|
||||
|
||||
// types
|
||||
export type UpdateDomainTaskModelType = {
|
||||
title?: string
|
||||
description?: string
|
||||
status?: TaskStatuses
|
||||
priority?: TaskPriorities
|
||||
startDate?: string
|
||||
deadline?: string
|
||||
}
|
||||
export type TasksStateType = {
|
||||
[key: string]: Array<TaskType>
|
||||
}
|
||||
type ActionsType =
|
||||
| ReturnType<typeof removeTaskAC>
|
||||
| ReturnType<typeof addTaskAC>
|
||||
| ReturnType<typeof updateTaskAC>
|
||||
| AddTodolistActionType
|
||||
| RemoveTodolistActionType
|
||||
| SetTodolistsActionType
|
||||
| ReturnType<typeof setTasksAC>
|
||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
||||
89
src/features/TodolistsList/todolists-reducer.test.ts
Normal file
89
src/features/TodolistsList/todolists-reducer.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
addTodolistAC, changeTodolistEntityStatusAC,
|
||||
changeTodolistFilterAC,
|
||||
changeTodolistTitleAC, FilterValuesType,
|
||||
removeTodolistAC, setTodolistsAC, TodolistDomainType,
|
||||
todolistsReducer
|
||||
} from './todolists-reducer'
|
||||
import {v1} from 'uuid'
|
||||
import {TodolistType} from '../../api/todolists-api'
|
||||
import {RequestStatusType} from '../../app/app-reducer'
|
||||
|
||||
let todolistId1: string
|
||||
let todolistId2: string
|
||||
let startState: Array<TodolistDomainType> = []
|
||||
|
||||
beforeEach(() => {
|
||||
todolistId1 = v1()
|
||||
todolistId2 = v1()
|
||||
startState = [
|
||||
{id: todolistId1, title: 'What to learn', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0},
|
||||
{id: todolistId2, title: 'What to buy', filter: 'all', entityStatus: 'idle', addedDate: '', order: 0}
|
||||
]
|
||||
})
|
||||
|
||||
test('correct todolist should be removed', () => {
|
||||
const endState = todolistsReducer(startState, removeTodolistAC(todolistId1))
|
||||
|
||||
expect(endState.length).toBe(1)
|
||||
expect(endState[0].id).toBe(todolistId2)
|
||||
})
|
||||
|
||||
test('correct todolist should be added', () => {
|
||||
let todolist: TodolistType = {
|
||||
title: 'New Todolist',
|
||||
id: 'any id',
|
||||
addedDate: '',
|
||||
order: 0
|
||||
}
|
||||
|
||||
|
||||
const endState = todolistsReducer(startState, addTodolistAC(todolist))
|
||||
|
||||
expect(endState.length).toBe(3)
|
||||
expect(endState[0].title).toBe(todolist.title)
|
||||
expect(endState[0].filter).toBe('all')
|
||||
})
|
||||
|
||||
test('correct todolist should change its name', () => {
|
||||
let newTodolistTitle = 'New Todolist'
|
||||
|
||||
const action = changeTodolistTitleAC(todolistId2, newTodolistTitle)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].title).toBe('What to learn')
|
||||
expect(endState[1].title).toBe(newTodolistTitle)
|
||||
})
|
||||
|
||||
test('correct filter of todolist should be changed', () => {
|
||||
let newFilter: FilterValuesType = 'completed'
|
||||
|
||||
const action = changeTodolistFilterAC(todolistId2, newFilter)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].filter).toBe('all')
|
||||
expect(endState[1].filter).toBe(newFilter)
|
||||
})
|
||||
test('todolists should be added', () => {
|
||||
|
||||
const action = setTodolistsAC(startState)
|
||||
|
||||
const endState = todolistsReducer([], action)
|
||||
|
||||
expect(endState.length).toBe(2)
|
||||
})
|
||||
test('correct entity status of todolist should be changed', () => {
|
||||
let newStatus: RequestStatusType = 'loading'
|
||||
|
||||
const action = changeTodolistEntityStatusAC(todolistId2, newStatus)
|
||||
|
||||
const endState = todolistsReducer(startState, action)
|
||||
|
||||
expect(endState[0].entityStatus).toBe('idle')
|
||||
expect(endState[1].entityStatus).toBe(newStatus)
|
||||
})
|
||||
|
||||
|
||||
|
||||
109
src/features/TodolistsList/todolists-reducer.ts
Normal file
109
src/features/TodolistsList/todolists-reducer.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {todolistsAPI, TodolistType} from '../../api/todolists-api'
|
||||
import {Dispatch} from 'redux'
|
||||
import {RequestStatusType, SetAppErrorActionType, setAppStatusAC, SetAppStatusActionType} from '../../app/app-reducer'
|
||||
import {handleServerNetworkError} from '../../utils/error-utils'
|
||||
import { AppThunk } from '../../app/store';
|
||||
|
||||
const initialState: Array<TodolistDomainType> = []
|
||||
|
||||
export const todolistsReducer = (state: Array<TodolistDomainType> = initialState, action: ActionsType): Array<TodolistDomainType> => {
|
||||
switch (action.type) {
|
||||
case 'REMOVE-TODOLIST':
|
||||
return state.filter(tl => tl.id != action.id)
|
||||
case 'ADD-TODOLIST':
|
||||
return [{...action.todolist, filter: 'all', entityStatus: 'idle'}, ...state]
|
||||
|
||||
case 'CHANGE-TODOLIST-TITLE':
|
||||
return state.map(tl => tl.id === action.id ? {...tl, title: action.title} : tl)
|
||||
case 'CHANGE-TODOLIST-FILTER':
|
||||
return state.map(tl => tl.id === action.id ? {...tl, filter: action.filter} : tl)
|
||||
case 'CHANGE-TODOLIST-ENTITY-STATUS':
|
||||
return state.map(tl => tl.id === action.id ? {...tl, entityStatus: action.status} : tl)
|
||||
case 'SET-TODOLISTS':
|
||||
return action.todolists.map(tl => ({...tl, filter: 'all', entityStatus: 'idle'}))
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
export const removeTodolistAC = (id: string) => ({type: 'REMOVE-TODOLIST', id} as const)
|
||||
export const addTodolistAC = (todolist: TodolistType) => ({type: 'ADD-TODOLIST', todolist} as const)
|
||||
export const changeTodolistTitleAC = (id: string, title: string) => ({
|
||||
type: 'CHANGE-TODOLIST-TITLE',
|
||||
id,
|
||||
title
|
||||
} as const)
|
||||
export const changeTodolistFilterAC = (id: string, filter: FilterValuesType) => ({
|
||||
type: 'CHANGE-TODOLIST-FILTER',
|
||||
id,
|
||||
filter
|
||||
} as const)
|
||||
export const changeTodolistEntityStatusAC = (id: string, status: RequestStatusType) => ({
|
||||
type: 'CHANGE-TODOLIST-ENTITY-STATUS', id, status } as const)
|
||||
export const setTodolistsAC = (todolists: Array<TodolistType>) => ({type: 'SET-TODOLISTS', todolists} as const)
|
||||
|
||||
// thunks
|
||||
export const fetchTodolistsTC = (): AppThunk => {
|
||||
return (dispatch) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.getTodolists()
|
||||
.then((res) => {
|
||||
dispatch(setTodolistsAC(res.data))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
.catch(error => {
|
||||
handleServerNetworkError(error, dispatch);
|
||||
})
|
||||
}
|
||||
}
|
||||
export const removeTodolistTC = (todolistId: string) => {
|
||||
return (dispatch: ThunkDispatch) => {
|
||||
//изменим глобальный статус приложения, чтобы вверху полоса побежала
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
//изменим статус конкретного тудулиста, чтобы он мог задизеблить что надо
|
||||
dispatch(changeTodolistEntityStatusAC(todolistId, 'loading'))
|
||||
todolistsAPI.deleteTodolist(todolistId)
|
||||
.then((res) => {
|
||||
dispatch(removeTodolistAC(todolistId))
|
||||
//скажем глобально приложению, что асинхронная операция завершена
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
}
|
||||
}
|
||||
export const addTodolistTC = (title: string) => {
|
||||
return (dispatch: ThunkDispatch) => {
|
||||
dispatch(setAppStatusAC('loading'))
|
||||
todolistsAPI.createTodolist(title)
|
||||
.then((res) => {
|
||||
dispatch(addTodolistAC(res.data.data.item))
|
||||
dispatch(setAppStatusAC('succeeded'))
|
||||
})
|
||||
}
|
||||
}
|
||||
export const changeTodolistTitleTC = (id: string, title: string) => {
|
||||
return (dispatch: Dispatch<ActionsType>) => {
|
||||
todolistsAPI.updateTodolist(id, title)
|
||||
.then((res) => {
|
||||
dispatch(changeTodolistTitleAC(id, title))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// types
|
||||
export type AddTodolistActionType = ReturnType<typeof addTodolistAC>;
|
||||
export type RemoveTodolistActionType = ReturnType<typeof removeTodolistAC>;
|
||||
export type SetTodolistsActionType = ReturnType<typeof setTodolistsAC>;
|
||||
type ActionsType =
|
||||
| RemoveTodolistActionType
|
||||
| AddTodolistActionType
|
||||
| ReturnType<typeof changeTodolistTitleAC>
|
||||
| ReturnType<typeof changeTodolistFilterAC>
|
||||
| SetTodolistsActionType
|
||||
| ReturnType<typeof changeTodolistEntityStatusAC>
|
||||
export type FilterValuesType = 'all' | 'active' | 'completed';
|
||||
export type TodolistDomainType = TodolistType & {
|
||||
filter: FilterValuesType
|
||||
entityStatus: RequestStatusType
|
||||
}
|
||||
type ThunkDispatch = Dispatch<ActionsType | SetAppStatusActionType | SetAppErrorActionType>
|
||||
27
src/features/TodolistsList/todolists-tasks-reducer.test.ts
Normal file
27
src/features/TodolistsList/todolists-tasks-reducer.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import {addTodolistAC, TodolistDomainType, todolistsReducer} from './todolists-reducer'
|
||||
import {tasksReducer, TasksStateType} from './tasks-reducer'
|
||||
import {TodolistType} from '../../api/todolists-api'
|
||||
|
||||
test('ids should be equals', () => {
|
||||
const startTasksState: TasksStateType = {};
|
||||
const startTodolistsState: Array<TodolistDomainType> = [];
|
||||
|
||||
let todolist: TodolistType = {
|
||||
title: 'new todolist',
|
||||
id: 'any id',
|
||||
addedDate: '',
|
||||
order: 0
|
||||
}
|
||||
|
||||
const action = addTodolistAC(todolist);
|
||||
|
||||
const endTasksState = tasksReducer(startTasksState, action)
|
||||
const endTodolistsState = todolistsReducer(startTodolistsState, action)
|
||||
|
||||
const keys = Object.keys(endTasksState);
|
||||
const idFromTasks = keys[0];
|
||||
const idFromTodolists = endTodolistsState[0].id;
|
||||
|
||||
expect(idFromTasks).toBe(action.todolist.id);
|
||||
expect(idFromTodolists).toBe(action.todolist.id);
|
||||
});
|
||||
Reference in New Issue
Block a user