ultisuite-client/components/gmail/settings/nav-item-settings-card.tsx
2026-05-25 13:52:40 +02:00

444 lines
12 KiB
TypeScript

"use client"
import { useEffect, useMemo, useState } from "react"
import { Trash2 } from "lucide-react"
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 {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { NavColorPickerTrigger } from "@/components/gmail/nav/nav-color-picker-trigger"
import {
NavMessageVisibilityFields,
NavSidebarVisibilityFields,
} from "@/components/gmail/nav/nav-visibility-fields"
import { useSidebarNav, folderMoveParentOptions } from "@/lib/sidebar-nav-context"
import { normalizeNavColorClass } from "@/lib/nav-color"
import { cn } from "@/lib/utils"
export function NavLabelSettingsCard({
id,
name,
color,
depth = 0,
}: {
id: string
name: string
color: string
depth?: number
}) {
const nav = useSidebarNav()
const prefs = nav.getNavItemPrefs(id)
const colorClass = normalizeNavColorClass(color)
const [renameDraft, setRenameDraft] = useState(name)
const [sublabelName, setSublabelName] = useState("")
useEffect(() => {
setRenameDraft(name)
}, [name])
return (
<NavItemSettingsShell
title={name}
color={colorClass}
depth={depth}
onColorChange={(sw) => nav.updateFolderOrLabelColor(id, sw)}
onDelete={() => nav.removeFolderOrLabelRow(id)}
deleteLabel="Supprimer le libellé"
>
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<NavSidebarVisibilityFields
listKind="labels"
value={prefs.sidebar}
onChange={(v) => nav.setNavItemSidebarVisibility(id, v)}
/>
<NavMessageVisibilityFields
value={prefs.messages}
onChange={(v) => nav.setNavItemMessageVisibility(id, v)}
/>
</div>
<div className="space-y-2">
<Label className="text-xs" htmlFor={`rename-label-${id}`}>
Renommer
</Label>
<div className="flex flex-wrap gap-2">
<Input
id={`rename-label-${id}`}
value={renameDraft}
onChange={(e) => setRenameDraft(e.target.value)}
className="min-w-[160px] flex-1"
/>
<Button
type="button"
variant="secondary"
disabled={!renameDraft.trim() || renameDraft.trim() === name}
onClick={() => nav.renameFolderOrLabel(id, renameDraft.trim())}
>
Enregistrer
</Button>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs" htmlFor={`sublabel-${id}`}>
Ajouter un sous-libellé
</Label>
<div className="flex flex-wrap gap-2">
<Input
id={`sublabel-${id}`}
value={sublabelName}
onChange={(e) => setSublabelName(e.target.value)}
placeholder="Nom du sous-libellé"
className="min-w-[160px] flex-1"
/>
<Button
type="button"
variant="secondary"
disabled={!sublabelName.trim()}
onClick={() => {
nav.addChildLabelRow(id, sublabelName.trim())
setSublabelName("")
}}
>
Ajouter
</Button>
</div>
</div>
</div>
</NavItemSettingsShell>
)
}
export function NavImapFolderSettingsCard({
id,
name,
remoteName,
depth = 0,
}: {
id: string
name: string
remoteName?: string
depth?: number
}) {
const nav = useSidebarNav()
const prefs = nav.getNavItemPrefs(id)
const subtitle =
remoteName && remoteName !== name ? remoteName : undefined
return (
<NavItemSettingsShell
title={name}
subtitle={subtitle}
depth={depth}
hideDelete
hideColor
>
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<NavSidebarVisibilityFields
listKind="folders"
value={prefs.sidebar}
onChange={(v) => nav.setNavItemSidebarVisibility(id, v)}
/>
<NavMessageVisibilityFields
value={prefs.messages}
onChange={(v) => nav.setNavItemMessageVisibility(id, v)}
/>
</div>
<p className="text-xs text-muted-foreground">
Dossier synchronisé depuis le serveur mail structure non modifiable ici.
</p>
</div>
</NavItemSettingsShell>
)
}
export function NavFolderSettingsCard({
id,
name,
color,
depth = 0,
}: {
id: string
name: string
color?: string
depth?: number
}) {
const nav = useSidebarNav()
const prefs = nav.getNavItemPrefs(id)
const colorClass = normalizeNavColorClass(color)
const [renameDraft, setRenameDraft] = useState(name)
const [moveParent, setMoveParent] = useState("__root__")
const [subfolderName, setSubfolderName] = useState("")
const moveTargets = useMemo(
() => folderMoveParentOptions(nav.folderTree, id),
[nav.folderTree, id]
)
useEffect(() => {
setRenameDraft(name)
}, [name])
return (
<NavItemSettingsShell
title={name}
color={colorClass}
depth={depth}
onColorChange={(sw) => nav.updateFolderOrLabelColor(id, sw)}
onDelete={() => nav.removeFolderOrLabelRow(id)}
deleteLabel="Supprimer le dossier"
>
<div className="space-y-4">
<div className="grid gap-4 sm:grid-cols-2">
<NavSidebarVisibilityFields
listKind="folders"
value={prefs.sidebar}
onChange={(v) => nav.setNavItemSidebarVisibility(id, v)}
/>
<NavMessageVisibilityFields
value={prefs.messages}
onChange={(v) => nav.setNavItemMessageVisibility(id, v)}
/>
</div>
<div className="space-y-2">
<Label className="text-xs" htmlFor={`rename-folder-${id}`}>
Renommer
</Label>
<div className="flex flex-wrap gap-2">
<Input
id={`rename-folder-${id}`}
value={renameDraft}
onChange={(e) => setRenameDraft(e.target.value)}
className="min-w-[160px] flex-1"
/>
<Button
type="button"
variant="secondary"
disabled={!renameDraft.trim() || renameDraft.trim() === name}
onClick={() => nav.renameFolderOrLabel(id, renameDraft.trim())}
>
Enregistrer
</Button>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs">Déplacer vers</Label>
<div className="flex flex-wrap gap-2">
<Select value={moveParent} onValueChange={setMoveParent}>
<SelectTrigger className="min-w-[200px] flex-1">
<SelectValue placeholder="Emplacement" />
</SelectTrigger>
<SelectContent>
{moveTargets.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
variant="secondary"
onClick={() =>
nav.moveFolder(id, moveParent === "__root__" ? null : moveParent)
}
>
Déplacer
</Button>
</div>
</div>
<div className="space-y-2">
<Label className="text-xs" htmlFor={`subfolder-${id}`}>
Nouveau sous-dossier
</Label>
<div className="flex flex-wrap gap-2">
<Input
id={`subfolder-${id}`}
value={subfolderName}
onChange={(e) => setSubfolderName(e.target.value)}
placeholder="Nom du sous-dossier"
className="min-w-[160px] flex-1"
/>
<Button
type="button"
variant="secondary"
disabled={!subfolderName.trim()}
onClick={() => {
nav.addSubfolder(id, subfolderName.trim())
setSubfolderName("")
}}
>
Créer
</Button>
</div>
</div>
</div>
</NavItemSettingsShell>
)
}
function NavItemSettingsShell({
title,
subtitle,
color,
depth,
onColorChange,
onDelete,
deleteLabel,
hideDelete = false,
hideColor = false,
children,
}: {
title: string
subtitle?: string
color?: string
depth: number
onColorChange?: (swatch: string) => void
onDelete?: () => void
deleteLabel?: string
hideDelete?: boolean
hideColor?: boolean
children: import("react").ReactNode
}) {
const [open, setOpen] = useState(false)
return (
<Collapsible
open={open}
onOpenChange={setOpen}
className="rounded-lg border border-border bg-card"
>
<div
className="flex items-center gap-1 px-3 py-2"
style={{ paddingLeft: `${depth * 12 + 12}px` }}
>
{!hideColor && color && onColorChange ? (
<NavColorPickerTrigger
value={color}
onChange={onColorChange}
aria-label="Couleur"
/>
) : null}
<CollapsibleTrigger className="flex min-w-0 flex-1 flex-col items-start text-left">
<span className="truncate text-sm font-medium">{title}</span>
{subtitle ? (
<span className="truncate text-xs text-muted-foreground">{subtitle}</span>
) : null}
</CollapsibleTrigger>
{!hideDelete && onDelete ? (
<Button
type="button"
variant="ghost"
size="icon"
className="shrink-0 text-muted-foreground hover:text-destructive"
aria-label={deleteLabel ?? "Supprimer"}
onClick={(e) => {
e.stopPropagation()
onDelete()
}}
>
<Trash2 className="size-4" />
</Button>
) : null}
</div>
<CollapsibleContent className="border-t border-border px-3 py-4">
{children}
</CollapsibleContent>
</Collapsible>
)
}
/** Flatten label rows for settings (parent/child via `/` in name). */
export function flattenLabelRowsForSettings(
rows: { id: string; label: string; color: string }[]
) {
return rows.map((row) => ({
...row,
depth: Math.max(0, row.label.split("/").length - 1),
}))
}
type FolderSettingsNode = {
id: string
label: string
color?: string
children?: FolderSettingsNode[]
}
export function FolderSettingsTree({
nodes,
depth = 0,
}: {
nodes: FolderSettingsNode[]
depth?: number
}) {
return (
<ul className={cn("space-y-2", depth > 0 && "mt-2")}>
{nodes.map((node) => (
<li key={node.id}>
<NavFolderSettingsCard
id={node.id}
name={node.label}
color={node.color}
depth={depth}
/>
{node.children?.length ? (
<FolderSettingsTree nodes={node.children} depth={depth + 1} />
) : null}
</li>
))}
</ul>
)
}
type ImapFolderSettingsNode = {
id: string
label: string
remoteName?: string
children?: ImapFolderSettingsNode[]
}
export function ImapFolderSettingsTree({
nodes,
depth = 0,
}: {
nodes: ImapFolderSettingsNode[]
depth?: number
}) {
return (
<ul className={cn("space-y-2", depth > 0 && "mt-2")}>
{nodes.map((node) => (
<li key={node.id}>
<NavImapFolderSettingsCard
id={node.id}
name={node.label}
remoteName={node.remoteName}
depth={depth}
/>
{node.children?.length ? (
<ImapFolderSettingsTree nodes={node.children} depth={depth + 1} />
) : null}
</li>
))}
</ul>
)
}