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 / faq-builder / components / index.js

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

257 lines 10.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useEffect, useMemo, useState } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import Pagination from "../../doc-categories/components/Pagination";
4 import { IconPlus } from "../../../icons";
5 import CategoryLoader from "../utils/CategoryLoader";
6 import CreateFaqLoader from "../utils/CreateFaqLoader";
7 import { getAllCategories, getSearchedFaqCategories, findCategoryPage } from "../utils/function";
8 import EmptyDashboard from "./../utils/EmptyDashboard";
9 import CategoryList from "./CategoryList";
10 import CategoryModal from "./CategoryModal";
11 import CreateFaq from "./CreateFaq";
12 import FaqStats from "./FaqStats";
13 import { useFAQ } from "../../components/FAQContext";
14 import { useFAQScope } from "../context/ScopeContext";
15 import { useOutletContext, useSearchParams } from "react-router-dom";
16 import { makeRouteURL, history } from "../../../utils/history";
17 import SearchedCategoryList from "./SearchedCategoryList";
18 import { __ } from "@wordpress/i18n";
19
20
21 const BetterDocsFaqRoot = () => {
22 const {
23 categories,
24 setCategories,
25 reFetchCategories,
26 setReFetchCategories,
27 focusTarget,
28 setFocusTarget,
29 highlightGroupId,
30 setHighlightGroupId,
31 highlightFaqId,
32 setHighlightFaqId,
33 } = useFAQ();
34 const { taxonomy, isProduct } = useFAQScope();
35 const [perPage, setPerPage] = useState(10);
36 const [currentPage, setCurrentPage] = useState(1);
37 const [, , , faqKeyword, faqOrder = 'default'] = useOutletContext();
38 // "Add New FAQ" from the dashboard header lands here with ?faq_action=create
39 // and opens an inline create form at the top of the group list (once).
40 const [searchParams] = useSearchParams();
41 const [showCreateFaqTop, setShowCreateFaqTop] = useState(
42 searchParams.get("faq_action") === "create"
43 );
44 const closeCreateFaqTop = () => {
45 setShowCreateFaqTop(false);
46 // Drop ?faq_action=create via the admin router's URL builder so the
47 // address stays a proper admin.php URL. (React Router's setSearchParams
48 // would rebuild a broken SPA-style path from the custom history's
49 // page-derived pathname, e.g. /betterdocs-faq?page=betterdocs-faq.)
50 if (searchParams.get("faq_action")) {
51 history.replace(makeRouteURL("betterdocs-faq", null, ["faq_action"]));
52 }
53 };
54 // Search runs against the active scope's taxonomy, so it works on both the
55 // General builder and the WooCommerce Product FAQ Groups sub-tab.
56 const searchMode = Boolean(faqKeyword?.length);
57
58 // Reset to page 1 whenever the sort order changes.
59 const prevFaqOrder = React.useRef(faqOrder);
60 useEffect(() => {
61 if (prevFaqOrder.current !== faqOrder) {
62 prevFaqOrder.current = faqOrder;
63 setCurrentPage(1);
64 }
65 }, [faqOrder]);
66
67 // After a FAQ is created (?faq_action=create) or moved into a group via
68 // Edit, reveal the destination group (jump to the page it lives on + expand
69 // it) and arm the flash for the FAQ row itself. The create/move handler
70 // sets `focusTarget` only after its API call resolves, so locating the page
71 // off a fresh server query is accurate even for the "most questions" order
72 // (whose position depends on the count that just changed). "uncategorized"
73 // lives in its own (unpaginated) panel, so it only needs the expand.
74 useEffect(() => {
75 if (!focusTarget) return;
76 const { groupId, faqId } = focusTarget;
77 setFocusTarget(null);
78 setHighlightFaqId(faqId != null ? String(faqId) : null);
79
80 if (groupId === "uncategorized") {
81 setHighlightGroupId("uncategorized");
82 return;
83 }
84
85 findCategoryPage({ taxonomy, orderKey: faqOrder, perPage, targetId: groupId })
86 .then((page) => {
87 setCurrentPage((cur) => (page && page !== cur ? page : cur));
88 setHighlightGroupId(String(groupId));
89 })
90 .catch(() => setHighlightGroupId(String(groupId)));
91 }, [focusTarget]);
92
93 // The group only needs the expand trigger briefly — once a Category reads
94 // it and opens, the open state persists on its own. Clear so a later manual
95 // collapse isn't re-opened by a stale value.
96 useEffect(() => {
97 if (highlightGroupId == null) return;
98 const t = setTimeout(() => setHighlightGroupId(null), 2500);
99 return () => clearTimeout(t);
100 }, [highlightGroupId]);
101
102 // The FAQ row consumes highlightFaqId the moment it renders (it may take a
103 // beat for the group to expand and its FAQ list to load). This is just a
104 // fallback so the signal doesn't linger if that row never mounts.
105 useEffect(() => {
106 if (highlightFaqId == null) return;
107 const t = setTimeout(() => setHighlightFaqId(null), 6000);
108 return () => clearTimeout(t);
109 }, [highlightFaqId]);
110
111 const {
112 isLoading: categoriesLoading,
113 isFetching: categoriesFetching,
114 data: allFaqCategories,
115 refetch,
116 } = useQuery(["allFaqCategories", perPage, currentPage, faqOrder, taxonomy], getAllCategories, {
117 keepPreviousData: true,
118 enabled: !searchMode ? true : false,
119 });
120
121 const {
122 isLoading: searchedFaqResultsLoading,
123 data: faqSearchedData,
124 isPreviousData: searchedFaqIsPreviousData,
125 refetch:refetchSearchedFaq
126 } = useQuery(["allFaqSearchedData", faqKeyword, taxonomy], getSearchedFaqCategories, {
127 keepPreviousData: true,
128 enabled: searchMode ? true : false,
129 });
130
131 // `keepPreviousData` keeps the previous keyword's results on screen while the
132 // new keyword fetches — which makes the list visibly "jump" when the fresh
133 // results swap in. Show the skeleton loader for that transition instead.
134 // `isPreviousData` is true only while stale data is being served for a new
135 // query key, so a same-keyword background refetch (e.g. after editing a FAQ
136 // within the results) won't flash the skeleton.
137 const searchedFaqResultsLoadingOrStale =
138 searchMode && (searchedFaqResultsLoading || searchedFaqIsPreviousData);
139
140 useEffect(() => {
141 setReFetchCategories(() => refetch);
142 }, [refetch]);
143
144 useEffect(() => {
145 if (allFaqCategories?.data) {
146 setCategories(allFaqCategories.data);
147 }
148 }, [allFaqCategories]);
149
150 const [uncategorizedLoading, setUncategorizedLoading] = useState(true);
151 const [uncategorizedEmpty, setUncategorizedEmpty] = useState(true);
152 const [showCategoryModal, setShowCategoryModal] = useState(false);
153 const [updateCategory, setUpdateCategory] = useState(null);
154
155 const isLoading = useMemo(
156 () => categoriesLoading || uncategorizedLoading,
157 [categoriesLoading, uncategorizedLoading]
158 );
159
160 const isEmptyDashboard = useMemo(
161 () => !categories.length && uncategorizedEmpty,
162 [categories, uncategorizedEmpty]
163 );
164
165 return (
166 <>
167 {searchedFaqResultsLoadingOrStale && <CategoryLoader />}
168 {searchMode && !searchedFaqResultsLoadingOrStale && (
169 <SearchedCategoryList
170 isLoading={searchedFaqResultsLoading}
171 categoriesLoading={searchedFaqResultsLoading}
172 allFaqCategories={faqSearchedData}
173 reFetchCategories={refetchSearchedFaq}
174 setUpdateCategory={setUpdateCategory}
175 setUncategorizedLoading={setUncategorizedLoading}
176 setUncategorizedEmpty={setUncategorizedEmpty}
177 />
178 )}
179 {(searchMode && !searchedFaqResultsLoadingOrStale && faqSearchedData?.length == 0) && (
180 <EmptyDashboard
181 reFetchCategories={refetch}
182 setShowCategoryModal={setShowCategoryModal}
183 showFaqActionButtons={false}
184 showNewFaqText={false}
185 />
186 )}
187 {!searchMode && isLoading && <CategoryLoader />}
188 {!searchMode && !isLoading && isEmptyDashboard && !showCreateFaqTop && (
189 <EmptyDashboard
190 reFetchCategories={refetch}
191 setShowCategoryModal={setShowCategoryModal}
192 showSampleFaqButton={true}
193 contentType={isProduct ? "product_faq" : "faq"}
194 />
195 )}
196 {!searchMode && !isLoading && !isEmptyDashboard && (
197 <FaqStats
198 groupsTotal={allFaqCategories?.total}
199 refreshSignal={allFaqCategories}
200 />
201 )}
202 {!searchMode && showCreateFaqTop && (
203 isLoading ? (
204 <CreateFaqLoader />
205 ) : (
206 <CreateFaq
207 topLevel
208 groups={categories}
209 reFetchFaqList={refetch}
210 reFetchCategories={refetch}
211 setCreateFaqAt={() => {}}
212 onClose={closeCreateFaqTop}
213 />
214 )
215 )}
216 {!searchMode && (
217 <CategoryList
218 isLoading={isLoading}
219 categoriesLoading={categoriesLoading}
220 categoriesFetching={categoriesFetching}
221 allFaqCategories={categories}
222 reFetchCategories={refetch}
223 setUpdateCategory={setUpdateCategory}
224 setUncategorizedLoading={setUncategorizedLoading}
225 setUncategorizedEmpty={setUncategorizedEmpty}
226 perPage={perPage}
227 currentPage={currentPage}
228 order={faqOrder}
229 highlightGroupId={highlightGroupId}
230 />
231 )}
232 {!searchMode && (
233 <Pagination
234 total={Number(allFaqCategories?.total)}
235 page={currentPage}
236 perPage={perPage}
237 onPageChange={(p) => setCurrentPage(p)}
238 onPerPageChange={(pp) => setPerPage(pp)}
239 />
240 )}
241 {showCategoryModal || updateCategory !== null ? (
242 <CategoryModal
243 open={true}
244 reFetchCategories={refetch}
245 setShowCategoryModal={setShowCategoryModal}
246 updateCategory={updateCategory}
247 setUpdateCategory={setUpdateCategory}
248 />
249 ) : (
250 ""
251 )}
252 </>
253 );
254 };
255
256 export default BetterDocsFaqRoot;
257