Skip to content

enhance: change the withdraw exporter to woocommerce one and add the proper filtering#3054

Open
imehedi357 wants to merge 8 commits intodevelopfrom
enhance/withdraw-exporter-change-and-proper-filtering
Open

enhance: change the withdraw exporter to woocommerce one and add the proper filtering#3054
imehedi357 wants to merge 8 commits intodevelopfrom
enhance/withdraw-exporter-change-and-proper-filtering

Conversation

@imehedi357
Copy link
Contributor

@imehedi357 imehedi357 commented Jan 2, 2026

Fix withdraw export ignoring filters, and disable the export button while processing using the new exporter.

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:

  • Fix Status Filter: Switched the controller from v2 to v1 which uses WooCommerce's export functionality.
  • Fix Status Filter: Updated WithdrawExportController to convert the status string (e.g., 'approved') received from the API into its corresponding integer status code (e.g., 1) before querying the database. Previously, passing the string directly caused the query to fail or default to the wrong status.
  • Fix Date Filter: Updated WithdrawExportController to map standard REST API date parameters (after, before) to the internal query arguments (start_date, end_date) required by dokan()->withdraw->all().
  • Frontend Update: Updated the withdraw dashboard frontend (index.tsx) to pass after and before parameters for date filtering instead of start_date and end_date, adhering to the API expectations.
  • UX Improvement: Disabled the "Export" button while an export is in progress to prevent multiple clicks.

Note on Filename: The issue description mentioned that the exported filename should reflect the applied filters (e.g., statement_vendorA_...). This change was not included in this PR. The export functionality relies on Automattic\WooCommerce\Admin\API\Reports\GenericController for generating the file, and modifying the filename generation logic would require overriding significant portions of the core WooCommerce Admin export handling, which was deemed too intrusive and risky for this fix.

Related Pull Request(s)

Closes

How to test the changes in this Pull Request:

  • Steps in the issue link

Changelog entry

fix filtered export and swap to new controller

  • Now, the exported CSV only contains data matching the active filters.
  • Pagination is removed during export to ensure all matching records are included in the generated file.
  • The export process now runs in the background.
  • A loader/spinner has been added to the Export button to provide visual feedback to the user while the export is in progress.

Before Changes

The export function ignored the active status and date filters, downloading an internal default set or all records regardless of what was shown in the table.

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

    • Server-backed withdrawal export with in-button progress, direct CSV download when ready, and optional emailed report with background polling.
    • Client-side CSV export for visible withdrawal data with enriched fields including vendor ID and vendor name; new export CSV columns added.
  • Improvements

    • More accurate handling of withdrawal filters (status and date range) during export.
    • Export button shows spinner, “Exporting…”, is disabled while processing, and uses toasts for status and errors.

@imehedi357 imehedi357 added Needs: Testing This requires further testing Needs: Dev Review It requires a developer review and approval labels Jan 2, 2026
@imehedi357 imehedi357 self-assigned this Jan 2, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces frontend withdraw export flow with client-side CSV download when full data is available and a backend export + polling flow otherwise; maps frontend filter params to backend equivalents (status → status_code, after → start_date, before → end_date); adds CSV utilities and type declarations.

Changes

Cohort / File(s) Summary
Backend Export Parameter Mapping
includes/REST/WithdrawExportController.php
Map request params: status → status code via dokan()->withdraw->get_status_code(...); rename afterstart_date and beforeend_date.
Frontend Export UI & Flow
src/admin/dashboard/pages/withdraw/index.tsx
Add export UI state and toasts, client-side direct CSV download when table data is complete, otherwise POST filters to /dokan/v1/reports/withdraws/export and poll for completion; enrich CSV rows (user_id, vendor_name), new CSV headers, button/spinner state and error handling.
CSV Utilities & Types
src/utils/download-csv.ts, types/externals.d.ts
Add objectToCSVRows and directDownloadCSV utilities; declare @woocommerce/csv-export functions (downloadCSVFile, generateCSVDataFromTable, generateCSVFileName).
Admin Page Columns
includes/Admin/Dashboard/Pages/Withdraw.php
Expose export columns by using WithdrawExportController->get_export_columns() in page settings.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as Withdraw Page UI
    participant Controller as WithdrawExportController
    participant ExportSvc as Export Service / Storage
    participant CSVUtil as Browser CSV Utility

    User->>UI: Click Export (with filters)
    activate UI
    UI->>UI: Set isExporting = true, show spinner/toast

    rect rgba(200,220,255,0.5)
    Note over UI,Controller: Send current filters (filterArgs)
    UI->>Controller: POST /dokan/v1/reports/withdraws/export (filterArgs)
    end

    activate Controller
    Controller->>Controller: Map status → status_code\nMap after → start_date\nMap before → end_date
    Controller->>ExportSvc: Enqueue export job / generate CSV
    ExportSvc-->>Controller: Return export_id or download_url
    Controller-->>UI: Return export_id / status endpoint
    deactivate Controller

    rect rgba(220,255,220,0.5)
    loop poll until ready/error
        UI->>ExportSvc: GET export status(export_id)
        ExportSvc-->>UI: Status (pending/ready/error) or download_url
    end
    end

    alt ready & URL returned
        UI->>UI: isExporting = false
        UI->>CSVUtil: Trigger direct download or fetch download_url
        UI->>User: File downloaded
    else error/timeout
        UI->>UI: isExporting = false
        UI->>User: Show error/toast
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

