PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / admin / doc-categories / utils / treeUtils.js

treeUtils.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/admin/doc-categories/utils/treeUtils.js

168 lines 4.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Build a hierarchical tree from a flat array of WP term objects.
3 * Each term has: id, parent, name, slug, description, count, meta.
4 */
5 export function buildTree(terms) {
6 const map = {};
7 const roots = [];
8
9 // Match the legacy admin/frontend order: ascending by the BetterDocs
10 // `doc_category_order` term meta. Terms missing the meta fall back to 0 (top),
11 // mirroring the old LEFT JOIN behaviour. Array.sort is stable, so ties keep the
12 // incoming order (fetched by id ascending). Sorting the flat list also orders
13 // siblings within each parent, since buildTree appends children in array order.
14 const sorted = [...terms].sort(
15 (a, b) =>
16 Number(a.doc_category_order ?? 0) - Number(b.doc_category_order ?? 0),
17 );
18
19 sorted.forEach((t) => {
20 map[t.id] = { ...t, children: [] };
21 });
22
23 sorted.forEach((t) => {
24 if (t.parent && map[t.parent]) {
25 map[t.parent].children.push(map[t.id]);
26 } else {
27 roots.push(map[t.id]);
28 }
29 });
30
31 return roots;
32 }
33
34 /**
35 * Flatten a tree into display rows, tracking depth and connector-line state.
36 * Returns [{node, depth, isLast, parentLastChain, hasChildren, isOpen}]
37 */
38 export function flattenTree(nodes, expanded, depth = 0, parentLastChain = []) {
39 const rows = [];
40
41 nodes.forEach((node, i) => {
42 const isLast = i === nodes.length - 1;
43 const hasChildren = node.children && node.children.length > 0;
44 const isOpen = expanded.has(node.id);
45
46 rows.push({
47 node,
48 depth,
49 isLast,
50 parentLastChain: [...parentLastChain],
51 hasChildren,
52 isOpen,
53 });
54
55 if (hasChildren && isOpen) {
56 const childRows = flattenTree(
57 node.children,
58 expanded,
59 depth + 1,
60 [...parentLastChain, isLast],
61 );
62 rows.push(...childRows);
63 }
64 });
65
66 return rows;
67 }
68
69 export function findNode(nodes, id) {
70 for (const node of nodes) {
71 if (node.id === id) return node;
72 if (node.children?.length) {
73 const found = findNode(node.children, id);
74 if (found) return found;
75 }
76 }
77 return null;
78 }
79
80 export function removeNode(nodes, id) {
81 return nodes
82 .filter((n) => n.id !== id)
83 .map((n) => ({
84 ...n,
85 children: removeNode(n.children || [], id),
86 }));
87 }
88
89 export function isDescendant(node, targetId) {
90 if (!node.children?.length) return false;
91 return node.children.some(
92 (c) => c.id === targetId || isDescendant(c, targetId),
93 );
94 }
95
96 /** Collect all ids in a subtree (the node itself + all descendants). */
97 export function collectIds(node) {
98 const ids = [node.id];
99 (node.children || []).forEach((c) => ids.push(...collectIds(c)));
100 return ids;
101 }
102
103 /**
104 * Insert newNode relative to targetId:
105 * pos = 'before' | 'after' | 'inside'
106 */
107 export function insertNode(nodes, targetId, pos, newNode) {
108 const result = [];
109
110 for (let i = 0; i < nodes.length; i++) {
111 const node = nodes[i];
112
113 if (pos === 'before' && node.id === targetId) {
114 result.push({ ...newNode, children: newNode.children || [] });
115 }
116
117 if (node.id === targetId && pos === 'inside') {
118 result.push({
119 ...node,
120 children: [
121 { ...newNode, children: newNode.children || [] },
122 ...(node.children || []),
123 ],
124 });
125 } else {
126 result.push({
127 ...node,
128 children: insertNode(node.children || [], targetId, pos, newNode),
129 });
130 }
131
132 if (pos === 'after' && node.id === targetId) {
133 result.push({ ...newNode, children: newNode.children || [] });
134 }
135 }
136
137 return result;
138 }
139
140 /** Generate a slug from a name string. */
141 export function slugify(name) {
142 return name
143 .toLowerCase()
144 .replace(/[^a-z0-9\s-]/g, '')
145 .trim()
146 .replace(/\s+/g, '-');
147 }
148
149 /**
150 * Mirror WordPress core's wp_unique_term_slug() behaviour: if `base` already
151 * exists in `existingSlugs`, append -2, -3, … until the slug is unique.
152 *
153 * @param {string} base - The desired slug (already slugified).
154 * @param {string[]} existingSlugs - All slugs currently in use.
155 * @param {string} [ownSlug] - The term's current slug when editing
156 * (excluded from the conflict check so a
157 * no-op save doesn't bump its own suffix).
158 */
159 export function makeUniqueSlug(base, existingSlugs, ownSlug = null) {
160 const others = ownSlug
161 ? existingSlugs.filter((s) => s !== ownSlug)
162 : existingSlugs;
163 if (!others.includes(base)) return base;
164 let n = 2;
165 while (others.includes(`${base}-${n}`)) n++;
166 return `${base}-${n}`;
167 }
168