704 lines
22 KiB
TypeScript
704 lines
22 KiB
TypeScript
/* eslint-disable max-lines -- the mobile task switcher owns its complete data and action boundary */
|
|
"use client";
|
|
|
|
import { useCallback, useMemo, useState } from "react";
|
|
import { useAppStore, useAppStoreApi } from "@/components/state-provider";
|
|
import { replaceTaskUrl } from "@/lib/links";
|
|
import { fetchWorkflowSnapshot, listWorkflows } from "@/lib/api";
|
|
import { useWorkspaceSidebarTasks } from "@/hooks/domains/kanban/use-workspace-sidebar-tasks";
|
|
import {
|
|
useTaskActions,
|
|
useArchiveAndSwitchTask,
|
|
type TaskActionOptions,
|
|
} from "@/hooks/use-task-actions";
|
|
import { useTaskDetachDialog } from "@/hooks/use-detach-task";
|
|
import { useNestTaskByDrag } from "@/hooks/use-nest-task";
|
|
import { useTaskRemoval, useTaskRemovalSuccessNotifier } from "@/hooks/use-task-removal";
|
|
import { workspaceModeFromMetadata } from "@/lib/kanban/map-task";
|
|
import { type Repository, type Task } from "@/lib/types/http";
|
|
import type { KanbanState } from "@/lib/state/slices";
|
|
import { findTaskInSnapshots } from "@/lib/kanban/find-task";
|
|
import { repositorySlug } from "@/lib/repository-slug";
|
|
import { mapSnapshotToKanban, sortByUpdatedAtDesc } from "./session-task-switcher-sheet-helpers";
|
|
import { toSheetItem, type SheetItemCtx } from "./session-task-switcher-sheet-item";
|
|
import {
|
|
selectTaskFromSheet,
|
|
type TaskSheetSelectionController,
|
|
} from "./session-task-switcher-sheet-selection";
|
|
import { taskPendingSelectionSnapshot } from "../task-select-helpers";
|
|
import { useTranslation } from "react-i18next";
|
|
import { useArchivedTaskState } from "../task-archived-context";
|
|
import { buildArchivedSidebarItem } from "../task-session-sidebar-archived-item";
|
|
import type { SidebarItemContext } from "../task-session-sidebar-item";
|
|
import type { TaskSwitcherItem } from "../task-switcher";
|
|
import {
|
|
classifyWorkspaceContextReadError,
|
|
isCurrentWorkspaceContext,
|
|
retryAfterMilliseconds,
|
|
} from "@/lib/state/workspace-context";
|
|
import { generateUUID } from "@/lib/utils";
|
|
|
|
function findSheetTask(
|
|
state: ReturnType<ReturnType<typeof useAppStoreApi>["getState"]>,
|
|
taskId: string,
|
|
) {
|
|
const activeTask = findTaskInSnapshots(taskId, state.kanbanMulti.snapshots, state.kanban.tasks);
|
|
if (activeTask) return activeTask;
|
|
for (const tasks of Object.values(state.sidebarArchivedTasks?.itemsByWorkspaceId ?? {})) {
|
|
const archivedTask = tasks.find((task) => task.id === taskId);
|
|
if (archivedTask) return archivedTask;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// eslint-disable-next-line max-lines-per-function -- task projection and recovery status share one memoized view model
|
|
export function useSheetData(workspaceId: string | null) {
|
|
const activeTaskId = useAppStore((state) => state.tasks.activeTaskId);
|
|
const {
|
|
allTasks,
|
|
pendingArchiveTaskIds,
|
|
allSteps,
|
|
stepsByWorkflowId,
|
|
wipQueueByTaskId,
|
|
workflows,
|
|
isLoading: tasksLoading,
|
|
archivedError,
|
|
retryArchivedTasks,
|
|
workspaceContextError,
|
|
workspaceContextPending,
|
|
workspaceContextAccessDenied,
|
|
retryWorkspaceContext,
|
|
} = useWorkspaceSidebarTasks(workspaceId);
|
|
const steps = useAppStore((state) => state.kanban.steps);
|
|
const workspaces = useAppStore((state) => state.workspaces.items);
|
|
const repositoriesByWorkspace = useAppStore((state) => state.repositories.itemsByWorkspaceId);
|
|
const automaticColorSettings = useAppStore(
|
|
(state) => state.userSettings.sidebarTaskColorAutomation,
|
|
);
|
|
const acknowledgedAgentErrors = useAppStore((state) => state.acknowledgedAgentErrors);
|
|
const dismissedAgentErrors = useAppStore((state) => state.dismissedAgentErrors);
|
|
const archivedState = useArchivedTaskState();
|
|
|
|
const selectedTaskId = activeTaskId;
|
|
|
|
const tasksWithRepositories = useMemo(() => {
|
|
const repositories = workspaceId ? (repositoriesByWorkspace[workspaceId] ?? []) : [];
|
|
const repositoriesById = new Map(
|
|
Object.values(repositoriesByWorkspace)
|
|
.flat()
|
|
.map((repo: Repository) => [repo.id, repo]),
|
|
);
|
|
const ctx: SheetItemCtx = {
|
|
repositoryPathsById: new Map(
|
|
repositories.map((repo: Repository) => [repo.id, repositorySlug(repo)]),
|
|
),
|
|
workflowNameById: new Map(workflows.map((w) => [w.id, w.name])),
|
|
stepTitleById: new Map(allSteps.map((s) => [s.id, s.title])),
|
|
acknowledgedAgentErrors,
|
|
dismissedAgentErrors,
|
|
wipQueueByTaskId,
|
|
workspaceId: workspaceId ?? undefined,
|
|
repositoriesById,
|
|
stepColorById: new Map(allSteps.map((step) => [step.id, step.color])),
|
|
automaticColorSettings,
|
|
pendingArchiveTaskIds,
|
|
};
|
|
const items: TaskSwitcherItem[] = workspaceContextAccessDenied
|
|
? []
|
|
: allTasks.map((task) => toSheetItem(task, ctx));
|
|
if (
|
|
!workspaceContextAccessDenied &&
|
|
archivedState.isArchived &&
|
|
archivedState.archivedTaskId &&
|
|
!items.some((task) => task.id === archivedState.archivedTaskId)
|
|
) {
|
|
const archivedContext: SidebarItemContext = {
|
|
repositorySlugById: ctx.repositoryPathsById,
|
|
titleById: new Map(allTasks.map((task) => [task.id, task.title])),
|
|
workflowNameById: ctx.workflowNameById,
|
|
stepTitleById: ctx.stepTitleById,
|
|
wipQueueByTaskId: ctx.wipQueueByTaskId,
|
|
acknowledgedAgentErrors: ctx.acknowledgedAgentErrors,
|
|
dismissedAgentErrors: ctx.dismissedAgentErrors,
|
|
workspaceId: ctx.workspaceId,
|
|
repositoriesById: ctx.repositoriesById,
|
|
stepColorById: ctx.stepColorById,
|
|
automaticColorSettings: ctx.automaticColorSettings,
|
|
pendingArchiveTaskIds: ctx.pendingArchiveTaskIds,
|
|
};
|
|
items.unshift(buildArchivedSidebarItem(archivedState, archivedContext));
|
|
}
|
|
return items;
|
|
}, [
|
|
repositoriesByWorkspace,
|
|
allTasks,
|
|
allSteps,
|
|
workflows,
|
|
workspaceId,
|
|
acknowledgedAgentErrors,
|
|
dismissedAgentErrors,
|
|
wipQueueByTaskId,
|
|
automaticColorSettings,
|
|
pendingArchiveTaskIds,
|
|
archivedState,
|
|
workspaceContextAccessDenied,
|
|
]);
|
|
|
|
const dialogSteps = useMemo(
|
|
() =>
|
|
steps.map((step: KanbanState["steps"][number]) => ({
|
|
id: step.id,
|
|
title: step.title,
|
|
color: step.color,
|
|
events: step.events,
|
|
})),
|
|
[steps],
|
|
);
|
|
|
|
return {
|
|
activeTaskId,
|
|
selectedTaskId,
|
|
workspaces,
|
|
workflows,
|
|
stepsByWorkflowId,
|
|
// Skeleton while the first snapshot fetch is in flight — otherwise shows "No tasks yet." even when tasks exist.
|
|
tasksLoading,
|
|
archivedError,
|
|
retryArchivedTasks,
|
|
workspaceContextError,
|
|
workspaceContextPending,
|
|
workspaceContextAccessDenied,
|
|
retryWorkspaceContext,
|
|
tasksWithRepositories,
|
|
dialogSteps,
|
|
};
|
|
}
|
|
|
|
type SheetNavOptions = {
|
|
navigate: (taskId: string) => void;
|
|
workspaceId: string | null;
|
|
store: ReturnType<typeof useAppStoreApi>;
|
|
loadTaskSessionsForTask: (
|
|
taskId: string,
|
|
options?: { force?: boolean },
|
|
) => Promise<Array<{ id: string; updated_at?: string | null }>>;
|
|
setActiveSession: (taskId: string, sessionId: string) => void;
|
|
setActiveTask: (taskId: string) => void;
|
|
onOpenChange: (open: boolean) => void;
|
|
};
|
|
|
|
export type WorkspaceTaskSession = {
|
|
id: string;
|
|
updated_at?: string | null;
|
|
};
|
|
|
|
export async function loadWorkspaceTaskSessions(
|
|
loader: SheetNavOptions["loadTaskSessionsForTask"],
|
|
taskId: string,
|
|
): Promise<WorkspaceTaskSession[]> {
|
|
try {
|
|
return await loader(taskId);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line max-lines-per-function -- workspace switching keeps its generation guard around every async phase
|
|
async function switchWorkspace(newWorkspaceId: string, opts: SheetNavOptions) {
|
|
const { store, loadTaskSessionsForTask, setActiveSession, setActiveTask, onOpenChange } = opts;
|
|
store.getState().setActiveWorkspace(newWorkspaceId);
|
|
const generation = store.getState().workspaceContextGeneration;
|
|
const requestId = generateUUID();
|
|
store
|
|
.getState()
|
|
.setWorkspaceContextRead(
|
|
"workflows",
|
|
newWorkspaceId,
|
|
generation,
|
|
"pending",
|
|
undefined,
|
|
requestId,
|
|
);
|
|
store.setState((state) => ({ ...state, kanban: { ...state.kanban, isLoading: true } }));
|
|
try {
|
|
const workflowsResponse = await listWorkflows(newWorkspaceId, {
|
|
cache: "no-store",
|
|
includeHidden: true,
|
|
});
|
|
if (!isCurrentWorkspaceContext(store.getState(), newWorkspaceId, generation)) return;
|
|
const newWorkspaceWorkflows = workflowsResponse.workflows ?? [];
|
|
const firstWorkflow = newWorkspaceWorkflows.find((w) => !w.hidden);
|
|
if (!firstWorkflow) {
|
|
store
|
|
.getState()
|
|
.setWorkspaceContextRead(
|
|
"workflows",
|
|
newWorkspaceId,
|
|
generation,
|
|
"success",
|
|
undefined,
|
|
requestId,
|
|
);
|
|
store.setState((state) => ({ ...state, kanban: { ...state.kanban, isLoading: false } }));
|
|
return;
|
|
}
|
|
const snapshot = await fetchWorkflowSnapshot(firstWorkflow.id);
|
|
if (!isCurrentWorkspaceContext(store.getState(), newWorkspaceId, generation)) return;
|
|
store
|
|
.getState()
|
|
.setWorkspaceContextRead(
|
|
"workflows",
|
|
newWorkspaceId,
|
|
generation,
|
|
"success",
|
|
undefined,
|
|
requestId,
|
|
);
|
|
store.setState((state) => ({
|
|
...state,
|
|
workflows: {
|
|
...state.workflows,
|
|
items: [
|
|
...state.workflows.items.filter(
|
|
(w: { workspaceId: string }) => w.workspaceId !== newWorkspaceId,
|
|
),
|
|
...newWorkspaceWorkflows.map((w) => ({
|
|
id: w.id,
|
|
workspaceId: w.workspace_id,
|
|
name: w.name,
|
|
hidden: w.hidden,
|
|
})),
|
|
],
|
|
activeId: firstWorkflow.id,
|
|
},
|
|
kanban: mapSnapshotToKanban(snapshot, firstWorkflow.id),
|
|
}));
|
|
const mostRecentTask = sortByUpdatedAtDesc(snapshot.tasks)[0];
|
|
if (mostRecentTask) {
|
|
const sessions = await loadWorkspaceTaskSessions(loadTaskSessionsForTask, mostRecentTask.id);
|
|
if (!isCurrentWorkspaceContext(store.getState(), newWorkspaceId, generation)) return;
|
|
const mostRecentSession = sortByUpdatedAtDesc(sessions)[0];
|
|
if (mostRecentSession) {
|
|
setActiveSession(mostRecentTask.id, mostRecentSession.id);
|
|
} else {
|
|
setActiveTask(mostRecentTask.id);
|
|
}
|
|
opts.navigate(mostRecentTask.id);
|
|
}
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
console.error("Failed to switch workspace:", error);
|
|
const state = store.getState();
|
|
if (!isCurrentWorkspaceContext(state, newWorkspaceId, generation)) return;
|
|
state.setWorkspaceContextRead(
|
|
"workflows",
|
|
newWorkspaceId,
|
|
generation,
|
|
classifyWorkspaceContextReadError(error),
|
|
retryAfterMilliseconds(error),
|
|
requestId,
|
|
);
|
|
store.setState((current) => ({
|
|
...current,
|
|
kanban: { ...current.kanban, isLoading: false },
|
|
}));
|
|
}
|
|
}
|
|
|
|
function mapTaskRepositories(
|
|
repositories: Task["repositories"],
|
|
): KanbanState["tasks"][number]["repositories"] {
|
|
return repositories?.map((r) => ({
|
|
id: r.id,
|
|
repository_id: r.repository_id,
|
|
base_branch: r.base_branch,
|
|
checkout_branch: r.checkout_branch,
|
|
branch_policy_id: r.branch_policy_id,
|
|
branch_policy_name: r.branch_policy_name,
|
|
branch_policy_base_branch: r.branch_policy_base_branch,
|
|
branch_policy_branch_template: r.branch_policy_branch_template,
|
|
branch_policy_pull_request_target: r.branch_policy_pull_request_target,
|
|
position: r.position,
|
|
}));
|
|
}
|
|
|
|
function mergeSessionFields(
|
|
task: Task,
|
|
existing: KanbanState["tasks"][number] | undefined,
|
|
taskSessionId: string | null,
|
|
) {
|
|
return {
|
|
primarySessionId: resolvePrimarySessionId(task, existing, taskSessionId),
|
|
primarySessionState: resolvePrimarySessionState(task, existing),
|
|
primarySessionPendingAction: resolvePrimarySessionPendingAction(task, existing),
|
|
taskPendingAction: resolveTaskPendingAction(task, existing),
|
|
sessionCount: resolveSessionCount(task, existing, taskSessionId),
|
|
reviewStatus: resolveReviewStatus(task, existing),
|
|
};
|
|
}
|
|
|
|
function resolveTaskPendingAction(task: Task, existing: KanbanState["tasks"][number] | undefined) {
|
|
if ("task_pending_action" in task) return task.task_pending_action ?? undefined;
|
|
return existing?.taskPendingAction ?? undefined;
|
|
}
|
|
|
|
function resolvePrimarySessionId(
|
|
task: Task,
|
|
existing: KanbanState["tasks"][number] | undefined,
|
|
taskSessionId: string | null,
|
|
) {
|
|
return taskSessionId ?? task.primary_session_id ?? existing?.primarySessionId ?? undefined;
|
|
}
|
|
|
|
function resolvePrimarySessionState(
|
|
task: Task,
|
|
existing: KanbanState["tasks"][number] | undefined,
|
|
) {
|
|
return task.primary_session_state ?? existing?.primarySessionState ?? undefined;
|
|
}
|
|
|
|
function resolvePrimarySessionPendingAction(
|
|
task: Task,
|
|
existing: KanbanState["tasks"][number] | undefined,
|
|
) {
|
|
if ("primary_session_pending_action" in task) {
|
|
return task.primary_session_pending_action ?? undefined;
|
|
}
|
|
return existing?.primarySessionPendingAction ?? undefined;
|
|
}
|
|
|
|
function resolveSessionCount(
|
|
task: Task,
|
|
existing: KanbanState["tasks"][number] | undefined,
|
|
taskSessionId: string | null,
|
|
) {
|
|
return task.session_count ?? existing?.sessionCount ?? (taskSessionId ? 1 : undefined);
|
|
}
|
|
|
|
function resolveReviewStatus(task: Task, existing: KanbanState["tasks"][number] | undefined) {
|
|
return task.review_status ?? existing?.reviewStatus ?? undefined;
|
|
}
|
|
|
|
/**
|
|
* Build the kanban-store representation of a task for an upsert. Session-
|
|
* derived fields (primarySessionId, sessionCount, etc.) fall through new
|
|
* DTO → existing entry → meta.taskSessionId — that way an "edit" call doesn't
|
|
* wipe sessions the existing entry carried, and "create with session" still
|
|
* sets the primary correctly.
|
|
*/
|
|
function buildKanbanTaskUpsert(
|
|
task: Task,
|
|
existing: KanbanState["tasks"][number] | undefined,
|
|
meta: { taskSessionId?: string | null } | undefined,
|
|
): KanbanState["tasks"][number] {
|
|
const taskSessionId = meta?.taskSessionId ?? null;
|
|
return {
|
|
id: task.id,
|
|
workspaceId: task.workspace_id,
|
|
parentTaskId: task.parent_id ?? undefined,
|
|
workspaceMode: workspaceModeFromMetadata(task.metadata),
|
|
workflowId: task.workflow_id,
|
|
workflowStepId: task.workflow_step_id,
|
|
title: task.title,
|
|
description: task.description,
|
|
position: task.position ?? 0,
|
|
state: task.state,
|
|
priority: task.priority,
|
|
origin: task.origin,
|
|
repositoryId: task.repositories?.[0]?.repository_id ?? undefined,
|
|
repositories: mapTaskRepositories(task.repositories),
|
|
updatedAt: task.updated_at,
|
|
...mergeSessionFields(task, existing, taskSessionId),
|
|
primaryExecutorId: task.primary_executor_id ?? undefined,
|
|
primaryExecutorProfileId: task.primary_executor_profile_id ?? undefined,
|
|
primaryExecutorType: task.primary_executor_type ?? undefined,
|
|
primaryExecutorName: task.primary_executor_name ?? undefined,
|
|
isRemoteExecutor: task.is_remote_executor ?? false,
|
|
};
|
|
}
|
|
|
|
function useWorkspaceAndTaskCreatedActions(opts: SheetNavOptions) {
|
|
const {
|
|
navigate,
|
|
workspaceId,
|
|
store,
|
|
loadTaskSessionsForTask,
|
|
setActiveSession,
|
|
setActiveTask,
|
|
onOpenChange,
|
|
} = opts;
|
|
|
|
const handleWorkspaceChange = useCallback(
|
|
async (newWorkspaceId: string) => {
|
|
if (newWorkspaceId === workspaceId) return;
|
|
await switchWorkspace(newWorkspaceId, {
|
|
navigate,
|
|
workspaceId,
|
|
store,
|
|
loadTaskSessionsForTask,
|
|
setActiveSession,
|
|
setActiveTask,
|
|
onOpenChange,
|
|
});
|
|
},
|
|
// Spread the individual fields rather than the `opts` object so callers
|
|
// re-passing a fresh literal each render don't defeat memoization.
|
|
[
|
|
workspaceId,
|
|
store,
|
|
loadTaskSessionsForTask,
|
|
setActiveSession,
|
|
setActiveTask,
|
|
onOpenChange,
|
|
navigate,
|
|
],
|
|
);
|
|
|
|
const handleTaskCreated = useCallback(
|
|
(
|
|
task: Task,
|
|
_mode: "create" | "edit",
|
|
meta?: { taskSessionId?: string | null; autoFocus?: boolean },
|
|
) => {
|
|
store.setState((state) => {
|
|
if (state.kanban.workflowId !== task.workflow_id) return state;
|
|
const existing = state.kanban.tasks.find(
|
|
(item: KanbanState["tasks"][number]) => item.id === task.id,
|
|
);
|
|
const nextTask = buildKanbanTaskUpsert(task, existing, meta);
|
|
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],
|
|
},
|
|
};
|
|
});
|
|
if (meta?.autoFocus === false) {
|
|
onOpenChange(false);
|
|
return;
|
|
}
|
|
setActiveTask(task.id);
|
|
if (meta?.taskSessionId) {
|
|
setActiveSession(task.id, meta.taskSessionId);
|
|
}
|
|
navigate(task.id);
|
|
onOpenChange(false);
|
|
},
|
|
[store, setActiveTask, setActiveSession, onOpenChange, navigate],
|
|
);
|
|
|
|
return { handleWorkspaceChange, handleTaskCreated };
|
|
}
|
|
|
|
function useSheetDeleteActions(
|
|
store: ReturnType<typeof useAppStoreApi>,
|
|
runTaskRemoval: ReturnType<typeof useTaskRemoval>["runTaskRemoval"],
|
|
) {
|
|
const { t } = useTranslation();
|
|
const { deleteTaskById } = useTaskActions();
|
|
const [deletingTask, setDeletingTask] = useState<{
|
|
id: string;
|
|
title: string;
|
|
executorType?: string | null;
|
|
} | null>(null);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
const handleDeleteTask = useCallback(
|
|
(taskId: string) => {
|
|
const state = store.getState();
|
|
const task = findSheetTask(state, taskId);
|
|
setDeletingTask({
|
|
id: taskId,
|
|
title: task?.title ?? t("task:thisTask"),
|
|
executorType: task?.primaryExecutorType,
|
|
});
|
|
},
|
|
[store, t],
|
|
);
|
|
|
|
const handleDeleteConfirm = useCallback(
|
|
async (opts?: TaskActionOptions) => {
|
|
if (!deletingTask || isDeleting) return;
|
|
const taskId = deletingTask.id;
|
|
setIsDeleting(true);
|
|
try {
|
|
await runTaskRemoval(
|
|
"delete",
|
|
{ taskId, mutate: () => deleteTaskById(taskId, opts) },
|
|
{ cascade: opts?.cascade },
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to delete task:", error);
|
|
} finally {
|
|
setIsDeleting(false);
|
|
setDeletingTask(null);
|
|
}
|
|
},
|
|
[deletingTask, isDeleting, deleteTaskById, runTaskRemoval],
|
|
);
|
|
|
|
const deletingTaskId = isDeleting ? (deletingTask?.id ?? null) : null;
|
|
|
|
return {
|
|
deletingTaskId,
|
|
deletingTask,
|
|
setDeletingTask,
|
|
isDeleting,
|
|
handleDeleteTask,
|
|
handleDeleteConfirm,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Re-parent via drag: runs the same composite nest operation the context menu
|
|
* uses, resolving the workflow from the snapshot keys.
|
|
*/
|
|
function useSheetNestTask() {
|
|
return useNestTaskByDrag();
|
|
}
|
|
|
|
export function useSheetArchiveActions(
|
|
store: ReturnType<typeof useAppStoreApi>,
|
|
archiveAndSwitch: ReturnType<typeof useArchiveAndSwitchTask>,
|
|
) {
|
|
const { t } = useTranslation();
|
|
const [archivingTask, setArchivingTask] = useState<{
|
|
id: string;
|
|
title: string;
|
|
executorType?: string | null;
|
|
} | null>(null);
|
|
const [isArchiving, setIsArchiving] = useState(false);
|
|
const [archivingTaskId, setArchivingTaskId] = useState<string | null>(null);
|
|
|
|
const runArchive = useCallback(
|
|
async (taskId: string, opts?: TaskActionOptions) => {
|
|
setIsArchiving(true);
|
|
setArchivingTaskId(taskId);
|
|
try {
|
|
await archiveAndSwitch(taskId, opts);
|
|
} catch (error) {
|
|
console.error("Failed to archive task:", error);
|
|
} finally {
|
|
setIsArchiving(false);
|
|
setArchivingTaskId((current) => (current === taskId ? null : current));
|
|
setArchivingTask((current) => (current?.id === taskId ? null : current));
|
|
}
|
|
},
|
|
[archiveAndSwitch],
|
|
);
|
|
|
|
const handleArchiveTask = useCallback(
|
|
(taskId: string, opts?: TaskActionOptions) => {
|
|
if (opts) {
|
|
void runArchive(taskId, opts);
|
|
return;
|
|
}
|
|
const task = findSheetTask(store.getState(), taskId);
|
|
setArchivingTask({
|
|
id: taskId,
|
|
title: task?.title ?? t("task:thisTask"),
|
|
executorType: task?.primaryExecutorType,
|
|
});
|
|
},
|
|
[runArchive, store, t],
|
|
);
|
|
|
|
const handleArchiveConfirm = useCallback(
|
|
async (opts?: TaskActionOptions) => {
|
|
if (!archivingTask) return;
|
|
await runArchive(archivingTask.id, opts);
|
|
},
|
|
[archivingTask, runArchive],
|
|
);
|
|
|
|
return {
|
|
handleArchiveTask,
|
|
archivingTask,
|
|
archivingTaskId,
|
|
setArchivingTask,
|
|
isArchiving,
|
|
handleArchiveConfirm,
|
|
};
|
|
}
|
|
|
|
export function useSheetActions(
|
|
workspaceId: string | null,
|
|
onOpenChange: (open: boolean) => void,
|
|
selection: TaskSheetSelectionController,
|
|
navigate: (taskId: string) => void = replaceTaskUrl,
|
|
) {
|
|
const setActiveTask = useAppStore((state) => state.setActiveTask);
|
|
const setActiveSession = useAppStore((state) => state.setActiveSession);
|
|
const store = useAppStoreApi();
|
|
const archiveAndSwitch = useArchiveAndSwitchTask();
|
|
const archiveActions = useSheetArchiveActions(store, archiveAndSwitch);
|
|
const notifySuccess = useTaskRemovalSuccessNotifier();
|
|
const { runTaskRemoval, loadTaskSessionsForTask } = useTaskRemoval({
|
|
store,
|
|
notifySuccess,
|
|
});
|
|
const deleteActions = useSheetDeleteActions(store, runTaskRemoval);
|
|
const detachActions = useTaskDetachDialog(store);
|
|
const handleNestTask = useSheetNestTask();
|
|
const handleSelectTask = useCallback(
|
|
(taskId: string) => {
|
|
const state = store.getState();
|
|
selectTaskFromSheet({
|
|
taskId,
|
|
selectionController: selection,
|
|
task: findSheetTask(state, taskId),
|
|
state: {
|
|
lastSessionByTaskId: state.tasks.lastSessionByTaskId,
|
|
environmentIdBySessionId: state.environmentIdBySessionId,
|
|
taskSessionsById: state.taskSessions.items,
|
|
},
|
|
setActiveTask,
|
|
setActiveSession,
|
|
loadTaskSessionsForTask,
|
|
getTaskPendingSnapshot: (selectedTaskId) => {
|
|
const selectedTask = findSheetTask(store.getState(), selectedTaskId);
|
|
return selectedTask ? taskPendingSelectionSnapshot(selectedTask) : undefined;
|
|
},
|
|
navigate,
|
|
onOpenChange,
|
|
});
|
|
},
|
|
[
|
|
loadTaskSessionsForTask,
|
|
setActiveSession,
|
|
setActiveTask,
|
|
store,
|
|
onOpenChange,
|
|
selection,
|
|
navigate,
|
|
],
|
|
);
|
|
|
|
const { handleWorkspaceChange, handleTaskCreated } = useWorkspaceAndTaskCreatedActions({
|
|
navigate,
|
|
workspaceId,
|
|
store,
|
|
loadTaskSessionsForTask,
|
|
setActiveSession,
|
|
setActiveTask,
|
|
onOpenChange,
|
|
});
|
|
|
|
return {
|
|
handleSelectTask,
|
|
...archiveActions,
|
|
handleWorkspaceChange,
|
|
handleTaskCreated,
|
|
handleNestTask,
|
|
...deleteActions,
|
|
...detachActions,
|
|
};
|
|
}
|