Skip to content

Conversation

@kzamanbd
Copy link
Contributor

@kzamanbd kzamanbd commented Dec 24, 2025

Previous Pull Request #2098

All Submissions:

  • My code follow the WordPress' coding standards
  • My code satisfies feature requirements
  • My code is tested
  • My code passes the PHPCS tests
  • My code has proper inline documentation
  • I've included related pull request(s) (optional)
  • I've included developer documentation (optional)
  • I've added proper labels to this pull request

Changes proposed in this Pull Request:

Related Pull Request(s)

Closes

How to test the changes in this Pull Request:

  • Steps or issue link

Changelog entry

Title

Detailed Description of the pull request. What was previous behaviour
and what will be changed in this PR.

Before Changes

Describe the issue before changes with screenshots(s).

After Changes

Describe the issue after changes with screenshot(s).

Feature Video (optional)

Link of detailed video if this PR is for a feature.

PR Self Review Checklist:

  • Code is not following code style guidelines
  • Bad naming: make sure you would understand your code if you read it a few months from now.
  • KISS: Keep it simple, Sweetie (not stupid!).
  • DRY: Don't Repeat Yourself.
  • Code that is not readable: too many nested 'if's are a bad sign.
  • Performance issues
  • Complicated constructions that need refactoring or comments: code should almost always be self-explanatory.
  • Grammar errors.

FOR PR REVIEWER ONLY:

As a reviewer, your feedback should be focused on the idea, not the person. Seek to understand, be respectful, and focus on constructive dialog.

As a contributor, your responsibility is to learn from suggestions and iterate your pull request should it be needed based on feedback. Seek to collaborate and produce the best possible contribution to the greater whole.

  • Correct — Does the change do what it’s supposed to? ie: code 100% fulfilling the requirements?
  • Secure — Would a nefarious party find some way to exploit this change? ie: everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities?
  • Readable — Will your future self be able to understand this change months down the road?
  • Elegant — Does the change fit aesthetically within the overall style and architecture?

Summary by CodeRabbit

  • New Features

    • New Product Form Manager UI for creating/editing products with customizable fields, images, gallery, files, prices, dates and rich text.
    • Variation management UI with attribute editing, generation and per-variation editing.
    • REST endpoint to fetch product form configurations.
  • Improvements

    • Settings page action to reset form field configurations.
    • Dashboard integration: mountable form manager, added assets and templates for seamless product edit flow.

✏️ Tip: You can customize this high-level summary in your review settings.

devAsadNur and others added 30 commits September 1, 2023 11:50
…nd made Single Step Product Edit page as default

refactor: replaced product add/edit page code with OOP
fix: added Dokan Pro checks for `product_vendors_can_create_tags` and `product_category_style` admin settings
… dokan_product_edit_meta_data, this will save some executions

refactor: removed update_post_meta and get_post_meta references
…Manager.php to prevent direct access to the files
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

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

🤖 Fix all issues with AI agents
In `@includes/Assets.php`:
- Around line 381-385: Update the asset registration for
'dokan-product-form-manager' to point to the correct CSS output path: replace
the src value using DOKAN_PLUGIN_ASSEST . '/js/form-manager.css' with
DOKAN_PLUGIN_ASSEST . '/css/form-manager.css' and update the filemtime()
argument from DOKAN_DIR . '/assets/js/form-manager.css' to DOKAN_DIR .
'/assets/css/form-manager.css' so the registered URL and version reflect the
actual built CSS file.

In `@includes/Dashboard/Templates/Products.php`:
- Around line 459-463: The shipping-field skip logic is inverted: currently it
checks if $_POST['_disable_shipping'] === 'no' and continues, but it should only
skip when shipping is actually disabled. Update the condition in the block that
defines $shipping_fields and checks $field->get_id() to use 'yes' ===
$_POST['_disable_shipping'] (keeping the isset guard) so shipping fields are
only skipped when _disable_shipping is 'yes'.

In `@includes/Product/FormManager.php`:
- Around line 142-158: Add an explicit capability check in
dokan_save_product_data to prevent unauthorized product creation: after the
nonce verification (in dokan_save_product_data) check
current_user_can('dokan_edit_product') and if it returns false call
wp_send_json_error with a suitable 'type' (e.g., 'permission') and message
(e.g., 'You are not allowed to perform this action') and exit; only proceed to
call dokan()->product->create($_POST) and wp_send_json_success when the
capability check passes.

In `@includes/Product/Manager.php`:
- Around line 376-384: The current checks use ! empty on
$args[FormElements::FEATURED_IMAGE_ID] and
$args[FormElements::GALLERY_IMAGE_IDS], which prevents clearing the featured
image by setting 0 or clearing the gallery by passing an empty array/string;
change the conditional to test presence (e.g.,
isset($args[FormElements::FEATURED_IMAGE_ID]) or array_key_exists for gallery)
so you always call $product->set_image_id(...) and
$product->set_gallery_image_ids(...) with the provided value (including 0 or
empty) to allow clearing images.

In `@includes/ProductForm/Init.php`:
- Around line 185-221: The value_callback closures for
Elements::DATE_ON_SALE_FROM and Elements::DATE_ON_SALE_TO currently return an
empty string when $value is non-empty, which discards user input; change each
callback so that if ('' !== $value) it returns $value (not ''), otherwise keep
the existing logic that checks $product instanceof WC_Product and returns the
product date formatted via get_date_on_sale_from('edit')->date('Y-m-d') or
get_date_on_sale_to('edit')->date('Y-m-d') as before.

In `@includes/REST/ProductController.php`:
- Around line 1768-1779: Sanitize $product_id with absint() at the start of
get_item_fields, then after the existing product existence check enforce
ownership similar to validate_post_author_override(): if the current user cannot
manage_options, compare (int) get_post_field('post_author', $product_id) to
dokan_get_current_user_id() and return a WP_Error('dokan_rest_forbidden',
'Sorry, you are not allowed to access this product.', ['status' => 403]) when
they differ; keep the rest of the response (FormManager::get_form_fields and
dokan()->commission->get_earning_by_product) unchanged.

In `@src/components/TreeSelectPicker.tsx`:
- Around line 42-57: The onChange handler in TreeSelectPicker mutates the
incoming array by calling value.pop(); instead, avoid mutating the prop by
deriving the last value immutably (e.g., use value.slice(-1) or access
value[value.length-1] with a length check) and pass that new array to
document.body.dispatchEvent, setSelected, and onChangeProp; update the onChange
function (referenced as onChange, multiple, setSelected, onChangeProp) to
compute lastValue without altering the original value.

In `@src/dashboard/form-manager/components/ImagePreview.tsx`:
- Around line 16-33: The code currently sets items = Array.isArray(images) ?
images : [images] which yields [undefined] when images is null/undefined and
leads to item.url crashes; update the normalization in the ImagePreview
component to convert null/undefined into an empty array and filter out falsy
entries (e.g., use a normalized items array by treating a single image or array,
then items = items.filter(Boolean)); also make the key usage in the items.map
fallback to index when item.url is missing and guard usages of item.url/item.alt
to avoid runtime errors (references: images, items, itemClassName, the mapping
block where item.url and item.alt are accessed).

In `@src/dashboard/form-manager/components/PriceEdit.tsx`:
- Around line 13-29: vendorEarningHandler is calling apiFetch for every
keystroke which causes excessive requests; wrap the API call in a debounced
function (e.g., useDebouncedCallback or a useRef+setTimeout debounce) so that
vendorEarningHandler schedules the fetch after a short delay and cancels on
further input; keep the same behavior of setting
setVendorEarning(Number(response)) on success and setVendorEarning(0) on error,
and apply the same debounced pattern to the other similar handler referenced
(the block around the second occurrence that also calls
apiFetch/setVendorEarning).

In `@src/dashboard/form-manager/components/variation/AttributeCard.tsx`:
- Around line 7-14: The accordion header in AttributeCard is not
keyboard-accessible and the remove control allows its click to bubble and toggle
the accordion; update the header element (the element given role="button" that
toggles expansion) to include tabIndex={0} and an onKeyDown handler that
triggers the same toggle on Enter and Space keys, and convert the remove control
from a <span role="button"> to a proper <button> element (in the same
AttributeCard component) so it receives keyboard focus natively; additionally,
ensure the remove button's onClick (and any onKeyDown handler if used) calls
event.stopPropagation() before invoking onRemove so removing the item does not
also toggle the accordion.
- Around line 54-61: attributeChangeHandler can receive null from react-select
when cleared, causing selected.map to throw; update the handler in
AttributeCard.tsx (attributeChangeHandler / onUpdate / attr) to guard against
null by computing values as selected ? selected.map((val:any)=>val.value) : []
and passing terms as selected || [] (or an empty array) into onUpdate so
clearing the control results in empty options/terms instead of a crash.

In `@src/dashboard/form-manager/components/variation/VariationCard.tsx`:
- Around line 45-83: The expand header (currently a div with role="button" using
fetchedVariationData and referencing isExpanded/variation.id) is not
keyboard-accessible and the "Remove" span (text-red-500...) has no action;
change both to interactive controls: replace the header div with a real button
(or add tabIndex={0} plus onKeyDown handling for Enter/Space) and ensure it
invokes fetchedVariationData on click/keyboard activation, and replace the
"Remove" span with a real button (or add tabIndex/onKeyDown) that calls the
actual delete handler (e.g. onRemoveVariation or removeVariation(variation.id))
or remove the UI until the handler exists; also add aria-labels for both
controls to improve accessibility.

