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
12 changes: 12 additions & 0 deletions src/js/_enqueues/admin/inline-edit-post.js
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,13 @@ $( function() {
var locked = data['wp-check-locked-posts'] || {},
lockedClass = 'wp-locked';

// A post change can affect filtering, ordering, and pagination.
// Defer reloading while an inline editor has unsaved changes.
if ( data['wp-refresh-post-list'] && ! $( '#the-list tr.inline-editor' ).length ) {
window.location.reload();
return;
}

$('#the-list tr').each( function(i, el) {
var key = el.id, row = $(el), lock_data, avatar;

Expand Down Expand Up @@ -659,6 +666,11 @@ $( function() {
if ( check.length ) {
data['wp-check-locked-posts'] = check;
}

data['wp-check-post-list'] = {
post_type: $( '.post_type_page' ).val(),
last_changed: $( '#posts-filter' ).attr( 'data-wp-post-list-last-changed' )
};
});

})( jQuery, window.wp );
3 changes: 2 additions & 1 deletion src/wp-admin/edit.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
exit;
}

$post_list_table_last_changed = _wp_get_post_list_table_last_changed( $post_type );
$wp_list_table->prepare_items();

wp_enqueue_script( 'inline-edit-post' );
Expand Down Expand Up @@ -485,7 +486,7 @@

<?php $wp_list_table->views(); ?>

<form id="posts-filter" method="get">
<form id="posts-filter" method="get" data-wp-post-list-last-changed="<?php echo esc_attr( $post_list_table_last_changed ); ?>">

<?php $wp_list_table->search_box( $post_type_object->labels->search_items, 'post' ); ?>

Expand Down
1 change: 1 addition & 0 deletions src/wp-admin/includes/admin-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
add_action( 'update_option_new_admin_email', 'update_option_new_admin_email', 10, 2 );

add_filter( 'heartbeat_received', 'wp_check_locked_posts', 10, 3 );
add_filter( 'heartbeat_received', 'wp_check_post_list_table_changes', 10, 2 );
add_filter( 'heartbeat_received', 'wp_refresh_post_lock', 10, 3 );
add_filter( 'heartbeat_received', 'heartbeat_autosave', 500, 2 );

Expand Down
34 changes: 34 additions & 0 deletions src/wp-admin/includes/misc.php
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,40 @@ function wp_check_locked_posts( $response, $data, $screen_id ) {
return $response;
}

/**
* Checks whether a post list table has changed.
*
* @since x.x.x
*
* @param array $response The Heartbeat response.
* @param array $data The $_POST data sent.
* @return array The Heartbeat response.
*/
function wp_check_post_list_table_changes( $response, $data ) {
if ( empty( $data['wp-check-post-list'] ) || ! is_array( $data['wp-check-post-list'] ) ) {
return $response;
}

$check = $data['wp-check-post-list'];

if ( empty( $check['post_type'] ) || ! is_string( $check['post_type'] ) || ! array_key_exists( 'last_changed', $check ) || ! is_string( $check['last_changed'] ) ) {
return $response;
}

$post_type = sanitize_key( $check['post_type'] );
$post_type_object = get_post_type_object( $post_type );

if ( 'attachment' === $post_type || ! $post_type_object || ! $post_type_object->show_ui || ! current_user_can( $post_type_object->cap->edit_posts ) ) {
return $response;
}

if ( _wp_get_post_list_table_last_changed( $post_type ) !== $check['last_changed'] ) {
$response['wp-refresh-post-list'] = true;
}

return $response;
}

/**
* Checks lock status on the New/Edit Post screen and refresh the lock.
*
Expand Down
10 changes: 10 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,16 @@
// Create a revision whenever a post is updated.
add_action( 'wp_after_insert_post', 'wp_save_post_revision_on_insert', 9, 3 );
add_action( 'post_updated', 'wp_save_post_revision', 10, 1 );

// Update post list table change tokens after post mutations.
add_action( 'wp_after_insert_post', '_wp_set_post_list_table_last_changed', 10, 4 );
add_action( 'deleted_post', '_wp_set_post_list_table_last_changed', 10, 2 );
add_action( 'added_post_meta', '_wp_set_post_list_table_last_changed_for_post_meta', 10, 2 );
add_action( 'updated_post_meta', '_wp_set_post_list_table_last_changed_for_post_meta', 10, 2 );
add_action( 'deleted_post_meta', '_wp_set_post_list_table_last_changed_for_post_meta', 10, 2 );
add_action( 'added_term_relationship', '_wp_set_post_list_table_last_changed_for_terms', 10, 3 );
add_action( 'deleted_term_relationships', '_wp_set_post_list_table_last_changed_for_terms', 10, 3 );
add_action( 'shutdown', '_wp_finalize_post_list_table_last_changed' );
add_action( 'publish_post', '_publish_post_hook', 5, 1 );
add_action( 'transition_post_status', '_transition_post_status', 5, 3 );
add_action( 'transition_post_status', '_update_term_count_on_transition_post_status', 10, 3 );
Expand Down
140 changes: 140 additions & 0 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -8634,6 +8634,146 @@
wp_cache_set_last_changed( 'posts' );
}

/**
* Retrieves the last changed token for a post type's list table.
*
* For internal use.
*
* @since x.x.x
* @access private
*
* @param string $post_type Post type.
* @return string Last changed token, or an empty string if no changes have been recorded.
*/
function _wp_get_post_list_table_last_changed( $post_type ) {
return get_option( '_wp_post_list_table_last_changed_' . sanitize_key( $post_type ), '' );
}

/**
* Marks post list table change tokens for update.
*
* The first post save or delete updates its token immediately. Standalone meta
* and term mutations, and additional changes, are finalized at shutdown.
*
* For internal use.
*
* @since x.x.x
* @access private
*
* @global array $_wp_post_list_table_changed
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool|null $update Whether this is an existing post being updated.
* @param WP_Post|null $post_before Post object before the update, or null.
* @param bool $defer Whether to defer the first token update until shutdown.
*/
function _wp_set_post_list_table_last_changed( $post_id, $post, $update = null, $post_before = null, $defer = false ) {
global $_wp_post_list_table_changed;

if ( 'auto-draft' === $post->post_status && ( ! $post_before || 'auto-draft' === $post_before->post_status ) ) {
return;
}

$post_types = array( $post->post_type );

if ( $post_before && $post_before->post_type !== $post->post_type ) {
$post_types[] = $post_before->post_type;
}

foreach ( $post_types as $post_type ) {
$post_type_object = get_post_type_object( $post_type );

if ( 'attachment' === $post_type || ! $post_type_object || ! $post_type_object->show_ui ) {
continue;
}

if ( ! isset( $_wp_post_list_table_changed[ $post_type ] ) && $defer ) {
$_wp_post_list_table_changed[ $post_type ] = 'pending';
continue;
}

if ( isset( $_wp_post_list_table_changed[ $post_type ] ) && 'pending' === $_wp_post_list_table_changed[ $post_type ] && $defer ) {
continue;
}

if ( ! isset( $_wp_post_list_table_changed[ $post_type ] ) || 'pending' === $_wp_post_list_table_changed[ $post_type ] ) {
$_wp_post_list_table_changed[ $post_type ] = false;

if ( ! update_option( '_wp_post_list_table_last_changed_' . $post_type, wp_generate_uuid4(), false ) && false === $_wp_post_list_table_changed[ $post_type ] ) {

Check warning on line 8703 in src/wp-includes/post.php

View workflow job for this annotation

GitHub Actions / PHP static analysis / Run PHP static analysis

Strict comparison using === between false and false will always evaluate to true.
$_wp_post_list_table_changed[ $post_type ] = true;
}
} else {
$_wp_post_list_table_changed[ $post_type ] = true;
}
}
}

/**
* Marks a post list table change token after a post meta mutation.
*
* For internal use.
*
* @since x.x.x
* @access private
*
* @param int $meta_id Post meta ID.
* @param int $post_id Post ID.
*/
function _wp_set_post_list_table_last_changed_for_post_meta( $meta_id, $post_id ) {
$post = get_post( $post_id );

if ( $post ) {
_wp_set_post_list_table_last_changed( $post_id, $post, null, null, true );
}
}

/**
* Marks a post list table change token after a term relationship mutation.
*
* For internal use.
*
* @since x.x.x
* @access private
*
* @param int $post_id Post ID.
* @param int|int[] $tt_ids Term taxonomy ID or IDs.
* @param string $taxonomy Taxonomy slug.
*/
function _wp_set_post_list_table_last_changed_for_terms( $post_id, $tt_ids, $taxonomy ) {
$post = get_post( $post_id );

if ( $post && is_object_in_taxonomy( $post->post_type, $taxonomy ) ) {
_wp_set_post_list_table_last_changed( $post_id, $post, null, null, true );
}
}

/**
* Finalizes coalesced post list table change token updates.
*
* For internal use.
*
* @since x.x.x
* @access private
*
* @global array $_wp_post_list_table_changed
*/
function _wp_finalize_post_list_table_last_changed() {
global $_wp_post_list_table_changed;

if ( empty( $_wp_post_list_table_changed ) ) {
return;
}

foreach ( $_wp_post_list_table_changed as $post_type => $needs_update ) {
if ( $needs_update ) {
update_option( '_wp_post_list_table_last_changed_' . $post_type, wp_generate_uuid4(), false );
}
}

$_wp_post_list_table_changed = array();
}

/**
* Gets all available post MIME types for a given post type.
*
Expand Down
78 changes: 77 additions & 1 deletion tests/e2e/specs/edit-posts.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
import { test, expect } from '@wordpress/e2e-test-utils-playwright';
import { Editor, test, expect } from '@wordpress/e2e-test-utils-playwright';

test.describe( 'Edit Posts', () => {
test.beforeEach( async ( { requestUtils }) => {
Expand Down Expand Up @@ -107,6 +107,82 @@ test.describe( 'Edit Posts', () => {
expect( posts.first() ).toHaveText( `${ title } Edited` );
} );

test( 'refreshes the post list when a post is updated in another window', async ( {
admin,
page,
requestUtils,
} ) => {
const originalTitle = 'Original title';
const updatedTitle = 'Updated title';
const post = await requestUtils.createPost( {
title: originalTitle,
status: 'publish',
} );

await admin.visitAdminPage( '/edit.php' );
const rowTitle = page.locator( `#post-${ post.id } .row-title` );
await expect( rowTitle ).toHaveText( originalTitle );

const otherPage = await page.context().newPage();
const otherEditor = new Editor( { page: otherPage } );
await otherPage.goto( `/wp-admin/post.php?post=${ post.id }&action=edit` );

const welcomeDialog = otherPage.getByRole( 'dialog', { name: 'Welcome to the editor' } );
if ( await welcomeDialog.isVisible() ) {
await welcomeDialog.getByRole( 'button', { name: 'Close' } ).click();
}

await otherEditor.canvas.getByRole( 'textbox', { name: 'Add title' } ).fill( updatedTitle );
await otherPage.getByRole( 'button', { name: 'Save', exact: true } ).click();
await expect.poll( async () => {
const updatedPost = await requestUtils.rest( {
path: `/wp/v2/posts/${ post.id }`,
params: { context: 'edit' },
} );
return updatedPost.title.raw;
} ).toBe( updatedTitle );

await otherPage.close();
await page.bringToFront();
await page.evaluate( () => window.wp.heartbeat.connectNow() );

await expect( rowTitle ).toHaveText( updatedTitle );
} );

test( 'defers refreshing the post list while Quick Edit has unsaved changes', async ( {
admin,
page,
requestUtils,
} ) => {
const post = await requestUtils.createPost( {
title: 'Original title',
status: 'publish',
} );

await admin.visitAdminPage( '/edit.php' );
await page.locator( `#post-${ post.id } .editinline` ).evaluate( ( button ) => button.click() );

const titleInput = page.locator( `#edit-${ post.id } input[name="post_title"]` );
await titleInput.fill( 'Unsaved title' );
await requestUtils.rest( {
method: 'POST',
path: `/wp/v2/posts/${ post.id }`,
data: { title: 'Updated title' },
} );

const heartbeatResponse = page.waitForResponse( ( response ) =>
response.url().includes( 'admin-ajax.php' ) &&
response.request().postData()?.includes( 'wp-check-post-list' )
);
await page.evaluate( () => window.wp.heartbeat.connectNow() );
await heartbeatResponse;
await expect( titleInput ).toHaveValue( 'Unsaved title' );

await page.locator( `#edit-${ post.id } .cancel` ).click();
await page.evaluate( () => window.wp.heartbeat.connectNow() );
await expect( page.locator( `#post-${ post.id } .row-title` ) ).toHaveText( 'Updated title' );
} );

test( 'allows an existing post to be deleted using the Trash button', async ( {
admin,
editor,
Expand Down
Loading
Loading