Skip to content

feature: FAQ section and shortcode#309

Open
sapayth wants to merge 7 commits intoweDevsOfficial:developfrom
sapayth:feature/faq_builder
Open

feature: FAQ section and shortcode#309
sapayth wants to merge 7 commits intoweDevsOfficial:developfrom
sapayth:feature/faq_builder

Conversation

@sapayth
Copy link
Copy Markdown
Member

@sapayth sapayth commented Apr 8, 2026

closes #314, closes #315, closes #221

Summary

Adds a full FAQ module to weDocs, allowing users to create and manage FAQs grouped by category directly from the WordPress admin.

What's included

  • New wedocs_faq post type and wedocs_faq_group taxonomy for structured FAQ storage
  • React-based admin UI (src/faq/) with a rich text editor (Tiptap) for FAQ content
  • Drag-and-drop sorting of FAQ groups and items via @dnd-kit
  • FAQ group management: create, rename, reorder, toggle active/inactive, delete groups
  • FAQ item CRUD: add, edit, reorder, and delete individual FAQ entries within groups
  • [wedocs_faq] shortcode for frontend display with accordion-style expand/collapse
    • Supports group, limit, orderby, and order attributes
    • Filterable query and template via wedocs_faq_shortcode_query_args, wedocs_get_faq_listing_template_dir, and wedocs_get_faq_listing_template_args hooks
  • General Settings toggle to show/hide FAQ section on the docs listing page
  • Frontend assets (assets/css/faq.css, assets/js/faq.js) for accordion behavior and styling
  • Admin submenu page under weDocs for managing FAQs
  • Minor readme updates for Gutenberg support and AI Doc Writer feature descriptions

Test plan

  • Activate the plugin and verify the FAQ submenu appears under weDocs
  • Create a new FAQ group, add FAQ items with rich text content, and save
  • Verify drag-and-drop reordering works for both groups and items
  • Toggle a group inactive and confirm it no longer appears on the frontend
  • Rename and delete groups; confirm changes persist
  • Use [wedocs_faq] shortcode on a page and verify FAQ accordion renders correctly
  • Test shortcode attributes: group="slug", limit="5", orderby="date", order="DESC"
  • Enable "Show FAQ" in weDocs General Settings and verify it appears on the docs listing page
  • Verify FAQ items expand/collapse on click in the frontend
  • Check that FAQ data uses proper nonces and capability checks (editor/admin roles)

Summary by CodeRabbit

  • New Features

    • Full FAQ management UI in admin: create/edit/duplicate/delete and reorder groups and items
    • Frontend FAQ shortcode and optional "Show FAQ on Docs Home" setting
    • Rich-text editor, add-item/group modals, confirm dialogs, and empty-state UI
    • Smooth expand/collapse animations and drag-and-drop ordering for groups and FAQs
    • Admin FAQ page with assets enqueued for editor and UI
  • Chores

    • Added editor-related dependencies to support rich-text FAQ editing

iftakharul-islam and others added 5 commits April 2, 2026 11:48
Register wedocs_faq post type, wedocs_faq_group taxonomy, and
associated meta fields. Add FAQ admin submenu page with React SPAusing tiptap rich text editor for FAQ content management.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37537c00-01d8-4d43-a934-17672f25c958

📥 Commits

Reviewing files that changed from the base of the PR and between 04db8f0 and 243f5eb.

📒 Files selected for processing (4)
  • assets/css/faq.css
  • includes/Admin/Menu.php
  • includes/Assets.php
  • templates/shortcode-faq.php
✅ Files skipped from review due to trivial changes (1)
  • assets/css/faq.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • templates/shortcode-faq.php
  • includes/Admin/Menu.php

Walkthrough

Adds a complete FAQ feature: new FAQ CPT/taxonomy and REST/meta, admin submenu and React-based FAQ builder, frontend shortcode/template rendering, new assets (CSS/JS + Tiptap), build entry, and a settings toggle to show FAQs on docs home.

Changes

