Some checks are pending
E2E / Playwright e2e (push) Waiting to run
- Added SessionGuard component to manage session expiration and online status. - Updated AuthProvider to streamline session fetching and handling. - Introduced IdentityProvidersSection for managing OAuth, SAML, and LDAP identity providers. - Implemented identity provider guides for easier configuration. - Enhanced mail settings with infinite scroll option for improved user experience. - Updated global styles and layout components for better consistency across the application.
372 lines
12 KiB
TypeScript
372 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"
|
|
import { MAIL_SETTINGS_TABS_LIST_CLASS } from "@/lib/mail-chrome-classes"
|
|
|
|
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 className={MAIL_SETTINGS_TABS_LIST_CLASS}>
|
|
<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>
|
|
)
|
|
}
|