Task attempt dec9197e-0572-4216-ac38-cc3b122df210 - Final changes
This commit is contained in:
32
frontend/src/components/keyboard-shortcuts-demo.tsx
Normal file
32
frontend/src/components/keyboard-shortcuts-demo.tsx
Normal file
@@ -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 (
|
||||||
|
<Card className="w-full max-w-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Keyboard Shortcuts</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{Object.values(shortcuts).map((shortcut) => (
|
||||||
|
<div key={shortcut.key} className="flex justify-between items-center">
|
||||||
|
<span className="text-sm">{shortcut.description}</span>
|
||||||
|
<kbd className="px-2 py-1 text-xs bg-muted rounded border">
|
||||||
|
{shortcut.key === 'KeyC' ? 'C' : shortcut.key}
|
||||||
|
</kbd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import * as React from "react"
|
|||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useDialogKeyboardShortcuts } from "@/lib/keyboard-shortcuts"
|
||||||
|
|
||||||
const Dialog = React.forwardRef<
|
const Dialog = React.forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -10,6 +11,13 @@ const Dialog = React.forwardRef<
|
|||||||
onOpenChange?: (open: boolean) => void
|
onOpenChange?: (open: boolean) => void
|
||||||
}
|
}
|
||||||
>(({ className, open, onOpenChange, children, ...props }, ref) => {
|
>(({ 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
|
if (!open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
112
frontend/src/lib/keyboard-shortcuts.ts
Normal file
112
frontend/src/lib/keyboard-shortcuts.ts
Normal file
@@ -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<typeof useNavigate>;
|
||||||
|
closeDialog?: () => void;
|
||||||
|
openCreateTask?: () => void;
|
||||||
|
currentPath?: string;
|
||||||
|
hasOpenDialog?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centralized shortcut definitions
|
||||||
|
export const createKeyboardShortcuts = (context: KeyboardShortcutContext): Record<string, KeyboardShortcut> => ({
|
||||||
|
'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]);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { Card, CardContent } from "@/components/ui/card";
|
|||||||
import { ArrowLeft, Plus } from "lucide-react";
|
import { ArrowLeft, Plus } from "lucide-react";
|
||||||
import { makeRequest } from "@/lib/api";
|
import { makeRequest } from "@/lib/api";
|
||||||
import { TaskFormDialog } from "@/components/tasks/TaskFormDialog";
|
import { TaskFormDialog } from "@/components/tasks/TaskFormDialog";
|
||||||
|
import { useKeyboardShortcuts } from "@/lib/keyboard-shortcuts";
|
||||||
|
|
||||||
import { TaskKanbanBoard } from "@/components/tasks/TaskKanbanBoard";
|
import { TaskKanbanBoard } from "@/components/tasks/TaskKanbanBoard";
|
||||||
import type { TaskStatus, TaskWithAttemptStatus } from "shared/types";
|
import type { TaskStatus, TaskWithAttemptStatus } from "shared/types";
|
||||||
@@ -36,6 +37,21 @@ export function ProjectTasks() {
|
|||||||
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false);
|
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false);
|
||||||
const [editingTask, setEditingTask] = useState<Task | null>(null);
|
const [editingTask, setEditingTask] = useState<Task | null>(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(() => {
|
useEffect(() => {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
fetchProject();
|
fetchProject();
|
||||||
@@ -178,11 +194,6 @@ export function ProjectTasks() {
|
|||||||
setIsTaskDialogOpen(true);
|
setIsTaskDialogOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateNewTask = () => {
|
|
||||||
setEditingTask(null);
|
|
||||||
setIsTaskDialogOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewTaskDetails = (task: Task) => {
|
const handleViewTaskDetails = (task: Task) => {
|
||||||
navigate(`/projects/${projectId}/tasks/${task.id}`);
|
navigate(`/projects/${projectId}/tasks/${task.id}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { ProjectList } from '@/components/projects/project-list'
|
import { ProjectList } from '@/components/projects/project-list'
|
||||||
import { ProjectDetail } from '@/components/projects/project-detail'
|
import { ProjectDetail } from '@/components/projects/project-detail'
|
||||||
|
import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts'
|
||||||
|
|
||||||
export function Projects() {
|
export function Projects() {
|
||||||
const { projectId } = useParams<{ projectId: string }>()
|
const { projectId } = useParams<{ projectId: string }>()
|
||||||
@@ -10,6 +11,15 @@ export function Projects() {
|
|||||||
navigate('/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) {
|
if (projectId) {
|
||||||
return (
|
return (
|
||||||
<ProjectDetail
|
<ProjectDetail
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { ArrowLeft, FileText, Code } from "lucide-react";
|
import { ArrowLeft, FileText, Code } from "lucide-react";
|
||||||
import { makeRequest } from "@/lib/api";
|
import { makeRequest } from "@/lib/api";
|
||||||
import { TaskFormDialog } from "@/components/tasks/TaskFormDialog";
|
import { TaskFormDialog } from "@/components/tasks/TaskFormDialog";
|
||||||
|
import { useKeyboardShortcuts } from "@/lib/keyboard-shortcuts";
|
||||||
import type {
|
import type {
|
||||||
TaskStatus,
|
TaskStatus,
|
||||||
TaskAttempt,
|
TaskAttempt,
|
||||||
@@ -93,6 +94,15 @@ export function TaskDetailsPage() {
|
|||||||
|
|
||||||
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false);
|
const [isTaskDialogOpen, setIsTaskDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// Setup keyboard shortcuts
|
||||||
|
useKeyboardShortcuts({
|
||||||
|
navigate,
|
||||||
|
currentPath: `/projects/${projectId}/tasks/${taskId}`,
|
||||||
|
hasOpenDialog: isTaskDialogOpen,
|
||||||
|
closeDialog: () => setIsTaskDialogOpen(false),
|
||||||
|
openCreateTask: () => {} // No task creation on task details page
|
||||||
|
});
|
||||||
|
|
||||||
// Check if the selected attempt is active (not in a final state)
|
// Check if the selected attempt is active (not in a final state)
|
||||||
const isAttemptRunning =
|
const isAttemptRunning =
|
||||||
selectedAttempt &&
|
selectedAttempt &&
|
||||||
|
|||||||
Reference in New Issue
Block a user