Dev Review Done, Upcoming Release

Suggested reviewers

  • mrabbani
  • MdAsifHossainNadim

Poem

🐰 I hopped across filters, mapped status with care,

Spinner hums softly while CSVs prepare.
Rows lined like carrots, tidy and neat,
Downloaded or mailed — a crunchy data treat.
Hooray for exports, crisp and fair!

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (25 files):

⚔️ .gitignore (content)
⚔️ CHANGELOG.md (content)
⚔️ assets/src/js/helper.js (content)
⚔️ composer.lock (content)
⚔️ dokan-class.php (content)
⚔️ dokan.php (content)
⚔️ includes/Admin/Dashboard/Pages/Withdraw.php (content)
⚔️ includes/Ajax.php (content)
⚔️ includes/Commission/Settings/OrderItem.php (content)
⚔️ includes/Dashboard/Templates/Settings.php (content)
⚔️ includes/Order/Hooks.php (content)
⚔️ includes/REST/StoreController.php (content)
⚔️ includes/REST/WithdrawExportController.php (content)
⚔️ languages/dokan-lite.pot (content)
⚔️ package-lock.json (content)
⚔️ package.json (content)
⚔️ readme.txt (content)
⚔️ src/admin/dashboard/pages/withdraw/index.tsx (content)
⚔️ src/components/dataviews/AdminDataViewTable.tsx (content)
⚔️ src/components/dataviews/DataViewTable.tsx (content)
⚔️ templates/settings/store-form.php (content)
⚔️ templates/settings/store-time.php (content)
⚔️ templates/whats-new.php (content)
⚔️ tests/pw/utils/helpers.ts (content)
⚔️ types/externals.d.ts (content)

These conflicts must be resolved before merging into develop.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main changes: switching to WooCommerce exporter and adding proper filtering for the withdraw export functionality.
Description check ✅ Passed The description covers the template requirements including changes proposed, related PRs, issue closure, testing steps, changelog, and before/after context, though developer documentation was omitted as noted.
Linked Issues check ✅ Passed The PR addresses the core coding requirements from issue #5251: fixes status filtering by converting strings to codes, maps date parameters correctly, disables export button during processing, and ensures filtered data is exported. Filename customization was intentionally omitted as too intrusive.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing filtered exports and integrating WooCommerce exporter. Backend parameter mapping, frontend API calls, CSV utility creation, and UI state management are all within scope of the stated objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch enhance/withdraw-exporter-change-and-proper-filtering

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/admin/dashboard/pages/withdraw/index.tsx (1)

578-582: Consider using a toast notification instead of alert().

While the error handling logic is correct and the finally block properly resets the state, using alert() for error messages provides a suboptimal user experience. Consider replacing it with a toast or notification component for more modern and user-friendly error feedback.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 30577f8 and 95af0b2.

📒 Files selected for processing (2)
  • includes/REST/WithdrawExportController.php
  • src/admin/dashboard/pages/withdraw/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
includes/REST/WithdrawExportController.php (2)
dokan.php (1)
  • dokan (90-92)
includes/Withdraw/Manager.php (1)
  • get_status_code (285-299)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: e2e tests (2, 3)
  • GitHub Check: e2e tests (3, 3)
  • GitHub Check: api tests (1, 1)
  • GitHub Check: e2e tests (1, 3)
🔇 Additional comments (6)
includes/REST/WithdrawExportController.php (2)

98-100: LGTM!

The mapping of after to start_date correctly transforms the REST API parameter to the internal query argument format expected by dokan()->withdraw->all().


102-104: LGTM!

The mapping of before to end_date correctly transforms the REST API parameter to the internal query argument format expected by dokan()->withdraw->all().

src/admin/dashboard/pages/withdraw/index.tsx (4)

24-24: LGTM!

The Loader2 import is correctly added and used for the export loading indicator.


91-91: LGTM!

The isExporting state correctly tracks the export operation status and controls the button's disabled state and loading indicator.


585-595: LGTM!

The conditional rendering provides clear visual feedback during the export process. The loading spinner with "Exporting..." text and the disabled button state create a good user experience.


554-571: Clarify whether per_page: 100 limits the export to 100 records or serves as a batch size for internal pagination.

The export implementation uses WooCommerce's GenericController + ExportableInterface pattern with background job processing (indicated by the polling mechanism). However, the WithdrawExportController::get_items() method directly applies per_page as a database LIMIT without visible pagination looping.

To confirm this fulfills the PR requirement ("export should include all matching rows across pagination"), verify:

  1. Whether WooCommerce's GenericController internally loops through pages during export processing
  2. Or if per_page: 100 acts as a hard limit on total exported records

If the latter, the export should either remove the per_page limit or loop through all pages to fetch complete datasets.

Comment on lines 90 to 92
if ( ! empty( $request['status'] ) ) {
$args['status'] = $request['status'];
$args['status'] = dokan()->withdraw->get_status_code( $request['status'] );
}
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

