All files / blong-browser/src/components/Navigator Navigator.tsx

39.65% Statements 46/116
50% Branches 1/2
0% Functions 0/2
39.65% Lines 46/116

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 1171x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                     1x                                                                                                         1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
/**
 * Navigator — tree view for hierarchical data navigation.
 * Typically placed on the left side of an Explorer.
 */
import {Tree, type TreeNode} from '../../primereact/index.js';
import React, {useState} from 'react';
 
interface INavigatorProps {
    /** Fetch function returning tree nodes */
    fetch?: () => Promise<Record<string, unknown[]>>;
    /** Called with the selected node value */
    onSelect?: (node: Record<string, unknown> | null) => void;
    keyField?: string;
    parentField?: string;
    field?: string;
    title?: string;
    /** Expected key in fetch result */
    resultSet?: string;
    /** Data passed directly (alternative to fetch) */
    data?: Record<string, unknown>[];
    className?: string;
}
 
function buildTree(
    items: Record<string, unknown>[],
    keyField: string,
    parentField: string,
    labelField: string,
): TreeNode[] {
    const roots: TreeNode[] = [];
    const map = new Map<unknown, TreeNode>();

    for (const item of items) {
        const node: TreeNode = {
            key: String(item[keyField]),
            label: String(item[labelField] ?? item[keyField]),
            data: item,
            children: [],
        };
        map.set(item[keyField], node);
    }

    for (const item of items) {
        const parentKey = item[parentField];
        const node = map.get(item[keyField])!;
        if (parentKey == null || !map.has(parentKey)) {
            roots.push(node);
        } else {
            const parent = map.get(parentKey)!;
            parent.children = [...(parent.children ?? []), node];
        }
    }

    return roots;
}

export function Navigator({
    data: staticData,
    fetch: fetchFn,
    onSelect,
    keyField = 'id',
    parentField = 'parentId',
    field = 'name',
    title,
    resultSet,
    className,
}: INavigatorProps) {
    const [treeData, setTreeData] = useState<TreeNode[]>(() => {
        if (staticData) return buildTree(staticData, keyField, parentField, field);
        return [];
    });
    const [loading, setLoading] = useState(false);
    const [selectedKey, setSelectedKey] = useState<string | null>(null);

    React.useEffect(() => {
        if (!fetchFn) return;
        // eslint-disable-next-line @eslint-react/set-state-in-effect
        setLoading(true);
        void fetchFn()
            .then(result => {
                const items = resultSet
                    ? (result[resultSet] as Record<string, unknown>[])
                    : (Object.values(result)[0] as Record<string, unknown>[]);
                setTreeData(buildTree(items ?? [], keyField, parentField, field));
            })
            .finally(() => setLoading(false));
    }, [fetchFn, keyField, parentField, field, resultSet]);

    return (
        <div className={`blong-navigator ${className ?? ''}`}>
            {title && <h4 className="blong-navigator__title">{title}</h4>}
            <Tree
                value={treeData}
                loading={loading}
                selectionKeys={selectedKey ?? undefined}
                onSelectionChange={e => {
                    const key = e.value as string;
                    setSelectedKey(key);
                    // Find the node data
                    const findNode = (nodes: TreeNode[]): TreeNode | undefined => {
                        for (const n of nodes) {
                            if (n.key === key) return n;
                            const found = findNode(n.children ?? []);
                            if (found) return found;
                        }
                    };
                    const node = findNode(treeData);
                    onSelect?.((node?.data as Record<string, unknown>) ?? null);
                }}
                className="blong-navigator__tree"
                filter
                filterPlaceholder="Search..."
            />
        </div>
    );
}