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

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

542 lines 26.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useEffect, useState } from "react";
2 import { useQuery } from "@tanstack/react-query";
3 import apiFetch from "@wordpress/api-fetch";
4 import { __, _n, sprintf } from "@wordpress/i18n";
5 import AllDocsFilter from "./AllDocsFilter.js";
6 import AllDocsListTable from "./AllDocsListTable.js";
7 import AllDocsSearch from "./AllDocsSearch.js";
8 import NoDocsBody from "./NoDocsBody.js";
9 import NoDocsLoader from "../loaders/NoDocsLoader.js";
10 import QuickEdit from "./QuickEdit.js";
11 import { useDashboardContext } from "../DashboardContext";
12 import { addQueryArgs } from "@wordpress/url";
13 import { toast } from "react-toastify";
14
15 import { useOutletContext } from "react-router-dom";
16
17 import { useNavigate } from "react-router-dom";
18 import { makeRouteURL } from "../../../utils/history.js";
19
20 import { objectDifference } from "../helpers/Helpers.js";
21
22 import useFetch from "../hooks/useFetch";
23 import useDebounce from "../hooks/useDebounce.js";
24
25 import Swal from "sweetalert2";
26
27 const AllDocsList = ( { post_status } ) => {
28 const [mkbFilterValue, refetch] = useOutletContext();
29 const [bulkData, setBulkData] = useState([]);
30 const { data, setData } = useDashboardContext();
31 const [filterOptions, setFilterOptions] = useState(undefined);
32 let [restoreIDS, setRestoreIDS] = useState([]);
33 let [LoaderIDS, setLoaderIDS] = useState([]);
34 const [searchKeyword, setSearchKeyword] = useState('');
35 const debouncedKeyword = useDebounce(searchKeyword);
36 const [queryParams, setQueryParams] = useState({
37 per_page:20,
38 // Default the "All Docs" panel to every status (drafts included), not just
39 // published — otherwise generated sample docs, which are inserted as drafts,
40 // aren't counted (totalItems stays 0 → isEmptyKB → "No Category Found") and
41 // don't appear in the list until one is published.
42 status:post_status || 'any',
43 page:1,
44 ...(mkbFilterValue != '' ? {'knowledge_base_slug': mkbFilterValue} : {})
45 });
46
47 const { data: docs, isLoading, error, totalPages, totalItems, setRefetch, setIsLoading, setData:setDocsData} = useFetch({
48 path: addQueryArgs(`/wp/v2/docs`, queryParams),
49 parse: false
50 });
51
52 // Doc-category count — the empty state must agree with the grid view (and with the
53 // server's kb_is_empty(), which is "no categories AND no docs"). A knowledge base
54 // with categories but no docs yet is NOT empty: the grid shows those categories, so
55 // the list must show the table (which renders its own "No Docs Found") rather than
56 // the onboarding panel. Only the term count is needed, hence per_page=1 + the header.
57 const langParam = betterdocs_admin?.current_admin_language ? { lang: betterdocs_admin.current_admin_language } : {};
58 const { isLoading: categoriesLoading, data: categoryCount } = useQuery(
59 ["docCategoriesCount", langParam],
60 () =>
61 apiFetch({
62 path: addQueryArgs("wp/v2/doc_category", { per_page: 1, hide_empty: false, _fields: "id", ...langParam }),
63 method: "GET",
64 parse: false,
65 }).then((response) => Number(response.headers.get("x-wp-total") || 0)),
66 { refetchOnMount: false, cacheTime: 0 }
67 );
68
69 const navigate = useNavigate();
70
71 const { quickAction, docId } = data;
72
73 /**
74 * This Effect Runs When Keyword Has Been Placed
75 */
76 useEffect(() => {
77 if( ! isLoading ) {
78 let defaultParams;
79 if( debouncedKeyword != '' ) {
80 defaultParams = {
81 ...(filterOptions != '' ? filterOptions : {} ),
82 page:1,
83 per_page:20,
84 status:'any',
85 search:debouncedKeyword
86 };
87 } else {
88 defaultParams = {
89 ...(filterOptions != '' ? filterOptions : {} ),
90 page:1,
91 status:'any',
92 per_page:20,
93 }
94 }
95 setIsLoading(true);
96 setQueryParams({...defaultParams});
97 }
98 }, [debouncedKeyword]);
99
100 /**
101 * If The Post Status Is Changes Set The Current Page to 1 For Trash/Any
102 */
103 useEffect(() => {
104 if( ! isLoading ) {
105 let defaultParams = {
106 page:1,
107 status:post_status || 'any',
108 per_page:20,
109 page:1
110 };
111 setQueryParams((prevState) => {
112 setIsLoading(true);
113 return {...defaultParams};
114 });
115 }
116 if( bulkData?.length > 0 ){
117 setBulkData([]);
118 }
119 }, [post_status]);
120
121 /**
122 * This Effect Runs for mkb filter and search value are not empty
123 */
124 useEffect(() => {
125 if( ! isLoading ) {
126 if( post_status != 'trash' ) {
127 setQueryParams((prevState) => {
128 if( mkbFilterValue == '' ){
129 if( prevState['knowledge_base_slug'] != undefined ){
130 setIsLoading(true);
131 delete prevState['knowledge_base_slug'];
132 }
133 } else {
134 setIsLoading(true);
135 prevState['knowledge_base_slug'] = mkbFilterValue;
136 }
137
138 return {...queryParams, ...prevState};
139 });
140 }
141 }
142 }, [mkbFilterValue, post_status]);
143
144 useEffect(() => {
145 if( quickAction != undefined ) {
146 let { action, docId, forceDelete } = quickAction;
147
148 if( action == 'trash' ){
149 LoaderIDS.push(docId);
150 setLoaderIDS([...LoaderIDS]);
151 wp.apiFetch({ path: `/wp/v2/docs/${docId}`, method:'DELETE'} ).then((result) => {
152 if( result != undefined ){
153 let docsUpdated = docs?.filter((doc) => {
154 return doc?.id != docId;
155 });
156 if(typeof refetch == 'function'){ //refetch when trash is initiated
157 refetch(true);
158 }
159 setDocsData([...docsUpdated]);
160 setTimeout(() => {
161 setLoaderIDS([]);
162 }, 1000);
163 setData((data) => {
164 return {...data, quickAction:{}};
165 });
166 if( docsUpdated?.length == 0 ) { //when docs are zero in list and post_type 'any', set loading to true and refetch
167 setIsLoading(true);
168 setRefetch(true);
169 }
170 }
171 }).catch((error) => {
172 setData((data) => {
173 return {...data, quickAction:{}};
174 });
175 setTimeout(() => {
176 setLoaderIDS([]);
177 }, 1000);
178 });
179 }
180
181 if( action == 'permanent-delete' ) {
182 Swal.fire({
183 title: __("Are you sure you want to permanently delete this post?", "betterdocs"),
184 icon: "warning",
185 showConfirmButton: true,
186 showCancelButton:true,
187 confirmButtonText: __('Yes', 'betterdocs'),
188 cancelButtonText: __('No', 'betterdocs'),
189 customClass: {
190 popup: 'betterdocs-bulk-action-popup'
191 }
192 }).then((result) => {
193 if(result?.isConfirmed){
194 LoaderIDS.push(docId);
195 setLoaderIDS([...LoaderIDS]);
196 wp.apiFetch({ path: `/wp/v2/docs/${docId}`, method:'DELETE', data:{'force':true}} ).then((result) => {
197 if( result != undefined ){
198 let docsUpdated = docs?.filter((doc) => {
199 return doc?.id != docId;
200 });
201 if(typeof refetch == 'function'){ //refetch when permanent-delete is initiated
202 refetch(true);
203 }
204 setDocsData([...docsUpdated]);
205 setTimeout(() => {
206 setLoaderIDS([]);
207 }, 1000);
208 setData((data) => {
209 return {...data, quickAction:{}};
210 });
211 if( docsUpdated?.length == 0 && totalPages == 1 ) { //when docs are zero, set default query params and redirect to post_type 'any'
212 setQueryParams((prevState) => {
213 return {...prevState, status:'any', page:1};
214 })
215 navigate(makeRouteURL("betterdocs-admin", {
216 mode: "list",
217 post_status: "any",
218 }));
219 } else if ( docsUpdated?.length == 0 ) { //refetch when docs are zero
220 setIsLoading(true);
221 setRefetch(true);
222 }
223 }
224 }).catch((error) => {
225 toast.error(__('Failed To Delete Permanently', 'betterdocs'));
226 setData((data) => {
227 return {...data, quickAction:{}};
228 });
229 setTimeout(() => {
230 setLoaderIDS([]);
231 }, 1000);
232 });
233 }
234 });
235 }
236
237 if( action == 'bulk-delete' ){
238 let data = {};
239 let docsUpdated = docs;
240 let updatedLoaders = LoaderIDS;
241 if( forceDelete != undefined ) {
242 data['force'] = forceDelete ? true : false;
243 }
244 //delete bulk data
245 for( let id of bulkData ) {
246 wp.apiFetch({ path: `/wp/v2/docs/${id}`, method:'DELETE', data:data } ).then((result) => {
247 if(result != undefined){
248 docsUpdated = docsUpdated?.filter((doc) => {
249 return doc?.id != id;
250 });
251 updatedLoaders = updatedLoaders?.filter((docId) => {
252 return docId != id;
253 });
254 if(typeof refetch == 'function' && post_status == 'any' || typeof refetch == 'function' && post_status == 'trash' || typeof refetch == 'function' && post_status == undefined){ //refetch the trash count whenever bulk delete is initiated
255 refetch(true);
256 }
257 setDocsData([...docsUpdated]);
258 setLoaderIDS([...updatedLoaders]);
259 if(docsUpdated?.length == 0 && post_status == 'any'){ //when list is empty refetch for more docs, and initiate loading effect
260 setIsLoading(true);
261 setRefetch(true);
262 }
263 if(docsUpdated?.length == 0 && post_status == 'trash' && totalPages == 1){ //when docs are zero in the list for bulk-delete & has only a single page, set default query params and redirect to post type 'any'
264 setQueryParams((prevState) => {
265 return {...prevState, status:'any', page:1};
266 });
267 navigate(makeRouteURL("betterdocs-admin", {
268 mode: "list",
269 post_status: "any",
270 }));
271 } else if (docsUpdated?.length == 0 && post_status == 'trash') { //refetch when docs are zero and post_status is trash
272 setIsLoading(true);
273 setRefetch(true);
274 }
275 }
276 });
277 }
278 setBulkData([]);
279 setData((data) => {
280 return {...data, quickAction:{}};
281 });
282 }
283
284 if( action =='restore' ) {
285 let statusUpdatedDoc = {'status':'publish'};
286 restoreIDS.push(docId);
287 setRestoreIDS([...restoreIDS]);
288 wp.apiFetch({
289 path: `/wp/v2/docs/${docId}`,
290 method: "POST",
291 data: statusUpdatedDoc,
292 }).then((result) => {
293 if(result != undefined){
294 let docsUpdated = docs?.filter((doc) => {
295 return doc?.id != docId;
296 });
297 if(typeof refetch == 'function'){ //refetch when restore is initiated
298 refetch(true);
299 }
300 setDocsData([...docsUpdated]);
301 setData((data) => {
302 return {...data, quickAction:{}};
303 });
304 setTimeout(() => {
305 setRestoreIDS([])
306 }, 1000);
307 if(docsUpdated?.length == 0 && totalPages == 1) { //when docs are zero in the list for bulk-delete and total pages are 1, set default query params and redirect to post type 'any'
308 setQueryParams((prevState) => {
309 return {...prevState, status:'any', page:1};
310 });
311 navigate(makeRouteURL("betterdocs-admin", {
312 mode: "list",
313 post_status: "any",
314 }));
315 } else if ( docsUpdated?.length == 0 ) { //refetch when docs are zero
316 setIsLoading(true);
317 setRefetch(true);
318 }
319 }
320 }).catch((error) => {
321 setTimeout(() => {
322 setRestoreIDS([])
323 }, 1000);
324 setData((data) => {
325 return {...data, quickAction:{}};
326 });
327 });
328 }
329
330 if( action == 'bulk-restore' ){
331 let statusUpdatedDoc = {'status':'publish'};
332 let docsUpdated = docs;
333 let updatedLoaders = restoreIDS;
334 for( let id of bulkData ) {
335 wp.apiFetch({
336 path: `/wp/v2/docs/${id}`,
337 method: "POST",
338 data: statusUpdatedDoc,
339 }).then((result) => {
340 if(result != undefined){
341 docsUpdated = docsUpdated?.filter((doc) => {
342 return doc?.id != id;
343 });
344 updatedLoaders = updatedLoaders?.filter((docId) => {
345 return docId != id;
346 });
347 if(typeof refetch == 'function'){ //refetch when bulk-restore is initiated
348 refetch(true);
349 }
350 setDocsData([...docsUpdated]);
351 setRestoreIDS([...updatedLoaders]);
352 if( docsUpdated?.length == 0 && post_status == 'trash' && totalPages == 1 ){ //when docs are zero in the list for bulk-restore & total pages are 1, set default query params and redirect to post type 'any'
353 setQueryParams((prevState) => {
354 return {...prevState, status:'any', page:1};
355 });
356 navigate(makeRouteURL("betterdocs-admin", {
357 mode: "list",
358 post_status: "any",
359 }));
360 } else if ( docsUpdated?.length == 0 && post_status == 'trash' ) { //refetch when docs are zero and post_status is trash
361 setIsLoading(true);
362 setRefetch(true);
363 }
364 }
365 })
366 }
367 setBulkData([]);
368 setData((data) => {
369 return {...data, quickAction:{}};
370 });
371 }
372 }
373 }, [quickAction]);
374
375 /**
376 * Effect Runs When Filters Are Triggered
377 */
378 useEffect(() => {
379 let defaultParams = {
380 ...queryParams,
381 page:1,
382 per_page:20,
383 status:'any',
384 }
385 setQueryParams((prevState) => {
386 if( filterOptions != undefined && Object.keys(filterOptions)?.length > 0 ) {
387 defaultParams = objectDifference(prevState, filterOptions, ['author', 'doc_category', 'year_month', 'status', 'order']);
388 setIsLoading(true);
389 return {...defaultParams, ...filterOptions};
390 }
391 if( filterOptions != undefined && Object.keys(filterOptions)?.length == 0 ) {
392 defaultParams = objectDifference(prevState, filterOptions, ['author', 'doc_category', 'year_month', 'status', 'order']);
393 setIsLoading(true);
394 return {...defaultParams, status:'any'};
395 }
396 return prevState;
397 });
398 }, [filterOptions]);
399
400 // Empty knowledge base: no docs at all, default view, no search / MKB filter.
401 // Show the sample-doc generation panel only (hide the filter bar + table),
402 // matching the grid view's empty state.
403 const defaultView = post_status !== 'trash'
404 && !searchKeyword?.trim()
405 && (mkbFilterValue == null || mkbFilterValue === '');
406 // Genuinely-empty KB is known server-side at page load, so during the initial
407 // fetch show a skeleton shaped like the No Category Found card instead of the
408 // row skeleton it would immediately replace.
409 const showEmptyLoader = (isLoading || categoriesLoading) && defaultView && Boolean(window.betterdocs_admin?.kb_is_empty);
410 // No docs AND no categories — otherwise the categories the grid view happily shows
411 // would be hidden here behind the onboarding panel.
412 const isEmptyKB = !isLoading
413 && !categoriesLoading
414 && Number(totalItems) === 0
415 && Number(categoryCount || 0) === 0
416 && defaultView;
417
418 return (
419 <section className="betterdocs-dashboard-body betterdocs-dashboard-category-list" onClick={(e) => {
420 if( data?.isListMenuOpen ) {
421 setData((state) => ({...state, isListMenuOpen:false}));
422 }
423 }}>
424 <div className="betterdocs-dashboard-container">
425 {showEmptyLoader && <NoDocsLoader />}
426 {isEmptyKB && <NoDocsBody isDocs={false} />}
427 {!isEmptyKB && !showEmptyLoader && post_status != 'trash' && <div className="betterdocs-dashboard-category-list-filter">
428 <div className="betterdocs-row">
429 <div className="betterdocs-col betterdocs-col-md-3">
430 <AllDocsSearch
431 setSearchKeyword={setSearchKeyword}
432 />
433 </div>
434 <div className="betterdocs-col betterdocs-col-md-9">
435 <div className="betterdocs-dashboard-filter-wrap">
436 <AllDocsFilter
437 onFilterSelect={setFilterOptions}
438 />
439 </div>
440 </div>
441 </div>
442 </div>}
443 {bulkData?.length > 0 && <div className="betterdocs-dashboard-bulk-option">
444 <div className="betterdocs-row">
445 <div className="betterdocs-col betterdocs-col-md-6">
446 {/* <span className="betterdocs-dashboard-custom-checkbox">
447 <input id="bulk-items" type="checkbox" checked={bulkData?.length > 0 ? true : false} onClick={(e) => {
448 setBulkData([]);
449 }}></input>
450 <label For="bulk-items"></label>
451 </span> */}
452 <h5 className="bulk-option-item-count">
453 <span>{sprintf(_n('%d Item Selected', '%d Items Selected', bulkData?.length, 'betterdocs'), bulkData?.length)}</span>
454 </h5>
455 </div>
456 <div className="betterdocs-col betterdocs-col-md-6">
457 <div className="betterdocs-dashboard-bulk-option-actions">
458 <button
459 className="bulk-option-action-btn"
460 type="button"
461 onClick={(e) => {
462 Swal.fire({
463 text: post_status == 'any'
464 ? sprintf(_n("Are you sure you want to move %d Doc to the trash?", "Are you sure you want to move %d Docs to the trash?", bulkData?.length, "betterdocs"), bulkData?.length)
465 : sprintf(_n("Are you sure you want to permanently delete %d Doc?", "Are you sure you want to permanently delete %d Docs?", bulkData?.length, "betterdocs"), bulkData?.length),
466 icon: "warning",
467 showConfirmButton: true,
468 showCancelButton:true,
469 confirmButtonText: __('Yes', 'betterdocs'),
470 cancelButtonText: __('No', 'betterdocs'),
471 customClass: {
472 popup: 'betterdocs-bulk-action-popup'
473 }
474 }).then((result) => {
475 if( result?.isConfirmed ) {
476 setLoaderIDS([...bulkData]);
477 setData((state) => {
478 return {...state, quickAction:{action:'bulk-delete', forceDelete:post_status == 'trash' ? true : false}}
479 });
480 }
481 });
482 }}
483 >
484 {post_status == 'trash' ? __('Delete Permanently', 'betterdocs') : __("Move to Trash", 'betterdocs')}
485 </button>
486 {post_status == 'trash' && <button
487 className="bulk-option-action-btn"
488 type="button"
489 onClick={(e) => {
490 Swal.fire({
491 text: sprintf(_n("Are you sure you want to restore %d Doc?", "Are you sure you want to restore %d Docs?", bulkData?.length, "betterdocs"), bulkData?.length),
492 icon: "warning",
493 showConfirmButton: true,
494 showCancelButton:true,
495 confirmButtonText: __('Yes', 'betterdocs'),
496 cancelButtonText: __('No', 'betterdocs'),
497 customClass: {
498 popup: 'betterdocs-bulk-action-popup'
499 }
500 }).then((result) => {
501 if( result?.isConfirmed ) {
502 setRestoreIDS([...bulkData]);
503 setData((state) => {
504 return {...state, quickAction:{action:'bulk-restore'}}
505 });
506 }
507 });
508 }}
509 >
510 {__('Restore', 'betterdocs')}
511 </button>}
512 </div>
513 </div>
514 </div>
515 </div>}
516 {!isEmptyKB && !showEmptyLoader && <div className="betterdocs-dashboard-item-list-wrap">
517 <AllDocsListTable
518 setLoading={setIsLoading}
519 onFilterSelect={setFilterOptions}
520 docs={docs}
521 totalItems={totalItems}
522 totalPages={totalPages}
523 setCurrentPage={setQueryParams}
524 currentPage={queryParams?.page}
525 perPage={queryParams?.per_page}
526 bulkData={bulkData}
527 setBulkData={setBulkData}
528 status={post_status}
529 setLoaderIDS={setLoaderIDS}
530 loaderIDS={LoaderIDS}
531 isLoading={isLoading}
532 restoreIDS={restoreIDS}
533 />
534 {data?.isOpen && <QuickEdit setDocuments={setDocsData}/>}
535 </div>}
536 </div>
537 </section>
538 );
539 };
540
541 export default AllDocsList;
542