- Updated .env.example to include configuration for OnlyOffice Document Server. - Modified the workspace configuration to remove the drive-suite path. - Adjusted TypeScript environment imports for consistency. - Enhanced Next.js configuration to disable canvas in Webpack. - Updated package.json to include new dependencies for OnlyOffice and PDF.js. - Added global styles for OnlyOffice theme integration in the CSS. - Created new layout and page components for the Drive feature, including public sharing and editing functionalities. - Updated metadata handling across various layouts to reflect the new app structure.
341 lines
10 KiB
TypeScript
341 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { useDeferredValue, useMemo, useState } from "react"
|
|
|
|
import {
|
|
Users,
|
|
Clock,
|
|
UserPlus,
|
|
Ban,
|
|
EyeOff,
|
|
Merge,
|
|
Upload,
|
|
Trash2,
|
|
Plus,
|
|
Tag,
|
|
Menu,
|
|
ChevronDown,
|
|
} from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu"
|
|
import { cn } from "@/lib/utils"
|
|
import {
|
|
CONTACTS_CREATE_BTN_CLASS,
|
|
CONTACTS_FIELD_CLASS,
|
|
CONTACTS_HEADING_TEXT,
|
|
CONTACTS_MUTED_TEXT,
|
|
CONTACTS_NAV_ACTIVE_CLASS,
|
|
CONTACTS_NAV_ICON_MUTED,
|
|
CONTACTS_NAV_ITEM_CLASS,
|
|
CONTACTS_CREATE_BTN_LABEL_CLASS,
|
|
CONTACTS_SIDEBAR_CLASS,
|
|
} from "@/lib/contacts-chrome-classes"
|
|
import { CONTACTS_MENU_SURFACE_CLASS } from "@/lib/contacts-chrome-classes"
|
|
import { useContactsList } from "@/lib/contacts/use-contacts-list"
|
|
import { useContactsStore } from "@/lib/contacts/contacts-store"
|
|
import { useDiscoveryCounts, useVisibleEnrichmentSuggestions } from "@/lib/api/hooks/use-contact-discovery"
|
|
import { findDuplicatePairs } from "@/lib/contacts/duplicate-detection"
|
|
import { useNavStore } from "@/lib/stores/nav-store"
|
|
import { ContactsPanelLogo } from "@/components/gmail/contacts/contacts-panel-logo"
|
|
import type { ContactsPageView } from "./contacts-app-shell"
|
|
|
|
interface ContactsSidebarProps {
|
|
open: boolean
|
|
overlay: boolean
|
|
onToggle: () => void
|
|
onClose: () => void
|
|
currentView: ContactsPageView
|
|
activeLabelId?: string | null
|
|
onNavigate: (view: ContactsPageView) => void
|
|
onHome?: () => void
|
|
onCreateContact: () => void
|
|
onBulkCreate?: () => void
|
|
onSelectLabel?: (id: string) => void
|
|
}
|
|
|
|
export function ContactsSidebar({
|
|
open,
|
|
overlay,
|
|
onToggle,
|
|
onClose,
|
|
currentView,
|
|
activeLabelId,
|
|
onNavigate,
|
|
onHome,
|
|
onCreateContact,
|
|
onBulkCreate,
|
|
onSelectLabel,
|
|
}: ContactsSidebarProps) {
|
|
const { contacts } = useContactsList()
|
|
const deferredContacts = useDeferredValue(contacts)
|
|
const ignoredMergePairs = useContactsStore((s) => s.ignoredMergePairs)
|
|
const { data: discoveryCounts } = useDiscoveryCounts()
|
|
const mergeSuggestionCount = useMemo(() => {
|
|
if (deferredContacts.length < 2) return 0
|
|
return findDuplicatePairs(deferredContacts, new Set(ignoredMergePairs)).length
|
|
}, [deferredContacts, ignoredMergePairs])
|
|
const { suggestions: coordinateSuggestions } = useVisibleEnrichmentSuggestions()
|
|
const otherContactsCount = discoveryCounts?.other_contacts ?? 0
|
|
const ignoredCount = discoveryCounts?.ignored ?? 0
|
|
const blockedCount = discoveryCounts?.blocked ?? 0
|
|
const coordinatesCount = coordinateSuggestions.length
|
|
const mergeBadgeCount = mergeSuggestionCount + coordinatesCount
|
|
const labelRows = useNavStore((s) => s.labelRows)
|
|
const addLabelRowFromSidebar = useNavStore((s) => s.addLabelRowFromSidebar)
|
|
const [labelInput, setLabelInput] = useState("")
|
|
const [showLabelInput, setShowLabelInput] = useState(false)
|
|
|
|
const labelsByContactCount = useMemo(() => {
|
|
return labelRows
|
|
.filter((r) => r.enabled !== false)
|
|
.map((label) => ({
|
|
label,
|
|
count: contacts.filter((c) => c.labels?.includes(label.id)).length,
|
|
}))
|
|
.sort(
|
|
(a, b) =>
|
|
b.count - a.count ||
|
|
a.label.label.localeCompare(b.label.label, "fr")
|
|
)
|
|
}, [labelRows, contacts])
|
|
|
|
function handleAddLabel() {
|
|
const trimmed = labelInput.trim()
|
|
if (trimmed) {
|
|
addLabelRowFromSidebar(trimmed)
|
|
setLabelInput("")
|
|
setShowLabelInput(false)
|
|
}
|
|
}
|
|
|
|
function handleMenuClick() {
|
|
if (overlay && open) {
|
|
onClose()
|
|
} else {
|
|
onToggle()
|
|
}
|
|
}
|
|
|
|
if (!overlay && !open) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<aside
|
|
className={cn(
|
|
CONTACTS_SIDEBAR_CLASS,
|
|
overlay
|
|
? cn(
|
|
"fixed inset-y-0 left-0 z-50 shadow-xl",
|
|
open ? "translate-x-0" : "-translate-x-full pointer-events-none"
|
|
)
|
|
: "relative"
|
|
)}
|
|
aria-hidden={overlay && !open}
|
|
>
|
|
<div className="flex h-16 items-center gap-2 px-4">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-10 w-10 rounded-full text-muted-foreground hover:bg-accent"
|
|
onClick={handleMenuClick}
|
|
aria-label={open ? "Fermer le menu" : "Ouvrir le menu"}
|
|
>
|
|
<Menu className="h-5 w-5" />
|
|
</Button>
|
|
<ContactsPanelLogo
|
|
onClick={onHome ?? (() => onNavigate("contacts"))}
|
|
titleClassName={cn("text-[22px] font-normal", CONTACTS_HEADING_TEXT)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Create button */}
|
|
<div className="px-3 pb-3">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className={CONTACTS_CREATE_BTN_CLASS}
|
|
>
|
|
<Plus className="h-5 w-5 text-primary" />
|
|
<span className={CONTACTS_CREATE_BTN_LABEL_CLASS}>Créer un contact</span>
|
|
<ChevronDown className={cn("h-4 w-4", CONTACTS_NAV_ICON_MUTED)} />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="start"
|
|
data-contacts-menu-surface
|
|
className={cn("w-56", CONTACTS_MENU_SURFACE_CLASS)}
|
|
>
|
|
<DropdownMenuItem onClick={onCreateContact}>
|
|
<UserPlus className="mr-2 h-4 w-4" />
|
|
Créer un contact
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={onBulkCreate}>
|
|
<Users className="mr-2 h-4 w-4" />
|
|
Créer plusieurs contacts
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
|
|
<nav className="flex-1 overflow-y-auto px-2">
|
|
<NavItem
|
|
icon={<Users className="h-5 w-5" />}
|
|
label="Contacts"
|
|
count={contacts.length}
|
|
active={currentView === "contacts"}
|
|
onClick={() => onNavigate("contacts")}
|
|
/>
|
|
<NavItem
|
|
icon={<Clock className="h-5 w-5" />}
|
|
label="Fréquents"
|
|
active={currentView === "frequent"}
|
|
onClick={() => onNavigate("frequent")}
|
|
/>
|
|
<NavItem
|
|
icon={<UserPlus className="h-5 w-5" />}
|
|
label="Autres contacts"
|
|
badge={otherContactsCount > 0 ? otherContactsCount : undefined}
|
|
active={currentView === "other"}
|
|
onClick={() => onNavigate("other")}
|
|
/>
|
|
<NavItem
|
|
icon={<Ban className="h-5 w-5" />}
|
|
label="Bloqués"
|
|
count={blockedCount > 0 ? blockedCount : undefined}
|
|
active={currentView === "blocked"}
|
|
onClick={() => onNavigate("blocked")}
|
|
/>
|
|
<NavItem
|
|
icon={<EyeOff className="h-5 w-5" />}
|
|
label="Ignorés"
|
|
count={ignoredCount > 0 ? ignoredCount : undefined}
|
|
active={currentView === "ignored"}
|
|
onClick={() => onNavigate("ignored")}
|
|
/>
|
|
|
|
<div className="my-2 border-t border-border" />
|
|
|
|
<p className={cn("px-3 py-2 text-xs font-medium", CONTACTS_MUTED_TEXT)}>Corriger et gérer</p>
|
|
|
|
<NavItem
|
|
icon={<Merge className="h-5 w-5" />}
|
|
label="Fusionner et corriger"
|
|
badge={mergeBadgeCount > 0 ? mergeBadgeCount : undefined}
|
|
active={currentView === "merge"}
|
|
onClick={() => onNavigate("merge")}
|
|
/>
|
|
<NavItem
|
|
icon={<Upload className="h-5 w-5" />}
|
|
label="Importer"
|
|
active={currentView === "import"}
|
|
onClick={() => onNavigate("import")}
|
|
/>
|
|
<NavItem
|
|
icon={<Trash2 className="h-5 w-5" />}
|
|
label="Corbeille"
|
|
active={currentView === "trash"}
|
|
onClick={() => onNavigate("trash")}
|
|
/>
|
|
|
|
<div className="my-2 border-t border-border" />
|
|
|
|
<div className="flex items-center gap-3 px-3 py-2">
|
|
<p className={cn("min-w-0 flex-1 text-xs font-medium", CONTACTS_MUTED_TEXT)}>
|
|
Libellés
|
|
</p>
|
|
<div className="flex w-6 shrink-0 justify-center">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowLabelInput(true)}
|
|
className="rounded-full p-1 text-muted-foreground hover:bg-accent"
|
|
aria-label="Ajouter un libellé"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{showLabelInput && (
|
|
<div className="flex items-center gap-1 px-3 pb-2">
|
|
<input
|
|
type="text"
|
|
value={labelInput}
|
|
onChange={(e) => setLabelInput(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && handleAddLabel()}
|
|
placeholder="Nom du libellé"
|
|
className={cn("flex-1", CONTACTS_FIELD_CLASS)}
|
|
autoFocus
|
|
/>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleAddLabel}>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
{labelsByContactCount.map(({ label, count }) => (
|
|
<NavItem
|
|
key={label.id}
|
|
icon={<Tag className="h-5 w-5" />}
|
|
label={label.label}
|
|
count={count}
|
|
active={currentView === "label" && activeLabelId === label.id}
|
|
onClick={() => onSelectLabel?.(label.id)}
|
|
/>
|
|
))}
|
|
</nav>
|
|
</aside>
|
|
)
|
|
}
|
|
|
|
function NavItem({
|
|
icon,
|
|
label,
|
|
count,
|
|
badge,
|
|
active,
|
|
onClick,
|
|
}: {
|
|
icon: React.ReactNode
|
|
label: string
|
|
count?: number
|
|
badge?: number
|
|
active: boolean
|
|
onClick: () => void
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
className={cn(
|
|
"flex w-full items-center gap-3 rounded-full px-3 py-2 text-sm transition-colors",
|
|
active ? CONTACTS_NAV_ACTIVE_CLASS : CONTACTS_NAV_ITEM_CLASS
|
|
)}
|
|
>
|
|
<span className={active ? "text-mail-nav-selected" : CONTACTS_NAV_ICON_MUTED}>{icon}</span>
|
|
<span className="flex-1 truncate text-left">{label}</span>
|
|
{badge !== undefined && (
|
|
<span className="flex h-5 min-w-5 items-center justify-center rounded-full bg-[#ea4335] px-1.5 text-[11px] font-medium text-white">
|
|
{badge}
|
|
</span>
|
|
)}
|
|
{count !== undefined && (
|
|
<span
|
|
className={cn(
|
|
"flex w-6 shrink-0 justify-center text-xs tabular-nums",
|
|
CONTACTS_MUTED_TEXT
|
|
)}
|
|
>
|
|
{count}
|
|
</span>
|
|
)}
|
|
</button>
|
|
)
|
|
}
|