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

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

315 lines 11.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useEffect, useCallback, useState } from "react";
2 import { __ } from "@wordpress/i18n";
3 import { decodeEntities } from "@wordpress/html-entities";
4 import Select from "react-select";
5 import { toast } from "react-toastify";
6 import TextEditor from "../utils/TextEditor";
7 import ErrorMessage from "../utils/ErrorMessage";
8 import { validateFormFields } from "../utils/validations";
9 import { createFAQ, searchCategories, getStickyTopOffset } from "./../utils/function";
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 { useFAQ } from "../../components/FAQContext";
14 import { useFAQScope } from "../context/ScopeContext";
15 import { useQuery, useQueryClient } from "@tanstack/react-query";
16
17 const CreateFaq = ({
18 reFetchFaqList,
19 createFaqAt,
20 setCreateFaqAt,
21 reFetchCategories,
22 // Top-level mode: rendered above the group list (via "Add New FAQ"), with a
23 // group picker since there is no parent category context. `onClose` closes
24 // the top form; `groups` are the loaded FAQ categories.
25 topLevel = false,
26 groups = [],
27 onClose,
28 }) => {
29 const [showFaqAIModalForm, setShowFaqAIModalForm] = useState(false);
30 const { bumpStats, setFocusTarget } = useFAQ();
31 const { taxonomy } = useFAQScope();
32 const queryClient = useQueryClient();
33
34 // Top-level mode is the dashboard "Add New FAQ" flow — scroll so the FORM's
35 // first field (FAQ Group) lands just below the WP admin bar. The rich editor's
36 // core (wp-tinymce) loads late and its init balloons the form mid-layout; if we
37 // scroll before that, the late layout shift/focus drags the view down onto the
38 // editor. So we scroll from the editor's `onReady` (see <TextEditor onReady>),
39 // with a fallback timeout in case TinyMCE never loads. Runs once.
40 const formRef = React.useRef(null);
41 const didScrollRef = React.useRef(false);
42 const scrollFormIntoView = useCallback(() => {
43 if (!topLevel || didScrollRef.current) return;
44 const el = formRef.current;
45 if (!el) return;
46 didScrollRef.current = true;
47 const doScroll = () => {
48 if (!formRef.current) return;
49 const offset = getStickyTopOffset();
50 const top =
51 window.scrollY + formRef.current.getBoundingClientRect().top - offset;
52 window.scrollTo({ top, behavior: "smooth" });
53 };
54 // Scroll now, then re-assert once — the editor can focus-scroll itself a beat
55 // after init (notably on SPA navigation from the dashboard), so the second
56 // pass guarantees the form top wins.
57 requestAnimationFrame(doScroll);
58 setTimeout(doScroll, 350);
59 }, [topLevel]);
60
61 useEffect(() => {
62 if (!topLevel) return;
63 // Fallback only — the editor's onReady is the primary trigger.
64 const t = setTimeout(scrollFormIntoView, 800);
65 return () => clearTimeout(t);
66 }, [topLevel, scrollFormIntoView]);
67
68 // Selected group for top-level mode ("" = Uncategorized).
69 const [selectedGroup, setSelectedGroup] = useState("");
70
71 // The `groups` prop is only the current paginated page of the builder, so it
72 // omits groups on other pages — including freshly created (still empty) ones,
73 // which sort last. Load the full group list for the picker via the search
74 // endpoint (hide_empty => false, returns every group). Fall back to the
75 // paginated prop while it loads.
76 const { data: allGroups } = useQuery(
77 ["faqGroupsForPicker", taxonomy],
78 () => searchCategories("", taxonomy),
79 { enabled: topLevel }
80 );
81 const pickerGroups =
82 topLevel && Array.isArray(allGroups) && allGroups.length ? allGroups : groups;
83 const groupOptions = [
84 { value: "", label: __("Uncategorized", "betterdocs") },
85 ...(pickerGroups || []).map((group) => ({
86 value: group?.id,
87 label: decodeEntities(group?.name),
88 })),
89 ];
90
91 const closeForm = () => {
92 if (topLevel) {
93 onClose?.();
94 } else {
95 setCreateFaqAt(null);
96 }
97 };
98
99 const [formFields, setFormFields] = useState({
100 question: "",
101 answer: "",
102 aiAnswer: null,
103 });
104 const [errors, setErrors] = useState({
105 question: "",
106 answer: "",
107 });
108
109 const [settings, setSettings] = useState(null);
110 // QA-002: guards the Create button against double submission.
111 const [submitting, setSubmitting] = useState(false);
112
113 useEffect(() => {
114 getSettingsData().then((getSettings) => {
115 setSettings(getSettings);
116 }).catch(function (error) {
117 console.log(error);
118 });
119 }, []);
120
121
122 const handleAnswerChange = (val) => {
123 setFormFields({
124 ...formFields,
125 answer: val,
126 });
127
128
129 };
130 const handleCancel = () => {
131 closeForm();
132 };
133
134 const validationRules = () => {
135 return {
136 question: {
137 required: {
138 value: true,
139 message: __("Please insert your question here.", "betterdocs"),
140 },
141 },
142 answer: {
143 minLength: {
144 value: 9,
145 stripHtml: true, // QA-003: measure visible text, not raw HTML
146 message: __("Answer must not be Empty.", "betterdocs"),
147 },
148 },
149 };
150 };
151
152 const handleSubmit = (e) => {
153 e.preventDefault();
154 // QA-002: ignore repeat submits while a request is in flight.
155 if (submitting) {
156 return;
157 }
158 const validated = validateFormFields(formFields, validationRules());
159 if (!validated.valid) {
160 setErrors(validated.errors);
161 return;
162 }
163 if (validated.valid) {
164 setErrors({
165 question: "",
166 answer: "",
167 });
168 setSubmitting(true);
169
170 createFAQ({
171 term_id: topLevel ? (selectedGroup || null) : createFaqAt,
172 post_title: formFields?.question,
173 post_content: formFields?.answer,
174 }, taxonomy)
175 .then((res) => {
176 reFetchFaqList();
177 reFetchCategories();
178 bumpStats();
179 if (res) {
180 toast.success(__("FAQ created.", "betterdocs"));
181 // create_post returns the new FAQ's post id. Reveal its group
182 // (page-jump + expand) and flash the new FAQ row itself. Top-level
183 // create can target any group (or Uncategorized); the in-group form
184 // already sits inside its open group, so reuse createFaqAt there.
185 const newFaqId = typeof res === "object" ? (res?.id ?? res?.ID) : res;
186 if (topLevel) {
187 if (selectedGroup) {
188 setFocusTarget({ groupId: selectedGroup, faqId: newFaqId });
189 } else {
190 // New uncategorized FAQ: refresh that panel's list, then flash it.
191 queryClient.invalidateQueries(["uncategorizedFaq", taxonomy]);
192 setFocusTarget({ groupId: "uncategorized", faqId: newFaqId });
193 }
194 } else if (createFaqAt) {
195 setFocusTarget({ groupId: createFaqAt, faqId: newFaqId });
196 }
197 }
198 })
199 .catch((err) => {
200 err?.name
201 ? toast.error(`${err?.name} :${err?.message}`)
202 : toast.error(`${err?.message}`);
203 })
204 .finally(() => {
205 setSubmitting(false);
206 closeForm();
207 });
208 }
209 };
210
211 return (
212 <>
213 <div className="bd-faq-create-new" ref={formRef}>
214 <form onSubmit={handleSubmit}>
215 <div className="bd-faq-create-new-body">
216 {topLevel && (
217 <div className="bd-faq-cgt-field">
218 <label className="bd-faq-label">
219 {__("FAQ Group", "betterdocs")}
220 </label>
221 <Select
222 className="bd-faq-select"
223 classNamePrefix="bd-faq-select"
224 isClearable={false}
225 options={groupOptions}
226 value={
227 groupOptions.find((o) => o.value === selectedGroup) ||
228 groupOptions[0]
229 }
230 onChange={(opt) => setSelectedGroup(opt?.value ?? "")}
231 menuPortalTarget={
232 typeof document !== "undefined" ? document.body : null
233 }
234 menuPlacement="auto"
235 styles={{ menuPortal: (base) => ({ ...base, zIndex: 9999 }) }}
236 placeholder={__("Select a FAQ group…", "betterdocs")}
237 />
238 </div>
239 )}
240 <div className="bd-faq-cgt-field">
241 <label className="bd-faq-label" required>
242 {__("Question", "betterdocs")}
243 </label>
244 <div className="bd-faq-cgt-field-wrapper">
245 <input
246 type="text"
247 className="bd-faq-input"
248 name="faq-question"
249 value={formFields?.question}
250 onChange={(e) =>
251 setFormFields({
252 ...formFields,
253 question: e.target.value.replace(/^\s+/, ""),
254 })
255 }
256 placeholder={__("Add New Question", "betterdocs")}
257 />
258 {
259 (settings && (settings?.enable_faq_write_with_ai ?? true)) && (
260 <button type="button" className="bd-faq-button button-success bd-write-with-ai-button" onClick={(e) => { setShowFaqAIModalForm(true) }}>
261 <BDIcon />
262 <span className="bd-faq-text">{__("Write With AI", "betterdocs")}</span>
263 </button>
264 )
265 }
266
267 </div>
268 <ErrorMessage error={errors.question} />
269 </div>
270 <div className="bd-faq-cgt-field">
271 <label className="bd-faq-label" required>
272 {__("Answer", "betterdocs")}
273 </label>
274 <TextEditor
275 value={formFields?.answer}
276 aiValue={formFields?.aiAnswer}
277 onChange={handleAnswerChange}
278 placeholder={__("Write your Answer here.", "betterdocs")}
279 onReady={topLevel ? scrollFormIntoView : undefined}
280 />
281 <ErrorMessage error={errors.answer} />
282 </div>
283 </div>
284 <div className="bd-faq-create-new-footer">
285 <div className="bd-faq-button-group">
286 <button
287 type="button"
288 className="bd-faq-button button-ghost"
289 onClick={() => handleCancel()}
290 >
291 <span className="bd-faq-text">{__("Cancel", "betterdocs")}</span>
292 </button>
293 <button type="submit" className="bd-faq-button button-success" disabled={submitting}>
294 <span className="bd-faq-text">{submitting ? __("Creating…", "betterdocs") : __("Create", "betterdocs")}</span>
295 </button>
296 </div>
297 </div>
298 </form>
299 </div>
300
301 {(showFaqAIModalForm && settings && (settings?.enable_faq_write_with_ai ?? true)) && (
302 <FaqAIModalForm
303 open={true}
304 setFormFields={setFormFields}
305 formFields={formFields}
306 setShowFaqAIModalForm={setShowFaqAIModalForm}
307 />
308 )
309 }
310 </>
311 );
312 };
313
314 export default CreateFaq;
315