Skip to content

Feature/user directory elementor widget#1825

Open
sapayth wants to merge 27 commits into
weDevsOfficial:developfrom
sapayth:feature/user_directory_elementor_widget
Open

Feature/user directory elementor widget#1825
sapayth wants to merge 27 commits into
weDevsOfficial:developfrom
sapayth:feature/user_directory_elementor_widget

Conversation

@sapayth
Copy link
Copy Markdown
Contributor

@sapayth sapayth commented Mar 4, 2026

depends on #1778

closes #1410, closes #1445, closes #1446

Summary

This PR adds an Elementor widget so the directory can be embedded anywhere using the Elementor page builder — no shortcodes required.

What's New

  • Elementor widget: A dedicated "User Directory" widget is available in Elementor's widget panel, allowing full visual control over placement and styling.

Technical Notes

  • New Elementor widget class: WeDevs\Wpuf\Integrations\Elementor\User_Directory_Widget

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Elementor User Directory widget with customizable controls for search, filters, user cards, typography, and pagination
    • Enabled pretty URL support for pages using the widget
    • Implemented editor preview for User Directory widget in Elementor

arifulhoque7 and others added 25 commits November 28, 2025 23:59
Implements a limited User Directory in the free version, including setup wizard UI, REST API, shortcode handler, and directory limit logic. Adds new assets, React components, admin menu, post type registration, and views for directory/profile layouts. Pro-only features are previewed with disabled states and upgrade prompts, following the Pro Preview pattern.
Added ! prefix to purple color classes in layout helpers and Tailwind config to ensure higher specificity. Updated safelist in Tailwind config to include both prefixed and non-prefixed classes, and adjusted button class usage for consistency.
Improves member count calculation in the REST API and displays live member counts in the directory list. Refines input styles for number fields, updates the toast notification component for better UX, and adds a Pro badge to the 'New Directory' button when the directory limit is reached.
Introduces a new 'Files' tab to the user profile with grouped file display and filtering, excluding private message attachments. Updates Gruntfile.js to add user directory build/watch tasks and ensures profile size is passed to the template. Also adds the corresponding template file for the new tab.
Introduced a wide range of new utility classes to form-builder.css for layout, spacing, sizing, display, border, background, and other style properties. These additions improve flexibility and consistency for admin UI components and support more granular styling options.
Moved user directory assets and configs to modules/user-directory, updated Gruntfile.js to reflect new paths, and adjusted build/watch tasks. Renamed Tailwind and Webpack configs, added PostCSS config, and updated package files to support the new module structure.
Updated sort option values from 'ID' to 'id' and adjusted related logic for consistency. Removed free/pro checks from handlers to simplify state updates. Improved UI/UX for avatar and sort options, added Pro-locked features with badges and tooltips, and enhanced descriptions for better clarity.
Changed primary color scheme from emerald to purple across helpers and Tailwind config. Updated profile layout-2 to add a cover photo section, adjust class names, and improve header overlap. Cleaned up the About tab by removing Pro upgrade prompts and related UI elements.
Changed Tailwind primary and hover colors from purple to emerald green in tailwind.config.js. Updated related CSS class in App.js to use emerald color for upgrade link, ensuring visual consistency with the new color scheme.
Updated avatar rendering to use the same logic as the directory listing, prioritizing custom profile photos, then Gravatar, and finally user initials as a fallback. Improved initials calculation and font sizing for better visual consistency. Simplified markup to show either the avatar image or initials, not both, and ensured consistent sizing and styling.
Add early return if there are no items to display in the pagination shortcode. Also update the current page styling to use layout-specific color classes for better theme consistency.
Refactored multiple files in the user-directory module, including Admin_Menu.php, Directory.php, Helpers.php, Post_Type.php, PrettyUrls.php, Shortcode.php, and profile layout. Updated Toast.js in the frontend and regenerated several minified CSS files to reflect style changes.
Updated both Directory API and Shortcode to skip applying the 'role__in' filter if 'all' is present in the roles array. This prevents unnecessary filtering when all roles should be included.
Updated default and saved settings to include all profile tabs for better Pro compatibility. Improved REST API to merge settings with defaults and existing values, and to sanitize and save all profile tab-related fields. Adjusted frontend JS and wizard defaults to reflect all tabs, and fixed minor issues with sort option selection and profile slug decoding.
Introduce a reusable SingleSelect dropdown and refactor User Directory UI/UX. Key changes:

- Add src/js/user-directory/components/common/SingleSelect.js: new reusable single-select dropdown with grouped options and ProBadge support.
- Refactor StepAdvanced to use SingleSelect for sort and gallery size, simplify Pro feature handling, change free avatar default from 192→128 and adjust avatar size isFree flags, and convert several hover-only Pro badges into clickable upgrade links.
- Update StepLayout to show Pro badge on hover with an upgrade link, simplify border/opacity logic and add local hover state.
- Update App.js ProBadge into a clickable upgrade link and reposition/remove some hover-only Pro UI (New Directory button now shows badge inline when limit reached; limit warning block removed/streamlined).
- package.json: ensure user-directory build/dev scripts run npm ci before npm run build/dev.
- Remove get_current_page() from modules/user-directory/Shortcode.php (cleanup).

These changes centralize dropdown logic, improve upgrade CTA consistency, and tidy UI behavior for Pro-locked features.
Replace the native <select> for the profile_base field with a SingleSelect component, passing profileBases as options and adapting the onChange to call the existing handleChange ({ target: { name, value } }). Remove the now-unused inputStyle constant and its comment block. This simplifies the markup and delegates select rendering/styling to the SingleSelect component.
Safelist new button and focus utilities in the user-directory Tailwind config and update DirectoryWizard and StepTabs components. DirectoryWizard: replace verbose button classnames with wpuf-btn-white, add focus ring utilities to Cancel/Prev/Next/Next (submit) buttons for better accessibility, adjust the footer container style to ensure correct left/right positioning and enforce background color. StepTabs: only render the ProBadge on hover to reduce visual clutter. These changes centralize button styles and improve keyboard focus visibility and layout consistency.
…ro-functionality' into feature/user_directory_elementor_widget

# Conflicts:
#	assets/css/admin/subscriptions.min.css
#	assets/css/ai-form-builder.min.css
#	assets/css/forms-list.min.css
#	assets/css/frontend-subscriptions.min.css
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

Walkthrough

This PR adds comprehensive Elementor integration for the User Directory widget, including new CSS styling, widget component implementation, rewrite rules management for pretty URLs, and asset enqueuing logic to support the widget in Elementor editor and frontend contexts.

Changes

Cohort / File(s) Summary
Styling and Configuration
assets/css/elementor-user-directory.css, modules/user-directory/postcss.config.js
New CSS file for User Directory widget styling (wrapper, empty states, profile separators) and PostCSS config with Tailwind CSS and Autoprefixer support.
Asset Registration
includes/Assets.php
Registers the new elementor-user-directory CSS asset for enqueuing alongside existing styles.
Elementor Integration Core
includes/Integrations/Elementor/Elementor.php
Adds rewrite rules management for Elementor pages containing User Directory widget, asset enqueuing for frontend styles/scripts, REST endpoint localization, and rewrite rules flushing on Elementor editor save.
Widget Implementation
includes/Integrations/Elementor/User_Directory_Widget.php
Complete Elementor widget class with metadata, extensive control registration for content/search/filters/typography/colors/borders/pagination, editor preview rendering, and directory retrieval utilities.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • #1813: Implements similar Elementor integration pattern for Forms widget, modifying the same Elementor.php and Assets.php files for asset registration and widget orchestration.

Suggested labels

needs: dev review

Poem

🐰 A widget hops into Elementor's grand stage,
With directory charm on each builder's page,
Rewrite rules dance, styles brightly align,
User previews preview—what design so fine!
Whiskers twitch with joy at this integration divine! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR changes do not address the linked issues #1410 (login error), #1445 (admin-bar page editing), or #1446 (file uploads in page builders). The PR implements User Directory Elementor widget functionality but fails to fix the critical issues it claims to close. Verify the actual issues this PR should address or fix the reported problems.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: introducing an Elementor widget for the User Directory feature.
Out of Scope Changes check ✅ Passed All changes are within scope of adding Elementor widget support: CSS styling, asset registration, Elementor integration, widget class, and PostCSS config.
Docstring Coverage ✅ Passed Docstring coverage is 92.55% 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

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

