From c1effa517a559d0171c77c13f53d97bfdda668f3 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Thu, 19 Jun 2025 21:16:28 -0400 Subject: [PATCH] Task attempt dec9197e-0572-4216-ac38-cc3b122df210 - Final changes --- .../components/keyboard-shortcuts-demo.tsx | 32 +++++ frontend/src/components/ui/dialog.tsx | 8 ++ frontend/src/lib/keyboard-shortcuts.ts | 112 ++++++++++++++++++ frontend/src/pages/project-tasks.tsx | 21 +++- frontend/src/pages/projects.tsx | 10 ++ frontend/src/pages/task-details.tsx | 10 ++ 6 files changed, 188 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/keyboard-shortcuts-demo.tsx create mode 100644 frontend/src/lib/keyboard-shortcuts.ts diff --git a/frontend/src/components/keyboard-shortcuts-demo.tsx b/frontend/src/components/keyboard-shortcuts-demo.tsx new file mode 100644 index 00000000..2e37172a --- /dev/null +++ b/frontend/src/components/keyboard-shortcuts-demo.tsx @@ -0,0 +1,32 @@ +import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +export function KeyboardShortcutsDemo() { + const shortcuts = useKeyboardShortcuts({ + navigate: undefined, + currentPath: '/demo', + hasOpenDialog: false, + closeDialog: () => {}, + openCreateTask: () => {} + }); + + return ( + + + Keyboard Shortcuts + + +
+ {Object.values(shortcuts).map((shortcut) => ( +
+ {shortcut.description} + + {shortcut.key === 'KeyC' ? 'C' : shortcut.key} + +
+ ))} +
+
+
+ ); +} diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index aab0623b..ffd051c9 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -2,6 +2,7 @@ import * as React from "react" import { X } from "lucide-react" import { cn } from "@/lib/utils" +import { useDialogKeyboardShortcuts } from "@/lib/keyboard-shortcuts" const Dialog = React.forwardRef< HTMLDivElement, @@ -10,6 +11,13 @@ const Dialog = React.forwardRef< onOpenChange?: (open: boolean) => void } >(({ className, open, onOpenChange, children, ...props }, ref) => { + // Add keyboard shortcut support for closing dialog with Esc + useDialogKeyboardShortcuts(() => { + if (open && onOpenChange) { + onOpenChange(false); + } + }); + if (!open) return null return ( diff --git a/frontend/src/lib/keyboard-shortcuts.ts b/frontend/src/lib/keyboard-shortcuts.ts new file mode 100644 index 00000000..ea13c399 --- /dev/null +++ b/frontend/src/lib/keyboard-shortcuts.ts @@ -0,0 +1,112 @@ +import { useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; + +// Define available keyboard shortcuts +export interface KeyboardShortcut { + key: string; + description: string; + action: (context?: KeyboardShortcutContext) => void; + requiresModifier?: boolean; + disabled?: boolean; +} + +export interface KeyboardShortcutContext { + navigate?: ReturnType; + closeDialog?: () => void; + openCreateTask?: () => void; + currentPath?: string; + hasOpenDialog?: boolean; +} + +// Centralized shortcut definitions +export const createKeyboardShortcuts = (context: KeyboardShortcutContext): Record => ({ + 'Escape': { + key: 'Escape', + description: 'Go back or close dialog', + action: () => { + // If there's an open dialog, close it + if (context.hasOpenDialog && context.closeDialog) { + context.closeDialog(); + return; + } + + // Otherwise, navigate back + if (context.navigate) { + const currentPath = context.currentPath || window.location.pathname; + + // Navigate back based on current path + if (currentPath.includes('/attempts/') && currentPath.includes('/compare')) { + // From compare page, go back to task details + const taskPath = currentPath.split('/attempts/')[0]; + context.navigate(taskPath); + } else if (currentPath.includes('/tasks/') && !currentPath.endsWith('/tasks')) { + // From task details, go back to project tasks + const projectPath = currentPath.split('/tasks/')[0] + '/tasks'; + context.navigate(projectPath); + } else if (currentPath.includes('/projects/') && currentPath.includes('/tasks')) { + // From project tasks, go back to projects + context.navigate('/projects'); + } else if (currentPath !== '/' && currentPath !== '/projects') { + // Default: go to projects page + context.navigate('/projects'); + } + } + } + }, + 'KeyC': { + key: 'c', + description: 'Create new task', + action: () => { + if (context.openCreateTask) { + context.openCreateTask(); + } + } + } +}); + +// Hook to register global keyboard shortcuts +export function useKeyboardShortcuts(context: KeyboardShortcutContext) { + const shortcuts = createKeyboardShortcuts(context); + + const handleKeyDown = useCallback((event: KeyboardEvent) => { + // Don't trigger shortcuts when typing in input fields + const target = event.target as HTMLElement; + if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) { + return; + } + + // Don't trigger shortcuts when modifier keys are pressed (except for specific shortcuts) + if (event.ctrlKey || event.metaKey || event.altKey) { + return; + } + + const shortcut = shortcuts[event.code] || shortcuts[event.key]; + + if (shortcut && !shortcut.disabled) { + event.preventDefault(); + shortcut.action(context); + } + }, [shortcuts, context]); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + return shortcuts; +} + +// Hook for dialog-specific keyboard shortcuts +export function useDialogKeyboardShortcuts(onClose: () => void) { + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + onClose(); + } + }; + + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onClose]); +} diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index 4bb80559..e1b98a1e 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -5,6 +5,7 @@ import { Card, CardContent } from "@/components/ui/card"; import { ArrowLeft, Plus } from "lucide-react"; import { makeRequest } from "@/lib/api"; import { TaskFormDialog } from "@/components/tasks/TaskFormDialog"; +import { useKeyboardShortcuts } from "@/lib/keyboard-shortcuts"; import { TaskKanbanBoard } from "@/components/tasks/TaskKanbanBoard"; import type { TaskStatus, TaskWithAttemptStatus } from "shared/types"; @@ -36,6 +37,21 @@ export function ProjectTasks() { const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false); const [editingTask, setEditingTask] = useState(null); + // Define task creation handler + const handleCreateNewTask = () => { + setEditingTask(null); + setIsTaskDialogOpen(true); + }; + + // Setup keyboard shortcuts + useKeyboardShortcuts({ + navigate, + currentPath: `/projects/${projectId}/tasks`, + hasOpenDialog: isTaskDialogOpen, + closeDialog: () => setIsTaskDialogOpen(false), + openCreateTask: handleCreateNewTask + }); + useEffect(() => { if (projectId) { fetchProject(); @@ -178,11 +194,6 @@ export function ProjectTasks() { setIsTaskDialogOpen(true); }; - const handleCreateNewTask = () => { - setEditingTask(null); - setIsTaskDialogOpen(true); - }; - const handleViewTaskDetails = (task: Task) => { navigate(`/projects/${projectId}/tasks/${task.id}`); }; diff --git a/frontend/src/pages/projects.tsx b/frontend/src/pages/projects.tsx index 6fcd408a..d21aaa56 100644 --- a/frontend/src/pages/projects.tsx +++ b/frontend/src/pages/projects.tsx @@ -1,6 +1,7 @@ import { useParams, useNavigate } from 'react-router-dom' import { ProjectList } from '@/components/projects/project-list' import { ProjectDetail } from '@/components/projects/project-detail' +import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts' export function Projects() { const { projectId } = useParams<{ projectId: string }>() @@ -10,6 +11,15 @@ export function Projects() { navigate('/projects') } + // Setup keyboard shortcuts (only Esc for back navigation, no task creation here) + useKeyboardShortcuts({ + navigate, + currentPath: projectId ? `/projects/${projectId}` : '/projects', + hasOpenDialog: false, + closeDialog: () => {}, + openCreateTask: () => {} // No-op for projects page + }) + if (projectId) { return ( setIsTaskDialogOpen(false), + openCreateTask: () => {} // No task creation on task details page + }); + // Check if the selected attempt is active (not in a final state) const isAttemptRunning = selectedAttempt &&