Files
kandev/apps/web/components/task/file-tree-utils.ts
T

90 lines
3.4 KiB
TypeScript

import type { FileTreeNode } from "@/lib/types/backend";
/** Sort comparator: directories first, then alphabetical by name. */
export const compareTreeNodes = (a: FileTreeNode, b: FileTreeNode): number => {
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
return a.name.localeCompare(b.name);
};
/**
* Merge a freshly-fetched tree node into an existing one, preserving
* already-loaded children so expanded folders don't collapse.
*/
export function mergeTreeNodes(existing: FileTreeNode, incoming: FileTreeNode): FileTreeNode {
if (!incoming.children) return { ...existing, ...incoming, children: existing.children };
if (!existing.children) return incoming;
const existingByPath = new Map(existing.children.map((c) => [c.path, c]));
const mergedChildren = incoming.children.map((inChild) => {
const exChild = existingByPath.get(inChild.path);
if (exChild && exChild.is_dir && inChild.is_dir) {
return mergeTreeNodes(exChild, inChild);
}
return inChild;
});
return { ...existing, ...incoming, children: mergedChildren };
}
/** Insert a file node into a parent folder, keeping children sorted (dirs first, then alpha). */
export function insertNodeInTree(
root: FileTreeNode,
parentPath: string,
node: FileTreeNode,
): FileTreeNode {
if (root.path === parentPath || (parentPath === "" && root.path === "")) {
const children = [...(root.children ?? []), node].sort(compareTreeNodes);
return { ...root, children };
}
if (!root.children) return root;
return { ...root, children: root.children.map((c) => insertNodeInTree(c, parentPath, node)) };
}
export function removeNodeFromTree(root: FileTreeNode, targetPath: string): FileTreeNode {
if (!root.children) return root;
const filtered = root.children.filter((c) => c.path !== targetPath);
return { ...root, children: filtered.map((c) => removeNodeFromTree(c, targetPath)) };
}
/** Rename a node in the tree, updating its name and path. */
function replacePathPrefix(path: string, oldPrefix: string, newPrefix: string): string {
if (path === oldPrefix) return newPrefix;
if (path.startsWith(`${oldPrefix}/`)) return `${newPrefix}${path.slice(oldPrefix.length)}`;
return path;
}
function renameSubtree(node: FileTreeNode, oldPath: string, newPath: string): FileTreeNode {
const nextPath = replacePathPrefix(node.path, oldPath, newPath);
const nextName = nextPath.split("/").pop() || nextPath;
const nextChildren = node.children?.map((child) => renameSubtree(child, oldPath, newPath));
return {
...node,
name: nextName,
path: nextPath,
children: nextChildren,
};
}
export function treeContainsPath(root: FileTreeNode, targetPath: string): boolean {
if (root.path === targetPath) return true;
if (!root.children) return false;
return root.children.some((child) => treeContainsPath(child, targetPath));
}
export function countFilesInTree(node: FileTreeNode): number {
if (!node.children || node.children.length === 0) return node.is_dir ? 0 : 1;
const base = node.is_dir ? 0 : 1;
return node.children.reduce((sum, child) => sum + countFilesInTree(child), base);
}
export function renameNodeInTree(
root: FileTreeNode,
oldPath: string,
newPath: string,
): FileTreeNode {
if (root.path === oldPath) {
return renameSubtree(root, oldPath, newPath);
}
if (!root.children) return root;
const nextChildren = root.children.map((c) => renameNodeInTree(c, oldPath, newPath));
return { ...root, children: nextChildren.sort(compareTreeNodes) };
}