Skip to content
Merged
46 changes: 46 additions & 0 deletions features/menu-item.feature
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,52 @@ Feature: Manage WordPress menu items
| custom | First | 1 | https://first.com |
| custom | Third | 2 | https://third.com |

Scenario: Menu order is recalculated on update
When I run `wp menu create "Sidebar Menu"`
Then STDOUT should not be empty

When I run `wp menu item add-custom sidebar-menu Alpha https://alpha.com --porcelain`
Then save STDOUT as {ITEM_ID_1}

When I run `wp menu item add-custom sidebar-menu Beta https://beta.com --porcelain`
Then save STDOUT as {ITEM_ID_2}

When I run `wp menu item add-custom sidebar-menu Gamma https://gamma.com --porcelain`
Then save STDOUT as {ITEM_ID_3}

When I run `wp menu item list sidebar-menu --fields=type,title,position,link`
Then STDOUT should be a table containing rows:
| type | title | position | link |
| custom | Alpha | 1 | https://alpha.com |
| custom | Beta | 2 | https://beta.com |
| custom | Gamma | 3 | https://gamma.com |

When I run `wp menu item update {ITEM_ID_3} --position=1`
Then STDOUT should be:
"""
Success: Menu item updated.
"""

When I run `wp menu item list sidebar-menu --fields=type,title,position,link`
Then STDOUT should be a table containing rows:
| type | title | position | link |
| custom | Gamma | 1 | https://gamma.com |
| custom | Alpha | 2 | https://alpha.com |
| custom | Beta | 3 | https://beta.com |

When I run `wp menu item update {ITEM_ID_1} --position=3`
Then STDOUT should be:
"""
Success: Menu item updated.
"""

When I run `wp menu item list sidebar-menu --fields=type,title,position,link`
Then STDOUT should be a table containing rows:
| type | title | position | link |
| custom | Gamma | 1 | https://gamma.com |
| custom | Beta | 2 | https://beta.com |
| custom | Alpha | 3 | https://alpha.com |

Scenario: Get menu item details
When I run `wp menu create "Sidebar Menu"`
Then STDOUT should not be empty
Expand Down
86 changes: 85 additions & 1 deletion src/Menu_Item_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,71 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) {
}

$menu_item_args['menu-item-type'] = $type;
$result = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args );
$pending_menu_order_updates = [];

// Reorder other menu items when the position changes on update.
if ( 'update' === $method ) {
$new_position = (int) $menu_item_args['menu-item-position'];
if ( $new_position > 0 ) {
// Fetch all menu items sorted by their raw menu_order to determine
// normalized (1-indexed) ranks, since wp_get_nav_menu_items(ARRAY_A)
// normalises menu_order to 1,2,3… which may differ from the raw DB values.
$sorted_item_ids = get_posts(
[
'post_type' => 'nav_menu_item',
'numberposts' => -1,
'orderby' => 'menu_order',
'order' => 'ASC',
'post_status' => 'any',
'tax_query' => [
[
'taxonomy' => 'nav_menu',
'field' => 'term_taxonomy_id',
'terms' => $menu->term_taxonomy_id,
],
],
'fields' => 'ids',
]
);

// Normalise to integers so that strict comparisons below work regardless of
// whether $wpdb->get_col() returned strings or integers.
$sorted_item_ids = array_map( 'intval', $sorted_item_ids );

// Clamp the requested position to the valid range of menu items.
$max_position = count( $sorted_item_ids );
if ( $max_position > 0 && $new_position > $max_position ) {
// Treat out-of-range positions as "move to end", consistent with core behavior.
$new_position = $max_position;
}

// Find the 1-indexed normalized rank of the item being moved.
$item_idx = array_search( (int) $menu_item_db_id, $sorted_item_ids, true );
$old_position_normalized = ( false !== $item_idx ) ? $item_idx + 1 : 0;

if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) {
if ( $new_position < $old_position_normalized ) {
// Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1.
for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) {
$pending_menu_order_updates[] = [
'ID' => $sorted_item_ids[ $i ],
'menu_order' => $i + 2,
];
}
} else {
// Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1.
for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) {
$pending_menu_order_updates[] = [
'ID' => $sorted_item_ids[ $i ],
'menu_order' => $i,
];
}
}
}
}
}

$result = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args );

if ( is_wp_error( $result ) ) {
WP_CLI::error( $result->get_error_message() );
Expand All @@ -645,6 +709,26 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) {
WP_CLI::error( "Couldn't update menu item." );
}
} else {
// Apply deferred reordering of other menu items only after a successful update.
if ( ! empty( $pending_menu_order_updates ) ) {
global $wpdb;

$ids_to_update = [];
$case_clauses = '';
foreach ( $pending_menu_order_updates as $update_args ) {
$item_id = (int) $update_args['ID'];
$ids_to_update[] = $item_id;
$case_clauses .= $wpdb->prepare( ' WHEN %d THEN %d', $item_id, $update_args['menu_order'] );
}

$ids_sql = implode( ',', $ids_to_update );
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $case_clauses and $ids_sql are constructed from prepared/safe integer values.
$wpdb->query( "UPDATE {$wpdb->posts} SET menu_order = CASE ID {$case_clauses} END WHERE ID IN ({$ids_sql})" );

foreach ( $ids_to_update as $id ) {
clean_post_cache( $id );
}
}

if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) {
$this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result );
Expand Down