From d27ec9152c44514af7d4bfe698a5b4f5d175232e Mon Sep 17 00:00:00 2001 From: Hardik Date: Fri, 29 May 2026 02:58:54 +0530 Subject: [PATCH] =?UTF-8?q?feat(admin):=20collapse=20row=20actions=20into?= =?UTF-8?q?=20=E2=8B=AF=20dropdown=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace per-row inline action buttons (Edit, Activate/Deactivate, Delete, Grant SuperUser) across all six admin tables with a Radix DropdownMenu triggered by a ⋯ button. Introduces RowActionsMenu/Item/DestructiveItem/ Separator primitives and a DeleteConfirmDialog modal. Each Edit*Button gains controlled open/onOpenChange props so the dialog can be driven from the table's per-row ActionsMenu sub-component. Toggle and delete actions use useTransition + router.refresh() directly in the table. Co-Authored-By: Claude Sonnet 4.6 --- .../(portal)/admin/accounts/account-form.tsx | 34 ++++---- .../admin/accounts/accounts-table.tsx | 65 +++++++++++--- .../admin/products/products-table.tsx | 54 +++++++++--- App/app/(portal)/admin/sites/site-form.tsx | 37 ++++---- App/app/(portal)/admin/sites/sites-table.tsx | 69 +++++++++++---- App/app/(portal)/admin/users/user-form.tsx | 34 ++++---- App/app/(portal)/admin/users/users-table.tsx | 87 +++++++++++++++---- .../(portal)/admin/vendors/vendor-form.tsx | 40 +++++---- .../(portal)/admin/vendors/vendors-table.tsx | 71 +++++++++++---- .../(portal)/admin/vessels/vessel-form.tsx | 32 +++---- .../(portal)/admin/vessels/vessels-table.tsx | 63 +++++++++++--- App/components/ui/delete-confirm-dialog.tsx | 71 +++++++++++++++ App/components/ui/row-actions-menu.tsx | 78 +++++++++++++++++ 13 files changed, 564 insertions(+), 171 deletions(-) create mode 100644 App/components/ui/delete-confirm-dialog.tsx create mode 100644 App/components/ui/row-actions-menu.tsx diff --git a/App/app/(portal)/admin/accounts/account-form.tsx b/App/app/(portal)/admin/accounts/account-form.tsx index 894e9da..a6ce069 100644 --- a/App/app/(portal)/admin/accounts/account-form.tsx +++ b/App/app/(portal)/admin/accounts/account-form.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { AdminDialog } from "@/components/ui/admin-dialog"; -import { createAccount, updateAccount, toggleAccountActive } from "./actions"; +import { createAccount, updateAccount } from "./actions"; type AccountRow = { id: string; @@ -79,13 +79,24 @@ export function AddAccountButton({ suggestedCode }: { suggestedCode?: string }) ); } -export function EditAccountButton({ account }: { account: AccountRow }) { +export function EditAccountButton({ + account, + open: controlledOpen, + onOpenChange, +}: { + account: AccountRow; + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) { const router = useRouter(); - const [open, setOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); const [pending, setPending] = useState(false); - const [toggling, setToggling] = useState(false); const [error, setError] = useState(""); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen; + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setPending(true); @@ -97,25 +108,14 @@ export function EditAccountButton({ account }: { account: AccountRow }) { else { setPending(false); setOpen(false); router.refresh(); } } - async function handleToggle() { - setToggling(true); - await toggleAccountActive(account.id); - router.refresh(); - setToggling(false); - } - return ( <> -
+ {!isControlled && ( - -
+ )} setOpen(false)}>
diff --git a/App/app/(portal)/admin/accounts/accounts-table.tsx b/App/app/(portal)/admin/accounts/accounts-table.tsx index 2704033..16b856b 100644 --- a/App/app/(portal)/admin/accounts/accounts-table.tsx +++ b/App/app/(portal)/admin/accounts/accounts-table.tsx @@ -1,10 +1,13 @@ "use client"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { useTableControls } from "@/components/ui/use-table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { AddAccountButton, EditAccountButton } from "./account-form"; -import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; -import { deleteAccount } from "./actions"; +import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; +import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; +import { deleteAccount, toggleAccountActive } from "./actions"; export type AccountRow = { id: string; @@ -16,6 +19,51 @@ export type AccountRow = { const CHIPS = ["Active", "Inactive"]; +function AccountActionsMenu({ account }: { account: AccountRow }) { + const router = useRouter(); + const [editOpen, setEditOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + function handleToggle() { + startTransition(async () => { + await toggleAccountActive(account.id); + router.refresh(); + }); + } + + return ( + <> + + setEditOpen(true)}>Edit + + {account.isActive ? "Deactivate" : "Activate"} + + + setDeleteOpen(true)}>Delete + + + + deleteAccount(account.id)} + /> + + ); +} + export function AccountsTable({ accounts, suggestedCode, @@ -66,7 +114,7 @@ export function AccountsTable({ toggleSort(k as keyof AccountRow)}>Name toggleSort(k as keyof AccountRow)}>Description toggleSort(k as keyof AccountRow)}>Status - + @@ -90,16 +138,7 @@ export function AccountsTable({ - - - - + ))} diff --git a/App/app/(portal)/admin/products/products-table.tsx b/App/app/(portal)/admin/products/products-table.tsx index 6e20625..c994b38 100644 --- a/App/app/(portal)/admin/products/products-table.tsx +++ b/App/app/(portal)/admin/products/products-table.tsx @@ -1,12 +1,15 @@ "use client"; import Link from "next/link"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { formatCurrency, formatDate } from "@/lib/utils"; import { useTableControls } from "@/components/ui/use-table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls"; -import { AddProductButton, ToggleProductButton } from "./product-form"; -import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; -import { deleteProduct } from "./actions"; +import { AddProductButton } from "./product-form"; +import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; +import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; +import { deleteProduct, toggleProductActive } from "./actions"; export type ProductRow = { id: string; @@ -22,6 +25,38 @@ export type ProductRow = { const CHIPS = ["Active", "Inactive"]; +function ProductActionsMenu({ product }: { product: ProductRow }) { + const router = useRouter(); + const [deleteOpen, setDeleteOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + function handleToggle() { + startTransition(async () => { + await toggleProductActive(product.id); + router.refresh(); + }); + } + + return ( + <> + + + {product.isActive ? "Deactivate" : "Activate"} + + + setDeleteOpen(true)}>Delete + + + deleteProduct(product.id)} + /> + + ); +} + export function ProductsTable({ products, canManage, @@ -78,7 +113,7 @@ export function ProductsTable({ toggleSort(k as keyof ProductRow)}>Last Vendor Updated toggleSort(k as keyof ProductRow)}>Status - {canManage && } + {canManage && } @@ -126,16 +161,7 @@ export function ProductsTable({ {canManage && ( - - - - + )} diff --git a/App/app/(portal)/admin/sites/site-form.tsx b/App/app/(portal)/admin/sites/site-form.tsx index f60e204..8df04ba 100644 --- a/App/app/(portal)/admin/sites/site-form.tsx +++ b/App/app/(portal)/admin/sites/site-form.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { AdminDialog } from "@/components/ui/admin-dialog"; -import { createSite, updateSite, toggleSiteActive } from "./actions"; +import { createSite, updateSite } from "./actions"; const INPUT = "w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"; @@ -74,12 +74,24 @@ export function AddSiteButton() { ); } -export function EditSiteButton({ site }: { site: SiteRow }) { +export function EditSiteButton({ + site, + open: controlledOpen, + onOpenChange, +}: { + site: SiteRow; + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) { const router = useRouter(); - const [open, setOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); const [pending, setPending] = useState(false); const [error, setError] = useState(""); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen; + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setPending(true); setError(""); const result = await updateSite(site.id, new FormData(e.currentTarget)); @@ -87,25 +99,18 @@ export function EditSiteButton({ site }: { site: SiteRow }) { else { setPending(false); setOpen(false); router.refresh(); } } - async function handleToggle() { - await toggleSiteActive(site.id); router.refresh(); - } - return ( <> - + {!isControlled && ( + + )} setOpen(false)} title={`Edit — ${site.name}`}> {error &&

