Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion includes/Admin/WithdrawLogExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
*
* @var string
*/
protected $filename = 'dokan-withdraw-log-export.csv';
protected $filename = 'dokan-withdraw-log.csv';

/**
* Items to export.
Expand Down Expand Up @@ -97,6 +97,62 @@
$this->total_rows = absint( $total_rows );
}

/**
* Generate and set filename based on applied filters.
*
* Format: dokan-withdraw-log{_vendor-name}{_status}{_method}{_date-range}.csv
* Only includes filter parts that are actually applied.
*
* @since DOKAN_SINCE
*
* @param array $filters Array of filter parameters (user_id, status, payment_method, start_date, end_date).
*
* @return void
*/
public function set_filename_from_filters( $filters = [] ) {
$parts = [ 'dokan-withdraw-log' ];

// Add vendor name if filtered by user_id.
if ( ! empty( $filters['user_id'] ) ) {
$store_info = dokan_get_store_info( absint( $filters['user_id'] ) );
$store_name = ! empty( $store_info['store_name'] )
? sanitize_title( $store_info['store_name'] )
: 'vendor-' . absint( $filters['user_id'] );

Check failure on line 121 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

Whitespace found at end of line
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

Fix trailing whitespace.

Static analysis detected trailing whitespace on lines 121 and 126. Please remove to comply with coding standards.

🔎 Proposed fix
             $store_name = ! empty( $store_info['store_name'] )
                 ? sanitize_title( $store_info['store_name'] )
                 : 'vendor-' . absint( $filters['user_id'] );
-            
+
             // Truncate if too long (max 50 chars for store name part).
             if ( strlen( $store_name ) > 50 ) {
                 $store_name = substr( $store_name, 0, 44 ) . '-trunc';
             }
-            
+
             $parts[] = $store_name;

Also applies to: 126-126

🧰 Tools
🪛 GitHub Check: Run PHPCS inspection

[failure] 121-121:
Whitespace found at end of line

🤖 Prompt for AI Agents
In includes/Admin/WithdrawLogExporter.php around lines 121 and 126, there are
trailing whitespace characters at the ends of those lines; remove the trailing
spaces so the lines end immediately after the intended code/text (no extra
spaces or tabs) and save the file to comply with coding standards and static
analysis.

// Truncate if too long (max 50 chars for store name part).
if ( strlen( $store_name ) > 50 ) {
$store_name = substr( $store_name, 0, 44 ) . '-trunc';
}

Check failure on line 126 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

Whitespace found at end of line
$parts[] = $store_name;
}

// Add status if filtered.
if ( ! empty( $filters['status'] ) ) {
$parts[] = sanitize_title( $filters['status'] );
}

// Add payment method if filtered.
if ( ! empty( $filters['payment_method'] ) ) {
$parts[] = sanitize_title( $filters['payment_method'] );
}

// Add date range if both start and end dates are provided (Y-M-d format).
if ( ! empty( $filters['start_date'] ) && ! empty( $filters['end_date'] ) ) {
$start_timestamp = strtotime( $filters['start_date'] );
$end_timestamp = strtotime( $filters['end_date'] );

// Only add if both dates are valid timestamps.
if ( $start_timestamp && $end_timestamp ) {
$start = strtolower( wp_date( 'Y-M-d', $start_timestamp ) );
$end = strtolower( wp_date( 'Y-M-d', $end_timestamp ) );
$parts[] = $start . '_to_' . $end;
}
}

$this->set_filename( implode( '_', $parts ) );
}

