ultisuite-client/components/drive/drive-filter-bar.tsx
R3D347HR4Y 6ec95262af Add OnlyOffice integration and update project configurations
- 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.
2026-06-07 15:49:21 +02:00

383 lines
12 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { Calendar, ChevronDown, Search } from "lucide-react"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { useSearchContacts } from "@/lib/api/hooks/use-contact-queries"
import type { DriveMimeCategory, DriveSourceId } from "@/lib/drive/drive-filters"
import {
useDriveFiltersStore,
type DriveDatePreset,
} from "@/lib/stores/drive-filters-store"
import { suitePublicAsset } from "@/lib/suite/suite-public-asset"
import { DriveMimeCategoryIcon } from "@/lib/drive/drive-file-icon"
import { DRIVE_CARD_PAD_X } from "@/lib/drive/drive-chrome-classes"
import { cn } from "@/lib/utils"
const FILTER_BORDER = "border-mail-border-subtle"
const FILTER_DIVIDER = "border-mail-border-subtle"
const FILTER_DROPDOWN_CONTENT_CLASS = cn(
FILTER_BORDER,
"bg-mail-surface-elevated shadow-sm"
)
const FILTER_CHECKBOX_ITEM_CLASS =
"gap-3 py-2 pl-2 pr-8 [&>span:first-child]:left-auto [&>span:first-child]:right-2"
const TYPE_OPTIONS: { id: DriveMimeCategory; label: string }[] = [
{ id: "folder", label: "Dossiers" },
{ id: "document", label: "Documents" },
{ id: "spreadsheet", label: "Feuilles de calcul" },
{ id: "presentation", label: "Présentations" },
{ id: "image", label: "Photos et images" },
{ id: "pdf", label: "Fichiers PDF" },
{ id: "video", label: "Vidéos" },
{ id: "audio", label: "Audio" },
{ id: "archive", label: "Archives (zip)" },
{ id: "other", label: "Autres fichiers" },
]
const SOURCE_OPTIONS: { id: DriveSourceId; label: string; iconSrc: string }[] = [
{
id: "ultimail",
label: "Ultimail",
iconSrc: suitePublicAsset("/brand/ultimail-header-icon.png"),
},
{
id: "ultimeet",
label: "Ultimeet",
iconSrc: suitePublicAsset("/ultimeet-mark.svg"),
},
]
function SourceAppIcon({ src, alt }: { src: string; alt: string }) {
return (
<img
src={src}
alt={alt}
className="h-4 w-4 shrink-0 object-contain"
/>
)
}
const DATE_OPTIONS: { id: Exclude<DriveDatePreset, null | "custom">; label: string }[] = [
{ id: "today", label: "Aujourd'hui" },
{ id: "last7", label: "7 derniers jours" },
{ id: "last30", label: "30 derniers jours" },
{ id: "thisYear", label: `Cette année (${new Date().getFullYear()})` },
{ id: "lastYear", label: `Année dernière (${new Date().getFullYear() - 1})` },
]
function FilterChip({
label,
active,
children,
}: {
label: string
active?: boolean
children: React.ReactNode
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
"inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-lg border px-3 text-sm font-medium whitespace-nowrap transition-colors",
active
? "border-[#d2e3fc] bg-mail-active text-[#1967d2] dark:text-[#8ab4f8]"
: cn(
FILTER_BORDER,
"bg-mail-surface text-mail-text hover:bg-mail-nav-hover"
)
)}
>
{label}
<ChevronDown className="h-4 w-4 opacity-60" />
</button>
</DropdownMenuTrigger>
{children}
</DropdownMenu>
)
}
function TypeFilterDropdown() {
const types = useDriveFiltersStore((s) => s.types)
const toggleType = useDriveFiltersStore((s) => s.toggleType)
return (
<FilterChip label="Type" active={types.size > 0}>
<DropdownMenuContent
align="start"
data-drive-menu-surface
className={cn(
FILTER_DROPDOWN_CONTENT_CLASS,
"max-h-[min(70vh,420px)] w-56 overflow-y-auto"
)}
>
{TYPE_OPTIONS.map((opt) => (
<DropdownMenuCheckboxItem
key={opt.id}
checked={types.has(opt.id)}
onCheckedChange={() => toggleType(opt.id)}
className={FILTER_CHECKBOX_ITEM_CLASS}
>
<DriveMimeCategoryIcon category={opt.id} size="sm" />
{opt.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</FilterChip>
)
}
function ContactsFilterDropdown() {
const contactName = useDriveFiltersStore((s) => s.contactName)
const setContact = useDriveFiltersStore((s) => s.setContact)
const [query, setQuery] = useState("")
const { data: results = [] } = useSearchContacts(query)
const chipLabel = contactName ?? "Contacts"
return (
<FilterChip label={chipLabel} active={Boolean(contactName)}>
<DropdownMenuContent align="start" data-drive-menu-surface className={cn(FILTER_DROPDOWN_CONTENT_CLASS, "w-80 p-0")}>
<div className={cn("border-b p-2", FILTER_DIVIDER)}>
<div className="relative">
<Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Rechercher des contacts…"
className={cn("h-9 pl-8", FILTER_BORDER)}
/>
</div>
</div>
<div className="max-h-64 overflow-y-auto py-1">
{query.length < 2 ? (
<p className="px-3 py-4 text-center text-xs text-muted-foreground">
Saisissez au moins 2 caractères
</p>
) : results.length === 0 ? (
<p className="px-3 py-4 text-center text-xs text-muted-foreground">Aucun contact</p>
) : (
results.map((c) => (
<DropdownMenuItem
key={c.uid}
className="flex flex-col items-start gap-0.5 py-2"
onClick={() => {
setContact({ name: c.full_name, email: c.email })
setQuery("")
}}
>
<span className="font-medium text-[#3c4043]">{c.full_name}</span>
{c.email ? (
<span className="text-xs text-muted-foreground">{c.email}</span>
) : null}
</DropdownMenuItem>
))
)}
</div>
{contactName ? (
<div className={cn("border-t p-2", FILTER_DIVIDER)}>
<Button
type="button"
variant="ghost"
size="sm"
className="w-full"
onClick={() => setContact(null)}
>
Effacer le contact
</Button>
</div>
) : null}
</DropdownMenuContent>
</FilterChip>
)
}
function DateFilterDropdown() {
const datePreset = useDriveFiltersStore((s) => s.datePreset)
const setDateRange = useDriveFiltersStore((s) => s.setDateRange)
const [open, setOpen] = useState(false)
const [customMode, setCustomMode] = useState(false)
const [draftFrom, setDraftFrom] = useState("")
const [draftTo, setDraftTo] = useState("")
useEffect(() => {
if (open) {
setCustomMode(datePreset === "custom")
setDraftFrom(useDriveFiltersStore.getState().dateFrom ?? "")
setDraftTo(useDriveFiltersStore.getState().dateTo ?? "")
}
}, [open, datePreset])
const dateLabel =
datePreset === "today"
? "Aujourd'hui"
: datePreset === "last7"
? "7 derniers jours"
: datePreset === "last30"
? "30 derniers jours"
: datePreset === "thisYear"
? "Cette année"
: datePreset === "lastYear"
? "Année dernière"
: datePreset === "custom"
? "Période personnalisée"
: "Date de modification"
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
"inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-lg border px-3 text-sm font-medium whitespace-nowrap transition-colors",
datePreset
? "border-[#d2e3fc] bg-mail-active text-[#1967d2] dark:text-[#8ab4f8]"
: cn(
FILTER_BORDER,
"bg-mail-surface text-mail-text hover:bg-mail-nav-hover"
)
)}
>
<Calendar className="h-4 w-4 shrink-0 opacity-70" />
{dateLabel}
<ChevronDown className="h-4 w-4 opacity-60" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" data-drive-menu-surface className={cn(FILTER_DROPDOWN_CONTENT_CLASS, "w-64 p-0")}>
<div className="py-1">
{DATE_OPTIONS.map((opt) => (
<DropdownMenuItem
key={opt.id}
onSelect={() => {
setDateRange(opt.id)
setOpen(false)
}}
className={cn(datePreset === opt.id && !customMode && "bg-accent")}
>
{opt.label}
</DropdownMenuItem>
))}
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault()
setCustomMode(true)
}}
className={cn((datePreset === "custom" || customMode) && "bg-accent")}
>
Période personnalisée
</DropdownMenuItem>
</div>
{customMode ? (
<>
<div className={cn("space-y-2 border-t px-3 py-2", FILTER_DIVIDER)}>
<label className="block text-xs text-muted-foreground">
Du
<Input
type="date"
value={draftFrom}
onChange={(e) => setDraftFrom(e.target.value)}
className={cn("mt-1 h-8", FILTER_BORDER)}
/>
</label>
<label className="block text-xs text-muted-foreground">
Au
<Input
type="date"
value={draftTo}
onChange={(e) => setDraftTo(e.target.value)}
className={cn("mt-1 h-8", FILTER_BORDER)}
/>
</label>
</div>
<div className={cn("flex items-center justify-end gap-1 border-t px-2 py-2", FILTER_DIVIDER)}>
<Button type="button" variant="ghost" size="sm" onClick={() => setOpen(false)}>
Annuler
</Button>
<Button
type="button"
size="sm"
disabled={!draftFrom}
onClick={() => {
setDateRange("custom", draftFrom, draftTo || null)
setOpen(false)
}}
>
Appliquer
</Button>
</div>
</>
) : null}
</DropdownMenuContent>
</DropdownMenu>
)
}
function SourceFilterDropdown() {
const sources = useDriveFiltersStore((s) => s.sources)
const toggleSource = useDriveFiltersStore((s) => s.toggleSource)
return (
<FilterChip label="Source" active={sources.size > 0}>
<DropdownMenuContent align="start" data-drive-menu-surface className={cn(FILTER_DROPDOWN_CONTENT_CLASS, "w-52")}>
{SOURCE_OPTIONS.map((opt) => (
<DropdownMenuCheckboxItem
key={opt.id}
checked={sources.has(opt.id)}
onCheckedChange={() => toggleSource(opt.id)}
className={FILTER_CHECKBOX_ITEM_CLASS}
>
<SourceAppIcon src={opt.iconSrc} alt={opt.label} />
{opt.label}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</FilterChip>
)
}
export function DriveFilterBar({ showContacts = true }: { showContacts?: boolean }) {
const clearAll = useDriveFiltersStore((s) => s.clearAll)
const hasFilters = useDriveFiltersStore((s) =>
s.types.size > 0 ||
s.sources.size > 0 ||
Boolean(s.contactName) ||
Boolean(s.datePreset)
)
return (
<div
className={cn(
"flex min-w-0 flex-nowrap items-center gap-2 overflow-x-auto py-1.5",
"[scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden",
DRIVE_CARD_PAD_X
)}
>
<TypeFilterDropdown />
{showContacts ? <ContactsFilterDropdown /> : null}
<DateFilterDropdown />
<SourceFilterDropdown />
{hasFilters ? (
<Button
type="button"
variant="ghost"
size="sm"
className="shrink-0 text-[#1967d2]"
onClick={clearAll}
>
Réinitialiser les filtres
</Button>
) : null}
</div>
)
}