fix(ui): persist task creation draft when modal closes (#455)
This commit is contained in:
@@ -19,7 +19,12 @@ import {
|
||||
useExecutorHint,
|
||||
useExecutorProfileOptions,
|
||||
} from "@/components/task-create-dialog-options";
|
||||
import { setLocalStorage } from "@/lib/local-storage";
|
||||
import {
|
||||
setLocalStorage,
|
||||
getTaskCreateDraft,
|
||||
setTaskCreateDraft,
|
||||
removeTaskCreateDraft,
|
||||
} from "@/lib/local-storage";
|
||||
import { STORAGE_KEYS } from "@/lib/settings/constants";
|
||||
import type {
|
||||
StepType,
|
||||
@@ -62,55 +67,154 @@ type FormResetters = {
|
||||
setGitHubPrHeadBranch: (v: string | null) => void;
|
||||
};
|
||||
|
||||
function useFormResetEffects(
|
||||
open: boolean,
|
||||
workspaceId: string | null,
|
||||
workflowId: string | null,
|
||||
initialValues: TaskCreateDialogInitialValues | undefined,
|
||||
resetters: FormResetters,
|
||||
) {
|
||||
type FormResetEffectsArgs = {
|
||||
open: boolean;
|
||||
workspaceId: string | null;
|
||||
workflowId: string | null;
|
||||
initialValues: TaskCreateDialogInitialValues | undefined;
|
||||
resetters: FormResetters;
|
||||
setDraftDescription: (v: string) => void;
|
||||
setCurrentDefaults: (v: { name: string; description: string }) => void;
|
||||
setOpenCycle: React.Dispatch<React.SetStateAction<number>>;
|
||||
prevOpenRef: React.RefObject<boolean>;
|
||||
};
|
||||
|
||||
function useFormResetEffects({
|
||||
open,
|
||||
workspaceId,
|
||||
workflowId,
|
||||
initialValues,
|
||||
resetters,
|
||||
setDraftDescription,
|
||||
setCurrentDefaults,
|
||||
setOpenCycle,
|
||||
prevOpenRef,
|
||||
}: FormResetEffectsArgs) {
|
||||
// Restore draft or initialValues when dialog opens
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const name = initialValues?.title || "";
|
||||
void Promise.resolve().then(() => {
|
||||
resetters.setTaskName(name);
|
||||
resetters.setHasTitle(name.trim().length > 0);
|
||||
resetters.setHasDescription(Boolean(initialValues?.description?.trim()));
|
||||
resetters.setRepositoryId(initialValues?.repositoryId ?? "");
|
||||
resetters.setBranch(initialValues?.branch ?? "");
|
||||
resetters.setAgentProfileId("");
|
||||
resetters.setExecutorId("");
|
||||
resetters.setExecutorProfileId("");
|
||||
resetters.setSelectedWorkflowId(workflowId);
|
||||
resetters.setFetchedSteps(null);
|
||||
});
|
||||
// Only run on rising edge (dialog opening)
|
||||
const wasOpen = prevOpenRef.current;
|
||||
(prevOpenRef as React.MutableRefObject<boolean>).current = open;
|
||||
|
||||
if (!open || wasOpen) return;
|
||||
|
||||
// Increment cycle to force TaskFormInputs remount
|
||||
setOpenCycle((c) => c + 1);
|
||||
|
||||
const defaults = resolveFormDefaults(initialValues, workspaceId);
|
||||
setCurrentDefaults(defaults);
|
||||
resetTaskForm(resetters, defaults.name, defaults.description, workflowId, initialValues);
|
||||
setDraftDescription(defaults.description);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
initialValues?.branch,
|
||||
initialValues?.description,
|
||||
initialValues?.repositoryId,
|
||||
initialValues?.title,
|
||||
open,
|
||||
workflowId,
|
||||
]);
|
||||
}, [open, workflowId, workspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
void Promise.resolve().then(() => {
|
||||
resetters.setDiscoveredRepositories([]);
|
||||
resetters.setDiscoveredRepoPath("");
|
||||
resetters.setSelectedLocalRepo(null);
|
||||
resetters.setLocalBranches([]);
|
||||
resetters.setDiscoverReposLoaded(false);
|
||||
resetters.setUseGitHubUrl(false);
|
||||
resetters.setGitHubUrl("");
|
||||
resetters.setGitHubBranches([]);
|
||||
resetters.setGitHubUrlError(null);
|
||||
resetters.setGitHubPrHeadBranch(null);
|
||||
});
|
||||
resetDiscoveryState(resetters);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, workspaceId]);
|
||||
}
|
||||
|
||||
/** Checks if initialValues has any user-provided content */
|
||||
function hasUserContent(initialValues?: TaskCreateDialogInitialValues): boolean {
|
||||
const title = initialValues?.title ?? "";
|
||||
const description = initialValues?.description ?? "";
|
||||
return title.trim().length > 0 || description.trim().length > 0;
|
||||
}
|
||||
|
||||
/** Resolves form defaults from draft (for create) or initialValues (for edit) */
|
||||
function resolveFormDefaults(
|
||||
initialValues: TaskCreateDialogInitialValues | undefined,
|
||||
workspaceId: string | null,
|
||||
) {
|
||||
// In edit mode (has content), use initialValues; in create mode, try draft
|
||||
const draft =
|
||||
!hasUserContent(initialValues) && workspaceId ? getTaskCreateDraft(workspaceId) : null;
|
||||
const initTitle = initialValues?.title ?? "";
|
||||
const initDesc = initialValues?.description ?? "";
|
||||
return {
|
||||
name: draft?.title ?? initTitle,
|
||||
description: draft?.description ?? initDesc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Resets task form fields to specified values */
|
||||
function resetTaskForm(
|
||||
resetters: FormResetters,
|
||||
name: string,
|
||||
description: string,
|
||||
workflowId: string | null,
|
||||
initialValues?: TaskCreateDialogInitialValues,
|
||||
) {
|
||||
resetters.setTaskName(name);
|
||||
resetters.setHasTitle(name.trim().length > 0);
|
||||
resetters.setHasDescription(description.trim().length > 0);
|
||||
resetters.setRepositoryId(initialValues?.repositoryId ?? "");
|
||||
resetters.setBranch(initialValues?.branch ?? "");
|
||||
resetters.setAgentProfileId("");
|
||||
resetters.setExecutorId("");
|
||||
resetters.setExecutorProfileId("");
|
||||
resetters.setSelectedWorkflowId(workflowId);
|
||||
resetters.setFetchedSteps(null);
|
||||
}
|
||||
|
||||
/** Resets repository discovery state */
|
||||
function resetDiscoveryState(resetters: FormResetters) {
|
||||
resetters.setDiscoveredRepositories([]);
|
||||
resetters.setDiscoveredRepoPath("");
|
||||
resetters.setSelectedLocalRepo(null);
|
||||
resetters.setLocalBranches([]);
|
||||
resetters.setDiscoverReposLoaded(false);
|
||||
resetters.setUseGitHubUrl(false);
|
||||
resetters.setGitHubUrl("");
|
||||
resetters.setGitHubBranches([]);
|
||||
resetters.setGitHubUrlError(null);
|
||||
resetters.setGitHubPrHeadBranch(null);
|
||||
}
|
||||
|
||||
/** Hook to manage draft persistence for task creation dialog */
|
||||
function useDraftPersistence(
|
||||
open: boolean,
|
||||
workspaceId: string | null,
|
||||
initialValues: TaskCreateDialogInitialValues | undefined,
|
||||
taskName: string,
|
||||
descriptionInputRef: React.RefObject<{ getValue: () => string } | null>,
|
||||
) {
|
||||
const wasOpenRef = useRef(false);
|
||||
const skipDraftSaveRef = useRef(false);
|
||||
|
||||
// Save draft when dialog closes (only in create mode without initialValues)
|
||||
useEffect(() => {
|
||||
const wasOpen = wasOpenRef.current;
|
||||
wasOpenRef.current = open;
|
||||
|
||||
if (!wasOpen || open || !workspaceId) return;
|
||||
// Skip if clearDraft was called (successful submission)
|
||||
if (skipDraftSaveRef.current) {
|
||||
skipDraftSaveRef.current = false;
|
||||
return;
|
||||
}
|
||||
const hasInitialValues = Boolean(
|
||||
initialValues?.title?.trim() || initialValues?.description?.trim(),
|
||||
);
|
||||
// Only save draft in create mode
|
||||
if (!hasInitialValues) {
|
||||
const currentDescription = descriptionInputRef.current?.getValue() ?? "";
|
||||
setTaskCreateDraft(workspaceId, { title: taskName, description: currentDescription });
|
||||
}
|
||||
}, [open, workspaceId, initialValues, taskName, descriptionInputRef]);
|
||||
|
||||
// Clear draft (call on successful submission before closing dialog)
|
||||
const clearDraft = useCallback(() => {
|
||||
if (workspaceId) {
|
||||
removeTaskCreateDraft(workspaceId);
|
||||
skipDraftSaveRef.current = true;
|
||||
}
|
||||
}, [workspaceId]);
|
||||
|
||||
return { clearDraft };
|
||||
}
|
||||
|
||||
function useGitHubUrlState() {
|
||||
const [useGitHubUrl, setUseGitHubUrl] = useState(false);
|
||||
const [githubUrl, setGitHubUrl] = useState("");
|
||||
@@ -134,55 +238,39 @@ function useGitHubUrlState() {
|
||||
};
|
||||
}
|
||||
|
||||
export function useDialogFormState(
|
||||
open: boolean,
|
||||
workspaceId: string | null,
|
||||
/** Core form state declarations */
|
||||
function useFormStateValues(
|
||||
workflowId: string | null,
|
||||
workspaceId: string | null,
|
||||
open: boolean,
|
||||
initialValues?: TaskCreateDialogInitialValues,
|
||||
) {
|
||||
// openCycle increments each time dialog opens - used in key to force TaskFormInputs remount
|
||||
const [openCycle, setOpenCycle] = useState(0);
|
||||
const prevOpenRef = useRef(open);
|
||||
|
||||
// currentDefaults stores the loaded draft/initial values for this open cycle
|
||||
const [currentDefaults, setCurrentDefaults] = useState<{ name: string; description: string }>({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
// These states are initialized with defaults and then managed by effects/handlers
|
||||
const [taskName, setTaskName] = useState("");
|
||||
const [hasTitle, setHasTitle] = useState(Boolean(initialValues?.title?.trim()));
|
||||
const [hasDescription, setHasDescription] = useState(Boolean(initialValues?.description?.trim()));
|
||||
const [hasTitle, setHasTitle] = useState(false);
|
||||
const [hasDescription, setHasDescription] = useState(false);
|
||||
const [draftDescription, setDraftDescription] = useState("");
|
||||
|
||||
const descriptionInputRef = useRef<{ getValue: () => string } | null>(null);
|
||||
const [repositoryId, setRepositoryId] = useState(initialValues?.repositoryId ?? "");
|
||||
const [branch, setBranch] = useState(initialValues?.branch ?? "");
|
||||
const [agentProfileId, setAgentProfileId] = useState("");
|
||||
const [executorId, setExecutorId] = useState("");
|
||||
const [executorProfileId, setExecutorProfileId] = useState("");
|
||||
const [discoveredRepositories, setDiscoveredRepositories] = useState<LocalRepository[]>([]);
|
||||
const [discoveredRepoPath, setDiscoveredRepoPath] = useState("");
|
||||
const [selectedLocalRepo, setSelectedLocalRepo] = useState<LocalRepository | null>(null);
|
||||
const [localBranches, setLocalBranches] = useState<Branch[]>([]);
|
||||
const [localBranchesLoading, setLocalBranchesLoading] = useState(false);
|
||||
const [discoverReposLoading, setDiscoverReposLoading] = useState(false);
|
||||
const [discoverReposLoaded, setDiscoverReposLoaded] = useState(false);
|
||||
const [selectedWorkflowId, setSelectedWorkflowId] = useState(workflowId);
|
||||
const [fetchedSteps, setFetchedSteps] = useState<StepType[] | null>(null);
|
||||
const [isCreatingSession, setIsCreatingSession] = useState(false);
|
||||
const [isCreatingTask, setIsCreatingTask] = useState(false);
|
||||
const ghUrl = useGitHubUrlState();
|
||||
useFormResetEffects(open, workspaceId, workflowId, initialValues, {
|
||||
setTaskName,
|
||||
setHasTitle,
|
||||
setHasDescription,
|
||||
setRepositoryId,
|
||||
setBranch,
|
||||
setAgentProfileId,
|
||||
setExecutorId,
|
||||
setExecutorProfileId,
|
||||
setSelectedWorkflowId,
|
||||
setFetchedSteps,
|
||||
setDiscoveredRepositories,
|
||||
setDiscoveredRepoPath,
|
||||
setSelectedLocalRepo,
|
||||
setLocalBranches,
|
||||
setDiscoverReposLoaded,
|
||||
setUseGitHubUrl: ghUrl.setUseGitHubUrl,
|
||||
setGitHubUrl: ghUrl.setGitHubUrl,
|
||||
setGitHubBranches: ghUrl.setGitHubBranches,
|
||||
setGitHubUrlError: ghUrl.setGitHubUrlError,
|
||||
setGitHubPrHeadBranch: ghUrl.setGitHubPrHeadBranch,
|
||||
});
|
||||
return {
|
||||
taskName,
|
||||
setTaskName,
|
||||
@@ -190,6 +278,8 @@ export function useDialogFormState(
|
||||
setHasTitle,
|
||||
hasDescription,
|
||||
setHasDescription,
|
||||
draftDescription,
|
||||
setDraftDescription,
|
||||
descriptionInputRef,
|
||||
repositoryId,
|
||||
setRepositoryId,
|
||||
@@ -201,6 +291,32 @@ export function useDialogFormState(
|
||||
setExecutorId,
|
||||
executorProfileId,
|
||||
setExecutorProfileId,
|
||||
selectedWorkflowId,
|
||||
setSelectedWorkflowId,
|
||||
fetchedSteps,
|
||||
setFetchedSteps,
|
||||
isCreatingSession,
|
||||
setIsCreatingSession,
|
||||
isCreatingTask,
|
||||
setIsCreatingTask,
|
||||
openCycle,
|
||||
setOpenCycle,
|
||||
currentDefaults,
|
||||
setCurrentDefaults,
|
||||
prevOpenRef,
|
||||
};
|
||||
}
|
||||
|
||||
/** Repository discovery state */
|
||||
function useDiscoveryState() {
|
||||
const [discoveredRepositories, setDiscoveredRepositories] = useState<LocalRepository[]>([]);
|
||||
const [discoveredRepoPath, setDiscoveredRepoPath] = useState("");
|
||||
const [selectedLocalRepo, setSelectedLocalRepo] = useState<LocalRepository | null>(null);
|
||||
const [localBranches, setLocalBranches] = useState<Branch[]>([]);
|
||||
const [localBranchesLoading, setLocalBranchesLoading] = useState(false);
|
||||
const [discoverReposLoading, setDiscoverReposLoading] = useState(false);
|
||||
const [discoverReposLoaded, setDiscoverReposLoaded] = useState(false);
|
||||
return {
|
||||
discoveredRepositories,
|
||||
setDiscoveredRepositories,
|
||||
discoveredRepoPath,
|
||||
@@ -215,18 +331,63 @@ export function useDialogFormState(
|
||||
setDiscoverReposLoading,
|
||||
discoverReposLoaded,
|
||||
setDiscoverReposLoaded,
|
||||
selectedWorkflowId,
|
||||
setSelectedWorkflowId,
|
||||
fetchedSteps,
|
||||
setFetchedSteps,
|
||||
isCreatingSession,
|
||||
setIsCreatingSession,
|
||||
isCreatingTask,
|
||||
setIsCreatingTask,
|
||||
...ghUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function useDialogFormState(
|
||||
open: boolean,
|
||||
workspaceId: string | null,
|
||||
workflowId: string | null,
|
||||
initialValues?: TaskCreateDialogInitialValues,
|
||||
) {
|
||||
const form = useFormStateValues(workflowId, workspaceId, open, initialValues);
|
||||
const discovery = useDiscoveryState();
|
||||
const ghUrl = useGitHubUrlState();
|
||||
|
||||
useFormResetEffects({
|
||||
open,
|
||||
workspaceId,
|
||||
workflowId,
|
||||
initialValues,
|
||||
setDraftDescription: form.setDraftDescription,
|
||||
setCurrentDefaults: form.setCurrentDefaults,
|
||||
setOpenCycle: form.setOpenCycle,
|
||||
prevOpenRef: form.prevOpenRef,
|
||||
resetters: {
|
||||
setTaskName: form.setTaskName,
|
||||
setHasTitle: form.setHasTitle,
|
||||
setHasDescription: form.setHasDescription,
|
||||
setRepositoryId: form.setRepositoryId,
|
||||
setBranch: form.setBranch,
|
||||
setAgentProfileId: form.setAgentProfileId,
|
||||
setExecutorId: form.setExecutorId,
|
||||
setExecutorProfileId: form.setExecutorProfileId,
|
||||
setSelectedWorkflowId: form.setSelectedWorkflowId,
|
||||
setFetchedSteps: form.setFetchedSteps,
|
||||
setDiscoveredRepositories: discovery.setDiscoveredRepositories,
|
||||
setDiscoveredRepoPath: discovery.setDiscoveredRepoPath,
|
||||
setSelectedLocalRepo: discovery.setSelectedLocalRepo,
|
||||
setLocalBranches: discovery.setLocalBranches,
|
||||
setDiscoverReposLoaded: discovery.setDiscoverReposLoaded,
|
||||
setUseGitHubUrl: ghUrl.setUseGitHubUrl,
|
||||
setGitHubUrl: ghUrl.setGitHubUrl,
|
||||
setGitHubBranches: ghUrl.setGitHubBranches,
|
||||
setGitHubUrlError: ghUrl.setGitHubUrlError,
|
||||
setGitHubPrHeadBranch: ghUrl.setGitHubPrHeadBranch,
|
||||
},
|
||||
});
|
||||
|
||||
const { clearDraft } = useDraftPersistence(
|
||||
open,
|
||||
workspaceId,
|
||||
initialValues,
|
||||
form.taskName,
|
||||
form.descriptionInputRef,
|
||||
);
|
||||
|
||||
return { ...form, ...discovery, ...ghUrl, clearDraft };
|
||||
}
|
||||
|
||||
export type { DialogFormState } from "@/components/task-create-dialog-types";
|
||||
export {
|
||||
computePassthroughProfile,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { launchSession } from "@/lib/services/session-launch-service";
|
||||
import { buildStartRequest } from "@/lib/services/session-launch-helpers";
|
||||
import { useToast } from "@/components/toast-provider";
|
||||
import { linkToSession } from "@/lib/links";
|
||||
|
||||
import {
|
||||
activatePlanMode,
|
||||
buildCreateTaskPayload,
|
||||
@@ -64,6 +65,7 @@ export type SubmitHandlersDeps = {
|
||||
setExecutorId: (v: string) => void;
|
||||
setSelectedWorkflowId: (v: string | null) => void;
|
||||
setFetchedSteps: (v: null) => void;
|
||||
clearDraft: () => void;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
@@ -102,6 +104,7 @@ export function useTaskSubmitHandlers({
|
||||
setExecutorId,
|
||||
setSelectedWorkflowId,
|
||||
setFetchedSteps,
|
||||
clearDraft,
|
||||
}: SubmitHandlersDeps) {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
@@ -314,6 +317,7 @@ export function useTaskSubmitHandlers({
|
||||
);
|
||||
const newSessionId = taskResponse.session_id ?? taskResponse.primary_session_id ?? null;
|
||||
onSuccess?.(taskResponse, "create", { taskSessionId: newSessionId });
|
||||
clearDraft();
|
||||
onOpenChange(false);
|
||||
if (planMode && newSessionId) {
|
||||
activatePlanMode({
|
||||
@@ -336,6 +340,7 @@ export function useTaskSubmitHandlers({
|
||||
isPassthroughProfile,
|
||||
onSuccess,
|
||||
onOpenChange,
|
||||
clearDraft,
|
||||
setActiveDocument,
|
||||
setPlanMode,
|
||||
router,
|
||||
@@ -361,6 +366,7 @@ export function useTaskSubmitHandlers({
|
||||
);
|
||||
const newSessionId = taskResponse.session_id ?? taskResponse.primary_session_id ?? null;
|
||||
onSuccess?.(taskResponse, "create", { taskSessionId: newSessionId });
|
||||
clearDraft();
|
||||
onOpenChange(false);
|
||||
if (newSessionId) {
|
||||
activatePlanMode({
|
||||
@@ -380,6 +386,7 @@ export function useTaskSubmitHandlers({
|
||||
executorProfileId,
|
||||
onSuccess,
|
||||
onOpenChange,
|
||||
clearDraft,
|
||||
setActiveDocument,
|
||||
setPlanMode,
|
||||
router,
|
||||
@@ -561,6 +568,7 @@ export function useTaskSubmitHandlers({
|
||||
executor_profile_id: executorProfileId || undefined,
|
||||
});
|
||||
onSuccess?.(taskResponse, "create");
|
||||
clearDraft();
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast({
|
||||
@@ -585,6 +593,7 @@ export function useTaskSubmitHandlers({
|
||||
getRepositoriesPayload,
|
||||
onSuccess,
|
||||
onOpenChange,
|
||||
clearDraft,
|
||||
toast,
|
||||
descriptionInputRef,
|
||||
setIsCreatingTask,
|
||||
|
||||
@@ -87,6 +87,12 @@ export type DialogFormState = {
|
||||
setHasTitle: (v: boolean) => void;
|
||||
hasDescription: boolean;
|
||||
setHasDescription: (v: boolean) => void;
|
||||
/** Restored draft description, used as initialDescription for TaskFormInputs */
|
||||
draftDescription: string;
|
||||
/** Cycle counter incremented each time dialog opens - used in key for remount */
|
||||
openCycle: number;
|
||||
/** Computed defaults for current open cycle (includes draft restoration) */
|
||||
currentDefaults: { name: string; description: string };
|
||||
descriptionInputRef: import("react").RefObject<{ getValue: () => string } | null>;
|
||||
repositoryId: string;
|
||||
setRepositoryId: (v: string) => void;
|
||||
@@ -132,4 +138,6 @@ export type DialogFormState = {
|
||||
setGitHubUrlError: (v: string | null) => void;
|
||||
githubPrHeadBranch: string | null;
|
||||
setGitHubPrHeadBranch: (v: string | null) => void;
|
||||
/** Clear draft on successful submission (before closing dialog) */
|
||||
clearDraft: () => void;
|
||||
};
|
||||
|
||||
@@ -230,7 +230,6 @@ function DialogHeaderContent(props: DialogHeaderContentProps) {
|
||||
}
|
||||
|
||||
type DialogFormBodyProps = {
|
||||
open: boolean;
|
||||
isSessionMode: boolean;
|
||||
isCreateMode: boolean;
|
||||
isTaskStarted: boolean;
|
||||
@@ -262,7 +261,6 @@ type DialogFormBodyProps = {
|
||||
};
|
||||
|
||||
function DialogFormBody({
|
||||
open,
|
||||
isSessionMode,
|
||||
isCreateMode,
|
||||
isTaskStarted,
|
||||
@@ -291,7 +289,7 @@ function DialogFormBody({
|
||||
return (
|
||||
<div className="flex-1 space-y-4 overflow-y-auto pr-1">
|
||||
<TaskFormInputs
|
||||
key={`${open}-${initialDescription}`}
|
||||
key={fs.openCycle}
|
||||
isSessionMode={isSessionMode}
|
||||
autoFocus={isTaskStarted ? false : true}
|
||||
initialDescription={initialDescription}
|
||||
@@ -429,6 +427,7 @@ function useTaskCreateDialogSetup(props: TaskCreateDialogProps) {
|
||||
setExecutorId: fs.setExecutorId,
|
||||
setSelectedWorkflowId: fs.setSelectedWorkflowId,
|
||||
setFetchedSteps: fs.setFetchedSteps,
|
||||
clearDraft: fs.clearDraft,
|
||||
});
|
||||
const handleKeyDown = useKeyboardShortcutHandler(SHORTCUTS.SUBMIT, (event) => {
|
||||
submitHandlers.handleSubmit(event as unknown as FormEvent);
|
||||
@@ -491,12 +490,11 @@ export function TaskCreateDialog(props: TaskCreateDialogProps) {
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-4 overflow-hidden">
|
||||
<DialogFormBody
|
||||
open={open}
|
||||
isSessionMode={isSessionMode}
|
||||
isCreateMode={isCreateMode}
|
||||
isTaskStarted={isTaskStarted}
|
||||
isPassthroughProfile={computed.isPassthroughProfile}
|
||||
initialDescription={initialValues?.description ?? ""}
|
||||
initialDescription={fs.currentDefaults.description}
|
||||
hasDescription={fs.hasDescription}
|
||||
branchOptions={computed.branchOptions}
|
||||
branchesLoading={branchesLoading || (fs.useGitHubUrl && fs.githubBranchesLoading)}
|
||||
|
||||
@@ -610,3 +610,51 @@ export function cleanupTaskStorage(taskId: string, sessionIds: string[]): void {
|
||||
removeSessionStorage(`kandev.comments.${sessionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Task creation draft persistence (sessionStorage, per workspace) ---
|
||||
|
||||
const TASK_CREATE_DRAFT_KEY = "kandev.taskCreateDraft";
|
||||
|
||||
/**
|
||||
* Draft data for task creation dialog.
|
||||
* Only persists user-entered content fields (title, description).
|
||||
* Other fields (repo, branch, agent) are handled by "last used" localStorage.
|
||||
*/
|
||||
export type TaskCreateDraft = {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the saved task creation draft for a workspace.
|
||||
* @param workspaceId - The workspace ID
|
||||
* @returns The draft data, or null if no draft exists
|
||||
*/
|
||||
export function getTaskCreateDraft(workspaceId: string): TaskCreateDraft | null {
|
||||
if (!workspaceId) return null;
|
||||
return getSessionStorage<TaskCreateDraft | null>(`${TASK_CREATE_DRAFT_KEY}.${workspaceId}`, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a task creation draft for a workspace.
|
||||
* @param workspaceId - The workspace ID
|
||||
* @param draft - The draft data to save
|
||||
*/
|
||||
export function setTaskCreateDraft(workspaceId: string, draft: TaskCreateDraft): void {
|
||||
if (!workspaceId) return;
|
||||
// Only save if there's actual content
|
||||
if (!draft.title.trim() && !draft.description.trim()) {
|
||||
removeTaskCreateDraft(workspaceId);
|
||||
return;
|
||||
}
|
||||
setSessionStorage(`${TASK_CREATE_DRAFT_KEY}.${workspaceId}`, draft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the task creation draft for a workspace.
|
||||
* @param workspaceId - The workspace ID
|
||||
*/
|
||||
export function removeTaskCreateDraft(workspaceId: string): void {
|
||||
if (!workspaceId) return;
|
||||
removeSessionStorage(`${TASK_CREATE_DRAFT_KEY}.${workspaceId}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user