/**
* Return an array of columns to export.
*
Expand Down Expand Up @@ -199,24 +255,24 @@
$column_value = $withdraw_item['created'] ?? '';
break;

case "paypal_email":

Check failure on line 258 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "paypal_email" does not require double quotes; use single quotes instead
$column_value = $withdraw_item['details']['paypal']['email'] ?? '';
break;

case "skrill_email":

Check failure on line 262 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "skrill_email" does not require double quotes; use single quotes instead
$column_value = $withdraw_item['details']['skrill']['email'] ?? '';
break;

case "dokan_custom_method":

Check failure on line 266 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "dokan_custom_method" does not require double quotes; use single quotes instead
case "dokan_custom_value":

Check failure on line 267 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "dokan_custom_value" does not require double quotes; use single quotes instead
$field = substr_replace( $key, '', 0, 13 );
$column_value = $withdraw_item['details']['dokan_custom'][ $field ] ?? '';
break;

case "bank_ac_name":

Check failure on line 272 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "bank_ac_name" does not require double quotes; use single quotes instead
case "bank_ac_number":

Check failure on line 273 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "bank_ac_number" does not require double quotes; use single quotes instead
case "bank_ac_type":

Check failure on line 274 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "bank_ac_type" does not require double quotes; use single quotes instead
case "bank_bank_name":

Check failure on line 275 in includes/Admin/WithdrawLogExporter.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

String "bank_bank_name" does not require double quotes; use single quotes instead
case "bank_bank_addr":
case "bank_declaration":
case "bank_iban":
Expand Down
19 changes: 15 additions & 4 deletions includes/REST/WithdrawController.php
Original file line number Diff line number Diff line change
Expand Up @@ -374,13 +374,23 @@ public function get_items( $request ) {

// Export items.
$exporter = new WithdrawLogExporter();
$step = isset( $params['page'] ) ? absint( $params['page'] ) : 1;
$statuses = [ 'pending', 'completed', 'cancelled' ];
$step = isset( $request['page'] ) ? absint( $request['page'] ) : 1;

// Set dynamic filename based on filters BEFORE generating file.
$exporter->set_filename_from_filters(
[
'user_id' => $args['user_id'] ?? '',
'status' => ! empty( $request['status'] ) ? sanitize_text_field( $request['status'] ) : '',
'payment_method' => $args['method'] ?? '',
'start_date' => $args['start_date'] ?? '',
'end_date' => $args['end_date'] ?? '',
]
);

$exporter->set_items( $data );
$exporter->set_page( $step );
$exporter->set_limit( $args['limit'] );
$exporter->set_total_rows( $statuses[ $args['status'] ] );
$exporter->set_total_rows( $withdraws->total );
$exporter->generate_file();

$percent = $exporter->get_percent_complete();
Expand All @@ -392,7 +402,8 @@ public function get_items( $request ) {
$export_data['step'] = 'done';
$export_data['url'] = add_query_arg(
[
'download-withdraw-log-csv' => wp_create_nonce( 'download-withdraw-log-csv-nonce' ),
'download-withdraw-log-csv' => wp_create_nonce( 'download-withdraw-log-csv-nonce' ),
'filename' => $exporter->get_filename(),
],
admin_url( 'admin.php' )
);
Expand Down
6 changes: 6 additions & 0 deletions includes/Withdraw/Hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ public function download_withdraw_log_export_file() {

// Export withdraw logs.
$exporter = new \WeDevs\Dokan\Admin\WithdrawLogExporter();

// Set filename from URL parameter (generated during file creation).
if ( isset( $_GET['filename'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$exporter->set_filename( sanitize_file_name( wp_unslash( $_GET['filename'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}

$exporter->export();
}

Expand Down
73 changes: 60 additions & 13 deletions src/admin/dashboard/pages/withdraw/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
DokanModal,
} from '@dokan/components';

import { Trash, ArrowDown, Home, Calendar, CreditCard } from 'lucide-react';
import { Trash, ArrowDown, Home, Calendar, CreditCard, Loader2 } from 'lucide-react';

// Define withdraw statuses for tab filtering
const WITHDRAW_STATUSES = [
Expand Down Expand Up @@ -88,6 +88,8 @@ const WithdrawPage = () => {
approved: 0,
cancelled: 0,
} );
const [ isExporting, setIsExporting ] = useState( false );
const [ exportProgress, setExportProgress ] = useState( 0 );
const [ filterArgs, setFilterArgs ] = useState( {} );
const [ activeStatus, setActiveStatus ] = useState( 'pending' );
const [ vendorFilter, setVendorFilter ] = useState< VendorSelect | null >(
Expand Down Expand Up @@ -544,27 +546,72 @@ const WithdrawPage = () => {
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"
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"
disabled={ isExporting }
onClick={ async () => {
setIsExporting( true );
setExportProgress( 0 );
try {
// Minimal placeholder; backend export flow may vary.
// Attempt to hit export endpoint via same query params.
const path = addQueryArgs( 'dokan/v2/withdraw', {
...view,
is_export: true,
} );
const res = await apiFetch( { path } );
if ( res && res.url ) {
window.location.assign( res.url as string );
let page = 1;
let isComplete = false;
let downloadUrl = null;

// Poll until export is complete
while ( ! isComplete ) {
const path = addQueryArgs( 'dokan/v2/withdraw', {
per_page: 100,
page: page,
search: view?.search ?? '',
status: view?.status === 'all' ? '' : view?.status,
...filterArgs,
is_export: true,
} );

const res = await apiFetch( { path } );

// Update progress if available
if ( res?.percentage !== undefined ) {
setExportProgress( res.percentage );
}

if ( res?.step === 'done' ) {
isComplete = true;
downloadUrl = res.url;
} else if ( res?.step ) {
// Continue to next page
page = res.step;
} else {
throw new Error( 'Invalid response from export' );
}
}

if ( downloadUrl ) {
window.location.assign( downloadUrl as string );
}
} catch ( e ) {
// eslint-disable-next-line no-console
console.error( 'Export failed or not supported yet', e );
alert( __( 'Export failed. Please try again.', 'dokan-lite' ) );
} finally {
setIsExporting( false );
setExportProgress( 0 );
}
} }
>
Comment on lines 547 to 600
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

Potential memory leak: Add cleanup for unmounted component.

The export loop (lines 560-586) continues making API calls until completion, but there's no mechanism to abort if the component unmounts during export. This can cause memory leaks and unnecessary API calls.

Additionally, consider adding a maximum iteration limit as a safety mechanism to prevent infinite loops if the backend returns unexpected responses.

🔎 Suggested improvements

Consider using an AbortController to cancel pending requests on unmount:

 onClick={ async () => {
     setIsExporting( true );
     setExportProgress( 0 );
+    const abortController = new AbortController();
+    
+    // Cleanup function
+    const cleanup = () => {
+        abortController.abort();
+        setIsExporting( false );
+        setExportProgress( 0 );
+    };
+
     try {
         let page = 1;
         let isComplete = false;
         let downloadUrl = null;
+        const MAX_ITERATIONS = 1000; // Safety limit
+        let iterations = 0;

         // Poll until export is complete
         while ( ! isComplete ) {
+            if ( iterations++ >= MAX_ITERATIONS ) {
+                throw new Error( 'Export timeout: too many pages' );
+            }
+
             const path = addQueryArgs( 'dokan/v2/withdraw', {
                 per_page: 100,
                 page: page,
                 search: view?.search ?? '',
                 status: view?.status === 'all' ? '' : view?.status,
                 ...filterArgs,
                 is_export: true,
             } );

-            const res = await apiFetch( { path } );
+            const res = await apiFetch( { 
+                path,
+                signal: abortController.signal 
+            } );

Then use a useEffect cleanup:

useEffect(() => {
    // Return cleanup function that aborts export on unmount
    return () => {
        if (isExporting) {
            // abort logic here
        }
    };
}, [isExporting]);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/admin/dashboard/pages/withdraw/index.tsx around lines 547 to 600, the
export polling loop can continue after the component unmounts and may never
terminate; add cancellation and a safety iteration limit: create an
AbortController for the export flow and pass its signal to apiFetch calls, track
a mounted flag or use the controller to avoid calling setState after unmount,
enforce a maxIterations counter (e.g., 1000) to break the loop if responses are
unexpected, and add a useEffect cleanup that aborts the controller when the
component unmounts or when export is cancelled so pending requests stop and
state updates are prevented.

<ArrowDown size={ 16 } />
{ __( 'Export', 'dokan-lite' ) }
{ isExporting ? (
<>
<Loader2 className="animate-spin" size={ 16 } />
{ exportProgress > 0
? __( `Exporting (${ exportProgress }%)`, 'dokan-lite' )
: __( 'Exporting...', 'dokan-lite' )
}
</>
) : (
<>
<ArrowDown size={ 16 } />
{ __( 'Export', 'dokan-lite' ) }
</>
) }
</button>,
];

Expand Down
4 changes: 2 additions & 2 deletions src/admin/pages/Withdraw.vue
Original file line number Diff line number Diff line change
Expand Up @@ -669,8 +669,8 @@ export default {

const args = {
is_export: true,
per_page: self.perPage,
page: self.currentPage,
per_page: 100,
page: page,
status: self.currentStatus,
user_id: user_id,
payment_method: self.paymentMethods.id,
Expand Down
Loading