From d7f2a6d0d9c195cc6f7af267a16b75a6331c2910 Mon Sep 17 00:00:00 2001 From: AbdBarho Date: Sun, 29 Jan 2023 10:56:54 +0100 Subject: [PATCH] Consolidate API calls to TaskContext --- website/src/components/TaskPage/TaskPage.tsx | 49 ++++++------- website/src/components/Tasks/Task/Task.tsx | 71 ++++++------------- website/src/components/Tasks/TaskTypes.tsx | 26 +------ website/src/context/TaskContext.ts | 7 ++ website/src/hooks/tasks/useGenericTaskAPI.tsx | 62 ++++++++++++---- website/src/types/Hooks.ts | 17 ++--- website/src/types/Task.ts | 29 +++++++- 7 files changed, 135 insertions(+), 126 deletions(-) create mode 100644 website/src/context/TaskContext.ts diff --git a/website/src/components/TaskPage/TaskPage.tsx b/website/src/components/TaskPage/TaskPage.tsx index 77b1bfe9..c76ec3d1 100644 --- a/website/src/components/TaskPage/TaskPage.tsx +++ b/website/src/components/TaskPage/TaskPage.tsx @@ -1,13 +1,14 @@ import Head from "next/head"; import { useTranslation } from "next-i18next"; +import { useMemo } from "react"; import { TaskEmptyState } from "src/components/EmptyState"; import { LoadingScreen } from "src/components/Loading/LoadingScreen"; import { Task } from "src/components/Tasks/Task"; import { TaskInfos } from "src/components/Tasks/TaskTypes"; +import { TaskContext } from "src/context/TaskContext"; import { taskApiHooks } from "src/lib/constants"; import { getTypeSafei18nKey } from "src/lib/i18n"; import { TaskType } from "src/types/Task"; -import { KnownTaskType } from "src/types/Tasks"; type TaskPageProps = { type: TaskType; @@ -15,39 +16,33 @@ type TaskPageProps = { export const TaskPage = ({ type }: TaskPageProps) => { const { t } = useTranslation(["tasks", "common"]); - const taskApiHook = taskApiHooks[type]; - const { response, isLoading, completeTask, skipTask } = taskApiHook(type); + const taskApiHook = useMemo(() => taskApiHooks[type], [type]); + const hookState = taskApiHook(type); + + const { response } = hookState; + + const body = useMemo(() => { + switch (response.taskAvailability) { + case "AWAITING_INITIAL": + return ; + + case "NONE_AVAILABLE": + return ; + + case "AVAILABLE": + return ; + } + }, [response, t]); + + // NOTE: this is independent of the fetched task type, it is usually identical, but not for the random task. const taskInfo = TaskInfos.find((taskType) => taskType.type === type); - - let body; - switch (response.taskAvailability) { - case "AWAITING_INITIAL": - body = ; - break; - case "NONE_AVAILABLE": - body = ; - break; - case "AVAILABLE": - body = ( - - ); - break; - } - return ( <> {t(getTypeSafei18nKey(`${taskInfo.id}.label`))} - {body} + {body} ); }; diff --git a/website/src/components/Tasks/Task/Task.tsx b/website/src/components/Tasks/Task/Task.tsx index 738c3667..2ddb863d 100644 --- a/website/src/components/Tasks/Task/Task.tsx +++ b/website/src/components/Tasks/Task/Task.tsx @@ -5,13 +5,12 @@ import { TaskControls } from "src/components/Survey/TaskControls"; import { CreateTask } from "src/components/Tasks/CreateTask"; import { EvaluateTask } from "src/components/Tasks/EvaluateTask"; import { LabelTask } from "src/components/Tasks/LabelTask"; -import { TaskCategory, TaskInfo, TaskInfos } from "src/components/Tasks/TaskTypes"; import { UnchangedWarning } from "src/components/Tasks/UnchangedWarning"; -import { post } from "src/lib/api"; +import { useTaskContext } from "src/context/TaskContext"; import { getTypeSafei18nKey } from "src/lib/i18n"; +import { TaskCategory, TaskInfo } from "src/types/Task"; import { BaseTask, TaskContent, TaskReplyValidity } from "src/types/Task"; -import { CreateTaskType, KnownTaskType, LabelTaskType, RankTaskType } from "src/types/Tasks"; -import useSWRMutation from "swr/mutation"; +import { CreateTaskType, LabelTaskType, RankTaskType } from "src/types/Tasks"; interface EditMode { mode: "EDIT"; @@ -63,16 +62,11 @@ export interface TaskSurveyProps { onValidityChanged: (validity: TaskReplyValidity) => void; } -interface TaskProps { - frontendId: string; - task: KnownTaskType; - isLoading: boolean; - completeTask: (TaskContent) => void; - skipTask: () => void; -} - -export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: TaskProps) => { +export const Task = () => { const { t } = useTranslation("tasks"); + const rootEl = useRef(null); + const replyContent = useRef(null); + const { rejectTask, completeTask, response, isLoading } = useTaskContext(); const [taskStatus, taskEvent] = useReducer( ( status: TaskStatus, @@ -114,7 +108,11 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta { mode: "EDIT", replyValidity: "INVALID" } ); - const replyContent = useRef(null); + if (response.taskAvailability !== "AVAILABLE") { + throw new Error("Cannot render task when it is unavailable yet"); + } + const { task, taskInfo } = response; + const updateValidity = useCallback( (replyValidity: TaskReplyValidity) => taskEvent({ action: "UPDATE_VALIDITY", replyValidity }), [taskEvent] @@ -122,26 +120,7 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta useEffect(() => { taskEvent({ action: "NEW_TASK" }); - }, [task.id, updateValidity]); - - const rootEl = useRef(null); - - const taskType = useMemo(() => { - return TaskInfos.find((taskType) => taskType.type === task.type); - }, [task.type]); - - const { trigger: sendRejection } = useSWRMutation("/api/reject_task", post, { - onSuccess: async () => { - skipTask(); - }, - }); - - const rejectTask = (reason: string) => { - sendRejection({ - id: frontendId, - reason, - }); - }; + }, [task.id]); const onReplyChanged = useCallback( (content: TaskContent) => { @@ -150,25 +129,21 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta [replyContent] ); - const submitResponse = () => { + const submitResponse = useCallback(() => { if (taskStatus.mode === "REVIEW") { - completeTask({ - id: frontendId, - update_type: taskType.update_type, - content: replyContent.current, - }); + completeTask(replyContent.current); taskEvent({ action: "SET_SUBMITTED" }); scrollToTop(rootEl.current); } - }; + }, [taskStatus.mode, completeTask]); const taskTypeComponent = useMemo(() => { - switch (taskType.category) { + switch (taskInfo.category) { case TaskCategory.Create: return ( ); } - }, [task, taskType, taskStatus.mode, onReplyChanged, updateValidity]); + }, [taskInfo, task, taskStatus.mode, onReplyChanged, updateValidity]); return (
@@ -214,8 +189,8 @@ export const Task = ({ frontendId, task, isLoading, completeTask, skipTask }: Ta /> taskEvent({ action: "RETURN_EDIT" })} onContinueAnyway={() => { diff --git a/website/src/components/Tasks/TaskTypes.tsx b/website/src/components/Tasks/TaskTypes.tsx index e2a53d23..4f204720 100644 --- a/website/src/components/Tasks/TaskTypes.tsx +++ b/website/src/components/Tasks/TaskTypes.tsx @@ -1,28 +1,4 @@ -import { TaskType } from "src/types/Task"; - -export enum TaskCategory { - Create = "Create", - Evaluate = "Evaluate", - Label = "Label", - Random = "Random", -} - -export enum TaskUpdateType { - MessageRanking = "message_ranking", - Random = "random", - TextLabels = "text_labels", - TextReplyToMessage = "text_reply_to_message", -} - -export interface TaskInfo { - category: TaskCategory; - help_link: string; - id: string; - mode?: string; - pathname: string; - type: string; - update_type: string; -} +import { TaskCategory, TaskInfo, TaskType, TaskUpdateType } from "src/types/Task"; export const TaskCategoryLabels: { [key in TaskCategory]: string } = { [TaskCategory.Random]: "grab_a_task", diff --git a/website/src/context/TaskContext.ts b/website/src/context/TaskContext.ts new file mode 100644 index 00000000..8950ddfd --- /dev/null +++ b/website/src/context/TaskContext.ts @@ -0,0 +1,7 @@ +import { createContext, useContext } from "react"; +import { TaskApiHook } from "src/types/Hooks"; +import { BaseTask } from "src/types/Task"; + +export const TaskContext = createContext>(null); + +export const useTaskContext = () => useContext(TaskContext); diff --git a/website/src/hooks/tasks/useGenericTaskAPI.tsx b/website/src/hooks/tasks/useGenericTaskAPI.tsx index fc09367b..f6fd2e40 100644 --- a/website/src/hooks/tasks/useGenericTaskAPI.tsx +++ b/website/src/hooks/tasks/useGenericTaskAPI.tsx @@ -1,38 +1,74 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; +import { TaskInfos } from "src/components/Tasks/TaskTypes"; import { get, post } from "src/lib/api"; import { TaskApiHook } from "src/types/Hooks"; -import { BaseTask, TaskAvailableResponse, TaskResponse, TaskType as TaskTypeEnum } from "src/types/Task"; +import { BaseTask, ServerTaskResponse, TaskResponse, TaskType as TaskTypeEnum } from "src/types/Task"; import useSWRImmutable from "swr/immutable"; import useSWRMutation from "swr/mutation"; -export const useGenericTaskAPI = (taskType: TaskTypeEnum): TaskApiHook => { - const [response, setReponse] = useState>({ taskAvailability: "AWAITING_INITIAL" }); +export const useGenericTaskAPI = ( + taskType: TaskTypeEnum +): TaskApiHook => { + const [response, setResponse] = useState>({ taskAvailability: "AWAITING_INITIAL" }); + // Note: We use isValidating to indiate we are loading beause it signals eash load, not just the first one. - const { isValidating: isLoading, mutate: requestNewTask } = useSWRImmutable>( + const { isValidating: isLoading, mutate: requestNewTask } = useSWRImmutable>( "/api/new_task/" + taskType, get, { - onSuccess: (response) => { - setReponse({ taskAvailability: "AVAILABLE", ...response }); + onSuccess: (taskResponse) => { + setResponse({ + ...taskResponse, + taskAvailability: "AVAILABLE", + taskInfo: TaskInfos.find((taskType) => taskType.type === taskResponse.task.type), + }); }, onError: () => { - // We could check for code 503 here for truely unavailable, but we need to do something with other errors anyway. - setReponse({ taskAvailability: "NONE_AVAILABLE" }); + // We could check for code 503 here for truly unavailable, but we need to do something with other errors anyway. + setResponse({ taskAvailability: "NONE_AVAILABLE" }); }, revalidateOnMount: true, dedupingInterval: 500, } ); - const { trigger: completeTask } = useSWRMutation>("/api/update_task", post, { + const { trigger: sendTaskContent } = useSWRMutation("/api/update_task", post, { onSuccess: () => { requestNewTask(); }, onError: () => { - // We could check for code 503 here for truely unavailable, but we need to do something with other errors anyway. - setReponse({ taskAvailability: "NONE_AVAILABLE" }); + // We could check for code 503 here for truly unavailable, but we need to do something with other errors anyway. + setResponse({ taskAvailability: "NONE_AVAILABLE" }); }, }); - return { response, isLoading, completeTask, skipTask: requestNewTask }; + // NOTE: it might make sense to split this hook into 2 parts + + // makes sure that requestNewTask is always called without parameters: + const skipTask = useCallback(async () => { + await requestNewTask(); + }, [requestNewTask]); + + const { trigger: sendRejection } = useSWRMutation("/api/reject_task", post, { onSuccess: skipTask }); + const rejectTask = useCallback( + async (reason: string) => { + if (response.taskAvailability !== "AVAILABLE") { + throw new Error("Cannot reject task that is not yet ready"); + } + await sendRejection({ id: response.id, reason }); + }, + [response, sendRejection] + ); + + const completeTask = useCallback( + async (content: ResponseContent) => { + if (response.taskAvailability !== "AVAILABLE") { + throw new Error("Cannot complete task that is not yet ready"); + } + await sendTaskContent({ id: response.id, update_type: response.taskInfo.update_type, content }); + }, + [response, sendTaskContent] + ); + + return { response, isLoading, rejectTask, completeTask, skipTask }; }; diff --git a/website/src/types/Hooks.ts b/website/src/types/Hooks.ts index 8da40527..acdd0b58 100644 --- a/website/src/types/Hooks.ts +++ b/website/src/types/Hooks.ts @@ -1,16 +1,11 @@ -import { BaseTask, TaskContent, TaskResponse, TaskType } from "src/types/Task"; +import { BaseTask, TaskResponse, TaskType } from "src/types/Task"; -interface TaskInteraction { - id: string; - update_type: string; - content: TaskContent; -} - -export type TaskApiHook = { +export type TaskApiHook = { response: TaskResponse; isLoading: boolean; - completeTask: (interaction: TaskInteraction) => void; - skipTask: () => void; + completeTask: (interaction: ResponseContent) => Promise; + skipTask: () => Promise; + rejectTask: (reason: string) => Promise; }; -export type TaskApiHooks = Record TaskApiHook>; +export type TaskApiHooks = Record TaskApiHook>; diff --git a/website/src/types/Task.ts b/website/src/types/Task.ts index 5e2abf16..cb493b56 100644 --- a/website/src/types/Task.ts +++ b/website/src/types/Task.ts @@ -14,6 +14,30 @@ export enum TaskType { random = "random", } +export enum TaskCategory { + Create = "Create", + Evaluate = "Evaluate", + Label = "Label", + Random = "Random", +} + +export interface TaskInfo { + category: TaskCategory; + help_link: string; + id: string; + mode?: string; + pathname: string; + type: string; + update_type: string; +} + +export enum TaskUpdateType { + MessageRanking = "message_ranking", + Random = "random", + TextLabels = "text_labels", + TextReplyToMessage = "text_reply_to_message", +} + // we need to reconsider how to handle task content types // eslint-disable-next-line @typescript-eslint/no-explicit-any export type TaskContent = any; @@ -29,14 +53,15 @@ export interface BaseTask { type: TaskType; } -export interface TaskAvailableResponse { +export interface ServerTaskResponse { id: string; userId: string; task: Task; } -interface TaskAvailable extends TaskAvailableResponse { +interface TaskAvailable extends ServerTaskResponse { taskAvailability: "AVAILABLE"; + taskInfo: TaskInfo; } interface AwaitingInitialTask {