From 06c5d764877f97ea89d4d8f74bd929f904ef70c1 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Mon, 22 Sep 2025 21:58:37 -0300 Subject: [PATCH 01/10] Use rest endpoint instead of ajax --- .../forms/changelog/rest-export-endpoint | 4 + .../class-contact-form-endpoint.php | 139 ++++++++++++++++++ .../class-contact-form-plugin.php | 15 +- .../dashboard/hooks/use-export-responses.ts | 52 ++++--- 4 files changed, 180 insertions(+), 30 deletions(-) create mode 100644 projects/packages/forms/changelog/rest-export-endpoint diff --git a/projects/packages/forms/changelog/rest-export-endpoint b/projects/packages/forms/changelog/rest-export-endpoint new file mode 100644 index 0000000000000..10e4b0f83db86 --- /dev/null +++ b/projects/packages/forms/changelog/rest-export-endpoint @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add REST API endpoint for exporting form responses, replacing legacy AJAX implementation diff --git a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php index 7a613264da59e..25aafc1676e1a 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php @@ -255,6 +255,43 @@ public function register_routes() { 'callback' => array( $this, 'get_forms_config' ), ) ); + + register_rest_route( + $this->namespace, + $this->rest_base . '/export', + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'permission_callback' => array( $this, 'export_permissions_check' ), + 'callback' => array( $this, 'export_responses' ), + 'args' => array( + 'selected' => array( + 'type' => 'array', + 'items' => array( 'type' => 'integer' ), + 'default' => array(), + ), + 'post' => array( + 'type' => 'string', + 'default' => 'all', + ), + 'search' => array( + 'type' => 'string', + 'default' => '', + ), + 'status' => array( + 'type' => 'string', + 'default' => 'publish', + ), + 'before' => array( + 'type' => 'string', + 'default' => '', + ), + 'after' => array( + 'type' => 'string', + 'default' => '', + ), + ), + ) + ); } /** @@ -1070,4 +1107,106 @@ public function get_forms_config( WP_REST_Request $request ) { // phpcs:ignore V return rest_ensure_response( $config ); } + + /** + * Checks if a given request has permissions to export responses. + * + * @param WP_REST_Request $request The request object. + * @return bool|WP_Error True if the request can export, error object otherwise. + */ + public function export_permissions_check( $request ) { //phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + if ( is_super_admin() ) { + return true; + } + + if ( ! is_user_member_of_blog( get_current_user_id(), get_current_blog_id() ) ) { + return new WP_Error( + 'rest_user_not_member', + __( 'Sorry, you are not a member of this site.', 'jetpack-forms' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + if ( ! current_user_can( 'export' ) ) { + return new WP_Error( + 'rest_user_cannot_export', + __( 'Sorry, you are not allowed to export form responses on this site.', 'jetpack-forms' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Export form responses to CSV. + * + * @param WP_REST_Request $request The request object. + * @return WP_REST_Response|WP_Error The response containing CSV data or error. + */ + public function export_responses( $request ) { + $selected = $request->get_param( 'selected' ); + $post_id = $request->get_param( 'post' ); + $search = $request->get_param( 'search' ); + $status = $request->get_param( 'status' ); + $before = $request->get_param( 'before' ); + $after = $request->get_param( 'after' ); + + $query_args = array( + 'post_type' => 'feedback', + 'posts_per_page' => -1, + 'post_status' => array( 'publish' ), + 'order' => 'ASC', + 'suppress_filters' => false, + ); + + if ( $status && $status !== 'publish' ) { + $query_args['post_status'] = explode( ',', $status ); + } + + if ( $post_id && $post_id !== 'all' ) { + $query_args['post_parent'] = intval( $post_id ); + } + + if ( $search ) { + $query_args['s'] = $search; + } + + if ( ! empty( $selected ) ) { + $query_args['post__in'] = array_map( 'intval', $selected ); + } + + if ( $before || $after ) { + $date_query = array(); + if ( $before ) { + $date_query['before'] = $before; + } + if ( $after ) { + $date_query['after'] = $after; + } + $query_args['date_query'] = array( $date_query ); + } + + $feedback_posts = get_posts( $query_args ); + + if ( empty( $feedback_posts ) ) { + return new WP_Error( 'no_responses', __( 'No responses found', 'jetpack-forms' ), array( 'status' => 404 ) ); + } + + $feedback_ids = array_map( + function ( $post ) { + return $post->ID; + }, + $feedback_posts + ); + + $plugin = Contact_Form_Plugin::init(); + $export_data = $plugin->get_export_feedback_data( $feedback_ids ); + + if ( empty( $export_data ) ) { + return new WP_Error( 'no_responses', __( 'No responses found', 'jetpack-forms' ), array( 'status' => 404 ) ); + } + + $plugin->download_feedback_as_csv( $export_data, $post_id ); + } } diff --git a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php index 86dca7d957d1f..1022fb3fa15e3 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php @@ -211,9 +211,7 @@ protected function __construct() { add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'register_personal_data_exporter' ) ); add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_personal_data_eraser' ) ); - // Export to CSV feature if ( is_admin() ) { - add_action( 'wp_ajax_feedback_export', array( $this, 'download_feedback_as_csv' ) ); add_action( 'wp_ajax_create_new_form', array( $this, 'create_new_form' ) ); } add_action( 'admin_menu', array( $this, 'admin_menu' ) ); @@ -2758,21 +2756,20 @@ function ( $selected ) { /** * Download exported data as CSV + * + * @param array $data Export data to generate CSV from. + * @param string $post_id Optional. Post ID for filename generation. */ - public function download_feedback_as_csv() { - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verification is done on get_feedback_entries_from_post function - $post_data = wp_unslash( $_POST ); - $data = $this->get_feedback_entries_from_post(); - + public function download_feedback_as_csv( $data, $post_id = '' ) { if ( empty( $data ) ) { return; } // Check if we want to download all the feedbacks or just a certain contact form - if ( ! empty( $post_data['post'] ) && $post_data['post'] !== 'all' ) { + if ( ! empty( $post_id ) && $post_id !== 'all' ) { $filename = sprintf( '%s - %s.csv', - Util::get_export_filename( get_the_title( (int) $post_data['post'] ) ), + Util::get_export_filename( get_the_title( (int) $post_id ) ), gmdate( 'Y-m-d H:i' ) ); } else { diff --git a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts index f5ca2c5fd6a97..05de76d25c5d8 100644 --- a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts +++ b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts @@ -3,6 +3,7 @@ */ import jetpackAnalytics from '@automattic/jetpack-analytics'; import { useBreakpointMatch } from '@automattic/jetpack-components'; +import apiFetch from '@wordpress/api-fetch'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { useState, useCallback, useEffect } from '@wordpress/element'; @@ -10,16 +11,24 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { config } from '..'; import { store as dashboardStore } from '../store'; +type ExportData = { + selected: number[]; + post: string; + search: string; + status: string; + before?: string; + after?: string; +}; + type ExportHookReturn = { showExportModal: boolean; openModal: () => void; closeModal: () => void; autoConnectGdrive: boolean; userCanExport: boolean; - onExport: ( action: string, nonceName: string ) => Promise< Response >; + onExport: () => Promise< Response >; selectedResponsesCount: number; currentStatus: string; exportLabel: string; @@ -75,25 +84,26 @@ export default function useExportResponses(): ExportHookReturn { return { selected: getSelectedResponsesFromCurrentDataset(), currentQuery: getCurrentQuery() }; }, [] ); - const onExport = useCallback( - ( action: string, nonceName: string ) => { - const data = new FormData(); - data.append( 'action', action ); - data.append( nonceName, config( 'exportNonce' ) ); - selected.forEach( ( id: string ) => data.append( 'selected[]', id ) ); - data.append( 'post', currentQuery.parent || 'all' ); - data.append( 'search', currentQuery.search || '' ); - data.append( 'status', currentQuery.status ); - - if ( currentQuery.before && currentQuery.after ) { - data.append( 'before', currentQuery.before ); - data.append( 'after', currentQuery.after ); - } - - return fetch( window.ajaxurl, { method: 'POST', body: data } ); - }, - [ currentQuery, selected ] - ); + const onExport = useCallback( () => { + const exportData: ExportData = { + selected: selected.map( id => parseInt( id, 10 ) ), + post: currentQuery.parent ? String( currentQuery.parent ) : 'all', + search: currentQuery.search || '', + status: currentQuery.status || 'publish', + }; + + if ( currentQuery.before && currentQuery.after ) { + exportData.before = currentQuery.before; + exportData.after = currentQuery.after; + } + + return apiFetch( { + path: '/wp/v2/feedback/export', + method: 'POST', + data: exportData, + parse: false, + } ); + }, [ currentQuery, selected ] ); useEffect( () => { const url = new URL( window.location.href ); From f28cd8ad4b7c5f18f62717f0ee8659e0a0a6ae58 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Tue, 23 Sep 2025 19:11:07 -0300 Subject: [PATCH 02/10] not deprecating --- .../forms/src/contact-form/class-contact-form-plugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php index 1022fb3fa15e3..fd600f259a864 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php @@ -2760,7 +2760,7 @@ function ( $selected ) { * @param array $data Export data to generate CSV from. * @param string $post_id Optional. Post ID for filename generation. */ - public function download_feedback_as_csv( $data, $post_id = '' ) { + public function download_feedback_as_csv( $data = null, $post_id = '' ) { if ( empty( $data ) ) { return; } From 258fed20159ffca3b8dc9976518a22e163c9a540 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 24 Sep 2025 09:37:18 -0300 Subject: [PATCH 03/10] Refactor CSV export to use admin-post --- .../class-contact-form-endpoint.php | 28 ++++++++++++--- .../class-contact-form-plugin.php | 35 +++++++++++++++++++ .../dashboard/hooks/use-export-responses.ts | 12 +++++-- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php index 25aafc1676e1a..ec36dbc7a3658 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php @@ -1200,13 +1200,31 @@ function ( $post ) { $feedback_posts ); - $plugin = Contact_Form_Plugin::init(); - $export_data = $plugin->get_export_feedback_data( $feedback_ids ); - - if ( empty( $export_data ) ) { + if ( empty( $feedback_ids ) ) { return new WP_Error( 'no_responses', __( 'No responses found', 'jetpack-forms' ), array( 'status' => 404 ) ); } - $plugin->download_feedback_as_csv( $export_data, $post_id ); + $nonce = wp_create_nonce( 'feedback_export_' . implode( ',', $feedback_ids ) ); + + $download_url = add_query_arg( + array( + 'action' => 'feedback_export', + 'feedback_ids' => implode( ',', $feedback_ids ), + 'post_id' => $post_id, + 'search' => $search, + 'status' => $status, + 'before' => $before, + 'after' => $after, + 'nonce' => $nonce, + ), + admin_url( 'admin-post.php' ) + ); + + return rest_ensure_response( + array( + 'download_url' => $download_url, + 'count' => count( $feedback_ids ), + ) + ); } } diff --git a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php index fd600f259a864..439c62d5f1d5b 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-plugin.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-plugin.php @@ -214,6 +214,9 @@ protected function __construct() { if ( is_admin() ) { add_action( 'wp_ajax_create_new_form', array( $this, 'create_new_form' ) ); } + + // Admin-post action for CSV export + add_action( 'admin_post_feedback_export', array( $this, 'admin_post_feedback_export' ) ); add_action( 'admin_menu', array( $this, 'admin_menu' ) ); add_action( 'current_screen', array( $this, 'unread_count' ) ); add_action( 'current_screen', array( $this, 'redirect_edit_feedback_to_jetpack_forms' ) ); @@ -2754,6 +2757,38 @@ function ( $selected ) { return $this->get_export_feedback_data( $feedbacks ); } + /** + * Admin-post handler for CSV export + */ + public function admin_post_feedback_export() { + $feedback_ids_str = sanitize_text_field( wp_unslash( $_GET['feedback_ids'] ?? '' ) ); + $post_id = sanitize_text_field( wp_unslash( $_GET['post_id'] ?? '' ) ); + $nonce = sanitize_text_field( wp_unslash( $_GET['nonce'] ?? '' ) ); + + if ( empty( $feedback_ids_str ) || empty( $nonce ) ) { + wp_die( esc_html__( 'Invalid request parameters.', 'jetpack-forms' ), 400 ); + } + + $feedback_ids = explode( ',', $feedback_ids_str ); + $feedback_ids = array_map( 'intval', $feedback_ids ); + + if ( ! wp_verify_nonce( $nonce, 'feedback_export_' . $feedback_ids_str ) ) { + wp_die( esc_html__( 'Security check failed.', 'jetpack-forms' ), 403 ); + } + + if ( ! current_user_can( 'export' ) ) { + wp_die( esc_html__( 'You do not have permission to export form responses.', 'jetpack-forms' ), 403 ); + } + + $export_data = $this->get_export_feedback_data( $feedback_ids ); + + if ( empty( $export_data ) ) { + wp_die( esc_html__( 'No responses found to export.', 'jetpack-forms' ), 404 ); + } + + $this->download_feedback_as_csv( $export_data, $post_id ); + } + /** * Download exported data as CSV * diff --git a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts index 05de76d25c5d8..c5b54c7f51521 100644 --- a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts +++ b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts @@ -84,7 +84,7 @@ export default function useExportResponses(): ExportHookReturn { return { selected: getSelectedResponsesFromCurrentDataset(), currentQuery: getCurrentQuery() }; }, [] ); - const onExport = useCallback( () => { + const onExport = useCallback( async () => { const exportData: ExportData = { selected: selected.map( id => parseInt( id, 10 ) ), post: currentQuery.parent ? String( currentQuery.parent ) : 'all', @@ -97,12 +97,18 @@ export default function useExportResponses(): ExportHookReturn { exportData.after = currentQuery.after; } - return apiFetch( { + const response = await apiFetch( { path: '/wp/v2/feedback/export', method: 'POST', data: exportData, - parse: false, } ); + + if ( response && response.download_url ) { + // Trigger download by navigating to the URL + window.location.href = response.download_url; + return response; + } + throw new Error( 'Invalid response: missing download URL' ); }, [ currentQuery, selected ] ); useEffect( () => { From 8a3ac4c541f0398966ec9cfaeeeeb52511e88e92 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 24 Sep 2025 09:49:30 -0300 Subject: [PATCH 04/10] simpler --- .../src/contact-form/class-contact-form-endpoint.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php index ec36dbc7a3658..b346fe82b24b9 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php @@ -1193,16 +1193,7 @@ public function export_responses( $request ) { return new WP_Error( 'no_responses', __( 'No responses found', 'jetpack-forms' ), array( 'status' => 404 ) ); } - $feedback_ids = array_map( - function ( $post ) { - return $post->ID; - }, - $feedback_posts - ); - - if ( empty( $feedback_ids ) ) { - return new WP_Error( 'no_responses', __( 'No responses found', 'jetpack-forms' ), array( 'status' => 404 ) ); - } + $feedback_ids = wp_list_pluck( $feedback_posts, 'ID' ); $nonce = wp_create_nonce( 'feedback_export_' . implode( ',', $feedback_ids ) ); From b9d4bf10a22837083681454447a0e6d3702b57df Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 24 Sep 2025 09:56:09 -0300 Subject: [PATCH 05/10] add sanitize_calllbacks --- .../class-contact-form-endpoint.php | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php index b346fe82b24b9..a2ba9e469aa70 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php @@ -270,24 +270,29 @@ public function register_routes() { 'default' => array(), ), 'post' => array( - 'type' => 'string', - 'default' => 'all', + 'type' => 'string', + 'default' => 'all', + 'sanitize_callback' => 'sanitize_text_field', ), 'search' => array( - 'type' => 'string', - 'default' => '', + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', ), 'status' => array( - 'type' => 'string', - 'default' => 'publish', + 'type' => 'string', + 'default' => 'publish', + 'sanitize_callback' => 'sanitize_text_field', ), 'before' => array( - 'type' => 'string', - 'default' => '', + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', ), 'after' => array( - 'type' => 'string', - 'default' => '', + 'type' => 'string', + 'default' => '', + 'sanitize_callback' => 'sanitize_text_field', ), ), ) From 9299540e041f3f5842ef5feea4236dc5a5cc4844 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 24 Sep 2025 10:09:00 -0300 Subject: [PATCH 06/10] fix types --- .../forms/src/dashboard/hooks/use-export-responses.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts index c5b54c7f51521..65db6db5a577a 100644 --- a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts +++ b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts @@ -22,13 +22,18 @@ type ExportData = { after?: string; }; +type ExportResponse = { + download_url: string; + count: number; +}; + type ExportHookReturn = { showExportModal: boolean; openModal: () => void; closeModal: () => void; autoConnectGdrive: boolean; userCanExport: boolean; - onExport: () => Promise< Response >; + onExport: () => Promise< ExportResponse >; selectedResponsesCount: number; currentStatus: string; exportLabel: string; @@ -84,7 +89,7 @@ export default function useExportResponses(): ExportHookReturn { return { selected: getSelectedResponsesFromCurrentDataset(), currentQuery: getCurrentQuery() }; }, [] ); - const onExport = useCallback( async () => { + const onExport = useCallback( async (): Promise< ExportResponse > => { const exportData: ExportData = { selected: selected.map( id => parseInt( id, 10 ) ), post: currentQuery.parent ? String( currentQuery.parent ) : 'all', @@ -97,7 +102,7 @@ export default function useExportResponses(): ExportHookReturn { exportData.after = currentQuery.after; } - const response = await apiFetch( { + const response = await apiFetch< ExportResponse >( { path: '/wp/v2/feedback/export', method: 'POST', data: exportData, From 2d8dae5ce5c0230afe304437fcf395482a83a6c5 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 24 Sep 2025 10:10:27 -0300 Subject: [PATCH 07/10] simpler --- .../forms/src/dashboard/hooks/use-export-responses.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts index 65db6db5a577a..1c1857c8e7779 100644 --- a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts +++ b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts @@ -91,14 +91,16 @@ export default function useExportResponses(): ExportHookReturn { const onExport = useCallback( async (): Promise< ExportResponse > => { const exportData: ExportData = { - selected: selected.map( id => parseInt( id, 10 ) ), + selected: selected.map( Number ), post: currentQuery.parent ? String( currentQuery.parent ) : 'all', search: currentQuery.search || '', status: currentQuery.status || 'publish', }; - if ( currentQuery.before && currentQuery.after ) { + if ( currentQuery.before ) { exportData.before = currentQuery.before; + } + if ( currentQuery.after ) { exportData.after = currentQuery.after; } From 1149953c640ed758029131312cb3c44367a58d9b Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 24 Sep 2025 10:31:12 -0300 Subject: [PATCH 08/10] simpler --- .../components/export-responses-modal/index.tsx | 7 ++++++- .../src/dashboard/inbox/export-responses/csv.tsx | 16 +--------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx b/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx index 1b263d953421c..b413d0cdf71cf 100644 --- a/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx +++ b/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx @@ -11,9 +11,14 @@ import GoogleDriveExport from '../../inbox/export-responses/google-drive'; import './style.scss'; +type ExportResponse = { + download_url: string; + count: number; +}; + type ExportResponsesModalProps = { onRequestClose: () => void; - onExport: ( action: string, nonceName: string ) => Promise< Response >; + onExport: () => Promise< ExportResponse >; autoConnectGdrive: boolean; }; diff --git a/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx b/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx index a7b5a31ffb6a1..ca1992bf88cf6 100644 --- a/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx +++ b/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx @@ -16,21 +16,7 @@ const CSVExport = ( { onExport } ) => { screen: 'form-responses-inbox', } ); - onExport( 'feedback_export', 'feedback_export_nonce_csv' ).then( async response => { - const blob = await response.blob(); - - const a = document.createElement( 'a' ); - a.href = window.URL.createObjectURL( blob ); - - const contentDispositionHeader = response.headers.get( 'Content-Disposition' ) ?? ''; - a.download = - contentDispositionHeader.split( 'filename=' )[ 1 ] || 'Jetpack Form Responses.csv'; - - document.body.appendChild( a ); - a.click(); - document.body.removeChild( a ); - window.URL.revokeObjectURL( a.href ); - } ); + onExport(); }, [ onExport, tracks ] ); const buttonClasses = clsx( 'button', 'export-button', 'export-csv' ); From ff69dc1e05e71edc70ecca9adc07acd017e9d3a1 Mon Sep 17 00:00:00 2001 From: Miguel Lezama Date: Wed, 24 Sep 2025 11:02:08 -0300 Subject: [PATCH 09/10] remove unused params --- .../forms/src/contact-form/class-contact-form-endpoint.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php index 963211dd92513..e532c5fa28d25 100644 --- a/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php +++ b/projects/packages/forms/src/contact-form/class-contact-form-endpoint.php @@ -1204,11 +1204,6 @@ public function export_responses( $request ) { array( 'action' => 'feedback_export', 'feedback_ids' => implode( ',', $feedback_ids ), - 'post_id' => $post_id, - 'search' => $search, - 'status' => $status, - 'before' => $before, - 'after' => $after, 'nonce' => $nonce, ), admin_url( 'admin-post.php' ) From a8e5b03b681327a95100f75ada5ca276f7acd1a7 Mon Sep 17 00:00:00 2001 From: Mikael Korpela Date: Thu, 25 Sep 2025 12:43:17 +0300 Subject: [PATCH 10/10] Add isExporting state and use it in the button --- .../export-responses-modal/index.tsx | 4 ++- .../dashboard/hooks/use-export-responses.ts | 31 ++++++++++++------- .../dashboard/inbox/export-responses/csv.tsx | 15 +++++++-- .../inbox/export-responses/index.tsx | 2 ++ 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx b/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx index a6bb7aee64f60..5c381151e8333 100644 --- a/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx +++ b/projects/packages/forms/src/dashboard/components/export-responses-modal/index.tsx @@ -18,12 +18,14 @@ type ExportResponsesModalProps = { onRequestClose: () => void; onExport: () => Promise< ExportResponse >; autoConnectGdrive: boolean; + isExporting?: boolean; }; const ExportResponsesModal = ( { onRequestClose, onExport, autoConnectGdrive, + isExporting, }: ExportResponsesModalProps ) => { return ( - + diff --git a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts index 1c1857c8e7779..848272dc63341 100644 --- a/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts +++ b/projects/packages/forms/src/dashboard/hooks/use-export-responses.ts @@ -34,6 +34,7 @@ type ExportHookReturn = { autoConnectGdrive: boolean; userCanExport: boolean; onExport: () => Promise< ExportResponse >; + isExporting: boolean; selectedResponsesCount: number; currentStatus: string; exportLabel: string; @@ -89,7 +90,10 @@ export default function useExportResponses(): ExportHookReturn { return { selected: getSelectedResponsesFromCurrentDataset(), currentQuery: getCurrentQuery() }; }, [] ); + const [ isExporting, setIsExporting ] = useState( false ); + const onExport = useCallback( async (): Promise< ExportResponse > => { + setIsExporting( true ); const exportData: ExportData = { selected: selected.map( Number ), post: currentQuery.parent ? String( currentQuery.parent ) : 'all', @@ -104,18 +108,22 @@ export default function useExportResponses(): ExportHookReturn { exportData.after = currentQuery.after; } - const response = await apiFetch< ExportResponse >( { - path: '/wp/v2/feedback/export', - method: 'POST', - data: exportData, - } ); - - if ( response && response.download_url ) { - // Trigger download by navigating to the URL - window.location.href = response.download_url; - return response; + try { + const response = await apiFetch< ExportResponse >( { + path: '/wp/v2/feedback/export', + method: 'POST', + data: exportData, + } ); + + if ( response && response.download_url ) { + // Trigger download by navigating to the URL + window.location.href = response.download_url; + return response; + } + throw new Error( 'Invalid response: missing download URL' ); + } finally { + setIsExporting( false ); } - throw new Error( 'Invalid response: missing download URL' ); }, [ currentQuery, selected ] ); useEffect( () => { @@ -138,6 +146,7 @@ export default function useExportResponses(): ExportHookReturn { autoConnectGdrive, userCanExport, onExport, + isExporting, selectedResponsesCount, currentStatus, exportLabel, diff --git a/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx b/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx index ca1992bf88cf6..a095d10269d02 100644 --- a/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx +++ b/projects/packages/forms/src/dashboard/inbox/export-responses/csv.tsx @@ -7,7 +7,12 @@ import { useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; -const CSVExport = ( { onExport } ) => { +type CSVExportProps = { + onExport: () => void; + isExporting?: boolean; +}; + +const CSVExport = ( { onExport, isExporting = false }: CSVExportProps ) => { const { tracks } = useAnalytics(); const downloadCSV = useCallback( () => { @@ -47,7 +52,13 @@ const CSVExport = ( { onExport } ) => { { __( 'Download your form response data as a CSV file.', 'jetpack-forms' ) }
-
diff --git a/projects/packages/forms/src/dashboard/inbox/export-responses/index.tsx b/projects/packages/forms/src/dashboard/inbox/export-responses/index.tsx index 0b7693fb8db71..652557d08c946 100644 --- a/projects/packages/forms/src/dashboard/inbox/export-responses/index.tsx +++ b/projects/packages/forms/src/dashboard/inbox/export-responses/index.tsx @@ -19,6 +19,7 @@ const ExportResponsesButton = () => { userCanExport, onExport, autoConnectGdrive, + isExporting, exportLabel, } = useExportResponses(); @@ -43,6 +44,7 @@ const ExportResponsesButton = () => { onRequestClose={ closeModal } onExport={ onExport } autoConnectGdrive={ autoConnectGdrive } + isExporting={ isExporting } /> ) }