feat: task search and save feedback fixes
- Add task search with debounce (title + description filtering) - Add sonner toast library for notifications - Visual save feedback: spinner → green 'Saved!' → toast confirmation - Error handling with descriptive toast messages - Works across Kanban and Backlog views
This commit is contained in:
parent
caa0bf1893
commit
9453f88df4
13
package-lock.json
generated
13
package-lock.json
generated
@ -20,7 +20,8 @@
|
|||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"firebase": "^12.9.0",
|
"firebase": "^12.9.0",
|
||||||
"resend": "^6.9.2"
|
"resend": "^6.9.2",
|
||||||
|
"sonner": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
@ -9328,6 +9329,16 @@
|
|||||||
"simple-concat": "^1.0.0"
|
"simple-concat": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sonner": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
|||||||
@ -20,7 +20,8 @@
|
|||||||
"better-sqlite3": "^12.6.2",
|
"better-sqlite3": "^12.6.2",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"firebase": "^12.9.0",
|
"firebase": "^12.9.0",
|
||||||
"resend": "^6.9.2"
|
"resend": "^6.9.2",
|
||||||
|
"sonner": "^2.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
import { JetBrains_Mono, Lexend, Source_Sans_3 } from 'next/font/google'
|
import { JetBrains_Mono, Lexend, Source_Sans_3 } from 'next/font/google'
|
||||||
|
import { Toaster } from 'sonner'
|
||||||
import './globals.css'
|
import './globals.css'
|
||||||
|
|
||||||
const headingFont = Lexend({
|
const headingFont = Lexend({
|
||||||
@ -31,6 +32,16 @@ export default function RootLayout({
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={`${headingFont.variable} ${bodyFont.variable} ${monoFont.variable} antialiased`}>
|
<body className={`${headingFont.variable} ${bodyFont.variable} ${monoFont.variable} antialiased`}>
|
||||||
{children}
|
{children}
|
||||||
|
<Toaster
|
||||||
|
position="bottom-right"
|
||||||
|
toastOptions={{
|
||||||
|
style: {
|
||||||
|
background: '#1e293b',
|
||||||
|
color: '#f1f5f9',
|
||||||
|
border: '1px solid #334155',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, type ChangeEvent, type ReactNode } from "react"
|
import { useState, useEffect, useMemo, type ChangeEvent, type ReactNode } from "react"
|
||||||
|
import { useDebounce } from "@/hooks/useDebounce"
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@ -34,7 +35,7 @@ import {
|
|||||||
} from "@/lib/attachments"
|
} from "@/lib/attachments"
|
||||||
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 { BacklogView } from "@/components/BacklogView"
|
import { BacklogView } from "@/components/BacklogView"
|
||||||
import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download } from "lucide-react"
|
import { Plus, MessageSquare, Calendar, Trash2, X, LayoutGrid, ListTodo, GripVertical, Paperclip, Download, Search } from "lucide-react"
|
||||||
|
|
||||||
interface AssignableUser {
|
interface AssignableUser {
|
||||||
id: string
|
id: string
|
||||||
@ -379,6 +380,8 @@ export default function Home() {
|
|||||||
const [authReady, setAuthReady] = useState(false)
|
const [authReady, setAuthReady] = useState(false)
|
||||||
const [initialSyncComplete, setInitialSyncComplete] = useState(false)
|
const [initialSyncComplete, setInitialSyncComplete] = useState(false)
|
||||||
const [users, setUsers] = useState<AssignableUser[]>([])
|
const [users, setUsers] = useState<AssignableUser[]>([])
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
const debouncedSearchQuery = useDebounce(searchQuery, 300)
|
||||||
|
|
||||||
const getTags = (taskLike: { tags?: unknown }) => {
|
const getTags = (taskLike: { tags?: unknown }) => {
|
||||||
if (!Array.isArray(taskLike.tags)) return [] as string[]
|
if (!Array.isArray(taskLike.tags)) return [] as string[]
|
||||||
@ -589,7 +592,17 @@ export default function Home() {
|
|||||||
|
|
||||||
// Filter tasks to only show current sprint tasks in Kanban (from ALL projects)
|
// Filter tasks to only show current sprint tasks in Kanban (from ALL projects)
|
||||||
const sprintTasks = currentSprint
|
const sprintTasks = currentSprint
|
||||||
? tasks.filter((t) => t.sprintId === currentSprint.id)
|
? tasks.filter((t) => {
|
||||||
|
if (t.sprintId !== currentSprint.id) return false
|
||||||
|
// Apply search filter
|
||||||
|
if (debouncedSearchQuery.trim()) {
|
||||||
|
const query = debouncedSearchQuery.toLowerCase()
|
||||||
|
const matchesTitle = t.title.toLowerCase().includes(query)
|
||||||
|
const matchesDescription = t.description?.toLowerCase().includes(query) ?? false
|
||||||
|
return matchesTitle || matchesDescription
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
: []
|
: []
|
||||||
const activeKanbanTask = activeKanbanTaskId
|
const activeKanbanTask = activeKanbanTaskId
|
||||||
? sprintTasks.find((task) => task.id === activeKanbanTaskId)
|
? sprintTasks.find((task) => task.id === activeKanbanTaskId)
|
||||||
@ -893,6 +906,25 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Search Input */}
|
||||||
|
<div className="relative hidden sm:block">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search tasks..."
|
||||||
|
className="w-48 lg:w-64 pl-9 pr-8 py-1.5 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500 transition-colors"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-slate-700 rounded text-slate-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<span className="flex items-center gap-1 text-xs text-blue-400">
|
<span className="flex items-center gap-1 text-xs text-blue-400">
|
||||||
<svg className="animate-spin h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg className="animate-spin h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
@ -937,7 +969,15 @@ export default function Home() {
|
|||||||
{currentSprint ? currentSprint.name : "Work Board"}
|
{currentSprint ? currentSprint.name : "Work Board"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-slate-400">
|
<p className="text-sm text-slate-400">
|
||||||
{sprintTasks.length} tasks · {sprintTasks.filter((t) => t.status === "done").length} done
|
{debouncedSearchQuery.trim() ? (
|
||||||
|
<>
|
||||||
|
{sprintTasks.length} of {currentSprint ? tasks.filter((t) => t.sprintId === currentSprint.id).length : 0} tasks match "{debouncedSearchQuery}"
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{sprintTasks.length} tasks · {sprintTasks.filter((t) => t.status === "done").length} done
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -973,9 +1013,31 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Search - shown only on small screens */}
|
||||||
|
<div className="sm:hidden mb-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
placeholder="Search tasks..."
|
||||||
|
className="w-full pl-9 pr-8 py-2 bg-slate-800 border border-slate-700 rounded-lg text-sm text-white placeholder-slate-500 focus:outline-none focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
onClick={() => setSearchQuery("")}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-slate-700 rounded text-slate-400 hover:text-white"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* View Content */}
|
{/* View Content */}
|
||||||
{viewMode === 'backlog' ? (
|
{viewMode === 'backlog' ? (
|
||||||
<BacklogView />
|
<BacklogView searchQuery={debouncedSearchQuery} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Current Sprint Header */}
|
{/* Current Sprint Header */}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useMemo, useState, type ChangeEvent } from "react"
|
import { useEffect, useMemo, useState, type ChangeEvent } from "react"
|
||||||
import { useParams, useRouter } from "next/navigation"
|
import { useParams, useRouter } from "next/navigation"
|
||||||
import { ArrowLeft, Download, MessageSquare, Paperclip, Trash2, X } from "lucide-react"
|
import { ArrowLeft, Check, Download, Loader2, MessageSquare, Paperclip, Save, Trash2, X } from "lucide-react"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
@ -28,6 +28,7 @@ import {
|
|||||||
type TaskType,
|
type TaskType,
|
||||||
type UserProfile,
|
type UserProfile,
|
||||||
} from "@/stores/useTaskStore"
|
} from "@/stores/useTaskStore"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
interface AssignableUser {
|
interface AssignableUser {
|
||||||
id: string
|
id: string
|
||||||
@ -239,6 +240,7 @@ export default function TaskDetailPage() {
|
|||||||
const [newComment, setNewComment] = useState("")
|
const [newComment, setNewComment] = useState("")
|
||||||
const [editedTaskLabelInput, setEditedTaskLabelInput] = useState("")
|
const [editedTaskLabelInput, setEditedTaskLabelInput] = useState("")
|
||||||
const [isSaving, setIsSaving] = useState(false)
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const [saveSuccess, setSaveSuccess] = useState(false)
|
||||||
const [replyDrafts, setReplyDrafts] = useState<Record<string, string>>({})
|
const [replyDrafts, setReplyDrafts] = useState<Record<string, string>>({})
|
||||||
const [openReplyEditors, setOpenReplyEditors] = useState<Record<string, boolean>>({})
|
const [openReplyEditors, setOpenReplyEditors] = useState<Record<string, boolean>>({})
|
||||||
const [authReady, setAuthReady] = useState(false)
|
const [authReady, setAuthReady] = useState(false)
|
||||||
@ -448,14 +450,39 @@ export default function TaskDetailPage() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
if (!editedTask) return
|
if (!editedTask) return
|
||||||
setIsSaving(true)
|
setIsSaving(true)
|
||||||
updateTask(editedTask.id, {
|
setSaveSuccess(false)
|
||||||
...editedTask,
|
|
||||||
comments: getComments(editedTask.comments),
|
try {
|
||||||
})
|
const success = await updateTask(editedTask.id, {
|
||||||
setIsSaving(false)
|
...editedTask,
|
||||||
|
comments: getComments(editedTask.comments),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setSaveSuccess(true)
|
||||||
|
toast.success("Task saved successfully", {
|
||||||
|
description: "Your changes have been saved to the server.",
|
||||||
|
duration: 3000,
|
||||||
|
})
|
||||||
|
// Reset success state after 2 seconds
|
||||||
|
setTimeout(() => setSaveSuccess(false), 2000)
|
||||||
|
} else {
|
||||||
|
toast.error("Failed to save task", {
|
||||||
|
description: "Changes were saved locally but could not sync to the server. Please try again.",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Error saving task", {
|
||||||
|
description: error instanceof Error ? error.message : "An unexpected error occurred.",
|
||||||
|
duration: 5000,
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openAttachment = async (attachment: TaskAttachment) => {
|
const openAttachment = async (attachment: TaskAttachment) => {
|
||||||
@ -954,8 +981,27 @@ export default function TaskDetailPage() {
|
|||||||
<Button variant="ghost" onClick={() => router.push("/")}>
|
<Button variant="ghost" onClick={() => router.push("/")}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSave} disabled={isSaving}>
|
<Button
|
||||||
{isSaving ? "Saving..." : "Save Changes"}
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className={saveSuccess ? "bg-green-600 hover:bg-green-700" : undefined}
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : saveSuccess ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-4 h-4 mr-2" />
|
||||||
|
Saved!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="w-4 h-4 mr-2" />
|
||||||
|
Save Changes
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -228,7 +228,11 @@ function TaskSection({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BacklogView() {
|
interface BacklogViewProps {
|
||||||
|
searchQuery?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BacklogView({ searchQuery = "" }: BacklogViewProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [assignableUsers, setAssignableUsers] = useState<AssignableUser[]>([])
|
const [assignableUsers, setAssignableUsers] = useState<AssignableUser[]>([])
|
||||||
const {
|
const {
|
||||||
@ -270,6 +274,15 @@ export function BacklogView() {
|
|||||||
return assignableUsers.find((user) => user.id === task.assigneeId)?.avatarUrl || task.assigneeAvatarUrl
|
return assignableUsers.find((user) => user.id === task.assigneeId)?.avatarUrl || task.assigneeAvatarUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter tasks by search query
|
||||||
|
const matchesSearch = (task: Task): boolean => {
|
||||||
|
if (!searchQuery.trim()) return true
|
||||||
|
const query = searchQuery.toLowerCase()
|
||||||
|
const matchesTitle = task.title.toLowerCase().includes(query)
|
||||||
|
const matchesDescription = task.description?.toLowerCase().includes(query) ?? false
|
||||||
|
return matchesTitle || matchesDescription
|
||||||
|
}
|
||||||
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
const [openSections, setOpenSections] = useState<Record<string, boolean>>({
|
const [openSections, setOpenSections] = useState<Record<string, boolean>>({
|
||||||
current: true,
|
current: true,
|
||||||
@ -306,10 +319,10 @@ export function BacklogView() {
|
|||||||
|
|
||||||
// Get tasks by section
|
// Get tasks by section
|
||||||
const currentSprintTasks = currentSprint
|
const currentSprintTasks = currentSprint
|
||||||
? tasks.filter((t) => t.sprintId === currentSprint.id)
|
? tasks.filter((t) => t.sprintId === currentSprint.id && matchesSearch(t))
|
||||||
: []
|
: []
|
||||||
|
|
||||||
const backlogTasks = tasks.filter((t) => !t.sprintId)
|
const backlogTasks = tasks.filter((t) => !t.sprintId && matchesSearch(t))
|
||||||
|
|
||||||
// Get active task for drag overlay
|
// Get active task for drag overlay
|
||||||
const activeTask = activeId ? tasks.find((t) => t.id === activeId) : null
|
const activeTask = activeId ? tasks.find((t) => t.id === activeId) : null
|
||||||
@ -400,7 +413,7 @@ export function BacklogView() {
|
|||||||
{otherSprints
|
{otherSprints
|
||||||
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime())
|
.sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime())
|
||||||
.map((sprint) => {
|
.map((sprint) => {
|
||||||
const sprintTasks = tasks.filter((t) => t.sprintId === sprint.id)
|
const sprintTasks = tasks.filter((t) => t.sprintId === sprint.id && matchesSearch(t))
|
||||||
console.log(`Sprint ${sprint.name}: ${sprintTasks.length} tasks`, sprintTasks.map(t => t.title))
|
console.log(`Sprint ${sprint.name}: ${sprintTasks.length} tasks`, sprintTasks.map(t => t.title))
|
||||||
return (
|
return (
|
||||||
<SectionDropZone key={sprint.id} id={`sprint-${sprint.id}`}>
|
<SectionDropZone key={sprint.id} id={`sprint-${sprint.id}`}>
|
||||||
|
|||||||
17
src/hooks/useDebounce.ts
Normal file
17
src/hooks/useDebounce.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useDebounce<T>(value: T, delay: number): T {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
@ -108,7 +108,7 @@ interface TaskStore {
|
|||||||
|
|
||||||
// Task actions
|
// Task actions
|
||||||
addTask: (task: Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'comments'>) => void
|
addTask: (task: Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'comments'>) => void
|
||||||
updateTask: (id: string, updates: Partial<Task>) => void
|
updateTask: (id: string, updates: Partial<Task>) => Promise<boolean>
|
||||||
deleteTask: (id: string) => void
|
deleteTask: (id: string) => void
|
||||||
selectTask: (id: string | null) => void
|
selectTask: (id: string | null) => void
|
||||||
|
|
||||||
@ -753,8 +753,11 @@ export const useTaskStore = create<TaskStore>()(
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTask: (id, updates) => {
|
updateTask: async (id, updates) => {
|
||||||
console.log('updateTask called:', id, updates)
|
console.log('updateTask called:', id, updates)
|
||||||
|
let syncSuccess = false
|
||||||
|
let updatedTask: Task | undefined
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const actor = profileToCommentAuthor(state.currentUser)
|
const actor = profileToCommentAuthor(state.currentUser)
|
||||||
const newTasks = state.tasks.map((t) =>
|
const newTasks = state.tasks.map((t) =>
|
||||||
@ -771,14 +774,17 @@ export const useTaskStore = create<TaskStore>()(
|
|||||||
} as Task)
|
} as Task)
|
||||||
: t
|
: t
|
||||||
)
|
)
|
||||||
const updatedTask = newTasks.find(t => t.id === id)
|
updatedTask = newTasks.find(t => t.id === id)
|
||||||
console.log('updateTask: updated task:', updatedTask)
|
console.log('updateTask: updated task:', updatedTask)
|
||||||
// Sync individual task to server (lightweight)
|
|
||||||
if (updatedTask) {
|
|
||||||
syncTaskToServer(updatedTask)
|
|
||||||
}
|
|
||||||
return { tasks: newTasks }
|
return { tasks: newTasks }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Sync individual task to server (lightweight)
|
||||||
|
if (updatedTask) {
|
||||||
|
syncSuccess = await syncTaskToServer(updatedTask)
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncSuccess
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTask: (id) => {
|
deleteTask: (id) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user