Consider validating the status conversion result.

If an invalid status somehow bypasses the enum validation or if get_status_code returns null, assigning null to $args['status'] might produce unexpected query behavior. Consider only setting the status if the conversion succeeds:

🔎 Suggested defensive check
 if ( ! empty( $request['status'] ) ) {
-    $args['status'] = dokan()->withdraw->get_status_code( $request['status'] );
+    $status_code = dokan()->withdraw->get_status_code( $request['status'] );
+    if ( null !== $status_code ) {
+        $args['status'] = $status_code;
+    }
 }
📝 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 ( ! empty( $request['status'] ) ) {
$args['status'] = $request['status'];
$args['status'] = dokan()->withdraw->get_status_code( $request['status'] );
}
if ( ! empty( $request['status'] ) ) {
$status_code = dokan()->withdraw->get_status_code( $request['status'] );
if ( null !== $status_code ) {
$args['status'] = $status_code;
}
}
🤖 Prompt for AI Agents
In includes/REST/WithdrawExportController.php around lines 90 to 92, the code
assigns $args['status'] directly from
dokan()->withdraw->get_status_code($request['status']) which may return null;
update this to capture the conversion result in a variable, check that it is not
null (or not false) before setting $args['status'], and if the conversion fails
either skip setting the status filter or handle the invalid value (e.g., return
a validation error or use a safe default) to avoid passing null into the query.

@imehedi357
Copy link
Contributor Author

@mrabbani, @imtiaz-pranto vai, the feature of the dynamic name of the filtered data is not added. The file name is currently the default name provided from WooCommerce.

@imehedi357 imehedi357 requested a review from mrabbani January 2, 2026 10:13
@dev-shahed
Copy link
Member

@mrabbani This pr not working for export..

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/admin/dashboard/pages/withdraw/index.tsx (1)

557-576: ⚠️ Potential issue | 🔴 Critical

Export request likely broken: endpoint, date params, and pagination concerns.

Several issues with this export flow:

  1. Endpoint mismatch with PR objective: The PR states the export controller was switched to v1 using WooCommerce Admin's export (/dokan/v1/reports/withdraws/export via POST), but this handler sends a GET to dokan/v2/withdraw. Which endpoint actually handles the export? The existing handleExportWithdraws (Line 668) already uses the v1 endpoint with POST — is this button supposed to use the same flow?

  2. Date filter params won't reach the backend: filterArgs stores dates as start_date / end_date (set at Line 1004–1005), but per the PR description the backend REST API expects after / before (which it then maps internally). The export request will silently drop date filters.

  3. Spreading ...view sends irrelevant UI state: view contains type, titleField, layout, fields (an array of column IDs), etc. These are meaningless to the API and could cause unexpected behavior if any key collides with a query param.

  4. Pagination not removed: PR changelog says "pagination removed during export so all matching records are included," but perPage and page from view are still sent.

Suggested approach (if using the v1 export endpoint)
-                    const path = addQueryArgs( 'dokan/v2/withdraw', {
-                        ...view,
-                        ...filterArgs,
-                        is_export: true,
-                    } );
-                    const res = await apiFetch( { path } );
-                    if ( res && res.url ) {
-                        window.location.assign( res.url as string );
-                    }
+                    // Map filterArgs date keys to what the REST API expects
+                    const exportArgs = {
+                        status: view.status === 'all' ? '' : view.status,
+                        search: view.search || '',
+                        ...(filterArgs.user_id && { user_id: filterArgs.user_id }),
+                        ...(filterArgs.payment_method && { payment_method: filterArgs.payment_method }),
+                        ...(filterArgs.start_date && { after: filterArgs.start_date }),
+                        ...(filterArgs.end_date && { before: filterArgs.end_date }),
+                    };
+                    const res = await apiFetch( {
+                        path: '/dokan/v1/reports/withdraws/export',
+                        method: 'POST',
+                        data: { report_args: exportArgs, email: false },
+                    } );
+                    // Handle response per the v1 export flow...
🧹 Nitpick comments (2)
src/admin/dashboard/pages/withdraw/index.tsx (2)

572-573: Consider replacing alert() with a toast/snackbar notification.

Native alert() blocks the UI thread and feels out of place in a modern React admin dashboard. A non-blocking toast (e.g., via @wordpress/notices or a similar pattern already used in this codebase) would be more consistent.


668-706: Dead or underutilized code: handleExportWithdraws is only used for PayPal mass payment.

This function implements the v1 export endpoint flow (/dokan/v1/reports/withdraws/export with POST + polling), which aligns with the PR's stated backend changes. However, the new Export button (Line 557) doesn't use it — it sends a GET to dokan/v2/withdraw instead. If the main export is supposed to use the v1 WooCommerce Admin export flow, consider wiring the Export button to call handleExportWithdraws (or a refactored version of it) rather than duplicating a separate, potentially broken flow.

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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/admin/dashboard/pages/withdraw/index.tsx (1)

753-809: ⚠️ Potential issue | 🟠 Major

