Files
kandev/apps/web/components/task/task-session-sidebar.tsx
T
2026-01-22 21:30:26 +00:00

172 lines
6.7 KiB
TypeScript

'use client';
import { useCallback, useMemo, useState } from 'react';
import type { TaskState, Workspace, Repository } from '@/lib/types/http';
import type { KanbanState } from '@/lib/state/slices';
import { TaskSwitcher } from './task-switcher';
import { Button } from '@kandev/ui/button';
import { IconPlus } from '@tabler/icons-react';
import { TaskCreateDialog } from '@/components/task-create-dialog';
import { useAppStore, useAppStoreApi } from '@/components/state-provider';
import { linkToSession } from '@/lib/links';
import { listTaskSessions } from '@/lib/api';
import { useTasks } from '@/hooks/use-tasks';
type TaskSessionSidebarProps = {
workspaceId: string | null;
boardId: string | null;
};
export function TaskSessionSidebar({ workspaceId, boardId }: TaskSessionSidebarProps) {
const activeTaskId = useAppStore((state) => state.tasks.activeTaskId);
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
const sessionsById = useAppStore((state) => state.taskSessions.items);
const { tasks } = useTasks(boardId);
const columns = useAppStore((state) => state.kanban.columns);
const workspaces = useAppStore((state) => state.workspaces.items);
const repositoriesByWorkspace = useAppStore((state) => state.repositories.itemsByWorkspaceId);
const setActiveTask = useAppStore((state) => state.setActiveTask);
const setActiveSession = useAppStore((state) => state.setActiveSession);
const setTaskSessionsForTask = useAppStore((state) => state.setTaskSessionsForTask);
const setTaskSessionsLoading = useAppStore((state) => state.setTaskSessionsLoading);
const selectedTaskId = useMemo(() => {
if (activeSessionId) {
return sessionsById[activeSessionId]?.task_id ?? activeTaskId;
}
return activeTaskId;
}, [activeSessionId, activeTaskId, sessionsById]);
const [taskDialogOpen, setTaskDialogOpen] = useState(false);
const store = useAppStoreApi();
const workspaceName = useMemo(() => {
if (!workspaceId) return 'Workspace';
return workspaces.find((workspace: Workspace) => workspace.id === workspaceId)?.name ?? 'Workspace';
}, [workspaceId, workspaces]);
const tasksWithRepositories = useMemo(() => {
const repositories = workspaceId ? repositoriesByWorkspace[workspaceId] ?? [] : [];
const repositoryPathsById = new Map(repositories.map((repo: Repository) => [repo.id, repo.local_path]));
return tasks.map((task: KanbanState['tasks'][number]) => ({
id: task.id,
title: task.title,
state: task.state as TaskState | undefined,
description: task.description,
columnId: task.columnId,
repositoryPath: task.repositoryId ? repositoryPathsById.get(task.repositoryId) : undefined,
}));
}, [repositoriesByWorkspace, tasks, workspaceId]);
const updateUrl = useCallback((sessionId: string) => {
if (typeof window === 'undefined') return;
window.history.replaceState({}, '', linkToSession(sessionId));
}, []);
const loadTaskSessionsForTask = useCallback(
async (taskId: string) => {
const state = store.getState();
if (state.taskSessionsByTask.loadedByTaskId[taskId]) {
return state.taskSessionsByTask.itemsByTaskId[taskId] ?? [];
}
if (state.taskSessionsByTask.loadingByTaskId[taskId]) {
return state.taskSessionsByTask.itemsByTaskId[taskId] ?? [];
}
setTaskSessionsLoading(taskId, true);
try {
const response = await listTaskSessions(taskId, { cache: 'no-store' });
setTaskSessionsForTask(taskId, response.sessions ?? []);
return response.sessions ?? [];
} catch (error) {
console.error('Failed to load task sessions:', error);
setTaskSessionsForTask(taskId, []);
return [];
} finally {
setTaskSessionsLoading(taskId, false);
}
},
[setTaskSessionsForTask, setTaskSessionsLoading, store]
);
const handleSelectTask = useCallback(
async (taskId: string) => {
const sessions = await loadTaskSessionsForTask(taskId);
const sessionId = sessions[0]?.id ?? null;
if (!sessionId) {
setActiveTask(taskId);
return;
}
setActiveSession(taskId, sessionId);
updateUrl(sessionId);
},
[loadTaskSessionsForTask, setActiveSession, setActiveTask, updateUrl]
);
return (
<>
<div className="h-full min-h-0 min-w-0 bg-card p-4 flex flex-col rounded-lg border border-border/70 border-r-0 mr-[5px]">
<div className="flex items-center justify-between">
<span className="text-sm font-medium truncate text-muted-foreground">{workspaceName || 'Workspace'}</span>
<Button
size="sm"
variant="outline"
className="h-6 gap-1 cursor-pointer"
onClick={() => setTaskDialogOpen(true)}
>
<IconPlus className="h-4 w-4" />
Task
</Button>
</div>
<div className="flex-1 min-h-0 overflow-y-auto space-y-4 pt-3">
<TaskSwitcher
tasks={tasksWithRepositories}
columns={columns.map((column: KanbanState['columns'][number]) => ({ id: column.id, title: column.title }))}
activeTaskId={activeTaskId}
selectedTaskId={selectedTaskId}
onSelectTask={(taskId) => {
handleSelectTask(taskId);
}}
/>
</div>
</div>
<TaskCreateDialog
open={taskDialogOpen}
onOpenChange={setTaskDialogOpen}
mode="task"
workspaceId={workspaceId}
boardId={boardId}
defaultColumnId={columns[0]?.id ?? null}
columns={columns.map((column: KanbanState['columns'][number]) => ({ id: column.id, title: column.title }))}
onSuccess={(task, _mode, meta) => {
store.setState((state) => {
if (state.kanban.boardId !== task.board_id) return state;
const nextTask = {
id: task.id,
columnId: task.column_id,
title: task.title,
description: task.description,
position: task.position ?? 0,
state: task.state,
repositoryId: task.repositories?.[0]?.repository_id ?? undefined,
};
return {
...state,
kanban: {
...state.kanban,
tasks: state.kanban.tasks.some((item: KanbanState['tasks'][number]) => item.id === task.id)
? state.kanban.tasks.map((item: KanbanState['tasks'][number]) => (item.id === task.id ? nextTask : item))
: [...state.kanban.tasks, nextTask],
},
};
});
setActiveTask(task.id);
if (meta?.taskSessionId) {
setActiveSession(task.id, meta.taskSessionId);
updateUrl(meta.taskSessionId);
}
}}
/>
</>
);
}