Conversation
WalkthroughThe vendors list page now supports extensibility through WordPress filters and plugin areas. Fields array and view configuration can be dynamically modified by extensions. Slot components enable plugin UI injections around the Add Vendor button, and a PluginArea component allows extension widgets at the bottom of the page. Changes
Sequence Diagram(s)sequenceDiagram
participant VendorsList as Vendors List<br/>Component
participant FilterHook as applyFilters<br/>Hook System
participant Extension as WordPress<br/>Extension/Plugin
participant UI as Rendered UI
VendorsList->>FilterHook: applyFilters('dokan-admin-vendors-list-column-fields', fields)
Extension->>FilterHook: Modify/Add fields
FilterHook->>VendorsList: Return augmented fields
VendorsList->>UI: Render columns
VendorsList->>FilterHook: applyFilters('dokan-admin-vendors-list-view', view)
Extension->>FilterHook: Modify view config
FilterHook->>VendorsList: Return modified view
VendorsList->>UI: Render with updated view
VendorsList->>UI: Render Slot (before Add Vendor)
Extension->>UI: Inject UI via Slot
VendorsList->>UI: Render PluginArea (page bottom)
Extension->>UI: Register widgets in PluginArea
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
src/admin/dashboard/pages/vendors.tsx (2)
263-366: Consider adding runtime validation for the filtered fields array.The
applyFilterscall returns an untyped value that's directly used as the fields configuration. If an extension returns an invalid structure, it could cause runtime errors.Apply this pattern to validate the filtered result:
- const fields = applyFilters( + let fields = applyFilters( 'dokan-admin-vendors-list-column-fields', [ { id: 'vendor', label: __( 'Vendor', 'dokan-lite' ), // ... rest of fields }, ], loadingClass, isLoading ); + + // Validate filtered result + if (!Array.isArray(fields)) { + console.error('dokan-admin-vendors-list-column-fields filter must return an array'); + fields = []; // fallback to safe default or original array + }
729-732: Consider adding runtime validation for the filtered view object.Similar to the fields filter, the view filter returns an untyped value. If an extension returns an invalid view configuration, it could cause the DataViews component to malfunction.
Apply this pattern to validate the filtered result:
- view={ applyFilters( + view={ (() => { + const filteredView = applyFilters( 'dokan-admin-vendors-list-view', view - ) } + ); + + // Validate filtered result + if (!filteredView || typeof filteredView !== 'object') { + console.error('dokan-admin-vendors-list-view filter must return a valid view object'); + return view; // fallback to original + } + + return filteredView; + })() }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/admin/dashboard/pages/vendors.tsx(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/admin/dashboard/pages/vendors.tsx (2)
src/definitions/dokan-vendor.ts (1)
Vendor(101-160)src/stores/vendors/selectors.ts (1)
isLoading(19-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: e2e tests (1, 3)
- GitHub Check: e2e tests (2, 3)
- GitHub Check: e2e tests (3, 3)
- GitHub Check: api tests (1, 1)
🔇 Additional comments (4)
src/admin/dashboard/pages/vendors.tsx (4)
16-19: LGTM!The new imports for
SlotandPluginAreaenable the extensibility features added in this PR, allowing the Pro version to inject store category functionality.
467-470: LGTM!The Slot component is properly configured to allow extensions to inject UI elements before the Add Vendor button, with appropriate props passed via
fillProps.
481-484: LGTM!The Slot component provides a symmetric injection point after the Add Vendor button, maintaining consistency with the before-slot implementation.
753-754: LGTM!The PluginArea component is properly configured to allow extensions to inject widgets at the bottom of the vendors page. This provides a flexible extension point for the Pro version to add features like store category management.
| const fields = applyFilters( | ||
| 'dokan-admin-vendors-list-column-fields', | ||
| [ | ||
| { | ||
| id: 'vendor', | ||
| label: __( 'Vendor', 'dokan-lite' ), | ||
| enableSorting: false, | ||
| render: ( { item }: { item: Vendor } ) => { | ||
| const name = item?.store_name || ''; | ||
| const avatar = item?.gravatar || ''; | ||
| return ( | ||
| <UserCard | ||
| name={ name } | ||
| avatar={ avatar } | ||
| isLoading={ isLoading } | ||
| loadingClass={ loadingClass } | ||
| onClick={ () => { | ||
| navigate( `/vendors/${ item.id }` ); | ||
| } } | ||
| subTitle={ item?.email || '' } | ||
| /> | ||
| ); | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| id: 'phone', | ||
| label: __( 'Phone', 'dokan-lite' ), | ||
| enableSorting: false, | ||
| render: ( { item }: { item: Vendor } ) => { | ||
| const phone = item?.phone || ''; | ||
| return ( | ||
| <div className="flex items-center gap-3"> | ||
| <span className="flex flex-col"> | ||
| { phone ? ( | ||
| <span | ||
| className={ twMerge( | ||
| 'text-[14px] font-[400] text-[#575757]', | ||
| isLoading ? loadingClass : '' | ||
| ) } | ||
| > | ||
| { phone } | ||
| </span> | ||
| ) : ( | ||
| <span className="text-gray-400">—</span> | ||
| ) } | ||
| </span> | ||
| </div> | ||
| ); | ||
| { | ||
| id: 'phone', | ||
| label: __( 'Phone', 'dokan-lite' ), | ||
| enableSorting: false, | ||
| render: ( { item }: { item: Vendor } ) => { | ||
| const phone = item?.phone || ''; | ||
| return ( | ||
| <div className="flex items-center gap-3"> | ||
| <span className="flex flex-col"> | ||
| { phone ? ( | ||
| <span | ||
| className={ twMerge( | ||
| 'text-[14px] font-[400] text-[#575757]', | ||
| isLoading ? loadingClass : '' | ||
| ) } | ||
| > | ||
| { phone } | ||
| </span> | ||
| ) : ( | ||
| <span className="text-gray-400">—</span> | ||
| ) } | ||
| </span> | ||
| </div> | ||
| ); | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| id: 'registered', | ||
| label: __( 'Registered', 'dokan-lite' ), | ||
| enableSorting: false, | ||
| render: ( { item }: { item: Vendor } ) => { | ||
| const registered = item?.registered || ''; | ||
| return ( | ||
| <div className="flex items-center gap-3"> | ||
| <div className="flex flex-col"> | ||
| { registered ? ( | ||
| <div | ||
| className={ twMerge( | ||
| 'text-[14px] font-[400] text-[#575757]', | ||
| isLoading ? loadingClass : '' | ||
| ) } | ||
| > | ||
| <DateTimeHtml.Date date={ registered } /> | ||
| </div> | ||
| ) : ( | ||
| <span className="text-gray-400">—</span> | ||
| ) } | ||
| { | ||
| id: 'registered', | ||
| label: __( 'Registered', 'dokan-lite' ), | ||
| enableSorting: false, | ||
| render: ( { item }: { item: Vendor } ) => { | ||
| const registered = item?.registered || ''; | ||
| return ( | ||
| <div className="flex items-center gap-3"> | ||
| <div className="flex flex-col"> | ||
| { registered ? ( | ||
| <div | ||
| className={ twMerge( | ||
| 'text-[14px] font-[400] text-[#575757]', | ||
| isLoading ? loadingClass : '' | ||
| ) } | ||
| > | ||
| <DateTimeHtml.Date | ||
| date={ registered } | ||
| /> | ||
| </div> | ||
| ) : ( | ||
| <span className="text-gray-400">—</span> | ||
| ) } | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| ); | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| id: 'status', | ||
| label: __( 'Status', 'dokan-lite' ), | ||
| enableSorting: false, | ||
| render: ( { item }: { item: Vendor } ) => { | ||
| return ( | ||
| <span | ||
| className={ twMerge( | ||
| 'inline-flex items-center px-3.5 py-1.5 rounded-full text-xs font-medium', | ||
| item?.enabled | ||
| ? 'bg-[#D4FBEF] text-[#00563F]' | ||
| : 'bg-[#F1F1F4] text-[#393939]', | ||
| isLoading ? loadingClass : '' | ||
| ) } | ||
| > | ||
| { item?.enabled | ||
| ? __( 'Enabled', 'dokan-lite' ) | ||
| : __( 'Disabled', 'dokan-lite' ) } | ||
| </span> | ||
| ); | ||
| { | ||
| id: 'status', | ||
| label: __( 'Status', 'dokan-lite' ), | ||
| enableSorting: false, | ||
| render: ( { item }: { item: Vendor } ) => { | ||
| return ( | ||
| <span | ||
| className={ twMerge( | ||
| 'inline-flex items-center px-3.5 py-1.5 rounded-full text-xs font-medium', | ||
| item?.enabled | ||
| ? 'bg-[#D4FBEF] text-[#00563F]' | ||
| : 'bg-[#F1F1F4] text-[#393939]', | ||
| isLoading ? loadingClass : '' | ||
| ) } | ||
| > | ||
| { item?.enabled | ||
| ? __( 'Enabled', 'dokan-lite' ) | ||
| : __( 'Disabled', 'dokan-lite' ) } | ||
| </span> | ||
| ); | ||
| }, | ||
| }, | ||
| }, | ||
| ]; | ||
| ], | ||
| loadingClass, | ||
| isLoading | ||
| ); |
There was a problem hiding this comment.
Inconsistent filter naming convention.
The new filter name dokan-admin-vendors-list-column-fields uses kebab-case, while existing filters in this file use snake_case (e.g., dokan_admin_vendors_list_filters at Line 48, dokan_admin_vendors_before_request at Line 98). This inconsistency may confuse extension developers.
Consider using snake_case for consistency:
- const fields = applyFilters(
- 'dokan-admin-vendors-list-column-fields',
+ const fields = applyFilters(
+ 'dokan_admin_vendors_list_column_fields',
[📝 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.
| const fields = applyFilters( | |
| 'dokan-admin-vendors-list-column-fields', | |
| [ | |
| { | |
| id: 'vendor', | |
| label: __( 'Vendor', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const name = item?.store_name || ''; | |
| const avatar = item?.gravatar || ''; | |
| return ( | |
| <UserCard | |
| name={ name } | |
| avatar={ avatar } | |
| isLoading={ isLoading } | |
| loadingClass={ loadingClass } | |
| onClick={ () => { | |
| navigate( `/vendors/${ item.id }` ); | |
| } } | |
| subTitle={ item?.email || '' } | |
| /> | |
| ); | |
| }, | |
| }, | |
| }, | |
| { | |
| id: 'phone', | |
| label: __( 'Phone', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const phone = item?.phone || ''; | |
| return ( | |
| <div className="flex items-center gap-3"> | |
| <span className="flex flex-col"> | |
| { phone ? ( | |
| <span | |
| className={ twMerge( | |
| 'text-[14px] font-[400] text-[#575757]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| { phone } | |
| </span> | |
| ) : ( | |
| <span className="text-gray-400">—</span> | |
| ) } | |
| </span> | |
| </div> | |
| ); | |
| { | |
| id: 'phone', | |
| label: __( 'Phone', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const phone = item?.phone || ''; | |
| return ( | |
| <div className="flex items-center gap-3"> | |
| <span className="flex flex-col"> | |
| { phone ? ( | |
| <span | |
| className={ twMerge( | |
| 'text-[14px] font-[400] text-[#575757]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| { phone } | |
| </span> | |
| ) : ( | |
| <span className="text-gray-400">—</span> | |
| ) } | |
| </span> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| }, | |
| { | |
| id: 'registered', | |
| label: __( 'Registered', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const registered = item?.registered || ''; | |
| return ( | |
| <div className="flex items-center gap-3"> | |
| <div className="flex flex-col"> | |
| { registered ? ( | |
| <div | |
| className={ twMerge( | |
| 'text-[14px] font-[400] text-[#575757]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| <DateTimeHtml.Date date={ registered } /> | |
| </div> | |
| ) : ( | |
| <span className="text-gray-400">—</span> | |
| ) } | |
| { | |
| id: 'registered', | |
| label: __( 'Registered', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const registered = item?.registered || ''; | |
| return ( | |
| <div className="flex items-center gap-3"> | |
| <div className="flex flex-col"> | |
| { registered ? ( | |
| <div | |
| className={ twMerge( | |
| 'text-[14px] font-[400] text-[#575757]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| <DateTimeHtml.Date | |
| date={ registered } | |
| /> | |
| </div> | |
| ) : ( | |
| <span className="text-gray-400">—</span> | |
| ) } | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| ); | |
| }, | |
| }, | |
| }, | |
| { | |
| id: 'status', | |
| label: __( 'Status', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| return ( | |
| <span | |
| className={ twMerge( | |
| 'inline-flex items-center px-3.5 py-1.5 rounded-full text-xs font-medium', | |
| item?.enabled | |
| ? 'bg-[#D4FBEF] text-[#00563F]' | |
| : 'bg-[#F1F1F4] text-[#393939]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| { item?.enabled | |
| ? __( 'Enabled', 'dokan-lite' ) | |
| : __( 'Disabled', 'dokan-lite' ) } | |
| </span> | |
| ); | |
| { | |
| id: 'status', | |
| label: __( 'Status', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| return ( | |
| <span | |
| className={ twMerge( | |
| 'inline-flex items-center px-3.5 py-1.5 rounded-full text-xs font-medium', | |
| item?.enabled | |
| ? 'bg-[#D4FBEF] text-[#00563F]' | |
| : 'bg-[#F1F1F4] text-[#393939]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| { item?.enabled | |
| ? __( 'Enabled', 'dokan-lite' ) | |
| : __( 'Disabled', 'dokan-lite' ) } | |
| </span> | |
| ); | |
| }, | |
| }, | |
| }, | |
| ]; | |
| ], | |
| loadingClass, | |
| isLoading | |
| ); | |
| const fields = applyFilters( | |
| 'dokan_admin_vendors_list_column_fields', | |
| [ | |
| { | |
| id: 'vendor', | |
| label: __( 'Vendor', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const name = item?.store_name || ''; | |
| const avatar = item?.gravatar || ''; | |
| return ( | |
| <UserCard | |
| name={ name } | |
| avatar={ avatar } | |
| isLoading={ isLoading } | |
| loadingClass={ loadingClass } | |
| onClick={ () => { | |
| navigate( `/vendors/${ item.id }` ); | |
| } } | |
| subTitle={ item?.email || '' } | |
| /> | |
| ); | |
| }, | |
| }, | |
| { | |
| id: 'phone', | |
| label: __( 'Phone', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const phone = item?.phone || ''; | |
| return ( | |
| <div className="flex items-center gap-3"> | |
| <span className="flex flex-col"> | |
| { phone ? ( | |
| <span | |
| className={ twMerge( | |
| 'text-[14px] font-[400] text-[#575757]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| { phone } | |
| </span> | |
| ) : ( | |
| <span className="text-gray-400">—</span> | |
| ) } | |
| </span> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| { | |
| id: 'registered', | |
| label: __( 'Registered', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| const registered = item?.registered || ''; | |
| return ( | |
| <div className="flex items-center gap-3"> | |
| <div className="flex flex-col"> | |
| { registered ? ( | |
| <div | |
| className={ twMerge( | |
| 'text-[14px] font-[400] text-[#575757]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| <DateTimeHtml.Date | |
| date={ registered } | |
| /> | |
| </div> | |
| ) : ( | |
| <span className="text-gray-400">—</span> | |
| ) } | |
| </div> | |
| </div> | |
| ); | |
| }, | |
| }, | |
| { | |
| id: 'status', | |
| label: __( 'Status', 'dokan-lite' ), | |
| enableSorting: false, | |
| render: ( { item }: { item: Vendor } ) => { | |
| return ( | |
| <span | |
| className={ twMerge( | |
| 'inline-flex items-center px-3.5 py-1.5 rounded-full text-xs font-medium', | |
| item?.enabled | |
| ? 'bg-[#D4FBEF] text-[#00563F]' | |
| : 'bg-[#F1F1F4] text-[#393939]', | |
| isLoading ? loadingClass : '' | |
| ) } | |
| > | |
| { item?.enabled | |
| ? __( 'Enabled', 'dokan-lite' ) | |
| : __( 'Disabled', 'dokan-lite' ) } | |
| </span> | |
| ); | |
| }, | |
| }, | |
| ], | |
| loadingClass, | |
| isLoading | |
| ); |
🤖 Prompt for AI Agents
In src/admin/dashboard/pages/vendors.tsx around lines 263 to 366, the filter
name 'dokan-admin-vendors-list-column-fields' uses kebab-case while the rest of
this file uses snake_case; change the filter name to
'dokan_admin_vendors_list_column_fields' in the applyFilters call and update any
corresponding external usages/listeners to the new snake_case name so naming is
consistent with other filters in the file.
| view={ applyFilters( | ||
| 'dokan-admin-vendors-list-view', | ||
| view | ||
| ) } |
There was a problem hiding this comment.
Inconsistent filter naming convention.
The filter name dokan-admin-vendors-list-view uses kebab-case, which is inconsistent with existing filters in this file that use snake_case. Consider using dokan_admin_vendors_list_view for consistency.
🤖 Prompt for AI Agents
In src/admin/dashboard/pages/vendors.tsx around lines 729 to 732, the filter
name uses kebab-case ('dokan-admin-vendors-list-view') which is inconsistent
with the file's snake_case convention; change the filter key to
dokan_admin_vendors_list_view and update any corresponding add_filter/useFilter
calls or tests to use the new snake_case name so all filter registration and
consumption remain consistent.
All Submissions:
Related Pull Request(s)
Closes
How to test the changes in this Pull Request:
Changelog entry
No neededSummary by CodeRabbit