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
|
||||
// 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];
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user