ultisuite-client/components/gmail/settings/sections/labels-folders-settings-section.tsx
2026-05-25 13:52:40 +02:00

371 lines
12 KiB
TypeScript

"use client"
import { useMemo, useState, type ReactNode } from "react"
import { Folder, Tag, type LucideIcon } from "lucide-react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { useMailAccounts } from "@/lib/api/hooks/use-mail-queries"
import { useLabels } from "@/lib/api/hooks/use-folder-label-queries"
import { useCreateUnifiedFolder, useUnifiedFolders } from "@/lib/api/hooks/use-unified-folder-queries"
import { useImapFoldersForAccount } from "@/lib/api/hooks/use-imap-folders"
import { buildImapFolderSettingsTree } from "@/lib/mail-settings/imap-folder-tree"
import { SettingsSectionHeader } from "@/components/gmail/settings/settings-section-header"
import { SettingsSyncBanner } from "@/components/gmail/settings/settings-sync-banner"
import { useAuthReady } from "@/lib/api/use-auth-ready"
import { buildFolderTreeFromUnified } from "@/lib/mail-settings/unified-folder-tree"
import { useSidebarNav } from "@/lib/sidebar-nav-context"
import { isSystemNavLabelId } from "@/lib/sidebar-nav-data"
import { NavColorPickerTrigger } from "@/components/gmail/nav/nav-color-picker-trigger"
import { DEFAULT_NAV_COLOR } from "@/lib/nav-color"
import {
flattenLabelRowsForSettings,
FolderSettingsTree,
ImapFolderSettingsTree,
NavLabelSettingsCard,
} from "@/components/gmail/settings/nav-item-settings-card"
function SettingsFormHeading({
icon: Icon,
children,
}: {
icon: LucideIcon
children: ReactNode
}) {
return (
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
<Icon className="size-4 shrink-0 opacity-70" aria-hidden />
{children}
</div>
)
}
function NavSettingsListPanel({
title,
icon: Icon,
loading,
emptyTitle,
emptyDescription,
children,
}: {
title: string
icon?: LucideIcon
loading: boolean
emptyTitle: string
emptyDescription?: string
children?: ReactNode
}) {
return (
<section className="space-y-3">
<div className="flex items-center gap-2">
{Icon ? <Icon className="size-4 shrink-0 opacity-70" aria-hidden /> : null}
<h3 className="text-sm font-medium text-foreground">{title}</h3>
</div>
{loading ? (
<div
className="min-h-[120px] rounded-lg border border-border bg-muted/20"
aria-hidden
/>
) : children ? (
children
) : (
<div className="flex min-h-[120px] flex-col items-center justify-center gap-1 rounded-lg border border-dashed border-border bg-muted/10 px-4 py-8 text-center">
<p className="text-sm text-foreground">{emptyTitle}</p>
{emptyDescription ? (
<p className="max-w-sm text-xs text-muted-foreground">{emptyDescription}</p>
) : null}
</div>
)}
</section>
)
}
export function LabelsFoldersSettingsSection() {
return (
<>
<SettingsSectionHeader
title="Libellés et dossiers"
description="Mêmes réglages que dans la barre latérale : couleur, affichage dans les listes, arborescence, renommage."
/>
<Tabs defaultValue="labels">
<TabsList>
<TabsTrigger value="labels">Libellés</TabsTrigger>
<TabsTrigger value="folders-global">Dossiers globaux</TabsTrigger>
<TabsTrigger value="folders-account">Dossiers par compte</TabsTrigger>
</TabsList>
<TabsContent value="labels" className="mt-4">
<LabelsPanel />
</TabsContent>
<TabsContent value="folders-global" className="mt-4">
<UnifiedFoldersPanel />
</TabsContent>
<TabsContent value="folders-account" className="mt-4">
<ImapAccountFoldersPanel />
</TabsContent>
</Tabs>
</>
)
}
function LabelsPanel() {
const { ready, authenticated } = useAuthReady()
const nav = useSidebarNav()
const { isPending } = useLabels()
const [name, setName] = useState("")
const [color, setColor] = useState(DEFAULT_NAV_COLOR)
const userLabels = useMemo(
() =>
flattenLabelRowsForSettings(
nav.labelRows.filter((row) => !isSystemNavLabelId(row.id))
),
[nav.labelRows]
)
const listLoading = ready && authenticated && isPending && userLabels.length === 0
return (
<div className="space-y-6">
<p className="text-sm text-muted-foreground">
Cliquez sur un libellé pour modifier couleur, visibilité et sous-libellés comme le menu
clic droit dans la barre latérale.
</p>
<div className="rounded-lg border border-border p-4 space-y-3">
<SettingsFormHeading icon={Tag}>Nouveau libellé</SettingsFormHeading>
<div className="flex flex-wrap items-end gap-2">
<div className="min-w-[160px] flex-1 space-y-1">
<Label className="text-xs" htmlFor="new-label-name">
Nom
</Label>
<div className="flex items-center gap-1">
<NavColorPickerTrigger
value={color}
onChange={setColor}
aria-label="Couleur du libellé"
/>
<Input
id="new-label-name"
value={name}
onChange={(e) => setName(e.target.value)}
className="flex-1"
/>
</div>
</div>
<Button
type="button"
className="shrink-0"
disabled={!name.trim()}
onClick={() => {
nav.addLabelRowFromSidebar(name.trim(), color)
setName("")
}}
>
Créer
</Button>
</div>
</div>
<NavSettingsListPanel
title="Vos libellés"
icon={Tag}
loading={listLoading}
emptyTitle="Aucun libellé personnalisé"
emptyDescription="Utilisez le formulaire ci-dessus pour en créer un."
>
{userLabels.length > 0 ? (
<ul className="space-y-2">
{userLabels.map((row) => (
<li key={row.id}>
<NavLabelSettingsCard
id={row.id}
name={row.label}
color={row.color}
depth={row.depth}
/>
</li>
))}
</ul>
) : null}
</NavSettingsListPanel>
</div>
)
}
function UnifiedFoldersPanel() {
const { ready, authenticated } = useAuthReady()
const createFolder = useCreateUnifiedFolder()
const { data: folders = [], isFetching, isError, refetch, isPending } =
useUnifiedFolders("global")
const [name, setName] = useState("")
const [color, setColor] = useState(DEFAULT_NAV_COLOR)
const [parentId, setParentId] = useState<string>("__root__")
const visible = folders.filter((f) => f.scope === "global")
const tree = useMemo(() => buildFolderTreeFromUnified(visible), [visible])
const parentOptions = useMemo(() => {
const opts: { value: string; label: string }[] = [{ value: "__root__", label: "Racine" }]
const walk = (nodes: typeof tree, depth: number) => {
for (const n of nodes) {
opts.push({
value: n.id,
label: `${"\u2003".repeat(depth * 2)}${n.label}`,
})
if (n.children?.length) walk(n.children, depth + 1)
}
}
walk(tree, 0)
return opts
}, [tree])
const listLoading = ready && authenticated && isPending && visible.length === 0
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Dossiers Ultimail globaux organisation virtuelle cross-comptes.
</p>
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
<div className="rounded-lg border border-border p-4 space-y-3">
<SettingsFormHeading icon={Folder}>Nouveau dossier</SettingsFormHeading>
<div className="flex flex-wrap items-end gap-2">
<div className="min-w-[160px] flex-1 space-y-1">
<Label className="text-xs" htmlFor="new-folder-name-global">
Nom
</Label>
<div className="flex items-center gap-1">
<NavColorPickerTrigger
value={color}
onChange={setColor}
aria-label="Couleur du dossier"
/>
<Input
id="new-folder-name-global"
value={name}
onChange={(e) => setName(e.target.value)}
className="flex-1"
/>
</div>
</div>
<div className="min-w-[200px] flex-1 space-y-1">
<Label className="text-xs">Emplacement</Label>
<Select value={parentId} onValueChange={setParentId}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{parentOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
type="button"
className="shrink-0"
disabled={!name.trim() || createFolder.isPending}
onClick={() => {
const parent = parentId === "__root__" ? undefined : parentId
createFolder.mutate({
name: name.trim(),
color,
parent_id: parent,
})
setName("")
}}
>
Créer
</Button>
</div>
</div>
<NavSettingsListPanel
title="Vos dossiers"
icon={Folder}
loading={listLoading}
emptyTitle="Aucun dossier Ultimail"
emptyDescription="Utilisez le formulaire ci-dessus pour en créer un."
>
{visible.length > 0 ? <FolderSettingsTree nodes={tree} /> : null}
</NavSettingsListPanel>
</div>
)
}
function ImapAccountFoldersPanel() {
const { ready, authenticated } = useAuthReady()
const { data: accounts = [] } = useMailAccounts()
const [accountId, setAccountId] = useState("")
const selectedAccountId = accountId || accounts[0]?.id
const {
data: folders = [],
isFetching,
isError,
refetch,
isPending,
} = useImapFoldersForAccount(selectedAccountId)
const tree = useMemo(
() =>
selectedAccountId
? buildImapFolderSettingsTree(folders, selectedAccountId)
: [],
[folders, selectedAccountId]
)
const listLoading = ready && authenticated && isPending && folders.length === 0
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Dossiers synchronisés depuis vos serveurs mail (IMAP). Masquez ceux que vous ne voulez
pas voir dans la barre latérale.
</p>
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
<div className="max-w-xs space-y-1">
<Label className="text-xs">Compte mail</Label>
<Select
value={selectedAccountId ?? ""}
onValueChange={setAccountId}
disabled={accounts.length === 0}
>
<SelectTrigger>
<SelectValue placeholder="Choisir un compte" />
</SelectTrigger>
<SelectContent>
{accounts.map((a) => (
<SelectItem key={a.id} value={a.id}>
{a.email}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<NavSettingsListPanel
title="Dossiers IMAP"
icon={Folder}
loading={listLoading}
emptyTitle="Aucun dossier IMAP personnalisé"
emptyDescription="Les dossiers système (Boîte de réception, Envoyés…) restent dans la navigation principale. Les dossiers personnalisés apparaissent ici après synchronisation."
>
{tree.length > 0 ? <ImapFolderSettingsTree nodes={tree} /> : null}
</NavSettingsListPanel>
</div>
)
}