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 / components / TreeRow.js

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

208 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useRef, useEffect } from "react";
2 import { __ } from "@wordpress/i18n";
3 import { applyFilters } from "@wordpress/hooks";
4 import { decodeEntities } from "@wordpress/html-entities";
5 import ConnectorColumn from "./ConnectorColumn";
6
7 // Right-pointing chevron from Claude design — rotates 90° when expanded
8 const ChevronIcon = () => (
9 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
10 <path d="m9 6 6 6-6 6" />
11 </svg>
12 );
13
14 const GripIcon = () => (
15 <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
16 <circle cx="9" cy="6" r="1.4" />
17 <circle cx="9" cy="12" r="1.4" />
18 <circle cx="9" cy="18" r="1.4" />
19 <circle cx="15" cy="6" r="1.4" />
20 <circle cx="15" cy="12" r="1.4" />
21 <circle cx="15" cy="18" r="1.4" />
22 </svg>
23 );
24
25 const PlusIcon = () => (
26 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
27 <path d="M6.5 1V12M1 6.5H12" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
28 </svg>
29 );
30
31 const EditIcon = () => (
32 <svg width="13" height="13" viewBox="0 0 13 13" fill="none" aria-hidden="true">
33 <path d="M8.5 1.5L11.5 4.5L4 12H1V9L8.5 1.5Z" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" />
34 </svg>
35 );
36
37 // Delete glyph shared with the AI Chatbot Logs row actions, so the control
38 // reads the same in every admin table. Drawn on a 24 viewBox at 16px
39 // (effective stroke ~1.33px) rather than the 13px/1.3 glyphs beside it.
40 const TrashIcon = () => (
41 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
42 <path d="M3 6h18" />
43 <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
44 <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
45 </svg>
46 );
47
48 const TreeRow = ({
49 rowData,
50 isSelected,
51 isHighlighted,
52 onSelect,
53 onToggle,
54 onEdit,
55 onAddChild,
56 onDelete,
57 dragOver,
58 onDragStart,
59 onDragOver,
60 onDragLeave,
61 onDrop,
62 }) => {
63 const { node, depth, isLast, parentLastChain, hasChildren, isOpen } = rowData;
64 const rowRef = useRef(null);
65
66 const dropZone = dragOver?.id === node.id ? dragOver.pos : null;
67 const childCount = node.children?.length || 0;
68
69 // Bring a freshly created row into view after the page jumps to it.
70 useEffect(() => {
71 if (isHighlighted && rowRef.current) {
72 rowRef.current.scrollIntoView({ block: "nearest", behavior: "smooth" });
73 }
74 }, [isHighlighted]);
75
76 return (
77 <div
78 ref={rowRef}
79 className={[
80 "bd-dc-row",
81 isSelected ? "bd-dc-row--selected" : "",
82 isHighlighted ? "bd-dc-row--highlight" : "",
83 dropZone === "before" ? "bd-dc-row--drop-before" : "",
84 dropZone === "after" ? "bd-dc-row--drop-after" : "",
85 dropZone === "inside" ? "bd-dc-row--drop-inside" : "",
86 ].filter(Boolean).join(" ")}
87 draggable
88 onDragStart={(e) => onDragStart(e, node)}
89 onDragOver={(e) => onDragOver(e, node, rowRef)}
90 onDragLeave={onDragLeave}
91 onDrop={(e) => onDrop(e, node)}
92 data-id={node.id}
93 >
94 {/* Grip */}
95 <span className="bd-dc-grip" title={__("Drag to reorder", "betterdocs")}>
96 <GripIcon />
97 </span>
98
99 {/* Checkbox */}
100 <input
101 type="checkbox"
102 className="bd-dc-checkbox"
103 checked={isSelected}
104 onChange={() => onSelect(node.id)}
105 aria-label={__("Select", "betterdocs")}
106 />
107
108 {/* Connector lines */}
109 <ConnectorColumn depth={depth} isLast={isLast} parentLastChain={parentLastChain} />
110
111 {/* Chevron expand/collapse — always green when node has children */}
112 {hasChildren ? (
113 <button
114 type="button"
115 className={`bd-dc-chevron ${isOpen ? "bd-dc-chevron--open" : ""}`}
116 onClick={() => onToggle(node.id)}
117 aria-label={isOpen ? __("Collapse", "betterdocs") : __("Expand", "betterdocs")}
118 >
119 <ChevronIcon />
120 </button>
121 ) : (
122 <span className="bd-dc-chevron-placeholder" />
123 )}
124
125 {/* Category icon thumbnail */}
126 {node.thumbnail && (
127 <span className="bd-dc-row-icon" aria-hidden="true">
128 <img src={node.thumbnail} alt="" />
129 </span>
130 )}
131
132 {/* Name & meta */}
133 <div className="bd-dc-cell-name">
134 <span
135 className="bd-dc-name"
136 onClick={() => onToggle(node.id)}
137 role="button"
138 tabIndex={0}
139 onKeyDown={(e) => e.key === "Enter" && onToggle(node.id)}
140 >
141 {decodeEntities(node.name)}
142 </span>
143
144 <div className="bd-dc-meta-row">
145 {node.slug && (
146 <span className="bd-dc-slug-badge">/{node.slug}</span>
147 )}
148 {hasChildren && childCount > 0 && (
149 <span className="bd-dc-children-count">
150 &middot; {childCount}{" "}
151 {childCount === 1
152 ? __("child", "betterdocs")
153 : __("children", "betterdocs")}
154 </span>
155 )}
156 </div>
157
158 <span className={`bd-dc-desc${!node.description ? " bd-dc-desc--empty" : ""}`}>
159 {node.description
160 ? decodeEntities(node.description)
161 : __("No description", "betterdocs")}
162 </span>
163 </div>
164
165 {/* Extra table column cell slot (e.g. Pro Knowledge Base column). */}
166 {applyFilters("betterdocs_doc_category_table_row_column", null, node)}
167
168 {/* Count badge */}
169 <div className="bd-dc-cell-count">
170 <span className="bd-dc-count-badge">{node.count}</span>
171 </div>
172
173 {/* Row actions — icon-only buttons, visible on row hover */}
174 <div className="bd-dc-cell-actions">
175 <button
176 type="button"
177 className="bd-dc-row-action bd-dc-row-action-icon"
178 onClick={() => onAddChild(node)}
179 title={__("Add child", "betterdocs")}
180 aria-label={__("Add child category", "betterdocs")}
181 >
182 <PlusIcon />
183 </button>
184 <button
185 type="button"
186 className="bd-dc-row-action bd-dc-row-action-icon"
187 onClick={() => onEdit(node)}
188 title={__("Edit", "betterdocs")}
189 aria-label={__("Edit category", "betterdocs")}
190 >
191 <EditIcon />
192 </button>
193 <button
194 type="button"
195 className="bd-dc-row-action bd-dc-row-action-icon bd-dc-row-action-delete"
196 onClick={() => onDelete(node)}
197 title={__("Delete", "betterdocs")}
198 aria-label={__("Delete category", "betterdocs")}
199 >
200 <TrashIcon />
201 </button>
202 </div>
203 </div>
204 );
205 };
206
207 export default TreeRow;
208