Files
vibe-kanban/frontend/src/components/dialogs/tasks/CreateAttemptDialog.tsx
Louis Knight-Webb 2a9655465c All checks pass. The implementation is complete. (#1403)
**Summary:** Added CMD+Enter keyboard shortcut to the CreateAttemptDialog by:

1. Importing `useKeySubmitTask` and `Scope` from `@/keyboard`
2. Adding the `useKeySubmitTask` hook that calls `handleCreate` when:
   - The dialog is visible (`modal.visible`)
   - Creation is allowed (`canCreate` - profile and branch selected, not loading, not already creating)

This follows the same pattern used in other dialogs like `RestoreLogsDialog.tsx`.
2025-12-01 19:53:05 +00:00

236 lines
7.2 KiB
TypeScript

import { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import BranchSelector from '@/components/tasks/BranchSelector';
import { ExecutorProfileSelector } from '@/components/settings';
import { useAttemptCreation } from '@/hooks/useAttemptCreation';
import {
useNavigateWithSearch,
useTask,
useAttempt,
useBranches,
useTaskAttempts,
} from '@/hooks';
import { useProject } from '@/contexts/ProjectContext';
import { useUserSystem } from '@/components/ConfigProvider';
import { paths } from '@/lib/paths';
import NiceModal, { useModal } from '@ebay/nice-modal-react';
import { defineModal } from '@/lib/modals';
import type { ExecutorProfileId, BaseCodingAgent } from 'shared/types';
import { useKeySubmitTask, Scope } from '@/keyboard';
export interface CreateAttemptDialogProps {
taskId: string;
}
const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
({ taskId }) => {
const modal = useModal();
const navigate = useNavigateWithSearch();
const { projectId } = useProject();
const { t } = useTranslation('tasks');
const { profiles, config } = useUserSystem();
const { createAttempt, isCreating, error } = useAttemptCreation({
taskId,
onSuccess: (attempt) => {
if (projectId) {
navigate(paths.attempt(projectId, taskId, attempt.id));
}
},
});
const [userSelectedProfile, setUserSelectedProfile] =
useState<ExecutorProfileId | null>(null);
const [userSelectedBranch, setUserSelectedBranch] = useState<string | null>(
null
);
const { data: branches = [], isLoading: isLoadingBranches } = useBranches(
projectId,
{ enabled: modal.visible && !!projectId }
);
const { data: attempts = [], isLoading: isLoadingAttempts } =
useTaskAttempts(taskId, {
enabled: modal.visible,
refetchInterval: 5000,
});
const { data: task, isLoading: isLoadingTask } = useTask(taskId, {
enabled: modal.visible,
});
const parentAttemptId = task?.parent_task_attempt ?? undefined;
const { data: parentAttempt, isLoading: isLoadingParent } = useAttempt(
parentAttemptId,
{ enabled: modal.visible && !!parentAttemptId }
);
const latestAttempt = useMemo(() => {
if (attempts.length === 0) return null;
return attempts.reduce((latest, attempt) =>
new Date(attempt.created_at) > new Date(latest.created_at)
? attempt
: latest
);
}, [attempts]);
useEffect(() => {
if (!modal.visible) {
setUserSelectedProfile(null);
setUserSelectedBranch(null);
}
}, [modal.visible]);
const defaultProfile: ExecutorProfileId | null = useMemo(() => {
if (latestAttempt?.executor) {
const lastExec = latestAttempt.executor as BaseCodingAgent;
// If the last attempt used the same executor as the user's current preference,
// we assume they want to use their preferred variant as well.
// Otherwise, we default to the "default" variant (null) since we don't know
// what variant they used last time (TaskAttempt doesn't store it).
const variant =
config?.executor_profile?.executor === lastExec
? config.executor_profile.variant
: null;
return {
executor: lastExec,
variant,
};
}
return config?.executor_profile ?? null;
}, [latestAttempt?.executor, config?.executor_profile]);
const currentBranchName: string | null = useMemo(() => {
return branches.find((b) => b.is_current)?.name ?? null;
}, [branches]);
const defaultBranch: string | null = useMemo(() => {
return (
parentAttempt?.branch ??
currentBranchName ??
latestAttempt?.target_branch ??
null
);
}, [
parentAttempt?.branch,
currentBranchName,
latestAttempt?.target_branch,
]);
const effectiveProfile = userSelectedProfile ?? defaultProfile;
const effectiveBranch = userSelectedBranch ?? defaultBranch;
const isLoadingInitial =
isLoadingBranches ||
isLoadingAttempts ||
isLoadingTask ||
isLoadingParent;
const canCreate = Boolean(
effectiveProfile && effectiveBranch && !isCreating && !isLoadingInitial
);
const handleCreate = async () => {
if (!effectiveProfile || !effectiveBranch) return;
try {
await createAttempt({
profile: effectiveProfile,
baseBranch: effectiveBranch,
});
modal.hide();
} catch (err) {
console.error('Failed to create attempt:', err);
}
};
const handleOpenChange = (open: boolean) => {
if (!open) modal.hide();
};
useKeySubmitTask(handleCreate, {
enabled: modal.visible && canCreate,
scope: Scope.DIALOG,
preventDefault: true,
});
return (
<Dialog open={modal.visible} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle>{t('createAttemptDialog.title')}</DialogTitle>
<DialogDescription>
{t('createAttemptDialog.description')}
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{profiles && (
<div className="space-y-2">
<ExecutorProfileSelector
profiles={profiles}
selectedProfile={effectiveProfile}
onProfileSelect={setUserSelectedProfile}
showLabel={true}
/>
</div>
)}
<div className="space-y-2">
<Label className="text-sm font-medium">
{t('createAttemptDialog.baseBranch')}{' '}
<span className="text-destructive">*</span>
</Label>
<BranchSelector
branches={branches}
selectedBranch={effectiveBranch}
onBranchSelect={setUserSelectedBranch}
placeholder={
isLoadingBranches
? t('createAttemptDialog.loadingBranches')
: t('createAttemptDialog.selectBranch')
}
/>
</div>
{error && (
<div className="text-sm text-destructive">
{t('createAttemptDialog.error')}
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => modal.hide()}
disabled={isCreating}
>
{t('common:buttons.cancel')}
</Button>
<Button onClick={handleCreate} disabled={!canCreate}>
{isCreating
? t('createAttemptDialog.creating')
: t('createAttemptDialog.start')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
);
export const CreateAttemptDialog = defineModal<CreateAttemptDialogProps, void>(
CreateAttemptDialogImpl
);