-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from codiumkg/feature/tasks
[FEATURE] Tasks CRUD
- Loading branch information
Showing
12 changed files
with
365 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { ITask } from "./task"; | ||
|
||
export interface IAnswer { | ||
id: number; | ||
text: string; | ||
isCorrectAnswer: boolean; | ||
taskId: number; | ||
task: ITask; | ||
createdAt: string; | ||
updatedAt: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { IAnswer } from "./answer"; | ||
import { ITopic } from "./topic"; | ||
|
||
export interface ITask { | ||
id: number; | ||
text: string; | ||
image?: string; | ||
tip?: string; | ||
topicId: number; | ||
topic: ITopic; | ||
answers: IAnswer[]; | ||
createdAt: string; | ||
updatedAt: string; | ||
} | ||
|
||
export interface ITaskCreate { | ||
text: string; | ||
image?: string; | ||
tip?: string; | ||
topicId: number; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import CustomInput from "@/components/shared/CustomInput/CustomInput"; | ||
import RelationInput from "@/components/shared/RelationInput/RelationInput"; | ||
import Resource from "@/components/shared/Resource/Resource"; | ||
import TextEditor from "@/components/shared/TextEditor/TextEditor"; | ||
import { QUERY_KEYS } from "@/constants/queryKeys"; | ||
import { ROUTES } from "@/constants/routes"; | ||
import { useNotification } from "@/hooks/useNotification"; | ||
import { IOption } from "@/interfaces/common"; | ||
import { ITaskCreate } from "@/interfaces/task"; | ||
import { | ||
useTaskDeletion, | ||
useTaskDetails, | ||
useTaskMutation, | ||
} from "@/queries/tasks"; | ||
import { useTopicsQuery } from "@/queries/topics"; | ||
import { useQueryClient } from "@tanstack/react-query"; | ||
import { useEffect, useMemo, useState } from "react"; | ||
import { Controller, SubmitHandler, useForm } from "react-hook-form"; | ||
import { useNavigate, useParams } from "react-router-dom"; | ||
|
||
const initialValues: ITaskCreate = { | ||
text: "", | ||
tip: "", | ||
topicId: -1, | ||
}; | ||
|
||
function TaskDetails() { | ||
const queryClient = useQueryClient(); | ||
|
||
const { id } = useParams(); | ||
|
||
const navigate = useNavigate(); | ||
|
||
const [search, setSearch] = useState(""); | ||
|
||
const { showSuccessNotification, showErrorNotification } = useNotification(); | ||
|
||
const { data: existingTask, isLoading: isTaskLoading } = useTaskDetails( | ||
+id!, | ||
{ enabled: !!id } | ||
); | ||
|
||
const { | ||
data: topics, | ||
isLoading: isTopicsLoading, | ||
refetch, | ||
} = useTopicsQuery({ params: { search } }); | ||
|
||
const [activeValue, setActiveValue] = useState<IOption>({ | ||
label: topics?.[0].title, | ||
value: topics?.[0].id.toString(), | ||
}); | ||
|
||
const topicOptions = useMemo( | ||
() => | ||
topics?.map((topic) => ({ | ||
label: topic.title, | ||
value: topic.id.toString(), | ||
})) || [], | ||
[topics] | ||
); | ||
|
||
const { mutate, isPending } = useTaskMutation({ | ||
onSuccess: () => { | ||
queryClient.invalidateQueries({ | ||
refetchType: "all", | ||
queryKey: [QUERY_KEYS.TASKS], | ||
}); | ||
|
||
navigate(ROUTES.TASKS); | ||
|
||
showSuccessNotification(); | ||
}, | ||
onError: () => { | ||
showErrorNotification(); | ||
}, | ||
}); | ||
|
||
const { mutate: deleteTask, isPending: isDeleting } = useTaskDeletion({ | ||
onSuccess: () => { | ||
queryClient.invalidateQueries({ | ||
refetchType: "all", | ||
queryKey: [QUERY_KEYS.TASKS], | ||
}); | ||
|
||
navigate(ROUTES.TASKS); | ||
|
||
showSuccessNotification(); | ||
}, | ||
onError: () => { | ||
showErrorNotification(); | ||
}, | ||
}); | ||
|
||
const taskForm = useForm<ITaskCreate>({ | ||
defaultValues: initialValues, | ||
mode: "onBlur", | ||
}); | ||
|
||
const isValid = Object.values(taskForm.formState.errors).length === 0; | ||
|
||
const onSubmit: SubmitHandler<ITaskCreate> = (data: ITaskCreate) => { | ||
mutate(data); | ||
}; | ||
|
||
useEffect(() => { | ||
if (existingTask && id) { | ||
taskForm.reset(existingTask); | ||
setActiveValue({ | ||
label: existingTask.topic.title, | ||
value: existingTask.topic.id.toString(), | ||
}); | ||
} | ||
}, [existingTask, taskForm, id]); | ||
|
||
useEffect(() => { | ||
if (!existingTask || !id) { | ||
setActiveValue({ | ||
label: topics?.[0].title, | ||
value: topics?.[0].id.toString(), | ||
}); | ||
} | ||
|
||
if (topics) { | ||
taskForm.setValue("topicId", topics[0].id); | ||
} | ||
}, [topics, taskForm, existingTask, id]); | ||
|
||
return ( | ||
<Resource | ||
title="Задача" | ||
isExisting={!!id} | ||
isLoading={isTaskLoading} | ||
isSaveDisabled={!isValid || !taskForm.formState.isDirty} | ||
isSaveButtonLoading={isPending} | ||
onDeleteClick={() => deleteTask(+id!)} | ||
isDeleting={isDeleting} | ||
onSaveClick={() => onSubmit(taskForm.getValues())} | ||
> | ||
<Controller | ||
name="text" | ||
control={taskForm.control} | ||
render={({ field }) => ( | ||
<TextEditor | ||
label="Содержимое" | ||
{...field} | ||
placeholder="Введите содержимое задачи..." | ||
/> | ||
)} | ||
/> | ||
|
||
<CustomInput | ||
{...taskForm.register("tip")} | ||
label="Подсказка" | ||
placeholder="Введите подсказку..." | ||
errorMessage={taskForm.formState.errors.tip?.message} | ||
onChangeCallback={(value) => taskForm.setValue("tip", value)} | ||
/> | ||
|
||
<RelationInput | ||
name="topic" | ||
options={topicOptions} | ||
activeValue={activeValue} | ||
setActiveValue={(value) => { | ||
taskForm.setValue("topicId", +value.value); | ||
setActiveValue(value); | ||
}} | ||
label="Топик" | ||
placeholder="Выберите топик..." | ||
isLoading={isTopicsLoading} | ||
onSearch={(value) => { | ||
setSearch(value); | ||
refetch(); | ||
}} | ||
/> | ||
</Resource> | ||
); | ||
} | ||
|
||
export default TaskDetails; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import ResourceList from "@/components/shared/ResourceList/ResourceList"; | ||
import Table, { TableColumn, TableRow } from "@/components/shared/Table/Table"; | ||
import { ROUTES } from "@/constants/routes"; | ||
import { useTasksQuery } from "@/queries/tasks"; | ||
import { useNavigate } from "react-router-dom"; | ||
|
||
function TasksPage() { | ||
const { data: tasks, isPending } = useTasksQuery(); | ||
|
||
const navigate = useNavigate(); | ||
|
||
return ( | ||
<ResourceList | ||
title="Задачи" | ||
isLoading={isPending} | ||
onCreateClick={() => navigate(ROUTES.TASK)} | ||
itemsLength={tasks?.length} | ||
> | ||
<Table | ||
headers={[{ title: "ID" }, { title: "Содержимое" }, { title: "Топик" }]} | ||
> | ||
{tasks?.map((task) => ( | ||
<TableRow | ||
key={task.id} | ||
onClick={() => navigate(`${ROUTES.TASK}/${task.id}`)} | ||
> | ||
<TableColumn>{task.id}</TableColumn> | ||
<TableColumn>{task.text}</TableColumn> | ||
<TableColumn>{task.topic?.title}</TableColumn> | ||
</TableRow> | ||
))} | ||
</Table> | ||
</ResourceList> | ||
); | ||
} | ||
|
||
export default TasksPage; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { QUERY_KEYS } from "@/constants/queryKeys"; | ||
import { ITask, ITaskCreate } from "@/interfaces/task"; | ||
import { | ||
createTask, | ||
getTaskDetails, | ||
getTasks, | ||
removeTask, | ||
updateTask, | ||
} from "@/requests/tasks"; | ||
import { useMutation, useQuery } from "@tanstack/react-query"; | ||
|
||
export const useTasksQuery = () => { | ||
const { data, isPending } = useQuery({ | ||
queryFn: getTasks, | ||
queryKey: [QUERY_KEYS.TASKS], | ||
}); | ||
|
||
return { | ||
data, | ||
isPending, | ||
}; | ||
}; | ||
|
||
interface QueryParams { | ||
enabled?: boolean; | ||
} | ||
|
||
export const useTaskDetails = (id: number, { enabled }: QueryParams) => { | ||
const { data, isLoading } = useQuery({ | ||
queryFn: () => getTaskDetails(id), | ||
queryKey: [QUERY_KEYS.TASKS, id], | ||
enabled, | ||
}); | ||
|
||
return { | ||
data, | ||
isLoading, | ||
}; | ||
}; | ||
|
||
interface MutationQuery { | ||
onSuccess?: (data: ITask) => void; | ||
onError?: () => void; | ||
} | ||
|
||
export const useTaskMutation = (params?: MutationQuery) => { | ||
const { data, mutate, isPending } = useMutation({ | ||
mutationFn: (data: ITaskCreate, id?: number) => | ||
id ? updateTask(id, data) : createTask(data), | ||
onSuccess: params?.onSuccess, | ||
onError: params?.onError, | ||
}); | ||
|
||
return { | ||
data, | ||
mutate, | ||
isPending, | ||
}; | ||
}; | ||
|
||
export const useTaskDeletion = (params?: MutationQuery) => { | ||
const { mutate, isPending } = useMutation({ | ||
mutationFn: (id: number) => removeTask(id), | ||
onSuccess: params?.onSuccess, | ||
onError: params?.onError, | ||
}); | ||
|
||
return { | ||
mutate, | ||
isPending, | ||
}; | ||
}; |
Oops, something went wrong.