- 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.
383 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|