Skip to content

Commit

Permalink
active task to url query param
Browse files Browse the repository at this point in the history
  • Loading branch information
j8seangel committed Jun 5, 2024
1 parent 33d1624 commit adcaf2a
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 54 deletions.
3 changes: 3 additions & 0 deletions apps/image-labeler/src/api/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export const gfwBaseQuery =
async ({ url, signal, body }) => {
try {
const data = await GFWAPI.fetch<Response>(baseUrl + url, { signal, method, body })
if (!data) {
return { data: body }
}
return { data }
} catch (gfwApiError: any) {
return {
Expand Down
14 changes: 13 additions & 1 deletion apps/image-labeler/src/api/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ type LabellingProjectsApiParams = {
limit: number
}

type LabellingProjectsByIdApiParams = {
projectId: string
taskId: string
}

export const projectApi = createApi({
reducerPath: 'projectApi',
baseQuery: gfwBaseQuery({
Expand All @@ -19,9 +24,16 @@ export const projectApi = createApi({
}
},
}),
getLabellingProjectTasksById: builder.query({
query: ({ projectId, taskId }: LabellingProjectsByIdApiParams) => {
return {
url: `/${projectId}/task/${taskId}`,
}
},
}),
}),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetLabellingProjectTasksQuery } = projectApi
export const { useGetLabellingProjectTasksQuery, useGetLabellingProjectTasksByIdQuery } = projectApi
88 changes: 62 additions & 26 deletions apps/image-labeler/src/features/project/Project.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,104 @@
import { getRouteApi } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { getRouteApi, useNavigate } from '@tanstack/react-router'
import { useCallback, useEffect, useMemo } from 'react'
import uniqBy from 'lodash/uniqBy'
import { Spinner } from '@globalfishingwatch/ui-components/spinner'
import { Button } from '@globalfishingwatch/ui-components/button'
import { useGetLabellingProjectTasksQuery } from '../../api/project'
import {
useGetLabellingProjectTasksByIdQuery,
useGetLabellingProjectTasksQuery,
} from '../../api/project'
import styles from './Project.module.css'
import Task, { LabellingTask } from './Task'

const route = getRouteApi('/project/$projectId')
const routePath = '/project/$projectId'
const route = getRouteApi(routePath)

export function Project() {
const { projectId } = route.useParams()
const { activeTaskId } = route.useSearch()
const navigate = useNavigate({ from: routePath })

const { data, isLoading } = useGetLabellingProjectTasksQuery({ projectId, limit: 25 })
// eslint-disable-next-line react-hooks/exhaustive-deps
const initialActiveTaskId = useMemo(() => activeTaskId as string | undefined, [])

const [activeTaskId, setActiveTaskId] = useState<string>()
const { data: taskData, isLoading: areTasksLoading } = useGetLabellingProjectTasksQuery({
projectId,
limit: 25,
})
const { data: activeTaskData, isLoading: isActiveTaskLoading } =
useGetLabellingProjectTasksByIdQuery(
{
projectId,
taskId: initialActiveTaskId as string,
},
{ skip: initialActiveTaskId === undefined }
)

useEffect(() => {
if (!activeTaskId) {
setActiveTaskId(data?.entries[0]?.id)
const isLoading = areTasksLoading || isActiveTaskLoading
const data = useMemo(() => {
if (!activeTaskData) {
return taskData?.entries as LabellingTask[]
}
}, [activeTaskId, data])
return uniqBy([activeTaskData, ...(taskData?.entries || [])], 'id') as LabellingTask[]
}, [activeTaskData, taskData?.entries])

if (isLoading || !data) {
return <Spinner />
}
const setActiveTaskId = useCallback(
(activeTaskId: string) => {
navigate({ search: { activeTaskId } })
},
[navigate]
)

const setNextTask = () => {
const currentIndex = (data?.entries as LabellingTask[]).findIndex(
(task) => task.id === activeTaskId
)
setActiveTaskId((data?.entries as LabellingTask[])[currentIndex + 1]?.id)
window.scrollTo(0, window.scrollY + 71)
}
const setNextTask = useCallback(
(taskId: string) => {
const currentIndex = data.findIndex((task) => task.id === taskId)
setActiveTaskId(data[currentIndex + 1]?.id)
window.scrollTo(0, window.scrollY + 71)
},
[data, setActiveTaskId]
)

const handleLoadMoreTasks = () => {
const handleLoadMoreTasks = useCallback(() => {
window.scrollTo(0, 0)
window.location.reload()
}, [])

useEffect(() => {
if (!activeTaskId) {
setActiveTaskId(data?.[0]?.id)
}
}, [activeTaskId, data, setActiveTaskId])

if (isLoading || !data) {
return <Spinner />
}

return (
<div className={styles.project}>
<h1 className={styles.pageTitle}>{data.metadata.name}</h1>
<h1 className={styles.pageTitle}>{taskData.metadata.name}</h1>
<div className={styles.projectInfo}>
<div className={styles.projectInfoItem}>
<label>Big Query Table</label>
<div>{data.metadata.bqTable}</div>
<div>{taskData.metadata.bqTable}</div>
</div>
<div className={styles.projectInfoItem}>
<label>Query</label>
<code>{data.metadata.bqQuery}</code>
<code>{taskData.metadata.bqQuery}</code>
</div>
<div className={styles.projectInfoItem}>
<label>Labels</label>
<div>{data.metadata.labels.join(', ')}</div>
<div>{taskData.metadata.labels.join(', ')}</div>
</div>
</div>

<h2 className={styles.tasksTitle}>Tasks</h2>
{(data?.entries as LabellingTask[]).map((task, index) => (
{data.map((task, index) => (
<Task
projectId={projectId}
task={task}
open={activeTaskId ? task.id === activeTaskId : index === 0}
key={task.id}
onClick={() => setActiveTaskId(task.id)}
onFinishTask={setNextTask}
/>
))}
Expand Down
4 changes: 4 additions & 0 deletions apps/image-labeler/src/features/project/Task.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
justify-content: space-between;
}

.task:not(.open) {
cursor: pointer;
}

.task:last-of-type {
border-bottom: var(--border);
}
Expand Down
39 changes: 26 additions & 13 deletions apps/image-labeler/src/features/project/Task.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ type TaskProps = {
projectId: string
task: LabellingTask
open: boolean
onFinishTask: () => void
onClick?: () => void
onFinishTask: (taskId: string) => void
}

export function Task({ projectId, task, open, onFinishTask }: TaskProps) {
export function Task({ projectId, task, open, onClick, onFinishTask }: TaskProps) {
const options: ChoiceOption[] = useMemo(
() =>
task.labels.map((label, index) => ({
Expand All @@ -36,24 +37,27 @@ export function Task({ projectId, task, open, onFinishTask }: TaskProps) {
fixedCacheKey: [projectId, task.id].join(),
})

const setFinishedTask = useCallback(() => {
onFinishTask(task.id)
}, [onFinishTask, task.id])

const handleSubmit = useCallback(() => {
if (activeOption) {
console.log('activeOption:', activeOption)
onFinishTask()
setFinishedTask()
setTask({ projectId, taskId: task.id, label: activeOption })
}
}, [activeOption, onFinishTask, projectId, setTask, task.id])
}, [activeOption, setFinishedTask, projectId, setTask, task.id])

useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
e.preventDefault()
// e.preventDefault()
if (e.key === 'Enter') {
handleSubmit()
return
}
if (e.key === 'Escape') {
setActiveOption(undefined)
onFinishTask()
setFinishedTask()
return
}
const option = options[parseInt(e.key) - 1]
Expand All @@ -65,26 +69,35 @@ export function Task({ projectId, task, open, onFinishTask }: TaskProps) {
window.addEventListener('keydown', handleKeyDown)
}
return () => {
window.removeEventListener('keydown', handleKeyDown)
if (open) {
window.removeEventListener('keydown', handleKeyDown)
}
}
}, [handleSubmit, onFinishTask, open, options])
}, [handleSubmit, setFinishedTask, open, options, task.id])

const setOption = (option: ChoiceOption) => {
setActiveOption(option.id)
}

return (
<div className={cx(styles.task, { [styles.open]: open })}>
<div
onClick={open || isLoading ? undefined : onClick}
className={cx(styles.task, { [styles.open]: open })}
>
<div className={styles.images}>
{task.thumbnails.map((thumbnail, index) => (
<img src={thumbnail} alt="thumbnail" key={index} />
))}
</div>
{open ? (
<div className={styles.labels}>
<Choice options={options} activeOption={activeOption} onSelect={setOption} />
<Choice
options={options}
activeOption={data?.label || activeOption}
onSelect={setOption}
/>
<div className={styles.buttons}>
<Button onClick={onFinishTask} type="secondary">
<Button onClick={setFinishedTask} type="secondary">
Skip (Esc)
</Button>
<Button
Expand All @@ -98,7 +111,7 @@ export function Task({ projectId, task, open, onFinishTask }: TaskProps) {
</div>
) : (
<div>
{isLoading ? <Spinner size="small" /> : <label>{activeOption || 'Unlabeled'}</label>}
{isLoading ? <Spinner size="small" /> : <label>{data?.label || 'Unlabeled'}</label>}
{error !== undefined && <p>{JSON.stringify(error)}</p>}
</div>
)}
Expand Down
14 changes: 6 additions & 8 deletions apps/image-labeler/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import { createFileRoute } from '@tanstack/react-router'
// Import Routes

import { Route as rootRoute } from './routes/__root'
import { Route as ProjectProjectIdImport } from './routes/project.$projectId'

// Create Virtual Routes

const IndexLazyImport = createFileRoute('/')()
const ProjectProjectIdLazyImport = createFileRoute('/project/$projectId')()

// Create/Update Routes

Expand All @@ -26,12 +26,10 @@ const IndexLazyRoute = IndexLazyImport.update({
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))

const ProjectProjectIdLazyRoute = ProjectProjectIdLazyImport.update({
const ProjectProjectIdRoute = ProjectProjectIdImport.update({
path: '/project/$projectId',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
import('./routes/project.$projectId.lazy').then((d) => d.Route),
)
} as any)

// Populate the FileRoutesByPath interface

Expand All @@ -48,7 +46,7 @@ declare module '@tanstack/react-router' {
id: '/project/$projectId'
path: '/project/$projectId'
fullPath: '/project/$projectId'
preLoaderRoute: typeof ProjectProjectIdLazyImport
preLoaderRoute: typeof ProjectProjectIdImport
parentRoute: typeof rootRoute
}
}
Expand All @@ -58,7 +56,7 @@ declare module '@tanstack/react-router' {

export const routeTree = rootRoute.addChildren({
IndexLazyRoute,
ProjectProjectIdLazyRoute,
ProjectProjectIdRoute,
})

/* prettier-ignore-end */
Expand All @@ -77,7 +75,7 @@ export const routeTree = rootRoute.addChildren({
"filePath": "index.lazy.tsx"
},
"/project/$projectId": {
"filePath": "project.$projectId.lazy.tsx"
"filePath": "project.$projectId.tsx"
}
}
}
Expand Down
6 changes: 0 additions & 6 deletions apps/image-labeler/src/routes/project.$projectId.lazy.tsx

This file was deleted.

16 changes: 16 additions & 0 deletions apps/image-labeler/src/routes/project.$projectId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createFileRoute } from '@tanstack/react-router'
import TasksComponent from '../features/project/Project'

type ProjectSearchState = {
activeTaskId?: string
}

export const Route = createFileRoute('/project/$projectId')({
component: TasksComponent,
validateSearch: (search: Record<string, unknown>): ProjectSearchState => {
// validate and parse the search params into a typed state
return {
activeTaskId: search.activeTaskId as string,
}
},
})

0 comments on commit adcaf2a

Please sign in to comment.