{error}

} -
- -
- - -
+
+ +
diff --git a/App/app/(portal)/admin/sites/sites-table.tsx b/App/app/(portal)/admin/sites/sites-table.tsx index f70bdeb..750f7a4 100644 --- a/App/app/(portal)/admin/sites/sites-table.tsx +++ b/App/app/(portal)/admin/sites/sites-table.tsx @@ -1,11 +1,14 @@ "use client"; import Link from "next/link"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { useTableControls } from "@/components/ui/use-table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { AddSiteButton, EditSiteButton } from "./site-form"; -import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; -import { deleteSite } from "./actions"; +import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; +import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; +import { deleteSite, toggleSiteActive } from "./actions"; export type SiteRow = { id: string; @@ -21,6 +24,53 @@ export type SiteRow = { const CHIPS = ["Active", "Inactive"]; +function SiteActionsMenu({ site }: { site: SiteRow }) { + const router = useRouter(); + const [editOpen, setEditOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + function handleToggle() { + startTransition(async () => { + await toggleSiteActive(site.id); + router.refresh(); + }); + } + + return ( + <> + + setEditOpen(true)}>Edit + + {site.isActive ? "Deactivate" : "Activate"} + + + setDeleteOpen(true)}>Delete + + + + deleteSite(site.id)} + /> + + ); +} + export function SitesTable({ sites, canEdit, @@ -77,7 +127,7 @@ export function SitesTable({ Items tracked Location toggleSort(k as keyof SiteRow)}>Status - {canEdit && } + {canEdit && } @@ -119,18 +169,7 @@ export function SitesTable({ {canEdit && ( - - - - + )} diff --git a/App/app/(portal)/admin/users/user-form.tsx b/App/app/(portal)/admin/users/user-form.tsx index 038bbe3..e0ff58b 100644 --- a/App/app/(portal)/admin/users/user-form.tsx +++ b/App/app/(portal)/admin/users/user-form.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; import { AdminDialog } from "@/components/ui/admin-dialog"; -import { createUser, updateUser, toggleUserActive } from "./actions"; +import { createUser, updateUser } from "./actions"; type UserRow = { id: string; @@ -112,13 +112,24 @@ export function AddUserButton() { ); } -export function EditUserButton({ user }: { user: UserRow }) { +export function EditUserButton({ + user, + open: controlledOpen, + onOpenChange, +}: { + user: UserRow; + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) { const router = useRouter(); - const [open, setOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); const [pending, setPending] = useState(false); - const [toggling, setToggling] = useState(false); const [error, setError] = useState(""); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen; + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setPending(true); @@ -130,25 +141,14 @@ export function EditUserButton({ user }: { user: UserRow }) { else { setPending(false); setOpen(false); router.refresh(); } } - async function handleToggle() { - setToggling(true); - await toggleUserActive(user.id); - router.refresh(); - setToggling(false); - } - return ( <> -
+ {!isControlled && ( - -
+ )} setOpen(false)}>
diff --git a/App/app/(portal)/admin/users/users-table.tsx b/App/app/(portal)/admin/users/users-table.tsx index 63f551d..59a59ce 100644 --- a/App/app/(portal)/admin/users/users-table.tsx +++ b/App/app/(portal)/admin/users/users-table.tsx @@ -1,11 +1,15 @@ "use client"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { useTableControls } from "@/components/ui/use-table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { AddUserButton, EditUserButton } from "./user-form"; -import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; -import { GrantSuperUserButton } from "./grant-superuser-button"; -import { deleteUser } from "./actions"; +import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; +import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; +import { deleteUser, toggleUserActive } from "./actions"; +import { grantSuperUser } from "../superuser-requests/actions"; +import { ShieldCheck } from "lucide-react"; export type UserRow = { id: string; @@ -29,6 +33,66 @@ const ROLE_LABELS: Record = { const CHIPS = ["Manning", "Technical", "Accounts", "Manager", "Superuser", "Auditor", "Admin", "Active", "Inactive"]; +function UserActionsMenu({ user }: { user: UserRow }) { + const router = useRouter(); + const [editOpen, setEditOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + const [grantPending, startGrantTransition] = useTransition(); + + function handleToggle() { + startTransition(async () => { + await toggleUserActive(user.id); + router.refresh(); + }); + } + + function handleGrantSuperUser() { + startGrantTransition(async () => { + await grantSuperUser(user.id); + router.refresh(); + }); + } + + return ( + <> + + setEditOpen(true)}>Edit + + {user.isActive ? "Deactivate" : "Activate"} + + {user.role !== "SUPERUSER" && user.role !== "ADMIN" && ( + + + Grant SuperUser + + )} + + setDeleteOpen(true)}>Delete + + + + deleteUser(user.id)} + /> + + ); +} + export function UsersTable({ users }: { users: UserRow[] }) { const { search, setSearch, sortKey, sortDir, toggleSort, activeFilters, toggleFilter, filtered } = useTableControls({ @@ -76,7 +140,7 @@ export function UsersTable({ users }: { users: UserRow[] }) { toggleSort(k as keyof UserRow)}>Role toggleSort(k as keyof UserRow)}>Status Created - + @@ -108,20 +172,7 @@ export function UsersTable({ users }: { users: UserRow[] }) { {new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric" }).format(new Date(user.createdAt))} - - {user.role !== "SUPERUSER" && user.role !== "ADMIN" && ( - - )} - - - + ))} diff --git a/App/app/(portal)/admin/vendors/vendor-form.tsx b/App/app/(portal)/admin/vendors/vendor-form.tsx index a7fd30a..bed7474 100644 --- a/App/app/(portal)/admin/vendors/vendor-form.tsx +++ b/App/app/(portal)/admin/vendors/vendor-form.tsx @@ -280,12 +280,24 @@ export function AddVendorButton({ suggestedId }: { suggestedId?: string }) { ); } -export function EditVendorButton({ vendor }: { vendor: VendorRow }) { +export function EditVendorButton({ + vendor, + open: controlledOpen, + onOpenChange, +}: { + vendor: VendorRow; + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) { const router = useRouter(); - const [open, setOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); const [pending, setPending] = useState(false); const [error, setError] = useState(""); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen; + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setPending(true); setError(""); const fd = new FormData(e.currentTarget); @@ -295,28 +307,22 @@ export function EditVendorButton({ vendor }: { vendor: VendorRow }) { else { setPending(false); setOpen(false); router.refresh(); } } - async function handleToggle() { await toggleVendorActive(vendor.id); router.refresh(); } - return ( <> - + {!isControlled && ( + + )} setOpen(false)}> {error &&

{error}

} -
- + -
- - -
diff --git a/App/app/(portal)/admin/vendors/vendors-table.tsx b/App/app/(portal)/admin/vendors/vendors-table.tsx index ca43bdd..ffa7b19 100644 --- a/App/app/(portal)/admin/vendors/vendors-table.tsx +++ b/App/app/(portal)/admin/vendors/vendors-table.tsx @@ -1,11 +1,14 @@ "use client"; import Link from "next/link"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { useTableControls } from "@/components/ui/use-table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { AddVendorButton, EditVendorButton } from "./vendor-form"; -import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; -import { deleteVendor } from "./actions"; +import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; +import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; +import { deleteVendor, toggleVendorActive } from "./actions"; type ContactRow = { name: string; @@ -30,6 +33,54 @@ export type VendorRow = { const CHIPS = ["Verified", "Unverified", "Active", "Inactive"]; +function VendorActionsMenu({ vendor }: { vendor: VendorRow }) { + const router = useRouter(); + const [editOpen, setEditOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + function handleToggle() { + startTransition(async () => { + await toggleVendorActive(vendor.id); + router.refresh(); + }); + } + + return ( + <> + + setEditOpen(true)}>Edit + + {vendor.isActive ? "Deactivate" : "Activate"} + + + setDeleteOpen(true)}>Delete + + + + deleteVendor(vendor.id)} + /> + + ); +} + export function VendorsTable({ vendors, suggestedVendorId, @@ -86,7 +137,7 @@ export function VendorsTable({ Items toggleSort(k as keyof VendorRow)}>Verified toggleSort(k as keyof VendorRow)}>Status - + @@ -141,19 +192,7 @@ export function VendorsTable({ - - - - + ))} diff --git a/App/app/(portal)/admin/vessels/vessel-form.tsx b/App/app/(portal)/admin/vessels/vessel-form.tsx index ab69b97..cc2c807 100644 --- a/App/app/(portal)/admin/vessels/vessel-form.tsx +++ b/App/app/(portal)/admin/vessels/vessel-form.tsx @@ -71,13 +71,24 @@ export function AddVesselButton() { ); } -export function EditVesselButton({ vessel }: { vessel: VesselRow }) { +export function EditVesselButton({ + vessel, + open: controlledOpen, + onOpenChange, +}: { + vessel: VesselRow; + open?: boolean; + onOpenChange?: (v: boolean) => void; +}) { const router = useRouter(); - const [open, setOpen] = useState(false); + const [internalOpen, setInternalOpen] = useState(false); const [pending, setPending] = useState(false); - const [toggling, setToggling] = useState(false); const [error, setError] = useState(""); + const isControlled = controlledOpen !== undefined; + const open = isControlled ? controlledOpen : internalOpen; + const setOpen = isControlled ? (onOpenChange ?? (() => {})) : setInternalOpen; + async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setPending(true); @@ -89,25 +100,14 @@ export function EditVesselButton({ vessel }: { vessel: VesselRow }) { else { setPending(false); setOpen(false); router.refresh(); } } - async function handleToggle() { - setToggling(true); - await toggleVesselActive(vessel.id); - router.refresh(); - setToggling(false); - } - return ( <> -
+ {!isControlled && ( - -
+ )} setOpen(false)}>
diff --git a/App/app/(portal)/admin/vessels/vessels-table.tsx b/App/app/(portal)/admin/vessels/vessels-table.tsx index 406dcee..8a89389 100644 --- a/App/app/(portal)/admin/vessels/vessels-table.tsx +++ b/App/app/(portal)/admin/vessels/vessels-table.tsx @@ -1,10 +1,13 @@ "use client"; +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; import { useTableControls } from "@/components/ui/use-table-controls"; import { TableControls, SortableTh } from "@/components/ui/table-controls"; import { AddVesselButton, EditVesselButton } from "./vessel-form"; -import { ConfirmDeleteButton } from "@/components/ui/confirm-delete-button"; -import { deleteVessel } from "./actions"; +import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; +import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; +import { deleteVessel, toggleVesselActive } from "./actions"; export type VesselRow = { id: string; @@ -16,6 +19,50 @@ export type VesselRow = { const CHIPS = ["Active", "Inactive"]; +function VesselActionsMenu({ vessel }: { vessel: VesselRow }) { + const router = useRouter(); + const [editOpen, setEditOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + + function handleToggle() { + startTransition(async () => { + await toggleVesselActive(vessel.id); + router.refresh(); + }); + } + + return ( + <> + + setEditOpen(true)}>Edit + + {vessel.isActive ? "Deactivate" : "Activate"} + + + setDeleteOpen(true)}>Delete + + + + deleteVessel(vessel.id)} + /> + + ); +} + export function VesselsTable({ vessels }: { vessels: VesselRow[] }) { const { search, setSearch, sortKey, sortDir, toggleSort, activeFilters, toggleFilter, filtered } = useTableControls({ @@ -61,7 +108,7 @@ export function VesselsTable({ vessels }: { vessels: VesselRow[] }) { toggleSort(k as keyof VesselRow)}>Name toggleSort(k as keyof VesselRow)}>Site toggleSort(k as keyof VesselRow)}>Status - + @@ -87,15 +134,7 @@ export function VesselsTable({ vessels }: { vessels: VesselRow[] }) { - - - - + ))} diff --git a/App/components/ui/delete-confirm-dialog.tsx b/App/components/ui/delete-confirm-dialog.tsx new file mode 100644 index 0000000..cf0d3df --- /dev/null +++ b/App/components/ui/delete-confirm-dialog.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { AdminDialog } from "@/components/ui/admin-dialog"; + +type ActionResult = { ok: true } | { error: string }; + +interface Props { + open: boolean; + onOpenChange: (v: boolean) => void; + label: string; + onConfirm: () => Promise; +} + +export function DeleteConfirmDialog({ open, onOpenChange, label, onConfirm }: Props) { + const router = useRouter(); + const [error, setError] = useState(""); + const [isPending, startTransition] = useTransition(); + + function handleClose() { + if (isPending) return; + setError(""); + onOpenChange(false); + } + + function handleConfirm() { + setError(""); + startTransition(async () => { + const result = await onConfirm(); + if ("error" in result) { + setError(result.error); + } else { + onOpenChange(false); + router.refresh(); + } + }); + } + + return ( + +
+

+ This action cannot be undone. Are you sure you want to permanently delete{" "} + {label}? +

+ {error && ( +

{error}

+ )} +
+ + +
+
+
+ ); +} diff --git a/App/components/ui/row-actions-menu.tsx b/App/components/ui/row-actions-menu.tsx new file mode 100644 index 0000000..3a8ff94 --- /dev/null +++ b/App/components/ui/row-actions-menu.tsx @@ -0,0 +1,78 @@ +"use client"; + +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { MoreHorizontal } from "lucide-react"; + +export function RowActionsMenu({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + + + ); +} + +export function RowActionsItem({ + children, + onClick, + disabled, +}: { + children: React.ReactNode; + onClick?: () => void; + disabled?: boolean; +}) { + return ( + { + e.preventDefault(); + onClick?.(); + }} + disabled={disabled} + className="flex items-center gap-2 px-3 py-2 text-sm cursor-pointer hover:bg-neutral-50 transition-colors text-neutral-700 outline-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed" + > + {children} + + ); +} + +export function RowActionsDestructiveItem({ + children, + onClick, + disabled, +}: { + children: React.ReactNode; + onClick?: () => void; + disabled?: boolean; +}) { + return ( + { + e.preventDefault(); + onClick?.(); + }} + disabled={disabled} + className="flex items-center gap-2 px-3 py-2 text-sm cursor-pointer hover:bg-danger-50 transition-colors text-danger-600 outline-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed" + > + {children} + + ); +} + +export function RowActionsSeparator() { + return ; +}