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

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

418 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { Suspense, useEffect } from "react";
2 import { useRoutes, Link } from "react-router-dom";
3
4 import { addFilter, applyFilters } from "@wordpress/hooks";
5 import { addQueryArgs } from "@wordpress/url";
6 import { __ } from "@wordpress/i18n";
7 import apiFetch from "@wordpress/api-fetch";
8
9 import Layout from "./Layout";
10 import CreateFAQGroup from "./CreateFAQGroup";
11 import CreateProductFAQGroup from "./CreateProductFAQGroup";
12 import CreateDocCategory from "./CreateDocCategory";
13 import CreateDocTag from "./CreateDocTag";
14 import { IconGrid, IconList, IconPlus } from "~betterdocs/icons";
15 import { makeRouteURL, history } from "~betterdocs/utils";
16 import Settings from "~betterdocs/admin/settings";
17 import Dashboard from "~betterdocs/admin/dashboard";
18 import DashboardOverview from "~betterdocs/admin/dashboard-overview";
19 import AIChatbot from "~betterdocs/admin/ai-chatbot";
20 import { FAQProvider } from "./FAQContext";
21 import { DocCategoriesProvider, useDocCategories } from "./DocCategoriesContext";
22 import { DocTagsProvider, useDocTags } from "./DocTagsContext";
23 import FaqBuilder from "~betterdocs/admin/faq-builder";
24 import Analytics from "~betterdocs/admin/analytics";
25 import ApiDocsTeaser from "~betterdocs/admin/api-docs";
26 import ContentIntelligenceTeaser from "~betterdocs/admin/content-intelligence";
27 import GlossaryTeaser from "~betterdocs/admin/glossaries/GlossaryTeaser";
28 import DocCategories from "~betterdocs/admin/doc-categories";
29 import DocTags from "~betterdocs/admin/doc-tags";
30 import Mcp from "~betterdocs/admin/mcp";
31 import { useSearchParams } from "react-router-dom";
32
33
34 const ComponentLoader = ({ Component, fallback }) => {
35 return (
36 <Suspense fallback={fallback ?? "Loading..."}>
37 <Component />
38 </Suspense>
39 );
40 };
41
42 const AddNewPost = () => {
43 return (
44 <a href="post-new.php?post_type=docs" className="admin-action-button">
45 <span className="action-button-icon">
46 <IconPlus />
47 </span>
48 <span className="action-button-icon-text">{__("Add New Docs", "betterdocs")}</span>
49 </a>
50 );
51 };
52
53 // Reusable plus glyph for the Dashboard's "Add" buttons (fill varies per button).
54 const PlusIcon = ({ fill }) => (
55 <span className="plus-icon">
56 <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
57 <path d="M5.16602 5.16406V0.164062H6.83268V5.16406H11.8327V6.83073H6.83268V11.8307H5.16602V6.83073H0.166016V5.16406H5.16602Z" fill={fill} />
58 </svg>
59 </span>
60 );
61
62 // Dashboard "create" actions — shown in the logo bar like every other screen's
63 // create button (registered via betterdocs_admin_header_action below).
64 const AddNewItems = () => (
65 <div className="betterdocs-addNewItems-button-group">
66 <Link
67 to={makeRouteURL("betterdocs-faq", { faq_action: "create" }, [], false)}
68 className="betterdocs-addNewItems-button betterdocs-faq-button"
69 >
70 <PlusIcon fill="#344054" />
71 <span>{__("Add New FAQ", "betterdocs")}</span>
72 </Link>
73 <a
74 href="post-new.php?post_type=docs"
75 className="betterdocs-addNewItems-button betterdocs-doc-button"
76 >
77 <PlusIcon fill="white" />
78 <span>{__("Add New Docs", "betterdocs")}</span>
79 </a>
80 </div>
81 );
82
83 const DocIcon = () => (
84 <svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true" style={{ flexShrink: 0 }}>
85 <path d="M3 1.5H8.5L11.5 4.5V12.5H3V1.5Z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" />
86 <path d="M8.5 1.5V4.5H11.5" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round" />
87 <path d="M5 7H9M5 9H7.5" stroke="currentColor" strokeWidth="1.1" strokeLinecap="round" />
88 </svg>
89 );
90
91 // Count badge component — reads from context so it stays in sync with loaded data.
92 const DocCategoryCountBadge = () => {
93 const ctx = useDocCategories();
94 if (!ctx || ctx.totalCount === null) return null;
95 return (
96 <span className="bd-dc-header-count-badge">
97 <DocIcon />
98 {ctx.totalCount} {__("total", "betterdocs")}
99 </span>
100 );
101 };
102
103 const DocTagCountBadge = () => {
104 const ctx = useDocTags();
105 if (!ctx || ctx.totalCount === null) return null;
106 return (
107 <span className="bd-dc-header-count-badge">
108 <DocIcon />
109 {ctx.totalCount} {__("total", "betterdocs")}
110 </span>
111 );
112 };
113
114 addFilter("betterdocs_admin_header_action", "betterdocs", (content, page) => {
115 switch (page) {
116 case "betterdocs-dashboard":
117 return <AddNewItems />;
118 case "betterdocs-admin":
119 return <AddNewPost />;
120 case "betterdocs-faq":
121 return (
122 <>
123 <CreateFAQGroup />
124 <CreateProductFAQGroup />
125 </>
126 );
127 case "betterdocs-doc-categories":
128 return <CreateDocCategory />;
129 case "betterdocs-doc-tags":
130 return <CreateDocTag />;
131 }
132 return content;
133 });
134
135 addFilter("betterdocs_admin_header_title", "betterdocs", (content, page) => {
136 switch (page) {
137 case "betterdocs-faq":
138 return "FAQ Builder";
139 case "betterdocs-dashboard":
140 return "Dashboard";
141 case "betterdocs-admin":
142 return "All Docs";
143 case "betterdocs-settings":
144 return "Settings";
145 case "betterdocs-analytics":
146 return "Analytics";
147 // Since U37 the MCP screen renders its own header row into the shell's
148 // container (`mcp/components/McpHeader.js`) and `Header.js` skips the
149 // default row for it, so this case no longer reaches the page — it is
150 // kept as the answer for anyone who applies the filter, exactly as the
151 // analytics case is kept here and in Pro.
152 case "betterdocs-mcp":
153 return "MCP";
154 case "betterdocs-glossaries":
155 return "Glossaries";
156 case "betterdocs-ai-chatbot":
157 return "AI Chatbot";
158 case "betterdocs-api-docs":
159 return "API Documentation";
160 case "betterdocs-content-iq":
161 return "Content Intelligence";
162 case "betterdocs-doc-categories":
163 return "Doc Categories";
164 case "betterdocs-doc-tags":
165 return "Doc Tags";
166 }
167 return content;
168 });
169
170 addFilter("betterdocs_admin_header_count", "betterdocs", (content, page) => {
171 if (page === "betterdocs-doc-categories") return <DocCategoryCountBadge />;
172 if (page === "betterdocs-doc-tags") return <DocTagCountBadge />;
173 return content;
174 });
175
176 addFilter(
177 "betterdocs_admin_header_action_icons",
178 "betterdocs",
179 (content, page, location, mode) => {
180 let newContents = [];
181 if ((mode == null && page === "betterdocs-admin") || (mode == 'grid' && page === "betterdocs-admin")) {
182 newContents.push(
183 <li key={2}>
184 <Link
185 onClick={(e) => {
186 betterdocs_admin.dashboard_mode = '';
187 apiFetch({
188 method: "POST",
189 path: "betterdocs/v1/dashboard-mode",
190 data: { dashboard_mode: 'list' }
191 });
192 }}
193 to={makeRouteURL("betterdocs-admin", { mode: "list", post_status: "any" })}
194 className={`betterdocs-dashboard-header-action betterdocs-dashboard-header-action-list ${location.search.includes('mode=list') ? 'active' : ''}`}
195 >
196 <IconList />
197 </Link>
198 </li>
199 );
200 return [...newContents, ...content];
201 } else if (mode == 'list' && page === "betterdocs-admin") {
202 newContents.push(
203 <li key={3}>
204 <Link
205 onClick={(e) => {
206 betterdocs_admin.dashboard_mode = '';
207 apiFetch({
208 method: "POST",
209 path: "betterdocs/v1/dashboard-mode",
210 data: { dashboard_mode: 'grid' }
211 });
212 }}
213 to={makeRouteURL("betterdocs-admin", { mode: "grid" }, [], false)}
214 className={`betterdocs-dashboard-header-action betterdocs-dashboard-header-action-grid ${location.search.includes('mode=grid') ? 'active' : ''}`}
215 >
216 <IconGrid />
217 </Link>
218 </li>
219 );
220 return [...newContents, ...content];
221 }
222 return content;
223 },
224 );
225
226 let getSiblings = function (e) {
227 let siblings = [];
228 if (!e.parentNode) return siblings;
229 let sibling = e.parentNode.firstChild;
230 while (sibling) {
231 if (sibling.nodeType === 1 && sibling !== e) siblings.push(sibling);
232 sibling = sibling.nextSibling;
233 }
234 return siblings;
235 };
236
237 const findRoutes = (carry, route) => {
238 if (route?.children) return route.children.reduce(findRoutes, carry);
239 if (route?.path) carry.push(route.path);
240 return carry;
241 };
242
243 const Routes = () => {
244 const [searchParams] = useSearchParams();
245
246 const routes = applyFilters("betterdocs_routes_wrapper", [
247 {
248 element: (
249 <FAQProvider>
250 <DocCategoriesProvider>
251 <DocTagsProvider>
252 <Layout />
253 </DocTagsProvider>
254 </DocCategoriesProvider>
255 </FAQProvider>
256 ),
257 children: applyFilters("betterdocs_routes", [
258 {
259 path: "betterdocs-admin",
260 element: <ComponentLoader Component={Dashboard} />,
261 exact: true,
262 index: true,
263 },
264 {
265 path: "betterdocs-dashboard",
266 element: <ComponentLoader Component={DashboardOverview} />,
267 exact: true,
268 index: true,
269 },
270 {
271 path: "betterdocs-settings",
272 element: <ComponentLoader Component={Settings} />,
273 exact: true,
274 },
275 {
276 path: "betterdocs-mcp",
277 element: <ComponentLoader Component={Mcp} />,
278 exact: true,
279 },
280 {
281 path: "betterdocs-faq",
282 element: <ComponentLoader Component={FaqBuilder} />,
283 exact: true,
284 },
285 {
286 path: "betterdocs-analytics",
287 element: <ComponentLoader Component={Analytics} />,
288 exact: true,
289 },
290 {
291 path: "betterdocs-doc-categories",
292 element: <ComponentLoader Component={DocCategories} />,
293 exact: true,
294 },
295 {
296 path: "betterdocs-doc-tags",
297 element: <ComponentLoader Component={DocTags} />,
298 exact: true,
299 },
300 ...(betterdocs_admin?.betterdocs_ChatBot_plugin == false ? [
301 {
302 path: "betterdocs-ai-chatbot",
303 element: <ComponentLoader Component={AIChatbot} />,
304 exact: true,
305 index: true,
306 }
307 ] : []),
308 // Free's locked API Docs teaser — PHP decides whether it belongs
309 // here, since it would otherwise shadow Pro's route. Truthy check:
310 // wp_localize_script sends "1" / "", never a real boolean.
311 ...(betterdocs_admin?.api_docs_teaser ? [
312 {
313 path: "betterdocs-api-docs",
314 element: <ComponentLoader Component={ApiDocsTeaser} />,
315 exact: true,
316 }
317 ] : []),
318 // Free's locked Glossaries teaser. Glossaries is Pro — the whole
319 // manager lives there and injects its own route on this same
320 // path, so Free must not register anything here while Pro is
321 // active or it would shadow it. Truthy check: wp_localize_script
322 // sends "1" / "", never a real boolean.
323 ...(betterdocs_admin?.glossaries_teaser ? [
324 {
325 path: "betterdocs-glossaries",
326 element: <ComponentLoader Component={GlossaryTeaser} />,
327 exact: true,
328 }
329 ] : []),
330 // Free's locked Content Intelligence teaser — same deal as API
331 // Docs above: PHP owns the decision so this never shadows Pro's
332 // route. Truthy check: wp_localize_script sends "1" / "", never
333 // a real boolean.
334 ...(betterdocs_admin?.content_intelligence_teaser ? [
335 {
336 path: "betterdocs-content-iq",
337 element: <ComponentLoader Component={ContentIntelligenceTeaser} />,
338 exact: true,
339 }
340 ] : [])
341 ]),
342 },
343 ]);
344
345 const routesPath = routes.reduce(findRoutes, []);
346
347 const handleClick = function (e) {
348 const path = this.getAttribute("href");
349 const menu_class = this.getAttribute("class");
350 const page = path.split("=")[1];
351 let args = { page };
352 if (betterdocs_admin.betterdocs_pro_plugin == true && betterdocs_admin.analytics_older == true && menu_class == 'betterdocs-analytics') {
353 e.preventDefault();
354 window.location.href = path;
355 return;
356 }
357
358 if (routesPath.includes(page)) {
359 e.preventDefault();
360 } else {
361 return;
362 }
363
364 if (path) {
365 if (searchParams.get('mode')) args.mode = searchParams.get('mode');
366 let url = addQueryArgs(window.location.href?.split('?')[0], args);
367 history.push(url);
368 const selected = this.parentNode;
369 let siblings = getSiblings(selected);
370 siblings.forEach((element) => element.classList.remove("current"));
371 selected.classList.add("current");
372 document.body.classList.add("betterdocs_page_" + page);
373 }
374 };
375
376 useEffect(() => {
377 const betterDocsMenu = document.querySelectorAll(
378 "#toplevel_page_betterdocs-dashboard a",
379 );
380 betterDocsMenu.forEach((element) => {
381 element.removeEventListener("click", handleClick);
382 element.addEventListener("click", handleClick);
383 });
384 return () => {
385 betterDocsMenu.forEach((element) => {
386 element.removeEventListener("click", handleClick);
387 });
388 };
389 }, [routesPath]);
390
391 // Keep the WP admin submenu highlight (and the page-scoped body class) in sync
392 // with the active route, so SPA navigation that bypasses the menu — e.g. the
393 // dashboard "Add New FAQ" Link — still activates the right menu item.
394 useEffect(() => {
395 const page = searchParams.get("page") || "betterdocs-dashboard";
396 const submenu = document.querySelector(
397 "#toplevel_page_betterdocs-dashboard .wp-submenu",
398 );
399 if (submenu) {
400 submenu.querySelectorAll("li").forEach((li) => {
401 const a = li.querySelector("a");
402 const match = (a?.getAttribute("href") || "").match(/[?&]page=([^&]+)/);
403 const isCurrent = Boolean(match && match[1] === page);
404 li.classList.toggle("current", isCurrent);
405 if (a) a.classList.toggle("current", isCurrent);
406 });
407 }
408 Array.from(document.body.classList)
409 .filter((c) => c.startsWith("betterdocs_page_"))
410 .forEach((c) => document.body.classList.remove(c));
411 document.body.classList.add("betterdocs_page_" + page);
412 }, [searchParams]);
413
414 return useRoutes(routes);
415 };
416
417 export default Routes;
418