feat: implement tag rename and delete functionality in UI and API
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Home, Archive, Settings, Tag, ChevronRight } from "lucide-react"
|
||||
import { Home, Archive, Settings, Tag, ChevronRight, Pencil, Trash2, MoreHorizontal } from "lucide-react"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -9,13 +9,22 @@ import {
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { Link, useLocation, useSearchParams } from "react-router-dom"
|
||||
import { Link, useLocation, useSearchParams, useNavigate } from "react-router-dom"
|
||||
import { SettingsDialog } from "@/components/settings-dialog"
|
||||
import { useState } from "react"
|
||||
import { useTags } from "@/hooks/use-notes"
|
||||
import { useTags, useDeleteTag, useRenameTag } from "@/hooks/use-notes"
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "sonner"
|
||||
|
||||
const items = [
|
||||
{
|
||||
@@ -30,6 +39,111 @@ const items = [
|
||||
},
|
||||
]
|
||||
|
||||
interface TagItemProps {
|
||||
tag: { id: string; name: string };
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
function TagItem({ tag, isActive }: TagItemProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editName, setEditName] = useState(tag.name);
|
||||
const { mutate: deleteTag } = useDeleteTag();
|
||||
const { mutate: renameTag } = useRenameTag();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleDelete = () => {
|
||||
if (confirm(`Delete tag "${tag.name}"? Notes will keep their content.`)) {
|
||||
deleteTag(tag.id, {
|
||||
onSuccess: () => {
|
||||
toast.success("Tag deleted");
|
||||
navigate("/");
|
||||
},
|
||||
onError: (err: any) => toast.error(err.message)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRename = () => {
|
||||
if (editName.trim() && editName.trim() !== tag.name) {
|
||||
renameTag({ id: tag.id, name: editName.trim() }, {
|
||||
onSuccess: () => {
|
||||
toast.success("Tag renamed");
|
||||
setIsEditing(false);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err.message);
|
||||
setEditName(tag.name);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setIsEditing(false);
|
||||
setEditName(tag.name);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
handleRename();
|
||||
} else if (e.key === "Escape") {
|
||||
setIsEditing(false);
|
||||
setEditName(tag.name);
|
||||
}
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<Input
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
onBlur={handleRename}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
className="h-7 text-xs"
|
||||
/>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarMenuItem className="group/tag">
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isActive}
|
||||
tooltip={tag.name}
|
||||
className="pr-0"
|
||||
>
|
||||
<Link to={`/?tag=${encodeURIComponent(tag.name)}`} className="flex items-center justify-between w-full">
|
||||
<Badge variant="secondary" className="text-xs px-1.5 py-0">
|
||||
{tag.name}
|
||||
</Badge>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={(e) => e.preventDefault()}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 opacity-0 group-hover/tag:opacity-100 transition-opacity"
|
||||
>
|
||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-32">
|
||||
<DropdownMenuItem onClick={(e) => { e.preventDefault(); setIsEditing(true); }}>
|
||||
<Pencil className="mr-2 h-3.5 w-3.5" />
|
||||
Rename
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={(e) => { e.preventDefault(); handleDelete(); }} className="text-destructive focus:text-destructive">
|
||||
<Trash2 className="mr-2 h-3.5 w-3.5" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppSidebar() {
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -86,19 +200,11 @@ export function AppSidebar() {
|
||||
<SidebarMenu>
|
||||
{tags && tags.length > 0 ? (
|
||||
tags.map((tag: { id: string; name: string }) => (
|
||||
<SidebarMenuItem key={tag.id}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={activeTag === tag.name}
|
||||
tooltip={tag.name}
|
||||
>
|
||||
<Link to={`/?tag=${encodeURIComponent(tag.name)}`}>
|
||||
<Badge variant="secondary" className="text-xs px-1.5 py-0">
|
||||
{tag.name}
|
||||
</Badge>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<TagItem
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
isActive={activeTag === tag.name}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
@@ -117,3 +223,4 @@ export function AppSidebar() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -177,3 +177,29 @@ export function useTags() {
|
||||
queryFn: () => api.get("/tags"),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteTag() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (id: string) => api.delete(`/tags/${id}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tags"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["notes"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useRenameTag() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ id, name }: { id: string; name: string }) =>
|
||||
api.patch(`/tags/${id}`, { name }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tags"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["notes"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user