Improve UI: hierarchical navigation + task deletion
All checks were successful
Build and Push Frontend / build (push) Successful in 8s

- Add delete button to tasks with confirmation
- Restructure Dashboard: select Project first, then view its Tasks
- Remove global Tasks tab, tasks are now scoped to project
- Add back button from project view to projects list
- Pre-select project when creating task from project view
- Make project cards clickable
- Better UX with clear navigation flow

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hector Ros
2026-01-20 02:10:19 +01:00
parent 8b5f75a289
commit 0ac135539b
4 changed files with 125 additions and 60 deletions

View File

@@ -7,15 +7,23 @@ interface CreateTaskModalProps {
onClose: () => void onClose: () => void
onCreated: () => void onCreated: () => void
projects: Project[] projects: Project[]
selectedProjectId?: string
} }
export default function CreateTaskModal({ isOpen, onClose, onCreated, projects }: CreateTaskModalProps) { export default function CreateTaskModal({ isOpen, onClose, onCreated, projects, selectedProjectId }: CreateTaskModalProps) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
projectId: '', projectId: selectedProjectId || '',
title: '', title: '',
description: '', description: '',
priority: 'medium', priority: 'medium',
}) })
// Update projectId when selectedProjectId changes
React.useEffect(() => {
if (selectedProjectId) {
setFormData((prev) => ({ ...prev, projectId: selectedProjectId }))
}
}, [selectedProjectId])
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')

View File

