Files
oranguru/src/Griddy/features/tree/transformTreeData.ts
2026-02-16 22:48:48 +02:00

139 lines
4.1 KiB
TypeScript

import type { TreeConfig } from '../../core/types';
/**
* Transforms flat data with parentId references into nested tree structure
* @param data - Flat array of data with parent references
* @param parentIdField - Field name containing parent ID (default: 'parentId')
* @param idField - Field name containing node ID (default: 'id')
* @param maxDepth - Maximum tree depth to build (default: Infinity)
* @returns Array of root nodes with subRows property
*/
export function transformFlatToNested<T extends Record<string, any>>(
data: T[],
parentIdField: keyof T | string = 'parentId',
idField: keyof T | string = 'id',
maxDepth = Infinity,
): T[] {
// Build a map of id -> node for quick lookups
const nodeMap = new Map<any, T & { subRows?: T[] }>();
const roots: (T & { subRows?: T[] })[] = [];
// First pass: create map of all nodes
data.forEach((item) => {
nodeMap.set(item[idField], { ...item, subRows: [] });
});
// Second pass: build tree structure
data.forEach((item) => {
const node = nodeMap.get(item[idField])!;
const parentId = item[parentIdField];
if (parentId == null || parentId === '') {
// Root node (no parent or empty parent)
roots.push(node);
} else {
const parent = nodeMap.get(parentId);
if (parent) {
// Add to parent's children
if (!parent.subRows) {
parent.subRows = [];
}
parent.subRows.push(node);
} else {
// Orphaned node (parent doesn't exist) - treat as root
roots.push(node);
}
}
});
// Enforce max depth by removing children beyond the limit
if (maxDepth !== Infinity) {
const enforceDepth = (nodes: (T & { subRows?: T[] })[], currentDepth: number) => {
if (currentDepth >= maxDepth) {
return;
}
nodes.forEach((node) => {
if (node.subRows && node.subRows.length > 0) {
if (currentDepth + 1 >= maxDepth) {
// Remove children at max depth
delete node.subRows;
} else {
enforceDepth(node.subRows, currentDepth + 1);
}
}
});
};
enforceDepth(roots, 0);
}
return roots;
}
/**
* Determines if a node has children (can be expanded)
* @param row - The data row
* @param config - Tree configuration
* @returns true if node has or can have children
*/
export function hasChildren<T>(row: any, config: TreeConfig<T>): boolean {
// If user provided hasChildren function, use it
if (config.hasChildren) {
return config.hasChildren(row);
}
// Check for children array
const childrenField = (config.childrenField as string) || 'children';
if (row[childrenField] && Array.isArray(row[childrenField])) {
return row[childrenField].length > 0;
}
// Check for subRows (TanStack Table convention)
if (row.subRows && Array.isArray(row.subRows)) {
return row.subRows.length > 0;
}
// Check for boolean flag (common pattern)
if (typeof row.hasChildren === 'boolean') {
return row.hasChildren;
}
// Check for childCount property
if (typeof row.childCount === 'number') {
return row.childCount > 0;
}
// Default: assume no children
return false;
}
/**
* Helper to insert children into data array at parent location
* Used by lazy loading to update the data array with fetched children
* @param data - Current data array
* @param parentId - ID of parent node
* @param children - Children to insert
* @param idField - Field name containing node ID
* @returns Updated data array with children inserted
*/
export function insertChildrenIntoData<T extends Record<string, any>>(
data: T[],
parentId: string,
children: T[],
idField: keyof T | string = 'id',
): T[] {
return data.map((item) => {
if (item[idField] === parentId) {
// Found the parent - add children as subRows
return { ...item, subRows: children };
}
// Recursively search in subRows
if (item.subRows && Array.isArray(item.subRows)) {
return {
...item,
subRows: insertChildrenIntoData(item.subRows, parentId, children, idField),
};
}
return item;
});
}