Feature/user directory elementor widget#1825
Feature/user directory elementor widget#1825sapayth wants to merge 25 commits intoweDevsOfficial:developfrom
Conversation
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
WalkthroughThis PR introduces a comprehensive free-version User Directory module for WP User Frontend. It adds admin UI for creating and managing directories, REST API endpoints for CRUD and user search, frontend rendering via shortcodes and Elementor widgets, Tailwind-based styling, and supporting build infrastructure. The module includes profile layouts, pagination, search, sorting, and role-based filtering capabilities. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 | 🟡 MinorDuplicate dismiss logic risks double
onCloseinvocation.The close button duplicates the auto-dismiss logic. If a user clicks close while the auto-dismiss timer is in flight,
onClosemay 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 | 🟡 MinorAvoid
#as fallback forupgradeUrl.When
upgradeUrlis 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 | 🟡 MinorMisleading comment: layout-6 has the largest avatar size, not medium.
The comment says "Grid layout - medium avatars" but
265is 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 | 🟡 MinorUnstable tooltip ID on each render and deprecated
substrusage.
tooltipIdis regenerated on every render, which can cause accessibility issues with screen readers when the tooltip becomes visible (the ID changes between renders).String.prototype.substr()is deprecated; usesubstring()orslice()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 | 🟡 MinorLocalize action button labels.
At Line 50 and Line 57,
Cancel/Deleteare 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 | 🟡 MinorClamp and default
current_pagebefore 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 | 🟡 MinorBuild
upgradeUrlwith encoded query params.At Line 17, manual concatenation with raw
utmcan produce malformed URLs ifutmcontains 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 | 🟡 MinorHandle falsy-but-valid selected values correctly.
Line 40 treats values like
0as 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 | 🟡 MinorHarden external links opened in new tabs.
Lines [33], [42], [51], and [60] should include
noreferreralongsidenoopenerfor 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 | 🟡 MinorAdd
relattributes to links opened withtarget="_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 | 🟡 MinorHarden
$userand$sizeinputs before rendering.Lines [18-24] should verify
WP_Usertype 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 | 🟡 MinorInitialize
$all_databefore nested key access.Lines [18-19] assume
$all_dataexists 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
URLparsing instead of string split logic so#fragmentand 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 | 🟡 MinorPotential 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 | 🟡 MinorMissing cleanup for debounce timeout on unmount.
The
searchTimeoutReftimeout 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 | 🟡 MinorMissing 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 | 🟡 MinorHarden new-tab links with
rel="noopener noreferrer".Both anchors open new tabs and should include
relto 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 | 🟡 MinorAdd
relfor 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 | 🟡 MinorNormalize
file/filestab key mapping.Label map uses
fileswhile other defaults/configs usefile, 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
⛔ Files ignored due to path filters (22)
assets/css/wpuf-user-directory-free.css.mapis excluded by!**/*.mapassets/images/user-directory/confetti.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-1.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-2.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-3.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-4.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-5.pngis excluded by!**/*.pngassets/images/user-directory/directory-layout-6.pngis excluded by!**/*.pngassets/images/user-directory/profile-layout-1.pngis excluded by!**/*.pngassets/images/user-directory/profile-layout-2.pngis excluded by!**/*.pngassets/images/user-directory/profile-layout-3.pngis excluded by!**/*.pngassets/images/user-directory/round-grids.pngis excluded by!**/*.pngassets/images/user-directory/sidecards.pngis excluded by!**/*.pngassets/images/user-directory/square-grids.pngis excluded by!**/*.pngassets/images/user-directory/table.pngis excluded by!**/*.pngassets/images/user-directory/thumb-male-1.svgis excluded by!**/*.svgassets/images/user-directory/thumb-male-2.svgis excluded by!**/*.svgassets/images/user-directory/thumb-male-3.svgis excluded by!**/*.svgassets/images/user-directory/wide-sidecards.pngis excluded by!**/*.pngassets/js/wpuf-user-directory-free.js.mapis excluded by!**/*.mapmodules/user-directory/package-lock.jsonis excluded by!**/package-lock.jsonpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (68)
Gruntfile.jsassets/css/admin/subscriptions.min.cssassets/css/admin/wpuf-module.cssassets/css/ai-form-builder.min.cssassets/css/elementor-user-directory.cssassets/css/forms-list.min.cssassets/css/frontend-subscriptions.min.cssassets/css/wpuf-user-directory-free.cssassets/css/wpuf-user-directory-frontend.cssassets/js/admin/wpuf-module.jsassets/js/ud-search-shortcode.jsassets/js/wpuf-user-directory-free.asset.phpassets/js/wpuf-user-directory-free.jsassets/js/wpuf-user-directory-free.js.LICENSE.txtassets/js/wpuf-user-directory-frontend.jsincludes/Assets.phpincludes/Free/Free_Loader.phpincludes/Integrations.phpincludes/Integrations/Elementor/Elementor.phpincludes/Integrations/Elementor/User_Directory_Widget.phpincludes/functions/modules.phpmodules/user-directory/Admin_Menu.phpmodules/user-directory/Api/Directory.phpmodules/user-directory/DirectoryStyles.phpmodules/user-directory/Helpers.phpmodules/user-directory/Post_Type.phpmodules/user-directory/PrettyUrls.phpmodules/user-directory/Shortcode.phpmodules/user-directory/User_Directory.phpmodules/user-directory/package.jsonmodules/user-directory/postcss.config.jsmodules/user-directory/tailwind.config.jsmodules/user-directory/views/admin-page.phpmodules/user-directory/views/directory/layout-3.phpmodules/user-directory/views/directory/template-parts/pagination-shortcode.phpmodules/user-directory/views/directory/template-parts/row-3.phpmodules/user-directory/views/directory/template-parts/search-field.phpmodules/user-directory/views/directory/template-parts/social-icons.phpmodules/user-directory/views/directory/template-parts/sort-field.phpmodules/user-directory/views/profile/layout-2.phpmodules/user-directory/views/profile/template-parts/about-2.phpmodules/user-directory/views/profile/template-parts/comments-2.phpmodules/user-directory/views/profile/template-parts/file-2.phpmodules/user-directory/views/profile/template-parts/posts-2.phpmodules/user-directory/views/profile/template-parts/user-avatar.phpmodules/user-directory/webpack.config.jspackage.jsonpostcss.user-directory.config.jssrc/js/user-directory/App.jssrc/js/user-directory/components/DirectoryList.jssrc/js/user-directory/components/DirectoryWizard.jssrc/js/user-directory/components/common/DeleteConfirmModal.jssrc/js/user-directory/components/common/Header.jssrc/js/user-directory/components/common/LayoutCard.jssrc/js/user-directory/components/common/MultiSelect.jssrc/js/user-directory/components/common/SingleSelect.jssrc/js/user-directory/components/common/Toast.jssrc/js/user-directory/components/common/Tooltip.jssrc/js/user-directory/components/steps/StepAdvanced.jssrc/js/user-directory/components/steps/StepBasics.jssrc/js/user-directory/components/steps/StepLayout.jssrc/js/user-directory/components/steps/StepProfile.jssrc/js/user-directory/components/steps/StepTabs.jssrc/js/user-directory/index.jssrc/js/user-directory/styles/main.csssrc/js/user-directory/utils/avatarSizeHelper.jstailwind.config.jswpuf-functions.php
| const SEARCH_DEBOUNCE = 300; | ||
| let debounceTimeout = null; |
There was a problem hiding this comment.
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.
| $page = ! empty( $request['page'] ) ? absint( $request['page'] ) : 1; | ||
| $orderby = ! empty( $request['orderby'] ) ? sanitize_text_field( $request['orderby'] ) : 'ID'; | ||
| $order = ! empty( $request['order'] ) ? strtoupper( sanitize_text_field( $request['order'] ) ) : 'DESC'; | ||
| $max_item = ! empty( $request['max_item'] ) ? absint( $request['max_item'] ) : 12; |
There was a problem hiding this comment.
Guard page/per_page bounds in search pagination.
page=0 yields negative offsets, and per_page=0 can trigger division by zero at Line [518].
Proposed fix
- $page = ! empty( $request['page'] ) ? absint( $request['page'] ) : 1;
+ $page = ! empty( $request['page'] ) ? max( 1, absint( $request['page'] ) ) : 1;
@@
- $settings_per_page = absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) );
- $per_page = $max_item > 0 ? $max_item : $settings_per_page;
+ $settings_per_page = max( 1, absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) ) );
+ $per_page = max( 1, $max_item > 0 ? $max_item : $settings_per_page );Also applies to: 473-475, 518-518
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@modules/user-directory/Api/Directory.php` around lines 453 - 456, The
pagination variables allow zero values which lead to negative offsets and
divide-by-zero; ensure $page and $max_item (aka per_page) are clamped to a safe
range after sanitization: set $page = max(1, absint($request['page'] ?? 1)) and
set $max_item = max(1, absint($request['max_item'] ?? 12)) (optionally enforce
an upper cap like 100), and apply the same clamping logic wherever
$page/$max_item are computed/used (including the other occurrences that compute
$page, $orderby, $order and the division/offset calculation) so offsets and
divisions cannot receive zero or negative values.
| $per_page = absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) ); | ||
| $offset = ( $paged - 1 ) * $per_page; |
There was a problem hiding this comment.
Clamp per_page before pagination math.
$per_page can be 0 from saved settings, which makes Line [333] divide by zero.
Proposed fix
- $per_page = absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) );
+ $per_page = max( 1, absint( $settings['users_per_page'] ?? ( $settings['per_page'] ?? 12 ) ) );
$offset = ( $paged - 1 ) * $per_page;Also applies to: 333-333
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@modules/user-directory/Shortcode.php` around lines 288 - 289, The issue is
that $per_page can be 0 from saved settings which leads to invalid pagination
math when computing $offset and later divisions; modify the logic around
$per_page in the Shortcode class (the code setting $per_page and using it to
compute $offset and any subsequent division) to clamp it to a minimum of 1 (e.g.
replace the direct absint(...) result with a sanitized value like $per_page =
max(1, absint(...)) or otherwise ensure $per_page >= 1) before any use in
$offset = ($paged - 1) * $per_page or other calculations so division-by-zero
cannot occur.
| $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'; |
There was a problem hiding this comment.
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.
| import { useState, useRef, useEffect } from '@wordpress/element'; | ||
| import { __ } from '@wordpress/i18n'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Read the entire file to verify React usage and imports
cat -n src/js/user-directory/components/common/MultiSelect.js | head -50Repository: weDevsOfficial/wp-user-frontend
Length of output: 2395
React is undefined where React.cloneElement is called.
Lines 29-37 reference React.cloneElement, but React is never imported. This will throw a ReferenceError at runtime when option icons are rendered.
Import cloneElement from @wordpress/element and use it directly:
Proposed fix
-import { useState, useRef, useEffect } from '@wordpress/element';
+import { useState, useRef, useEffect, cloneElement } from '@wordpress/element';
@@
- return React.cloneElement(option.icon, {
- children: React.cloneElement(option.icon.props.children, {
+ return cloneElement(option.icon, {
+ children: cloneElement(option.icon.props.children, {
@@
- return React.cloneElement(option.icon, {
- children: React.cloneElement(option.icon.props.children, {
+ return cloneElement(option.icon, {
+ children: cloneElement(option.icon.props.children, {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/js/user-directory/components/common/MultiSelect.js` around lines 1 - 2,
The code calls React.cloneElement in MultiSelect.js (around the option icon
rendering) but React is never imported; import cloneElement from
'@wordpress/element' (add it to the existing import list with
useState/useRef/useEffect) and replace React.cloneElement(...) calls with direct
cloneElement(...) calls so the option icons render without a ReferenceError.
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
Technical Notes
WeDevs\Wpuf\Integrations\Elementor\User_Directory_WidgetSummary by CodeRabbit
Release Notes