@@ -4,9 +4,10 @@ import type { Project } from '../types'
interface ProjectListProps { interface ProjectListProps {
projects: Project[] projects: Project[]
onRefresh: () => void onRefresh: () => void
onSelectProject: (project: Project) => void
} }
export default function ProjectList({ projects, onRefresh }: ProjectListProps) { export default function ProjectList({ projects, onRefresh, onSelectProject }: ProjectListProps) {
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case 'active': case 'active':
@@ -31,7 +32,11 @@ export default function ProjectList({ projects, onRefresh }: ProjectListProps) {
return ( return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{projects.map((project) => ( {projects.map((project) => (
<div key={project.id} className="bg-white rounded-lg shadow p-6"> <div
key={project.id}
className="bg-white rounded-lg shadow p-6 cursor-pointer hover:shadow-lg transition-shadow"
onClick={() => onSelectProject(project)}
>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">{project.name}</h3> <h3 className="text-lg font-semibold text-gray-900">{project.name}</h3>

View File

@@ -1,4 +1,5 @@
import React from 'react' import React, { useState } from 'react'
import apiClient from '../api/client'
import type { Task } from '../types' import type { Task } from '../types'
interface TaskListProps { interface TaskListProps {
@@ -7,6 +8,23 @@ interface TaskListProps {
} }
export default function TaskList({ tasks, onRefresh }: TaskListProps) { export default function TaskList({ tasks, onRefresh }: TaskListProps) {
const [deleting, setDeleting] = useState<string | null>(null)
const handleDelete = async (taskId: string) => {
if (!confirm('Are you sure you want to delete this task?')) return
setDeleting(taskId)
try {
await apiClient.delete(`/tasks/${taskId}`)
onRefresh()
} catch (error) {
console.error('Failed to delete task:', error)
alert('Failed to delete task')
} finally {
setDeleting(null)
}
}
const getStateColor = (state: string) => { const getStateColor = (state: string) => {
switch (state) { switch (state) {
case 'backlog': case 'backlog':
@@ -54,8 +72,18 @@ export default function TaskList({ tasks, onRefresh }: TaskListProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{tasks.map((task) => ( {tasks.map((task) => (
<div key={task.id} className="bg-white rounded-lg shadow p-6"> <div key={task.id} className="bg-white rounded-lg shadow p-6 relative">
<div className="flex items-start justify-between"> <button
onClick={() => handleDelete(task.id)}
disabled={deleting === task.id}
className="absolute top-4 right-4 text-red-600 hover:text-red-800 disabled:opacity-50"
title="Delete task"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<div className="flex items-start justify-between pr-8">
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<h3 className="text-lg font-semibold text-gray-900">{task.title}</h3> <h3 className="text-lg font-semibold text-gray-900">{task.title}</h3>

View File

@@ -13,10 +13,16 @@ export default function Dashboard() {
const [tasks, setTasks] = useState<Task[]>([]) const [tasks, setTasks] = useState<Task[]>([])
const [agents, setAgents] = useState<Agent[]>([]) const [agents, setAgents] = useState<Agent[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<'projects' | 'tasks' | 'agents'>('projects') const [selectedProject, setSelectedProject] = useState<Project | null>(null)
const [view, setView] = useState<'projects' | 'agents'>('projects')
const [showProjectModal, setShowProjectModal] = useState(false) const [showProjectModal, setShowProjectModal] = useState(false)
const [showTaskModal, setShowTaskModal] = useState(false) const [showTaskModal, setShowTaskModal] = useState(false)
// Filter tasks for selected project
const filteredTasks = selectedProject
? tasks.filter((t) => t.projectId === selectedProject.id)
: []
useEffect(() => { useEffect(() => {
loadData() loadData()
// Refresh data every 10 seconds // Refresh data every 10 seconds
@@ -54,68 +60,85 @@ export default function Dashboard() {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="mb-8 flex items-center justify-between"> <div className="mb-8 flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">AiWorker Dashboard</h1> {selectedProject ? (
<p className="mt-2 text-gray-600">Manage your AI agents and development tasks</p> <div>
<button
onClick={() => setSelectedProject(null)}
className="text-sm text-blue-600 hover:text-blue-800 mb-2 flex items-center"
>
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Back to Projects
</button>
<h1 className="text-3xl font-bold text-gray-900">{selectedProject.name}</h1>
<p className="mt-2 text-gray-600">{selectedProject.description || 'Project tasks and details'}</p>
</div>
) : (
<div>
<h1 className="text-3xl font-bold text-gray-900">AiWorker Dashboard</h1>
<p className="mt-2 text-gray-600">Manage your AI agents and development tasks</p>
</div>
)}
</div> </div>
<div className="flex space-x-3"> <div className="flex space-x-3">
<button {selectedProject ? (
onClick={() => setShowProjectModal(true)} <button
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium" onClick={() => setShowTaskModal(true)}
> className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
New Project >
</button> New Task
<button </button>
onClick={() => setShowTaskModal(true)} ) : (
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium" <button
> onClick={() => setShowProjectModal(true)}
New Task className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
</button> >
New Project
</button>
)}
</div> </div>
</div> </div>
<StatsCards projects={projects} tasks={tasks} agents={agents} /> <StatsCards projects={projects} tasks={tasks} agents={agents} />
{/* Tabs */} {/* Navigation */}
<div className="border-b border-gray-200 mb-6"> {!selectedProject && (
<nav className="flex space-x-8"> <div className="border-b border-gray-200 mb-6">
<button <nav className="flex space-x-8">
onClick={() => setActiveTab('projects')} <button
className={`py-4 px-1 border-b-2 font-medium text-sm ${ onClick={() => setView('projects')}
activeTab === 'projects' className={`py-4 px-1 border-b-2 font-medium text-sm ${
? 'border-blue-500 text-blue-600' view === 'projects'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' ? 'border-blue-500 text-blue-600'
}`} : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
> }`}
Projects ({projects.length}) >
</button> Projects ({projects.length})
<button </button>
onClick={() => setActiveTab('tasks')} <button
className={`py-4 px-1 border-b-2 font-medium text-sm ${ onClick={() => setView('agents')}
activeTab === 'tasks' className={`py-4 px-1 border-b-2 font-medium text-sm ${
? 'border-blue-500 text-blue-600' view === 'agents'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' ? 'border-blue-500 text-blue-600'
}`} : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
> }`}
Tasks ({tasks.length}) >
</button> Agents ({agents.length})
<button </button>
onClick={() => setActiveTab('agents')} </nav>
className={`py-4 px-1 border-b-2 font-medium text-sm ${ </div>
activeTab === 'agents' )}
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Agents ({agents.length})
</button>
</nav>
</div>
{/* Content */} {/* Content */}
<div> <div>
{activeTab === 'projects' && <ProjectList projects={projects} onRefresh={loadData} />} {selectedProject ? (
{activeTab === 'tasks' && <TaskList tasks={tasks} onRefresh={loadData} />} <TaskList tasks={filteredTasks} onRefresh={loadData} />
{activeTab === 'agents' && <AgentStatus agents={agents} onRefresh={loadData} />} ) : view === 'projects' ? (
<ProjectList projects={projects} onRefresh={loadData} onSelectProject={setSelectedProject} />
) : (
<AgentStatus agents={agents} onRefresh={loadData} />
)}
</div> </div>
</div> </div>
@@ -130,6 +153,7 @@ export default function Dashboard() {
onClose={() => setShowTaskModal(false)} onClose={() => setShowTaskModal(false)}
onCreated={loadData} onCreated={loadData}
projects={projects} projects={projects}
selectedProjectId={selectedProject?.id}
/> />
</div> </div>
) )