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 / Category.js

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

387 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useRef } from "react";
2 import { decodeEntities } from "@wordpress/html-entities";
3 import { __, _n, sprintf } from "@wordpress/i18n";
4 import React, { useEffect, useState } from "react";
5 import { useQueryClient } from "@tanstack/react-query";
6 import { Draggable } from "react-beautiful-dnd";
7 import { toast } from "react-toastify";
8 import Swal from "sweetalert2";
9 import { categoryStatusChange, deleteFaqCategory, scrollExpandedIntoView } from "./../utils/function";
10 import CreateFaq from "./CreateFaq";
11 import { IconPlus } from "../../../icons";
12 import { useFAQScope } from "../context/ScopeContext";
13 import { useFAQ } from "../../components/FAQContext";
14
15 // icons
16 import { ReactComponent as Grip } from "./../assets/images/grip.svg";
17 import { ReactComponent as Delete } from "./../assets/images/delete.svg";
18 import { ReactComponent as DownArrow } from "./../assets/images/down-arrow.svg";
19 import { ReactComponent as Edit } from "./../assets/images/edit.svg";
20 import FaqList from "./FaqList";
21
22 const Category = ({
23 index,
24 category,
25 order,
26 categoriesLoading,
27 allFaqCategories,
28 reFetchCategories,
29 setFetchUncategorizedFaq,
30 setUpdateCategory,
31 categoryExpanded,
32 setCategoryExpanded,
33 setCreateFaqAt,
34 createFaqAt,
35 faqExpanded,
36 setFaqExpanded,
37 changeFaqSort,
38 setChangeFaqSort,
39 reFetchFaqForCategory,
40 setReFetchFaqForCategory,
41 setShowFaqAIModalForm,
42 highlightGroupId,
43 }) => {
44 const { taxonomy } = useFAQScope();
45 // Set only when a specific FAQ inside is being revealed (create/move). A
46 // freshly created group has no faqId, so it should flash but stay closed.
47 const { highlightFaqId, bumpStats } = useFAQ();
48 const queryClient = useQueryClient();
49
50 /**
51 * Refresh everything a group deletion invalidates.
52 *
53 * The group list, the pagination total and the "FAQ Groups" stat card all read the
54 * SAME query (["allFaqCategories", …] — its total comes from the response's
55 * X-WP-Total header), so they must not depend on a refetch callback threaded down
56 * three levels of props: if that link is dropped anywhere, the numbers silently keep
57 * the pre-delete value until a full page reload. Invalidate at the cache instead, so
58 * every consumer of the query re-reads it no matter who triggered the delete.
59 */
60 const refreshAfterGroupChange = () => {
61 queryClient.invalidateQueries({ queryKey: ["allFaqCategories"] });
62 queryClient.invalidateQueries({ queryKey: ["uncategorizedFaq"] });
63 // The Total/Published/Drafts cards come from a different endpoint (docs-faq-count),
64 // which nothing above touches — deleting a group with its FAQs changes those too.
65 if (typeof bumpStats === "function") {
66 bumpStats();
67 }
68 };
69 const [reFetchData, setReFetchData] = useState();
70 // Initialise from the actual status so the toggle renders in its final
71 // position immediately. Defaulting to `true` made it flip false→true once
72 // data resolved, replaying the slide animation on every mount / page change
73 // (issue #70).
74 const [categoryStatus, setCategoryStatus] = useState(category?.meta?.status?.[0]);
75 const isFetching = useRef(false);
76
77 // When this group is the focus target (after it's created, or after a FAQ
78 // was created in / moved into it), open it, scroll it into view, and flash
79 // the group card itself — the group analogue of the FAQ row's 2s flash, so
80 // a brand-new (empty) group is still visibly highlighted. Run a local 2s
81 // flash so it completes even after highlightGroupId is cleared.
82 const [flashing, setFlashing] = useState(false);
83 const isHighlighted =
84 highlightGroupId != null && String(highlightGroupId) === String(category?.id);
85 useEffect(() => {
86 if (!isHighlighted) return;
87 // When a specific FAQ is being revealed (created in / moved into this
88 // group), expand the group so the row is visible but DON'T flash the
89 // group card — only the new FAQ header should highlight. A brand-new
90 // (empty) group has no highlightFaqId, so it just flashes itself.
91 if (highlightFaqId != null) {
92 setCategoryExpanded(category?.id);
93 } else {
94 setFlashing(true);
95 }
96 scrollExpandedIntoView(`ctg-${category?.id}`);
97 }, [isHighlighted]);
98 useEffect(() => {
99 if (!flashing) return;
100 const t = setTimeout(() => setFlashing(false), 2000);
101 return () => clearTimeout(t);
102 }, [flashing]);
103
104 useEffect(() => {
105 if (categoryExpanded !== createFaqAt) {
106 setCreateFaqAt(null);
107 }
108 }, [categoryExpanded, createFaqAt]);
109
110 useEffect(() => {
111 setCategoryStatus(category?.meta?.status[0]);
112 }, [category?.meta?.status[0]]);
113
114 const createFaq = (term_id) => {
115 setCreateFaqAt(term_id);
116 setFaqExpanded(undefined);
117 setCategoryExpanded(term_id);
118 scrollExpandedIntoView(`ctg-${term_id}`);
119 };
120
121 const deleteCategory = (term_id) => {
122 Swal.fire({
123 title: __("Delete this FAQ Group?", "betterdocs"),
124 icon: "warning",
125 showConfirmButton: true,
126 showCancelButton:true,
127 confirmButtonText: __('Yes', 'betterdocs'),
128 cancelButtonText: __('No', 'betterdocs'),
129 input: "checkbox",
130 inputValue: 0,
131 inputPlaceholder: __(
132 "Also delete all FAQs inside this FAQ Group",
133 "betterdocs",
134 ),
135 customClass: {
136 popup: 'betterdocs-bulk-action-popup'
137 }
138 }).then((result) => {
139 if (result?.isConfirmed) {
140 if (result?.value == 0) {
141 deleteFaqCategory(term_id, false, taxonomy)
142 .then(() => {
143 refreshAfterGroupChange();
144 toast.success(__("FAQ Group deleted.", "betterdocs"));
145 })
146 .catch((err) => {
147 err?.name
148 ? toast.error(`${err?.name} :${err?.message}`)
149 : toast.error(`${err?.message}`);
150 });
151 } else {
152 deleteFaqCategory(term_id, true, taxonomy)
153 .then(() => {
154 refreshAfterGroupChange();
155 toast.success(__("FAQ Group deleted with all FAQs.", "betterdocs"));
156 })
157 .catch((err) => {
158 err?.name
159 ? toast.error(`${err?.name} :${err?.message}`)
160 : toast.error(`${err?.message}`);
161 });
162 }
163 }
164 });
165 };
166
167 const handleShow = (term_id) => {
168 if (categoryExpanded == term_id) {
169 setCategoryExpanded(undefined);
170 setFaqExpanded(undefined);
171 setCreateFaqAt(null);
172 } else {
173 setCategoryExpanded(term_id);
174 scrollExpandedIntoView(`ctg-${term_id}`);
175 }
176 };
177
178 const handleCategoryStatus = (term_id) => {
179 if (isFetching.current) return;
180
181 isFetching.current = true;
182 categoryStatusChange(term_id, categoryStatus === "1" ? "0" : "1")
183 .then(() => {
184 categoryStatus === "1"
185 ? toast.error(__("FAQ Group Disabled.", "betterdocs"))
186 : toast.success(__("FAQ Group Enabled.", "betterdocs"));
187 reFetchCategories();
188 })
189 .catch((err) => {
190 err?.name
191 ? toast.error(`${err?.name} :${err?.message}`)
192 : toast.error(`${err?.message}`);
193 })
194 .finally(() => {
195 isFetching.current = false;
196 });
197 };
198
199 const getItemStyle = (isDragging, draggableStyle) => ({
200 boxShadow: isDragging && "0 0 5px rgba(0, 0, 0, 0.5)",
201 ...draggableStyle,
202 });
203
204 return (
205 <>
206 <Draggable
207 key={category?.id}
208 draggableId={category?.id.toString()}
209 index={index}
210 isDragDisabled={order !== 'default'}
211 >
212 {(provided, snapshot) => (
213 <div
214 className={`bd-faq-ctg${flashing ? " bd-faq-ctg--highlight" : ""}`}
215 data-bd-expand-id={`ctg-${category?.id}`}
216 ref={provided.innerRef}
217 {...provided.draggableProps}
218 style={getItemStyle(
219 snapshot.isDragging,
220 provided.draggableProps.style,
221 )}
222 >
223 <div className="bd-faq-ctg-head">
224 <button
225 type="button"
226 className="bd-faq-button-reset bd-faq-ctg-drag-button"
227 {...provided.dragHandleProps}
228 >
229 <Grip />
230 </button>
231 <div
232 className="bd-faq-ctg-title-wrap"
233 onClick={() => handleShow(category?.id)}
234 style={{ cursor: "pointer" }}
235 >
236 <h3 className="bd-faq-ctg-title">
237 {decodeEntities(category?.name)}
238 </h3>
239 {typeof category?.count !== "undefined" && (
240 <span className="bd-faq-ctg-meta">
241 {/* `count` is the term count, i.e. PUBLISHED only. Alongside a draft
242 count that reads as "0 questions · 3 draft", which looks like the
243 drafts aren't being counted at all. When drafts exist, name both
244 numbers for what they are — matching the Published / Drafts cards
245 at the top of the page. With no drafts, "5 questions" is clearer
246 than "5 published", so keep that. */}
247 <span className="bd-faq-ctg-meta-count">
248 {category?.betterdocs_draft_count > 0
249 ? sprintf(
250 /* translators: %d: number of published FAQs in this group. */
251 __("%d published", "betterdocs"),
252 category?.count
253 )
254 : sprintf(
255 /* translators: %d: number of FAQs in this group. */
256 _n("%d question", "%d questions", category?.count, "betterdocs"),
257 category?.count
258 )}
259 </span>
260 {category?.betterdocs_draft_count > 0 && (
261 <>
262 <span className="bd-faq-ctg-meta-sep" />
263 <span className="bd-faq-ctg-meta-draft">
264 {sprintf(
265 /* translators: %d: number of draft FAQs in this group. */
266 _n("%d draft", "%d drafts", category?.betterdocs_draft_count, "betterdocs"),
267 category?.betterdocs_draft_count
268 )}
269 </span>
270 </>
271 )}
272 </span>
273 )}
274 </div>
275 <button
276 type="button"
277 className="bd-faq-button button-secondary"
278 onClick={() => createFaq(category?.id)}
279 >
280 <span className="action-button-icon">
281 <IconPlus />
282 </span>
283 <span className="bd-faq-text">
284 {__("Add a New FAQ", "betterdocs")}
285 </span>
286 </button>
287 <button
288 type="button"
289 className="bd-faq-button-reset bd-faq-action-button"
290 onClick={() => setUpdateCategory(category)}
291 >
292 <Edit />
293 </button>
294 <button
295 type="button"
296 className="bd-faq-button-reset bd-faq-action-button delete-icon"
297 onClick={() => deleteCategory(category?.id)}
298 >
299 <Delete />
300 </button>
301 <label
302 className="bd-faq-toggler"
303 onClick={() =>
304 handleCategoryStatus(category?.id)
305 }
306 >
307 <input
308 type="checkbox"
309 checked={categoryStatus === "1"}
310 onChange={() => handleCategoryStatus(category?.id)}
311 className="checkbox"
312 />
313 <span className="knobs"></span>
314 <span className="layer"></span>
315 </label>
316 <button
317 type="button"
318 className={`bd-faq-accordion-icon ${
319 categoryExpanded == category?.id
320 ? "active"
321 : ""
322 }`}
323 onClick={() => handleShow(category?.id)}
324 >
325 <DownArrow />
326 </button>
327 </div>
328 <div
329 className={`bd-faq-category-body ${
330 categoryExpanded == category?.id ? "active" : ""
331 }`}
332 >
333 <div className="bd-faq-category-body-clip">
334 <div className="bd-faq-category-body-inner">
335 {createFaqAt === category?.id && (
336 <CreateFaq
337 reFetchFaqList={reFetchData?.func}
338 createFaqAt={createFaqAt}
339 setCreateFaqAt={setCreateFaqAt}
340 reFetchCategories={
341 reFetchCategories
342 }
343 setShowFaqAIModalForm={
344 setShowFaqAIModalForm
345 }
346 />
347 )}
348 {categoryExpanded == category?.id ? (
349 <FaqList
350 category={category}
351 order={order}
352 allFaqCategories={allFaqCategories}
353 changeFaqSort={changeFaqSort}
354 reFetchFaqForCategory={
355 reFetchFaqForCategory
356 }
357 createFaqAt={createFaqAt}
358 categoriesLoading={
359 categoriesLoading
360 }
361 reFetchCategories={
362 reFetchCategories
363 }
364 setCreateFaqAt={setCreateFaqAt}
365 faqExpanded={faqExpanded}
366 setFaqExpanded={setFaqExpanded}
367 setReFetchFaqForCategory={
368 setReFetchFaqForCategory
369 }
370 setChangeFaqSort={setChangeFaqSort}
371 setReFetchData={setReFetchData}
372 />
373 ) : (
374 ""
375 )}
376 </div>
377 </div>
378 </div>
379 </div>
380 )}
381 </Draggable>
382 </>
383 );
384 };
385
386 export default Category;
387