139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
import type { FolderTreeNode } from "@/lib/sidebar-nav-maps"
|
||
|
||
/** Slug URL-safe pour un segment de chemin (dossier / partie de libellé). */
|
||
export function slugifyNavSegment(name: string): string {
|
||
const s = name
|
||
.trim()
|
||
.toLowerCase()
|
||
.normalize("NFD")
|
||
.replace(/\p{M}/gu, "")
|
||
.replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/^-|-$/g, "")
|
||
return s || "dossier"
|
||
}
|
||
|
||
export function folderIdFromPathSlugs(slugs: string[]): string {
|
||
return `folder-${slugs.join("-")}`
|
||
}
|
||
|
||
export function findFolderPath(
|
||
nodes: FolderTreeNode[],
|
||
id: string,
|
||
acc: FolderTreeNode[] = []
|
||
): FolderTreeNode[] | null {
|
||
for (const n of nodes) {
|
||
const next = [...acc, n]
|
||
if (n.id === id) return next
|
||
if (n.children?.length) {
|
||
const hit = findFolderPath(n.children, id, next)
|
||
if (hit) return hit
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
export function collectFolderIdsInTree(nodes: FolderTreeNode[]): string[] {
|
||
const out: string[] = []
|
||
const walk = (ns: FolderTreeNode[]) => {
|
||
for (const e of ns) {
|
||
out.push(e.id)
|
||
if (e.children?.length) walk(e.children)
|
||
}
|
||
}
|
||
walk(nodes)
|
||
return out
|
||
}
|
||
|
||
function collectSubtreeIdsFromNode(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 collectSubtreeIdsFromNode(c)) s.add(x)
|
||
}
|
||
}
|
||
return s
|
||
}
|
||
|
||
export function uniqueFolderPathId(
|
||
pathSlugs: string[],
|
||
usedIds: Set<string>
|
||
): string {
|
||
let base = folderIdFromPathSlugs(pathSlugs)
|
||
if (!usedIds.has(base)) return base
|
||
let n = 2
|
||
while (usedIds.has(`${base}-${n}`)) n += 1
|
||
return `${base}-${n}`
|
||
}
|
||
|
||
/** Slugs des ancêtres (noms affichés) pour un parent `parentId` dans l’arbre. */
|
||
export function ancestorLabelSlugsForParentId(
|
||
tree: FolderTreeNode[],
|
||
parentId: string | null
|
||
): string[] {
|
||
if (parentId === null) return []
|
||
const path = findFolderPath(tree, parentId)
|
||
if (!path) return []
|
||
return path.map((n) => slugifyNavSegment(n.label))
|
||
}
|
||
|
||
export function proposeNewFolderId(
|
||
tree: FolderTreeNode[],
|
||
labelRowIds: readonly string[],
|
||
parentId: string | null,
|
||
displayLabel: string
|
||
): string {
|
||
const used = new Set<string>([
|
||
...collectFolderIdsInTree(tree),
|
||
...labelRowIds,
|
||
])
|
||
const anc = ancestorLabelSlugsForParentId(tree, parentId)
|
||
const leaf = slugifyNavSegment(displayLabel)
|
||
return uniqueFolderPathId([...anc, leaf], used)
|
||
}
|
||
|
||
/**
|
||
* Recalcule les ids du sous-arbre enraciné à `rootId` (chemins slug à partir des libellés).
|
||
* `tree` doit déjà contenir les bons libellés pour ce sous-arbre.
|
||
*/
|
||
export function rekeyFolderSubtreeAt(
|
||
tree: FolderTreeNode[],
|
||
rootId: string,
|
||
labelRowIds: readonly string[]
|
||
): { tree: FolderTreeNode[]; idMap: Record<string, string> } | null {
|
||
const path = findFolderPath(tree, rootId)
|
||
if (!path) return null
|
||
const rootNode = path[path.length - 1]!
|
||
const ancestorSlugs = path.slice(0, -1).map((n) => slugifyNavSegment(n.label))
|
||
|
||
const subtreeOldIds = collectSubtreeIdsFromNode(rootNode)
|
||
const used = new Set<string>([...labelRowIds])
|
||
for (const id of collectFolderIdsInTree(tree)) {
|
||
if (!subtreeOldIds.has(id)) used.add(id)
|
||
}
|
||
|
||
const idMap: Record<string, string> = {}
|
||
|
||
function rebuild(node: FolderTreeNode, anc: string[]): FolderTreeNode {
|
||
const pathSlugs = [...anc, slugifyNavSegment(node.label)]
|
||
const newId = uniqueFolderPathId(pathSlugs, used)
|
||
used.add(newId)
|
||
if (newId !== node.id) idMap[node.id] = newId
|
||
const kids = node.children?.length
|
||
? node.children.map((c) => rebuild(c, pathSlugs))
|
||
: undefined
|
||
return { ...node, id: newId, children: kids }
|
||
}
|
||
|
||
const rebuiltRoot = rebuild(rootNode, ancestorSlugs)
|
||
|
||
function replaceAt(nodes: FolderTreeNode[]): FolderTreeNode[] {
|
||
return nodes.map((n) => {
|
||
if (n.id === rootId) return rebuiltRoot
|
||
if (n.children?.length) return { ...n, children: replaceAt(n.children) }
|
||
return n
|
||
})
|
||
}
|
||
|
||
return { tree: replaceAt(tree), idMap }
|
||
}
|