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

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

423 lines 11.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { decodeEntities } from "@wordpress/html-entities";
2 import { __ } from "@wordpress/i18n";
3 import React, { useEffect, useState } from "react";
4 import AsyncSelect from "react-select/async";
5 import { toast } from "react-toastify";
6 import ErrorMessage from "../utils/ErrorMessage";
7 import TextEditor from "../utils/TextEditor";
8 import { getToggleResponse, searchCategories, updateFaq } from "../utils/function";
9 import { validateFormFields } from "../utils/validations";
10 import { ReactComponent as BDIcon } from "./../assets/images/betterdocs-icon-white.svg";
11 import FaqAIModalForm from "./FaqAIModalForm";
12 import { getSettingsData } from "../utils/function";
13 import { useFAQScope } from "../context/ScopeContext";
14 import { useFAQ } from "../../components/FAQContext";
15 import { useQuery } from "@tanstack/react-query";
16
17 const EditFaq = ({
18 categoriesLoading,
19 allFaqCategories,
20 reFetchCategories,
21 reFetchFaqList,
22 faq,
23 faqExpanded,
24 setFaqExpanded,
25 setReFetchFaqForCategory,
26 showFaqAIModalForm,
27 setShowFaqAIModalForm,
28 settings
29 }) => {
30
31 const { taxonomy } = useFAQScope();
32 const { setFocusTarget } = useFAQ();
33
34 // allFaqCategories is only the current paginated page, so the group picker
35 // would miss groups on other pages. Load the full group list (searchCategories
36 // with empty input returns every group, hide_empty => false). Shared cache
37 // key with the create form's picker.
38 const { data: allGroups } = useQuery(
39 ["faqGroupsForPicker", taxonomy],
40 () => searchCategories("", taxonomy)
41 );
42
43 // const [showFaqAIModalForm, setShowFaqAIModalForm] = useState(false);
44
45 const [formFields, setFormFields] = useState({
46 question: decodeEntities(
47 faq?.title?.rendered
48 ? faq?.title?.rendered
49 : faq?.post_title
50 ? faq?.post_title
51 : ""
52 ),
53 answer: faq?.content?.rendered
54 ? faq?.content?.rendered
55 : faq?.post_content
56 ? faq?.post_content
57 : "",
58 aiAnswer: null,
59
60 });
61 const [allCats, setAllCats] = useState({
62 default: { value: null, label: "Uncategorized" },
63 cats: [],
64 });
65 const [selectedCategory, setSelectedCategory] = useState("not-set");
66 const [isAjaxRunning, setIsAjaxRunning] = useState(false);
67 // QA-002: guards the Update button against double submission.
68 const [submitting, setSubmitting] = useState(false);
69 // QA-010: bumped whenever this row (re)opens so the rich-text editor resets
70 // to the FAQ's saved content instead of keeping stale/unsaved text.
71 const [editorResetKey, setEditorResetKey] = useState(0);
72 // "Keep It Open By default" defaults to OFF — only enabled (open) once the
73 // stored meta is explicitly truthy ('1'). A loose `== 1` keeps '' / undefined
74 // / '0' all OFF so an uncategorized FAQ (whose endpoint may omit the meta)
75 // no longer defaults to open.
76 const [toggleDropdown, setToggleDropdown] = useState(faq?.meta?.faq_open_by_default == 1);
77
78
79 useEffect(() => {
80 if (faqExpanded == (faq?.id || faq?.ID)) {
81 setFormFields({
82 question: decodeEntities(
83 faq?.title?.rendered
84 ? faq?.title?.rendered
85 : faq?.post_title
86 ? faq?.post_title
87 : ""
88 ),
89 answer: faq?.content?.rendered
90 ? faq?.content?.rendered
91 : faq?.post_content
92 ? faq?.post_content
93 : "",
94 });
95 // QA-010: also force the editor to reload the saved content, so a
96 // reopened row never shows stale text from a prior (unsaved) edit.
97 setEditorResetKey((k) => k + 1);
98 }
99 }, [faqExpanded]);
100
101 useEffect(() => {
102 // Prefer the full group list; fall back to the current page while it loads.
103 const groupSource =
104 Array.isArray(allGroups) && allGroups.length ? allGroups : allFaqCategories;
105 if (groupSource?.length) {
106 let def;
107 if (faq?.betterdocs_faq_category) {
108 let defData = groupSource?.find(
109 (cat) => cat?.id == faq?.betterdocs_faq_category[0] || cat?.term_id == faq?.betterdocs_faq_category[0]
110 );
111 def = {
112 value: defData?.id || defData?.term_id,
113 label: decodeEntities(defData?.name),
114 };
115 } else {
116 def = { value: null, label: "Uncategorized" };
117 }
118 let cats = groupSource?.map((cat) => ({
119 value: cat?.id || cat?.term_id,
120 label: decodeEntities(cat?.name),
121 }));
122 setAllCats({
123 default: def,
124 cats: [...cats],
125 });
126 }
127 }, [allGroups, allFaqCategories, faq]);
128
129 const [errors, setErrors] = useState({
130 question: "",
131 answer: "",
132 });
133
134 const handleAnswerChange = (val) => {
135 setFormFields({
136 ...formFields,
137 answer: val.replace(/^\s+/, ""),
138 });
139 };
140
141 const resetFormField = () => {
142 setFormFields({
143 question: "",
144 answer: "",
145 });
146 };
147
148 const resetErrors = () => {
149 setErrors({
150 question: "",
151 answer: "",
152 });
153 };
154
155 const handleCancel = () => {
156 setFaqExpanded(undefined);
157 resetFormField();
158 resetErrors();
159 setSelectedCategory("not-set");
160 };
161
162 const validationRules = () => {
163 return {
164 question: {
165 required: {
166 value: true,
167 message: __(
168 "Please insert your question here.",
169 "betterdocs"
170 ),
171 },
172 },
173 answer: {
174 minLength: {
175 value: 9,
176 stripHtml: true, // QA-003: measure visible text, not raw HTML
177 message: __("Answer must not be Empty.", "betterdocs"),
178 },
179 },
180 };
181 };
182
183 const handleSubmit = (e) => {
184 e.preventDefault();
185 // QA-002: ignore rapid re-submits while a request is in flight.
186 if (submitting) {
187 return;
188 }
189 const validated = validateFormFields(formFields, validationRules());
190 if (!validated.valid) {
191 setErrors(validated.errors);
192 return;
193 }
194 if (validated.valid) {
195 resetErrors();
196 setSubmitting(true);
197 updateFaq(
198 !selectedCategory || selectedCategory == "not-set"
199 ? {
200 post_id: faq?.id || faq?.ID,
201 post_title: formFields?.question,
202 post_content: formFields?.answer,
203 }
204 : {
205 post_id: faq?.id || faq?.ID,
206 post_title: formFields?.question,
207 post_content: formFields?.answer,
208 term_id: selectedCategory,
209 },
210 taxonomy
211 )
212 .then((res) => {
213 if (res) {
214 toast.success(__("FAQ updated.", "betterdocs"));
215 }
216 })
217 .catch((err) => {
218 err?.name
219 ? toast.error(`${err?.name} :${err?.message}`)
220 : toast.error(`${err?.message}`);
221 })
222 .finally(() => {
223 setSubmitting(false);
224 // Keep the edited FAQ's accordion open after a successful
225 // update instead of collapsing it (issue #45).
226 reFetchCategories();
227 // Pass the chosen group so callers (e.g. the Uncategorized
228 // panel) can react to a FAQ moving into a group.
229 reFetchFaqList(selectedCategory);
230 if (selectedCategory && selectedCategory != "not-set") {
231 setReFetchFaqForCategory(selectedCategory);
232 // FAQ moved into a group → reveal that group (jumping
233 // pages if needed) and flash the moved FAQ row itself.
234 setFocusTarget({
235 groupId: selectedCategory,
236 faqId: faq?.id || faq?.ID,
237 });
238 }
239 });
240 }
241 };
242
243 const handleCategoryChange = (e) => {
244 if (e == null) {
245 setSelectedCategory("not-set");
246 } else setSelectedCategory(e?.value);
247 };
248
249 const handleToggle = (faq) => {
250 setToggleDropdown(!toggleDropdown);
251 // QA-011: uncategorized FAQs expose `ID` (not `id`); without the fallback
252 // the toggle posts faq_id=0 and the "open by default" meta never saves.
253 getToggleResponse(faq?.id || faq?.ID, !toggleDropdown);
254 }
255
256 const promiseOptions = (inputValue, callback) => {
257 if (!isAjaxRunning && inputValue) {
258 setIsAjaxRunning(true);
259 window.lastRequest = null;
260 searchCategories(inputValue, taxonomy)
261 .then((response) => {
262 callback(
263 response?.map((r) => {
264 return {
265 value: r?.id,
266 label: r?.name,
267 };
268 })
269 );
270 })
271 .catch((err) => callback([]))
272 .finally(() => {
273 setIsAjaxRunning(false);
274 if (window.lastRequest) {
275 const lr = window.lastRequest;
276 window.lastRequest = null;
277 promiseOptions(...lr);
278 }
279 });
280 } else {
281 window.lastRequest = [inputValue, callback];
282 }
283 };
284
285 return (
286 <>
287 <div className="bd-faq-create-new">
288 <form onSubmit={handleSubmit}>
289 <div className="bd-faq-create-new-body">
290 {allCats?.cats?.length ? (
291 <div className="bd-faq-cgt-field">
292 <label className="bd-faq-label">
293 {__("FAQ Group", "betterdocs")}
294 </label>
295 <AsyncSelect
296 cacheOptions
297 className="bd-faq-select"
298 classNamePrefix="bd-faq-select"
299 defaultOptions={allCats?.cats}
300 defaultValue={allCats?.default}
301 isLoading={categoriesLoading}
302 isClearable={false}
303 isSearchable={true}
304 name="category"
305 loadOptions={promiseOptions}
306 onChange={(e) => handleCategoryChange(e)}
307 noOptionsMessage={() => "No FAQ Group Found!"}
308 // The FAQ body now clips its content
309 // (overflow:hidden) to slide open smoothly, so
310 // portal the menu to <body> to keep it from
311 // being cut off when it opens.
312 menuPortalTarget={document.body}
313 menuPosition="fixed"
314 styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }}
315 />
316 </div>
317 ) : (
318 ""
319 )}
320 <div className="bd-faq-cgt-field">
321 <label className="bd-faq-label" required>
322 {__("Question", "betterdocs")}
323 </label>
324 <div className="bd-faq-cgt-field-wrapper">
325 <input
326 type="text"
327 className="bd-faq-input"
328 name="faq-question"
329 value={formFields?.question}
330 onChange={(e) =>
331 setFormFields({
332 ...formFields,
333 question: e.target.value.replace(
334 /^\s+/,
335 ""
336 ),
337 })
338 }
339 placeholder={__("Add New Question", "betterdocs")}
340 />
341 {
342 (settings && (settings?.enable_faq_write_with_ai ?? true)) && (
343 <button type="button" className="bd-faq-button button-success bd-write-with-ai-button" onClick={(e) => { setShowFaqAIModalForm(true) }}>
344 <BDIcon />
345 <span className="bd-faq-text">{__("Write With AI", "betterdocs")}</span>
346 </button>
347 )
348 }
349 </div>
350
351 <ErrorMessage error={errors.question} />
352 </div>
353 <div className="bd-faq-cgt-field">
354 <label className="bd-faq-label" required>
355 {__("Answer", "betterdocs")}
356 </label>
357
358 <TextEditor
359 value={formFields?.answer}
360 aiValue={formFields?.aiAnswer}
361 onChange={handleAnswerChange}
362 resetKey={editorResetKey}
363 placeholder={__("Write your Answer here.", "betterdocs")}
364 />
365 <ErrorMessage error={errors.answer} />
366 </div>
367 </div>
368 <div className="bd-faq-create-new-footer">
369 <div className="bd-toggle-switcher-group">
370 <p>{__('Keep It Open By default', 'betterdocs')}</p>
371 <label
372 className="bd-faq-toggler"
373 >
374 <input
375 type="checkbox"
376 checked={toggleDropdown}
377 onChange={() => {
378 handleToggle(faq);
379 }}
380 className="checkbox"
381 />
382 <span className="knobs"></span>
383 <span className="layer"></span>
384 </label>
385 </div>
386 <div className="bd-faq-button-group">
387 <button
388 type="button"
389 className="bd-faq-button button-ghost"
390 onClick={() => handleCancel()}
391 >
392 <span className="bd-faq-text">
393 {__("Cancel", "betterdocs")}
394 </span>
395 </button>
396 <button
397 type="submit"
398 className="bd-faq-button button-success"
399 disabled={submitting}
400 >
401 <span className="bd-faq-text">
402 {submitting ? __("Updating…", "betterdocs") : __("Update", "betterdocs")}
403 </span>
404 </button>
405 </div>
406 </div>
407 </form>
408 </div>
409 {(showFaqAIModalForm && settings && (settings?.enable_faq_write_with_ai ?? true)) && (
410 <FaqAIModalForm
411 open={true}
412 setFormFields={setFormFields}
413 formFields={formFields}
414 setShowFaqAIModalForm={setShowFaqAIModalForm}
415 />
416 )
417 }
418 </>
419 );
420 };
421
422 export default EditFaq;
423