Polling implementation has structural issues.

  1. resolve is undefined on first call (Line 808): checkStatus is called without arguments, so resolve is undefined. The if (resolve) guard prevents a crash, but means the outer await on Line 808 resolves immediately (since checkStatus returns after the first iteration without awaiting the recursive chain). The polling effectively becomes fire-and-forget.

  2. Recursive setTimeout + async/await mix is fragile: Wrapping setTimeout in a new Promise and recursively calling checkStatus inside it creates a hard-to-follow control flow. A simple loop with await new Promise(r => setTimeout(r, 5000)) would be clearer and correctly awaitable.

Suggested rewrite
-    const pollExportStatus = async ( exportId ) => {
-        const maxAttempts = 60;
-        let attempts = 0;
-
-        const checkStatus = async ( resolve ) => {
-            try {
-                const statusResponse = await apiFetch( {
-                    path: `/dokan/v1/reports/withdraws/export/${ exportId }/status`,
-                    method: 'GET',
-                } );
-
-                console.log( 'Export status:', statusResponse );
-
-                if ( statusResponse.percent_complete === 100 ) {
-                    if ( statusResponse.download_url ) {
-                            const link = document.createElement( 'a' );
-                            link.href = statusResponse.download_url;
-                            link.download = '';
-                            document.body.appendChild( link );
-                            link.click();
-                            document.body.removeChild( link );
-                            console.log( 'Export completed and downloaded' );
-                        } else {
-                            throw new Error( 'Download URL not available' );
-                        }
-                } else {
-                    attempts++;
-                    if ( attempts < maxAttempts ) {
-                        await new Promise(
-                            ( res ) =>
-                                setTimeout(
-                                    async () => await checkStatus( res ),
-                                    5000
-                                )
-                        );
-                    } else {
-                        throw new Error( 'Export timeout - please try again' );
-                    }
-                }
-            } catch ( error ) {
-                console.error( 'Error checking export status:', error );
-                alert( __( 'Export failed. Please try again.', 'dokan-lite' ) );
-            }
-
-            if ( resolve ) {
-                resolve();
-            }
-        };
-
-        await checkStatus();
-    };
+    const pollExportStatus = async ( exportId ) => {
+        const maxAttempts = 60;
+
+        for ( let attempt = 0; attempt < maxAttempts; attempt++ ) {
+            const statusResponse = await apiFetch( {
+                path: `/dokan/v1/reports/withdraws/export/${ exportId }/status`,
+                method: 'GET',
+            } );
+
+            if ( statusResponse.percent_complete === 100 ) {
+                if ( ! statusResponse.download_url ) {
+                    throw new Error( 'Download URL not available' );
+                }
+                const link = document.createElement( 'a' );
+                link.href = statusResponse.download_url;
+                link.download = '';
+                document.body.appendChild( link );
+                link.click();
+                document.body.removeChild( link );
+                return;
+            }
+
+            await new Promise( ( r ) => setTimeout( r, 5000 ) );
+        }
+
+        throw new Error( 'Export timeout - please try again' );
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 753 - 809, The
polling routine in pollExportStatus is broken because checkStatus is called
without a resolver so the outer await returns immediately and the recursive
setTimeout/Promise mix is fragile; replace the recursive checkStatus pattern
with a clear loop inside pollExportStatus (use maxAttempts and attempts already
present) that awaits a simple sleep (await new Promise(r => setTimeout(r,
5000))) between iterations, perform the apiFetch each loop, handle
percent_complete === 100 by downloading via the same DOM-link logic in your
diff, and on errors either throw or return a rejected Promise so callers
awaiting pollExportStatus actually wait for completion or failure; update/remove
checkStatus and ensure pollExportStatus is an async function that only resolves
after either successful download or a terminal error/timeout.
🧹 Nitpick comments (3)
src/utils/download-csv.ts (1)

64-64: Remove commented-out debug statement.

Leftover debug artifact.

Proposed fix
-    //console.log(rows);
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/download-csv.ts` at line 64, Remove the leftover commented debug
statement "//console.log(rows);" from the download-csv utility; locate the
commented line in the downloadCsv/downloadCSV function or wherever "rows" is
handled and delete the commented console.log to clean up the code and prevent
clutter in production.
src/admin/dashboard/pages/withdraw/index.tsx (2)

712-751: handleExportWithdraws uses console.log and alert — prefer toast for consistency.

The rest of the new export code uses toast() for user feedback, but this function still uses console.log (Line 716, 732) and alert (Line 744). Consider aligning to the toast pattern for consistent UX.

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

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 712 - 751, In
handleExportWithdraws replace the console.log and alert usages with the app's
toast pattern: use toast.info or toast.loading when starting the export (instead
of console.log 'Starting withdraw export...'), use toast.success when export is
initiated (instead of console.log for exportResponse.export_id), and use
toast.error in the catch block with the same message used in the alert (instead
of alert and console.error); keep polling via pollExportStatus and apiFetch
unchanged so only the user-facing messages switch to toast for consistency.

578-589: shouldEmail heuristic is fragile — it compares a single page to the total.

data holds only the current page (e.g. 10 items) while totalItems is the full count. So data.length < totalItems will almost always be true whenever there's more than one page, forcing the email path even for modest result sets. If the intent is to offer direct download when all data is available client-side, you'd need to fetch all pages first or always use the backend export for multi-page data sets (which seems to be the case). Consider adding a comment clarifying this intent, as shouldEmail reads like a heuristic rather than a deliberate design choice.

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

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 578 - 589, The
current heuristic using shouldEmail (const shouldEmail = data && data.length <
totalItems) is fragile because data is a single page; replace it with an
explicit check for whether all results are loaded (e.g., introduce an
isAllDataLoaded boolean or use totalItems === data.length) and use that to
decide directDownloadCSV vs backend email; if you can't load all pages
client-side, always use the backend export for multi-page results and add a
comment above the check clarifying the intent (referencing shouldEmail, data,
totalItems, directDownloadCSV, WITHDRAW_CSV_HEADERS, and filterArgs).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/admin/dashboard/pages/withdraw/index.tsx`:
- Around line 574-621: The export onClick handler mishandles errors and misses
the backend-polling path: update the async handler so the catch block shows a
user-facing toast (or other UI error) instead of re-throwing; when checking
exportResponse ensure you safely read the error message (e.g., fallback to
exportResponse?.data?.message || exportResponse?.message || a default) before
throwing; and after a successful POST that returns exportResponse.export_id (and
when shouldEmail is false and directDownloadCSV wasn't used) call the existing
pollExportStatus helper (or reuse handleExportWithdraws flow) to poll/download
the CSV, ensuring setIsExporting(false) still runs in finally and toasts are
used for success/failure notifications.

In `@types/externals.d.ts`:
- Around line 59-62: Update the generateCSVDataFromTable declaration to use the
correct row element shape: change the rows parameter type from
Array<Array<string>> to Array<Array<{ display: string; value: string | number
}>> so it matches `@woocommerce/csv-export` expectations; locate the function
signature for generateCSVDataFromTable in types/externals.d.ts and replace the
rows type accordingly, keeping headers as Array<{ key: string; label: string }>
and preserving the return type string.

---

Outside diff comments:
In `@src/admin/dashboard/pages/withdraw/index.tsx`:
- Around line 753-809: The polling routine in pollExportStatus is broken because
checkStatus is called without a resolver so the outer await returns immediately
and the recursive setTimeout/Promise mix is fragile; replace the recursive
checkStatus pattern with a clear loop inside pollExportStatus (use maxAttempts
and attempts already present) that awaits a simple sleep (await new Promise(r =>
setTimeout(r, 5000))) between iterations, perform the apiFetch each loop, handle
percent_complete === 100 by downloading via the same DOM-link logic in your
diff, and on errors either throw or return a rejected Promise so callers
awaiting pollExportStatus actually wait for completion or failure; update/remove
checkStatus and ensure pollExportStatus is an async function that only resolves
after either successful download or a terminal error/timeout.

---

Nitpick comments:
In `@src/admin/dashboard/pages/withdraw/index.tsx`:
- Around line 712-751: In handleExportWithdraws replace the console.log and
alert usages with the app's toast pattern: use toast.info or toast.loading when
starting the export (instead of console.log 'Starting withdraw export...'), use
toast.success when export is initiated (instead of console.log for
exportResponse.export_id), and use toast.error in the catch block with the same
message used in the alert (instead of alert and console.error); keep polling via
pollExportStatus and apiFetch unchanged so only the user-facing messages switch
to toast for consistency.
- Around line 578-589: The current heuristic using shouldEmail (const
shouldEmail = data && data.length < totalItems) is fragile because data is a
single page; replace it with an explicit check for whether all results are
loaded (e.g., introduce an isAllDataLoaded boolean or use totalItems ===
data.length) and use that to decide directDownloadCSV vs backend email; if you
can't load all pages client-side, always use the backend export for multi-page
results and add a comment above the check clarifying the intent (referencing
shouldEmail, data, totalItems, directDownloadCSV, WITHDRAW_CSV_HEADERS, and
filterArgs).

In `@src/utils/download-csv.ts`:
- Line 64: Remove the leftover commented debug statement "//console.log(rows);"
from the download-csv utility; locate the commented line in the
downloadCsv/downloadCSV function or wherever "rows" is handled and delete the
commented console.log to clean up the code and prevent clutter in production.

Comment on lines 574 to 621
onClick={ async () => {
setIsExporting(true);

try {
// Minimal placeholder; backend export flow may vary.
// Attempt to hit export endpoint via same query params.
const path = addQueryArgs( 'dokan/v2/withdraw', {
const shouldEmail = data && data.length < totalItems;

// If all data is already loaded, download directly
if ( ! shouldEmail && data && data.length > 0 ) {
const csvData = data.map( ( item ) => ( {
...item,
user_id: item.user?.id,
vendor_name: item.user?.store_name,
} ) );
directDownloadCSV( 'withdraws', WITHDRAW_CSV_HEADERS, csvData, filterArgs );
return;
}

if ( shouldEmail ) {
toast( {
type: 'info',
title: __( 'Your withdraw Report will be emailed to you.', 'dokan-lite' ),
} );
}

// Otherwise, use email delivery via API
const reportArgs = {
...view,
...filterArgs,
is_export: true,
} );
const res = await apiFetch( { path } );
if ( res && res.url ) {
window.location.assign( res.url as string );
};

const exportResponse = await apiFetch({
path: '/dokan/v1/reports/withdraws/export',
method: 'POST',
data: {
report_args: reportArgs,
email: shouldEmail,
},
});

if ( ! exportResponse.export_id ) {
throw new Error(exportResponse.data.message);
}
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( 'Export failed or not supported yet', e );

} catch (e) {
throw new Error( __('Export failed. Please try again.', 'dokan-lite') );
} finally {
setIsExporting(false);
}
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

Export handler has multiple error-handling issues and a dead-end code path.

  1. Unhandled re-throw (Line 618): The catch block re-throws a new Error, but since this is an async onClick handler, the re-thrown error becomes an unhandled promise rejection. The user sees no feedback. Either show a toast/alert here instead of re-throwing, or remove the re-throw.

  2. Potential TypeError (Line 614): If the API response lacks export_id, accessing exportResponse.data.message may throw a TypeError if exportResponse.data is also undefined.

  3. No polling after backend export (Lines 604–615): When shouldEmail is false but the direct-download path at Line 581 is skipped (e.g., empty data), the code obtains an export_id from the API but never calls pollExportStatus to retrieve the file. The handleExportWithdraws function (Line 713) already implements this pattern — consider reusing it or adding polling here.

Proposed fix for error handling
-                } catch (e) {
-                    throw new Error( __('Export failed. Please try again.', 'dokan-lite') );
+                } catch (e) {
+                    toast( {
+                        type: 'error',
+                        title: __( 'Export failed. Please try again.', 'dokan-lite' ),
+                    } );
                 } finally {
                     setIsExporting(false);
                 }
                     if ( ! exportResponse.export_id ) {
-                        throw new Error(exportResponse.data.message);
+                        throw new Error(exportResponse?.data?.message || __('Export failed.', 'dokan-lite'));
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 574 - 621, The
export onClick handler mishandles errors and misses the backend-polling path:
update the async handler so the catch block shows a user-facing toast (or other
UI error) instead of re-throwing; when checking exportResponse ensure you safely
read the error message (e.g., fallback to exportResponse?.data?.message ||
exportResponse?.message || a default) before throwing; and after a successful
POST that returns exportResponse.export_id (and when shouldEmail is false and
directDownloadCSV wasn't used) call the existing pollExportStatus helper (or
reuse handleExportWithdraws flow) to poll/download the CSV, ensuring
setIsExporting(false) still runs in finally and toasts are used for
success/failure notifications.

@rakib1904107 rakib1904107 self-assigned this Feb 17, 2026
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: 4

🧹 Nitpick comments (1)
src/admin/dashboard/pages/withdraw/index.tsx (1)

787-789: Inconsistent error notification: alert() vs toast().

pollExportStatus uses alert() for error feedback (line 789), while the rest of the new export flow uses toast(). Consider using toast here as well for a consistent UX.

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

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 787 - 789, The
catch block in the pollExportStatus function uses alert(...) which is
inconsistent with the rest of the export flow that uses toast; replace the alert
call with the same toast-based notification used elsewhere (e.g., toast.error or
toast(...)) and ensure the toast utility/import used by the module is referenced
(match the existing toast usage pattern in this file) so the error message and
UX remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/admin/dashboard/pages/withdraw/index.tsx`:
- Around line 28-34: WITHDRAW_CSV_HEADERS is computed at module load using
Object.entries(dokanAdminDashboardSettings.withdraw.columns) which will throw if
withdraw or columns is missing; change this to safely access the path and
provide a fallback empty object, e.g. replace the top-level computation with a
guarded expression using optional chaining and a default:
Object.entries(dokanAdminDashboardSettings?.withdraw?.columns ?? {}) and map
that result, and to avoid module-load crashes compute it lazily where used (e.g.
inside the component or a useMemo) rather than at top-level; update references
to WITHDRAW_CSV_HEADERS accordingly or generate headers via a
getWithdrawCsvHeaders() helper that performs the same guarded access.
- Around line 527-529: The current mapping of fields produces an empty string
for the vendor field (fields: fields.map(... field.id !== 'vendor' ? field.id :
'')) which leaves a bogus entry; replace this with a filter-first (or
filter-after) approach so only real ids are included—for example use
fields.filter(f => f.id !== 'vendor').map(f => f.id) (or map then
.filter(Boolean)) so the fields array passed to DataViews contains no empty
strings; update the code that builds fields (the fields variable/mapping)
accordingly.
- Around line 556-578: The onClick handler in tabsAdditionalContents can fall
through and call the backend export with email: false when there are zero rows;
update the handler to short-circuit early when totalItems === 0 (or when data &&
data.length === 0 and totalItems === 0) to avoid kicking off a backend export
for zero rows: if no items, reset isExporting (call setIsExporting(false)),
optionally notify the user, and return before building csvData or calling the
export API; keep the existing directDownloadCSV path (data.length > 0) and the
backend POST path for real exports unchanged.
- Around line 569-577: The CSV export is including the raw nested details object
so the details column becomes "[object Object]"; update the csvData mapping used
before calling directDownloadCSV so that instead of spreading item.details as-is
you normalize/flatten it the same way the UI does (use the existing
processDetails(item.details) helper or its returned flattened fields) and assign
those flattened/serializable values (or a JSON/string representation) to the
details-related CSV fields; keep WITHDRAW_CSV_HEADERS in sync with those
flattened keys so objectToCSVRows produces readable CSV rows.

---

Nitpick comments:
In `@src/admin/dashboard/pages/withdraw/index.tsx`:
- Around line 787-789: The catch block in the pollExportStatus function uses
alert(...) which is inconsistent with the rest of the export flow that uses
toast; replace the alert call with the same toast-based notification used
elsewhere (e.g., toast.error or toast(...)) and ensure the toast utility/import
used by the module is referenced (match the existing toast usage pattern in this
file) so the error message and UX remain consistent.

Comment on lines +28 to +34
// Define withdraw CSV headers
const WITHDRAW_CSV_HEADERS = Object.entries(
dokanAdminDashboardSettings.withdraw.columns
).map( ( [ key, value ] ) => ( {
key,
label: 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

No defensive check on dokanAdminDashboardSettings.withdraw.columns.

This runs at module-load time. If withdraw or columns is missing from the global settings object (e.g., a different page context, a race, or a plugin conflict), Object.entries(...) will throw and the entire page will crash.

Proposed fix
 // Define withdraw CSV headers
 const WITHDRAW_CSV_HEADERS = Object.entries(
-    dokanAdminDashboardSettings.withdraw.columns
+    dokanAdminDashboardSettings?.withdraw?.columns ?? {}
 ).map( ( [ key, value ] ) => ( {
     key,
     label: 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
// Define withdraw CSV headers
const WITHDRAW_CSV_HEADERS = Object.entries(
dokanAdminDashboardSettings.withdraw.columns
).map( ( [ key, value ] ) => ( {
key,
label: value,
} ) );
// Define withdraw CSV headers
const WITHDRAW_CSV_HEADERS = Object.entries(
dokanAdminDashboardSettings?.withdraw?.columns ?? {}
).map( ( [ key, value ] ) => ( {
key,
label: value,
} ) );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 28 - 34,
WITHDRAW_CSV_HEADERS is computed at module load using
Object.entries(dokanAdminDashboardSettings.withdraw.columns) which will throw if
withdraw or columns is missing; change this to safely access the path and
provide a fallback empty object, e.g. replace the top-level computation with a
guarded expression using optional chaining and a default:
Object.entries(dokanAdminDashboardSettings?.withdraw?.columns ?? {}) and map
that result, and to avoid module-load crashes compute it lazily where used (e.g.
inside the component or a useMemo) rather than at top-level; update references
to WITHDRAW_CSV_HEADERS accordingly or generate headers via a
getWithdrawCsvHeaders() helper that performs the same guarded access.

Comment on lines +527 to +529
fields: fields.map( ( field ) =>
field.id !== 'vendor' ? field.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 | 🟡 Minor

Empty string in fields array — use filter instead of mapping to ''.

Mapping the vendor field to an empty string leaves a bogus entry in the array. If the DataViews component iterates over fields to render columns, it may produce an empty/broken column or a console warning.

Proposed fix
-        fields: fields.map( ( field ) =>
-            field.id !== 'vendor' ? field.id : ''
-        ),
+        fields: fields
+            .map( ( field ) => field.id )
+            .filter( ( id ) => id !== 'vendor' ),
📝 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
fields: fields.map( ( field ) =>
field.id !== 'vendor' ? field.id : ''
),
fields: fields
.map( ( field ) => field.id )
.filter( ( id ) => id !== 'vendor' ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 527 - 529, The
current mapping of fields produces an empty string for the vendor field (fields:
fields.map(... field.id !== 'vendor' ? field.id : '')) which leaves a bogus
entry; replace this with a filter-first (or filter-after) approach so only real
ids are included—for example use fields.filter(f => f.id !== 'vendor').map(f =>
f.id) (or map then .filter(Boolean)) so the fields array passed to DataViews
contains no empty strings; update the code that builds fields (the fields
variable/mapping) accordingly.

Comment on lines 556 to +578
const tabsAdditionalContents = [
<button
type="button"
className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-[#575757] hover:bg-[#7047EB] hover:text-white"
disabled={ isExporting }
className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-[#575757] hover:bg-[#7047EB] hover:text-white disabled:opacity-50 disabled:cursor-not-allowed"
onClick={ async () => {
setIsExporting(true);

try {
// Minimal placeholder; backend export flow may vary.
// Attempt to hit export endpoint via same query params.
const path = addQueryArgs( 'dokan/v2/withdraw', {
const shouldEmail = data && data.length < totalItems;

// If all data is already loaded, download directly
if ( ! shouldEmail && data && data.length > 0 ) {
const csvData = data.map( ( item ) => ( {
...item,
user_id: item.user?.id,
vendor_name: item.user?.store_name,
payment_method: item.method_title,
payable: item.receivable,
date: item.created,
} ) );
directDownloadCSV( 'withdraws', WITHDRAW_CSV_HEADERS, csvData, filterArgs );
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

Direct-download path doesn't account for the !shouldEmail && data.length === 0 edge case.

When data is loaded but empty (data.length === 0 and totalItems === 0), shouldEmail is false and the direct-download guard (data.length > 0) is also false, so execution falls through to the API POST with email: false. This silently kicks off a backend export for zero rows. Consider short-circuiting early:

Proposed fix
             try {
+                    if ( ! data || data.length === 0 && totalItems === 0 ) {
+                        toast( {
+                            type: 'info',
+                            title: __( 'No data to export.', 'dokan-lite' ),
+                        } );
+                        return;
+                    }
+
                     const shouldEmail = data && data.length < totalItems;
📝 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 tabsAdditionalContents = [
<button
type="button"
className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-[#575757] hover:bg-[#7047EB] hover:text-white"
disabled={ isExporting }
className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-[#575757] hover:bg-[#7047EB] hover:text-white disabled:opacity-50 disabled:cursor-not-allowed"
onClick={ async () => {
setIsExporting(true);
try {
// Minimal placeholder; backend export flow may vary.
// Attempt to hit export endpoint via same query params.
const path = addQueryArgs( 'dokan/v2/withdraw', {
const shouldEmail = data && data.length < totalItems;
// If all data is already loaded, download directly
if ( ! shouldEmail && data && data.length > 0 ) {
const csvData = data.map( ( item ) => ( {
...item,
user_id: item.user?.id,
vendor_name: item.user?.store_name,
payment_method: item.method_title,
payable: item.receivable,
date: item.created,
} ) );
directDownloadCSV( 'withdraws', WITHDRAW_CSV_HEADERS, csvData, filterArgs );
return;
const tabsAdditionalContents = [
<button
type="button"
disabled={ isExporting }
className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-[`#575757`] hover:bg-[`#7047EB`] hover:text-white disabled:opacity-50 disabled:cursor-not-allowed"
onClick={ async () => {
setIsExporting(true);
try {
if ( ! data || data.length === 0 && totalItems === 0 ) {
toast( {
type: 'info',
title: __( 'No data to export.', 'dokan-lite' ),
} );
return;
}
const shouldEmail = data && data.length < totalItems;
// If all data is already loaded, download directly
if ( ! shouldEmail && data && data.length > 0 ) {
const csvData = data.map( ( item ) => ( {
...item,
user_id: item.user?.id,
vendor_name: item.user?.store_name,
payment_method: item.method_title,
payable: item.receivable,
date: item.created,
} ) );
directDownloadCSV( 'withdraws', WITHDRAW_CSV_HEADERS, csvData, filterArgs );
return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 556 - 578, The
onClick handler in tabsAdditionalContents can fall through and call the backend
export with email: false when there are zero rows; update the handler to
short-circuit early when totalItems === 0 (or when data && data.length === 0 and
totalItems === 0) to avoid kicking off a backend export for zero rows: if no
items, reset isExporting (call setIsExporting(false)), optionally notify the
user, and return before building csvData or calling the export API; keep the
existing directDownloadCSV path (data.length > 0) and the backend POST path for
real exports unchanged.

Comment on lines +569 to +577
const csvData = data.map( ( item ) => ( {
...item,
user_id: item.user?.id,
vendor_name: item.user?.store_name,
payment_method: item.method_title,
payable: item.receivable,
date: item.created,
} ) );
directDownloadCSV( 'withdraws', WITHDRAW_CSV_HEADERS, csvData, filterArgs );
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

details column will export as [object Object] in the CSV.

item.details is a nested object (see processDetails() usage throughout this file). When spread into csvData, the details key retains the raw object, so objectToCSVRows will serialize it as "[object Object]" in the CSV output. You need to flatten it the same way the UI does.

Proposed fix
 const csvData = data.map( ( item ) => ( {
     ...item,
     user_id: item.user?.id,
     vendor_name: item.user?.store_name,
     payment_method: item.method_title,
     payable: item.receivable,
     date: item.created,
+    details: processDetails( item.details, item.method ),
 } ) );
📝 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 csvData = data.map( ( item ) => ( {
...item,
user_id: item.user?.id,
vendor_name: item.user?.store_name,
payment_method: item.method_title,
payable: item.receivable,
date: item.created,
} ) );
directDownloadCSV( 'withdraws', WITHDRAW_CSV_HEADERS, csvData, filterArgs );
const csvData = data.map( ( item ) => ( {
...item,
user_id: item.user?.id,
vendor_name: item.user?.store_name,
payment_method: item.method_title,
payable: item.receivable,
date: item.created,
details: processDetails( item.details, item.method ),
} ) );
directDownloadCSV( 'withdraws', WITHDRAW_CSV_HEADERS, csvData, filterArgs );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/admin/dashboard/pages/withdraw/index.tsx` around lines 569 - 577, The CSV
export is including the raw nested details object so the details column becomes
"[object Object]"; update the csvData mapping used before calling
directDownloadCSV so that instead of spreading item.details as-is you
normalize/flatten it the same way the UI does (use the existing
processDetails(item.details) helper or its returned flattened fields) and assign
those flattened/serializable values (or a JSON/string representation) to the
details-related CSV fields; keep WITHDRAW_CSV_HEADERS in sync with those
flattened keys so objectToCSVRows produces readable CSV rows.

Rakib Al-Hasan added 2 commits February 17, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Dev Review It requires a developer review and approval Needs: Testing This requires further testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

Comments