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.
192 lines
5.4 KiB
TypeScript
192 lines
5.4 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 { Input } from "@/components/ui/input"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import type { FullContact } from "@/lib/contacts/types"
|
|
import {
|
|
CONTACT_BULK_EDIT_FIELDS,
|
|
collectBulkFieldSuggestions,
|
|
getContactBulkFieldValue,
|
|
type ContactBulkEditField,
|
|
} from "@/lib/contacts/bulk-edit-fields"
|
|
import {
|
|
CONTACTS_FIELD_CLASS,
|
|
CONTACTS_MUTED_TEXT,
|
|
CONTACTS_PRIMARY_BTN_CLASS,
|
|
} from "@/lib/contacts-chrome-classes"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
interface ContactsBulkEditDialogProps {
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
contacts: FullContact[]
|
|
onApply: (field: ContactBulkEditField, value: string) => void
|
|
isApplying?: boolean
|
|
}
|
|
|
|
export function ContactsBulkEditDialog({
|
|
open,
|
|
onOpenChange,
|
|
contacts,
|
|
onApply,
|
|
isApplying = false,
|
|
}: ContactsBulkEditDialogProps) {
|
|
const [field, setField] = useState<ContactBulkEditField>("company")
|
|
const [value, setValue] = useState("")
|
|
|
|
const suggestions = useMemo(
|
|
() => collectBulkFieldSuggestions(contacts, field),
|
|
[contacts, field],
|
|
)
|
|
|
|
const mixedValues = useMemo(() => {
|
|
const values = new Set(
|
|
contacts.map((c) => getContactBulkFieldValue(c, field)).filter(Boolean),
|
|
)
|
|
return values.size > 1
|
|
}, [contacts, field])
|
|
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setField("company")
|
|
setValue("")
|
|
return
|
|
}
|
|
if (suggestions.length === 1) {
|
|
setValue(suggestions[0]!)
|
|
} else {
|
|
setValue("")
|
|
}
|
|
}, [open, field, suggestions])
|
|
|
|
const isNotes = field === "notes"
|
|
const canApply = contacts.length > 0
|
|
|
|
function handleApply() {
|
|
if (!canApply) return
|
|
onApply(field, value)
|
|
onOpenChange(false)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="sm:max-w-md">
|
|
<DialogHeader>
|
|
<DialogTitle>Édition de masse</DialogTitle>
|
|
<p className={cn("text-sm", CONTACTS_MUTED_TEXT)}>
|
|
{contacts.length} contact{contacts.length > 1 ? "s" : ""} sélectionné
|
|
{contacts.length > 1 ? "s" : ""}
|
|
</p>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground">
|
|
Propriété
|
|
</label>
|
|
<Select
|
|
value={field}
|
|
onValueChange={(v) => setField(v as ContactBulkEditField)}
|
|
>
|
|
<SelectTrigger className={CONTACTS_FIELD_CLASS}>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{CONTACT_BULK_EDIT_FIELDS.map((f) => (
|
|
<SelectItem key={f.id} value={f.id}>
|
|
{f.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground">
|
|
Nouvelle valeur
|
|
</label>
|
|
{isNotes ? (
|
|
<Textarea
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
placeholder={
|
|
mixedValues ? "Valeurs mixtes dans la sélection…" : "Notes…"
|
|
}
|
|
className={cn(CONTACTS_FIELD_CLASS, "min-h-24 resize-y")}
|
|
/>
|
|
) : (
|
|
<Input
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
placeholder={
|
|
mixedValues
|
|
? "Valeurs mixtes dans la sélection…"
|
|
: `Nouvelle valeur…`
|
|
}
|
|
className={CONTACTS_FIELD_CLASS}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{suggestions.length > 0 && (
|
|
<div className="space-y-2">
|
|
<p className={cn("text-xs", CONTACTS_MUTED_TEXT)}>
|
|
Valeurs présentes dans la sélection
|
|
</p>
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{suggestions.map((suggestion) => (
|
|
<button
|
|
key={suggestion}
|
|
type="button"
|
|
onClick={() => setValue(suggestion)}
|
|
className={cn(
|
|
"max-w-full truncate rounded-full border border-border px-2.5 py-1 text-xs text-foreground transition-colors hover:bg-muted",
|
|
value === suggestion && "border-primary bg-primary/10",
|
|
)}
|
|
>
|
|
{suggestion}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => onOpenChange(false)}
|
|
>
|
|
Annuler
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
className={CONTACTS_PRIMARY_BTN_CLASS}
|
|
disabled={!canApply || isApplying}
|
|
onClick={handleApply}
|
|
>
|
|
Appliquer
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|