In `@src/dashboard/form-manager/context/VariationContext.tsx`:
- Around line 186-210: The generateVariations async function needs robust error
handling and i18n/UI consistency: wrap the confirmation text with the
translation helper (e.g. __('Are you sure you want to generate variations? This
will overwrite existing variations.')) and replace the native confirm() with the
dokan-ui confirmation modal used elsewhere; surround the AJAX call (jQuery.ajax)
in a try-catch, log errors, and call toast({type: 'error', title: error.message
|| __('Failed to generate variations')}) on failure; ensure success still shows
the success toast using response.data.message and preserve the existing console
logs if desired.

In `@src/dashboard/form-manager/factories/form-data.ts`:
- Around line 33-36: DefaultHandler's getValue currently uses a logical OR
(field.value || '') which incorrectly turns valid falsy values like 0 or false
into an empty string; change the implementation of DefaultHandler.getValue to
use nullish coalescing (use field.value ?? '') so only null or undefined become
'' while preserving 0/false values.

In `@templates/products/form-manager/form-content.php`:
- Around line 12-15: The template directly calls $product->get_id() which will
fatally error if $product is not set; update the template to first check that
$product is defined and is an object with get_id (e.g., isset($product) &&
is_object($product) && method_exists($product, 'get_id')), and if that check
fails output a safe fallback (empty string or 0) for the hidden input value so
the template renders in create flows without throwing.
♻️ Duplicate comments (2)
includes/Assets.php (1)

549-552: Use the dynamic tipTip handle (already flagged).
The hardcoded jquery-tiptip can break on WC 10.3+ where the handle is wc-jquery-tiptip. Please swap to $jquery_tiptip as noted previously.

includes/Dashboard/Templates/Products.php (1)

149-162: Potential database bloat from repeated auto-draft creation.

Lines 155-158 create a new auto-draft product every time product_id is 0. If users repeatedly navigate to the add-product page without completing the form, orphaned auto-draft products will accumulate.

Consider reusing existing auto-drafts owned by the current user or implementing a cleanup mechanism for abandoned drafts.

🟡 Minor comments (12)
includes/Dashboard/Templates/Products.php-485-492 (1)

485-492: Required field check may incorrectly reject valid falsy values.

Using empty( $_POST[ $field_name ] ) will treat '0', 0, and '' as missing values. For fields like quantity or price where 0 is valid, this could incorrectly trigger a required field error.

Suggested approach
-            if ( $field->is_required() && empty( $_POST[ $field_name ] ) ) {
+            if ( $field->is_required() && ! isset( $_POST[ $field_name ] ) || ( is_string( $_POST[ $field_name ] ) && '' === trim( $_POST[ $field_name ] ) ) ) {

Or use a dedicated method on the field object to validate presence based on field type.

src/dashboard/form-manager/components/DateTimePickerEdit.tsx-4-6 (1)

4-6: Potential typo: field.error vs field.errors.

FormField declares errors, so field.error likely stays undefined and error UI won’t render. Please align with the actual field shape.

💡 Suggested fix
-    <CustomField label={ field.label } error={ field.error }>
+    <CustomField label={ field.label } error={ field.errors }>
templates/products/form-manager/form-wrapper.php-1-11 (1)

1-11: Add ABSPATH guard for direct access.

This template doesn't prevent direct access. The sibling template form-content.php in the same directory includes this guard, and it's standard across the Dokan codebase.

🛡️ Proposed fix
 <?php
 /**
  *  Dokan Dashboard Product form manager Template
  *
  *  Load product related template
  *
  *  `@since` DOKAN_SINCE
  *
  *  `@package` dokan
  */
 ?>
+
+<?php defined( 'ABSPATH' ) || exit; ?>
src/dashboard/form-manager/components/PriceEdit.tsx-9-11 (1)

9-11: Guard against NaN when initializing vendorEarning.

Number(undefined) returns NaN, which could cause display issues if data.vendor_earning is not set.

💡 Suggested fix
 const [ vendorEarning, setVendorEarning ] = useState(
-    Number( data.vendor_earning )
+    Number( data.vendor_earning ) || 0
 );
src/dashboard/form-manager/components/PriceEdit.tsx-36-38 (1)

36-38: Grammar error and missing internationalization.

"Your Earn" should be "You Earn" or "Your Earnings". Additionally, this string should use __() for translation consistency.

💡 Suggested fix
 <span className="text-xs font-normal text-gray-500">
-    ( Your Earn: { formatPrice( vendorEarning ) } )
+    ( { __( 'You Earn:', 'dokan-lite' ) } { formatPrice( vendorEarning ) } )
 </span>
src/dashboard/form-manager/components/ImagePreview.tsx-35-44 (1)

35-44: Add an accessible label to the remove icon button.

Icon-only buttons need an aria-label so screen readers can announce the action.

🔧 Suggested fix
-                    <button
+                    <button
                         type="button"
+                        aria-label="Remove image"
                         className="absolute top-1 right-1 bg-white rounded-full p-0.5 shadow-sm opacity-0 group-hover:opacity-100 transition-opacity duration-200 hover:text-red-500"
src/dashboard/form-manager/components/FileUploadEdit.tsx-44-57 (1)

44-57: Guard against undefined selection in onSelectFile.

The MediaUploader may pass an empty array or the selection could be undefined in edge cases. Accessing .id on undefined will throw. Add a guard clause to safely handle empty selections.

🔧 Suggested fix
     const onSelectFile = ( value: any, index: number ) => {
         const selectedValue = Array.isArray( value ) ? value[ 0 ] : value;
+        if ( ! selectedValue ) {
+            return;
+        }
         const newFiles = [ ...files ];
src/dashboard/form-manager/components/AttributeVariationEdit.tsx-15-36 (1)

15-36: Use the guarded options variable instead of spreading field.elements directly.

field.elements is optional in FieldConfig but is spread without a guard at line 25. Since line 15 already safely guards it with const options = field.elements || [], use that variable instead. Update the dependency array accordingly.

🔧 Suggested fix
     const addOptions = useMemo( () => {
-        const options = [
+        const optionsList = [
             { label: __( 'Custom Attribute', 'dokan-lite' ), value: '' },
-            ...field.elements,
+            ...options,
         ];
-        return options.filter( ( opt ) => {
+        return optionsList.filter( ( opt ) => {
             // Exclude already added attributes
             return ! attributes.some( ( attr ) => {
                 if ( attr.is_taxonomy ) {
                     return Number( attr.id ) === Number( opt.value );
                 }
                 return false;
             } );
         } );
-    }, [ field.elements, attributes ] );
+    }, [ options, attributes ] );
src/dashboard/form-manager/components/SelectEdit.tsx-75-101 (1)

75-101: Normalize selected-value comparisons to avoid missing selections when value types differ.

The Option type allows both string and number values, but the selection logic uses strict equality without type coercion. If the backend returns IDs as strings while options contain numbers (or vice versa), comparisons on lines 94 and 100 will silently fail and selections won't render.

💡 Proposed fix
-            return options.filter( ( option: Option ) =>
-                currentValue.includes( option.value )
-            );
+            const normalized = currentValue.map( ( v ) => String( v ) );
+            return options.filter( ( option: Option ) =>
+                normalized.includes( String( option.value ) )
+            );
...
-        return (
-            options.find( ( option ) => option.value === currentValue ) || null
-        );
+        return (
+            options.find(
+                ( option ) => String( option.value ) === String( currentValue )
+            ) || null
+        );
src/dashboard/form-manager/App.tsx-40-45 (1)

40-45: Add noopener to the external product link.

While rel="noreferrer" provides tabnabbing protection implicitly, explicitly including noopener noreferrer is the recommended best practice for clarity and legacy browser compatibility.

🔒 Proposed fix
-                                    rel="noreferrer"
+                                    rel="noopener noreferrer"
src/dashboard/form-manager/factories/handlers.ts-111-116 (1)

111-116: Non-null assertion may cause runtime error.

The field!.id on line 113 uses a non-null assertion, but field is an optional parameter (per FieldHandler type). If selectHandler is ever called without a field argument, this will throw a TypeError.

Proposed fix
     // async select fields
     const asyncSelectFields = [ 'upsell_ids', 'cross_sell_ids', 'children' ];
-    if ( asyncSelectFields.includes( field!.id ) ) {
+    if ( field?.id && asyncSelectFields.includes( field.id ) ) {
         config.Edit = AsyncSelectEdit;
     }
src/dashboard/form-manager/utils.tsx-59-72 (1)

59-72: Asymmetric value normalization may cause unexpected behavior.

The normalization of 'on'/'yes'true and 'off'/'no'false is only applied to targetValue (the condition's value), but not to depValue (the actual data value). This means if data[depField] is 'yes' and the condition's value is true, the comparison 'yes' === true will fail.

Consider normalizing depValue as well:

Proposed fix
         let targetValue = value;
         if ( value === 'on' || value === 'yes' ) {
             targetValue = true;
         }
         if ( value === 'off' || value === 'no' ) {
             targetValue = false;
         }

+        let normalizedDepValue = depValue;
+        if ( depValue === 'on' || depValue === 'yes' ) {
+            normalizedDepValue = true;
+        }
+        if ( depValue === 'off' || depValue === 'no' ) {
+            normalizedDepValue = false;
+        }
+
         if ( operator === 'equal' ) {
-            return depValue === targetValue;
+            return normalizedDepValue === targetValue;
         }
         if ( operator === 'not_equal' ) {
-            return depValue !== targetValue;
+            return normalizedDepValue !== targetValue;
         }
🧹 Nitpick comments (22)
src/components/Select.tsx (2)

13-17: Consider consolidating duplicate DefaultOption type.

This type is defined identically in AsyncSelect.tsx. Extracting it to a shared types file (e.g., @src/components/select/types.ts) would prevent drift and improve maintainability.


54-64: Consider reducing as any escape hatches.

The combination of @ts-ignore (line 55), MultiValue as any (line 61), and props.components as any (line 63) suggests underlying type compatibility issues. While functional, this silences type checking and could mask runtime errors.

If the issue stems from react-select's complex generics, consider defining a more targeted type assertion:

type ComponentOverrides = Partial<SelectComponents<Option, IsMulti, GroupBase<Option>>>;

Then cast to that instead of any. Alternatively, if this is a known library limitation, a brief comment explaining why the casts are necessary would help future maintainers.

src/components/TreeSelectPicker.tsx (1)

19-25: Consider strengthening the onChange type.

The any type on the onChange callback reduces type safety. Since the component always passes a string[], the signature could be more explicit.

♻️ Suggested improvement
 type TreeSelectPickerProps = {
     options: Option[];
     initialValues: string[];
     multiple?: boolean;
-    onChange: ( value: any ) => void;
+    onChange: ( value: string[] ) => void;
     placeholder?: string;
 };
src/dashboard/form-manager/index.scss (1)

20-23: Avoid masking Gutenberg validation/focus styles with !important.

Overriding .components-input-control__backdrop with !important can suppress built‑in error/focus visuals. If those states are still required, prefer higher specificity without !important or add explicit state overrides.

includes/ProductForm/Component.php (1)

52-57: Use the $id parameter in the base constructor.
It’s currently unused; setting it here avoids missed initialization in future subclasses.

♻️ Suggested change
-    public function __construct( string $id, array $args = [] ) {
+    public function __construct( string $id, array $args = [] ) {
+        $this->set_id( $id );
         foreach ( $args as $key => $value ) {
             if ( method_exists( $this, "set_{$key}" ) && null !== $value ) {
                 $this->{"set_{$key}"}( $value );
             }
         }
     }
includes/Dashboard/Templates/Products.php (2)

206-231: Unused $post parameter in hook callbacks.

The $post parameter is unused in load_download_virtual_template, load_inventory_template, load_downloadable_template, and load_others_template. Since these are hook callbacks and the signature is defined externally, consider prefixing with underscore to signal intentional non-use.

Suggested change
-    public static function load_download_virtual_template( $post, $post_id ) {
+    public static function load_download_virtual_template( $_post, $post_id ) {

Apply similarly to lines 243, 274, and 310.


436-436: Unused loop variable $field_id.

The $field_id variable from the foreach loop is never used. Use underscore prefix to indicate intentional non-use.

Suggested change
-        foreach ( ProductFormFactory::get_fields() as $field_id => $field ) {
+        foreach ( ProductFormFactory::get_fields() as $_field_id => $field ) {
src/dashboard/form-manager/components/AsyncSelectEdit.tsx (1)

4-17: Add proper TypeScript types instead of any.

Using any defeats TypeScript's type safety benefits. Define an interface for the props and field shape.

Suggested typing
+interface AsyncSelectField {
+    id: string;
+    label?: string;
+    error?: string;
+    multiple?: boolean;
+    placeholder?: string;
+}
+
+interface AsyncSelectEditProps {
+    data: Record< string, unknown >;
+    field: AsyncSelectField;
+    onChange: ( value: Record< string, unknown > ) => void;
+}
+
-const AsyncSelectEdit = ( { data, field, onChange }: any ) => {
+const AsyncSelectEdit = ( { data, field, onChange }: AsyncSelectEditProps ) => {
     return (
         <CustomField label={ field.label } error={ field.error }>
             <ProductAsyncSelect
                 value={ data[ field.id ] }
-                onChange={ ( value: any ) => {
+                onChange={ ( value: unknown ) => {
                     onChange( { [ field.id ]: value } );
                 } }
src/dashboard/form-manager/components/CustomField.tsx (2)

14-14: Simplify redundant label rendering.

The expression { label && label } is redundant. Since label is already checked for truthiness, it will render correctly with just { label }.

Suggested change
-            { label && label }
+            { label }

1-40: Consider extracting the warning icon to a reusable component.

The inline SVG for the validation indicator could be extracted into a shared icon component for reusability across the form manager, especially if similar icons are used elsewhere.

src/dashboard/form-manager/components/GalleryImages.tsx (2)

7-8: Add proper TypeScript types and consider state synchronization.

Using any loses type safety. Additionally, initializing useState with field.value only captures the initial value—if field.value changes externally (e.g., from a parent reset), the local state won't update.

Suggested improvements
+interface ImageFile {
+    id: number;
+    url?: string;
+}
+
+interface GalleryField {
+    id: string;
+    label?: string;
+    error?: string;
+    value?: ImageFile[];
+}
+
+interface GalleryImagesProps {
+    field: GalleryField;
+    onChange: ( value: Record< string, number[] > ) => void;
+}
+
-const GalleryImages = ( { field, onChange }: any ) => {
-    const [ files, setFiles ] = useState( field.value || [] );
+const GalleryImages = ( { field, onChange }: GalleryImagesProps ) => {
+    const [ files, setFiles ] = useState< ImageFile[] >( field.value || [] );

If external updates to field.value should sync to local state, consider using useEffect to update when the prop changes, or lifting state entirely to the parent.


12-14: Simplify deduplication logic.

The deduplication can be simplified using a Map or Set for cleaner code.

Alternative approach
-        const uniqueValues = newValues.filter( ( img, index, self ) => {
-            return index === self.findIndex( ( t ) => t.id === img.id );
-        } );
+        const uniqueValues = [
+            ...new Map( newValues.map( ( img ) => [ img.id, img ] ) ).values(),
+        ];
src/dashboard/form-manager/components/variation/VariationForm.tsx (1)

36-38: Use a stable key for variations.

key={index} can cause incorrect reuse when variations are added or removed via the generateVariations and addVariation functions. Use the stable id property from the variation object instead.

♻️ Suggested change
-{ variations.map( ( variation, index: number ) => (
-    <VariationCard key={ index } variation={ variation } />
-) ) }
+{ variations.map( ( variation ) => (
+    <VariationCard key={ variation.id } variation={ variation } />
+) ) }
src/dashboard/form-manager/hooks/useVariationLayout.tsx (1)

101-102: Avoid as any type assertion; define a proper type for the layout structure.

The as any assertion bypasses TypeScript's type checking, reducing type safety. Consider defining and using a proper type for the form layout structure.

💡 Suggested approach
+type FormLayout = {
+    fields: Array<string | {
+        id: string;
+        layout: {
+            type: string;
+            alignment?: string;
+            styles?: Record<string, Record<string, string>>;
+            withHeader?: boolean;
+        };
+        children: Array<string | object>;
+    }>;
+};
+
 const useVariationLayout = () => {
-    const formLayouts = useMemo( () => {
+    const formLayouts = useMemo<FormLayout>( () => {
         // ...
-        return { fields } as any;
+        return { fields };
     }, [] );
src/dashboard/form-manager/components/FeatureImage.tsx (1)

8-9: Local state may become stale if field.value changes externally.

The image state is initialized once from field.value. If the parent updates field.value (e.g., form reset, external data load), the local state won't reflect the new value.

💡 Suggested fix using useEffect to sync state
+import { useState, useEffect } from '@wordpress/element';
 
 const FeatureImage = ( { field, onChange }: any ) => {
     const [ image, setImage ] = useState( field.value );
+
+    useEffect( () => {
+        setImage( field.value );
+    }, [ field.value ] );
src/dashboard/form-manager/components/variation/VariationInternalForm.tsx (1)

28-51: Avoid using array index as React key when items can change.

Using idx as the key can cause rendering issues if attributes are reordered, added, or removed. Use a stable identifier like attr.value or attr.label instead.

💡 Suggested fix
 { variation.attributes.map( ( attr, idx: number ) => {
     return (
-        <div key={ idx }>
+        <div key={ attr.value || attr.label }>
             <Select
src/dashboard/form-manager/factories/index.ts (1)

33-36: Consider adding explicit handlers for common text-based field types if they require special behavior.

Field types like 'text', 'email', 'url', and 'tel' are not in the handlers map and will fall back to defaultHandler. If these fields need custom validation, input restrictions, or specific UI behavior, consider adding explicit handlers for them. Otherwise, the current fallback approach is acceptable.

src/dashboard/form-manager/components/SelectEdit.tsx (1)

114-116: Keep multi-select menus open outside tree mode.

For non‑tree multi‑selects, the menu closes after each pick because closeMenuOnSelect is tied to isTreeMode. Using isMulti aligns with typical multi‑select UX.

♻️ Suggested tweak
-                closeMenuOnSelect={ ! isTreeMode }
+                closeMenuOnSelect={ ! isMulti }
src/dashboard/form-manager/types/index.ts (1)

3-11: Align DokanFormManagerData types with the localized payload.

is_new_product is localized as a boolean and product_id as an integer, but the types declare them as strings. Tightening these types reduces casting and avoids silent mismatches.

🔧 Suggested update
-    is_new_product: string;
-    product_id: string;
+    is_new_product: boolean;
+    product_id: number;
src/dashboard/form-manager/utils.tsx (1)

236-263: Mutating input parameter may cause unexpected side effects.

injectRemainingFields mutates the remainingFieldsBySection argument via delete remainingFieldsBySection[newItem.id] on line 252. This side effect could cause issues if the caller intends to reuse the object.

Consider cloning the object at the start or documenting this mutation behavior.

Proposed fix
 export const injectRemainingFields = (
     items: any[],
     remainingFieldsBySection: Record< string, string[] >
 ): any[] => {
+    const remaining = { ...remainingFieldsBySection };
     return items.map( ( item ) => {
         if ( typeof item === 'string' ) {
             return item;
         }
         const newItem = { ...item };

         // Append if section ID matches
-        if ( newItem.id && remainingFieldsBySection[ newItem.id ] ) {
+        if ( newItem.id && remaining[ newItem.id ] ) {
             newItem.children = [
                 ...( newItem.children || [] ),
-                ...remainingFieldsBySection[ newItem.id ],
+                ...remaining[ newItem.id ],
             ];
-            delete remainingFieldsBySection[ newItem.id ];
+            delete remaining[ newItem.id ];
         }

         if ( newItem.children ) {
             newItem.children = injectRemainingFields(
                 newItem.children,
-                remainingFieldsBySection
+                remaining
             );
         }
         return newItem;
     } );
 };
src/dashboard/form-manager/context/VariationContext.tsx (1)

172-172: Remove debug logging before production.

console.log statements should be removed or replaced with proper logging infrastructure for production code.

Proposed fix
-            console.log( 'Variation saved successfully:', response );
src/dashboard/form-manager/factories/handlers.ts (1)

126-134: Consider simplifying multiSelectHandler.

The config variable is returned immediately without modification. You could simplify this to a direct return like other handlers.

Proposed fix
-export const multiSelectHandler: FieldHandler = ( field ) => {
-    const config: any = {
-        type: 'array',
-        elements: getElementsFromOptions( field ),
-        Edit: SelectEdit,
-        multiple: true,
-    };
-    return config;
-};
+export const multiSelectHandler: FieldHandler = ( field ) => ( {
+    type: 'array',
+    elements: getElementsFromOptions( field ),
+    Edit: SelectEdit,
+    multiple: true,
+} );

Comment on lines +381 to +385
'dokan-product-form-manager' => [
'src' => DOKAN_PLUGIN_ASSEST . '/js/form-manager.css',
'deps' => [ 'wp-components' ],
'version' => filemtime( DOKAN_DIR . '/assets/js/form-manager.css' ),
],
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify location of form-manager.css
fd -a -g 'form-manager.css' .

# Also check the constant definition
echo "=== Searching for DOKAN_PLUGIN_ASSEST constant definition ==="
rg "DOKAN_PLUGIN_ASSEST" -n --type php

Repository: getdokan/dokan

Length of output: 18427


🏁 Script executed:

#!/bin/bash
# Check if assets directory structure exists and look for any form-manager file
echo "=== Assets directory structure ==="
find assets -type f -name "*form-manager*" 2>/dev/null | head -20

echo -e "\n=== Check /css directory ==="
ls -la assets/css/ 2>/dev/null | grep -i form

echo -e "\n=== Check /js directory ==="
ls -la assets/js/ 2>/dev/null | grep -i form

Repository: getdokan/dokan

Length of output: 151


🏁 Script executed:

#!/bin/bash
# Check for build configuration that might generate form-manager.css
echo "=== Looking for build scripts ==="
find . -maxdepth 2 -type f \( -name "package.json" -o -name "webpack.config.js" -o -name "gulpfile.js" -o -name "Makefile" -o -name ".webpack*" \) 2>/dev/null

echo -e "\n=== Check root for build/dist directories ==="
ls -la | grep -E "^d.*\b(build|dist|assets)\b"

echo -e "\n=== Check assets directory existence ==="
test -d assets && echo "assets/ exists" || echo "assets/ does NOT exist"

Repository: getdokan/dokan

Length of output: 290


🏁 Script executed:

#!/bin/bash
# List assets directory structure
echo "=== Assets directory structure ==="
find assets -type f | head -30

echo -e "\n=== Search for form-manager in entire repo ==="
find . -name "*form-manager*" 2>/dev/null

echo -e "\n=== Check webpack.config.js for form-manager ==="
rg "form-manager" webpack.config.js -A 3 -B 3

Repository: getdokan/dokan

Length of output: 1454


🏁 Script executed:

#!/bin/bash
# Check webpack config more thoroughly
echo "=== webpack.config.js content ==="
cat webpack.config.js | head -100

echo -e "\n=== Form-manager source files ==="
find ./src/dashboard/form-manager ./templates/products/form-manager -type f 2>/dev/null | head -20

Repository: getdokan/dokan

Length of output: 4739


🏁 Script executed:

#!/bin/bash
# Check webpack-entries.js for form-manager entry
echo "=== webpack-entries.js content ==="
cat webpack-entries.js | grep -A 5 -B 5 "form-manager"

echo -e "\n=== Check MiniCssExtractPlugin filename pattern ==="
cat webpack.config.js | grep -A 10 "MiniCssExtractPlugin"

Repository: getdokan/dokan

Length of output: 1583


🏁 Script executed:

#!/bin/bash
# Verify the exact registration in Assets.php around line 382
echo "=== Assets.php lines 380-385 ==="
sed -n '380,385p' includes/Assets.php

echo -e "\n=== Check if form-manager is a modules chunk ==="
grep -i "modules" webpack-entries.js | head -10

Repository: getdokan/dokan

Length of output: 426


🏁 Script executed:

#!/bin/bash
# Verify the webpack output pattern for form-manager
echo "=== Full webpack config MiniCssExtractPlugin section ==="
sed -n '/new MiniCssExtractPlugin/,/new VueLoaderPlugin/p' webpack.config.js

echo -e "\n=== Confirm form-manager is not a modules entry ==="
grep -E "form-manager.*modules|modules.*form-manager" webpack-entries.js || echo "No modules pattern found for form-manager"

Repository: getdokan/dokan

Length of output: 545


🏁 Script executed:

#!/bin/bash
# Verify webpack output directory configuration
echo "=== Webpack output path ==="
grep -A 5 "output:" webpack.config.js | head -10

echo -e "\n=== Check assets/css directory for form-manager.css ==="
ls -la assets/css/ | grep -i form || echo "form-manager.css NOT found in assets/css"

echo -e "\n=== Trace the MiniCssExtractPlugin logic ==="
echo "Based on webpack.config.js:"
echo "- Output path: ./assets/js"
echo "- form-manager chunk name: 'form-manager' (no /modules/ pattern)"
echo "- So filename template returns: '../css/[name].css'"
echo "- Resolved path: ./assets/js/../css/form-manager.css = ./assets/css/form-manager.css"
echo ""
echo "But Assets.php line 382 registers: DOKAN_PLUGIN_ASSEST . '/js/form-manager.css'"
echo "This points to: ./assets/js/form-manager.css"
echo ""
echo "MISMATCH: Webpack outputs to /css/, but Assets.php expects /js/"

Repository: getdokan/dokan

Length of output: 915


Fix the form-manager CSS asset path: use /css/ instead of /js/.

The webpack build outputs CSS to assets/css/form-manager.css (via MiniCssExtractPlugin), but line 382 registers it at assets/js/form-manager.css. This causes a 404 and leaves the form-manager UI unstyled. Change to:

'src'     => DOKAN_PLUGIN_ASSEST . '/css/form-manager.css',

Also update the filemtime() call to DOKAN_DIR . '/assets/css/form-manager.css'.

🤖 Prompt for AI Agents
In `@includes/Assets.php` around lines 381 - 385, Update the asset registration
for 'dokan-product-form-manager' to point to the correct CSS output path:
replace the src value using DOKAN_PLUGIN_ASSEST . '/js/form-manager.css' with
DOKAN_PLUGIN_ASSEST . '/css/form-manager.css' and update the filemtime()
argument from DOKAN_DIR . '/assets/js/form-manager.css' to DOKAN_DIR .
'/assets/css/form-manager.css' so the registered URL and version reflect the
actual built CSS file.

Comment on lines 459 to 463
// Skip shipping related fields if "This product requires shipping" field disabled.
$shipping_fields = [ 'weight', 'length', 'width', 'height', 'shipping_class_id' ];
if ( in_array( $field->get_id(), $shipping_fields, true ) && isset( $_POST['_disable_shipping'] ) && 'no' === $_POST['_disable_shipping'] ) {
continue;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n -C5 '_disable_shipping' --type=php

Repository: getdokan/dokan

Length of output: 1236


🏁 Script executed:

rg -n '_disable_shipping' --type-list | head -20

Repository: getdokan/dokan

Length of output: 447


🏁 Script executed:

rg -n '_disable_shipping' -i

Repository: getdokan/dokan

Length of output: 1083


🏁 Script executed:

rg -n 'disable.*shipping|shipping.*disable' --type=php | head -30

Repository: getdokan/dokan

Length of output: 737


🏁 Script executed:

rg -n 'DISABLE_SHIPPING_META' --type=php -B2 -A5

Repository: getdokan/dokan

Length of output: 1674


🏁 Script executed:

rg -n '_disable_shipping' --type=php -B5 -A5 | grep -E "(label|title|description|requires|disable|shipping)" -i

Repository: getdokan/dokan

Length of output: 540


Fix the inverted _disable_shipping condition.

The condition currently skips shipping fields when _disable_shipping is 'no', but this is backwards. When _disable_shipping === 'no', it means shipping is NOT disabled (the product requires shipping), so shipping fields should be shown, not skipped. Change the condition to 'yes' === $_POST['_disable_shipping'] so that fields are only skipped when shipping is actually disabled.

🤖 Prompt for AI Agents
In `@includes/Dashboard/Templates/Products.php` around lines 459 - 463, The
shipping-field skip logic is inverted: currently it checks if
$_POST['_disable_shipping'] === 'no' and continues, but it should only skip when
shipping is actually disabled. Update the condition in the block that defines
$shipping_fields and checks $field->get_id() to use 'yes' ===
$_POST['_disable_shipping'] (keeping the isset guard) so shipping fields are
only skipped when _disable_shipping is 'yes'.

Comment on lines +142 to +158
public function dokan_save_product_data() {
if ( ! isset( $_POST['_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_nonce'] ), 'form_manager' ) ) {
wp_send_json_error(
[
'type' => 'nonce',
'message' => __( 'Are you cheating?', 'dokan-lite' ),
]
);
}
try {
$product = dokan()->product->create( $_POST );
wp_send_json_success(
[
'product' => $product->get_data(),
'message' => __( 'Product saved successfully', 'dokan-lite' ),
]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "FormManager.php" -type f | head -20

Repository: getdokan/dokan

Length of output: 92


🏁 Script executed:

fd -t f "FormManager.php"

Repository: getdokan/dokan

Length of output: 90


🏁 Script executed:

git ls-files | grep -i formmanager

Repository: getdokan/dokan

Length of output: 177


🏁 Script executed:

git ls-files includes/Product/ | head -20

Repository: getdokan/dokan

Length of output: 286


🏁 Script executed:

git ls-files includes/ | grep -i product | head -30

Repository: getdokan/dokan

Length of output: 1291


🏁 Script executed:

cat -n includes/Product/FormManager.php | sed -n '130,170p'

Repository: getdokan/dokan

Length of output: 1356


🏁 Script executed:

stat includes/Product/FormManager.php 2>&1

Repository: getdokan/dokan

Length of output: 102


🏁 Script executed:

wc -l includes/Product/FormManager.php

Repository: getdokan/dokan

Length of output: 94


🏁 Script executed:

cat -n includes/Product/FormManager.php | sed -n '100,200p'

Repository: getdokan/dokan

Length of output: 3879


🏁 Script executed:

rg -A 20 "function create\(" includes/Product/ -t php

Repository: getdokan/dokan

Length of output: 1563


🏁 Script executed:

git ls-files includes/Product/

Repository: getdokan/dokan

Length of output: 286


🏁 Script executed:

rg "dokan_edit_product" --type php

Repository: getdokan/dokan

Length of output: 5193


🏁 Script executed:

rg "current_user_can" includes/Product/ --type php -A 3

Repository: getdokan/dokan

Length of output: 3136


🏁 Script executed:

rg "wp_verify_nonce.*form_manager" --type php -B 5 -A 15

Repository: getdokan/dokan

Length of output: 1434


🏁 Script executed:

rg "dokan_save_product" --type php -B 5 -A 20

Repository: getdokan/dokan

Length of output: 8652


🏁 Script executed:

cat includes/Product/FormManager.php

Repository: getdokan/dokan

Length of output: 13238


🏁 Script executed:

rg "add_action.*dokan_save_product" --type php -B 2 -A 2

Repository: getdokan/dokan

Length of output: 540


🏁 Script executed:

git ls-files | grep -i "\.php" | xargs grep -l "class.*Product" | grep -v node_modules | head -20

Repository: getdokan/dokan

Length of output: 945


🏁 Script executed:

rg "function create\(" includes/ --type php -B 3 -A 15 | grep -A 20 "Product"

Repository: getdokan/dokan

Length of output: 2423


🏁 Script executed:

git ls-files includes/ | head -50

Repository: getdokan/dokan

Length of output: 2432


🏁 Script executed:

fd -e php includes/ | xargs grep -l "->product->" | head -10

Repository: getdokan/dokan

Length of output: 510


🏁 Script executed:

rg "dokan\(\)->product->create" --type php -B 5 -A 30

Repository: getdokan/dokan

Length of output: 7758


🏁 Script executed:

rg "current_user_can" --type php | grep -i product | head -20

Repository: getdokan/dokan

Length of output: 2234


🏁 Script executed:

rg "wp_send_json_error.*permission" --type php -B 10 -A 5 | head -50

Repository: getdokan/dokan

Length of output: 2360


🏁 Script executed:

rg "wp_verify_nonce" --type php -B 3 -A 10 | head -100

Repository: getdokan/dokan

Length of output: 6998


🏁 Script executed:

rg "add_action.*wp_ajax" --type php | head -20

Repository: getdokan/dokan

Length of output: 2343


🏁 Script executed:

rg "dokan_edit_product|dokan.*product.*cap" --type php

Repository: getdokan/dokan

Length of output: 5292


🏁 Script executed:

rg "capabilities|add_cap" --type php | grep -i product | head -15

Repository: getdokan/dokan

Length of output: 137


🏁 Script executed:

git ls-files | xargs grep -l "class.*Product.*Service\|class Product" | grep -v node_modules | head -20

Repository: getdokan/dokan

Length of output: 1038


🏁 Script executed:

git ls-files | grep -E "(Service|Product).*\.php$" | head -30

Repository: getdokan/dokan

Length of output: 1717


🏁 Script executed:

git ls-files includes/ | head -100

Repository: getdokan/dokan

Length of output: 4563


🏁 Script executed:

rg "dokan_save_product_data" --type php

Repository: getdokan/dokan

Length of output: 266


🏁 Script executed:

git ls-files "*.php" | xargs grep -l "dokan_save_product_data"

Repository: getdokan/dokan

Length of output: 90


🏁 Script executed:

find . -name "*.php" -type f | xargs grep -l "dokan_save_product_data"

Repository: getdokan/dokan

Length of output: 92


🏁 Script executed:

ls -la includes/ 2>/dev/null || ls -la . | head -20

Repository: getdokan/dokan

Length of output: 4211


🏁 Script executed:

git ls-files --others --exclude-standard --cached | head -100

Repository: getdokan/dokan

Length of output: 3431


🏁 Script executed:

git status | head -20

Repository: getdokan/dokan

Length of output: 257


🏁 Script executed:

git ls-files | grep Product | head -20

Repository: getdokan/dokan

Length of output: 932


🏁 Script executed:

git ls-files "*.php" | xargs grep -l "form_manager" 2>/dev/null | head -20

Repository: getdokan/dokan

Length of output: 251


🏁 Script executed:

rg "Are you cheating" --type php

Repository: getdokan/dokan

Length of output: 1665


🏁 Script executed:

git ls-files | xargs grep -r "Are you cheating" 2>/dev/null

Repository: getdokan/dokan

Length of output: 1718


🏁 Script executed:

rg "Product saved successfully" --type php

Repository: getdokan/dokan

Length of output: 161


🏁 Script executed:

git ls-files | wc -l

Repository: getdokan/dokan

Length of output: 62


🏁 Script executed:

git ls-files | grep -E "\.php$" | wc -l

Repository: getdokan/dokan

Length of output: 61


🏁 Script executed:

git ls-files | grep -E "\.php$" | head -50

Repository: getdokan/dokan

Length of output: 2349


🏁 Script executed:

git log --oneline | head -5

Repository: getdokan/dokan

Length of output: 118


🏁 Script executed:

git branch -a

Repository: getdokan/dokan

Length of output: 182


🏁 Script executed:

grep -r "dokan_save_product_data" . 2>/dev/null --include="*.php" || echo "Pattern not found in repository"

Repository: getdokan/dokan

Length of output: 270


🏁 Script executed:

grep -r "Are you cheating" . 2>/dev/null --include="*.php" | head -5

Repository: getdokan/dokan

Length of output: 676


🏁 Script executed:

find . -type d -name "Product" 2>/dev/null

Repository: getdokan/dokan

Length of output: 76


🏁 Script executed:

find . -type d -name "includes" 2>/dev/null

Repository: getdokan/dokan

Length of output: 68


🏁 Script executed:

ls -la . 2>&1 | head -30

Repository: getdokan/dokan

Length of output: 1912


🏁 Script executed:

stat includes/Product/FormManager.php 2>&1 && echo "File exists" || echo "File does not exist"

Repository: getdokan/dokan

Length of output: 120


🏁 Script executed:

find . -name "FormManager.php" -type f 2>&1

Repository: getdokan/dokan

Length of output: 92


🏁 Script executed:

git ls-files | tail -100

Repository: getdokan/dokan

Length of output: 3701


🏁 Script executed:

git log --all --full-history -- "includes/Product/FormManager.php" 2>&1 | head -20

Repository: getdokan/dokan

Length of output: 490


🏁 Script executed:

pwd && git remote -v

Repository: getdokan/dokan

Length of output: 193


🏁 Script executed:

git log --oneline | head -1

Repository: getdokan/dokan

Length of output: 118


🏁 Script executed:

cat includes/Product/Manager.php | head -200

Repository: getdokan/dokan

Length of output: 6764


🏁 Script executed:

wc -l includes/Product/Manager.php

Repository: getdokan/dokan

Length of output: 90


🏁 Script executed:

cat -n includes/Product/Manager.php | sed -n '1,150p'

Repository: getdokan/dokan

Length of output: 5829


🏁 Script executed:

cat -n includes/Product/Manager.php | sed -n '1,100p'

Repository: getdokan/dokan

Length of output: 3642


🏁 Script executed:

rg "current_user_can|dokan_edit_product" includes/Product/Manager.php -A 3 -B 3

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

rg "wp_ajax.*product" includes/Product/ -B 2 -A 10

Repository: getdokan/dokan

Length of output: 2697


🏁 Script executed:

rg "get_post_author|post_author" includes/Product/ -B 2 -A 2

Repository: getdokan/dokan

Length of output: 1895


🏁 Script executed:

cat -n includes/Product/FormManager.php | sed -n '70,110p'

Repository: getdokan/dokan

Length of output: 1671


🏁 Script executed:

cat -n includes/Product/FormManager.php | sed -n '140,180p'

Repository: getdokan/dokan

Length of output: 1530


🏁 Script executed:

rg "apply_filters.*product.*create|do_action.*product.*create" includes/Product/ -B 2 -A 5

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

rg "wp_ajax_" includes/Ajax.php | head -20

Repository: getdokan/dokan

Length of output: 1926


🏁 Script executed:

cat -n includes/Ajax.php | sed -n '1,100p'

Repository: getdokan/dokan

Length of output: 4818


🏁 Script executed:

rg "change_order_status" includes/Ajax.php -A 15 | head -30

Repository: getdokan/dokan

Length of output: 1605


🏁 Script executed:

rg "wp_ajax_nopriv_dokan_save_product_data|wp_ajax_dokan_save_product_data" includes/

Repository: getdokan/dokan

Length of output: 185


🏁 Script executed:

rg "dokan_edit_product" includes/Product/FormManager.php -B 5 -A 5

Repository: getdokan/dokan

Length of output: 911


🏁 Script executed:

cat -n includes/Product/FormManager.php | sed -n '68,132p'

Repository: getdokan/dokan

Length of output: 2774


Add explicit capability check before saving product data.

The nonce validates request origin, but this AJAX endpoint lacks authorization checks. Any logged-in user can currently call this endpoint to create or save products. Add a current_user_can( 'dokan_edit_product' ) check before processing the product creation.

Suggested fix
public function dokan_save_product_data() {
+   if ( ! current_user_can( 'dokan_edit_product' ) ) {
+       wp_send_json_error(
+           [
+               'type'    => 'permission',
+               'message' => __( 'You have no permission to perform this action', 'dokan-lite' ),
+           ],
+           403
+       );
+   }
    if ( ! isset( $_POST['_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_nonce'] ), 'form_manager' ) ) {
        wp_send_json_error(
            [
                'type'    => 'nonce',
                'message' => __( 'Are you cheating?', 'dokan-lite' ),
            ]
        );
    }
🤖 Prompt for AI Agents
In `@includes/Product/FormManager.php` around lines 142 - 158, Add an explicit
capability check in dokan_save_product_data to prevent unauthorized product
creation: after the nonce verification (in dokan_save_product_data) check
current_user_can('dokan_edit_product') and if it returns false call
wp_send_json_error with a suitable 'type' (e.g., 'permission') and message
(e.g., 'You are not allowed to perform this action') and exit; only proceed to
call dokan()->product->create($_POST) and wp_send_json_success when the
capability check passes.

Comment on lines 376 to 384
// Set featured image id
if ( ! empty( $args['featured_image_id'] ) ) {
$product->set_image_id( $args['featured_image_id'] );
if ( ! empty( $args[ FormElements::FEATURED_IMAGE_ID ] ) ) {
$product->set_image_id( $args[ FormElements::FEATURED_IMAGE_ID ] );
}

// Set gallery image ids
if ( ! empty( $args['gallery_image_ids'] ) ) {
$product->set_gallery_image_ids( $args['gallery_image_ids'] );
if ( ! empty( $args[ FormElements::GALLERY_IMAGE_IDS ] ) ) {
$product->set_gallery_image_ids( $args[ FormElements::GALLERY_IMAGE_IDS ] );
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Allow clearing featured/gallery images.
Using ! empty prevents clearing the featured image (0) or emptying the gallery. This can leave stale images.

🔧 Suggested fix
-        if ( ! empty( $args[ FormElements::FEATURED_IMAGE_ID ] ) ) {
-            $product->set_image_id( $args[ FormElements::FEATURED_IMAGE_ID ] );
-        }
+        if ( array_key_exists( FormElements::FEATURED_IMAGE_ID, $args ) ) {
+            $product->set_image_id( absint( $args[ FormElements::FEATURED_IMAGE_ID ] ) );
+        }

-        if ( ! empty( $args[ FormElements::GALLERY_IMAGE_IDS ] ) ) {
-            $product->set_gallery_image_ids( $args[ FormElements::GALLERY_IMAGE_IDS ] );
-        }
+        if ( array_key_exists( FormElements::GALLERY_IMAGE_IDS, $args ) ) {
+            $product->set_gallery_image_ids(
+                array_map( 'absint', (array) $args[ FormElements::GALLERY_IMAGE_IDS ] )
+            );
+        }
🤖 Prompt for AI Agents
In `@includes/Product/Manager.php` around lines 376 - 384, The current checks use
! empty on $args[FormElements::FEATURED_IMAGE_ID] and
$args[FormElements::GALLERY_IMAGE_IDS], which prevents clearing the featured
image by setting 0 or clearing the gallery by passing an empty array/string;
change the conditional to test presence (e.g.,
isset($args[FormElements::FEATURED_IMAGE_ID]) or array_key_exists for gallery)
so you always call $product->set_image_id(...) and
$product->set_gallery_image_ids(...) with the provided value (including 0 or
empty) to allow clearing images.

Comment on lines +185 to +221
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return '';
}

if ( ! $product instanceof WC_Product ) {
return '';
}

return $product->get_date_on_sale_from( 'edit' ) ? $product->get_date_on_sale_from( 'edit' )->date( 'Y-m-d' ) : '';
},
'dependency_condition' => [
'section' => 'general',
'field' => Elements::CREATE_SCHEDULE_FOR_DISCOUNT,
'operator' => 'equal',
'value' => 'on',
],
]
);

$section->add_field(
Elements::DATE_ON_SALE_TO, [
'title' => __( 'To', 'dokan-lite' ),
'field_type' => 'date',
'name' => '_sale_price_dates_to',
'placeholder' => 'YYYY-MM-DD',
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return '';
}

if ( ! $product instanceof WC_Product ) {
return '';
}

return $product->get_date_on_sale_to( 'edit' ) ? $product->get_date_on_sale_to( 'edit' )->date( 'Y-m-d' ) : '';
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sale date value callbacks discard provided input.
When $value is non-empty, these callbacks return '', which drops user-entered values (e.g., after a validation error). They should return $value.

🔧 Suggested fix
-                'value_callback'       => function ( $product, $value = '' ) {
-                    if ( '' !== $value ) {
-                        return '';
-                    }
+                'value_callback'       => function ( $product, $value = '' ) {
+                    if ( '' !== $value ) {
+                        return $value;
+                    }
                     if ( ! $product instanceof WC_Product ) {
                         return '';
                     }
-                'value_callback'       => function ( $product, $value = '' ) {
-                    if ( '' !== $value ) {
-                        return '';
-                    }
+                'value_callback'       => function ( $product, $value = '' ) {
+                    if ( '' !== $value ) {
+                        return $value;
+                    }
                     if ( ! $product instanceof WC_Product ) {
                         return '';
                     }
📝 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
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return '';
}
if ( ! $product instanceof WC_Product ) {
return '';
}
return $product->get_date_on_sale_from( 'edit' ) ? $product->get_date_on_sale_from( 'edit' )->date( 'Y-m-d' ) : '';
},
'dependency_condition' => [
'section' => 'general',
'field' => Elements::CREATE_SCHEDULE_FOR_DISCOUNT,
'operator' => 'equal',
'value' => 'on',
],
]
);
$section->add_field(
Elements::DATE_ON_SALE_TO, [
'title' => __( 'To', 'dokan-lite' ),
'field_type' => 'date',
'name' => '_sale_price_dates_to',
'placeholder' => 'YYYY-MM-DD',
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return '';
}
if ( ! $product instanceof WC_Product ) {
return '';
}
return $product->get_date_on_sale_to( 'edit' ) ? $product->get_date_on_sale_to( 'edit' )->date( 'Y-m-d' ) : '';
},
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return $value;
}
if ( ! $product instanceof WC_Product ) {
return '';
}
return $product->get_date_on_sale_from( 'edit' ) ? $product->get_date_on_sale_from( 'edit' )->date( 'Y-m-d' ) : '';
},
'dependency_condition' => [
'section' => 'general',
'field' => Elements::CREATE_SCHEDULE_FOR_DISCOUNT,
'operator' => 'equal',
'value' => 'on',
],
]
);
$section->add_field(
Elements::DATE_ON_SALE_TO, [
'title' => __( 'To', 'dokan-lite' ),
'field_type' => 'date',
'name' => '_sale_price_dates_to',
'placeholder' => 'YYYY-MM-DD',
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return $value;
}
if ( ! $product instanceof WC_Product ) {
return '';
}
return $product->get_date_on_sale_to( 'edit' ) ? $product->get_date_on_sale_to( 'edit' )->date( 'Y-m-d' ) : '';
},
🤖 Prompt for AI Agents
In `@includes/ProductForm/Init.php` around lines 185 - 221, The value_callback
closures for Elements::DATE_ON_SALE_FROM and Elements::DATE_ON_SALE_TO currently
return an empty string when $value is non-empty, which discards user input;
change each callback so that if ('' !== $value) it returns $value (not ''),
otherwise keep the existing logic that checks $product instanceof WC_Product and
returns the product date formatted via
get_date_on_sale_from('edit')->date('Y-m-d') or
get_date_on_sale_to('edit')->date('Y-m-d') as before.

Comment on lines +54 to +61
const attributeChangeHandler = ( selected: any ) => {
const values = selected.map( ( val: any ) => val.value );
onUpdate( {
...attr,
options: values,
terms: selected,
} );
};
Copy link
Contributor

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:

cat -n src/dashboard/form-manager/components/variation/AttributeCard.tsx | sed -n '40,75p'

Repository: getdokan/dokan

Length of output: 172


🌐 Web query:

react-select onChange null value behavior when cleared

💡 Result:

Short answer

  • For react-select (single select, isClearable), when the user clears the control the onChange handler is called with value === null. Handle that case in your onChange. [1][2][3]

Details / practical notes

  • Check for null before accessing properties (e.g. onChange = v => { if (!v) { /* cleared / } else { / v.value */ } }). Optional chaining (v?.value) avoids errors. [2][3]
  • If you want the placeholder to show when value is empty, pass undefined (not null) as the Select value/controlled prop. [4]

Sources

  • react-select / StackOverflow examples showing onChange receives null when cleared and recommended null-checks. [1][2][3][4]

Guard against null selections from react-select.

react-select emits null to the onChange handler when the control is cleared. Without a null check, calling .map() will throw "Cannot read property 'map' of null" and crash the form.

🐛 Proposed fix
-    const attributeChangeHandler = ( selected: any ) => {
-        const values = selected.map( ( val: any ) => val.value );
+    const attributeChangeHandler = ( selected: any ) => {
+        const safeSelected = Array.isArray( selected ) ? selected : [];
+        const values = safeSelected.map( ( val: any ) => val.value );
         onUpdate( {
             ...attr,
             options: values,
             terms: selected,
         } );
     };
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/components/variation/AttributeCard.tsx` around
lines 54 - 61, attributeChangeHandler can receive null from react-select when
cleared, causing selected.map to throw; update the handler in AttributeCard.tsx
(attributeChangeHandler / onUpdate / attr) to guard against null by computing
values as selected ? selected.map((val:any)=>val.value) : [] and passing terms
as selected || [] (or an empty array) into onUpdate so clearing the control
results in empty options/terms instead of a crash.

Comment on lines 45 to 83
<div className="border rounded bg-white shadow-sm overflow-hidden">
<div
role="button"
className="flex justify-between items-center p-3 bg-gray-50 border-b cursor-pointer select-none"
onClick={ fetchedVariationData }
>
<div className="font-semibold text-gray-700 text-sm">
# { variation.id }
</div>
<div className="flex items-center gap-3">
<span
role="button"
className="text-red-500 hover:text-red-700 text-xs font-medium"
>
{ __( 'Remove', 'dokan-lite' ) }
</span>

<span
className={ `transform transition-transform duration-200 ${
isExpanded ? 'rotate-180' : ''
}` }
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-4 w-4 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={ 2 }
d="M19 9l-7 7-7-7"
/>
</svg>
</span>
</div>
</div>
Copy link
Contributor

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:

cat -n src/dashboard/form-manager/components/variation/VariationCard.tsx | head -150

Repository: getdokan/dokan

Length of output: 4878


Make expand/remove controls keyboard-accessible and wire the remove action.

The expand control (div with role="button") lacks tabIndex and keyboard handlers, preventing keyboard users from expanding the card. The "Remove" control (span with role="button") is completely inert with no handler. Convert these to real <button> elements or add tabIndex={0} and onKeyDown handlers to support Enter/Space activation. Wire the "Remove" control to an actual delete action or remove it until implemented.

🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/components/variation/VariationCard.tsx` around
lines 45 - 83, The expand header (currently a div with role="button" using
fetchedVariationData and referencing isExpanded/variation.id) is not
keyboard-accessible and the "Remove" span (text-red-500...) has no action;
change both to interactive controls: replace the header div with a real button
(or add tabIndex={0} plus onKeyDown handling for Enter/Space) and ensure it
invokes fetchedVariationData on click/keyboard activation, and replace the
"Remove" span with a real button (or add tabIndex/onKeyDown) that calls the
actual delete handler (e.g. onRemoveVariation or removeVariation(variation.id))
or remove the UI until the handler exists; also add aria-labels for both
controls to improve accessibility.

Comment on lines 186 to 210
const generateVariations = async () => {
console.log( 'Generating variations...', productId );
// specific logic to generate variations
if (
confirm(
'Are you sure you want to generate variations? This will overwrite existing variations.'
)
) {
const data = {
action: 'dokan_link_all_variations',
post_id: productId,
security: dokan.link_variation_nonce,
};
const response = await jQuery.ajax( {
url: dokan.ajaxurl,
type: 'POST',
data,
} );
console.log( 'Variations generated successfully:', response );
toast( {
type: 'success',
title: response.data.message,
} );
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing error handling and non-internationalized confirmation message.

Several issues in generateVariations:

  1. Missing try-catch: The async function lacks error handling, which could result in unhandled promise rejections if the AJAX call fails.
  2. Hardcoded English string: The confirmation message on line 191 is not wrapped with __() for internationalization.
  3. Native confirm(): Consider using a modal component from dokan-ui for consistency with the rest of the UI.
Proposed fix for error handling and i18n
     const generateVariations = async () => {
-        console.log( 'Generating variations...', productId );
         // specific logic to generate variations
         if (
             confirm(
-                'Are you sure you want to generate variations? This will overwrite existing variations.'
+                __( 'Are you sure you want to generate variations? This will overwrite existing variations.', 'dokan-lite' )
             )
         ) {
-            const data = {
-                action: 'dokan_link_all_variations',
-                post_id: productId,
-                security: dokan.link_variation_nonce,
-            };
-            const response = await jQuery.ajax( {
-                url: dokan.ajaxurl,
-                type: 'POST',
-                data,
-            } );
-            console.log( 'Variations generated successfully:', response );
-            toast( {
-                type: 'success',
-                title: response.data.message,
-            } );
+            try {
+                const data = {
+                    action: 'dokan_link_all_variations',
+                    post_id: productId,
+                    security: dokan.link_variation_nonce,
+                };
+                const response = await jQuery.ajax( {
+                    url: dokan.ajaxurl,
+                    type: 'POST',
+                    data,
+                } );
+                toast( {
+                    type: 'success',
+                    title: response.data.message,
+                } );
+            } catch ( error ) {
+                toast( {
+                    type: 'error',
+                    title: __( 'Error generating variations', 'dokan-lite' ),
+                } );
+            }
         }
     };
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/context/VariationContext.tsx` around lines 186 -
210, The generateVariations async function needs robust error handling and
i18n/UI consistency: wrap the confirmation text with the translation helper
(e.g. __('Are you sure you want to generate variations? This will overwrite
existing variations.')) and replace the native confirm() with the dokan-ui
confirmation modal used elsewhere; surround the AJAX call (jQuery.ajax) in a
try-catch, log errors, and call toast({type: 'error', title: error.message ||
__('Failed to generate variations')}) on failure; ensure success still shows the
success toast using response.data.message and preserve the existing console logs
if desired.

Comment on lines +33 to +36
const DefaultHandler: FieldDataHandler = {
canHandle: () => true,
getValue: ( field ) => field.value || '',
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Default handler drops valid falsy values (0/false).

field.value || '' will convert 0/false to '', losing legitimate values (e.g., zero price, boolean false). Use nullish coalescing instead.

🐛 Proposed fix
-    getValue: ( field ) => field.value || '',
+    getValue: ( field ) => field.value ?? '',
📝 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
const DefaultHandler: FieldDataHandler = {
canHandle: () => true,
getValue: ( field ) => field.value || '',
};
const DefaultHandler: FieldDataHandler = {
canHandle: () => true,
getValue: ( field ) => field.value ?? '',
};
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/factories/form-data.ts` around lines 33 - 36,
DefaultHandler's getValue currently uses a logical OR (field.value || '') which
incorrectly turns valid falsy values like 0 or false into an empty string;
change the implementation of DefaultHandler.getValue to use nullish coalescing
(use field.value ?? '') so only null or undefined become '' while preserving
0/false values.

Comment on lines +12 to +15
defined( 'ABSPATH' ) || exit;

?>
<input type="hidden" name="dokan_product_id" id="dokan_product_id" value="<?php echo esc_attr( $product->get_id() ); ?>" />
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against missing product instance.

If this template is rendered when $product is not set (e.g., create flow), $product->get_id() will fatal. Consider a safe fallback.

🐛 Proposed fix
-?>
-<input type="hidden" name="dokan_product_id" id="dokan_product_id" value="<?php echo esc_attr( $product->get_id() ); ?>" />
+<?php
+$product_id = $product ? $product->get_id() : 0;
+?>
+<input type="hidden" name="dokan_product_id" id="dokan_product_id" value="<?php echo esc_attr( $product_id ); ?>" />
📝 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
defined( 'ABSPATH' ) || exit;
?>
<input type="hidden" name="dokan_product_id" id="dokan_product_id" value="<?php echo esc_attr( $product->get_id() ); ?>" />
defined( 'ABSPATH' ) || exit;
<?php
$product_id = $product ? $product->get_id() : 0;
?>
<input type="hidden" name="dokan_product_id" id="dokan_product_id" value="<?php echo esc_attr( $product_id ); ?>" />
🤖 Prompt for AI Agents
In `@templates/products/form-manager/form-content.php` around lines 12 - 15, The
template directly calls $product->get_id() which will fatally error if $product
is not set; update the template to first check that $product is defined and is
an object with get_id (e.g., isset($product) && is_object($product) &&
method_exists($product, 'get_id')), and if that check fails output a safe
fallback (empty string or 0) for the hidden input value so the template renders
in create flows without throwing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@includes/Product/FormManager.php`:
- Around line 386-398: The code uses get_terms() and only checks truthiness of
$product_tags; update the logic to check for WP_Error like get_products_brands()
does: after calling get_terms($drop_down_tags) test is_wp_error($product_tags)
and return an empty array (or handle the error) before iterating over
$product_tags, ensuring you still build $data from valid term objects
($term->term_id, ->slug, ->name).
- Around line 260-270: The code calls $parent_product =
wc_get_product($product_id) and immediately uses
$parent_product->get_attributes(), but wc_get_product() can return false; add a
falsy check for $parent_product before the foreach over
$parent_product->get_attributes('edit') (for example, if (!$parent_product) {
continue; } or return/throw depending on context) so you only call
get_attributes() on a valid \WC_Product instance; update the loop around
$variations and any downstream logic (e.g., references to $parent_product,
get_attributes, or $attribute) to handle the early-skip case.
- Around line 83-106: After retrieving the product with
wc_get_product($product_id) add a verification that the current user actually
owns the product using dokan_is_product_author($product_id,
get_current_user_id()); if that returns false render the same
dokan_get_template_part('global/dokan-error', ...) (use a message like "You do
not have permission to edit this product") and return early; this ensures
existing-product access is blocked when dokan_is_product_author fails even if
dokan_edit_product capability is present.
♻️ Duplicate comments (1)
includes/Product/FormManager.php (1)

140-165: Add capability check before processing product data.

This issue was previously flagged: the AJAX handler validates the nonce but lacks an authorization check. Any logged-in WordPress user (not just vendors with dokan_edit_product capability) can call this endpoint.

Additionally, for product updates, ownership verification should be added to ensure vendors can only modify their own products.

Suggested fix
     public function dokan_save_product_data() {
+        if ( ! current_user_can( 'dokan_edit_product' ) ) {
+            wp_send_json_error(
+                [
+                    'type'    => 'permission',
+                    'message' => __( 'You do not have permission to perform this action', 'dokan-lite' ),
+                ],
+                403
+            );
+        }
+
         if ( ! isset( $_POST['_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_nonce'] ), 'form_manager' ) ) {
             wp_send_json_error(
                 [
                     'type'    => 'nonce',
                     'message' => __( 'Are you cheating?', 'dokan-lite' ),
                 ]
             );
         }
+
+        // Verify ownership for existing products
+        $product_id = isset( $_POST['ID'] ) ? absint( $_POST['ID'] ) : 0;
+        if ( $product_id ) {
+            $product = wc_get_product( $product_id );
+            if ( $product && (int) $product->get_meta( '_dokan_vendor_id', true ) !== dokan_get_current_user_id() ) {
+                wp_send_json_error(
+                    [
+                        'type'    => 'permission',
+                        'message' => __( 'You do not have permission to edit this product', 'dokan-lite' ),
+                    ],
+                    403
+                );
+            }
+        }
+
         try {
🧹 Nitpick comments (1)
includes/Product/Hooks.php (1)

387-394: Unused $product parameter is acceptable for filter callback signature.

The static analysis tool flags $product as unused. This is expected since the filter dokan_product_edit_meta_data passes three arguments, and the callback must accept all of them even if not all are used. The implementation correctly handles the early return for non-new products.

Consider adding a brief inline comment or suppression annotation if this triggers CI warnings:

Optional: Suppress PHPMD warning
-    public function set_new_product_email_status( array $meta_data, WC_Product $product, bool $is_new_product ) {
+    public function set_new_product_email_status( array $meta_data, WC_Product $product, bool $is_new_product ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed -- $product required by filter signature

Comment on lines +83 to +106
$product_id = isset( $_GET['product_id'] ) ? intval( wp_unslash( $_GET['product_id'] ) ) : 0; //phpcs:ignore
$new_product = false;

if ( ! $product_id ) {
// this is `add new` product page
$product = new WC_Product_Simple();
$product->set_status( 'auto-draft' );
$product->set_name( '' );
$product->save();
$new_product = true;
$product_id = $product->get_id();
}
$product = wc_get_product( $product_id );

if ( ! $product ) {
dokan_get_template_part(
'global/dokan-error', '', [
'deleted' => false,
'message' => __( 'Product not found', 'dokan-lite' ),
]
);

return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for how product ownership is verified in the codebase
rg -n "dokan_vendor_id|get_post_field.*post_author|dokan_is_product_author" --type php -C 3

Repository: getdokan/dokan

Length of output: 29952


🏁 Script executed:

#!/bin/bash
# Also look for broader ownership/authorization patterns
rg -n "product.*author|vendor.*product|product.*permission" --type php -C 2 | head -100

Repository: getdokan/dokan

Length of output: 8373


🏁 Script executed:

#!/bin/bash
# Get the full FormManager.php file to see complete context
cat -n includes/Product/FormManager.php | head -150

Repository: getdokan/dokan

Length of output: 5710


Add product ownership verification for existing products.

The method checks for the dokan_edit_product capability but doesn't verify that the current user owns the product when editing an existing one. A vendor could access another vendor's product by passing a different product_id parameter. Use dokan_is_product_author() to validate ownership.

Suggested fix
         $product = wc_get_product( $product_id );

         if ( ! $product ) {
             dokan_get_template_part(
                 'global/dokan-error', '', [
                     'deleted' => false,
                     'message' => __( 'Product not found', 'dokan-lite' ),
                 ]
             );

             return;
         }
+
+        // Verify current user owns the product
+        if ( ! $new_product && ! dokan_is_product_author( $product_id ) ) {
+            dokan_get_template_part(
+                'global/dokan-error', '', [
+                    'deleted' => false,
+                    'message' => __( 'You do not have permission to edit this product', 'dokan-lite' ),
+                ]
+            );
+
+            return;
+        }
🤖 Prompt for AI Agents
In `@includes/Product/FormManager.php` around lines 83 - 106, After retrieving the
product with wc_get_product($product_id) add a verification that the current
user actually owns the product using dokan_is_product_author($product_id,
get_current_user_id()); if that returns false render the same
dokan_get_template_part('global/dokan-error', ...) (use a message like "You do
not have permission to edit this product") and return early; this ensures
existing-product access is blocked when dokan_is_product_author fails even if
dokan_edit_product capability is present.

Comment on lines +260 to +270
$parent_product = wc_get_product( $product_id );

if ( $variations ) {
$iteration = 0;
foreach ( $variations as $variation ) {
/** @var \WC_Product_Variation $variation */
$variation_id = $variation->get_id();
$formatted_attrs = [];
$attribute_values = $variation->get_attributes( 'edit' );

foreach ( $parent_product->get_attributes( 'edit' ) as $attribute ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add null check for parent product before accessing its methods.

wc_get_product() can return false if the product doesn't exist. Accessing get_attributes() on a falsy value will cause a fatal error.

Suggested fix
         $variations_data = [];
         $parent_product  = wc_get_product( $product_id );

+        if ( ! $parent_product ) {
+            return $variations_data;
+        }
+
         if ( $variations ) {
             $iteration = 0;
             foreach ( $variations as $variation ) {
📝 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
$parent_product = wc_get_product( $product_id );
if ( $variations ) {
$iteration = 0;
foreach ( $variations as $variation ) {
/** @var \WC_Product_Variation $variation */
$variation_id = $variation->get_id();
$formatted_attrs = [];
$attribute_values = $variation->get_attributes( 'edit' );
foreach ( $parent_product->get_attributes( 'edit' ) as $attribute ) {
$parent_product = wc_get_product( $product_id );
if ( ! $parent_product ) {
return $variations_data;
}
if ( $variations ) {
$iteration = 0;
foreach ( $variations as $variation ) {
/** `@var` \WC_Product_Variation $variation */
$variation_id = $variation->get_id();
$formatted_attrs = [];
$attribute_values = $variation->get_attributes( 'edit' );
foreach ( $parent_product->get_attributes( 'edit' ) as $attribute ) {
🤖 Prompt for AI Agents
In `@includes/Product/FormManager.php` around lines 260 - 270, The code calls
$parent_product = wc_get_product($product_id) and immediately uses
$parent_product->get_attributes(), but wc_get_product() can return false; add a
falsy check for $parent_product before the foreach over
$parent_product->get_attributes('edit') (for example, if (!$parent_product) {
continue; } or return/throw depending on context) so you only call
get_attributes() on a valid \WC_Product instance; update the loop around
$variations and any downstream logic (e.g., references to $parent_product,
get_attributes, or $attribute) to handle the early-skip case.

Comment on lines +386 to +398
$data = [];
$product_tags = get_terms( $drop_down_tags );
if ( $product_tags ) {
foreach ( $product_tags as $term ) {
$data[] = [
'value' => $term->term_id,
'slug' => $term->slug,
'label' => $term->name,
];
}
}

return $data;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add WP_Error check for consistency with get_products_brands().

get_terms() can return a WP_Error object. Unlike get_products_brands() which properly checks for this, this method only checks for truthiness, which could lead to unexpected behavior if an error occurs.

Suggested fix
         $data = [];
         $product_tags = get_terms( $drop_down_tags );
-        if ( $product_tags ) {
+        if ( ! is_wp_error( $product_tags ) && ! empty( $product_tags ) ) {
             foreach ( $product_tags as $term ) {
                 $data[] = [
                     'value' => $term->term_id,
                     'slug'  => $term->slug,
                     'label' => $term->name,
                 ];
             }
         }

         return $data;
🤖 Prompt for AI Agents
In `@includes/Product/FormManager.php` around lines 386 - 398, The code uses
get_terms() and only checks truthiness of $product_tags; update the logic to
check for WP_Error like get_products_brands() does: after calling
get_terms($drop_down_tags) test is_wp_error($product_tags) and return an empty
array (or handle the error) before iterating over $product_tags, ensuring you
still build $data from valid term objects ($term->term_id, ->slug, ->name).

- Refactor field properties to use 'hidden_scope' instead of 'hide_on_product_types'.
- Implement AJAX handler 'dokan_get_product_variations' in FormManager.
- Update various components and hooks to support the new hidden scope logic.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@includes/Product/FormManager.php`:
- Around line 409-417: The dokan_get_product_variations AJAX handler lacks nonce
and authorization checks: add the same nonce verification used in
dokan_save_product_data (verify the 'form_manager' action nonce) and then check
that the current user is authorized to access/edit the target product (e.g.
verify current_user_can or the vendor ownership check used elsewhere before
calling get_product_variations). Return wp_send_json_error on failed nonce or
failed authorization and only call self::get_product_variations($product_id) and
wp_send_json_success when both checks pass.

In `@includes/ProductForm/Component.php`:
- Around line 366-377: The docblock for set_show_in_admin_settings is incorrect
— it says "Get show in admin settings" but the method is a setter; update the
docblock for the set_show_in_admin_settings method to describe it as a setter
(e.g., "Set show in admin settings"), ensure the `@since`, `@param` (bool
$show_in_admin_settings) description is accurate, and change the `@return` tag to
indicate void (or remove if already correct) so the comment matches the method's
purpose.

In `@includes/ProductForm/Init.php`:
- Around line 234-236: The category error messages are reversed: when
ProductCategoryHelper::product_category_selection_is_single() returns true you
should use the singular message "Please select a category" and when it returns
false use "Please select at least one category!"; update the assignment to
$category_error_message so the ternary returns the singular string for the true
branch and the plural/at-least-one string for the false branch.
- Around line 165-177: The value_callback for CREATE_SCHEDULE_FOR_DISCOUNT
currently returns mixed types (timestamp/false vs 'on'/'off'); change it so both
branches return the same type (preferably the checkbox state 'on' or 'off').
Specifically, in the anonymous value_callback (the function using
dokan_current_datetime()->modify($value) and referencing
$product->get_date_on_sale_to()/get_date_on_sale_from()), replace the branch
that returns a timestamp or false with logic that returns 'on' when the modified
time is valid and 'off' when not, and keep the product-read branch returning
'on'/'off' to ensure consistent string return values.
- Around line 336-349: The closure used as 'value_callback' calls the
nonexistent method WC_Product::get_brand_ids(), causing a fatal error; update
the closure to call the correct provider method by passing the product ID
instead (use dokan()->product->get_brand_ids( $product->get_id() )) and ensure
you still return an empty array when $product is not a WC_Product or when $value
is non-empty; locate the anonymous function assigned to 'value_callback' in
includes/ProductForm/Init.php and replace the incorrect
$product->get_brand_ids() call with dokan()->product->get_brand_ids(
$product->get_id() ).

In `@src/dashboard/form-manager/App.tsx`:
- Around line 23-25: The isNewProduct boolean currently uses
Boolean(formData.is_new_product) which treats any non-empty string like "0" as
true; change the logic where isNewProduct is defined in App.tsx to parse
formData.is_new_product explicitly (e.g., compare to '1' or 'true' or coerce
with parseInt and check !== 0) so that "0" yields false and valid truthy string
values yield true; update usages of isNewProduct (and keep productUrl =
formData.view_product_url unchanged) so edit flows render correctly.

In `@src/dashboard/form-manager/types/index.ts`:
- Around line 3-11: Update the DokanFormManagerData interface to match the
localized payload: change is_new_product from string to boolean, change
product_id from string to number, and replace variations: any[] with variations:
VariationType[] (ensure VariationType is imported/defined in the same file or
visible to DokanFormManagerData).

In `@src/dashboard/form-manager/utils.tsx`:
- Around line 32-57: The validation currently treats any falsy value as empty
(so numeric 0 or "0" get rejected); in validateProductForm update the empty
check (around values[field.id]) to only consider undefined, null, empty strings
(after trimming) or empty arrays as missing — keep references to field.required,
field.visibility, checkDependency(field.dependency_condition, values), and
values[field.id] and add explicit checks like value === undefined || value ===
null || (typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0) before setting newErrors[field.id].
♻️ Duplicate comments (6)
includes/ProductForm/Init.php (1)

188-224: Sale date value callbacks discard provided input.

When $value is non-empty, these callbacks return '' instead of $value, which discards user-entered values. This should return $value when it's provided.

🔧 Suggested fix
             'value_callback'       => function ( $product, $value = '' ) {
                 if ( '' !== $value ) {
-                    return '';
+                    return $value;
                 }

                 if ( ! $product instanceof WC_Product ) {
                     return '';
                 }

                 return $product->get_date_on_sale_from( 'edit' ) ? $product->get_date_on_sale_from( 'edit' )->date( 'Y-m-d' ) : '';
             },

Apply the same fix to DATE_ON_SALE_TO value_callback at line 214.

includes/Product/FormManager.php (4)

96-107: Verify product ownership on edit.
Editing an existing product should confirm the current user owns it before rendering the form.

🔒 Suggested fix
         $product = wc_get_product( $product_id );

         if ( ! $product ) {
             dokan_get_template_part(
                 'global/dokan-error', '', [
                     'deleted' => false,
                     'message' => __( 'Product not found', 'dokan-lite' ),
                 ]
             );

             return;
         }
+
+        if ( ! $new_product && ! dokan_is_product_author( $product_id ) ) {
+            dokan_get_template_part(
+                'global/dokan-error', '', [
+                    'deleted' => false,
+                    'message' => __( 'You do not have permission to edit this product', 'dokan-lite' ),
+                ]
+            );
+            return;
+        }

141-156: Add capability guard to the save AJAX endpoint.
Nonce validation is not authorization; ensure the caller can edit products.

🔒 Suggested fix
     public function dokan_save_product_data() {
         if ( ! isset( $_POST['_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['_nonce'] ), 'form_manager' ) ) {
             wp_send_json_error(
                 [
                     'type'    => 'nonce',
                     'message' => __( 'Are you cheating?', 'dokan-lite' ),
                 ]
             );
         }
+        if ( ! current_user_can( 'dokan_edit_product' ) ) {
+            wp_send_json_error(
+                [
+                    'type'    => 'permission',
+                    'message' => __( 'You have no permission to perform this action', 'dokan-lite' ),
+                ],
+                403
+            );
+        }
         try {
             $product = dokan()->product->create( $_POST );
             wp_send_json_success(
                 [
                     'product' => $product->get_data(),
                     'message'    => __( 'Product saved successfully', 'dokan-lite' ),
                 ]
             );

260-272: Guard against missing parent product before reading attributes.
wc_get_product() can return false; accessing attributes would fatal.

🛡️ Suggested fix
         $variations_data = [];
         $parent_product  = wc_get_product( $product_id );
 
+        if ( ! $parent_product ) {
+            return $variations_data;
+        }
+
         if ( $variations ) {
             $iteration = 0;
             foreach ( $variations as $variation ) {
                 /** `@var` \WC_Product_Variation $variation */
                 $variation_id     = $variation->get_id();

387-395: Handle WP_Error from get_terms().

✅ Suggested fix
         $data = [];
         $product_tags = get_terms( $drop_down_tags );
-        if ( $product_tags ) {
+        if ( ! is_wp_error( $product_tags ) && ! empty( $product_tags ) ) {
             foreach ( $product_tags as $term ) {
                 $data[] = [
                     'value' => $term->term_id,
                     'slug'  => $term->slug,
src/dashboard/form-manager/components/variation/VariationCard.tsx (1)

46-65: Make expand/remove controls keyboard-accessible.

The header and “Remove” controls are not keyboard operable (no tabIndex/onKeyDown, and span with role="button"). This blocks keyboard-only users.

Proposed fix
-            <div
-                role="button"
-                className="flex justify-between items-center p-3 bg-gray-50 border-b cursor-pointer select-none"
-                onClick={ fetchedVariationData }
-            >
+            <div
+                role="button"
+                tabIndex={ 0 }
+                className="flex justify-between items-center p-3 bg-gray-50 border-b cursor-pointer select-none"
+                onClick={ fetchedVariationData }
+                onKeyDown={ ( e ) => {
+                    if ( e.key === 'Enter' || e.key === ' ' ) {
+                        e.preventDefault();
+                        fetchedVariationData();
+                    }
+                } }
+                aria-expanded={ isExpanded }
+            >
...
-                    <span
-                        role="button"
-                        onClick={ ( e ) => {
-                            e.stopPropagation();
-                            e.preventDefault();
-                            removeVariation( variation );
-                        } }
-                        className="text-red-500 hover:text-red-700 text-xs font-medium"
-                    >
+                    <button
+                        type="button"
+                        onClick={ ( e ) => {
+                            e.stopPropagation();
+                            e.preventDefault();
+                            removeVariation( variation );
+                        } }
+                        className="text-red-500 hover:text-red-700 text-xs font-medium"
+                        aria-label={ __( 'Remove variation', 'dokan-lite' ) }
+                    >
                         { __( 'Remove', 'dokan-lite' ) }
-                    </span>
+                    </button>
🧹 Nitpick comments (4)
includes/ProductForm/Init.php (1)

817-825: Consider unique section order values.

The "others" section has order => 30, which is the same as the "downloadable" section (line 680). This could result in non-deterministic ordering between these sections depending on the sort implementation.

♻️ Suggested fix
         $section = Factory::add_section(
             'others',
             [
                 'title'       => __( 'Other Options', 'dokan-lite' ),
                 'description' => __( 'Set your extra product options', 'dokan-lite' ),
-                'order'       => 30,
+                'order'       => 40,
             ]
         );
includes/ProductForm/Component.php (2)

53-59: Unused $id parameter in constructor.

The $id parameter is declared but not used within the constructor. Child classes (Field and Section) call $this->set_id($id) before invoking parent::__construct(), making this parameter redundant.

♻️ Suggested fix
-    public function __construct( string $id, array $args = [] ) {
+    public function __construct( array $args = [] ) {
         foreach ( $args as $key => $value ) {
             if ( method_exists( $this, "set_{$key}" ) && null !== $value ) {
                 $this->{"set_{$key}"}( $value );
             }
         }
     }

Note: This would require updating child class calls from parent::__construct( $id, $args ) to parent::__construct( $args ).


486-500: Consider making get_missing_arguments static or adjusting child class calls.

This method is non-static but relies on $this->required_fields. Child classes call it via self::get_missing_arguments($this->data), which works in an instance context but is unconventional. Consider either:

  1. Making the method static and passing $required_fields as a parameter, or
  2. Having child classes call it via $this->get_missing_arguments($this->data)
src/dashboard/form-manager/components/variation/VariationCard.tsx (1)

90-93: Avoid false ending up in the className.

When expanded, the template literal includes the string "false". Consider a ternary (or clsx) to keep className clean.

Proposed fix
-            <div
-                className={ `p-4 flex flex-col gap-4 bg-white border-t variation-form ${
-                    ! isExpanded && 'hidden'
-                }` }
-            >
+            <div
+                className={ `p-4 flex flex-col gap-4 bg-white border-t variation-form ${
+                    ! isExpanded ? 'hidden' : ''
+                }` }
+            >

Comment on lines +409 to +417
public function dokan_get_product_variations() {
if ( ! isset( $_REQUEST['product_id'] ) ) {// phpcs:ignore
wp_send_json_error( __( 'Product ID is required', 'dokan-lite' ) );
}

$product_id = intval( wp_unslash( $_REQUEST['product_id'] ) ); // phpcs:ignore
$variations = self::get_product_variations( $product_id );

wp_send_json_success( $variations );
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find and examine the dokan_get_product_variations method
fd "FormManager.php" --exec wc -l {} \;

Repository: getdokan/dokan

Length of output: 96


🏁 Script executed:

# Search for the method in the file
rg -n "dokan_get_product_variations" --type php

Repository: getdokan/dokan

Length of output: 288


🏁 Script executed:

# Check the AJAX hook registration
rg -n "wp_ajax.*dokan_get_product_variations|add_action.*dokan_get_product_variations" --type php

Repository: getdokan/dokan

Length of output: 198


🏁 Script executed:

# Look for similar AJAX methods in FormManager to understand security patterns
rg -n "wp_ajax|add_action.*wp_ajax" includes/Product/FormManager.php -A 3 --type php

Repository: getdokan/dokan

Length of output: 287


🏁 Script executed:

# Verify helper functions
rg -n "function dokan_is_product_author|function dokan_edit_product" --type php

Repository: getdokan/dokan

Length of output: 242


🏁 Script executed:

# View the actual implementation of dokan_get_product_variations
sed -n '409,420p' includes/Product/FormManager.php

Repository: getdokan/dokan

Length of output: 476


🏁 Script executed:

# Check another AJAX method for comparison (dokan_save_product_data)
rg -n "dokan_save_product_data" includes/Product/FormManager.php -A 20 --type php | head -30

Repository: getdokan/dokan

Length of output: 1205


🏁 Script executed:

# Search for dokan_edit_product capability usage
rg -n "dokan_edit_product" --type php

Repository: getdokan/dokan

Length of output: 5343


🏁 Script executed:

# Check for nonce verification patterns in FormManager
rg -n "wp_verify_nonce|wp_create_nonce" includes/Product/FormManager.php --type php

Repository: getdokan/dokan

Length of output: 258


Protect variation AJAX with nonce and authorization checks.
The endpoint currently allows any logged-in user to fetch variation data for arbitrary products. The similar dokan_save_product_data() method implements nonce verification for the 'form_manager' action but this method lacks both nonce and authorization checks.

🔒 Suggested fix
     public function dokan_get_product_variations() {
+        if ( ! isset( $_REQUEST['_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_REQUEST['_nonce'] ), 'form_manager' ) ) {// phpcs:ignore
+            wp_send_json_error(
+                [
+                    'type'    => 'nonce',
+                    'message' => __( 'Are you cheating?', 'dokan-lite' ),
+                ],
+                403
+            );
+        }
+        if ( ! current_user_can( 'dokan_edit_product' ) ) {
+            wp_send_json_error(
+                [
+                    'type'    => 'permission',
+                    'message' => __( 'You have no permission to perform this action', 'dokan-lite' ),
+                ],
+                403
+            );
+        }
         if ( ! isset( $_REQUEST['product_id'] ) ) {// phpcs:ignore
             wp_send_json_error( __( 'Product ID is required', 'dokan-lite' ) );
         }
 
         $product_id = intval( wp_unslash( $_REQUEST['product_id'] ) ); // phpcs:ignore
+        if ( ! dokan_is_product_author( $product_id ) ) {
+            wp_send_json_error(
+                [
+                    'type'    => 'permission',
+                    'message' => __( 'You do not have permission to view this product', 'dokan-lite' ),
+                ],
+                403
+            );
+        }
         $variations = self::get_product_variations( $product_id );
 
         wp_send_json_success( $variations );
     }
🤖 Prompt for AI Agents
In `@includes/Product/FormManager.php` around lines 409 - 417, The
dokan_get_product_variations AJAX handler lacks nonce and authorization checks:
add the same nonce verification used in dokan_save_product_data (verify the
'form_manager' action nonce) and then check that the current user is authorized
to access/edit the target product (e.g. verify current_user_can or the vendor
ownership check used elsewhere before calling get_product_variations). Return
wp_send_json_error on failed nonce or failed authorization and only call
self::get_product_variations($product_id) and wp_send_json_success when both
checks pass.

Comment on lines +366 to +377
/**
* Get show in admin settings
*
* @since DOKAN_SINCE
*
* @param bool $show_in_admin_settings
*
* @return void
*/
public function set_show_in_admin_settings( bool $show_in_admin_settings ): void {
$this->data['show_in_admin_settings'] = $show_in_admin_settings;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Documentation mismatch: doc says "Get" but method is a setter.

The docblock at line 366 says "Get show in admin settings" but the method set_show_in_admin_settings is a setter.

🔧 Suggested fix
     /**
-     * Get show in admin settings
+     * Set show in admin settings
      *
      * `@since` DOKAN_SINCE
      *
      * `@param` bool $show_in_admin_settings
      *
      * `@return` void
      */
     public function set_show_in_admin_settings( bool $show_in_admin_settings ): void {
📝 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
/**
* Get show in admin settings
*
* @since DOKAN_SINCE
*
* @param bool $show_in_admin_settings
*
* @return void
*/
public function set_show_in_admin_settings( bool $show_in_admin_settings ): void {
$this->data['show_in_admin_settings'] = $show_in_admin_settings;
}
/**
* Set show in admin settings
*
* `@since` DOKAN_SINCE
*
* `@param` bool $show_in_admin_settings
*
* `@return` void
*/
public function set_show_in_admin_settings( bool $show_in_admin_settings ): void {
$this->data['show_in_admin_settings'] = $show_in_admin_settings;
}
🤖 Prompt for AI Agents
In `@includes/ProductForm/Component.php` around lines 366 - 377, The docblock for
set_show_in_admin_settings is incorrect — it says "Get show in admin settings"
but the method is a setter; update the docblock for the
set_show_in_admin_settings method to describe it as a setter (e.g., "Set show in
admin settings"), ensure the `@since`, `@param` (bool $show_in_admin_settings)
description is accurate, and change the `@return` tag to indicate void (or remove
if already correct) so the comment matches the method's purpose.

Comment on lines +165 to +177
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
$time = dokan_current_datetime()->modify( $value );

return $time ? $time->getTimestamp() : false;
}

if ( ! $product instanceof WC_Product ) {
return false;
}

return ! empty( $product->get_date_on_sale_to() ?? $product->get_date_on_sale_from() ) ? 'on' : 'off';
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent return types in CREATE_SCHEDULE_FOR_DISCOUNT value_callback.

This callback returns different types depending on the code path:

  • When $value is non-empty: returns timestamp (int) or false
  • When reading from product: returns 'on' or 'off' (string)

This inconsistency could cause issues in form handling. Consider returning consistent types.

🔧 Suggested fix

If the intent is to return 'on'/'off' for the checkbox state:

             'value_callback'       => function ( $product, $value = '' ) {
                 if ( '' !== $value ) {
-                    $time = dokan_current_datetime()->modify( $value );
-
-                    return $time ? $time->getTimestamp() : false;
+                    return $value;
                 }

                 if ( ! $product instanceof WC_Product ) {
                     return false;
                 }

                 return ! empty( $product->get_date_on_sale_to() ?? $product->get_date_on_sale_from() ) ? 'on' : 'off';
             },
📝 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
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
$time = dokan_current_datetime()->modify( $value );
return $time ? $time->getTimestamp() : false;
}
if ( ! $product instanceof WC_Product ) {
return false;
}
return ! empty( $product->get_date_on_sale_to() ?? $product->get_date_on_sale_from() ) ? 'on' : 'off';
},
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return $value;
}
if ( ! $product instanceof WC_Product ) {
return false;
}
return ! empty( $product->get_date_on_sale_to() ?? $product->get_date_on_sale_from() ) ? 'on' : 'off';
},
🤖 Prompt for AI Agents
In `@includes/ProductForm/Init.php` around lines 165 - 177, The value_callback for
CREATE_SCHEDULE_FOR_DISCOUNT currently returns mixed types (timestamp/false vs
'on'/'off'); change it so both branches return the same type (preferably the
checkbox state 'on' or 'off'). Specifically, in the anonymous value_callback
(the function using dokan_current_datetime()->modify($value) and referencing
$product->get_date_on_sale_to()/get_date_on_sale_from()), replace the branch
that returns a timestamp or false with logic that returns 'on' when the modified
time is valid and 'off' when not, and keep the product-read branch returning
'on'/'off' to ensure consistent string return values.

Comment on lines +234 to +236
$category_error_message = ProductCategoryHelper::product_category_selection_is_single()
? __( 'Please select at least one category!', 'dokan-lite' )
: __( 'Please select a category', 'dokan-lite' );
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Category error messages appear swapped.

When single selection is enforced, "Please select a category" (singular) is more appropriate. When multiple selection is allowed, "Please select at least one category" makes more sense.

🔧 Suggested fix
-        $category_error_message = ProductCategoryHelper::product_category_selection_is_single()
-            ? __( 'Please select at least one category!', 'dokan-lite' )
-            : __( 'Please select a category', 'dokan-lite' );
+        $category_error_message = ProductCategoryHelper::product_category_selection_is_single()
+            ? __( 'Please select a category', 'dokan-lite' )
+            : __( 'Please select at least one category!', 'dokan-lite' );
📝 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
$category_error_message = ProductCategoryHelper::product_category_selection_is_single()
? __( 'Please select at least one category!', 'dokan-lite' )
: __( 'Please select a category', 'dokan-lite' );
$category_error_message = ProductCategoryHelper::product_category_selection_is_single()
? __( 'Please select a category', 'dokan-lite' )
: __( 'Please select at least one category!', 'dokan-lite' );
🤖 Prompt for AI Agents
In `@includes/ProductForm/Init.php` around lines 234 - 236, The category error
messages are reversed: when
ProductCategoryHelper::product_category_selection_is_single() returns true you
should use the singular message "Please select a category" and when it returns
false use "Please select at least one category!"; update the assignment to
$category_error_message so the ternary returns the singular string for the true
branch and the plural/at-least-one string for the false branch.

Comment on lines +336 to +349
'value_callback' => function ( $product, $value = '' ) {
if ( '' !== $value ) {
return $value;
}

if ( ! $product instanceof WC_Product ) {
return [];
}

return $product->get_brand_ids();
},
'options' => FormData::get_products_brands(),
]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for get_brand_ids definition
echo "=== Searching for get_brand_ids definition ==="
rg -n 'get_brand_ids' --type=php -B2 -A5

echo -e "\n=== Searching for WC_Product class definition/extension ==="
rg -n 'class.*WC_Product|get_brand_ids' --type=php | head -30

echo -e "\n=== Check if method is added via add_filter or similar ==="
rg -n 'add_filter.*get_brand_ids|register.*get_brand_ids' --type=php

echo -e "\n=== Check Init.php for any setup/setup hooks ==="
grep -n 'add_action\|add_filter' includes/ProductForm/Init.php | head -20

Repository: getdokan/dokan

Length of output: 2144


🏁 Script executed:

#!/bin/bash
# Check if WC_Product is extended via hooks to add get_brand_ids
echo "=== Check for hooks that extend WC_Product ==="
rg -n 'WC_Product.*get_brand_ids|add_method|register_rest_field' --type=php

# Check the full context of Init.php around line 345
echo -e "\n=== Full context of the problematic code in Init.php ==="
sed -n '320,360p' includes/ProductForm/Init.php

# See if there's a hook that adds methods to WC_Product dynamically
echo -e "\n=== Check for woocommerce_product_class or similar hooks ==="
rg -n 'woocommerce.*product.*class|WC_Product|add_filter.*product' includes/ProductForm/Init.php | head -15

# Check the Manager class to understand the relationship
echo -e "\n=== Check Manager class definition and how it's instantiated ==="
head -50 includes/Product/Manager.php

Repository: getdokan/dokan

Length of output: 4407


🏁 Script executed:

#!/bin/bash
# Search for any hook that might add methods to WC_Product
echo "=== Search for hooks related to WC_Product methods ==="
rg -n 'woocommerce_product_classes_include|WC_Product|add_method' --type=php | grep -i 'brands\|method\|add_filter'

# Check if there's a REST field registration or other registration
echo -e "\n=== Search for REST endpoint or field registration with brands ==="
rg -n 'register_rest_field|register_meta|rest_register' --type=php -A2 | grep -B2 -A2 'brand'

# Check if there's any trait or mixin that adds this method to WC_Product
echo -e "\n=== Search for trait files and brand-related code ==="
find . -name "*.php" -type f | xargs grep -l 'trait.*Brand\|function get_brand_ids' 2>/dev/null | head -10

# Search for instantiation of Manager in Init.php context
echo -e "\n=== Check if Manager is used or instantiated in Init.php ==="
rg -n 'Manager|get_brand_ids' includes/ProductForm/Init.php -B5 -A2

Repository: getdokan/dokan

Length of output: 822


🏁 Script executed:

#!/bin/bash
# Look for any custom WC_Product classes or extensions
echo "=== Search for custom WC_Product extensions ==="
rg -n 'class.*WC_Product|extends WC_Product' --type=php

# Check if get_brand_ids is added dynamically anywhere
echo -e "\n=== Search for add_method or method_exists in relation to WC_Product ==="
rg -n 'method_exists.*get_brand_ids|add_method.*brand' --type=php

# Let's look at what FormData (FormManager) actually does
echo -e "\n=== Check FormManager class for brand-related methods ==="
find . -name "FormManager.php" -type f | xargs cat 2>/dev/null | head -100

# Check if there's any hook that adds the method to WC_Product
echo -e "\n=== Search for 'woocommerce_product_object_updated_props' or similar hooks ==="
rg -n 'add_filter.*product|add_action.*product' includes/Product/ --type=php | grep -i 'brand\|method'

Repository: getdokan/dokan

Length of output: 4709


🏁 Script executed:

#!/bin/bash
# Check the Hooks.php file to see how brands are handled
echo "=== Check Hooks.php for brand-related code ==="
cat includes/Product/Hooks.php

# Also check if WC_Product gets methods added dynamically
echo -e "\n=== Search for any meta-based brand retrieval ==="
rg -n 'get_meta|get_term|wp_get_post_terms.*brand' includes/Product/ --type=php

Repository: getdokan/dokan

Length of output: 24737


🏁 Script executed:

#!/bin/bash
# Search for any traits or method additions to WC_Product
echo "=== Search for trait additions related to brands ==="
find . -name "*.php" -type f | xargs grep -l 'trait.*Brand'

# Check if there's any hook that modifies WC_Product class
echo -e "\n=== Search for WC_Product method additions via hooks ==="
rg -n 'add_filter.*woocommerce_product|call_user_func.*get_brand' --type=php

# Check if the ProductForm/Init.php is part of any extension system
echo -e "\n=== Check file structure around Init.php ==="
ls -la includes/ProductForm/

# Look at get_brands method in Manager to understand the pattern
echo -e "\n=== Check the get_brands method in Manager ==="
sed -n '915,930p' includes/Product/Manager.php

# Check for any method_exists check in the codebase
echo -e "\n=== Search for method_exists usage with WC_Product ==="
rg -n 'method_exists.*WC_Product|method_exists.*product' --type=php

Repository: getdokan/dokan

Length of output: 1706


Call to undefined method WC_Product::get_brand_ids()

Line 345 calls $product->get_brand_ids() on a WC_Product instance, but this method does not exist on WC_Product. The method is only defined in the Manager class and accepts a product ID, not a product object. This will cause a fatal error at runtime.

Use dokan()->product->get_brand_ids( $product->get_id() ) instead.

🤖 Prompt for AI Agents
In `@includes/ProductForm/Init.php` around lines 336 - 349, The closure used as
'value_callback' calls the nonexistent method WC_Product::get_brand_ids(),
causing a fatal error; update the closure to call the correct provider method by
passing the product ID instead (use dokan()->product->get_brand_ids(
$product->get_id() )) and ensure you still return an empty array when $product
is not a WC_Product or when $value is non-empty; locate the anonymous function
assigned to 'value_callback' in includes/ProductForm/Init.php and replace the
incorrect $product->get_brand_ids() call with dokan()->product->get_brand_ids(
$product->get_id() ).

Comment on lines +23 to +25
const productUrl = formData.view_product_url;
const isNewProduct = Boolean( formData.is_new_product );

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix isNewProduct when is_new_product is a string.

is_new_product is a string; Boolean("0") is true, so edit flows render as “New Product”. Parse it instead.

Proposed fix
-    const isNewProduct = Boolean( formData.is_new_product );
+    const isNewProduct = Number( formData.is_new_product ) === 1;
📝 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
const productUrl = formData.view_product_url;
const isNewProduct = Boolean( formData.is_new_product );
const productUrl = formData.view_product_url;
const isNewProduct = Number( formData.is_new_product ) === 1;
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/App.tsx` around lines 23 - 25, The isNewProduct
boolean currently uses Boolean(formData.is_new_product) which treats any
non-empty string like "0" as true; change the logic where isNewProduct is
defined in App.tsx to parse formData.is_new_product explicitly (e.g., compare to
'1' or 'true' or coerce with parseInt and check !== 0) so that "0" yields false
and valid truthy string values yield true; update usages of isNewProduct (and
keep productUrl = formData.view_product_url unchanged) so edit flows render
correctly.

Comment on lines +3 to +11
export interface DokanFormManagerData {
sections: Section[];
is_new_product: string;
product_id: string;
view_product_url: string;
form_manager_nonce: string;
vendor_earning: number;
variations: any[];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check the interface definition
cat -n src/dashboard/form-manager/types/index.ts

Repository: getdokan/dokan

Length of output: 2496


🏁 Script executed:

# Search for where is_new_product and product_id are assigned/used
rg "is_new_product|product_id" --type ts --type tsx -B 2 -A 2 | head -100

Repository: getdokan/dokan

Length of output: 85


🏁 Script executed:

# Search for VariationType definition
rg "type VariationType|interface VariationType" --type ts --type tsx

Repository: getdokan/dokan

Length of output: 85


🏁 Script executed:

# Search for localization or payload setup related to form manager
rg "localize|form_manager_data|DokanFormManagerData" --type ts --type tsx -B 2 -A 2 | head -100

Repository: getdokan/dokan

Length of output: 85


🏁 Script executed:

# Check how variations field is used
rg "variations" src/dashboard/form-manager --type ts --type tsx -B 1 -A 1

Repository: getdokan/dokan

Length of output: 85


🏁 Script executed:

# Search for where DokanFormManagerData is populated/localized
rg "is_new_product|product_id" --type ts -B 3 -A 3 | head -150

Repository: getdokan/dokan

Length of output: 8912


🏁 Script executed:

# Look for PHP files that might be localizing this data
find . -name "*.php" -type f | xargs grep -l "is_new_product\|product_id" | grep -i form | head -10

Repository: getdokan/dokan

Length of output: 175


🏁 Script executed:

# Check React/JS component that uses DokanFormManagerData
rg "DokanFormManagerData" --type ts -B 2 -A 5

Repository: getdokan/dokan

Length of output: 1546


🏁 Script executed:

# Look for where variations data is used
rg "variations:" --type ts -B 2 -A 3 | head -80

Repository: getdokan/dokan

Length of output: 5573


🏁 Script executed:

# Check FormManager.php for localization
cat -n ./includes/Product/FormManager.php | head -150

Repository: getdokan/dokan

Length of output: 5765


🏁 Script executed:

# Search for where is_new_product is set in PHP files
rg "is_new_product" --type php -B 2 -A 2

Repository: getdokan/dokan

Length of output: 4868


🏁 Script executed:

# Search for where product_id is set/localized in PHP
rg "product_id" ./includes/Product/FormManager.php -B 2 -A 2 | head -60

Repository: getdokan/dokan

Length of output: 1986


Fix mismatched types in the localized payload.
is_new_product and product_id are localized as boolean and integer respectively (see FormManager.php lines 125–126), but currently typed as string. Additionally, variations should use the VariationType type defined in the same file instead of any[].

✅ Suggested fix
 export interface DokanFormManagerData {
     sections: Section[];
-    is_new_product: string;
-    product_id: string;
+    is_new_product: boolean;
+    product_id: number;
     view_product_url: string;
     form_manager_nonce: string;
     vendor_earning: number;
-    variations: any[];
+    variations: VariationType[];
 }
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/types/index.ts` around lines 3 - 11, Update the
DokanFormManagerData interface to match the localized payload: change
is_new_product from string to boolean, change product_id from string to number,
and replace variations: any[] with variations: VariationType[] (ensure
VariationType is imported/defined in the same file or visible to
DokanFormManagerData).

Comment on lines +32 to +57
export const validateProductForm = (
sections: Section[],
values: Record< string, any >
): Record< string, string > => {
const newErrors: Record< string, string > = {};

sections.forEach( ( section ) => {
section.fields.forEach( ( field ) => {
if ( ! field.required ) {
return;
}
// Check visibility
if ( ! field.visibility ) {
return;
}
if ( ! checkDependency( field.dependency_condition, values ) ) {
return;
}

const value = values[ field.id ];
if ( ! value || ( Array.isArray( value ) && value.length === 0 ) ) {
newErrors[ field.id ] = __(
'Please fill out this field.',
'dokan-lite'
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid rejecting numeric 0 for required fields.
!value treats 0/"0" as empty, which can break required numeric fields (e.g., stock/price).

🐛 Suggested fix
             const value = values[ field.id ];
-            if ( ! value || ( Array.isArray( value ) && value.length === 0 ) ) {
+            const isEmpty =
+                value === undefined ||
+                value === null ||
+                value === '' ||
+                ( Array.isArray( value ) && value.length === 0 );
+            if ( isEmpty ) {
                 newErrors[ field.id ] = __(
                     'Please fill out this field.',
                     'dokan-lite'
                 );
             }
📝 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
export const validateProductForm = (
sections: Section[],
values: Record< string, any >
): Record< string, string > => {
const newErrors: Record< string, string > = {};
sections.forEach( ( section ) => {
section.fields.forEach( ( field ) => {
if ( ! field.required ) {
return;
}
// Check visibility
if ( ! field.visibility ) {
return;
}
if ( ! checkDependency( field.dependency_condition, values ) ) {
return;
}
const value = values[ field.id ];
if ( ! value || ( Array.isArray( value ) && value.length === 0 ) ) {
newErrors[ field.id ] = __(
'Please fill out this field.',
'dokan-lite'
);
}
export const validateProductForm = (
sections: Section[],
values: Record< string, any >
): Record< string, string > => {
const newErrors: Record< string, string > = {};
sections.forEach( ( section ) => {
section.fields.forEach( ( field ) => {
if ( ! field.required ) {
return;
}
// Check visibility
if ( ! field.visibility ) {
return;
}
if ( ! checkDependency( field.dependency_condition, values ) ) {
return;
}
const value = values[ field.id ];
const isEmpty =
value === undefined ||
value === null ||
value === '' ||
( Array.isArray( value ) && value.length === 0 );
if ( isEmpty ) {
newErrors[ field.id ] = __(
'Please fill out this field.',
'dokan-lite'
);
}
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/utils.tsx` around lines 32 - 57, The validation
currently treats any falsy value as empty (so numeric 0 or "0" get rejected); in
validateProductForm update the empty check (around values[field.id]) to only
consider undefined, null, empty strings (after trimming) or empty arrays as
missing — keep references to field.required, field.visibility,
checkDependency(field.dependency_condition, values), and values[field.id] and
add explicit checks like value === undefined || value === null || (typeof value
=== 'string' && value.trim() === '') || (Array.isArray(value) && value.length
=== 0) before setting newErrors[field.id].

Prefixed shipping-related meta keys with underscores in Elements.php and updated all corresponding references in PHP and TypeScript files. Also improved robustness in Field.php by checking for method existence before calling, and refactored useLayouts.tsx to remove an unused processLayout function.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@includes/Product/Manager.php`:
- Around line 79-96: In the create() method, guard against wc_get_product(...)
returning false/null before using $product: after the branch that sets $product
via wc_get_product( $product_id ) (when FormElements::ID is set and TYPE is
not), check that the result is a valid WC_Product object (not false/null) and if
not, return a WP_Error (or bail early) consistent with other product-handling
code; ensure subsequent setter calls operate only on a valid $product instance
(or create a fallback WC_Product_Simple) so you never dereference a boolean.
- Around line 725-739: prepare_downloads currently builds $downloads with
numeric keys and assumes aligned arrays, which leads to lost IDs and possible
undefined index notices; change prepare_downloads so each download is keyed by
its hash (use the cleaned/file_hash value as the array key) and when reading
$file_names and $file_hashes use guards (isset) and fallback to empty strings
before wc_clean/wrapping functions. Also ensure the 'download_id' field still
contains the cleaned hash string so save_downloadable_files() can call
$download->set_id( $key ) with the actual hash key (not 0/1/2), and update any
iteration expecting numeric keys to iterate over $downloads as key => $download.
- Around line 560-565: FormElements::SHIPPING_CLASS currently returns
'shipping_class_id' but the code always treats its value as a slug by calling
get_data_store()->get_shipping_class_id_by_slug(), which causes numeric IDs to
be mis-handled; update the block in the Product Manager so it accepts both
forms: retrieve the raw cleaned value from FormElements::SHIPPING_CLASS, if it's
numeric (is_numeric) pass it directly into
$product->set_shipping_class_id((int)$value), otherwise resolve it via
$data_store->get_shipping_class_id_by_slug(wc_clean($value)) and then call
$product->set_shipping_class_id($shipping_class_id); ensure you still wc_clean()
the input and handle a false/empty lookup result by not overriding the existing
shipping class.

In `@includes/ProductForm/Field.php`:
- Around line 218-231: The get_options method returns inconsistent types and
uses a misleading comparison: update get_options so it returns a single scalar
(not the whole variadic array) when falling back to the passed-in $value (e.g.
return the first element or null) and change the check against
$this->data['options'] to a proper array/emptiness check (e.g. is_array + !empty
or !== []) so the intent matches the initialization; keep the callback path
using get_options_callback and call_user_func(...) but ensure its return type is
respected and that the final else returns a single scalar rather than the entire
$value array.

In `@src/dashboard/form-manager/hooks/useVariationLayout.tsx`:
- Around line 4-106: The hook useVariationLayout returns formLayouts as a raw
array, but useLayouts expects an object shape { fields: [...] }; update
useVariationLayout so the internal const formLayouts is an object with a fields
property (e.g., formLayouts = { fields: [ ...your current fields array... ] })
and adjust the hook's return to return { formLayouts } so consumers receive the
same structure as useLayouts; ensure references to formLayouts in this file
remain valid.
♻️ Duplicate comments (2)
includes/ProductForm/Field.php (1)

135-168: Fix inconsistent return type in get_value fallback.

Line 167 returns $value, which is the variadic parameter array, not a scalar. When the field type is neither prop, meta, nor custom, this creates an inconsistent return type. The method should return an empty string or appropriate scalar in this fallback case.

🔧 Proposed fix
-        return $value;
+        return '';
includes/Product/Manager.php (1)

376-384: Allow clearing featured and gallery images.
Line 376–383 still uses ! empty, which prevents clearing the featured image (0) or emptying the gallery. This was flagged previously.

🧹 Nitpick comments (4)
includes/ProductForm/Field.php (2)

392-397: Address TODO: Add field type validation.

The TODO at line 393 indicates that field type validation is pending. Consider validating against allowed HTML input types (e.g., text, number, select, checkbox, textarea, etc.) similar to how set_type validates against allowed values.

Would you like me to generate the validation logic or open an issue to track this task?


522-535: Consider using wp_kses_post for output and CSS class instead of inline style.

While $symbol is a hardcoded string (safe), following WordPress coding standards for consistency is recommended. Also, inline styles are harder to maintain and override.

♻️ Suggested improvement
     public function print_required_symbol( bool $output = true ) {
         if ( ! $this->is_required() ) {
             return '';
         }
 
-        $symbol = '<span style="color: red; display: inline-block;">*</span>';
+        $symbol = '<span class="dokan-required-symbol">*</span>';
 
         if ( $output ) {
-            echo $symbol;
+            echo wp_kses_post( $symbol );
             return;
         }
 
         return $symbol;
     }

Then define .dokan-required-symbol { color: red; display: inline-block; } in your stylesheet.

src/dashboard/form-manager/hooks/useLayouts.tsx (1)

97-111: Consider extracting the magic number for the responsive breakpoint.

The 768 breakpoint is hardcoded. If this value is used elsewhere or likely to change, consider extracting it to a constant for consistency.

♻️ Suggested refactor
+const RESPONSIVE_BREAKPOINT = 768;
+
 const rootLayout = useMemo( () => {
-    if ( width && width > 768 ) {
+    if ( width && width > RESPONSIVE_BREAKPOINT ) {
         return {
             type: 'row',
includes/Product/Manager.php (1)

436-461: Prefer FormElements::ID for consistency.
update() uses a raw 'id' key while the rest of the manager uses FormElements::ID. Small consistency win.

Comment on lines 79 to 96
public function create( $args = [] ) {
$id = isset( $args['id'] ) ? absint( $args['id'] ) : 0;
$product_id = isset( $args[ FormElements::ID ] ) ? absint( $args[ FormElements::ID ] ) : 0;
$is_update = ! empty( $product_id );

// Type is the most important part here because we need to be using the correct class and methods.
if ( isset( $args['type'] ) ) {
$classname = \WC_Product_Factory::get_classname_from_product_type( $args['type'] );
if ( isset( $args[ FormElements::TYPE ] ) ) {
$classname = \WC_Product_Factory::get_classname_from_product_type( $args[ FormElements::TYPE ] );

if ( ! class_exists( $classname ) ) {
$classname = 'WC_Product_Simple';
}

$product = new $classname( $id );
} elseif ( isset( $args['id'] ) ) {
$product = wc_get_product( $id );
$product = new $classname( $product_id );
} elseif ( isset( $args[ FormElements::ID ] ) ) {
$product = wc_get_product( $product_id );
} else {
$product = new \WC_Product_Simple();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the Manager.php file and read the full create() method
find . -type f -name "Manager.php" -path "*/Product/*" 2>/dev/null

Repository: getdokan/dokan

Length of output: 88


🏁 Script executed:

# Once found, check the complete create() method
cat -n includes/Product/Manager.php | sed -n '75,120p'

Repository: getdokan/dokan

Length of output: 2236


🏁 Script executed:

# Search for wc_get_product usage patterns to understand behavior
rg "wc_get_product" -A 3 -B 1 --type php | head -50

Repository: getdokan/dokan

Length of output: 3341


🏁 Script executed:

# Read more of the method to see if there's error handling
cat -n includes/Product/Manager.php | sed -n '95,150p'

Repository: getdokan/dokan

Length of output: 2509


🏁 Script executed:

# Search for similar patterns of wc_get_product checks in the codebase
rg "wc_get_product" -A 5 includes/ --type php | grep -A 5 "if.*product"

Repository: getdokan/dokan

Length of output: 16275


Guard against invalid product IDs before dereferencing.

If wc_get_product() returns false at line 93, subsequent setter calls will fatal with "Call to a member function on bool". The method's return type is documented as WC_Product|null|false, and the result must be validated before use. Consider checking for false/null and bailing early, or returning WP_Error consistently with other product-handling code in the codebase.

🐛 Proposed fix
         } elseif ( isset( $args[ FormElements::ID ] ) ) {
             $product = wc_get_product( $product_id );
+            if ( ! $product ) {
+                return false;
+            }
         } else {
📝 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
public function create( $args = [] ) {
$id = isset( $args['id'] ) ? absint( $args['id'] ) : 0;
$product_id = isset( $args[ FormElements::ID ] ) ? absint( $args[ FormElements::ID ] ) : 0;
$is_update = ! empty( $product_id );
// Type is the most important part here because we need to be using the correct class and methods.
if ( isset( $args['type'] ) ) {
$classname = \WC_Product_Factory::get_classname_from_product_type( $args['type'] );
if ( isset( $args[ FormElements::TYPE ] ) ) {
$classname = \WC_Product_Factory::get_classname_from_product_type( $args[ FormElements::TYPE ] );
if ( ! class_exists( $classname ) ) {
$classname = 'WC_Product_Simple';
}
$product = new $classname( $id );
} elseif ( isset( $args['id'] ) ) {
$product = wc_get_product( $id );
$product = new $classname( $product_id );
} elseif ( isset( $args[ FormElements::ID ] ) ) {
$product = wc_get_product( $product_id );
} else {
$product = new \WC_Product_Simple();
}
public function create( $args = [] ) {
$product_id = isset( $args[ FormElements::ID ] ) ? absint( $args[ FormElements::ID ] ) : 0;
$is_update = ! empty( $product_id );
// Type is the most important part here because we need to be using the correct class and methods.
if ( isset( $args[ FormElements::TYPE ] ) ) {
$classname = \WC_Product_Factory::get_classname_from_product_type( $args[ FormElements::TYPE ] );
if ( ! class_exists( $classname ) ) {
$classname = 'WC_Product_Simple';
}
$product = new $classname( $product_id );
} elseif ( isset( $args[ FormElements::ID ] ) ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
return false;
}
} else {
$product = new \WC_Product_Simple();
}
🤖 Prompt for AI Agents
In `@includes/Product/Manager.php` around lines 79 - 96, In the create() method,
guard against wc_get_product(...) returning false/null before using $product:
after the branch that sets $product via wc_get_product( $product_id ) (when
FormElements::ID is set and TYPE is not), check that the result is a valid
WC_Product object (not false/null) and if not, return a WP_Error (or bail early)
consistent with other product-handling code; ensure subsequent setter calls
operate only on a valid $product instance (or create a fallback
WC_Product_Simple) so you never dereference a boolean.

Comment on lines 560 to 565
// Shipping class.
if ( isset( $data['shipping_class'] ) ) {
if ( isset( $data[ FormElements::SHIPPING_CLASS ] ) ) {
$data_store = $product->get_data_store();
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data['shipping_class'] ) );
$shipping_class_id = $data_store->get_shipping_class_id_by_slug( wc_clean( $data[ FormElements::SHIPPING_CLASS ] ) );
$product->set_shipping_class_id( $shipping_class_id );
}
Copy link
Contributor

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:

# First, let's find the FormElements class definition
fd FormElements.php

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

# Search for FormElements constant definition
rg "SHIPPING_CLASS" -t php --max-count 20

Repository: getdokan/dokan

Length of output: 391


🏁 Script executed:

# Find the FormElements class
rg "class FormElements" -A 20 -t php

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

# Look for the SHIPPING_CLASS constant
rg "const SHIPPING_CLASS" -B 2 -A 2 -t php

Repository: getdokan/dokan

Length of output: 422


🏁 Script executed:

# Get context around line 560-565 in Manager.php
sed -n '555,575p' includes/Product/Manager.php

Repository: getdokan/dokan

Length of output: 787


🏁 Script executed:

# Search for get_shipping_class_id_by_slug method
rg "get_shipping_class_id_by_slug" -B 5 -A 15 -t php

Repository: getdokan/dokan

Length of output: 2708


🏁 Script executed:

# Find data store related files
fd -e php | xargs rg "get_shipping_class_id_by_slug" -l

Repository: getdokan/dokan

Length of output: 122


🏁 Script executed:

# Look for shipping class related code
rg "shipping_class" -t php -B 2 -A 2 --max-count 50

Repository: getdokan/dokan

Length of output: 5092


🏁 Script executed:

git ls-files | grep -E "\.(php|json)$" | head -30

Repository: getdokan/dokan

Length of output: 1146


🏁 Script executed:

# Check repository structure
ls -la includes/
ls -la includes/Product/

Repository: getdokan/dokan

Length of output: 4795


🏁 Script executed:

# Get the file to understand line numbers and context
wc -l includes/Product/Manager.php
head -n 580 includes/Product/Manager.php | tail -n 30

Repository: getdokan/dokan

Length of output: 1183


🏁 Script executed:

# Search for SHIPPING_CLASS definition and get_shipping_class_id_by_slug
rg -i "shipping_class" includes/ -A 3 -B 1

Repository: getdokan/dokan

Length of output: 5242


🏁 Script executed:

# Look for FormElements
find . -name "*.php" -type f | xargs grep -l "FormElements" | head -10

Repository: getdokan/dokan

Length of output: 132


🏁 Script executed:

# Cat the Manager.php file and search for FormElements
cat -n includes/Product/Manager.php | grep -A 10 -B 10 "shipping_class" | head -50

Repository: getdokan/dokan

Length of output: 958


🏁 Script executed:

# List all files in includes directory recursively
find includes -type f -name "*.php" | head -20

Repository: getdokan/dokan

Length of output: 869


🏁 Script executed:

# Read Manager.php to see its contents around line 560
head -n 570 includes/Product/Manager.php | tail -n 50

Repository: getdokan/dokan

Length of output: 1773


🏁 Script executed:

# Check file existence and size
ls -lh includes/Product/Manager.php 2>/dev/null || echo "File not found"
# Try alternative paths
find . -name "Manager.php" -type f 2>/dev/null | head -5

Repository: getdokan/dokan

Length of output: 318


🏁 Script executed:

# See what's in the root
ls -la | head -20
# Check if there's a specific directory structure
find . -maxdepth 3 -type d | head -20

Repository: getdokan/dokan

Length of output: 1586


🏁 Script executed:

# List all PHP files that might be relevant
git ls-files "*.php" | grep -i manager

Repository: getdokan/dokan

Length of output: 827


🏁 Script executed:

# Search for the code snippet mentioned in the review
git ls-files "*.php" | xargs grep -l "get_shipping_class_id_by_slug" 2>/dev/null

Repository: getdokan/dokan

Length of output: 122


🏁 Script executed:

# Search for FormElements and SHIPPING_CLASS
git ls-files "*.php" | xargs grep -l "FormElements" 2>/dev/null | head -10

Repository: getdokan/dokan

Length of output: 128


🏁 Script executed:

# Get all PHP files in the repo
git ls-files | grep "\.php$"

Repository: getdokan/dokan

Length of output: 28634


🏁 Script executed:

# Search for the actual code pattern from the review
rg "FormElements::SHIPPING_CLASS" --type php 2>/dev/null || echo "Not found"
rg "get_shipping_class_id_by_slug" --type php 2>/dev/null || echo "Not found"

Repository: getdokan/dokan

Length of output: 620


🏁 Script executed:

# Check repository structure
pwd
find . -maxdepth 2 -type f -name "*.php" 2>/dev/null | head -20

Repository: getdokan/dokan

Length of output: 655


🏁 Script executed:

# List all files in the repository to understand structure
git ls-files | head -50

Repository: getdokan/dokan

Length of output: 1503


🏁 Script executed:

# Count PHP files
git ls-files | grep "\.php$" | wc -l
# Show them
git ls-files | grep "\.php$"

Repository: getdokan/dokan

Length of output: 28638


🏁 Script executed:

# List repository contents
ls -R | head -100

Repository: getdokan/dokan

Length of output: 1564


🌐 Web query:

WooCommerce get_shipping_class_id_by_slug method documentation

💡 Result:

WC_Product_Data_Store_Interface::get_shipping_class_id_by_slug(string $slug) — returns the shipping class term ID (int) or false if not found. Declaration/location: includes/interfaces/class-wc-product-data-store-interface.php (method name: get_shipping_class_id_by_slug). (woocommerce.github.io)

Example alternatives:

  • Use get_term_by('slug', $slug, 'product_shipping_class') to fetch the term and read ->term_id. (rudrastyh.com)

Citations:


🌐 Web query:

WooCommerce Product Manager shipping_class_id handling PHP

💡 Result:

Two common ways to set / handle a product’s shipping class (shipping_class_id) in PHP:

  1. WC_Product CRUD (recommended)
  • Use the product object methods (available since WooCommerce 3+):
    $product = wc_get_product( $product_id );
    $product->set_shipping_class_id( $shipping_class_id );
    $product->save();
  • This sets the numeric shipping class ID directly. (stackoverflow.com)
  1. WordPress taxonomy (alternate / lower-level)
  • Shipping classes are a taxonomy 'product_shipping_class'; you can set terms by ID:
    wp_set_post_terms( $product_id, array( $shipping_class_id ), 'product_shipping_class' );
  • This also assigns the shipping class to the product. (stackoverflow.com)

How to find a shipping class ID

  • In WooCommerce admin (WooCommerce → Settings → Shipping → Shipping classes) inspect the edit link or add a small admin column/snippet to show IDs. (Many guides show how to display IDs in the shipping-class table.) (woocommerce.com)

Notes

  • API and some integrations require the numeric shipping_class_id (not the slug). (reddit.com)

If you want, tell me whether you need this for a plugin, REST/API import, or bulk update and I’ll give the exact snippet for that case.

Citations:


🌐 Web query:

WooCommerce FormElements SHIPPING_CLASS constant

💡 Result:

I couldn't find a WooCommerce class named "FormElements" that defines a SHIPPING_CLASS constant. Do you have a link or the specific file/namespace (or a code snippet) you’re looking at? With that I can locate the constant and show where it’s defined and how it’s used.


🏁 Script executed:

# Check the ProductForm Elements to see shipping_class_id field definition
rg "SHIPPING_CLASS" -B 5 -A 5 includes/ProductForm/ -t php

Repository: getdokan/dokan

Length of output: 854


🏁 Script executed:

# Search for how shipping_class_id field is defined in forms
rg "shipping_class_id" includes/ProductForm/ includes/Dashboard/ -B 2 -A 2 -t php | head -60

Repository: getdokan/dokan

Length of output: 1007


🏁 Script executed:

# Look at ProductForm files
cat -n includes/ProductForm/Factory.php | grep -A 10 -B 5 shipping

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

# Check the form manager to see if shipping_class_id field is defined
rg "shipping.class" includes/Product/FormManager.php -B 3 -A 3

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

# Check dashboard templates for shipping class field
rg "shipping_class" includes/Dashboard/Templates/Products.php -B 2 -A 2

Repository: getdokan/dokan

Length of output: 429


🏁 Script executed:

# Check REST ProductController schema for shipping_class details
sed -n '1,100p' includes/REST/ProductController.php | grep -A 10 "shipping_class"

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

# Get more context around shipping_class in REST controller
rg "'shipping_class'" includes/REST/ProductController.php -B 2 -A 8 | head -80

Repository: getdokan/dokan

Length of output: 2463


🏁 Script executed:

# Check the actual form template to see shipping class field rendering
fd -e php templates/products/ | xargs grep -l "shipping_class"

Repository: getdokan/dokan

Length of output: 441


🏁 Script executed:

# Check product form section definitions
rg "shipping.class" includes/ProductSections/ -i -B 2 -A 5 -t php

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

# Check all constants in FormElements to understand the pattern
cat includes/ProductForm/Elements.php

Repository: getdokan/dokan

Length of output: 3106


🏁 Script executed:

# Now check how other form element constants are used in Manager.php
rg "FormElements::" includes/Product/Manager.php | head -20

Repository: getdokan/dokan

Length of output: 1578


🏁 Script executed:

# Check where save_product_shipping_data is called and what data is passed
rg "save_product_shipping_data" includes/Product/Manager.php -B 5 -A 5

Repository: getdokan/dokan

Length of output: 905


🏁 Script executed:

# Compare how REST handles shipping_class vs Manager.php
grep -n "shipping_class" includes/REST/ProductController.php | head -20

Repository: getdokan/dokan

Length of output: 630


🏁 Script executed:

sed -n '280,350p' includes/REST/ProductController.php

Repository: getdokan/dokan

Length of output: 2631


Mismatch between form key naming and slug-expecting method.
The form element FormElements::SHIPPING_CLASS resolves to 'shipping_class_id', implying the input is a numeric ID. However, the code unconditionally passes this value to get_shipping_class_id_by_slug(), which expects a slug string. If the form or any caller sends a numeric ID (consistent with the key name), the lookup fails and returns false, silently clearing the shipping class. The REST API correctly handles this with separate keys: 'shipping_class' (string slug) and 'shipping_class_id' (numeric ID). Either update the form element key to 'shipping_class' for consistency, or implement the proposed fix to handle both numeric IDs and slugs.

🤖 Prompt for AI Agents
In `@includes/Product/Manager.php` around lines 560 - 565,
FormElements::SHIPPING_CLASS currently returns 'shipping_class_id' but the code
always treats its value as a slug by calling
get_data_store()->get_shipping_class_id_by_slug(), which causes numeric IDs to
be mis-handled; update the block in the Product Manager so it accepts both
forms: retrieve the raw cleaned value from FormElements::SHIPPING_CLASS, if it's
numeric (is_numeric) pass it directly into
$product->set_shipping_class_id((int)$value), otherwise resolve it via
$data_store->get_shipping_class_id_by_slug(wc_clean($value)) and then call
$product->set_shipping_class_id($shipping_class_id); ensure you still wc_clean()
the input and handle a false/empty lookup result by not overriding the existing
shipping class.

Comment on lines 725 to 739
public function prepare_downloads( $file_names, $file_urls, $file_hashes ) {
$downloads = [];

if ( ! empty( $file_urls ) ) {
$file_url_size = count( $file_urls );

for ( $i = 0; $i < $file_url_size; $i++ ) {
if ( ! empty( $file_urls[ $i ] ) ) {
$downloads[] = [
'name' => wc_clean( $file_names[ $i ] ),
'file' => wp_unslash( trim( $file_urls[ $i ] ) ),
'download_id' => wc_clean( $file_hashes[ $i ] ),
];
}
}
Copy link
Contributor

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
# First, locate the function and see more context around the reviewed lines
rg -n "prepare_downloads" -A 20 -B 5 includes/Product/Manager.php

Repository: getdokan/dokan

Length of output: 964


🏁 Script executed:

#!/bin/bash
# Search for all callers of prepare_downloads to understand input sources
rg -n "prepare_downloads\s*\(" --type php

Repository: getdokan/dokan

Length of output: 310


🏁 Script executed:

#!/bin/bash
# Check how the downloads array is accessed/used after being prepared
rg -n "downloads" includes/Product/Manager.php -B 2 -A 2

Repository: getdokan/dokan

Length of output: 1297


🏁 Script executed:

#!/bin/bash
# Check the caller context to see how arrays are constructed
cat -n includes/ProductForm/Init.php | sed -n '680,715p'

Repository: getdokan/dokan

Length of output: 1903


🏁 Script executed:

#!/bin/bash
# Look at more context around the prepare_downloads call
rg -n "prepare_downloads" includes/ProductForm/Init.php -B 30 -A 5

Repository: getdokan/dokan

Length of output: 1741


🏁 Script executed:

#!/bin/bash
# Check if there are any other callers or usages of downloads
rg -n "\$downloads" includes/Product/Manager.php -B 3 -A 3

Repository: getdokan/dokan

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check WooCommerce's download structure and if it expects download_id as key
rg -n "set_downloads|get_downloads" includes/Product/Manager.php -B 2 -A 2

Repository: getdokan/dokan

Length of output: 190


🏁 Script executed:

#!/bin/bash
# Look at save_downloadable_files more carefully to see how downloads are used
rg -n "save_downloadable_files" includes/Product/Manager.php -A 20

Repository: getdokan/dokan

Length of output: 1831


🏁 Script executed:

#!/bin/bash
# Search for 'downloadable' field type to understand how arrays are constructed
rg -n "field_type.*downloadable|downloadable.*field_type" --type php

Repository: getdokan/dokan

Length of output: 146


Key downloads by ID and guard misaligned array indexes.

The $key from $downloads is used directly as the download ID in save_downloadable_files() (line 674: $download->set_id( $key )). Currently, numeric keys (0, 1, 2) are used instead of the download hash, causing download IDs to be lost on save. Additionally, $file_names[$i] and $file_hashes[$i] access with no guards risk undefined index notices if arrays are misaligned.

🔧 Proposed fix
            for ( $i = 0; $i < $file_url_size; $i++ ) {
                if ( ! empty( $file_urls[ $i ] ) ) {
-                   $downloads[] = [
+                   $download_id = ! empty( $file_hashes[ $i ] ) ? wc_clean( $file_hashes[ $i ] ) : md5( $file_urls[ $i ] );
+                   $downloads[ $download_id ] = [
                        'name'        => wc_clean( $file_names[ $i ] ?? '' ),
                        'file'        => wp_unslash( trim( $file_urls[ $i ] ) ),
-                       'download_id' => wc_clean( $file_hashes[ $i ] ),
+                       'download_id' => $download_id,
                    ];
                }
            }
📝 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
public function prepare_downloads( $file_names, $file_urls, $file_hashes ) {
$downloads = [];
if ( ! empty( $file_urls ) ) {
$file_url_size = count( $file_urls );
for ( $i = 0; $i < $file_url_size; $i++ ) {
if ( ! empty( $file_urls[ $i ] ) ) {
$downloads[] = [
'name' => wc_clean( $file_names[ $i ] ),
'file' => wp_unslash( trim( $file_urls[ $i ] ) ),
'download_id' => wc_clean( $file_hashes[ $i ] ),
];
}
}
public function prepare_downloads( $file_names, $file_urls, $file_hashes ) {
$downloads = [];
if ( ! empty( $file_urls ) ) {
$file_url_size = count( $file_urls );
for ( $i = 0; $i < $file_url_size; $i++ ) {
if ( ! empty( $file_urls[ $i ] ) ) {
$download_id = ! empty( $file_hashes[ $i ] ) ? wc_clean( $file_hashes[ $i ] ) : md5( $file_urls[ $i ] );
$downloads[ $download_id ] = [
'name' => wc_clean( $file_names[ $i ] ?? '' ),
'file' => wp_unslash( trim( $file_urls[ $i ] ) ),
'download_id' => $download_id,
];
}
}
🤖 Prompt for AI Agents
In `@includes/Product/Manager.php` around lines 725 - 739, prepare_downloads
currently builds $downloads with numeric keys and assumes aligned arrays, which
leads to lost IDs and possible undefined index notices; change prepare_downloads
so each download is keyed by its hash (use the cleaned/file_hash value as the
array key) and when reading $file_names and $file_hashes use guards (isset) and
fallback to empty strings before wc_clean/wrapping functions. Also ensure the
'download_id' field still contains the cleaned hash string so
save_downloadable_files() can call $download->set_id( $key ) with the actual
hash key (not 0/1/2), and update any iteration expecting numeric keys to iterate
over $downloads as key => $download.

Comment on lines +218 to +231
public function get_options( ...$value ) {
$callback = $this->get_options_callback();
if ( ! empty( $callback ) && is_callable( $callback ) ) {
$value[] = $this->data['options'];
return call_user_func( $callback, ...$value );
}

// if options are set under the field, return that options
if ( '' !== $this->data['options'] || empty( $value ) ) {
return $this->data['options'];
}

return $value;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same return-type inconsistency and potentially flawed condition.

Two issues:

  1. Line 230 returns $value (the variadic array) instead of a scalar, mirroring the bug in get_value.

  2. Line 226 compares $this->data['options'] to '', but options is initialized as [] (an array). The comparison '' !== [] is always true, making this condition potentially misleading—though it won't cause runtime errors, it obscures intent.

🔧 Proposed fix
-        if ( '' !== $this->data['options'] || empty( $value ) ) {
+        if ( ! empty( $this->data['options'] ) || empty( $value ) ) {
             return $this->data['options'];
         }
 
-        return $value;
+        return [];
🤖 Prompt for AI Agents
In `@includes/ProductForm/Field.php` around lines 218 - 231, The get_options
method returns inconsistent types and uses a misleading comparison: update
get_options so it returns a single scalar (not the whole variadic array) when
falling back to the passed-in $value (e.g. return the first element or null) and
change the check against $this->data['options'] to a proper array/emptiness
check (e.g. is_array + !empty or !== []) so the intent matches the
initialization; keep the callback path using get_options_callback and
call_user_func(...) but ensure its return type is respected and that the final
else returns a single scalar rather than the entire $value array.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@src/dashboard/form-manager/components/PriceEdit.tsx`:
- Around line 62-65: The onChange handler currently passes rawValue directly to
vendorEarningHandler and the commission API; guard against empty or non-numeric
input by normalizing and validating rawValue before calling
vendorEarningHandler: trim the value, ensure it is not empty, convert to a
numeric type (e.g., parseFloat or Number), check !isNaN(value) and value >= 0,
then call onChange with the original/raw string for the form and only invoke
vendorEarningHandler(field.id, numericValue) when the numeric validation passes
(otherwise skip the API call or pass null/undefined to indicate no valid
amount).
- Around line 14-29: vendorEarningHandler can yield out‑of‑order responses that
overwrite newer earnings; implement request versioning by keeping a persistent
request id (e.g., useRef currentRequestId) and incrementing it before each
apiFetch call in vendorEarningHandler, capture the id in the local scope, and
only call setVendorEarning(…) (or set to 0 on error) if the captured id matches
currentRequestId.current; update references to apiFetch/addQueryArgs as before
and ensure the ref is initialized outside vendorEarningHandler and considered in
cleanup (component unmount) if needed.

In `@src/dashboard/form-manager/context/VariationContext.tsx`:
- Around line 235-238: The toast call in VariationContext.tsx currently accesses
response.data.message unsafely and can throw; update the toast invocations (the
ones that call toast({ type: 'success', title: response.data.message }) and the
similar block at the later location) to use optional chaining and a fallback
(e.g., response?.data?.message ?? a sensible default like a generic success text
or response?.statusText) so missing response.data won't crash the UI; locate the
toast calls inside the function(s) handling the API response and replace the
direct property access with the optional-chained expression and fallback.
- Around line 226-242: The catch block in addVariation logs errors to console
but doesn't notify users; update the addVariation function to call toast in the
catch (similar to other handlers) with type: 'error' and a helpful title/message
(use error.message or a default string) so users see the failure; locate
addVariation and its ajaxRequest/dokan.add_variation_nonce usage to add the
toast call and keep the existing console.error for debugging.
- Around line 244-269: The catch block in removeVariation (function
removeVariation) logs failures to the console but never notifies the user—add a
toast error notification there similar to addVariation: in the catch of
removeVariation call toast({ type: 'error', title: error?.message ||
responseFallbackMessage }) (or a localized string) to display the failure, and
keep the existing console.error for debugging; ensure you reference
dokan.delete_variations_nonce logic and response.data.message pattern for
consistency with the success path.

In `@src/dashboard/form-manager/utils.tsx`:
- Around line 109-128: The current check `if ( typeof depsCondition === 'object'
)` in the deps evaluation logic (where depsCondition is destructured into {
field: depField, operator, value } and depValue is read) does not guard against
null because typeof null === 'object'; update the condition to also ensure
depsCondition is not null (e.g., `depsCondition && typeof depsCondition ===
'object'`) before destructuring so the code in the block (references:
depsCondition, depField, operator, value, depValue) cannot throw when
depsCondition is null; keep the rest of the boolean normalization and operator
checks unchanged.
♻️ Duplicate comments (2)
src/dashboard/form-manager/utils.tsx (1)

78-84: Avoid rejecting numeric 0 for required fields.

The !value check treats 0 and "0" as empty, which will incorrectly flag required numeric fields (e.g., stock quantity, price) as invalid when set to zero.

🐛 Proposed fix
             const value = values[ field.id ];
-            if ( ! value || ( Array.isArray( value ) && value.length === 0 ) ) {
+            const isEmpty =
+                value === undefined ||
+                value === null ||
+                value === '' ||
+                ( Array.isArray( value ) && value.length === 0 );
+            if ( isEmpty ) {
                 newErrors[ field.id ] = __(
                     'Please fill out this field.',
                     'dokan-lite'
                 );
             }
src/dashboard/form-manager/context/VariationContext.tsx (1)

203-224: Missing error handling in generateVariations.

The async function lacks a try-catch block. If ajaxRequest or fetchVariations fails, the error will be unhandled. Also, response.data.message is accessed without null-checking.

🐛 Proposed fix
     const generateVariations = async () => {
         // specific logic to generate variations
         if (
             confirm(
                 __(
                     'Are you sure you want to generate variations? This will overwrite existing variations.',
                     'dokan-lite'
                 )
             )
         ) {
-            const response: any = await ajaxRequest( {
-                action: 'dokan_link_all_variations',
-                post_id: productId,
-                security: dokan.link_variation_nonce,
-            } );
-            await fetchVariations();
-            toast( {
-                type: 'success',
-                title: response.data.message,
-            } );
+            try {
+                const response: any = await ajaxRequest( {
+                    action: 'dokan_link_all_variations',
+                    post_id: productId,
+                    security: dokan.link_variation_nonce,
+                } );
+                await fetchVariations();
+                toast( {
+                    type: 'success',
+                    title: response.data?.message || __( 'Variations generated successfully', 'dokan-lite' ),
+                } );
+            } catch ( error ) {
+                console.error( 'Error generating variations:', error );
+                toast( {
+                    type: 'error',
+                    title: __( 'Error generating variations', 'dokan-lite' ),
+                } );
+            }
         }
     };
🧹 Nitpick comments (2)
src/dashboard/form-manager/utils.tsx (1)

8-30: Consider adding a timeout to prevent indefinite hangs.

The ajaxRequest function wraps jQuery.ajax but doesn't specify a timeout. If the server becomes unresponsive, the Promise will never resolve or reject, potentially leaving the UI in a loading state.

♻️ Suggested improvement
 export const ajaxRequest = (
     data: Record< string, any > | FormData,
-    method: string = 'POST'
+    method: string = 'POST',
+    timeout: number = 30000
 ) => {
     return new Promise( ( resolve, reject ) => {
         const options: any = {
             data,
             url: dokan.ajaxurl,
             type: method,
+            timeout,
             success( response: any ) {
                 resolve( response );
             },
             error( error: any ) {
                 reject( error );
             },
         };
src/dashboard/form-manager/hooks/useVariationLayouts.tsx (1)

9-109: Object reference in dependency array may cause excessive recalculations.

The useMemo depends on product, which is an object. If the parent component creates a new object reference on each render (even with identical content), this will recalculate the layouts unnecessarily. Consider memoizing product in the parent or using specific primitive dependencies.

#!/bin/bash
# Check how product is passed to useVariationLayouts to understand reference stability
rg -n "useVariationLayouts\s*\(" --type=ts --type=tsx -A 3 -B 2

Comment on lines +14 to +29
const vendorEarningHandler = async ( price: number ) => {
if ( field.id === 'regular_price' ) {
// fetch the vendor earning
try {
const response = await apiFetch( {
path: addQueryArgs( '/dokan/v1/commission', {
amount: price,
product_id: data.id,
category_ids: data.category_ids || [],
context: 'seller',
} ),
} );
setVendorEarning( Number( response ) );
} catch ( error ) {
setVendorEarning( 0 );
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent stale “Your Earn” from out‑of‑order responses.

Rapid edits can trigger multiple in‑flight requests; a slower response for an older price can overwrite the latest earning. Track the latest request (or abort previous) before setting state.

✅ Suggested fix (request versioning)
-import { useState } from '@wordpress/element';
+import { useRef, useState } from '@wordpress/element';
@@
 const PriceEdit = ( { data, field, onChange }: any ) => {
     const [ vendorEarning, setVendorEarning ] = useState(
         Number( data.vendor_earning )
     );
+    const lastRequestId = useRef( 0 );
@@
-    const vendorEarningHandler = async ( price: number ) => {
+    const vendorEarningHandler = async ( price: number ) => {
         if ( field.id === 'regular_price' ) {
             // fetch the vendor earning
+            const requestId = ++lastRequestId.current;
             try {
                 const response = await apiFetch( {
                     path: addQueryArgs( '/dokan/v1/commission', {
                         amount: price,
                         product_id: data.id,
                         category_ids: data.category_ids || [],
                         context: 'seller',
                     } ),
                 } );
-                setVendorEarning( Number( response ) );
+                if ( requestId === lastRequestId.current ) {
+                    setVendorEarning( Number( response ) );
+                }
             } catch ( error ) {
-                setVendorEarning( 0 );
+                if ( requestId === lastRequestId.current ) {
+                    setVendorEarning( 0 );
+                }
             }
         }
     };
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/components/PriceEdit.tsx` around lines 14 - 29,
vendorEarningHandler can yield out‑of‑order responses that overwrite newer
earnings; implement request versioning by keeping a persistent request id (e.g.,
useRef currentRequestId) and incrementing it before each apiFetch call in
vendorEarningHandler, capture the id in the local scope, and only call
setVendorEarning(…) (or set to 0 on error) if the captured id matches
currentRequestId.current; update references to apiFetch/addQueryArgs as before
and ensure the ref is initialized outside vendorEarningHandler and considered in
cleanup (component unmount) if needed.

Comment on lines +62 to +65
onChange={ ( _, rawValue ) => {
onChange( { [ field.id ]: rawValue } );
void vendorEarningHandler( rawValue );
} }
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against empty/non‑numeric input before calling the API.

rawValue can be empty or non‑numeric; calling the commission endpoint with that value is noisy and may return invalid earnings.

✅ Suggested fix (normalize input)
                 onChange={ ( _, rawValue ) => {
                     onChange( { [ field.id ]: rawValue } );
-                    void vendorEarningHandler( rawValue );
+                    const numericValue =
+                        rawValue === '' || rawValue === null || rawValue === undefined
+                            ? NaN
+                            : Number( rawValue );
+                    if ( Number.isFinite( numericValue ) ) {
+                        void vendorEarningHandler( numericValue );
+                    } else if ( field.id === 'regular_price' ) {
+                        setVendorEarning( 0 );
+                    }
                 } }
📝 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
onChange={ ( _, rawValue ) => {
onChange( { [ field.id ]: rawValue } );
void vendorEarningHandler( rawValue );
} }
onChange={ ( _, rawValue ) => {
onChange( { [ field.id ]: rawValue } );
const numericValue =
rawValue === '' || rawValue === null || rawValue === undefined
? NaN
: Number( rawValue );
if ( Number.isFinite( numericValue ) ) {
void vendorEarningHandler( numericValue );
} else if ( field.id === 'regular_price' ) {
setVendorEarning( 0 );
}
} }
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/components/PriceEdit.tsx` around lines 62 - 65,
The onChange handler currently passes rawValue directly to vendorEarningHandler
and the commission API; guard against empty or non-numeric input by normalizing
and validating rawValue before calling vendorEarningHandler: trim the value,
ensure it is not empty, convert to a numeric type (e.g., parseFloat or Number),
check !isNaN(value) and value >= 0, then call onChange with the original/raw
string for the form and only invoke vendorEarningHandler(field.id, numericValue)
when the numeric validation passes (otherwise skip the API call or pass
null/undefined to indicate no valid amount).

Comment on lines +226 to +242
const addVariation = async () => {
try {
const response: any = await ajaxRequest( {
action: 'dokan_add_variation',
post_id: productId,
security: dokan.add_variation_nonce,
loop: 0,
} );
await fetchVariations();
toast( {
type: 'success',
title: response.data.message,
} );
} catch ( error ) {
console.error( 'Error adding variation:', error );
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing error toast in addVariation catch block.

The catch block logs the error but doesn't notify the user via toast, unlike other error handlers in this file.

🐛 Proposed fix
         } catch ( error ) {
             console.error( 'Error adding variation:', error );
+            toast( {
+                type: 'error',
+                title: __( 'Error adding variation', 'dokan-lite' ),
+            } );
         }
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/context/VariationContext.tsx` around lines 226 -
242, The catch block in addVariation logs errors to console but doesn't notify
users; update the addVariation function to call toast in the catch (similar to
other handlers) with type: 'error' and a helpful title/message (use
error.message or a default string) so users see the failure; locate addVariation
and its ajaxRequest/dokan.add_variation_nonce usage to add the toast call and
keep the existing console.error for debugging.

Comment on lines +235 to +238
toast( {
type: 'success',
title: response.data.message,
} );
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unsafe access to response.data.message may throw.

If the response structure differs from expected (e.g., response.data is undefined), accessing .message will throw. Use optional chaining with a fallback.

🐛 Proposed fix
             toast( {
                 type: 'success',
-                title: response.data.message,
+                title: response.data?.message || __( 'Operation completed successfully', 'dokan-lite' ),
             } );

Also applies to: 262-265

🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/context/VariationContext.tsx` around lines 235 -
238, The toast call in VariationContext.tsx currently accesses
response.data.message unsafely and can throw; update the toast invocations (the
ones that call toast({ type: 'success', title: response.data.message }) and the
similar block at the later location) to use optional chaining and a fallback
(e.g., response?.data?.message ?? a sensible default like a generic success text
or response?.statusText) so missing response.data won't crash the UI; locate the
toast calls inside the function(s) handling the API response and replace the
direct property access with the optional-chained expression and fallback.

Comment on lines +244 to +269
const removeVariation = async ( variation: VariationType ) => {
if (
! confirm(
__(
'Are you sure you want to remove this variation?',
'dokan-lite'
)
)
) {
return;
}
try {
const response: any = await ajaxRequest( {
action: 'dokan_remove_variation',
'variation_ids[]': variation.id,
security: dokan.delete_variations_nonce,
} );
await fetchVariations();
toast( {
type: 'success',
title: response.data.message,
} );
} catch ( error ) {
console.error( 'Error removing variation:', error );
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing error toast in removeVariation catch block.

Same issue as addVariation - the user isn't notified when removal fails.

🐛 Proposed fix
         } catch ( error ) {
             console.error( 'Error removing variation:', error );
+            toast( {
+                type: 'error',
+                title: __( 'Error removing variation', 'dokan-lite' ),
+            } );
         }
📝 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
const removeVariation = async ( variation: VariationType ) => {
if (
! confirm(
__(
'Are you sure you want to remove this variation?',
'dokan-lite'
)
)
) {
return;
}
try {
const response: any = await ajaxRequest( {
action: 'dokan_remove_variation',
'variation_ids[]': variation.id,
security: dokan.delete_variations_nonce,
} );
await fetchVariations();
toast( {
type: 'success',
title: response.data.message,
} );
} catch ( error ) {
console.error( 'Error removing variation:', error );
}
};
const removeVariation = async ( variation: VariationType ) => {
if (
! confirm(
__(
'Are you sure you want to remove this variation?',
'dokan-lite'
)
)
) {
return;
}
try {
const response: any = await ajaxRequest( {
action: 'dokan_remove_variation',
'variation_ids[]': variation.id,
security: dokan.delete_variations_nonce,
} );
await fetchVariations();
toast( {
type: 'success',
title: response.data.message,
} );
} catch ( error ) {
console.error( 'Error removing variation:', error );
toast( {
type: 'error',
title: __( 'Error removing variation', 'dokan-lite' ),
} );
}
};
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/context/VariationContext.tsx` around lines 244 -
269, The catch block in removeVariation (function removeVariation) logs failures
to the console but never notifies the user—add a toast error notification there
similar to addVariation: in the catch of removeVariation call toast({ type:
'error', title: error?.message || responseFallbackMessage }) (or a localized
string) to display the failure, and keep the existing console.error for
debugging; ensure you reference dokan.delete_variations_nonce logic and
response.data.message pattern for consistency with the success path.

Comment on lines +109 to +128
if ( typeof depsCondition === 'object' ) {
const { field: depField, operator, value } = depsCondition;
const depValue = data[ depField ];

let targetValue = value;
if ( value === 'on' || value === 'yes' ) {
targetValue = true;
}
if ( value === 'off' || value === 'no' ) {
targetValue = false;
}

if ( operator === 'equal' ) {
return depValue === targetValue;
}
if ( operator === 'not_equal' ) {
return depValue !== targetValue;
}
}
return true;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against null in object type check.

typeof null === 'object' in JavaScript, so if depsCondition is null, the code will attempt to destructure it and throw an error.

🐛 Proposed fix
-    if ( typeof depsCondition === 'object' ) {
+    if ( depsCondition && typeof depsCondition === 'object' ) {
         const { field: depField, operator, value } = depsCondition;
📝 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
if ( typeof depsCondition === 'object' ) {
const { field: depField, operator, value } = depsCondition;
const depValue = data[ depField ];
let targetValue = value;
if ( value === 'on' || value === 'yes' ) {
targetValue = true;
}
if ( value === 'off' || value === 'no' ) {
targetValue = false;
}
if ( operator === 'equal' ) {
return depValue === targetValue;
}
if ( operator === 'not_equal' ) {
return depValue !== targetValue;
}
}
return true;
if ( depsCondition && typeof depsCondition === 'object' ) {
const { field: depField, operator, value } = depsCondition;
const depValue = data[ depField ];
let targetValue = value;
if ( value === 'on' || value === 'yes' ) {
targetValue = true;
}
if ( value === 'off' || value === 'no' ) {
targetValue = false;
}
if ( operator === 'equal' ) {
return depValue === targetValue;
}
if ( operator === 'not_equal' ) {
return depValue !== targetValue;
}
}
return true;
🤖 Prompt for AI Agents
In `@src/dashboard/form-manager/utils.tsx` around lines 109 - 128, The current
check `if ( typeof depsCondition === 'object' )` in the deps evaluation logic
(where depsCondition is destructured into { field: depField, operator, value }
and depValue is read) does not guard against null because typeof null ===
'object'; update the condition to also ensure depsCondition is not null (e.g.,
`depsCondition && typeof depsCondition === 'object'`) before destructuring so
the code in the block (references: depsCondition, depField, operator, value,
depValue) cannot throw when depsCondition is null; keep the rest of the boolean
normalization and operator checks unchanged.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@templates/products/products.php`:
- Around line 9-12: Add an immediate capability check inside the
load_product_edit_template() method: verify
current_user_can('dokan_edit_product') and
dokan_is_seller_enabled(dokan_get_current_user_id()), and call wp_die with a
permission message if the check fails; if it passes, continue to load the form
wrapper (dokan_get_template_part('products/form-manager/form-wrapper')). This
ensures authorization is enforced before the
do_action/dokan_product_form_manager_inside_content hook runs.

Comment on lines +9 to +12
if ( isset( $_GET['form_manager'] ) ) {
do_action( 'dokan_render_product_form_manager_template', $product_action );
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for where the action hook is registered and its handler
rg -n "add_action.*dokan_render_product_form_manager_template" --type php

# Also search for the callback function definition
rg -n "function.*form_manager|do_action.*dokan_render_product_form_manager_template" --type php -A 10

# Search for handlers that might contain capability checks
rg -n "dokan_render_product_form_manager_template" --type php -B 5 -A 10

Repository: getdokan/dokan

Length of output: 2793


🏁 Script executed:

# Search for permission/capability checks in the templates directory
rg -n "current_user_can|dokan_is_seller|user_can" templates/ --type php

# Also search the wider codebase for the hook handler
find . -name "*.php" -type f ! -path "./vendor/*" | xargs grep -l "dokan_render_product_form_manager_template" 2>/dev/null

Repository: getdokan/dokan

Length of output: 3116


🏁 Script executed:

# Read the FormManager.php file to find the load_product_edit_template method
wc -l includes/Product/FormManager.php

Repository: getdokan/dokan

Length of output: 94


🏁 Script executed:

# Get the load_product_edit_template method implementation
rg -n "function load_product_edit_template" includes/Product/FormManager.php -A 30

Repository: getdokan/dokan

Length of output: 1164


🏁 Script executed:

# Find the form-wrapper template
find . -name "form-wrapper.php" -path "*/form-manager/*" -type f ! -path "./vendor/*"

Repository: getdokan/dokan

Length of output: 108


🏁 Script executed:

# Check if the form-wrapper template has capability checks
rg -n "current_user_can|dokan_is_seller" includes/Product/FormManager.php -B 5 -A 5

Repository: getdokan/dokan

Length of output: 919


🏁 Script executed:

# Read the form-wrapper.php file to check for capability checks
cat -n ./templates/products/form-manager/form-wrapper.php

Repository: getdokan/dokan

Length of output: 3060


Add capability check directly in the form manager handler.

The nonce verification on line 4 protects against CSRF for this code path. However, the handler load_product_edit_template() currently lacks direct authorization checks. While capability checks do exist in the nested dokan_product_form_manager_inside_content action hook (which validates dokan_edit_product and seller status), the form-wrapper template loads before those checks are evaluated. Add a direct capability check to load_product_edit_template() to validate user permissions immediately:

public function load_product_edit_template() {
    if ( ! current_user_can( 'dokan_edit_product' ) || ! dokan_is_seller_enabled( dokan_get_current_user_id() ) ) {
        wp_die( __( 'You do not have permission to access this page.', 'dokan-lite' ) );
    }
    dokan_get_template_part( 'products/form-manager/form-wrapper' );
}
🤖 Prompt for AI Agents
In `@templates/products/products.php` around lines 9 - 12, Add an immediate
capability check inside the load_product_edit_template() method: verify
current_user_can('dokan_edit_product') and
dokan_is_seller_enabled(dokan_get_current_user_id()), and call wp_die with a
permission message if the check fails; if it passes, continue to load the form
wrapper (dokan_get_template_part('products/form-manager/form-wrapper')). This
ensures authorization is enforced before the
do_action/dokan_product_form_manager_inside_content hook runs.

@kzamanbd
Copy link
Contributor Author

This PR Move into #3083

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants