Signed-off-by: Max <ai-agent@topdoglabs.com>
This commit is contained in:
Max 2026-02-22 17:56:39 -06:00
parent b2b2beef2d
commit ab8cc0a6a1
6 changed files with 79 additions and 7 deletions

View File

@ -7,7 +7,7 @@ export const runtime = "nodejs";
// Sprint dates are stored as SQL DATE values (YYYY-MM-DD). We accept either
// date-only or ISO datetime inputs and normalize to the date prefix.
const DATE_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})/;
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const SPRINT_STATUSES = ["planning", "active", "completed"] as const;
type SprintStatus = (typeof SPRINT_STATUSES)[number];

View File

@ -61,7 +61,7 @@ const TASK_TYPES: Task["type"][] = ["idea", "task", "bug", "research", "plan"];
const TASK_STATUSES: Task["status"][] = ["open", "todo", "blocked", "in-progress", "review", "validate", "archived", "canceled", "done"];
const TASK_PRIORITIES: Task["priority"][] = ["low", "medium", "high", "urgent"];
const SPRINT_STATUSES: Sprint["status"][] = ["planning", "active", "completed"];
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
// Optimized field selection - only fetch fields needed for board display
// Full task details (description, comments, attachments) fetched lazily

View File

@ -38,6 +38,7 @@ import {
import { useTaskStore, Task, TaskType, TaskStatus, Priority, TaskAttachment, type CommentAuthor } from "@/stores/useTaskStore"
import { BacklogSkeleton, SearchSkeleton } from "@/components/LoadingSkeletons"
import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download, Search, Archive } from "lucide-react"
import { toast } from "sonner"
// Dynamic imports for heavy view components - only load when needed
const BacklogView = dynamic(() => import("@/components/BacklogView").then(mod => mod.BacklogView), {
@ -830,7 +831,14 @@ export default function Home() {
if (newTask.title?.trim()) {
// If a specific sprint is selected, use that sprint's project
const selectedSprint = newTask.sprintId ? sprints.find(s => s.id === newTask.sprintId) : null
const targetProjectId = selectedSprint?.projectId || selectedProjectId || projects[0]?.id || '2'
const targetProjectId = selectedSprint?.projectId || selectedProjectId || projects[0]?.id
if (!targetProjectId) {
toast.error("Cannot create task", {
description: "No project is available. Create or select a project first.",
duration: 5000,
})
return
}
const taskToCreate: Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'comments'> = {
title: newTask.title.trim(),

View File

@ -227,6 +227,7 @@ export default function TaskDetailPage() {
const {
tasks,
projects,
sprints,
currentUser,
updateTask,
@ -355,9 +356,10 @@ export default function TaskDetailPage() {
const sortedSprints = useMemo(
() =>
sprints
.filter((sprint) => sprint.projectId === editedTask?.projectId)
.slice()
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()),
[sprints]
[sprints, editedTask?.projectId]
)
const handleAttachmentUpload = async (event: ChangeEvent<HTMLInputElement>) => {
@ -459,8 +461,28 @@ export default function TaskDetailPage() {
})
}
const setEditedTaskProject = (projectId: string) => {
if (!editedTask) return
const sprintStillMatchesProject =
!editedTask.sprintId || sprints.some((sprint) => sprint.id === editedTask.sprintId && sprint.projectId === projectId)
setEditedTask({
...editedTask,
projectId,
sprintId: sprintStillMatchesProject ? editedTask.sprintId : undefined,
})
}
const handleSave = async () => {
if (!editedTask) return
if (!editedTask.projectId) {
toast.error("Project is required", {
description: "Select a project before saving this task.",
duration: 5000,
})
return
}
setIsSaving(true)
setSaveSuccess(false)
@ -777,6 +799,22 @@ export default function TaskDetailPage() {
</select>
</div>
<div>
<Label className="text-slate-400">Project</Label>
<select
value={editedTask.projectId}
onChange={(event) => setEditedTaskProject(event.target.value)}
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="" disabled>Select project</option>
{projects.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select>
</div>
<div>
<Label className="text-slate-400">Sprint</Label>
<select

View File

@ -370,7 +370,7 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
}
const handleCreateSprint = () => {
if (!newSprint.name) return
if (!newSprint.name || !selectedProjectId) return
addSprint({
name: newSprint.name,
@ -378,7 +378,7 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
startDate: newSprint.startDate || toLocalDateInputValue(),
endDate: newSprint.endDate || toLocalDateInputValue(),
status: "planning",
projectId: selectedProjectId || "2",
projectId: selectedProjectId,
})
setIsCreatingSprint(false)

View File

@ -5,6 +5,7 @@ export type TaskType = 'idea' | 'task' | 'bug' | 'research' | 'plan'
export type TaskStatus = 'open' | 'todo' | 'blocked' | 'in-progress' | 'review' | 'validate' | 'archived' | 'canceled' | 'done'
export type Priority = 'low' | 'medium' | 'high' | 'urgent'
export type SprintStatus = 'planning' | 'active' | 'completed'
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
export interface Sprint {
id: string
@ -293,6 +294,23 @@ async function requestApi(path: string, init: RequestInit): Promise<unknown> {
// Helper to sync a single task to server (lightweight)
async function syncTaskToServer(task: Task) {
console.log('>>> syncTaskToServer: saving task', task.id, task.title)
const isValidUuid = (value: string | undefined) => typeof value === 'string' && UUID_PATTERN.test(value)
if (!isValidUuid(task.id)) {
console.error('>>> syncTaskToServer: invalid task.id (expected UUID)', task.id)
return false
}
if (!isValidUuid(task.projectId)) {
console.error('>>> syncTaskToServer: invalid task.projectId (expected UUID)', task.projectId)
return false
}
if (task.sprintId && !isValidUuid(task.sprintId)) {
console.error('>>> syncTaskToServer: invalid task.sprintId (expected UUID)', task.sprintId)
return false
}
if (task.assigneeId && !isValidUuid(task.assigneeId)) {
console.error('>>> syncTaskToServer: invalid task.assigneeId (expected UUID)', task.assigneeId)
return false
}
try {
const res = await fetch('/api/tasks', {
method: 'POST',
@ -303,7 +321,15 @@ async function syncTaskToServer(task: Task) {
console.log('>>> syncTaskToServer: saved successfully')
return true
} else {
const errorPayload = await res.json().catch(() => null)
const rawBody = await res.text().catch(() => '')
let errorPayload: unknown = null
if (rawBody) {
try {
errorPayload = JSON.parse(rawBody)
} catch {
errorPayload = { rawBody }
}
}
console.error('>>> syncTaskToServer: failed with status', res.status, errorPayload)
return false
}