| 1 |
import { applyFilters } from "@wordpress/hooks"; |
| 2 |
import { __, sprintf } from "@wordpress/i18n"; |
| 3 |
import React from "react"; |
| 4 |
import Select from "react-select"; |
| 5 |
import KnowledgeBaseFilter from "../dashboard/components/KnowledgeBaseFilter"; |
| 6 |
import { useLocation, useSearchParams } from "react-router-dom"; |
| 7 |
|
| 8 |
import { Link } from "react-router-dom"; |
| 9 |
|
| 10 |
import { IconAutorenew, Logo } from "../../icons/index"; |
| 11 |
|
| 12 |
import { makeRouteURL, history } from "../../utils/history"; |
| 13 |
|
| 14 |
import DarkModeToggle from "./DarkModeToggle"; |
| 15 |
|
| 16 |
// FAQ Builder header "order" options. Rendered with react-select (instead of a |
| 17 |
// native <select>) so the menu sits cleanly below the control — matching the |
| 18 |
// Knowledge Base filter — instead of overlapping the search field. |
| 19 |
const FAQ_ORDER_OPTIONS = [ |
| 20 |
{ value: "default", label: __("Default Order", "betterdocs") }, |
| 21 |
{ value: "most_recent", label: __("Most Recent", "betterdocs") }, |
| 22 |
{ value: "least_recent", label: __("Least Recent", "betterdocs") }, |
| 23 |
{ value: "a_to_z", label: __("A → Z", "betterdocs") }, |
| 24 |
{ value: "z_to_a", label: __("Z → A", "betterdocs") }, |
| 25 |
{ value: "most_questions", label: __("Most Questions", "betterdocs") }, |
| 26 |
]; |
| 27 |
|
| 28 |
const CLASSIC_UI_URLS = { |
| 29 |
"betterdocs-admin": "edit.php?post_type=docs&bdocs_view=classic", |
| 30 |
"betterdocs-faq": "edit-tags.php?taxonomy=betterdocs_faq_category&post_type=betterdocs_faq", |
| 31 |
"betterdocs-glossaries": "edit-tags.php?taxonomy=glossaries&post_type=docs", |
| 32 |
"betterdocs-doc-categories": "edit-tags.php?taxonomy=doc_category&post_type=docs", |
| 33 |
"betterdocs-doc-tags": "edit-tags.php?taxonomy=doc_tag&post_type=docs", |
| 34 |
}; |
| 35 |
|
| 36 |
const Header = ({ |
| 37 |
page, |
| 38 |
filter, |
| 39 |
trash, |
| 40 |
postStatus, |
| 41 |
docs, |
| 42 |
mode, |
| 43 |
faqSearchKeyword, |
| 44 |
handleFAQSearch, |
| 45 |
faqOrder, |
| 46 |
onFaqOrderChange, |
| 47 |
docCategoriesKeyword, |
| 48 |
handleDocCategoriesSearch, |
| 49 |
docTagsKeyword, |
| 50 |
handleDocTagsSearch, |
| 51 |
}) => { |
| 52 |
const [mkbFilter, setMkbFilterValue] = filter; |
| 53 |
const [isLoading, totalItems] = docs; |
| 54 |
const location = useLocation(); |
| 55 |
const [searchParams] = useSearchParams(); |
| 56 |
const faqTab = searchParams.get("faq_tab") || "builder"; |
| 57 |
const faqSubtab = searchParams.get("faq_subtab") || "groups"; |
| 58 |
const isFaqBuilderTab = page === "betterdocs-faq" && faqTab === "builder"; |
| 59 |
// The WooCommerce "Product FAQ Groups" sub-tab lists FAQ groups just like the |
| 60 |
// General builder, so it gets the same search / order / Classic UI controls. |
| 61 |
const isWooGroupsTab = |
| 62 |
page === "betterdocs-faq" && |
| 63 |
faqTab === "woocommerce" && |
| 64 |
faqSubtab === "groups"; |
| 65 |
const showFaqGroupControls = isFaqBuilderTab || isWooGroupsTab; |
| 66 |
const wcActive = Boolean(window.betterdocs_admin?.is_woocommerce_active); |
| 67 |
|
| 68 |
const routeClass = page ? `route-${page.replace(/^betterdocs-/, '')}` : ''; |
| 69 |
|
| 70 |
// FAQ search shows only on the groups tabs (General builder / Woo groups), |
| 71 |
// not on the WooCommerce product-assignment sub-tab. Glossaries is absent |
| 72 |
// here on purpose: it moved to Pro, which renders its own field into the |
| 73 |
// `betterdocs_admin_header_search` slot below. |
| 74 |
const hasSearch = showFaqGroupControls |
| 75 |
|| page === "betterdocs-doc-categories" || page === "betterdocs-doc-tags"; |
| 76 |
|
| 77 |
const searchValue = page === "betterdocs-faq" ? faqSearchKeyword |
| 78 |
: page === "betterdocs-doc-categories" ? docCategoriesKeyword |
| 79 |
: page === "betterdocs-doc-tags" ? docTagsKeyword |
| 80 |
: ""; |
| 81 |
|
| 82 |
const searchHandler = page === "betterdocs-faq" ? handleFAQSearch |
| 83 |
: page === "betterdocs-doc-categories" ? handleDocCategoriesSearch |
| 84 |
: page === "betterdocs-doc-tags" ? handleDocTagsSearch |
| 85 |
: null; |
| 86 |
|
| 87 |
const searchPlaceholder = page === "betterdocs-faq" ? __("Search groups & questions…", "betterdocs") |
| 88 |
: page === "betterdocs-doc-categories" ? __("Search categories…", "betterdocs") |
| 89 |
: page === "betterdocs-doc-tags" ? __("Search tags…", "betterdocs") |
| 90 |
: ""; |
| 91 |
|
| 92 |
// Allow Pro/add-ons to register Classic-UI links for their own React pages |
| 93 |
// (e.g. the Knowledge Base admin page). |
| 94 |
const classicUIUrls = applyFilters("betterdocs_classic_ui_urls", CLASSIC_UI_URLS, page); |
| 95 |
let classicUIUrl = classicUIUrls[page] || null; |
| 96 |
// FAQ: only show Classic UI on the groups tabs, and point the WooCommerce |
| 97 |
// groups tab at the product FAQ category taxonomy. |
| 98 |
if (page === "betterdocs-faq") { |
| 99 |
classicUIUrl = showFaqGroupControls |
| 100 |
? `edit-tags.php?taxonomy=${isWooGroupsTab ? "betterdocs_product_faq_category" : "betterdocs_faq_category"}&post_type=betterdocs_faq` |
| 101 |
: null; |
| 102 |
} |
| 103 |
|
| 104 |
return ( |
| 105 |
<section className={`betterdocs-dashboard-header${routeClass ? ` ${routeClass}` : ''}`}> |
| 106 |
<div className="betterdocs-dashboard-header-top"> |
| 107 |
<div className="betterdocs-dashboard-container"> |
| 108 |
<div className="betterdocs-row betterdocs-logo-action"> |
| 109 |
<div className="betterdocs-col betterdocs-col-xs-6 betterdocs-dashboard-header-left betterdocs-dashboard-header-items"> |
| 110 |
<div className="betterdocs-dashboard-logo"> |
| 111 |
<Logo /> |
| 112 |
</div> |
| 113 |
</div> |
| 114 |
<div className="betterdocs-col betterdocs-col-xs-6 betterdocs-dashboard-header-right betterdocs-dashboard-header-items"> |
| 115 |
{/* Per-page "create" action (Add New Docs/FAQ/Category/…) |
| 116 |
— the Dashboard's Add FAQ/Docs buttons are registered |
| 117 |
here too via betterdocs_admin_header_action (Routes.js). |
| 118 |
The mode switcher lives in the container row below (same |
| 119 |
spot on every screen), not in this sticky logo bar. */} |
| 120 |
{applyFilters("betterdocs_admin_header_action", null, page)} |
| 121 |
</div> |
| 122 |
</div> |
| 123 |
</div> |
| 124 |
</div> |
| 125 |
<div className="betterdocs-dashboard-container"> |
| 126 |
{/* Analytics, Content Intelligence and MCP render their own |
| 127 |
per-route header into this container, so skip the default row |
| 128 |
for them. Each portals the dark-mode switcher back in with its |
| 129 |
own row — dropping this row without it would take the screen's |
| 130 |
only global control with it (analytics/components/Chrome.js, |
| 131 |
content-intelligence/index.js, mcp/components/McpHeader.js). */} |
| 132 |
{page !== "betterdocs-analytics" && page !== "betterdocs-content-iq" && page !== "betterdocs-mcp" && ( |
| 133 |
<div className="betterdocs-row"> |
| 134 |
<div className="betterdocs-col betterdocs-header-left betterdocs-col-xs-6"> |
| 135 |
<h2 className="betterdocs-dashboard-header-title"> |
| 136 |
{applyFilters( |
| 137 |
"betterdocs_admin_header_title", |
| 138 |
__("All Docs", "betterdocs"), |
| 139 |
page, |
| 140 |
)} |
| 141 |
</h2> |
| 142 |
{/* Per-page count badge rendered via filter */} |
| 143 |
{applyFilters("betterdocs_admin_header_count", null, page)} |
| 144 |
{/* The General FAQ / WooCommerce tab strip only makes sense |
| 145 |
when WooCommerce is active — without it there is nothing to |
| 146 |
switch to, so render no tabs and show the builder directly. */} |
| 147 |
{page === "betterdocs-faq" && wcActive && ( |
| 148 |
<div className="betterdocs-faq-page-tabs" role="tablist"> |
| 149 |
<button |
| 150 |
type="button" |
| 151 |
role="tab" |
| 152 |
aria-selected={faqTab === "builder"} |
| 153 |
className={`betterdocs-faq-page-tab${faqTab === "builder" ? " active" : ""}`} |
| 154 |
onClick={() => |
| 155 |
history.push(makeRouteURL("betterdocs-faq", { faq_tab: "builder" })) |
| 156 |
} |
| 157 |
> |
| 158 |
{__("General FAQ", "betterdocs")} |
| 159 |
</button> |
| 160 |
<button |
| 161 |
type="button" |
| 162 |
role="tab" |
| 163 |
aria-selected={faqTab === "woocommerce"} |
| 164 |
className={`betterdocs-faq-page-tab${faqTab === "woocommerce" ? " active" : ""}`} |
| 165 |
onClick={() => |
| 166 |
history.push(makeRouteURL("betterdocs-faq", { faq_tab: "woocommerce" })) |
| 167 |
} |
| 168 |
> |
| 169 |
{__("WooCommerce", "betterdocs")} |
| 170 |
</button> |
| 171 |
</div> |
| 172 |
)} |
| 173 |
{(page == 'betterdocs-admin' && !isLoading && totalItems > 0) && ( |
| 174 |
<> |
| 175 |
<p className="trash-line"></p> |
| 176 |
<Link className="header-trash" to={makeRouteURL("betterdocs-admin", { |
| 177 |
mode: "list", |
| 178 |
post_status: "trash" |
| 179 |
})}> {sprintf(__("Trash (%d)", "betterdocs"), totalItems)} </Link> |
| 180 |
</> |
| 181 |
)} |
| 182 |
</div> |
| 183 |
<div className="betterdocs-col betterdocs-header-right betterdocs-col-xs-6 betterdocs-d-flex betterdocs-justify-end"> |
| 184 |
{/* Multiple-KB filter (Pro) — styled like the FAQ order select, in the header-right control group. */} |
| 185 |
{betterdocs_admin?.multiple_kb == 1 && applyFilters( |
| 186 |
"betterdocs_kb_filter_section", |
| 187 |
'', |
| 188 |
[KnowledgeBaseFilter, mkbFilter, setMkbFilterValue, page, postStatus], |
| 189 |
)} |
| 190 |
{hasSearch && ( |
| 191 |
<div className="betterdocs-header-search-field"> |
| 192 |
<input |
| 193 |
type="text" |
| 194 |
placeholder={searchPlaceholder} |
| 195 |
value={searchValue} |
| 196 |
onChange={searchHandler} |
| 197 |
className="search-input" |
| 198 |
/> |
| 199 |
<span className="betterdocs-header-search-icon"> |
| 200 |
<svg xmlns="http://www.w3.org/2000/svg" width="38" viewBox="0 0 50 50"><path d="M21 3C11.602 3 4 10.602 4 20s7.602 17 17 17c3.355 0 6.46-.984 9.094-2.656l12.281 12.281 4.25-4.25L34.5 30.281C36.68 27.421 38 23.88 38 20c0-9.398-7.602-17-17-17m0 4c7.2 0 13 5.8 13 13s-5.8 13-13 13S8 27.2 8 20 13.8 7 21 7" /></svg> |
| 201 |
</span> |
| 202 |
</div> |
| 203 |
)} |
| 204 |
{/* Header search slot for Pro/add-on React pages (e.g. Knowledge Base). */} |
| 205 |
{!hasSearch && applyFilters("betterdocs_admin_header_search", null, page)} |
| 206 |
{showFaqGroupControls && onFaqOrderChange && ( |
| 207 |
<div className="betterdocs-dropdown-wrapper"> |
| 208 |
<span className="betterdocs-dropdown-icon" aria-hidden="true"> |
| 209 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> |
| 210 |
<path d="M2 4h12M4 8h8M6 12h4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/> |
| 211 |
</svg> |
| 212 |
</span> |
| 213 |
<Select |
| 214 |
className="betterdocs-dropdown-select" |
| 215 |
classNamePrefix="betterdocs-dropdown" |
| 216 |
value={FAQ_ORDER_OPTIONS.find( |
| 217 |
(opt) => opt.value === (faqOrder || "default") |
| 218 |
)} |
| 219 |
options={FAQ_ORDER_OPTIONS} |
| 220 |
onChange={(opt) => onFaqOrderChange(opt?.value || "default")} |
| 221 |
isSearchable={false} |
| 222 |
isClearable={false} |
| 223 |
menuPlacement="bottom" |
| 224 |
/> |
| 225 |
</div> |
| 226 |
)} |
| 227 |
{classicUIUrl && ( |
| 228 |
<div className="betterdocs-header-button"> |
| 229 |
<a |
| 230 |
href={classicUIUrl} |
| 231 |
className="betterdocs-dashboard-header-action betterdocs-classic-ui-btn" |
| 232 |
> |
| 233 |
<span><IconAutorenew /></span>{" "} |
| 234 |
{__("Classic UI", "betterdocs")} |
| 235 |
</a> |
| 236 |
</div> |
| 237 |
)} |
| 238 |
<ul className="betterdocs-dashboard-header-action-list betterdocs-text-align-right"> |
| 239 |
{/* Mode (dark/light) switcher — same spot on every |
| 240 |
screen. Page-specific extras (e.g. the All Docs |
| 241 |
grid/list toggle) are prepended via the filter. |
| 242 |
The Dashboard's Add FAQ/Docs buttons live in the |
| 243 |
sticky logo bar above (betterdocs_admin_header_action), |
| 244 |
not here. Analytics never reaches this row — the whole |
| 245 |
default row is skipped for it above. */} |
| 246 |
{applyFilters( |
| 247 |
"betterdocs_admin_header_action_icons", |
| 248 |
[ |
| 249 |
<li key="dark-mode-toggle"> |
| 250 |
<DarkModeToggle key="dark-mode-button" /> |
| 251 |
</li> |
| 252 |
], |
| 253 |
page, |
| 254 |
location, |
| 255 |
mode |
| 256 |
)} |
| 257 |
</ul> |
| 258 |
</div> |
| 259 |
</div> |
| 260 |
)} |
| 261 |
</div> |
| 262 |
</section> |
| 263 |
); |
| 264 |
}; |
| 265 |
|
| 266 |
export default Header; |
| 267 |
|