Make WordPress Core

Changeset 65060


Ignore:
Timestamp:
10/30/2026 05:18:14 PM (3 days ago)
Author:
lancewillett
Message:

General: Preserve Walker hierarchy when children sort first.

When no top-level element is included, select the first element whose parent is absent from the input instead of assuming the first element is a root. Use the configured db_fields and retain the first-element fallback for cycles.

This preserves the hierarchy and depth limits when wp_list_pages() sorts descendants before their included ancestors.

Developed in: ​https://github.com/WordPress/wordpress-develop/pull/14684

Props rob-castlegate, salvatorecorsi, mukesh27.
Fixes #28801.

Location:
trunk
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-walker.php

    r63542 r65060  
    231231                /*
    232232                 * When none of the elements is top level.
    233                  * Assume the first one must be root of the sub elements.
     233                 * Find an element whose parent is not in the list to use as the root.
    234234                 */
    235235                if ( empty( $top_level_elements ) ) {
    236 
     236                        $id_field    = $this->db_fields['id'];
     237                        $element_ids = array();
     238                        foreach ( $elements as $e ) {
     239                                $element_ids[ $e->$id_field ] = true;
     240                        }
     241
     242                        // Fall back to the first element when all parents are included, as with a cycle.
    237243                        $root = array_first( $elements );
     244                        foreach ( $elements as $e ) {
     245                                if ( ! isset( $element_ids[ $e->$parent_field ] ) ) {
     246                                        $root = $e;
     247                                        break;
     248                                }
     249                        }
    238250
    239251                        $top_level_elements = array();
  • trunk/tests/phpunit/tests/post/wpListPages.php

    r58812 r65060  
    541541                );
    542542        }
     543
     544        /**
     545         * @ticket 28801
     546         * @dataProvider data_wp_list_pages_omitted_ancestor
     547         *
     548         * @param int $depth Maximum depth to display.
     549         */
     550        public function test_wp_list_pages_preserves_hierarchy_when_menu_order_places_child_first( $depth ) {
     551                $ancestor = self::factory()->post->create( array( 'post_type' => 'page' ) );
     552                $parent   = self::factory()->post->create(
     553                        array(
     554                                'post_type'   => 'page',
     555                                'post_parent' => $ancestor,
     556                                'post_title'  => 'Included parent',
     557                                'menu_order'  => 10,
     558                        )
     559                );
     560                $child    = self::factory()->post->create(
     561                        array(
     562                                'post_type'   => 'page',
     563                                'post_parent' => $parent,
     564                                'post_title'  => 'Included child',
     565                                'menu_order'  => 0,
     566                        )
     567                );
     568
     569                $expected = '<li class="page_item page-item-' . $parent . ' page_item_has_children"><a href="' . get_permalink( $parent ) . '">Included parent</a>';
     570                if ( 1 !== $depth ) {
     571                        $expected .= '<ul class=\'children\'><li class="page_item page-item-' . $child . '"><a href="' . get_permalink( $child ) . '">Included child</a></li></ul>';
     572                }
     573                $expected .= '</li>';
     574
     575                $this->assertSame(
     576                        $expected,
     577                        wp_list_pages(
     578                                array(
     579                                        'include'      => array( $parent, $child ),
     580                                        'sort_column'  => 'menu_order',
     581                                        'depth'        => $depth,
     582                                        'echo'         => false,
     583                                        'title_li'     => '',
     584                                        'item_spacing' => 'discard',
     585                                )
     586                        )
     587                );
     588        }
     589
     590        /**
     591         * Data provider for test_wp_list_pages_preserves_hierarchy_when_menu_order_places_child_first().
     592         *
     593         * @return array
     594         */
     595        public static function data_wp_list_pages_omitted_ancestor() {
     596                return array(
     597                        'unlimited depth' => array( 0 ),
     598                        'depth one'       => array( 1 ),
     599                        'depth two'       => array( 2 ),
     600                );
     601        }
    543602}
  • trunk/tests/phpunit/tests/walker.php

    r56548 r65060  
    274274
    275275        /**
     276         * @ticket 28801
     277         * @dataProvider data_walk_without_top_level_elements
     278         *
     279         * @param array  $item_data Element IDs and their parent IDs.
     280         * @param int    $max_depth Maximum depth to walk.
     281         * @param string $expected Expected hierarchical output.
     282         */
     283        public function test_walk_without_top_level_elements( $item_data, $max_depth, $expected ) {
     284                $items = array();
     285                foreach ( $item_data as $key => $item ) {
     286                        $items[ $key ] = (object) array(
     287                                'id'     => $item[0],
     288                                'parent' => $item[1],
     289                        );
     290                }
     291
     292                $this->assertSame( $expected, $this->walker->walk( $items, $max_depth ) );
     293        }
     294
     295        /**
     296         * Data provider for test_walk_without_top_level_elements().
     297         *
     298         * @return array
     299         */
     300        public static function data_walk_without_top_level_elements() {
     301                return array(
     302                        'parent first, unlimited depth'     => array(
     303                                array( array( 2, 1 ), array( 3, 2 ) ),
     304                                0,
     305                                '<li>2<ul><li>3</li></ul></li>',
     306                        ),
     307                        'parent first, depth one'           => array(
     308                                array( array( 2, 1 ), array( 3, 2 ) ),
     309                                1,
     310                                '<li>2</li>',
     311                        ),
     312                        'parent first, depth two'           => array(
     313                                array( array( 2, 1 ), array( 3, 2 ) ),
     314                                2,
     315                                '<li>2<ul><li>3</li></ul></li>',
     316                        ),
     317                        'child first, unlimited depth'      => array(
     318                                array( array( 3, 2 ), array( 2, 1 ) ),
     319                                0,
     320                                '<li>2<ul><li>3</li></ul></li>',
     321                        ),
     322                        'child first, depth one'            => array(
     323                                array( array( 3, 2 ), array( 2, 1 ) ),
     324                                1,
     325                                '<li>2</li>',
     326                        ),
     327                        'child first, depth two'            => array(
     328                                array( array( 3, 2 ), array( 2, 1 ) ),
     329                                2,
     330                                '<li>2<ul><li>3</li></ul></li>',
     331                        ),
     332                        'grandchild first, unlimited depth' => array(
     333                                array( array( 4, 3 ), array( 3, 2 ), array( 2, 1 ) ),
     334                                0,
     335                                '<li>2<ul><li>3<ul><li>4</li></ul></li></ul></li>',
     336                        ),
     337                        'grandchild first, depth two'       => array(
     338                                array( array( 4, 3 ), array( 3, 2 ), array( 2, 1 ) ),
     339                                2,
     340                                '<li>2<ul><li>3</li></ul></li>',
     341                        ),
     342                        'child sibling order is preserved'  => array(
     343                                array( array( 4, 2 ), array( 3, 2 ), array( 2, 1 ) ),
     344                                0,
     345                                '<li>2<ul><li>4</li><li>3</li></ul></li>',
     346                        ),
     347                        'numeric string identifiers'        => array(
     348                                array( array( '3', '2' ), array( 2, '1' ) ),
     349                                0,
     350                                '<li>2<ul><li>3</li></ul></li>',
     351                        ),
     352                        'associative input keys'            => array(
     353                                array(
     354                                        'child'  => array( 3, 2 ),
     355                                        'parent' => array( 2, 1 ),
     356                                ),
     357                                0,
     358                                '<li>2<ul><li>3</li></ul></li>',
     359                        ),
     360                        'root sibling order is preserved'   => array(
     361                                array( array( 4, 2 ), array( 3, 1 ), array( 2, 1 ) ),
     362                                0,
     363                                '<li>3</li><li>2<ul><li>4</li></ul></li>',
     364                        ),
     365                );
     366        }
     367
     368        /**
     369         * @ticket 28801
     370         */
     371        public function test_walk_without_top_level_elements_uses_db_fields() {
     372                $this->walker->db_fields = array(
     373                        'id'     => 'node_id',
     374                        'parent' => 'node_parent',
     375                );
     376
     377                $items = array(
     378                        (object) array(
     379                                'id'          => 30,
     380                                'node_id'     => 3,
     381                                'node_parent' => 2,
     382                        ),
     383                        (object) array(
     384                                'id'          => 20,
     385                                'node_id'     => 2,
     386                                'node_parent' => 1,
     387                        ),
     388                );
     389
     390                $this->assertSame( '<li>20<ul><li>30</li></ul></li>', $this->walker->walk( $items, 0 ) );
     391        }
     392
     393        /**
    276394         * @ticket 53474
    277395         */
Note: See TracChangeset for help on using the changeset viewer.