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 / dashboard / components / AllDocsGrid.js

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

371 lines 19.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { useQuery, useQueryClient, QueryCache, useMutation } from "@tanstack/react-query";
2 import React, { useEffect, useState, useRef } from "react";
3 import { DragDropContext } from "react-beautiful-dnd";
4 import { __ } from "@wordpress/i18n";
5 import { decodeEntities } from "@wordpress/html-entities";
6 import useFetch from "../hooks/useFetch";
7 import AllDocsGridItem from "./AllDocsGridItem";
8 import NoDocsBody from "./NoDocsBody";
9 import { useOutletContext } from "react-router-dom";
10 import { ajaxRequest, constructFormData } from "../../utils/ajaxRequest";
11 import apiFetch from "@wordpress/api-fetch";
12 import GridLoader from '../loaders/GridLoader';
13 import NoDocsLoader from '../loaders/NoDocsLoader';
14 import { addQueryArgs } from "@wordpress/url";
15 import GridQuickEdit from "./GridQuickEdit";
16 import Pagination from "rc-pagination";
17 import "rc-pagination/assets/index.css";
18 import { IconNext, IconPrev } from "../../../icons";
19
20 const AllDocsGrid = () => {
21 const queryClient = useQueryClient();
22 const [quickEdit, setQuickEdit] = useState({isOpen:false});
23 const [mkbFilterValue, refetch] = useOutletContext();
24 const [isDragging, setIsDragging] = useState(false);
25 const dragTimeoutRef = useRef(null);
26 const [stateManager, setStateManager] = useState({
27 docCategoriesQueryParams:{
28 per_page:100,
29 hide_empty:false,
30 orderby:'doc_category_order',
31 ...(mkbFilterValue != '' ? {'knowledge_base': mkbFilterValue} : {}),
32 page:1
33 },
34 });
35
36 const { docCategoriesQueryParams } = stateManager;
37 let options = [];
38
39 // Pass the admin UI language so the REST read path resolves to the same
40 // language-specific meta key the drag-and-drop AJAX writes to. Without this,
41 // WPML resolves REST requests to the site default language and the dashboard
42 // appears to revert ordering after reload when the admin language is a secondary one.
43 const langParam = betterdocs_admin?.current_admin_language ? { lang: betterdocs_admin.current_admin_language } : {};
44
45 const {
46 isLoading: docsCategoriesLoading,
47 data: docsCategoriesData,
48 } = useQuery(["docsCategories", docCategoriesQueryParams], ({queryKey}) => {
49 let [_,params] = queryKey;
50 return apiFetch({
51 path: addQueryArgs('betterdocs/v1/doc-categories', { ...params, ...langParam }),
52 method: "GET",
53 parse: false,
54 }).then((data) => {
55 return data.json();
56 }).then((result) => {
57 return result;
58 });
59 },{
60 refetchOnMount:false,
61 cacheTime:0
62 });
63
64 const { isLoading:categoriesLoading, data:categories } = useQuery(['docCategories',docCategoriesQueryParams], ({queryKey}) => {
65 let [_,params] = queryKey;
66 return apiFetch({
67 path: addQueryArgs(`wp/v2/doc_category`, { ...params, ...langParam }),
68 method: "GET",
69 parse: false,
70 }).then((data) => {
71 return data.json();
72 }).then((result) => {
73 return result;
74 });
75 },{
76 refetchOnMount:false,
77 cacheTime:0
78 });
79
80 const { mutate, data, isLoading, variables } = useMutation({
81 mutationKey:['deleteDocs'],
82 mutationFn:(deleteIds) => {
83 for( let termId of Object.keys(deleteIds) ) {
84 const { post_id_index, term_id, post_id } = deleteIds[termId];
85 return apiFetch({ path: `/wp/v2/docs/${post_id}`, method:'DELETE' } );
86 }
87 },
88 onMutate:async(deleteIds) => {
89 return deleteIds;
90 },
91 onSettled:(data) => {
92 queryClient.invalidateQueries(['docsCategories', docCategoriesQueryParams])
93 },
94 onSuccess:(serverResponse, deleteIds) => {
95 const previousQueryData = queryClient.getQueryData(['docsCategories', docCategoriesQueryParams]);
96 for( let termId of Object.keys(deleteIds) ) {
97 const { post_id_index, term_id, post_id } = deleteIds[termId];
98 previousQueryData?.data[term_id]?.splice(post_id_index, 1);
99 delete deleteIds[termId];
100 if( typeof refetch == 'function'){
101 refetch(true);
102 }
103 queryClient.setQueryData(['docsCategories'], {...previousQueryData});
104 }
105 }
106 });
107
108 const { mutate:dragDrop } = useMutation({
109 mutationKey:['sortDocsDragDrop'],
110 mutationFn:(data) => {
111 return null; //not needed
112 },
113 onMutate:async(updatedData) => {
114 await queryClient.cancelQueries(['docsCategories', docCategoriesQueryParams]);
115 queryClient.setQueryData(['docsCategories', docCategoriesQueryParams], {...updatedData});
116 },
117 onSettled:(serverResponse) => {
118 return null; //not needed
119 }
120 });
121
122 const itemRender = (current, type, originalElement, totalItems) => {
123 if (type === 'prev') {
124 if (current === 0) {
125 return React.cloneElement(originalElement, { title: '' }, <IconPrev />);
126 }
127 return React.cloneElement(originalElement, { title: __("Previous Page", "betterdocs") }, <IconPrev />);
128 }
129 if (type === 'next') {
130 if (current === Math.ceil(totalItems / 20)) {
131 return React.cloneElement(originalElement, { title: '' }, <IconNext />);
132 }
133 return React.cloneElement(originalElement, { title: __("Next Page", "betterdocs") }, <IconNext />);
134 }
135 return originalElement;
136 };
137
138 useEffect(() => {
139 if( ! categoriesLoading && ! docsCategoriesLoading ) {
140 if( mkbFilterValue != '' ) {
141 setStateManager({...stateManager, docCategoriesQueryParams:{...docCategoriesQueryParams, page:1, knowledge_base:mkbFilterValue}});
142 } else {
143 if( docCategoriesQueryParams['knowledge_base'] != undefined ) {
144 delete docCategoriesQueryParams['knowledge_base'];
145 }
146 setStateManager({...stateManager, docCategoriesQueryParams:{ ...docCategoriesQueryParams, page:1, per_page:10, hide_empty:false, orderby:'doc_category_order'}});
147 }
148 }
149 }, [mkbFilterValue]);
150
151 // Cleanup timeout on unmount
152 useEffect(() => {
153 return () => {
154 if (dragTimeoutRef.current) {
155 clearTimeout(dragTimeoutRef.current);
156 }
157 };
158 }, []);
159
160
161 const onDragStart = () => {
162 setIsDragging(true);
163 // Clear any existing timeout
164 if (dragTimeoutRef.current) {
165 clearTimeout(dragTimeoutRef.current);
166 }
167 };
168
169 const onDragEnd = (result) => {
170 setIsDragging(false);
171
172 const { destination, source } = result;
173
174 if( !destination ) {
175 return null;
176 }
177
178 // Prevent rapid successive drag operations
179 if (dragTimeoutRef.current) {
180 clearTimeout(dragTimeoutRef.current);
181 }
182
183 const {droppableId:destinationCategoryId, index:destinationPostIndex} = destination;
184 const {droppableId:sourceCategoryId, index:sourcePostIndex} = source;
185
186 // Debounce the drag operation to prevent multiple rapid calls
187 dragTimeoutRef.current = setTimeout(() => {
188 if( sourceCategoryId == 'uncategorized' || destinationCategoryId == 'uncategorized' ){
189 let sourcePost = docsCategoriesData?.data[sourceCategoryId]?.splice(sourcePostIndex, 1);
190 docsCategoriesData?.data[destinationCategoryId]?.splice(destinationPostIndex, 0, sourcePost[0]);
191 dragDrop(docsCategoriesData, docsCategoriesData[parseInt(sourceCategoryId)], docsCategoriesData[parseInt(destinationCategoryId)], sourceCategoryId, destinationCategoryId, sourcePost[0]?.id);
192 sendAjaxRequest(docsCategoriesData?.data[sourceCategoryId], docsCategoriesData?.data[destinationCategoryId], sourceCategoryId == 'uncategorized' ? '' : sourceCategoryId , destinationCategoryId == 'uncategorized' ? '' : destinationCategoryId, sourcePost[0]?.id);
193 } else {
194 let sourcePost = docsCategoriesData?.data[parseInt(sourceCategoryId)]?.splice(sourcePostIndex, 1);
195 docsCategoriesData?.data[parseInt(destinationCategoryId)]?.splice(destinationPostIndex, 0, sourcePost[0]);
196 dragDrop(docsCategoriesData, docsCategoriesData[parseInt(sourceCategoryId)], docsCategoriesData[parseInt(destinationCategoryId)], sourceCategoryId, destinationCategoryId, sourcePost[0]?.id);
197 sendAjaxRequest(docsCategoriesData?.data[parseInt(sourceCategoryId)], docsCategoriesData?.data[parseInt(destinationCategoryId)], sourceCategoryId, destinationCategoryId, sourcePost[0]?.id);
198 }
199 }, 300); // 300ms debounce delay
200 };
201
202 const sendAjaxRequest = async (sourceCategories, destinationCategories, sourceCategoryId, destinationCategoryId, sourcePostId) => {
203 console.log('Starting drag and drop AJAX requests:', {
204 sourcePostId,
205 sourceCategoryId,
206 destinationCategoryId,
207 sourceCategories: sourceCategories?.length,
208 destinationCategories: destinationCategories?.length
209 });
210
211 try {
212 // Step 1: Update document category assignment first
213 console.log('Step 1: Updating document category assignment');
214 let orderObject = constructFormData({
215 action: 'update_docs_term',
216 object_id: sourcePostId,
217 prev_term_id: sourceCategoryId,
218 list_term_id: destinationCategoryId,
219 'doc_cat_order_nonce': betterdocs_admin?.doc_cat_order_nonce,
220 // Pass language for WPML/multilingual compatibility
221 ...(betterdocs_admin?.current_admin_language ? { lang: betterdocs_admin.current_admin_language } : {})
222 });
223
224 const termUpdateResult = await ajaxRequest(orderObject, betterdocs_admin?.ajaxurl, 'POST', true);
225 console.log('Step 1 completed:', termUpdateResult);
226
227 // Step 2: Update source category order (only if different from destination)
228 if (sourceCategoryId !== destinationCategoryId && sourceCategories && sourceCategories.length > 0) {
229 console.log('Step 2: Updating source category order');
230 let sourceDataObject = constructFormData({
231 'action': 'update_doc_order_by_category',
232 'doc_cat_order_nonce': betterdocs_admin?.doc_cat_order_nonce,
233 'list_term_id': sourceCategoryId,
234 'docs_ordering_data': sourceCategories,
235 // Pass language for WPML/multilingual compatibility
236 ...(betterdocs_admin?.current_admin_language ? { lang: betterdocs_admin.current_admin_language } : {})
237 });
238
239 const sourceUpdateResult = await ajaxRequest(sourceDataObject, betterdocs_admin?.ajaxurl, 'POST', true);
240 console.log('Step 2 completed:', sourceUpdateResult);
241 }
242
243 // Step 3: Update destination category order
244 if (destinationCategories && destinationCategories.length > 0) {
245 console.log('Step 3: Updating destination category order');
246 let destinationDataObject = constructFormData({
247 'action': 'update_doc_order_by_category',
248 'doc_cat_order_nonce': betterdocs_admin?.doc_cat_order_nonce,
249 'list_term_id': destinationCategoryId,
250 'docs_ordering_data': destinationCategories,
251 // Pass language for WPML/multilingual compatibility
252 ...(betterdocs_admin?.current_admin_language ? { lang: betterdocs_admin.current_admin_language } : {})
253 });
254
255 const destUpdateResult = await ajaxRequest(destinationDataObject, betterdocs_admin?.ajaxurl, 'POST', true);
256 console.log('Step 3 completed:', destUpdateResult);
257 }
258
259 console.log('All drag and drop AJAX requests completed successfully');
260 } catch (error) {
261 console.error('Drag and drop AJAX request failed:', error);
262 // Refresh the data to restore correct state
263 console.log('Refreshing data due to error...');
264 queryClient.invalidateQueries(['docsCategories', docCategoriesQueryParams]);
265 }
266 }
267
268 for( let i = 10; i <= 100; i+=10 ) { // set the number of terms per page option for select field
269 options.push(<option value={i}>{i}</option>)
270 }
271 // console.log(docsCategoriesData?.total_terms);
272 const hasUncategorizedDocs = docsCategoriesData?.data && docsCategoriesData.data['uncategorized'] != undefined && docsCategoriesData.data['uncategorized']?.length > 0;
273 const loading = categoriesLoading || docsCategoriesLoading;
274 // Genuinely-empty KB (no categories, no docs) is known server-side at page
275 // load, so on the first paint we can show a skeleton shaped like the empty
276 // card instead of the category-grid skeleton it would immediately replace.
277 const kbEmpty = Boolean(window.betterdocs_admin?.kb_is_empty);
278 // Hide the (otherwise empty) category grid section in the no-docs empty state.
279 const showCategoryGrid = loading || categories?.length > 0 || hasUncategorizedDocs;
280 return (
281 <>
282 <DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
283 {loading && kbEmpty && <NoDocsLoader />}
284 {showCategoryGrid && !(loading && kbEmpty) && (
285 <section className="betterdocs-dashboard-body betterdocs-dashboard-category-grid">
286 <div className="betterdocs-dashboard-container">
287 <div className="betterdocs-row">
288 {( categoriesLoading || docsCategoriesLoading ) && <GridLoader/>}
289 {(!categoriesLoading && !docsCategoriesLoading && categories?.length > 0) &&
290 categories.map((category, key) => {
291 return <div
292 className="betterdocs-col betterdocs-col-md-4"
293 key={key}
294 >
295 <div className="betterdocs-dashboard-category-wrap">
296 <div className="betterdocs-dashboard-category-header">
297 <h3 className="betterdocs-dashboard-category-name">
298 {decodeEntities(category?.name)}
299 </h3>
300 </div>
301 {docsCategoriesData?.data[category?.id] != undefined && <AllDocsGridItem category={category} docsData={docsCategoriesData?.data[category?.id]} deleteIds={variables == undefined ? {} : variables} isLoading={isLoading} mutate={mutate} setQuickEdit={setQuickEdit} />}
302 <div className="betterdocs-dashboard-category-footer">
303 <a
304 href={`post-new.php?post_type=docs&cat=${category.id}`}
305 className="betterdocs-dashboard-btn betterdocs-dashboard-btn-white"
306 >
307 {__("Add New Doc", "betterdocs")}
308 </a>
309 </div>
310 </div>
311 </div>
312 })}
313 {!categoriesLoading && !docsCategoriesLoading && docsCategoriesData?.data['uncategorized'] != undefined && docsCategoriesData?.data['uncategorized']?.length > 0 && (
314 <div
315 className="betterdocs-col betterdocs-col-md-4"
316 key={'uncategorized'}
317 >
318 <div className="betterdocs-dashboard-category-wrap">
319 <div className="betterdocs-dashboard-category-header">
320 <h3 className="betterdocs-dashboard-category-name">
321 {__('Uncategorized', 'betterdocs')}
322 </h3>
323 </div>
324 {<AllDocsGridItem category={{
325 'id':'uncategorized'
326 }} docsData={docsCategoriesData?.data['uncategorized']} deleteIds={variables == undefined ? {} : variables} isLoading={isLoading} mutate={mutate} setQuickEdit={setQuickEdit} />}
327 <div className="betterdocs-dashboard-category-footer">
328 <a
329 href={`post-new.php?post_type=docs&cat=${''}`}
330 className="betterdocs-dashboard-btn betterdocs-dashboard-btn-white"
331 >
332 {__("Add New Doc", "betterdocs")}
333 </a>
334 </div>
335 </div>
336 </div>
337 )}
338 </div>
339 {(!categoriesLoading && !docsCategoriesLoading && parseInt(docsCategoriesData?.total_terms) > 100) && <div className="doc-list-pagination-wrap">
340 {<div className="pagination-group"><select value={docCategoriesQueryParams?.per_page} className="number-of-categories" onChange={(e) => {
341 docCategoriesQueryParams['per_page'] = e?.target?.value;
342 setStateManager({...stateManager, docCategoriesQueryParams:{...docCategoriesQueryParams}});
343 }}>
344 {options?.map((value) => {
345 return value;
346 })}
347 </select>
348 <p>{__('category in this page', 'betterdocs')}</p></div>}
349 <Pagination
350 pageSize={docCategoriesQueryParams?.per_page}
351 current={docCategoriesQueryParams?.page}
352 total={docsCategoriesData?.total_terms}
353 onChange={(value) => {
354 docCategoriesQueryParams['page'] = value;
355 setStateManager({...stateManager, docCategoriesQueryParams:{...docCategoriesQueryParams}});
356 }}
357 itemRender={(current, type, originalElement) => itemRender(current, type, originalElement, docsCategoriesData?.total_terms)}
358 />
359 </div>}
360 </div>
361 </section>
362 )}
363 </DragDropContext>
364 {(!categoriesLoading && !docsCategoriesLoading && categories?.length == 0) && <NoDocsBody isDocs={docsCategoriesData?.data?.length == 0 && docsCategoriesData?.data['uncategorized'] == undefined ? false : true}/>}
365 {(quickEdit?.isOpen) && <GridQuickEdit data={quickEdit?.data} setQuickEdit={setQuickEdit} setCategoriesPosts={queryClient} docCategoriesQueryParams={docCategoriesQueryParams}/>}
366 </>
367 );
368 };
369
370 export default AllDocsGrid;
371