Cohort / File(s) Summary
Post types & REST meta
includes/Post_Types.php
Adds wedocs_faq CPT and wedocs_faq_group taxonomy, REST-exposed post/meta and term meta (icon, order, status), and a get_terms reorder filter.
Admin menu & page
includes/Admin/Menu.php, templates/admin/faq.php
Adds FAQ submenu under wedocs, registers load hook wedocs_load_faq_page, and adds admin template mount point.
Assets registration & enqueue
includes/Assets.php, includes/Frontend.php
Registers and enqueues FAQ style/script, localizes wedocs-faq-script with weDocsFaqVars, and hooks enqueue on admin FAQ page.
Frontend CSS/JS
assets/css/faq.css, assets/js/faq.js
New FAQ stylesheet for layout and details/answer transitions; JS implements smooth expand/collapse animation for <details> FAQ items.
Shortcode & templates
includes/Shortcode.php, templates/shortcode-faq.php, templates/shortcode.php
Adds wedocs_faq shortcode and wedocs_faq() renderer; integrates FAQ block into main shortcode output; adds frontend template rendering grouped FAQs with <details> honoring _faq_open_by_default.
React FAQ builder app
src/faq/index.js, src/faq/components/*
New FAQ builder entry and components (FaqApp, FaqGroupRow, FaqItem, Tiptap editor, modals, drag-and-drop, CRUD flows) with REST interactions for groups and FAQs.
FAQ UI assets (src)
src/faq/assets/faq.css, src/faq/components/TiptapEditor.js
Tailwind/Tiptap editor styling and editor component with toolbar and behavior.
Forms & modals (src)
src/faq/components/AddFaqForm.js, AddFaqGroupModal.js, FaqConfirmDialog.js, EmptyFaq.js
Add UI for creating/editing groups and FAQs, confirmations, and empty-state.
Management & ordering (src)
src/faq/components/FaqApp.js, FaqGroupRow.js, FaqItem.js
Group list management, drag-and-drop reordering (dnd-kit), duplication/deletion flows, lazy FAQ loading, and per-item edit UI.
Build config & deps
webpack.config.js, package.json
Adds faq webpack entry and new Tiptap dependencies in package.json.
Settings UI
src/components/Settings/GeneralSettings.js
Adds "Show FAQ on Docs Home" toggle bound to show_faq setting.
Admin/frontend scaffolding
templates/admin/faq.php, templates/shortcode-faq.php
Adds admin mount point and frontend shortcode template for grouped FAQ output.
Misc
readme.txt
EOF newline normalization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Needs Review

Suggested reviewers

  • arifulhoque7
  • iftakharul-islam

Poem

🐰 I nudge a new branch with whiskers bright,

Groups and answers tucked just right,
Toggle, drag, and edit with glee,
A hopping builder for FAQty,
I thump my foot — the docs feel light!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feature: FAQ section and shortcode' clearly and concisely summarizes the main changes: addition of a new FAQ feature with both admin management and frontend shortcode functionality.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.46)

PHP Fatal error: Uncaught Error: Undefined constant "ABSPATH" in /includes/functions.php:355
Stack trace:
#0 /includes/functions.php(329): wedocs_is_plugin_active()
#1 /vendor/composer/autoload_real.php(39): require('...')
#2 /vendor/composer/autoload_real.php(43): {closure}()
#3 /vendor/autoload.php(25): ComposerAutoloaderInit23cbe5df2ef01f83092bb39fbf73cf03::getLoader()
#4 phar:///usr/bin/phpstan/bin/phpstan(45): require_once('...')
#5 phar:///usr/bin/phpstan/bin/phpstan(105): _PHPStan_5a70c2d68{closure}()
#6 /usr/bin/phpstan(7): require('...')
#7 {main}
thrown in /includes/functions.php on line 355
Fatal error: Uncaught Error: Undefined constant "ABSPATH" in /includes/functions.php:355
Stack trace:
#0 /includes/functions.php(329): wedocs_is_plugin_active()
#1 /vendor/composer/autoload_real.php(39): require('...')
#2 /vendor/composer/autoload_real.php(43): {closure}()
#3 /vendor/autoload.php(25): ComposerAutoloaderInit23cbe5df2ef01f83092bb39fbf73cf03::getLoader()
#4 phar:///usr/bin/phpstan/bin/phpstan(45): require_once('...')
#5 phar:///usr/bin/phpstan/bin/phpstan(105): _PHPStan_5a70c2d68{closure}()
#6 /usr/bin/phpstan(7): require('...')
#7 {main}
thrown in /includes/functions.php on line 355


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (15)
includes/Post_Types.php (1)

234-256: Consider adding auth_callback to term meta fields for consistency.

The post meta _faq_open_by_default has an auth_callback restricting writes to users with edit_docs capability (line 229-231), but the term meta fields (icon, order, status) don't have this protection. While the taxonomy capabilities should provide some protection, adding explicit auth_callback functions would ensure consistent authorization behavior.

💡 Optional: Add auth_callback to term meta
 register_term_meta( 'wedocs_faq_group', 'icon', [
     'type'          => 'integer',
     'single'        => true,
     'default'       => 0,
     'show_in_rest'  => true,
     'sanitize_callback' => 'absint',
+    'auth_callback'     => function () {
+        return current_user_can( 'edit_doc_terms' );
+    },
 ] );

 register_term_meta( 'wedocs_faq_group', 'order', [
     'type'          => 'integer',
     'single'        => true,
     'default'       => 0,
     'show_in_rest'  => true,
     'sanitize_callback' => 'absint',
+    'auth_callback'     => function () {
+        return current_user_can( 'edit_doc_terms' );
+    },
 ] );

 register_term_meta( 'wedocs_faq_group', 'status', [
     'type'              => 'boolean',
     'single'            => true,
     'default'           => true,
     'show_in_rest'      => true,
     'sanitize_callback' => 'rest_sanitize_boolean',
+    'auth_callback'     => function () {
+        return current_user_can( 'edit_doc_terms' );
+    },
 ] );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Post_Types.php` around lines 234 - 256, The term meta registrations
for register_term_meta on taxonomy 'wedocs_faq_group' for keys 'icon', 'order',
and 'status' lack an auth_callback unlike the post meta `_faq_open_by_default`;
add an auth_callback to each register_term_meta call that restricts writes to
users with the same capability check used for the post meta (e.g.,
current_user_can('edit_docs')) so authorization is explicit and consistent
across register_term_meta registrations.
src/faq/components/EmptyFaq.js (1)

13-71: Minor semantic HTML issue: <h2> contains <p> elements.

The heading element (<h2>) wraps both the SVG and paragraph elements (lines 65-70). This is invalid HTML as <h2> should only contain phrasing content, not block elements like <p>.

💡 Fix semantic structure
-                        <h2 className="mb-6">
+                        <div className="mb-6">
                             <svg
                                 xmlns="http://www.w3.org/2000/svg"
                                 ...
                             </svg>
-                            <p className="text-[`#3B3F4A`] font-bold text-2xl mx-auto">
+                            <h2 className="text-[`#3B3F4A`] font-bold text-2xl mx-auto">
                                 { __( 'Get started by creating your first FAQ', 'wedocs' ) }
-                            </p>
+                            </h2>
                             <p className="text-[`#666B79`] text-lg mx-auto mt-2">
                                 { __( 'Create frequently asked questions to help your users find answers quickly.', 'wedocs' ) }
                             </p>
-                        </h2>
+                        </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/EmptyFaq.js` around lines 13 - 71, The JSX in EmptyFaq.js
currently wraps block <p> elements and an <svg> inside a single <h2>, which is
invalid; change the outer <h2> to a <div> (preserve className="mb-6") and create
a proper heading element that contains only the title text (e.g., add a separate
<h2 className="text-[`#3B3F4A`] font-bold text-2xl mx-auto"> containing { __('Get
started by creating your first FAQ', 'wedocs') }). Keep the SVG and the
explanatory <p className="text-[`#666B79`]..."> as sibling elements under the new
wrapper so the structure is semantic and accessible.
src/faq/components/AddFaqGroupModal.js (1)

78-96: wp.media availability in admin context is guaranteed—the suggested guard is optional.

The wp.media global is available in all WordPress admin pages by default, including the FAQ admin interface. Since AddFaqGroupModal is used exclusively in the admin FAQ page, no guard is required for normal operation.

However, adding a defensive check as suggested is optional refactoring that would improve robustness if the component were ever used outside the admin context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/AddFaqGroupModal.js` around lines 78 - 96, The component
assumes wp.media exists in openMediaUploader which is fine in admin but fragile
if reused; add a defensive check at the top of openMediaUploader to verify wp
and wp.media exist and return early (or log an error) if they don't, then
proceed to create mediaUploader, attach the 'select' handler that calls setIcon
with attachment.id/url/alt, and open the uploader; reference openMediaUploader,
mediaUploader, and setIcon when making the change.
includes/Assets.php (1)

73-82: Duplicate array keys in weDocsAdminVars localization (pre-existing issue).

adminUrl and hasManageCap are defined twice in this array (lines 74/76 and 75/77). In PHP, duplicate array keys cause the later value to silently overwrite the earlier one. While this doesn't break functionality here since values are identical, it indicates copy-paste error and should be cleaned up.

♻️ Remove duplicate keys
             wp_localize_script(
                 'wedocs-app-script',
                 'weDocsAdminVars',
                 array(
                     'adminUrl'     => admin_url(),
                     'hasManageCap' => current_user_can( 'manage_options' ),
                     'aiProviderConfigs' => wedocs_get_ai_provider_configs(),
-                    'adminUrl'      => admin_url(),
-                    'hasManageCap'  => current_user_can( 'manage_options' ),
                     'weDocsUrl'     => admin_url( 'admin.php?page=wedocs#/' ),
                     'pro_active'    => wedocs_is_pro_active(),
                     'upgradePopupContent' => wedocs_get_upgrade_popup_content(),
                 ),
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Assets.php` around lines 73 - 82, The weDocsAdminVars localization
array contains duplicate keys "adminUrl" and "hasManageCap" (in the
weDocsAdminVars array emitted from Assets.php); remove the redundant entries so
each key appears only once (keep one 'adminUrl' => admin_url() and one
'hasManageCap' => current_user_can('manage_options')), preserving the other keys
like 'aiProviderConfigs', 'weDocsUrl', 'pro_active', and 'upgradePopupContent'
and ensuring no other duplicate keys remain.
assets/css/faq.css (1)

48-58: Unused transform transition on expand/collapse indicator.

Line 53 applies transition: transform 0.3s ease to the ::after pseudo-element, but the indicator uses content change ('+' to '−') rather than a transform to indicate state. The transition has no visual effect.

Consider either removing the unused transition or switching to a rotation-based indicator for smoother visual feedback.

♻️ Option A: Remove unused transition
 .wedocs-faq-item__question::after {
     content: '+';
     font-size: 20px;
     font-weight: 300;
     color: `#6b7280`;
-    transition: transform 0.3s ease;
 }
♻️ Option B: Use rotation-based indicator
 .wedocs-faq-item__question::after {
-    content: '+';
+    content: '';
+    width: 12px;
+    height: 12px;
+    border-right: 2px solid `#6b7280`;
+    border-bottom: 2px solid `#6b7280`;
+    transform: rotate(45deg);
-    font-size: 20px;
-    font-weight: 300;
-    color: `#6b7280`;
     transition: transform 0.3s ease;
 }
 
 .wedocs-faq-item[open] .wedocs-faq-item__question::after {
-    content: '\2212';
+    transform: rotate(-135deg);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/css/faq.css` around lines 48 - 58, The current
.wedocs-faq-item__question::after uses transition: transform 0.3s ease but the
visual state toggles by changing content (plus/minus), so either remove the
unused transition or switch to a rotation-based indicator: Option A - delete the
transition property from .wedocs-faq-item__question::after (and keep the content
swap in .wedocs-faq-item[open] .wedocs-faq-item__question::after); Option B -
keep a single symbol (e.g. '+') in .wedocs-faq-item__question::after, add
display:inline-block and transform:rotate(0deg) plus transition:transform 0.3s
ease, then in .wedocs-faq-item[open] .wedocs-faq-item__question::after replace
content change with transform:rotate(45deg) or rotate(45deg)/rotate(90deg) to
visually indicate open state; update the selectors
.wedocs-faq-item__question::after and .wedocs-faq-item[open]
.wedocs-faq-item__question::after accordingly.
assets/js/faq.js (2)

5-7: Consider null-checks on queried elements.

If the template markup is malformed or modified by another plugin, summary or answer could be null, causing errors when accessing .style or .addEventListener.

🛡️ Add defensive null checks
     document.querySelectorAll( '.wedocs-faq-item' ).forEach( function ( details ) {
         var summary = details.querySelector( '.wedocs-faq-item__question' );
         var answer  = details.querySelector( '.wedocs-faq-item__answer' );
+
+        if ( ! summary || ! answer ) {
+            return;
+        }
 
         // Items open by default need the inline style set so transitions work.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/faq.js` around lines 5 - 7, Add defensive null-checks for queried
elements inside the loop: after selecting each '.wedocs-faq-item' node and
assigning variables summary and answer, verify that summary and answer are
non-null before accessing their properties (like .style or calling
.addEventListener). Update the block that iterates
document.querySelectorAll('.wedocs-faq-item') to skip or safely handle items
where summary or answer is falsy (e.g., continue the loop or wrap usages in if
(summary && answer) { ... }) so malformed/modified templates don’t cause runtime
errors.

17-23: Closing animation may stall if transitionend doesn't fire.

The transitionend handler on lines 20-23 removes the open attribute after the animation completes. However, if the transition doesn't fire (e.g., prefers-reduced-motion, zero-duration transition, or element hidden during animation), the handler never executes and details.open remains true despite the visual state being closed.

Consider adding a timeout fallback or listening for prefers-reduced-motion:

♻️ Add timeout fallback for reliability
             if ( details.open ) {
                 // Closing: let the transition finish before removing open.
                 answer.style.gridTemplateRows = '0fr';
+                var transitionDuration = 350; // slightly longer than CSS 0.3s
+                var timeoutId = setTimeout( function() {
+                    details.open = false;
+                }, transitionDuration );
                 answer.addEventListener( 'transitionend', function handler() {
+                    clearTimeout( timeoutId );
                     details.open = false;
                     answer.removeEventListener( 'transitionend', handler );
                 } );
             } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/faq.js` around lines 17 - 23, The closing flow relying solely on
the transitionend handler can stall if the transition never fires; update the
block that sets answer.style.gridTemplateRows = '0fr' and the transitionend
listener (the anonymous handler which sets details.open = false and removes
itself) to include a reliable fallback: after attaching the transitionend
listener, start a short timeout (e.g., 300ms) that will perform the same cleanup
(set details.open = false and remove the listener) if transitionend doesn't
fire, and also check window.matchMedia('(prefers-reduced-motion: reduce)') to
skip the animation and perform immediate cleanup; ensure both paths remove the
event listener and clear the timeout to avoid leaks.
src/faq/components/AddFaqForm.js (1)

148-156: Add ARIA role for the toggle switch.

The toggle button should have role="switch" and aria-checked for better screen reader support.

♿ Accessibility improvement
 <button
     onClick={ () => setOpenByDefault( ( prev ) => ! prev ) }
+    role="switch"
+    aria-checked={ openByDefault }
     className={ `relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${ openByDefault ? 'bg-indigo-600' : 'bg-gray-300' }` }
     aria-label={ __( 'Toggle open by default', 'wedocs' ) }
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/AddFaqForm.js` around lines 148 - 156, The toggle button
in AddFaqForm toggles openByDefault via setOpenByDefault but lacks ARIA switch
semantics; update the button element (where setOpenByDefault and openByDefault
are used) to include role="switch" and aria-checked={openByDefault} and ensure
the aria-label remains descriptive (e.g., "Toggle open by default"); keep the
existing onClick handler and visual classes unchanged so behavior and styling
are preserved while adding the role and aria-checked attributes for screen
readers.
src/faq/components/FaqItem.js (2)

268-276: Add ARIA role for the toggle switch.

Similar to AddFaqForm, the toggle button should have role="switch" and aria-checked for accessibility.

♿ Accessibility improvement
 <button
     onClick={ () => handleToggleOpenByDefault( ! openByDefault ) }
+    role="switch"
+    aria-checked={ openByDefault }
     className={ `relative inline-flex h-5 w-9 items-center rounded-full transition-colors ${ openByDefault ? 'bg-indigo-600' : 'bg-gray-300' }` }
     aria-label={ __( 'Toggle open by default', 'wedocs' ) }
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/FaqItem.js` around lines 268 - 276, The toggle button in
FaqItem.js lacks ARIA semantics; update the button rendered where
handleToggleOpenByDefault and openByDefault are used to include role="switch"
and aria-checked={openByDefault} (and ensure aria-label remains) so screen
readers treat it as a switch; keep the existing onClick handler and class logic
unchanged but add those ARIA attributes to the same button element.

61-91: Inconsistent answer validation compared to AddFaqForm.

handleSave only validates that question.trim() !== '' but doesn't validate the answer field. This is inconsistent with AddFaqForm which requires both question and answer. Consider adding answer validation if empty answers should be rejected.

🔍 Add answer validation for consistency
 const handleSave = async () => {
-    if ( question.trim() === '' || isSaving ) {
+    if ( question.trim() === '' || answer.trim() === '' || isSaving ) {
         return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/FaqItem.js` around lines 61 - 91, handleSave currently
only validates question.trim() and allows empty answer values, which is
inconsistent with AddFaqForm; update handleSave in FaqItem.js to also check
answer.trim() along with question.trim() and isSaving before proceeding,
returning early if either is empty, so the same validation logic as AddFaqForm
is applied; keep the existing setIsSaving, try/catch/finally flow and ensure the
UI stays in edit mode on failure and onFaqUpdated(updated) is still called on
success.
src/faq/components/TiptapEditor.js (1)

126-141: Consider replacing window.prompt with a modal dialog.

Using window.prompt for URL input works but provides a basic UX and can be blocked by browser settings. A custom modal dialog would provide better styling consistency and reliability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/TiptapEditor.js` around lines 126 - 141, The setLink
function currently uses window.prompt to collect the URL which is unreliable and
offers poor UX; replace this by invoking a reusable modal dialog component that
returns the entered URL (or null) asynchronously. Implement a modal (e.g.,
OpenUrlModal) that handles validation, accessibility and styling, call it from
setLink instead of window.prompt, then keep the existing logic that checks for
null (cancel) or empty string (unsetLink via
editor.chain().focus().extendMarkRange('link').unsetLink().run()) and otherwise
call editor.chain().focus().extendMarkRange('link').setLink({ href: url
}).run(); ensure the modal returns the same semantics as prompt so existing
conditional flows in setLink remain valid.
includes/Admin/Menu.php (1)

60-84: Update the @since placeholder before release.

The WEDOCS_SINCE placeholder on lines 63 and 78 should be replaced with the actual version number before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Admin/Menu.php` around lines 60 - 84, Replace the placeholder tag
WEDOCS_SINCE in the docblocks for the methods faq_menu_action() and
display_faq() with the actual release version string; update both occurrences in
the Admin\\Menu class docblocks so the `@since` annotations show the real version
number (e.g., "1.2.3") before merging.
src/faq/components/FaqGroupRow.js (2)

241-269: Consider using batch deletion or Promise.all for FAQ deletion.

The sequential deletion of FAQs in a for-of loop can be slow for groups with many FAQs. Consider using Promise.all for parallel deletion (with appropriate rate limiting if needed).

⚡ Parallel deletion for better performance
-            for ( const faq of groupFaqs ) {
-                await apiFetch( {
+            await Promise.all( groupFaqs.map( ( faq ) =>
+                apiFetch( {
                     path: `/wp/v2/wedocs-faqs/${ faq.id }?force=true`,
                     method: 'DELETE',
-                } );
-            }
+                } )
+            ) );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/FaqGroupRow.js` around lines 241 - 269, The handleDelete
function currently deletes FAQs sequentially in a for-of loop which is slow for
many items; refactor it to delete FAQs in parallel by mapping groupFaqs to
apiFetch DELETE promises and awaiting Promise.all (or a small-batch Promise.all
for rate-limiting) before deleting the group itself, ensuring you still catch
errors and call setIsDeleting, setShowDeleteConfirm, and onGroupDeleted (use
group.id) appropriately; reference handleDelete, groupFaqs, apiFetch,
onGroupDeleted, setIsDeleting, and setShowDeleteConfirm when making the change.

357-366: Add ARIA role for the toggle switch.

For consistency with accessibility standards and other toggle implementations, add role="switch" and aria-checked.

♿ Accessibility improvement
 <button
     onClick={ handleToggle }
     disabled={ isToggling }
+    role="switch"
+    aria-checked={ isActive }
     className={ `relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ isActive ? 'bg-indigo-600' : 'bg-gray-300' } ${ isToggling ? 'opacity-50 cursor-not-allowed' : '' }` }
     aria-label={ isActive ? __( 'Deactivate FAQ group', 'wedocs' ) : __( 'Activate FAQ group', 'wedocs' ) }
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/FaqGroupRow.js` around lines 357 - 366, The toggle button
in the FaqGroupRow component lacks ARIA attributes for a proper switch control;
update the button element used by handleToggle to include role="switch" and
aria-checked={isActive} (keeping the existing aria-label and disabled handling),
and ensure the props/variables isActive and isToggling are used to set those
attributes so assistive tech gets the correct on/off state and disabled state.
src/faq/components/FaqApp.js (1)

93-101: Consider batching the order persistence requests.

The current implementation fires N separate API requests when reordering groups. For large numbers of groups, this could be slow and cause rate-limiting issues. Consider implementing a debounced batch update or a single endpoint that accepts multiple order updates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/faq/components/FaqApp.js` around lines 93 - 101, The current
reordered.forEach loop issues N apiFetch calls (each PATCH/POST to
`/wp/v2/wedocs-faq-groups/${group.id}`) which should be replaced with a single
batched request: build an array payload of { id: group.id, order: index } from
reordered (preserving meta.order values), then send one apiFetch call (e.g.,
POST to a new batch endpoint like `/wp/v2/wedocs-faq-groups/batch` or to an
existing bulk update handler) with data: { orders: [...] }; alternatively, if a
server batch endpoint isn't available, debounce the reorder handler (wrap
reordered processing with a short debounce) and then send fewer combined
requests, ensuring you stop using reordered.forEach to fire immediate per-item
requests and instead call a single function (e.g., sendGroupOrderBatch) that
constructs the payload and calls apiFetch once.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@includes/Shortcode.php`:
- Around line 225-237: Validate the $args['orderby'] and $args['order'] values
before placing them into $faq_query_args: check $args['orderby'] against a
whitelist of allowed orderby values (e.g.,
'none','ID','author','title','name','type','date','modified','parent','rand','menu_order','meta_value','meta_value_num','post__in')
and $args['order'] against ['ASC','DESC'] (case-insensitive), and if a value is
not allowed replace it with the safe default used by the shortcode (e.g., 'date'
and 'DESC'); perform normalization (uppercase for order) and fallback handling
where these sanitized values are then assigned to $faq_query_args['orderby'] and
$faq_query_args['order'] so WP_Query receives only validated inputs.

---

Nitpick comments:
In `@assets/css/faq.css`:
- Around line 48-58: The current .wedocs-faq-item__question::after uses
transition: transform 0.3s ease but the visual state toggles by changing content
(plus/minus), so either remove the unused transition or switch to a
rotation-based indicator: Option A - delete the transition property from
.wedocs-faq-item__question::after (and keep the content swap in
.wedocs-faq-item[open] .wedocs-faq-item__question::after); Option B - keep a
single symbol (e.g. '+') in .wedocs-faq-item__question::after, add
display:inline-block and transform:rotate(0deg) plus transition:transform 0.3s
ease, then in .wedocs-faq-item[open] .wedocs-faq-item__question::after replace
content change with transform:rotate(45deg) or rotate(45deg)/rotate(90deg) to
visually indicate open state; update the selectors
.wedocs-faq-item__question::after and .wedocs-faq-item[open]
.wedocs-faq-item__question::after accordingly.

In `@assets/js/faq.js`:
- Around line 5-7: Add defensive null-checks for queried elements inside the
loop: after selecting each '.wedocs-faq-item' node and assigning variables
summary and answer, verify that summary and answer are non-null before accessing
their properties (like .style or calling .addEventListener). Update the block
that iterates document.querySelectorAll('.wedocs-faq-item') to skip or safely
handle items where summary or answer is falsy (e.g., continue the loop or wrap
usages in if (summary && answer) { ... }) so malformed/modified templates don’t
cause runtime errors.
- Around line 17-23: The closing flow relying solely on the transitionend
handler can stall if the transition never fires; update the block that sets
answer.style.gridTemplateRows = '0fr' and the transitionend listener (the
anonymous handler which sets details.open = false and removes itself) to include
a reliable fallback: after attaching the transitionend listener, start a short
timeout (e.g., 300ms) that will perform the same cleanup (set details.open =
false and remove the listener) if transitionend doesn't fire, and also check
window.matchMedia('(prefers-reduced-motion: reduce)') to skip the animation and
perform immediate cleanup; ensure both paths remove the event listener and clear
the timeout to avoid leaks.

In `@includes/Admin/Menu.php`:
- Around line 60-84: Replace the placeholder tag WEDOCS_SINCE in the docblocks
for the methods faq_menu_action() and display_faq() with the actual release
version string; update both occurrences in the Admin\\Menu class docblocks so
the `@since` annotations show the real version number (e.g., "1.2.3") before
merging.

In `@includes/Assets.php`:
- Around line 73-82: The weDocsAdminVars localization array contains duplicate
keys "adminUrl" and "hasManageCap" (in the weDocsAdminVars array emitted from
Assets.php); remove the redundant entries so each key appears only once (keep
one 'adminUrl' => admin_url() and one 'hasManageCap' =>
current_user_can('manage_options')), preserving the other keys like
'aiProviderConfigs', 'weDocsUrl', 'pro_active', and 'upgradePopupContent' and
ensuring no other duplicate keys remain.

In `@includes/Post_Types.php`:
- Around line 234-256: The term meta registrations for register_term_meta on
taxonomy 'wedocs_faq_group' for keys 'icon', 'order', and 'status' lack an
auth_callback unlike the post meta `_faq_open_by_default`; add an auth_callback
to each register_term_meta call that restricts writes to users with the same
capability check used for the post meta (e.g., current_user_can('edit_docs')) so
authorization is explicit and consistent across register_term_meta
registrations.

In `@src/faq/components/AddFaqForm.js`:
- Around line 148-156: The toggle button in AddFaqForm toggles openByDefault via
setOpenByDefault but lacks ARIA switch semantics; update the button element
(where setOpenByDefault and openByDefault are used) to include role="switch" and
aria-checked={openByDefault} and ensure the aria-label remains descriptive
(e.g., "Toggle open by default"); keep the existing onClick handler and visual
classes unchanged so behavior and styling are preserved while adding the role
and aria-checked attributes for screen readers.

In `@src/faq/components/AddFaqGroupModal.js`:
- Around line 78-96: The component assumes wp.media exists in openMediaUploader
which is fine in admin but fragile if reused; add a defensive check at the top
of openMediaUploader to verify wp and wp.media exist and return early (or log an
error) if they don't, then proceed to create mediaUploader, attach the 'select'
handler that calls setIcon with attachment.id/url/alt, and open the uploader;
reference openMediaUploader, mediaUploader, and setIcon when making the change.

In `@src/faq/components/EmptyFaq.js`:
- Around line 13-71: The JSX in EmptyFaq.js currently wraps block <p> elements
and an <svg> inside a single <h2>, which is invalid; change the outer <h2> to a
<div> (preserve className="mb-6") and create a proper heading element that
contains only the title text (e.g., add a separate <h2 className="text-[`#3B3F4A`]
font-bold text-2xl mx-auto"> containing { __('Get started by creating your first
FAQ', 'wedocs') }). Keep the SVG and the explanatory <p
className="text-[`#666B79`]..."> as sibling elements under the new wrapper so the
structure is semantic and accessible.

In `@src/faq/components/FaqApp.js`:
- Around line 93-101: The current reordered.forEach loop issues N apiFetch calls
(each PATCH/POST to `/wp/v2/wedocs-faq-groups/${group.id}`) which should be
replaced with a single batched request: build an array payload of { id:
group.id, order: index } from reordered (preserving meta.order values), then
send one apiFetch call (e.g., POST to a new batch endpoint like
`/wp/v2/wedocs-faq-groups/batch` or to an existing bulk update handler) with
data: { orders: [...] }; alternatively, if a server batch endpoint isn't
available, debounce the reorder handler (wrap reordered processing with a short
debounce) and then send fewer combined requests, ensuring you stop using
reordered.forEach to fire immediate per-item requests and instead call a single
function (e.g., sendGroupOrderBatch) that constructs the payload and calls
apiFetch once.

In `@src/faq/components/FaqGroupRow.js`:
- Around line 241-269: The handleDelete function currently deletes FAQs
sequentially in a for-of loop which is slow for many items; refactor it to
delete FAQs in parallel by mapping groupFaqs to apiFetch DELETE promises and
awaiting Promise.all (or a small-batch Promise.all for rate-limiting) before
deleting the group itself, ensuring you still catch errors and call
setIsDeleting, setShowDeleteConfirm, and onGroupDeleted (use group.id)
appropriately; reference handleDelete, groupFaqs, apiFetch, onGroupDeleted,
setIsDeleting, and setShowDeleteConfirm when making the change.
- Around line 357-366: The toggle button in the FaqGroupRow component lacks ARIA
attributes for a proper switch control; update the button element used by
handleToggle to include role="switch" and aria-checked={isActive} (keeping the
existing aria-label and disabled handling), and ensure the props/variables
isActive and isToggling are used to set those attributes so assistive tech gets
the correct on/off state and disabled state.

In `@src/faq/components/FaqItem.js`:
- Around line 268-276: The toggle button in FaqItem.js lacks ARIA semantics;
update the button rendered where handleToggleOpenByDefault and openByDefault are
used to include role="switch" and aria-checked={openByDefault} (and ensure
aria-label remains) so screen readers treat it as a switch; keep the existing
onClick handler and class logic unchanged but add those ARIA attributes to the
same button element.
- Around line 61-91: handleSave currently only validates question.trim() and
allows empty answer values, which is inconsistent with AddFaqForm; update
handleSave in FaqItem.js to also check answer.trim() along with question.trim()
and isSaving before proceeding, returning early if either is empty, so the same
validation logic as AddFaqForm is applied; keep the existing setIsSaving,
try/catch/finally flow and ensure the UI stays in edit mode on failure and
onFaqUpdated(updated) is still called on success.

In `@src/faq/components/TiptapEditor.js`:
- Around line 126-141: The setLink function currently uses window.prompt to
collect the URL which is unreliable and offers poor UX; replace this by invoking
a reusable modal dialog component that returns the entered URL (or null)
asynchronously. Implement a modal (e.g., OpenUrlModal) that handles validation,
accessibility and styling, call it from setLink instead of window.prompt, then
keep the existing logic that checks for null (cancel) or empty string (unsetLink
via editor.chain().focus().extendMarkRange('link').unsetLink().run()) and
otherwise call editor.chain().focus().extendMarkRange('link').setLink({ href:
url }).run(); ensure the modal returns the same semantics as prompt so existing
conditional flows in setLink remain valid.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 28ab20c7-9a76-4d58-b3df-8d550549cbb6

📥 Commits

Reviewing files that changed from the base of the PR and between 82ee4e0 and d115337.

📒 Files selected for processing (24)
  • assets/css/faq.css
  • assets/js/faq.js
  • includes/Admin/Menu.php
  • includes/Assets.php
  • includes/Frontend.php
  • includes/Post_Types.php
  • includes/Shortcode.php
  • package.json
  • readme.txt
  • src/components/Settings/GeneralSettings.js
  • src/faq/assets/faq.css
  • src/faq/components/AddFaqForm.js
  • src/faq/components/AddFaqGroupModal.js
  • src/faq/components/EmptyFaq.js
  • src/faq/components/FaqApp.js
  • src/faq/components/FaqConfirmDialog.js
  • src/faq/components/FaqGroupRow.js
  • src/faq/components/FaqItem.js
  • src/faq/components/TiptapEditor.js
  • src/faq/index.js
  • templates/admin/faq.php
  • templates/shortcode-faq.php
  • templates/shortcode.php
  • webpack.config.js

Comment thread includes/Shortcode.php
Comment on lines +225 to +237
$faq_query_args = [
'post_type' => 'wedocs_faq',
'posts_per_page' => (int) $args['limit'],
'orderby' => $args['orderby'],
'order' => $args['order'],
'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
[
'taxonomy' => 'wedocs_faq_group',
'field' => 'term_id',
'terms' => $group->term_id,
],
],
];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate orderby and order attributes against allowed values.

The orderby and order shortcode attributes are passed directly to WP_Query without validation. While WordPress sanitizes these internally, explicitly validating against allowed values prevents unexpected behavior and potential information disclosure.

🛡️ Proposed fix
+        $allowed_orderby = [ 'menu_order', 'title', 'date', 'ID' ];
+        $allowed_order   = [ 'ASC', 'DESC' ];
+
         $faq_query_args = [
             'post_type'      => 'wedocs_faq',
             'posts_per_page' => (int) $args['limit'],
-            'orderby'        => $args['orderby'],
-            'order'          => $args['order'],
+            'orderby'        => in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'menu_order',
+            'order'          => in_array( strtoupper( $args['order'] ), $allowed_order, true ) ? strtoupper( $args['order'] ) : 'ASC',
             'tax_query'      => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$faq_query_args = [
'post_type' => 'wedocs_faq',
'posts_per_page' => (int) $args['limit'],
'orderby' => $args['orderby'],
'order' => $args['order'],
'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
[
'taxonomy' => 'wedocs_faq_group',
'field' => 'term_id',
'terms' => $group->term_id,
],
],
];
$allowed_orderby = [ 'menu_order', 'title', 'date', 'ID' ];
$allowed_order = [ 'ASC', 'DESC' ];
$faq_query_args = [
'post_type' => 'wedocs_faq',
'posts_per_page' => (int) $args['limit'],
'orderby' => in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'menu_order',
'order' => in_array( strtoupper( $args['order'] ), $allowed_order, true ) ? strtoupper( $args['order'] ) : 'ASC',
'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
[
'taxonomy' => 'wedocs_faq_group',
'field' => 'term_id',
'terms' => $group->term_id,
],
],
];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Shortcode.php` around lines 225 - 237, Validate the $args['orderby']
and $args['order'] values before placing them into $faq_query_args: check
$args['orderby'] against a whitelist of allowed orderby values (e.g.,
'none','ID','author','title','name','type','date','modified','parent','rand','menu_order','meta_value','meta_value_num','post__in')
and $args['order'] against ['ASC','DESC'] (case-insensitive), and if a value is
not allowed replace it with the safe default used by the shortcode (e.g., 'date'
and 'DESC'); perform normalization (uppercase for order) and fallback handling
where these sanitized values are then assigned to $faq_query_args['orderby'] and
$faq_query_args['order'] so WP_Query receives only validated inputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants