Add URL-based task detail page and route navigation
This commit is contained in:
parent
002f380893
commit
6361ba1bb3
@ -15,6 +15,7 @@ import {
|
||||
closestCorners,
|
||||
} from "@dnd-kit/core"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@ -289,6 +290,7 @@ function KanbanTaskCard({
|
||||
|
||||
export default function Home() {
|
||||
console.log('>>> PAGE: Component rendering')
|
||||
const router = useRouter()
|
||||
const {
|
||||
projects,
|
||||
tasks,
|
||||
@ -377,6 +379,12 @@ export default function Home() {
|
||||
})
|
||||
}, [syncFromServer])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTaskId) {
|
||||
selectTask(null)
|
||||
}
|
||||
}, [selectedTaskId, selectTask])
|
||||
|
||||
// Log when tasks change
|
||||
useEffect(() => {
|
||||
console.log('>>> PAGE: tasks changed, new count:', tasks.length)
|
||||
@ -696,7 +704,7 @@ export default function Home() {
|
||||
key={task.id}
|
||||
task={task}
|
||||
taskTags={getTags(task)}
|
||||
onOpen={() => selectTask(task.id)}
|
||||
onOpen={() => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
|
||||
onDelete={() => deleteTask(task.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
548
src/app/tasks/[taskId]/page.tsx
Normal file
548
src/app/tasks/[taskId]/page.tsx
Normal file
@ -0,0 +1,548 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo, useState, type ChangeEvent } from "react"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import { ArrowLeft, Download, MessageSquare, Paperclip, Trash2, X } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { useTaskStore, type Priority, type Task, type TaskAttachment, type TaskStatus, type TaskType } from "@/stores/useTaskStore"
|
||||
|
||||
const typeColors: Record<TaskType, string> = {
|
||||
idea: "bg-purple-500",
|
||||
task: "bg-blue-500",
|
||||
bug: "bg-red-500",
|
||||
research: "bg-green-500",
|
||||
plan: "bg-amber-500",
|
||||
}
|
||||
|
||||
const typeLabels: Record<TaskType, string> = {
|
||||
idea: "💡 Idea",
|
||||
task: "📋 Task",
|
||||
bug: "🐛 Bug",
|
||||
research: "🔬 Research",
|
||||
plan: "📐 Plan",
|
||||
}
|
||||
|
||||
const priorityColors: Record<Priority, string> = {
|
||||
low: "text-slate-400",
|
||||
medium: "text-blue-400",
|
||||
high: "text-orange-400",
|
||||
urgent: "text-red-400",
|
||||
}
|
||||
|
||||
const allStatuses: TaskStatus[] = ["open", "todo", "blocked", "in-progress", "review", "validate", "archived", "canceled", "done"]
|
||||
|
||||
const getTags = (taskLike: { tags?: unknown }) => {
|
||||
if (!Array.isArray(taskLike.tags)) return [] as string[]
|
||||
return taskLike.tags.filter((tag): tag is string => typeof tag === "string" && tag.trim().length > 0)
|
||||
}
|
||||
|
||||
const getAttachments = (taskLike: { attachments?: unknown }) => {
|
||||
if (!Array.isArray(taskLike.attachments)) return [] as TaskAttachment[]
|
||||
return taskLike.attachments.filter((attachment): attachment is TaskAttachment => {
|
||||
if (!attachment || typeof attachment !== "object") return false
|
||||
const candidate = attachment as Partial<TaskAttachment>
|
||||
return typeof candidate.id === "string"
|
||||
&& typeof candidate.name === "string"
|
||||
&& typeof candidate.dataUrl === "string"
|
||||
&& typeof candidate.uploadedAt === "string"
|
||||
&& typeof candidate.type === "string"
|
||||
&& typeof candidate.size === "number"
|
||||
})
|
||||
}
|
||||
|
||||
const toLabel = (raw: string) => raw.trim().replace(/^#/, "")
|
||||
|
||||
const addUniqueLabel = (existing: string[], raw: string) => {
|
||||
const nextLabel = toLabel(raw)
|
||||
if (!nextLabel) return existing
|
||||
const alreadyExists = existing.some((label) => label.toLowerCase() === nextLabel.toLowerCase())
|
||||
return alreadyExists ? existing : [...existing, nextLabel]
|
||||
}
|
||||
|
||||
const removeLabel = (existing: string[], labelToRemove: string) =>
|
||||
existing.filter((label) => label.toLowerCase() !== labelToRemove.toLowerCase())
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B"
|
||||
const units = ["B", "KB", "MB", "GB"]
|
||||
const unitIndex = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||
const value = bytes / 1024 ** unitIndex
|
||||
return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${units[unitIndex]}`
|
||||
}
|
||||
|
||||
const readFileAsDataUrl = (file: File) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(String(reader.result || ""))
|
||||
reader.onerror = () => reject(new Error(`Failed to read file: ${file.name}`))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
|
||||
export default function TaskDetailPage() {
|
||||
const params = useParams<{ taskId: string }>()
|
||||
const router = useRouter()
|
||||
const routeTaskId = Array.isArray(params.taskId) ? params.taskId[0] : params.taskId
|
||||
const taskId = decodeURIComponent(routeTaskId || "")
|
||||
|
||||
const {
|
||||
tasks,
|
||||
sprints,
|
||||
updateTask,
|
||||
deleteTask,
|
||||
syncFromServer,
|
||||
isLoading,
|
||||
} = useTaskStore()
|
||||
|
||||
const selectedTask = tasks.find((task) => task.id === taskId)
|
||||
const [editedTask, setEditedTask] = useState<Task | null>(null)
|
||||
const [newComment, setNewComment] = useState("")
|
||||
const [editedTaskLabelInput, setEditedTaskLabelInput] = useState("")
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
syncFromServer()
|
||||
}, [syncFromServer])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTask) {
|
||||
setEditedTask({
|
||||
...selectedTask,
|
||||
tags: getTags(selectedTask),
|
||||
attachments: getAttachments(selectedTask),
|
||||
})
|
||||
setEditedTaskLabelInput("")
|
||||
}
|
||||
}, [selectedTask])
|
||||
|
||||
const editedTaskTags = editedTask ? getTags(editedTask) : []
|
||||
const editedTaskAttachments = editedTask ? getAttachments(editedTask) : []
|
||||
|
||||
const allLabels = useMemo(() => {
|
||||
const labels = new Map<string, number>()
|
||||
tasks.forEach((task) => {
|
||||
getTags(task).forEach((label) => {
|
||||
labels.set(label, (labels.get(label) || 0) + 1)
|
||||
})
|
||||
})
|
||||
return Array.from(labels.keys())
|
||||
}, [tasks])
|
||||
|
||||
const handleAttachmentUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = Array.from(event.target.files || [])
|
||||
if (files.length === 0) return
|
||||
|
||||
try {
|
||||
const uploadedAt = new Date().toISOString()
|
||||
const attachments = await Promise.all(
|
||||
files.map(async (file) => ({
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: file.name,
|
||||
type: file.type || "application/octet-stream",
|
||||
size: file.size,
|
||||
dataUrl: await readFileAsDataUrl(file),
|
||||
uploadedAt,
|
||||
}))
|
||||
)
|
||||
|
||||
setEditedTask((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
attachments: [...getAttachments(prev), ...attachments],
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to upload attachment:", error)
|
||||
} finally {
|
||||
event.target.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddComment = () => {
|
||||
if (!editedTask || !newComment.trim()) return
|
||||
|
||||
setEditedTask({
|
||||
...editedTask,
|
||||
comments: [
|
||||
...(editedTask.comments || []),
|
||||
{
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
text: newComment.trim(),
|
||||
createdAt: new Date().toISOString(),
|
||||
author: "user",
|
||||
},
|
||||
],
|
||||
})
|
||||
setNewComment("")
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
if (!editedTask) return
|
||||
setIsSaving(true)
|
||||
updateTask(editedTask.id, editedTask)
|
||||
setIsSaving(false)
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!editedTask) return
|
||||
if (!window.confirm("Delete this task?")) return
|
||||
deleteTask(editedTask.id)
|
||||
router.push("/")
|
||||
}
|
||||
|
||||
if (!taskId) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||
<p className="text-slate-400">Invalid task URL.</p>
|
||||
<Button className="mt-4" onClick={() => router.push("/")}>Back to Board</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!selectedTask && !isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||
<h1 className="text-xl font-semibold text-white">Task not found</h1>
|
||||
<p className="text-slate-400 mt-2">This task may have been deleted.</p>
|
||||
<Button className="mt-4" onClick={() => router.push("/")}>Back to Board</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!editedTask) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||
<p className="text-slate-400">Loading task...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
<div className="max-w-4xl mx-auto p-4 md:p-6">
|
||||
<div className="flex items-center justify-between gap-3 mb-4">
|
||||
<Button variant="ghost" onClick={() => router.push("/")}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Board
|
||||
</Button>
|
||||
<span className="text-xs text-slate-500">Task URL: /tasks/{editedTask.id}</span>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-800 rounded-xl bg-slate-900/60 p-4 md:p-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Badge className={`${typeColors[editedTask.type]} text-white border-0`}>
|
||||
{typeLabels[editedTask.type]}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={priorityColors[editedTask.priority]}>
|
||||
{editedTask.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={editedTask.title}
|
||||
onChange={(event) => setEditedTask({ ...editedTask, title: event.target.value })}
|
||||
className="w-full text-xl font-semibold bg-slate-800 border border-slate-700 rounded px-3 py-2 text-white"
|
||||
/>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
<div>
|
||||
<Label className="text-slate-400">Description</Label>
|
||||
<textarea
|
||||
value={editedTask.description || ""}
|
||||
onChange={(event) => setEditedTask({ ...editedTask, description: event.target.value })}
|
||||
rows={3}
|
||||
className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-400">Priority</Label>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{(["low", "medium", "high", "urgent"] as Priority[]).map((priority) => (
|
||||
<button
|
||||
key={priority}
|
||||
onClick={() => setEditedTask({ ...editedTask, priority })}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm transition-colors capitalize ${
|
||||
editedTask.priority === priority
|
||||
? priority === "urgent"
|
||||
? "bg-red-600 text-white"
|
||||
: priority === "high"
|
||||
? "bg-orange-600 text-white"
|
||||
: priority === "medium"
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-slate-600 text-white"
|
||||
: "bg-slate-800 text-slate-400 hover:bg-slate-700"
|
||||
}`}
|
||||
>
|
||||
{priority}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-400">Status</Label>
|
||||
<select
|
||||
value={editedTask.status}
|
||||
onChange={(event) => setEditedTask({ ...editedTask, status: event.target.value as TaskStatus })}
|
||||
className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
{allStatuses.map((status) => (
|
||||
<option key={status} value={status}>{status.replace("-", " ").toUpperCase()}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-400">Sprint</Label>
|
||||
<select
|
||||
value={editedTask.sprintId || ""}
|
||||
onChange={(event) => setEditedTask({ ...editedTask, sprintId: event.target.value || undefined })}
|
||||
className="w-full mt-2 px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white focus:outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="">No Sprint</option>
|
||||
{sprints.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()).map((sprint) => (
|
||||
<option key={sprint.id} value={sprint.id}>
|
||||
{sprint.name} ({new Date(sprint.startDate).toLocaleDateString()})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-400">Labels</Label>
|
||||
<div className="mt-2 space-y-2">
|
||||
{editedTaskTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{editedTaskTags.map((label) => (
|
||||
<Badge
|
||||
key={label}
|
||||
variant="secondary"
|
||||
className="bg-slate-800 text-slate-300 gap-1"
|
||||
>
|
||||
{label}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setEditedTask({ ...editedTask, tags: removeLabel(editedTaskTags, label) })
|
||||
}
|
||||
className="text-slate-500 hover:text-slate-200"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
list="edit-task-label-suggestions"
|
||||
value={editedTaskLabelInput}
|
||||
onChange={(event) => setEditedTaskLabelInput(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" || event.key === ",") {
|
||||
event.preventDefault()
|
||||
setEditedTask({ ...editedTask, tags: addUniqueLabel(editedTaskTags, editedTaskLabelInput) })
|
||||
setEditedTaskLabelInput("")
|
||||
}
|
||||
}}
|
||||
className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
||||
placeholder="Type a label and press Enter"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditedTask({ ...editedTask, tags: addUniqueLabel(editedTaskTags, editedTaskLabelInput) })
|
||||
setEditedTaskLabelInput("")
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<datalist id="edit-task-label-suggestions">
|
||||
{allLabels.map((label) => (
|
||||
<option key={label} value={label} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label className="text-slate-400">Attachments</Label>
|
||||
<div className="mt-2 space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<label
|
||||
htmlFor="task-attachment-upload"
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-md border border-slate-700 bg-slate-800 text-sm text-slate-200 hover:border-slate-500 cursor-pointer"
|
||||
>
|
||||
<Paperclip className="w-3.5 h-3.5" />
|
||||
Add Files
|
||||
</label>
|
||||
<input
|
||||
id="task-attachment-upload"
|
||||
type="file"
|
||||
multiple
|
||||
onChange={handleAttachmentUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
<span className="text-xs text-slate-500">
|
||||
{editedTaskAttachments.length} file{editedTaskAttachments.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{editedTaskAttachments.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">No attachments yet.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{editedTaskAttachments.map((attachment) => (
|
||||
<div
|
||||
key={attachment.id}
|
||||
className="flex items-center justify-between gap-3 p-3 rounded-lg border border-slate-800 bg-slate-800/50"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<a
|
||||
href={attachment.dataUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-sm text-blue-300 hover:text-blue-200 truncate block"
|
||||
>
|
||||
{attachment.name}
|
||||
</a>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{formatBytes(attachment.size)} · {new Date(attachment.uploadedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<a
|
||||
href={attachment.dataUrl}
|
||||
download={attachment.name}
|
||||
className="p-1.5 rounded text-slate-400 hover:text-slate-200 hover:bg-slate-700"
|
||||
title="Download attachment"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setEditedTask({
|
||||
...editedTask,
|
||||
attachments: editedTaskAttachments.filter((item) => item.id !== attachment.id),
|
||||
})
|
||||
}
|
||||
className="p-1.5 rounded text-slate-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
title="Remove attachment"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-800 pt-6">
|
||||
<h4 className="font-medium text-white mb-4 flex items-center gap-2">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
Notes & Comments ({editedTask.comments?.length || 0})
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4 mb-4">
|
||||
{!editedTask.comments || editedTask.comments.length === 0 ? (
|
||||
<p className="text-slate-500 text-sm">No notes yet. Add the first one!</p>
|
||||
) : (
|
||||
editedTask.comments.map((comment) => (
|
||||
<div
|
||||
key={comment.id}
|
||||
className={`flex gap-3 p-3 rounded-lg ${
|
||||
comment.author === "assistant" ? "bg-blue-900/20" : "bg-slate-800/50"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium ${
|
||||
comment.author === "assistant"
|
||||
? "bg-blue-600 text-white"
|
||||
: "bg-slate-700 text-slate-300"
|
||||
}`}
|
||||
>
|
||||
{comment.author === "assistant" ? "AI" : "You"}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-medium text-slate-300">
|
||||
{comment.author === "assistant" ? "Assistant" : "You"}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">
|
||||
{new Date(comment.createdAt).toLocaleString()}
|
||||
</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditedTask({
|
||||
...editedTask,
|
||||
comments: editedTask.comments.filter((entry) => entry.id !== comment.id),
|
||||
})
|
||||
}
|
||||
className="text-slate-600 hover:text-red-400"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-slate-300 text-sm whitespace-pre-wrap">{comment.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Textarea
|
||||
value={newComment}
|
||||
onChange={(event) => setNewComment(event.target.value)}
|
||||
placeholder="Add a note or comment..."
|
||||
className="flex-1 bg-slate-800 border-slate-700 text-white placeholder-slate-500"
|
||||
rows={2}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && event.metaKey) {
|
||||
handleAddComment()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button onClick={handleAddComment} className="self-end">
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-slate-800 pt-4 flex justify-between">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete Task
|
||||
</Button>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="ghost" onClick={() => router.push("/")}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -18,6 +18,7 @@ import {
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useTaskStore, Task } from "@/stores/useTaskStore"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
@ -193,11 +194,11 @@ function TaskSection({
|
||||
}
|
||||
|
||||
export function BacklogView() {
|
||||
const router = useRouter()
|
||||
const {
|
||||
tasks,
|
||||
sprints,
|
||||
selectedProjectId,
|
||||
selectTask,
|
||||
updateTask,
|
||||
addSprint,
|
||||
} = useTaskStore()
|
||||
@ -314,7 +315,7 @@ export function BacklogView() {
|
||||
tasks={currentSprintTasks}
|
||||
isOpen={openSections.current}
|
||||
onToggle={() => toggleSection("current")}
|
||||
onTaskClick={(task) => selectTask(task.id)}
|
||||
onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
|
||||
sprintInfo={
|
||||
currentSprint
|
||||
? {
|
||||
@ -345,7 +346,7 @@ export function BacklogView() {
|
||||
tasks={sprintTasks}
|
||||
isOpen={openSections[sprint.id] ?? false}
|
||||
onToggle={() => toggleSection(sprint.id)}
|
||||
onTaskClick={(task) => selectTask(task.id)}
|
||||
onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
|
||||
sprintInfo={{
|
||||
name: sprint.name,
|
||||
date: (() => {
|
||||
@ -422,7 +423,7 @@ export function BacklogView() {
|
||||
tasks={backlogTasks}
|
||||
isOpen={openSections.backlog}
|
||||
onToggle={() => toggleSection("backlog")}
|
||||
onTaskClick={(task) => selectTask(task.id)}
|
||||
onTaskClick={(task) => router.push(`/tasks/${encodeURIComponent(task.id)}`)}
|
||||
/>
|
||||
</SectionDropZone>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user