fixed
Signed-off-by: Max <ai-agent@topdoglabs.com>
This commit is contained in:
parent
b2b2beef2d
commit
ab8cc0a6a1
@ -7,7 +7,7 @@ export const runtime = "nodejs";
|
|||||||
// Sprint dates are stored as SQL DATE values (YYYY-MM-DD). We accept either
|
// 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.
|
// date-only or ISO datetime inputs and normalize to the date prefix.
|
||||||
const DATE_PREFIX_PATTERN = /^(\d{4}-\d{2}-\d{2})/;
|
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;
|
const SPRINT_STATUSES = ["planning", "active", "completed"] as const;
|
||||||
type SprintStatus = (typeof SPRINT_STATUSES)[number];
|
type SprintStatus = (typeof SPRINT_STATUSES)[number];
|
||||||
|
|
||||||
|
|||||||
@ -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_STATUSES: Task["status"][] = ["open", "todo", "blocked", "in-progress", "review", "validate", "archived", "canceled", "done"];
|
||||||
const TASK_PRIORITIES: Task["priority"][] = ["low", "medium", "high", "urgent"];
|
const TASK_PRIORITIES: Task["priority"][] = ["low", "medium", "high", "urgent"];
|
||||||
const SPRINT_STATUSES: Sprint["status"][] = ["planning", "active", "completed"];
|
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
|
// Optimized field selection - only fetch fields needed for board display
|
||||||
// Full task details (description, comments, attachments) fetched lazily
|
// Full task details (description, comments, attachments) fetched lazily
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import {
|
|||||||
import { useTaskStore, Task, TaskType, TaskStatus, Priority, TaskAttachment, type CommentAuthor } from "@/stores/useTaskStore"
|
import { useTaskStore, Task, TaskType, TaskStatus, Priority, TaskAttachment, type CommentAuthor } from "@/stores/useTaskStore"
|
||||||
import { BacklogSkeleton, SearchSkeleton } from "@/components/LoadingSkeletons"
|
import { BacklogSkeleton, SearchSkeleton } from "@/components/LoadingSkeletons"
|
||||||
import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download, Search, Archive } from "lucide-react"
|
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
|
// Dynamic imports for heavy view components - only load when needed
|
||||||
const BacklogView = dynamic(() => import("@/components/BacklogView").then(mod => mod.BacklogView), {
|
const BacklogView = dynamic(() => import("@/components/BacklogView").then(mod => mod.BacklogView), {
|
||||||
@ -830,7 +831,14 @@ export default function Home() {
|
|||||||
if (newTask.title?.trim()) {
|
if (newTask.title?.trim()) {
|
||||||
// If a specific sprint is selected, use that sprint's project
|
// If a specific sprint is selected, use that sprint's project
|
||||||
const selectedSprint = newTask.sprintId ? sprints.find(s => s.id === newTask.sprintId) : null
|
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'> = {
|
const taskToCreate: Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'comments'> = {
|
||||||
title: newTask.title.trim(),
|
title: newTask.title.trim(),
|
||||||
|
|||||||
@ -227,6 +227,7 @@ export default function TaskDetailPage() {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
tasks,
|
tasks,
|
||||||
|
projects,
|
||||||
sprints,
|
sprints,
|
||||||
currentUser,
|
currentUser,
|
||||||
updateTask,
|
updateTask,
|
||||||
@ -355,9 +356,10 @@ export default function TaskDetailPage() {
|
|||||||
const sortedSprints = useMemo(
|
const sortedSprints = useMemo(
|
||||||
() =>
|
() =>
|
||||||
sprints
|
sprints
|
||||||
|
.filter((sprint) => sprint.projectId === editedTask?.projectId)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()),
|
.sort((a, b) => parseSprintStart(a.startDate).getTime() - parseSprintStart(b.startDate).getTime()),
|
||||||
[sprints]
|
[sprints, editedTask?.projectId]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleAttachmentUpload = async (event: ChangeEvent<HTMLInputElement>) => {
|
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 () => {
|
const handleSave = async () => {
|
||||||
if (!editedTask) return
|
if (!editedTask) return
|
||||||
|
if (!editedTask.projectId) {
|
||||||
|
toast.error("Project is required", {
|
||||||
|
description: "Select a project before saving this task.",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
setSaveSuccess(false)
|
setSaveSuccess(false)
|
||||||
|
|
||||||
@ -777,6 +799,22 @@ export default function TaskDetailPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<Label className="text-slate-400">Sprint</Label>
|
<Label className="text-slate-400">Sprint</Label>
|
||||||
<select
|
<select
|
||||||
|
|||||||
@ -370,7 +370,7 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleCreateSprint = () => {
|
const handleCreateSprint = () => {
|
||||||
if (!newSprint.name) return
|
if (!newSprint.name || !selectedProjectId) return
|
||||||
|
|
||||||
addSprint({
|
addSprint({
|
||||||
name: newSprint.name,
|
name: newSprint.name,
|
||||||
@ -378,7 +378,7 @@ export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
|
|||||||
startDate: newSprint.startDate || toLocalDateInputValue(),
|
startDate: newSprint.startDate || toLocalDateInputValue(),
|
||||||
endDate: newSprint.endDate || toLocalDateInputValue(),
|
endDate: newSprint.endDate || toLocalDateInputValue(),
|
||||||
status: "planning",
|
status: "planning",
|
||||||
projectId: selectedProjectId || "2",
|
projectId: selectedProjectId,
|
||||||
})
|
})
|
||||||
|
|
||||||
setIsCreatingSprint(false)
|
setIsCreatingSprint(false)
|
||||||
|
|||||||
@ -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 TaskStatus = 'open' | 'todo' | 'blocked' | 'in-progress' | 'review' | 'validate' | 'archived' | 'canceled' | 'done'
|
||||||
export type Priority = 'low' | 'medium' | 'high' | 'urgent'
|
export type Priority = 'low' | 'medium' | 'high' | 'urgent'
|
||||||
export type SprintStatus = 'planning' | 'active' | 'completed'
|
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 {
|
export interface Sprint {
|
||||||
id: string
|
id: string
|
||||||
@ -293,6 +294,23 @@ async function requestApi(path: string, init: RequestInit): Promise<unknown> {
|
|||||||
// Helper to sync a single task to server (lightweight)
|
// Helper to sync a single task to server (lightweight)
|
||||||
async function syncTaskToServer(task: Task) {
|
async function syncTaskToServer(task: Task) {
|
||||||
console.log('>>> syncTaskToServer: saving task', task.id, task.title)
|
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 {
|
try {
|
||||||
const res = await fetch('/api/tasks', {
|
const res = await fetch('/api/tasks', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -303,7 +321,15 @@ async function syncTaskToServer(task: Task) {
|
|||||||
console.log('>>> syncTaskToServer: saved successfully')
|
console.log('>>> syncTaskToServer: saved successfully')
|
||||||
return true
|
return true
|
||||||
} else {
|
} 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)
|
console.error('>>> syncTaskToServer: failed with status', res.status, errorPayload)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user