Improve UI: hierarchical navigation + task deletion
All checks were successful
Build and Push Frontend / build (push) Successful in 8s
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:
@@ -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('')
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -53,35 +59,57 @@ export default function Dashboard() {
|
|||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<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>
|
||||||
|
{selectedProject ? (
|
||||||
|
<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>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">AiWorker Dashboard</h1>
|
<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>
|
<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)}
|
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
|
|
||||||
>
|
|
||||||
New Project
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowTaskModal(true)}
|
onClick={() => setShowTaskModal(true)}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
|
||||||
>
|
>
|
||||||
New Task
|
New Task
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowProjectModal(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 text-sm font-medium"
|
||||||
|
>
|
||||||
|
New Project
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatsCards projects={projects} tasks={tasks} agents={agents} />
|
<StatsCards projects={projects} tasks={tasks} agents={agents} />
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Navigation */}
|
||||||
|
{!selectedProject && (
|
||||||
<div className="border-b border-gray-200 mb-6">
|
<div className="border-b border-gray-200 mb-6">
|
||||||
<nav className="flex space-x-8">
|
<nav className="flex space-x-8">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('projects')}
|
onClick={() => setView('projects')}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||||
activeTab === 'projects'
|
view === 'projects'
|
||||||
? 'border-blue-500 text-blue-600'
|
? 'border-blue-500 text-blue-600'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
@@ -89,19 +117,9 @@ export default function Dashboard() {
|
|||||||
Projects ({projects.length})
|
Projects ({projects.length})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('tasks')}
|
onClick={() => setView('agents')}
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||||
activeTab === 'tasks'
|
view === 'agents'
|
||||||
? 'border-blue-500 text-blue-600'
|
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Tasks ({tasks.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveTab('agents')}
|
|
||||||
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
|
||||||
activeTab === 'agents'
|
|
||||||
? 'border-blue-500 text-blue-600'
|
? 'border-blue-500 text-blue-600'
|
||||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
}`}
|
}`}
|
||||||
@@ -110,12 +128,17 @@ export default function Dashboard() {
|
|||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user