ultisuite-client/lib/sidebar-nav-context.tsx
2026-05-16 20:30:50 +02:00

284 lines
8.0 KiB
TypeScript

"use client"
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
type ReactNode,
} from "react"
import type { FolderTreeNode, LabelRowItem } from "@/lib/sidebar-nav-data"
import {
useNavStore,
type LabelListSidebarVisibility,
type LabelInMessageListVisibility,
type NavItemPrefs,
} from "@/lib/stores/nav-store"
import {
buildEmailLabelToSidebarFolderId,
buildFolderIdToLabelRecord,
} from "@/lib/sidebar-nav-maps"
export type { LabelListSidebarVisibility, LabelInMessageListVisibility, NavItemPrefs }
export type NavEmailSync = {
renameLabel: (from: string, to: string) => void
removeLabel: (label: string) => void
}
const navEmailSyncRef = { current: null as NavEmailSync | null }
export function registerNavEmailSync(cb: NavEmailSync | null) {
navEmailSyncRef.current = cb
}
type SidebarNavContextValue = {
folderTree: FolderTreeNode[]
labelRows: LabelRowItem[]
folderIdToLabel: Record<string, string>
emailLabelToSidebarFolderId: Record<string, string>
getNavItemPrefs: (id: string) => Required<Pick<NavItemPrefs, "sidebar" | "messages">>
setNavItemSidebarVisibility: (id: string, v: LabelListSidebarVisibility) => void
setNavItemMessageVisibility: (id: string, v: LabelInMessageListVisibility) => void
ensureLabelRowForLabelText: (label: string) => void
addLabelRowFromSidebar: (label: string) => void
addFolder: (parentId: string | null, name: string) => void
updateFolderOrLabelColor: (id: string, color: string) => void
renameFolderOrLabel: (id: string, newLabel: string) => void
removeFolderOrLabelRow: (id: string) => void
moveFolder: (id: string, newParentId: string | null) => void
reorderLabelRows: (
draggedId: string,
targetId: string,
placement: "before" | "after"
) => void
moveFolderRelative: (
draggedId: string,
targetId: string,
placement: "before" | "after" | "inside"
) => void
addSubfolder: (parentId: string, name: string) => void
addChildLabelRow: (parentLabelRowId: string, childName: string) => void
setLabelRowEnabled: (id: string, enabled: boolean) => void
}
const SidebarNavContext = createContext<SidebarNavContextValue | null>(null)
type SidebarNavProviderProps = {
children: ReactNode
routeFolderId?: string | null
onRouteFolderIdChange?: (nextFolderId: string) => void
}
export function SidebarNavProvider({
children,
routeFolderId,
onRouteFolderIdChange,
}: SidebarNavProviderProps) {
const routeSyncRef = useRef({
routeFolderId: null as string | null | undefined,
onRouteFolderIdChange: undefined as
| ((nextFolderId: string) => void)
| undefined,
})
useEffect(() => {
routeSyncRef.current = { routeFolderId, onRouteFolderIdChange }
}, [routeFolderId, onRouteFolderIdChange])
const scheduleRouteFolderIdSync = useCallback(
(idMap: Record<string, string>) => {
if (Object.keys(idMap).length === 0) return
queueMicrotask(() => {
const { routeFolderId: rid, onRouteFolderIdChange: on } =
routeSyncRef.current
if (!rid || !on) return
const next = idMap[rid]
if (next) on(next)
})
},
[]
)
const folderTree = useNavStore((s) => s.folderTree)
const labelRows = useNavStore((s) => s.labelRows)
const navItemPrefs = useNavStore((s) => s.navItemPrefs)
const navActions = useNavStore.getState()
const folderIdToLabel = useMemo(
() => buildFolderIdToLabelRecord(folderTree, labelRows),
[folderTree, labelRows]
)
const emailLabelToSidebarFolderId = useMemo(
() => buildEmailLabelToSidebarFolderId(folderIdToLabel),
[folderIdToLabel]
)
const getNavItemPrefs = useCallback(
(id: string): Required<Pick<NavItemPrefs, "sidebar" | "messages">> => {
const p = navItemPrefs[id]
return {
sidebar: p?.sidebar ?? "show",
messages: p?.messages ?? "show",
}
},
[navItemPrefs]
)
const renameFolderOrLabel = useCallback(
(id: string, newLabel: string) => {
const { idMap, emailRename } = useNavStore.getState().renameFolderOrLabel(id, newLabel)
scheduleRouteFolderIdSync(idMap)
if (emailRename) {
queueMicrotask(() => {
navEmailSyncRef.current?.renameLabel(emailRename.from, emailRename.to)
})
}
},
[scheduleRouteFolderIdSync]
)
const removeFolderOrLabelRow = useCallback(
(id: string) => {
const labelsToRemove = useNavStore.getState().removeFolderOrLabelRow(id)
if (labelsToRemove.length > 0) {
queueMicrotask(() => {
for (const lab of labelsToRemove) {
navEmailSyncRef.current?.removeLabel(lab)
}
})
}
},
[]
)
const moveFolder = useCallback(
(id: string, newParentId: string | null) => {
const idMap = useNavStore.getState().moveFolder(id, newParentId)
scheduleRouteFolderIdSync(idMap)
},
[scheduleRouteFolderIdSync]
)
const moveFolderRelative = useCallback(
(
draggedId: string,
targetId: string,
placement: "before" | "after" | "inside"
) => {
const idMap = useNavStore
.getState()
.moveFolderRelative(draggedId, targetId, placement)
scheduleRouteFolderIdSync(idMap)
},
[scheduleRouteFolderIdSync]
)
const reorderLabelRows = useCallback(
(
draggedId: string,
targetId: string,
placement: "before" | "after"
) => {
useNavStore.getState().reorderLabelRows(draggedId, targetId, placement)
},
[]
)
const value = useMemo(
() => ({
folderTree,
labelRows,
folderIdToLabel,
emailLabelToSidebarFolderId,
getNavItemPrefs,
setNavItemSidebarVisibility: navActions.setNavItemSidebarVisibility,
setNavItemMessageVisibility: navActions.setNavItemMessageVisibility,
ensureLabelRowForLabelText: navActions.ensureLabelRowForLabelText,
addLabelRowFromSidebar: navActions.addLabelRowFromSidebar,
addFolder: navActions.addFolder,
updateFolderOrLabelColor: navActions.updateFolderOrLabelColor,
renameFolderOrLabel,
removeFolderOrLabelRow,
moveFolder,
reorderLabelRows,
moveFolderRelative,
addSubfolder: navActions.addSubfolder,
addChildLabelRow: navActions.addChildLabelRow,
setLabelRowEnabled: navActions.setLabelRowEnabled,
}),
[
folderTree,
labelRows,
folderIdToLabel,
emailLabelToSidebarFolderId,
getNavItemPrefs,
navActions,
renameFolderOrLabel,
removeFolderOrLabelRow,
moveFolder,
reorderLabelRows,
moveFolderRelative,
]
)
return (
<SidebarNavContext.Provider value={value}>
{children}
</SidebarNavContext.Provider>
)
}
export function useSidebarNav(): SidebarNavContextValue {
const ctx = useContext(SidebarNavContext)
if (!ctx) {
throw new Error("useSidebarNav must be used within SidebarNavProvider")
}
return ctx
}
export function folderMoveParentOptions(
tree: FolderTreeNode[],
excludeId: string
): { value: string; label: string; depth: number }[] {
function findNode(nodes: FolderTreeNode[], id: string): FolderTreeNode | null {
for (const n of nodes) {
if (n.id === id) return n
if (n.children?.length) {
const hit = findNode(n.children, id)
if (hit) return hit
}
}
return null
}
function collectIds(node: FolderTreeNode): Set<string> {
const s = new Set<string>([node.id])
if (node.children?.length) {
for (const c of node.children) {
for (const x of collectIds(c)) s.add(x)
}
}
return s
}
const ex = findNode(tree, excludeId)
const banned = ex ? collectIds(ex) : new Set([excludeId])
const out: { value: string; label: string; depth: number }[] = [
{ value: "__root__", label: "Racine", depth: 0 },
]
const walk = (nodes: FolderTreeNode[], depth: number) => {
for (const n of nodes) {
if (banned.has(n.id)) continue
out.push({
value: n.id,
label: `${"\u2003".repeat(depth * 2)}${n.label}`,
depth,
})
if (n.children?.length) walk(n.children, depth + 1)
}
}
walk(tree, 0)
return out
}