Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: changed

Forms: optimize inbox performance
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ public function register_routes() {
'callback' => array( $this, 'get_forms_config' ),
)
);

register_rest_route(
$this->namespace,
$this->rest_base . '/counts',
array(
'methods' => \WP_REST_Server::READABLE,
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'callback' => array( $this, 'get_status_counts' ),
)
);
}

/**
Expand All @@ -263,43 +273,53 @@ public function register_routes() {
* @return WP_REST_Response Response object on success.
*/
public function get_filters() {
// TODO: investigate how we can do this better regarding usage of $wpdb
// performance by querying all the entities, etc..
global $wpdb;

$cache_key = 'jetpack_forms_filters';
$cached_result = get_transient( $cache_key );
if ( false !== $cached_result ) {
return rest_ensure_response( $cached_result );
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$months = $wpdb->get_results(
"SELECT DISTINCT YEAR( post_date ) AS year, MONTH( post_date ) AS month
FROM $wpdb->posts
WHERE post_type = 'feedback'
AND post_status IN ('publish', 'draft')
ORDER BY post_date DESC"
);
// phpcs:enable

$source_ids = Contact_Form_Plugin::get_all_parent_post_ids(
array_diff_key( array( 'post_status' => array( 'draft', 'publish', 'spam', 'trash' ) ), array( 'post_parent' => '' ) )
array( 'post_status' => array( 'draft', 'publish' ) )
);
return rest_ensure_response(
array(
'date' => array_map(
static function ( $row ) {
return array(
'month' => (int) $row->month,
'year' => (int) $row->year,
);
},
$months
),
'source' => array_map(
static function ( $post_id ) {
return array(
'id' => $post_id,
'title' => get_the_title( $post_id ),
'url' => get_permalink( $post_id ),
);
},
$source_ids
),
)

$result = array(
'date' => array_map(
static function ( $row ) {
return array(
'month' => (int) $row->month,
'year' => (int) $row->year,
);
},
$months
),
'source' => array_map(
static function ( $post_id ) {
return array(
'id' => $post_id,
'title' => get_the_title( $post_id ),
'url' => get_permalink( $post_id ),
);
},
$source_ids
),
);

set_transient( $cache_key, $result, 5 * MINUTE_IN_SECONDS );

return rest_ensure_response( $result );
}

/**
Expand Down Expand Up @@ -1068,4 +1088,47 @@ public function get_forms_config( WP_REST_Request $request ) { // phpcs:ignore V

return rest_ensure_response( $config );
}

/**
* Get optimized status counts for feedback posts.
*
* @param WP_REST_Request $request Request object.
* @return WP_REST_Response Response object.
*/
public function get_status_counts( WP_REST_Request $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
global $wpdb;

$cache_key = 'jetpack_forms_status_counts';
$cached_result = get_transient( $cache_key );
if ( false !== $cached_result ) {
return rest_ensure_response( $cached_result );
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
$counts = $wpdb->get_row(
$wpdb->prepare(
"SELECT
SUM(CASE WHEN post_status IN ('publish', 'draft') THEN 1 ELSE 0 END) as inbox,
SUM(CASE WHEN post_status = %s THEN 1 ELSE 0 END) as spam,
SUM(CASE WHEN post_status = %s THEN 1 ELSE 0 END) as trash
FROM $wpdb->posts
WHERE post_type = %s",
'spam',
'trash',
'feedback'
),
ARRAY_A
);
// phpcs:enable

$result = array(
'inbox' => (int) ( $counts['inbox'] ?? 0 ),
'spam' => (int) ( $counts['spam'] ?? 0 ),
'trash' => (int) ( $counts['trash'] ?? 0 ),
);

set_transient( $cache_key, $result, 30 );

return rest_ensure_response( $result );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ class Contact_Form_Plugin {
*/
public static $step_count = 0;

/**
* REST controller instance used for cache invalidation.
*
* @var string
*/
private $post_type = 'feedback';

/*
* Field keys that might be present in the entry json but we don't want to show to the admin
* since they not something that the visitor entered into the form.
Expand Down Expand Up @@ -180,6 +187,9 @@ public static function strip_tags( $data_with_tags ) {
protected function __construct() {
$this->add_shortcode();

add_action( 'transition_post_status', array( $this, 'maybe_invalidate_caches' ), 10, 3 );
add_action( 'deleted_post', array( $this, 'maybe_invalidate_caches_on_delete' ), 10, 2 );

// While generating the output of a text widget with a contact-form shortcode, we need to know its widget ID.
add_action( 'dynamic_sidebar', array( $this, 'track_current_widget' ) );
add_action( 'dynamic_sidebar_before', array( $this, 'track_current_widget_before' ) );
Expand Down Expand Up @@ -224,7 +234,7 @@ protected function __construct() {

// custom post type we'll use to keep copies of the feedback items
register_post_type(
'feedback',
$this->post_type,
array(
'labels' => array(
'name' => __( 'Form Responses', 'jetpack-forms' ),
Expand Down Expand Up @@ -2967,17 +2977,45 @@ public function esc_csv( $field ) {
* @return array The array of post IDs
*/
public static function get_all_parent_post_ids( $query_args = array() ) {
global $wpdb;

$default_query_args = array(
'fields' => 'id=>parent',
'posts_per_page' => 100000, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
'post_type' => 'feedback',
'post_status' => 'publish',
'suppress_filters' => false,
'post_status' => 'publish',
);
$args = array_merge( $default_query_args, $query_args );
// Get the feedbacks' parents' post IDs
$feedbacks = get_posts( $args );
return array_values( array_unique( array_values( $feedbacks ) ) );

$statuses = is_array( $args['post_status'] ) ? $args['post_status'] : explode( ',', $args['post_status'] );
$statuses = array_map( 'trim', $statuses );
sort( $statuses );

$cache_key = 'jetpack_forms_parent_ids_' . md5( implode( ',', $statuses ) );

$cached_result = get_transient( $cache_key );
if ( false !== $cached_result ) {
return $cached_result;
}

$placeholders = implode( ',', array_fill( 0, count( $statuses ), '%s' ) );

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$parent_ids = $wpdb->get_col(
$wpdb->prepare(
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
"SELECT DISTINCT post_parent
FROM $wpdb->posts
WHERE post_type = %s
AND post_status IN ($placeholders)
AND post_parent > 0
ORDER BY post_parent ASC",
array_merge( array( 'feedback' ), $statuses )
)
);
// phpcs:enable
$parent_ids = array_map( 'intval', $parent_ids );

set_transient( $cache_key, $parent_ids, 5 * MINUTE_IN_SECONDS );

return $parent_ids;
}

/**
Expand Down Expand Up @@ -3443,4 +3481,50 @@ public function redirect_edit_feedback_to_jetpack_forms() {
wp_safe_redirect( $redirect_url );
exit;
}

/**
* Maybe invalidate caches when a post status changes.
*
* Hooked to transition_post_status action.
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param \WP_Post $post Post object.
*/
public function maybe_invalidate_caches( $new_status, $old_status, $post ) {
if ( $this->post_type !== $post->post_type || $new_status === $old_status ) {
return;
}

$this->invalidate_feedback_caches();
}

/**
* Maybe invalidate caches when a post is deleted.
*
* Hooked to deleted_post action.
*
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
*/
public function maybe_invalidate_caches_on_delete( $post_id, $post ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionPar
if ( $this->post_type !== $post->post_type ) {
return;
}

$this->invalidate_feedback_caches();
}

/**
* Invalidate feedback caches when content changes.
*/
private function invalidate_feedback_caches() {
delete_transient( 'jetpack_forms_status_counts' );
delete_transient( 'jetpack_forms_filters' );
delete_transient( 'jetpack_forms_parent_ids_' . md5( 'publish' ) );
delete_transient( 'jetpack_forms_parent_ids_' . md5( 'draft,publish' ) );

wp_cache_delete( 'jetpack_forms_parent_ids_' . md5( 'publish' ), 'transient' );
wp_cache_delete( 'jetpack_forms_parent_ids_' . md5( 'draft,publish' ), 'transient' );
}
}
43 changes: 39 additions & 4 deletions projects/packages/forms/src/dashboard/class-dashboard.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public function load_admin_scripts() {
'in_footer' => true,
'textdomain' => 'jetpack-forms',
'enqueue' => true,
'dependencies' => array( 'wp-api-fetch' ),
'dependencies' => array( 'wp-api-fetch', 'wp-data', 'wp-core-data', 'wp-dom-ready' ),
)
);

Expand All @@ -83,14 +83,49 @@ public function load_admin_scripts() {
Connection_Initial_State::render_script( self::SCRIPT_HANDLE );

// Preload Forms endpoints needed in dashboard context.
$preload_paths = array(
// Pre-fetch the first inbox page so the UI renders instantly on first load.
$preload_params = array(
'_fields' => 'id,status,date,date_gmt,author_name,author_email,author_url,author_avatar,ip,entry_title,entry_permalink,has_file,fields',
'context' => 'view',
'order' => 'desc',
'orderby' => 'date',
'page' => 1,
'per_page' => 20,
'status' => 'draft,publish',
);
\ksort( $preload_params );
$initial_responses_path = \add_query_arg( $preload_params, '/wp/v2/feedback' );
$initial_responses_locale_path = \add_query_arg(
\array_merge(
$preload_params,
array( '_locale' => 'user' )
),
'/wp/v2/feedback'
);
$preload_paths = array(
'/wp/v2/types?context=view',
'/wp/v2/feedback/config',
'/wp/v2/feedback/integrations?version=2',
'/wp/v2/feedback/counts',
'/wp/v2/feedback/filters',
$initial_responses_path,
$initial_responses_locale_path,
);
$preload_data = array_reduce( $preload_paths, 'rest_preload_api_request', array() );
$preload_data_raw = array_reduce( $preload_paths, 'rest_preload_api_request', array() );

// Normalize keys to match what apiFetch will request (without domain).
$preload_data = array();
foreach ( $preload_data_raw as $key => $value ) {
$normalized_key = preg_replace( '#^https?://[^/]+/wp-json#', '', $key );
$preload_data[ $normalized_key ] = $value;
}

wp_add_inline_script(
self::SCRIPT_HANDLE,
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( ' . wp_json_encode( $preload_data ) . ' ) );',
sprintf(
'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );',
wp_json_encode( $preload_data )
),
'before'
);
}
Expand Down
Loading