@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: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟡 Minor comments (19)
src/js/user-directory/components/common/Toast.js-72-79 (1)

72-79: ⚠️ Potential issue | 🟡 Minor

Duplicate dismiss logic risks double onClose invocation.

The close button duplicates the auto-dismiss logic. If a user clicks close while the auto-dismiss timer is in flight, onClose may fire twice. Extract the dismiss logic into a shared handler and guard against duplicate calls.

🔧 Proposed fix reusing the shared handler
                     <button
-                        onClick={() => {
-                            setIsLeaving(true);
-                            setTimeout(() => {
-                                setIsVisible(false);
-                                onClose && onClose();
-                            }, 300);
-                        }}
+                        onClick={handleClose}
                         className="wpuf-ml-4 wpuf-flex-shrink-0 wpuf-inline-flex wpuf-bg-transparent wpuf-border-0 wpuf-p-0 wpuf-text-white hover:wpuf-text-gray-200 focus:wpuf-outline-none wpuf-cursor-pointer"
                     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/Toast.js` around lines 72 - 79,
Extract the dismiss sequence into a single handler (e.g., dismissToast) and
replace the duplicated inline logic in the close button and the auto-dismiss
timer with calls to that handler; inside dismissToast call setIsLeaving(true),
start the 300ms timeout to setIsVisible(false) and call onClose(), and guard
against double invocation with a local ref/flag (e.g., dismissedRef or
isDismissing) so subsequent calls are no-ops; update places that previously
called setIsLeaving/setIsVisible/onClose directly to call dismissToast instead
and ensure the flag is checked/set atomically to prevent duplicate onClose
calls.
src/js/user-directory/components/common/LayoutCard.js-73-81 (1)

73-81: ⚠️ Potential issue | 🟡 Minor

Avoid # as fallback for upgradeUrl.

When upgradeUrl is not provided, the link defaults to #, which scrolls to the page top—confusing for users. Consider hiding the link entirely or using a more meaningful fallback.

🛠️ Proposed fix to conditionally render the upgrade link
-                    <a
-                        href={upgradeUrl || '#'}
-                        target="_blank"
-                        rel="noopener noreferrer"
-                        className="wpuf-px-4 wpuf-py-2 wpuf-bg-indigo-600 wpuf-text-white wpuf-text-xs wpuf-font-medium wpuf-rounded-md hover:wpuf-bg-indigo-700 wpuf-no-underline wpuf-transition-colors"
-                        onClick={(e) => e.stopPropagation()}
-                    >
-                        {i18n?.upgrade_to_pro || __('Upgrade to Pro', 'wp-user-frontend')}
-                    </a>
+                    {upgradeUrl && (
+                        <a
+                            href={upgradeUrl}
+                            target="_blank"
+                            rel="noopener noreferrer"
+                            className="wpuf-px-4 wpuf-py-2 wpuf-bg-indigo-600 wpuf-text-white wpuf-text-xs wpuf-font-medium wpuf-rounded-md hover:wpuf-bg-indigo-700 wpuf-no-underline wpuf-transition-colors"
+                            onClick={(e) => e.stopPropagation()}
+                        >
+                            {i18n?.upgrade_to_pro || __('Upgrade to Pro', 'wp-user-frontend')}
+                        </a>
+                    )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/LayoutCard.js` around lines 73 - 81,
The anchor in LayoutCard currently falls back to href="#" when upgradeUrl is
falsy which causes unwanted scrolling; change the JSX to conditionally render
the upgrade link only when upgradeUrl is truthy (or explicitly hide it) instead
of using "#" as a fallback, keeping the same attributes (target, rel, className,
onClick) and i18n label; locate the anchor in the LayoutCard component and
replace the unconditional <a href={upgradeUrl || '#'} ...> with a conditional
render that only outputs the anchor when upgradeUrl is provided.
src/js/user-directory/utils/avatarSizeHelper.js-14-14 (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Misleading comment: layout-6 has the largest avatar size, not medium.

The comment says "Grid layout - medium avatars" but 265 is the largest size in the map. This could confuse future maintainers.

-        'layout-6': '265'   // Grid layout - medium avatars
+        'layout-6': '265'   // Grid layout - large avatars
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/utils/avatarSizeHelper.js` at line 14, The comment for
the 'layout-6' entry in avatarSizeHelper.js is misleading—'layout-6': '265' is
the largest avatar size, not "medium"; update the comment next to the 'layout-6'
mapping (in the avatar size map or function that returns sizes) to accurately
reflect that this is the largest grid avatar size (e.g., "Grid layout - largest
avatars") so future maintainers aren’t confused.
src/js/user-directory/components/common/Tooltip.js-3-5 (1)

3-5: ⚠️ Potential issue | 🟡 Minor

Unstable tooltip ID on each render and deprecated substr usage.

  1. tooltipId is regenerated on every render, which can cause accessibility issues with screen readers when the tooltip becomes visible (the ID changes between renders).
  2. String.prototype.substr() is deprecated; use substring() or slice() instead.
🔧 Proposed fix using useMemo for stable ID
-import { useState } from '@wordpress/element';
+import { useState, useMemo } from '@wordpress/element';

 const Tooltip = ( { content, children, className = '' } ) => {
     const [visible, setVisible] = useState(false);
-    const tooltipId = `wpuf-tooltip-${Math.random().toString(36).substr(2, 9)}`;
+    const tooltipId = useMemo(
+        () => `wpuf-tooltip-${Math.random().toString(36).slice(2, 11)}`,
+        []
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/Tooltip.js` around lines 3 - 5,
Tooltip currently generates tooltipId on every render and uses deprecated
String.prototype.substr; change tooltipId to a stable value created once per
component lifetime (e.g., useRef or useMemo) and replace substr(2, 9) with
slice(2, 11) or substring(2, 11) to avoid deprecated API; update the Tooltip
component to compute tooltipId via useRef/useMemo so it doesn't change across
renders and use slice/substring for the character extraction.
src/js/user-directory/components/common/DeleteConfirmModal.js-47-58 (1)

47-58: ⚠️ Potential issue | 🟡 Minor

Localize action button labels.

At Line 50 and Line 57, Cancel/Delete are hardcoded and skip translation.

Proposed fix
                     <button
                         onClick={onCancel}
                         className="wpuf-w-[101px] wpuf-h-[50px] wpuf-rounded-md wpuf-border wpuf-border-gray-300 wpuf-bg-white wpuf-text-gray-700 wpuf-font-medium hover:wpuf-bg-gray-50 wpuf-pt-[13px] wpuf-pb-[13px] wpuf-pl-[25px] wpuf-pr-[23px] wpuf-text-[16px] wpuf-leading-[24px]">
-                        Cancel
+                        {__( 'Cancel', 'wp-user-frontend' )}
                     </button>
@@
                     <button
                         onClick={onConfirm}
                         className="wpuf-w-[151px] wpuf-h-[50px] wpuf-rounded-md wpuf-bg-[`#EF4444`] wpuf-text-white wpuf-font-medium wpuf-shadow-sm hover:wpuf-bg-red-600 wpuf-pt-[13px] wpuf-pb-[13px] wpuf-pl-[25px] wpuf-pr-[25px] wpuf-text-[16px] wpuf-leading-[24px]"
                         style={{ boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)' }}>
-                        Delete
+                        {__( 'Delete', 'wp-user-frontend' )}
                     </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/DeleteConfirmModal.js` around lines
47 - 58, Replace the hardcoded button labels "Cancel" and "Delete" in the
DeleteConfirmModal component with localized strings; update the two buttons (the
one with onClick={onCancel} and the one with onClick={onConfirm}) to use your
app's i18n function (e.g., t('cancel') and t('delete')) or accept translated
label props, and ensure you import or obtain the translator (e.g.,
useTranslation or passed-in t) at the top of DeleteConfirmModal so both labels
are rendered via the translation function instead of literal text.
modules/user-directory/views/directory/template-parts/pagination-shortcode.php-41-43 (1)

41-43: ⚠️ Potential issue | 🟡 Minor

Clamp and default current_page before pagination math.

At Line 41, direct casting from $pagination['current_page'] can produce out-of-range values and invalid prev/next URLs. Normalize it to [1..$total] and default safely when missing.

Proposed fix
-$current = (int) $pagination['current_page'];
-$total   = (int) $pagination['total_pages'];
+$total   = max( 1, (int) $pagination['total_pages'] );
+$current = isset( $pagination['current_page'] ) ? (int) $pagination['current_page'] : 1;
+$current = max( 1, min( $current, $total ) );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@modules/user-directory/views/directory/template-parts/pagination-shortcode.php`
around lines 41 - 43, Normalize and clamp the current page before using it in
pagination math: ensure $total is set from $pagination['total_pages'] then set
$current from $pagination['current_page'] with a safe default (1) and clamp it
to the range 1..$total to avoid out-of-range prev/next URLs; update the logic
around the $current and $total variables in pagination-shortcode.php so all
downstream calculations use the clamped $current value.
src/js/user-directory/components/common/Header.js-17-17 (1)

17-17: ⚠️ Potential issue | 🟡 Minor

Build upgradeUrl with encoded query params.

At Line 17, manual concatenation with raw utm can produce malformed URLs if utm contains reserved characters.

Proposed fix
-    const upgradeUrl = (wpuf.upgradeUrl || 'https://wedevs.com/wp-user-frontend-pro/pricing/') + '?utm_source=' + utm + '&utm_medium=wpuf-header';
+    const upgradeBase = wpuf.upgradeUrl || 'https://wedevs.com/wp-user-frontend-pro/pricing/';
+    const upgrade = new URL( upgradeBase );
+    upgrade.searchParams.set( 'utm_source', utm );
+    upgrade.searchParams.set( 'utm_medium', 'wpuf-header' );
+    const upgradeUrl = upgrade.toString();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/Header.js` at line 17, The upgradeUrl
is constructed by string concatenation and can break when utm contains reserved
characters; update the build for upgradeUrl (the const upgradeUrl that uses
wpuf.upgradeUrl and utm) to assemble query parameters using URLSearchParams or
encodeURIComponent for utm and other params so values are percent-encoded, e.g.
derive base = wpuf.upgradeUrl ||
'https://wedevs.com/wp-user-frontend-pro/pricing/' then append a properly
encoded query string (utm_source and utm_medium) instead of concatenating raw
utm.
src/js/user-directory/components/common/SingleSelect.js-40-43 (1)

40-43: ⚠️ Potential issue | 🟡 Minor

Handle falsy-but-valid selected values correctly.

Line 40 treats values like 0 as unselected, so the placeholder can show even when a real option is selected.

Proposed fix
-        if (!value) return placeholder || __('Select...', 'wp-user-frontend');
+        if (value === undefined || value === null || value === '') {
+            return placeholder || __('Select...', 'wp-user-frontend');
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/common/SingleSelect.js` around lines 40 -
43, The current early-return treats any falsy value (e.g., 0) as unselected;
update the conditional in the label-rendering logic so it only returns the
placeholder when value is null or undefined (not when value === 0 or other valid
falsy values). In other words, change the check around the value variable used
before calling options.find (the block referencing options.find, placeholder and
__('Select...', 'wp-user-frontend')) to explicitly test value === null || value
=== undefined, then proceed to find the matching option and return option.label
or placeholder as before.
modules/user-directory/views/directory/template-parts/social-icons.php-33-33 (1)

33-33: ⚠️ Potential issue | 🟡 Minor

Harden external links opened in new tabs.

Lines [33], [42], [51], and [60] should include noreferrer alongside noopener for better tabnabbing/privacy hardening.

Proposed fix
-<a href="<?php echo esc_url( $facebook_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $facebook_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">
...
-<a href="<?php echo esc_url( $twitter_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $twitter_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">
...
-<a href="<?php echo esc_url( $linkedin_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $linkedin_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">
...
-<a href="<?php echo esc_url( $instagram_url ); ?>" target="_blank" rel="noopener" class="wpuf-social-icon">
+<a href="<?php echo esc_url( $instagram_url ); ?>" target="_blank" rel="noopener noreferrer" class="wpuf-social-icon">

Also applies to: 42-42, 51-51, 60-60

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

In `@modules/user-directory/views/directory/template-parts/social-icons.php` at
line 33, Update the external social link anchors (the <a> elements that use
esc_url($facebook_url), esc_url($twitter_url), esc_url($linkedin_url),
esc_url($instagram_url) and have class "wpuf-social-icon") to include
"noreferrer" in the rel attribute alongside "noopener" (i.e., change
rel="noopener" to rel="noopener noreferrer") for all four anchor tags so
external links opened in new tabs are hardened for tabnabbing/privacy.
includes/Integrations/Elementor/User_Directory_Widget.php-146-147 (1)

146-147: ⚠️ Potential issue | 🟡 Minor

Add rel attributes to links opened with target="_blank".

Lines [146-147] and [1339-1340] should include rel="noopener noreferrer" for tabnabbing/privacy hardening.

Proposed fix
-__( 'No user directory found. <a href="%s" target="_blank">Create one</a> first.', 'wp-user-frontend' ),
+__( 'No user directory found. <a href="%s" target="_blank" rel="noopener noreferrer">Create one</a> first.', 'wp-user-frontend' ),
...
-__( '<a href="%s" target="_blank">Create a user directory</a> in WP User Frontend settings.', 'wp-user-frontend' ),
+__( '<a href="%s" target="_blank" rel="noopener noreferrer">Create a user directory</a> in WP User Frontend settings.', 'wp-user-frontend' ),

Also applies to: 1339-1340

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

In `@includes/Integrations/Elementor/User_Directory_Widget.php` around lines 146 -
147, Update the two hardcoded anchor tags that open in a new tab to include
rel="noopener noreferrer": find the translation strings passed to __() in
includes/Integrations/Elementor/User_Directory_Widget.php (the strings
containing '<a href="%s" target="_blank">Create one</a>' and the other
occurrence later in the file) and add rel="noopener noreferrer" inside the <a>
tag so they become '<a href="%s" target="_blank" rel="noopener noreferrer">...'.
Ensure both occurrences are updated so the anchor markup returned by the __()
calls includes the rel attribute.
modules/user-directory/views/profile/template-parts/user-avatar.php-18-24 (1)

18-24: ⚠️ Potential issue | 🟡 Minor

Harden $user and $size inputs before rendering.

Lines [18-24] should verify WP_User type and clamp size to a positive integer to prevent notices and invalid dimensions.

Proposed fix
-if ( ! $user ) {
+if ( empty( $user ) || ! ( $user instanceof WP_User ) ) {
     return;
 }
 
 // Get avatar size from parameter or use default
-$size = isset( $size ) ? intval( $size ) : 128;
+$size = isset( $size ) ? max( 1, (int) $size ) : 128;
 $wrapper_class = isset( $wrapper_class ) ? $wrapper_class : '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/profile/template-parts/user-avatar.php` around
lines 18 - 24, Ensure the template hardens $user and $size before rendering:
verify $user is a WP_User (use instanceof WP_User or resolve it with get_user_by
when $user may be an ID) and return early if it cannot be resolved; coerce $size
to an int and clamp it to a positive value (e.g., size = max(1, intval($size)))
to avoid zero/negative dimensions; also ensure $wrapper_class has a safe default
(e.g., empty string) and consider sanitizing it before output; update the checks
in user-avatar.php around the $user/$size/$wrapper_class handling to implement
these validations.
modules/user-directory/views/directory/template-parts/sort-field.php-18-19 (1)

18-19: ⚠️ Potential issue | 🟡 Minor

Initialize $all_data before nested key access.

Lines [18-19] assume $all_data exists and is an array. Add a local fallback to avoid notices in edge include paths.

Proposed fix
+ $all_data = ( isset( $all_data ) && is_array( $all_data ) ) ? $all_data : [];
  $orderby = ! empty( $all_data['orderby'] ) ? $all_data['orderby'] : 'id';
  $order = ! empty( $all_data['order'] ) ? $all_data['order'] : 'desc';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/directory/template-parts/sort-field.php` around
lines 18 - 19, Ensure $all_data is defined as an array before accessing its
keys: add a local fallback that checks if $all_data exists and is an array
(otherwise set it to an empty array) immediately before the lines that compute
$orderby and $order so the expressions using $all_data['orderby'] and
$all_data['order'] cannot trigger notices; keep the rest of the logic that sets
$orderby and $order unchanged.
assets/js/wpuf-user-directory-frontend.js-47-64 (1)

47-64: ⚠️ Potential issue | 🟡 Minor

removeUrlParam() drops hash fragments and can rewrite URLs incorrectly.

Lines [47-64] should use URL parsing instead of string split logic so #fragment and edge-case query strings are preserved.

Proposed fix
 function removeUrlParam(url, param) {
-    var urlParts = url.split('?');
-    
-    if (urlParts.length < 2) {
-        return url;
-    }
-
-    var params = new URLSearchParams(urlParts[1]);
-    params.delete(param);
-    
-    var newParams = params.toString();
-    
-    if (newParams) {
-        return urlParts[0] + '?' + newParams;
-    }
-    
-    return urlParts[0];
+    var parsedUrl = new URL(url, window.location.origin);
+    parsedUrl.searchParams.delete(param);
+    return parsedUrl.toString();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/wpuf-user-directory-frontend.js` around lines 47 - 64, The
removeUrlParam function currently splits on '?' which drops hash fragments and
mis-handles edge-case query strings; replace the manual split with the URL API:
parse the input with new URL(url, window.location.href) to support absolute and
relative URLs, use urlObj.searchParams.delete(param) to remove the parameter,
and return urlObj.href (or urlObj.toString()) so the original pathname,
preserved query ordering, and hash fragment remain intact; if URL construction
throws, fall back to returning the original url unchanged.
src/js/user-directory/components/DirectoryWizard.js-131-134 (1)

131-134: ⚠️ Potential issue | 🟡 Minor

Potential issue with JSON parsing on error responses.

If the server returns a non-JSON error response (e.g., HTML error page), response.json() will throw, and this exception won't be caught gracefully since it's inside the try block but before the main error handling.

🛠️ Suggested improvement
             if (!response.ok) {
-                const data = await response.json();
-                throw new Error(data.message || __('Something went wrong', 'wp-user-frontend'));
+                let errorMessage = __('Something went wrong', 'wp-user-frontend');
+                try {
+                    const data = await response.json();
+                    errorMessage = data.message || errorMessage;
+                } catch (parseError) {
+                    // Response wasn't JSON, use default message
+                }
+                throw new Error(errorMessage);
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/DirectoryWizard.js` around lines 131 - 134,
The error handling path currently assumes response.json() will succeed; update
the non-ok branch in DirectoryWizard.js so you attempt to parse JSON inside its
own try/catch and fall back to await response.text() (or a generic message) when
JSON parsing fails, then throw a new Error that uses the parsed message/text or
a fallback __('Something went wrong', 'wp-user-frontend'); locate the existing
if (!response.ok) block and replace the single response.json() call with this
safe-parse-and-fallback logic to avoid unhandled exceptions when the server
returns non-JSON error bodies.
src/js/user-directory/components/steps/StepBasics.js-67-81 (1)

67-81: ⚠️ Potential issue | 🟡 Minor

Missing cleanup for debounce timeout on unmount.

The searchTimeoutRef timeout is not cleared when the component unmounts, which could cause a memory leak or state update on an unmounted component.

🛠️ Suggested fix
     // Close dropdown when clicking outside
     useEffect(() => {
         const handleClickOutside = (event) => {
             if (userDropdownRef.current && !userDropdownRef.current.contains(event.target)) {
                 setShowUserDropdown(false);
             }
         };

         document.addEventListener('mousedown', handleClickOutside);
         return () => {
             document.removeEventListener('mousedown', handleClickOutside);
+            // Clear any pending search timeout
+            if (searchTimeoutRef.current) {
+                clearTimeout(searchTimeoutRef.current);
+            }
         };
     }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/js/user-directory/components/steps/StepBasics.js` around lines 67 - 81,
The debounced timeout stored in searchTimeoutRef used by handleSearchChange is
never cleared on unmount; add a useEffect in the StepBasics component that on
mount returns a cleanup function which checks searchTimeoutRef.current and calls
clearTimeout(searchTimeoutRef.current) (and optionally sets it to null) to avoid
leaks or setState-after-unmount when searchUsers runs; keep existing
handleSearchChange, searchUsers and setSearchTerm logic unchanged.
assets/js/ud-search-shortcode.js-52-63 (1)

52-63: ⚠️ Potential issue | 🟡 Minor

Missing error handling for non-JSON responses and no request timeout.

The fetch call doesn't handle cases where the server returns non-JSON content (e.g., HTML error page) and has no timeout, which could leave the UI in a loading state indefinitely.

🛠️ Suggested improvement
-        fetch(apiUrl + '?' + params.toString(), {
+        const controller = new AbortController();
+        const timeoutId = setTimeout(() => controller.abort(), 30000);
+        
+        fetch(apiUrl + '?' + params.toString(), {
             credentials: 'same-origin',
+            signal: controller.signal,
         })
-            .then(res => res.json())
+            .then(res => {
+                clearTimeout(timeoutId);
+                if (!res.ok) {
+                    throw new Error('Network response was not ok');
+                }
+                const contentType = res.headers.get('content-type');
+                if (!contentType || !contentType.includes('application/json')) {
+                    throw new Error('Response was not JSON');
+                }
+                return res.json();
+            })
             .then(data => {
                 if (data && data.success) {
                     onSuccess(data);
                 } else {
                     onError(data);
                 }
             })
-            .catch(onError);
+            .catch(err => {
+                clearTimeout(timeoutId);
+                onError(err);
+            });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/ud-search-shortcode.js` around lines 52 - 63, The fetch call using
apiUrl + '?' + params.toString() should handle non-JSON responses and enforce a
timeout: wrap the fetch in an AbortController with a setTimeout to abort after a
chosen timeout, pass controller.signal to fetch, and clear the timer on
completion; after receiving the Response check response.ok and attempt to parse
JSON inside a try/catch (if parsing fails or response.ok is false, create a
descriptive error object and call onError), otherwise call onSuccess with the
parsed data; ensure the existing .catch still calls onError for network/abort
errors.
modules/user-directory/views/profile/layout-2.php-95-95 (1)

95-95: ⚠️ Potential issue | 🟡 Minor

Harden new-tab links with rel="noopener noreferrer".

Both anchors open new tabs and should include rel to prevent opener access.

Proposed fix
-                                <a href="<?php echo esc_url( $contact_item['value'] ); ?>" target="_blank" class="!wpuf-text-sm !wpuf-text-gray-900 !wpuf-font-medium hover:!wpuf-text-emerald-600">
+                                <a href="<?php echo esc_url( $contact_item['value'] ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-text-sm !wpuf-text-gray-900 !wpuf-font-medium hover:!wpuf-text-emerald-600">
@@
-                    <a href="<?php echo esc_url( $private_message_link ); ?>" target="_blank" class="!wpuf-h-11 !wpuf-w-11 !wpuf-bg-emerald-600 !wpuf-text-white !wpuf-rounded-lg hover:!wpuf-bg-emerald-700 !wpuf-transition-colors !wpuf-flex !wpuf-items-center !wpuf-justify-center !wpuf-no-underline">
+                    <a href="<?php echo esc_url( $private_message_link ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-h-11 !wpuf-w-11 !wpuf-bg-emerald-600 !wpuf-text-white !wpuf-rounded-lg hover:!wpuf-bg-emerald-700 !wpuf-transition-colors !wpuf-flex !wpuf-items-center !wpuf-justify-center !wpuf-no-underline">

Also applies to: 122-122

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

In `@modules/user-directory/views/profile/layout-2.php` at line 95, The anchor
that renders contact links (the <a> with href="<?php echo esc_url(
$contact_item['value'] ); ?>" and target="_blank") must be hardened by adding
rel="noopener noreferrer"; update that anchor (and the other similar anchor at
the other occurrence) to include rel="noopener noreferrer" whenever
target="_blank" is present so the rendered link prevents opener access.
modules/user-directory/views/profile/template-parts/file-2.php-219-219 (1)

219-219: ⚠️ Potential issue | 🟡 Minor

Add rel for new-tab file links.

This link opens in a new tab and should include rel="noopener noreferrer" for tabnabbing protection.

Proposed fix
-                            <a href="<?php echo esc_url( $file_url ); ?>" target="_blank" class="!wpuf-block">
+                            <a href="<?php echo esc_url( $file_url ); ?>" target="_blank" rel="noopener noreferrer" class="!wpuf-block">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/views/profile/template-parts/file-2.php` at line 219,
The anchor that opens $file_url in a new tab (the <a ... target="_blank"
class="!wpuf-block"> element) must include rel="noopener noreferrer" to prevent
tabnabbing; update that anchor in file-2.php to add rel="noopener noreferrer"
alongside the existing attributes.
modules/user-directory/Helpers.php-618-623 (1)

618-623: ⚠️ Potential issue | 🟡 Minor

Normalize file/files tab key mapping.

Label map uses files while other defaults/configs use file, causing inconsistent tab labels and lookups.

Proposed fix
     $labels = [
         'about'    => __( 'About', 'wp-user-frontend' ),
         'posts'    => __( 'Posts', 'wp-user-frontend' ),
         'comments' => __( 'Comments', 'wp-user-frontend' ),
+        'file'     => __( 'Files', 'wp-user-frontend' ),
         'files'    => __( 'Files', 'wp-user-frontend' ),
         'activity' => __( 'Activity', 'wp-user-frontend' ),
     ];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/user-directory/Helpers.php` around lines 618 - 623, The labels map in
Helpers.php uses the key 'files' which is inconsistent with the rest of the
codebase that expects 'file'; update the $labels array to use 'file' instead of
'files' (in the associative array defined around $labels) so tab label lookups
and defaults align, and scan related usages for $labels, get_tab_label or
similar helpers to ensure they reference 'file' consistently.

ℹ️ Review info
Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 322b89b7-09c7-438d-903b-ded508f62716

📥 Commits

Reviewing files that changed from the base of the PR and between 5f313fa and fe97092.

⛔ Files ignored due to path filters (22)
  • assets/css/wpuf-user-directory-free.css.map is excluded by !**/*.map
  • assets/images/user-directory/confetti.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-1.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-2.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-3.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-4.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-5.png is excluded by !**/*.png
  • assets/images/user-directory/directory-layout-6.png is excluded by !**/*.png
  • assets/images/user-directory/profile-layout-1.png is excluded by !**/*.png
  • assets/images/user-directory/profile-layout-2.png is excluded by !**/*.png
  • assets/images/user-directory/profile-layout-3.png is excluded by !**/*.png
  • assets/images/user-directory/round-grids.png is excluded by !**/*.png
  • assets/images/user-directory/sidecards.png is excluded by !**/*.png
  • assets/images/user-directory/square-grids.png is excluded by !**/*.png
  • assets/images/user-directory/table.png is excluded by !**/*.png
  • assets/images/user-directory/thumb-male-1.svg is excluded by !**/*.svg
  • assets/images/user-directory/thumb-male-2.svg is excluded by !**/*.svg
  • assets/images/user-directory/thumb-male-3.svg is excluded by !**/*.svg
  • assets/images/user-directory/wide-sidecards.png is excluded by !**/*.png
  • assets/js/wpuf-user-directory-free.js.map is excluded by !**/*.map
  • modules/user-directory/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (68)
  • Gruntfile.js
  • assets/css/admin/subscriptions.min.css
  • assets/css/admin/wpuf-module.css
  • assets/css/ai-form-builder.min.css
  • assets/css/elementor-user-directory.css
  • assets/css/forms-list.min.css
  • assets/css/frontend-subscriptions.min.css
  • assets/css/wpuf-user-directory-free.css
  • assets/css/wpuf-user-directory-frontend.css
  • assets/js/admin/wpuf-module.js
  • assets/js/ud-search-shortcode.js
  • assets/js/wpuf-user-directory-free.asset.php
  • assets/js/wpuf-user-directory-free.js
  • assets/js/wpuf-user-directory-free.js.LICENSE.txt
  • assets/js/wpuf-user-directory-frontend.js
  • includes/Assets.php
  • includes/Free/Free_Loader.php
  • includes/Integrations.php
  • includes/Integrations/Elementor/Elementor.php
  • includes/Integrations/Elementor/User_Directory_Widget.php
  • includes/functions/modules.php
  • modules/user-directory/Admin_Menu.php
  • modules/user-directory/Api/Directory.php
  • modules/user-directory/DirectoryStyles.php
  • modules/user-directory/Helpers.php
  • modules/user-directory/Post_Type.php
  • modules/user-directory/PrettyUrls.php
  • modules/user-directory/Shortcode.php
  • modules/user-directory/User_Directory.php
  • modules/user-directory/package.json
  • modules/user-directory/postcss.config.js
  • modules/user-directory/tailwind.config.js
  • modules/user-directory/views/admin-page.php
  • modules/user-directory/views/directory/layout-3.php
  • modules/user-directory/views/directory/template-parts/pagination-shortcode.php
  • modules/user-directory/views/directory/template-parts/row-3.php
  • modules/user-directory/views/directory/template-parts/search-field.php
  • modules/user-directory/views/directory/template-parts/social-icons.php
  • modules/user-directory/views/directory/template-parts/sort-field.php
  • modules/user-directory/views/profile/layout-2.php
  • modules/user-directory/views/profile/template-parts/about-2.php
  • modules/user-directory/views/profile/template-parts/comments-2.php
  • modules/user-directory/views/profile/template-parts/file-2.php
  • modules/user-directory/views/profile/template-parts/posts-2.php
  • modules/user-directory/views/profile/template-parts/user-avatar.php
  • modules/user-directory/webpack.config.js
  • package.json
  • postcss.user-directory.config.js
  • src/js/user-directory/App.js
  • src/js/user-directory/components/DirectoryList.js
  • src/js/user-directory/components/DirectoryWizard.js
  • src/js/user-directory/components/common/DeleteConfirmModal.js
  • src/js/user-directory/components/common/Header.js
  • src/js/user-directory/components/common/LayoutCard.js
  • src/js/user-directory/components/common/MultiSelect.js
  • src/js/user-directory/components/common/SingleSelect.js
  • src/js/user-directory/components/common/Toast.js
  • src/js/user-directory/components/common/Tooltip.js
  • src/js/user-directory/components/steps/StepAdvanced.js
  • src/js/user-directory/components/steps/StepBasics.js
  • src/js/user-directory/components/steps/StepLayout.js
  • src/js/user-directory/components/steps/StepProfile.js
  • src/js/user-directory/components/steps/StepTabs.js
  • src/js/user-directory/index.js
  • src/js/user-directory/styles/main.css
  • src/js/user-directory/utils/avatarSizeHelper.js
  • tailwind.config.js
  • wpuf-functions.php

Comment on lines +13 to +14
const SEARCH_DEBOUNCE = 300;
let debounceTimeout = null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Shared debounce state causes race conditions with multiple directories.

The debounceTimeout variable is declared at the module scope and shared across all directory instances on a page. If a user types in one directory's search and then quickly types in another, the debounce will be cancelled for the first directory.

Move the debounce timeout into initUserDirectorySearch to scope it per instance:

🐛 Proposed fix
 (function(window, document) {
     'use strict';

     const SEARCH_DEBOUNCE = 300;
-    let debounceTimeout = null;

     function fetchUsers({
         // ... unchanged
     }) {
         // ... unchanged
     }

     function initUserDirectorySearch(container, blockId, pageId) {
         let currentRequestId = 0;
+        let debounceTimeout = null;

         // ... rest of function

         // Search input handler
         input.addEventListener('input', function(e) {
             const value = e.target.value.trim();
             if (debounceTimeout) clearTimeout(debounceTimeout);
             debounceTimeout = setTimeout(() => {
                 performSearch(value, 1);
             }, SEARCH_DEBOUNCE);
         });

         // ... rest of function
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/ud-search-shortcode.js` around lines 13 - 14, The module-scoped
debounceTimeout (declared alongside SEARCH_DEBOUNCE) is shared across all
directory instances causing cross-instance cancellation; move the
debounceTimeout variable into the initUserDirectorySearch function so each call
gets its own timeout state, update uses inside initUserDirectorySearch (and any
inner handlers like the input event listener) to reference the instance-scoped
debounceTimeout, and remove/replace the module-level debounceTimeout declaration
to avoid the race condition.

Comment thread modules/user-directory/Api/Directory.php
Comment thread modules/user-directory/Shortcode.php
Comment on lines +201 to +205
$file_url = wp_get_attachment_url( $file->ID );
$file_type = get_post_mime_type( $file->ID );
$file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
$is_image = strpos( $file_type, 'image/' ) === 0;
$is_pdf = $file_type === 'application/pdf';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard against missing attachment URLs before calling pathinfo().

If wp_get_attachment_url() returns false, pathinfo() will throw and break profile rendering.

Proposed fix
-                        $file_url = wp_get_attachment_url( $file->ID );
-                        $file_type = get_post_mime_type( $file->ID );
-                        $file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
+                        $file_url = wp_get_attachment_url( $file->ID );
+                        if ( ! $file_url ) {
+                            continue;
+                        }
+                        $file_type = (string) get_post_mime_type( $file->ID );
+                        $file_extension = strtoupper( pathinfo( $file_url, PATHINFO_EXTENSION ) );
@@
-                                        <?php echo esc_html( $file->post_title ?: pathinfo( $file_url, PATHINFO_FILENAME ) ); ?>
+                                        <?php echo esc_html( $file->post_title ?: pathinfo( $file_url, PATHINFO_FILENAME ) ); ?>

Also applies to: 245-246

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

In `@modules/user-directory/views/profile/template-parts/file-2.php` around lines
201 - 205, Guard against wp_get_attachment_url() returning false before calling
pathinfo by checking the $file_url truthiness; in the block that sets $file_url,
$file_type, $file_extension, $is_image, $is_pdf (and the similar block at the
other occurrence) only call pathinfo() when $file_url is non-false, otherwise
set $file_extension to an empty string (or null) and adjust $is_image/$is_pdf
using $file_type alone or default false; update the assignments around the
wp_get_attachment_url($file->ID) call and the $file_extension =
strtoupper(pathinfo(...)) usage to perform this guard so profile rendering won’t
break when the attachment URL is missing.

Comment thread src/js/user-directory/components/common/MultiSelect.js Outdated
…ectory_elementor_widget

# Conflicts:
#	assets/css/admin/subscriptions.min.css
#	assets/css/admin/wpuf-module.css
#	assets/css/ai-form-builder.min.css
#	assets/css/forms-list.min.css
#	assets/css/frontend-subscriptions.min.css
#	assets/js/ud-search-shortcode.js
#	assets/js/wpuf-user-directory-free.asset.php
#	assets/js/wpuf-user-directory-free.js
#	assets/js/wpuf-user-directory-free.js.map
#	assets/js/wpuf-user-directory-frontend.js
#	includes/Free/Free_Loader.php
#	includes/functions/modules.php
#	modules/user-directory/Api/Directory.php
#	modules/user-directory/DirectoryStyles.php
#	modules/user-directory/Helpers.php
#	modules/user-directory/Post_Type.php
#	modules/user-directory/PrettyUrls.php
#	modules/user-directory/Shortcode.php
#	modules/user-directory/User_Directory.php
#	modules/user-directory/tailwind.config.js
#	modules/user-directory/views/directory/layout-3.php
#	modules/user-directory/views/directory/template-parts/pagination-shortcode.php
#	modules/user-directory/views/directory/template-parts/row-3.php
#	modules/user-directory/views/directory/template-parts/search-field.php
#	modules/user-directory/views/directory/template-parts/social-icons.php
#	modules/user-directory/views/profile/layout-2.php
#	modules/user-directory/views/profile/template-parts/comments-2.php
#	modules/user-directory/views/profile/template-parts/file-2.php
#	modules/user-directory/views/profile/template-parts/posts-2.php
#	modules/user-directory/webpack.config.js
#	postcss.user-directory.config.js
#	src/js/user-directory/App.js
#	src/js/user-directory/components/DirectoryList.js
#	src/js/user-directory/components/DirectoryWizard.js
#	src/js/user-directory/components/common/DeleteConfirmModal.js
#	src/js/user-directory/components/common/MultiSelect.js
#	src/js/user-directory/components/common/SingleSelect.js
#	src/js/user-directory/components/common/Tooltip.js
#	src/js/user-directory/components/steps/StepAdvanced.js
#	src/js/user-directory/components/steps/StepTabs.js
#	src/js/user-directory/utils/avatarSizeHelper.js
@Rubaiyat-E-Mohammad
Copy link
Copy Markdown
Contributor

Issues found so far: @sapayth vai

  • Search Bar and filter controls placeholders are being cropped
image
  • No editing is working for user cards and everything after it

Copy link
Copy Markdown

@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: 4

🧹 Nitpick comments (1)
includes/Integrations/Elementor/Elementor.php (1)

78-97: Gate User Directory assets behind widget presence detection.

enqueue_styles() and enqueue_scripts() fire on elementor/frontend/after_enqueue_* hooks for every Elementor page, unconditionally loading wpuf-user-directory-frontend, wpuf-elementor-user-directory, and Pro's UD styles/scripts on unrelated pages. This duplicates work already handled by the User_Directory_Widget's get_style_depends() and get_script_depends() methods.

Additionally, wp_localize_script('wpuf-ud-search-shortcode', 'wpufUserDirectorySearch', ...) adds a global JS object on every Elementor page regardless of whether the widget is used.

Refactor to check if the current page's _elementor_data contains "widgetType":"wpuf-user-directory" before enqueuing these assets, using the same pattern already established elsewhere in the class (e.g., maybe_flush_rules_on_elementor_save(), register_elementor_page_rewrite_rules()).

Also applies to: lines 122-152

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

In `@includes/Integrations/Elementor/Elementor.php` around lines 78 - 97, The
Elementor enqueues are firing on every Elementor page; update the
enqueue_styles() and enqueue_scripts() logic to first detect whether the current
post's _elementor_data contains the widgetType "wpuf-user-directory" (use the
same pattern as maybe_flush_rules_on_elementor_save() /
register_elementor_page_rewrite_rules() for reading/parsing _elementor_data),
and only then call wp_register_style/wp_enqueue_style/wp_enqueue_script and
wp_localize_script('wpuf-ud-search-shortcode', ...). Apply the same gated check
to the corresponding assets around lines 122-152 so Pro UD styles/scripts and
the global wpufUserDirectorySearch object are only added when the Elementor page
actually contains the wpuf-user-directory widget.
🤖 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/Integrations/Elementor/Elementor.php`:
- Around line 388-399: The get_posts multiline call assigned to $pages violates
PHPCS multiline-call rules: move the opening parenthesis of get_posts to end the
line (i.e., put get_posts( on its own line) and ensure the closing parenthesis
is on its own line after the array; reformat the array argument so each
key/value pair stays on its own line. Apply the identical formatting change to
the fallback get_posts call used later (the other get_posts([...]) block) so
both calls follow the same multiline-call pattern.
- Around line 38-42: The register_elementor_page_rewrite_rules() function
currently runs heavy DB work on every init; change it to read cached slugs from
a transient (e.g. 'wpuf_ud_elementor_page_slugs') and only run the expensive
queries on cache miss, using WP_Query with 'fields' => 'ids' and a sensible
pagination cap instead of posts_per_page => -1; move the one-time backfill scan
so it only runs under is_admin() (or during plugin activation/CLI) rather than
on every init; update maybe_flush_rules_on_elementor_save() to delete the
transient to invalidate cache and ensure flush_rewrite_rules() is only called
from admin context and only when slugs actually changed.
- Around line 353-372: In maybe_flush_rules_on_elementor_save (hooked to
elementor/editor/after_save) stop re-reading _elementor_data from DB and instead
parse the provided $data payload to detect '"widgetType":"wpuf-user-directory"';
read the existing flag via get_post_meta($post_id,
'_wpuf_has_ud_elementor_widget', true) and only call update_post_meta or
delete_post_meta when the flag actually needs to change, and call
flush_rewrite_rules() only when the flag transitions (0→1 or 1→0) so rewrites
are flushed on real state changes.
- Around line 425-432: The rewrite rule currently uses only $page->post_name and
doesn't handle hierarchical pages or regex-special characters; replace that with
the page's full URI via get_page_uri($page->ID), escape it with preg_quote(...,
'/') when building the regex, and use that escaped full path for both the
pattern and the pagename query var (so pagename gets the full hierarchical path
returned by get_page_uri); update the loop that builds rules (the code around
add_rewrite_rule in Elementor.php) to use get_page_uri($page->ID) and preg_quote
on the path before constructing the '^.../([^/]+)/?$' pattern and the
'index.php?pagename=...&wpuf_user_profile=$matches[1]' replacement.

---

Nitpick comments:
In `@includes/Integrations/Elementor/Elementor.php`:
- Around line 78-97: The Elementor enqueues are firing on every Elementor page;
update the enqueue_styles() and enqueue_scripts() logic to first detect whether
the current post's _elementor_data contains the widgetType "wpuf-user-directory"
(use the same pattern as maybe_flush_rules_on_elementor_save() /
register_elementor_page_rewrite_rules() for reading/parsing _elementor_data),
and only then call wp_register_style/wp_enqueue_style/wp_enqueue_script and
wp_localize_script('wpuf-ud-search-shortcode', ...). Apply the same gated check
to the corresponding assets around lines 122-152 so Pro UD styles/scripts and
the global wpufUserDirectorySearch object are only added when the Elementor page
actually contains the wpuf-user-directory widget.
🪄 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: e4a7e269-1e68-4ab0-a9fa-c0ad81b40f38

📥 Commits

Reviewing files that changed from the base of the PR and between fe97092 and e3dbeae.

📒 Files selected for processing (2)
  • includes/Assets.php
  • includes/Integrations/Elementor/Elementor.php
✅ Files skipped from review due to trivial changes (1)
  • includes/Assets.php

Comment on lines +38 to +42
// of which module is active, ensuring the rules are always registered.
add_action( 'init', [ $this, 'register_elementor_page_rewrite_rules' ], 5 );

// Flush rewrite rules when Elementor saves a page with our widget
add_action( 'elementor/editor/after_save', [ $this, 'maybe_flush_rules_on_elementor_save' ], 10, 2 );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical performance issue: heavy DB work on every init.

register_elementor_page_rewrite_rules() is hooked to init (line 39), so it runs on every request — frontend, admin, AJAX, REST, cron.

  • The fast path (lines 388–399) runs a get_posts with meta_query and posts_per_page => -1 per request.
  • The fallback (lines 404–423) is far worse: when no flagged pages exist (the common case for installs that don't use the widget), it scans all published pages and issues an N+1 get_post_meta per page on every init — forever, since nothing will ever back-fill the flag in that scenario.
  • flush_rewrite_rules() at line 421 can fire mid-request from any context.

Recommended approach:

  1. Cache the slugs in a transient/option keyed by something cheap to invalidate (the maybe_flush_rules_on_elementor_save() hook is the natural invalidator). Read from cache on init; only run the query on cache miss.
  2. Only run the one-time back-fill scan in admin context (e.g. behind is_admin()) or trigger it explicitly (activation hook / admin notice / WP-CLI), so it never runs on a frontend page load.
  3. Drop posts_per_page => -1 in favor of a sensible cap (fields => 'ids' also avoids hydrating full post objects).
♻️ Sketch of a transient-backed approach
 public function register_elementor_page_rewrite_rules() {
-    // Fast path: query only pages that have been flagged via the meta key.
-    // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
-    $pages = get_posts( [
-        'post_type'      => 'page',
-        'post_status'    => 'publish',
-        'posts_per_page' => -1,
-        'meta_query'     => [
-            [
-                'key'     => '_wpuf_has_ud_elementor_widget',
-                'value'   => '1',
-                'compare' => '=',
-            ],
-        ],
-    ] );
-
-    // Fallback: ... (scan all pages on every init)
+    $slugs = get_transient( 'wpuf_ud_elementor_page_slugs' );
+    if ( false === $slugs ) {
+        // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+        $page_ids = get_posts( [
+            'post_type'      => 'page',
+            'post_status'    => 'publish',
+            'posts_per_page' => 200,
+            'fields'         => 'ids',
+            'meta_query'     => [
+                [
+                    'key'   => '_wpuf_has_ud_elementor_widget',
+                    'value' => '1',
+                ],
+            ],
+        ] );
+        $slugs = array_filter( array_map( static function ( $id ) {
+            $p = get_post( $id );
+            return $p ? $p->post_name : null;
+        }, $page_ids ) );
+        set_transient( 'wpuf_ud_elementor_page_slugs', $slugs, DAY_IN_SECONDS );
+    }
+
+    foreach ( $slugs as $slug ) {
+        add_rewrite_rule(
+            '^' . preg_quote( $slug, '/' ) . '/([^/]+)/?$',
+            'index.php?pagename=' . $slug . '&wpuf_user_profile=$matches[1]',
+            'top'
+        );
+    }
 }

And invalidate via delete_transient( 'wpuf_ud_elementor_page_slugs' ) inside maybe_flush_rules_on_elementor_save().

Also applies to: 385-433

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

In `@includes/Integrations/Elementor/Elementor.php` around lines 38 - 42, The
register_elementor_page_rewrite_rules() function currently runs heavy DB work on
every init; change it to read cached slugs from a transient (e.g.
'wpuf_ud_elementor_page_slugs') and only run the expensive queries on cache
miss, using WP_Query with 'fields' => 'ids' and a sensible pagination cap
instead of posts_per_page => -1; move the one-time backfill scan so it only runs
under is_admin() (or during plugin activation/CLI) rather than on every init;
update maybe_flush_rules_on_elementor_save() to delete the transient to
invalidate cache and ensure flush_rewrite_rules() is only called from admin
context and only when slugs actually changed.

Comment on lines +353 to +372
public function maybe_flush_rules_on_elementor_save( $post_id, $data ) {
$post = get_post( $post_id );

if ( ! $post || 'page' !== $post->post_type ) {
return;
}

$elementor_data = get_post_meta( $post_id, '_elementor_data', true );

$has_widget = ! empty( $elementor_data ) && is_string( $elementor_data ) && strpos( $elementor_data, '"widgetType":"wpuf-user-directory"' ) !== false;

// Keep a queryable meta flag so register_elementor_page_rewrite_rules() can find
// these pages efficiently without scanning all _elementor_data values on every init.
if ( $has_widget ) {
update_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', '1' );
flush_rewrite_rules();
} else {
delete_post_meta( $post_id, '_wpuf_has_ud_elementor_widget' );
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use the $data parameter and only flush on actual state change.

Two issues here:

  1. The $data argument from elementor/editor/after_save already contains the editor payload — there's no need to re-fetch _elementor_data from the DB. Using it also resolves the UnusedFormalParameter warning from PHPMD.
  2. flush_rewrite_rules() runs on every save where the widget is present, even when the meta flag was already '1' (no state change). Symmetrically, removing the widget (delete_post_meta) does not flush, so stale rules can remain registered until the next save that does have the widget. Flush should be conditional on the flag actually transitioning.
♻️ Proposed refactor
-public function maybe_flush_rules_on_elementor_save( $post_id, $data ) {
-    $post = get_post( $post_id );
-
-    if ( ! $post || 'page' !== $post->post_type ) {
-        return;
-    }
-
-    $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
-
-    $has_widget = ! empty( $elementor_data ) && is_string( $elementor_data ) && strpos( $elementor_data, '"widgetType":"wpuf-user-directory"' ) !== false;
-
-    // Keep a queryable meta flag so register_elementor_page_rewrite_rules() can find
-    // these pages efficiently without scanning all _elementor_data values on every init.
-    if ( $has_widget ) {
-        update_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', '1' );
-        flush_rewrite_rules();
-    } else {
-        delete_post_meta( $post_id, '_wpuf_has_ud_elementor_widget' );
-    }
-}
+public function maybe_flush_rules_on_elementor_save( $post_id, $data ) {
+    $post = get_post( $post_id );
+    if ( ! $post || 'page' !== $post->post_type ) {
+        return;
+    }
+
+    $has_widget = $this->elementor_data_has_ud_widget( $data );
+    $had_widget = (bool) get_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', true );
+
+    if ( $has_widget === $had_widget ) {
+        return; // nothing changed, no need to flush
+    }
+
+    if ( $has_widget ) {
+        update_post_meta( $post_id, '_wpuf_has_ud_elementor_widget', '1' );
+    } else {
+        delete_post_meta( $post_id, '_wpuf_has_ud_elementor_widget' );
+    }
+
+    // Invalidate any cache used by register_elementor_page_rewrite_rules(), then flush.
+    flush_rewrite_rules();
+}
+
+private function elementor_data_has_ud_widget( $data ) {
+    if ( is_array( $data ) ) {
+        $json = wp_json_encode( $data );
+    } elseif ( is_string( $data ) ) {
+        $json = $data;
+    } else {
+        return false;
+    }
+    return is_string( $json ) && strpos( $json, '"widgetType":"wpuf-user-directory"' ) !== false;
+}
🧰 Tools
🪛 GitHub Check: Run PHPCS inspection

[warning] 353-353:
The method parameter $data is never used

🪛 PHPMD (2.15.0)

[warning] 353-353: Avoid unused parameters such as '$data'. (undefined)

(UnusedFormalParameter)

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

In `@includes/Integrations/Elementor/Elementor.php` around lines 353 - 372, In
maybe_flush_rules_on_elementor_save (hooked to elementor/editor/after_save) stop
re-reading _elementor_data from DB and instead parse the provided $data payload
to detect '"widgetType":"wpuf-user-directory"'; read the existing flag via
get_post_meta($post_id, '_wpuf_has_ud_elementor_widget', true) and only call
update_post_meta or delete_post_meta when the flag actually needs to change, and
call flush_rewrite_rules() only when the flag transitions (0→1 or 1→0) so
rewrites are flushed on real state changes.

Comment on lines +388 to +399
$pages = get_posts( [
'post_type' => 'page',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => '_wpuf_has_ud_elementor_widget',
'value' => '1',
'compare' => '=',
],
],
] );
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix PHPCS multiline-call formatting violations.

The opening ( must end the line and the closing ) must be on its own line, per PHPCS report on lines 388, 399, 405, 409.

🎨 Proposed fix
-        $pages = get_posts( [
-            'post_type'      => 'page',
-            ...
-        ] );
+        $pages = get_posts(
+            [
+                'post_type'      => 'page',
+                ...
+            ]
+        );

Apply the same pattern to the get_posts( [ ... ] ) call inside the fallback at lines 405–409.

Also applies to: 405-409

🧰 Tools
🪛 GitHub Check: Run PHPCS inspection

[failure] 399-399:
Closing parenthesis of a multi-line function call must be on a line by itself


[warning] 392-392:
Detected usage of meta_query, possible slow query.


[failure] 388-388:
Opening parenthesis of a multi-line function call must be the last content on the line

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

In `@includes/Integrations/Elementor/Elementor.php` around lines 388 - 399, The
get_posts multiline call assigned to $pages violates PHPCS multiline-call rules:
move the opening parenthesis of get_posts to end the line (i.e., put get_posts(
on its own line) and ensure the closing parenthesis is on its own line after the
array; reformat the array argument so each key/value pair stays on its own line.
Apply the identical formatting change to the fallback get_posts call used later
(the other get_posts([...]) block) so both calls follow the same multiline-call
pattern.

Comment on lines +425 to +432
foreach ( $pages as $page ) {
$page_slug = $page->post_name;
add_rewrite_rule(
'^' . $page_slug . '/([^/]+)/?$',
'index.php?pagename=' . $page_slug . '&wpuf_user_profile=$matches[1]',
'top'
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C5 '\b(add_rewrite_rule|wpuf_user_profile)\b' --type=php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 6883


🏁 Script executed:

cat -n modules/user-directory/PrettyUrls.php | head -120

Repository: weDevsOfficial/wp-user-frontend

Length of output: 4387


🏁 Script executed:

rg -n 'get_page_uri' --type=php

Repository: weDevsOfficial/wp-user-frontend

Length of output: 57


🏁 Script executed:

rg -n 'wpuf_ud_rewrite' --type=php -B3 -A3

Repository: weDevsOfficial/wp-user-frontend

Length of output: 1672


🏁 Script executed:

web_search
Does WordPress pagename query parameter work with nested/hierarchical page paths

Repository: weDevsOfficial/wp-user-frontend

Length of output: 169


🌐 Web query:

WordPress pagename query parameter hierarchical nested pages

💡 Result:

In WordPress, the 'pagename' query parameter handles hierarchical nested pages by using the full path of the page hierarchy as its value. For a nested page structure like Parent > Child > Grandchild, the URL /parent/child/grandchild/ sets the 'pagename' query var to 'parent/child/grandchild'. WordPress then uses get_page_by_path('parent/child/grandchild') to retrieve the correct page object based on this full path. Key mechanisms: - Rewrite rules: WP_Rewrite::page_rewrite_rules adds the tag '%pagename%' with regex '(.?.+?)' and generates rules matching page permalinks to 'pagename=$matches[0]'. The regex (.?.+?) captures the full path including slashes. - Query var access: Use get_query_var('pagename') to retrieve the full hierarchical path string. - Page lookup: WP_Query uses this path with get_page_by_path, which queries the database for pages matching the path segments via post_name and post_parent relationships. - Hierarchical support: Pages are hierarchical post_type='page', so child pages have post_parent set to parent ID, enabling path-based resolution. Example usage: $pagename = get_query_var('pagename'); // e.g., 'parent/child' $page = get_page_by_path($pagename); This works for permalinks; without pretty permalinks, use ?pagename=parent/child/grandchild directly. Flush rewrite rules after permalink changes.

Citations:


Rewrite rule does not support hierarchical pages.

The pattern uses only post_name (leaf slug) and cannot match nested pages. For a page at /parent/child/, the URL /parent/child/username/ won't match the rule expecting /child/username/. While WordPress's pagename parameter supports full hierarchical paths, this code doesn't provide them.

Additionally, unlike the PrettyUrls implementation (which has the same limitation), this code lacks preg_quote() protection for regex special characters in the slug. Both should use get_page_uri() to build the full hierarchical path:

Suggested fix
 foreach ( $pages as $page ) {
-    $page_slug = $page->post_name;
-    add_rewrite_rule(
-        '^' . $page_slug . '/([^/]+)/?$',
-        'index.php?pagename=' . $page_slug . '&wpuf_user_profile=$matches[1]',
-        'top'
-    );
+    $page_uri = get_page_uri( $page ); // returns 'parent/child' for nested pages
+    if ( empty( $page_uri ) ) {
+        continue;
+    }
+    add_rewrite_rule(
+        '^' . preg_quote( $page_uri, '/' ) . '/([^/]+)/?$',
+        'index.php?pagename=' . $page_uri . '&wpuf_user_profile=$matches[1]',
+        'top'
+    );
 }
📝 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
foreach ( $pages as $page ) {
$page_slug = $page->post_name;
add_rewrite_rule(
'^' . $page_slug . '/([^/]+)/?$',
'index.php?pagename=' . $page_slug . '&wpuf_user_profile=$matches[1]',
'top'
);
}
foreach ( $pages as $page ) {
$page_uri = get_page_uri( $page ); // returns 'parent/child' for nested pages
if ( empty( $page_uri ) ) {
continue;
}
add_rewrite_rule(
'^' . preg_quote( $page_uri, '/' ) . '/([^/]+)/?$',
'index.php?pagename=' . $page_uri . '&wpuf_user_profile=$matches[1]',
'top'
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@includes/Integrations/Elementor/Elementor.php` around lines 425 - 432, The
rewrite rule currently uses only $page->post_name and doesn't handle
hierarchical pages or regex-special characters; replace that with the page's
full URI via get_page_uri($page->ID), escape it with preg_quote(..., '/') when
building the regex, and use that escaped full path for both the pattern and the
pagename query var (so pagename gets the full hierarchical path returned by
get_page_uri); update the loop that builds rules (the code around
add_rewrite_rule in Elementor.php) to use get_page_uri($page->ID) and preg_quote
on the path before constructing the '^.../([^/]+)/?$' pattern and the
'index.php?pagename=...&wpuf_user_profile=$matches[1]' replacement.

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.

3 participants