diff --git a/src/app/components/ConfirmDialog.tsx b/src/app/components/ConfirmDialog.tsx new file mode 100644 index 00000000..b8498bc3 --- /dev/null +++ b/src/app/components/ConfirmDialog.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/app/shadcn_components/ui/dialog"; +import { Button } from "@/app/shadcn_components/ui/button"; +import type { ReactNode } from "react"; + +// Controlled confirmation dialog. Pass `open` + `onOpenChange` to control +// visibility (so the parent can stash any context needed by onConfirm), +// and `onConfirm` is called when the user clicks the destructive action. +export function ConfirmDialog({ + open, + onOpenChange, + title, + description, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + destructive = false, + isPending = false, + onConfirm, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + title: string; + description: ReactNode; + confirmLabel?: string; + cancelLabel?: string; + destructive?: boolean; + isPending?: boolean; + onConfirm: () => void; +}) { + return ( + + + + {title} + {description} + + + + + + + + ); +} diff --git a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx index a5f61cf3..c6a07a5f 100644 --- a/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx +++ b/src/app/portfolio/[slug]/(portfolio)/settings/UsersPermissionsCard.tsx @@ -27,6 +27,8 @@ import { fetchCollaborators, type CollaboratorsResponse, } from "./collaboratorsClient"; +import { ConfirmDialog } from "@/app/components/ConfirmDialog"; +import { useToast } from "@/app/hooks/use-toast"; type PendingInvitation = { invitationId: string; @@ -122,7 +124,12 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("read"); const [inviteName, setInviteName] = useState(""); + const [pendingRemoval, setPendingRemoval] = + useState<{ portfolioUserId: string; email: string } | null>(null); + const [pendingRevoke, setPendingRevoke] = + useState<{ invitationId: string; email: string } | null>(null); + const { toast } = useToast(); const queryClient = useQueryClient(); const usersKey = COLLABORATORS_QUERY_KEY(portfolioId); @@ -209,13 +216,22 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { role: Role; name: string; }) => invitePortfolioUser(portfolioId, email, role, name), - onSuccess: () => { + onSuccess: (_data, vars) => { invalidateBoth(); setInviteEmail(""); setInviteName(""); + toast({ + title: "Invitation sent", + description: `We've emailed ${vars.email} an invitation to this portfolio.`, + }); }, onError: (err) => { console.error("Invite failed:", err); + toast({ + title: "Couldn't send invitation", + description: err instanceof Error ? err.message : "Please try again.", + variant: "destructive", + }); }, }); @@ -239,14 +255,29 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { ); return { previous }; }, + onSuccess: (_data, _portfolioUserId) => { + const email = pendingRemoval?.email; + toast({ + title: "User removed", + description: email + ? `${email} no longer has access to this portfolio.` + : "User no longer has access to this portfolio.", + }); + }, onError: (err, _vars, context) => { if (context?.previous) { queryClient.setQueryData(usersKey, context.previous); } console.error("Failed to remove user:", err); + toast({ + title: "Couldn't remove user", + description: err instanceof Error ? err.message : "Please try again.", + variant: "destructive", + }); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: usersKey }); + setPendingRemoval(null); }, }); @@ -263,14 +294,29 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { ); return { previous }; }, + onSuccess: () => { + const email = pendingRevoke?.email; + toast({ + title: "Invitation revoked", + description: email + ? `${email}'s invitation has been cancelled.` + : "The invitation has been cancelled.", + }); + }, onError: (err, _vars, context) => { if (context?.previous) { queryClient.setQueryData(invitationsKey, context.previous); } console.error("Failed to revoke invitation:", err); + toast({ + title: "Couldn't revoke invitation", + description: err instanceof Error ? err.message : "Please try again.", + variant: "destructive", + }); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: invitationsKey }); + setPendingRevoke(null); }, }); @@ -286,14 +332,22 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) { changeRoleMutation.mutate({ portfolioUserId, role }); } - function onRemove(portfolioUserId: string) { - if (!confirm("Remove this user from the portfolio?")) return; - removeUserMutation.mutate(portfolioUserId); + function onRemove(portfolioUserId: string, email: string) { + setPendingRemoval({ portfolioUserId, email }); } - function onRevokeInvitation(invitationId: string) { - if (!confirm("Revoke this pending invitation?")) return; - revokeInvitationMutation.mutate(invitationId); + function onRevokeInvitation(invitationId: string, email: string) { + setPendingRevoke({ invitationId, email }); + } + + function confirmRemove() { + if (!pendingRemoval) return; + removeUserMutation.mutate(pendingRemoval.portfolioUserId); + } + + function confirmRevoke() { + if (!pendingRevoke) return; + revokeInvitationMutation.mutate(pendingRevoke.invitationId); } return ( @@ -419,7 +473,9 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {