"use client"; import { useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; import { api } from "@/lib/trpc"; import { Loader2, Trash2, UserPlus, Users } from "lucide-react"; import { ZBookmarkList } from "@karakeep/shared/types/lists"; export function ManageCollaboratorsModal({ open: userOpen, setOpen: userSetOpen, list, children, readOnly = false, }: { open?: boolean; setOpen?: (v: boolean) => void; list: ZBookmarkList; children?: React.ReactNode; readOnly?: boolean; }) { if ( (userOpen !== undefined && !userSetOpen) || (userOpen === undefined && userSetOpen) ) { throw new Error("You must provide both open and setOpen or neither"); } const [customOpen, customSetOpen] = useState(false); const [open, setOpen] = [ userOpen ?? customOpen, userSetOpen ?? customSetOpen, ]; const [newCollaboratorEmail, setNewCollaboratorEmail] = useState(""); const [newCollaboratorRole, setNewCollaboratorRole] = useState< "viewer" | "editor" >("viewer"); const { t } = useTranslation(); const utils = api.useUtils(); const invalidateListCaches = () => Promise.all([ utils.lists.getCollaborators.invalidate({ listId: list.id }), utils.lists.get.invalidate({ listId: list.id }), utils.lists.list.invalidate(), utils.bookmarks.getBookmarks.invalidate({ listId: list.id }), ]); // Fetch collaborators const { data: collaboratorsData, isLoading } = api.lists.getCollaborators.useQuery({ listId: list.id }, { enabled: open }); // Mutations const addCollaborator = api.lists.addCollaborator.useMutation({ onSuccess: async () => { toast({ description: t("lists.collaborators.invitation_sent"), }); setNewCollaboratorEmail(""); await invalidateListCaches(); }, onError: (error) => { toast({ variant: "destructive", description: error.message || t("lists.collaborators.failed_to_add"), }); }, }); const removeCollaborator = api.lists.removeCollaborator.useMutation({ onSuccess: async () => { toast({ description: t("lists.collaborators.removed"), }); await invalidateListCaches(); }, onError: (error) => { toast({ variant: "destructive", description: error.message || t("lists.collaborators.failed_to_remove"), }); }, }); const updateCollaboratorRole = api.lists.updateCollaboratorRole.useMutation({ onSuccess: async () => { toast({ description: t("lists.collaborators.role_updated"), }); await invalidateListCaches(); }, onError: (error) => { toast({ variant: "destructive", description: error.message || t("lists.collaborators.failed_to_update_role"), }); }, }); const revokeInvitation = api.lists.revokeInvitation.useMutation({ onSuccess: async () => { toast({ description: t("lists.collaborators.invitation_revoked"), }); await invalidateListCaches(); }, onError: (error) => { toast({ variant: "destructive", description: error.message || t("lists.collaborators.failed_to_revoke"), }); }, }); const handleAddCollaborator = () => { if (!newCollaboratorEmail.trim()) { toast({ variant: "destructive", description: t("lists.collaborators.please_enter_email"), }); return; } addCollaborator.mutate({ listId: list.id, email: newCollaboratorEmail, role: newCollaboratorRole, }); }; return ( { setOpen(s); }} > {children && {children}} {readOnly ? t("lists.collaborators.collaborators") : t("lists.collaborators.manage")} Beta {readOnly ? t("lists.collaborators.people_with_access") : t("lists.collaborators.add_or_remove")}
{/* Add Collaborator Section */} {!readOnly && (
setNewCollaboratorEmail(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { handleAddCollaborator(); } }} />

{t("lists.collaborators.viewer")}:{" "} {t("lists.collaborators.viewer_description")}
{t("lists.collaborators.editor")}:{" "} {t("lists.collaborators.editor_description")}

)} {/* Current Collaborators */}
{isLoading ? (
) : collaboratorsData ? (
{/* Show owner first */} {collaboratorsData.owner && (
{collaboratorsData.owner.name}
{collaboratorsData.owner.email && (
{collaboratorsData.owner.email}
)}
{t("lists.collaborators.owner")}
)} {/* Show collaborators */} {collaboratorsData.collaborators.length > 0 ? ( collaboratorsData.collaborators.map((collaborator) => (
{collaborator.user.name}
{collaborator.status === "pending" && ( {t("lists.collaborators.pending")} )} {collaborator.status === "declined" && ( {t("lists.collaborators.declined")} )}
{collaborator.user.email && (
{collaborator.user.email}
)}
{readOnly ? (
{collaborator.role}
) : collaborator.status !== "accepted" ? (
{collaborator.role}
) : (
)}
)) ) : !collaboratorsData.owner ? (
{readOnly ? t("lists.collaborators.no_collaborators_readonly") : t("lists.collaborators.no_collaborators")}
) : null}
) : (
{readOnly ? t("lists.collaborators.no_collaborators_readonly") : t("lists.collaborators.no_collaborators")}
)}
); }