import { useState } from "react"; import { MoreVertical, Pencil, Tag as TagIcon, Trash2, Unlink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { TagPicker } from "@/components/elements/TagPicker"; import { useTags, useTagMutations, useInvestmentAccountTags } from "@/hooks/useTags"; import { api } from "@/lib/api"; import { toast } from "sonner"; interface Props { accountId: string; accountLabel: string; isManual: boolean; onRenamed: () => void; onDeleted: () => void; } /** * Rename, tag, and delete/disconnect for an investment account, manual or * connected alike - renaming and tagging already work identically for both * server-side (PATCH /investment-accounts/{id} has no provider check), only * "Delete" vs "Disconnect" reads differently since a connected account has a * live sync to sever rather than data to simply remove. */ export function AccountMenu({ accountId, accountLabel, isManual, onRenamed, onDeleted }: Props) { const [renameOpen, setRenameOpen] = useState(false); const [tagsOpen, setTagsOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [newLabel, setNewLabel] = useState(accountLabel); const [saving, setSaving] = useState(false); const { data: allTags } = useTags(); const { create } = useTagMutations(); const { data: accountTags, setTags } = useInvestmentAccountTags(tagsOpen ? accountId : undefined); const handleRename = async (e: React.FormEvent) => { e.preventDefault(); const trimmed = newLabel.trim(); if (!trimmed || trimmed === accountLabel) { setRenameOpen(false); return; } setSaving(true); try { await api.investments.rename(accountId, trimmed); onRenamed(); setRenameOpen(false); toast.success("Account renamed"); } catch { toast.error("Failed to rename account"); } finally { setSaving(false); } }; const handleDelete = async () => { try { await api.investments.disconnect(accountId); onDeleted(); toast.success(isManual ? "Portfolio deleted" : "Account disconnected"); } catch { toast.error(isManual ? "Failed to delete portfolio" : "Failed to disconnect account"); } }; return ( <> {/* Stop propagation so clicking the menu on a card/link doesn't navigate */} e.preventDefault()} > Account actions {/* React portals bubble events through the REACT tree, not the DOM tree: even though this content renders into document.body (a Radix Portal), a click on one of its items still bubbles, as a React synthetic event, up through this component to the parent row's , navigating it before the dialog we opened ever shows. Stopping it here, once, covers every item below. */} e.stopPropagation()}> { setNewLabel(accountLabel); setRenameOpen(true); }} > Rename setTagsOpen(true)}> Tags setDeleteOpen(true)} className="text-destructive focus:text-destructive" > {isManual ? : } {isManual ? "Delete" : "Disconnect"} {/* Rename dialog */} e.stopPropagation()}> Rename account Name setNewLabel(e.target.value)} autoFocus required /> setRenameOpen(false)}> Cancel {saving ? "Saving…" : "Save"} {/* Tags dialog */} e.stopPropagation()}> Tag "{accountLabel}" t.id)} onChange={(ids) => setTags.mutate(ids)} onCreate={async (name) => { const created = await create.mutateAsync({ name }); return created.id; }} /> {/* Delete/disconnect confirmation */} e.stopPropagation()}> {isManual ? "Delete" : "Disconnect"} {accountLabel}? {isManual ? "This removes the portfolio and all its holdings. This action cannot be undone." : "This severs the connection to this account. Holdings already synced stay in your history."} Cancel {isManual ? "Delete" : "Disconnect"} > ); }