Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Introduced new ContactAvatar and ContactAvatarPicker components for enhanced avatar management in contact views. - Updated ContactDetailView and ContactFormView to utilize the new avatar components, improving user experience when adding or editing contacts. - Enhanced ContactHoverCard and ContactRow components to display avatars, providing a more visually appealing interface. - Added loading and error states in ContactsListView for better user feedback during data fetching. - Implemented a new ContactsLoadState component to handle loading and error scenarios in the contacts list. - Updated package.json to include @formkit/auto-animate for improved UI animations.
137 lines
4.8 KiB
TypeScript
137 lines
4.8 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog"
|
|
import { Button } from "@/components/ui/button"
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
|
import { ContactAvatar } from "@/components/gmail/contacts/contact-avatar"
|
|
import { mergeManyContacts, pickContactMergePrimary } from "@/lib/contacts/merge-contacts"
|
|
import { fullContactDisplayName, type FullContact } from "@/lib/contacts/types"
|
|
import {
|
|
CONTACTS_MUTED_TEXT,
|
|
CONTACTS_PRIMARY_BTN_CLASS,
|
|
} from "@/lib/contacts-chrome-classes"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface ContactsBulkMergeDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
contacts: FullContact[]
|
|
onMerge: (primaryId: string) => void
|
|
isMerging?: boolean
|
|
}
|
|
|
|
function contactListLabel(contact: FullContact): string {
|
|
return (
|
|
fullContactDisplayName(contact) ||
|
|
contact.emails[0]?.value ||
|
|
contact.phones[0]?.value ||
|
|
"Contact sans nom"
|
|
)
|
|
}
|
|
|
|
export function ContactsBulkMergeDialog({
|
|
open,
|
|
onOpenChange,
|
|
contacts,
|
|
onMerge,
|
|
isMerging = false,
|
|
}: ContactsBulkMergeDialogProps) {
|
|
const defaultPrimary = useMemo(() => pickContactMergePrimary(contacts), [contacts])
|
|
const [primaryId, setPrimaryId] = useState(defaultPrimary?.id ?? "")
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
setPrimaryId(defaultPrimary?.id ?? contacts[0]?.id ?? "")
|
|
}, [open, defaultPrimary, contacts])
|
|
|
|
const preview = useMemo(
|
|
() => (primaryId ? mergeManyContacts(contacts, primaryId) : null),
|
|
[contacts, primaryId],
|
|
)
|
|
|
|
const canMerge = contacts.length >= 2 && !!primaryId && !!preview?.primary.etag
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Fusionner les contacts</DialogTitle>
|
|
<p className={cn("text-sm", CONTACTS_MUTED_TEXT)}>
|
|
{contacts.length} contacts sélectionnés → 1 contact. Les autres seront
|
|
supprimés après fusion.
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3">
|
|
<p className="text-sm font-medium text-foreground">Contact principal</p>
|
|
<RadioGroup value={primaryId} onValueChange={setPrimaryId} className="gap-2">
|
|
{contacts.map((contact) => {
|
|
const label = contactListLabel(contact)
|
|
return (
|
|
<label
|
|
key={contact.id}
|
|
className={cn(
|
|
"flex cursor-pointer items-center gap-3 rounded-lg border border-border px-3 py-2 transition-colors hover:bg-muted/50",
|
|
primaryId === contact.id && "border-primary bg-primary/5",
|
|
)}
|
|
>
|
|
<RadioGroupItem value={contact.id} id={`merge-primary-${contact.id}`} />
|
|
<ContactAvatar contact={contact} size="xs" />
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block truncate text-sm text-foreground">{label}</span>
|
|
{contact.emails[0]?.value && (
|
|
<span className="block truncate text-xs text-muted-foreground">
|
|
{contact.emails[0].value}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</label>
|
|
)
|
|
})}
|
|
</RadioGroup>
|
|
|
|
{preview && (
|
|
<div className={cn("rounded-lg border border-border px-3 py-2 text-sm", CONTACTS_MUTED_TEXT)}>
|
|
<p className="font-medium text-foreground">Résultat fusionné</p>
|
|
<ul className="mt-1.5 space-y-0.5">
|
|
<li>{preview.merged.emails.length} e-mail{preview.merged.emails.length > 1 ? "s" : ""}</li>
|
|
<li>{preview.merged.phones.length} téléphone{preview.merged.phones.length > 1 ? "s" : ""}</li>
|
|
{(preview.merged.labels?.length ?? 0) > 0 && (
|
|
<li>{preview.merged.labels!.length} libellé{(preview.merged.labels!.length > 1 ? "s" : "")}</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
|
|
{!preview?.primary.etag && (
|
|
<p className="text-sm text-destructive">
|
|
Version du contact principal inconnue. Rechargez la liste avant de fusionner.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
className={CONTACTS_PRIMARY_BTN_CLASS}
|
|
disabled={!canMerge || isMerging}
|
|
onClick={() => onMerge(primaryId)}
|
|
>
|
|
Fusionner
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|