Skip to content

Commit ee3acf7

Browse files
CopilotswissspidyCopilot
authored
Fix: wp menu item update --position does not reshuffle other menu items (#603)
Co-authored-by: swissspidy <841956+swissspidy@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler <pascal.birchler@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Pascal Birchler <pascalb@google.com>
1 parent 36afb61 commit ee3acf7

File tree

2 files changed

+131
-1
lines changed

2 files changed

+131
-1
lines changed

features/menu-item.feature

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,52 @@ Feature: Manage WordPress menu items
195195
| custom | First | 1 | https://first.com |
196196
| custom | Third | 2 | https://third.com |
197197

198+
Scenario: Menu order is recalculated on update
199+
When I run `wp menu create "Sidebar Menu"`
200+
Then STDOUT should not be empty
201+
202+
When I run `wp menu item add-custom sidebar-menu Alpha https://alpha.com --porcelain`
203+
Then save STDOUT as {ITEM_ID_1}
204+
205+
When I run `wp menu item add-custom sidebar-menu Beta https://beta.com --porcelain`
206+
Then save STDOUT as {ITEM_ID_2}
207+
208+
When I run `wp menu item add-custom sidebar-menu Gamma https://gamma.com --porcelain`
209+
Then save STDOUT as {ITEM_ID_3}
210+
211+
When I run `wp menu item list sidebar-menu --fields=type,title,position,link`
212+
Then STDOUT should be a table containing rows:
213+
| type | title | position | link |
214+
| custom | Alpha | 1 | https://alpha.com |
215+
| custom | Beta | 2 | https://beta.com |
216+
| custom | Gamma | 3 | https://gamma.com |
217+
218+
When I run `wp menu item update {ITEM_ID_3} --position=1`
219+
Then STDOUT should be:
220+
"""
221+
Success: Menu item updated.
222+
"""
223+
224+
When I run `wp menu item list sidebar-menu --fields=type,title,position,link`
225+
Then STDOUT should be a table containing rows:
226+
| type | title | position | link |
227+
| custom | Gamma | 1 | https://gamma.com |
228+
| custom | Alpha | 2 | https://alpha.com |
229+
| custom | Beta | 3 | https://beta.com |
230+
231+
When I run `wp menu item update {ITEM_ID_1} --position=3`
232+
Then STDOUT should be:
233+
"""
234+
Success: Menu item updated.
235+
"""
236+
237+
When I run `wp menu item list sidebar-menu --fields=type,title,position,link`
238+
Then STDOUT should be a table containing rows:
239+
| type | title | position | link |
240+
| custom | Gamma | 1 | https://gamma.com |
241+
| custom | Beta | 2 | https://beta.com |
242+
| custom | Alpha | 3 | https://alpha.com |
243+
198244
Scenario: Get menu item details
199245
When I run `wp menu create "Sidebar Menu"`
200246
Then STDOUT should not be empty

src/Menu_Item_Command.php

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,71 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) {
634634
}
635635

636636
$menu_item_args['menu-item-type'] = $type;
637-
$result = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args );
637+
$pending_menu_order_updates = [];
638+
639+
// Reorder other menu items when the position changes on update.
640+
if ( 'update' === $method ) {
641+
$new_position = (int) $menu_item_args['menu-item-position'];
642+
if ( $new_position > 0 ) {
643+
// Fetch all menu items sorted by their raw menu_order to determine
644+
// normalized (1-indexed) ranks, since wp_get_nav_menu_items(ARRAY_A)
645+
// normalises menu_order to 1,2,3… which may differ from the raw DB values.
646+
$sorted_item_ids = get_posts(
647+
[
648+
'post_type' => 'nav_menu_item',
649+
'numberposts' => -1,
650+
'orderby' => 'menu_order',
651+
'order' => 'ASC',
652+
'post_status' => 'any',
653+
'tax_query' => [
654+
[
655+
'taxonomy' => 'nav_menu',
656+
'field' => 'term_taxonomy_id',
657+
'terms' => $menu->term_taxonomy_id,
658+
],
659+
],
660+
'fields' => 'ids',
661+
]
662+
);
663+
664+
// Normalise to integers so that strict comparisons below work regardless of
665+
// whether $wpdb->get_col() returned strings or integers.
666+
$sorted_item_ids = array_map( 'intval', $sorted_item_ids );
667+
668+
// Clamp the requested position to the valid range of menu items.
669+
$max_position = count( $sorted_item_ids );
670+
if ( $max_position > 0 && $new_position > $max_position ) {
671+
// Treat out-of-range positions as "move to end", consistent with core behavior.
672+
$new_position = $max_position;
673+
}
674+
675+
// Find the 1-indexed normalized rank of the item being moved.
676+
$item_idx = array_search( (int) $menu_item_db_id, $sorted_item_ids, true );
677+
$old_position_normalized = ( false !== $item_idx ) ? $item_idx + 1 : 0;
678+
679+
if ( $old_position_normalized > 0 && $new_position !== $old_position_normalized ) {
680+
if ( $new_position < $old_position_normalized ) {
681+
// Moving up: items at 0-indexed [new_pos-1, old_pos-2] shift down by +1.
682+
for ( $i = $new_position - 1; $i <= $old_position_normalized - 2; $i++ ) {
683+
$pending_menu_order_updates[] = [
684+
'ID' => $sorted_item_ids[ $i ],
685+
'menu_order' => $i + 2,
686+
];
687+
}
688+
} else {
689+
// Moving down: items at 0-indexed [old_pos, new_pos-1] shift up by -1.
690+
for ( $i = $old_position_normalized; $i <= $new_position - 1; $i++ ) {
691+
$pending_menu_order_updates[] = [
692+
'ID' => $sorted_item_ids[ $i ],
693+
'menu_order' => $i,
694+
];
695+
}
696+
}
697+
}
698+
}
699+
}
700+
701+
$result = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args );
638702

639703
if ( is_wp_error( $result ) ) {
640704
WP_CLI::error( $result->get_error_message() );
@@ -645,6 +709,26 @@ private function add_or_update_item( $method, $type, $args, $assoc_args ) {
645709
WP_CLI::error( "Couldn't update menu item." );
646710
}
647711
} else {
712+
// Apply deferred reordering of other menu items only after a successful update.
713+
if ( ! empty( $pending_menu_order_updates ) ) {
714+
global $wpdb;
715+
716+
$ids_to_update = [];
717+
$case_clauses = '';
718+
foreach ( $pending_menu_order_updates as $update_args ) {
719+
$item_id = (int) $update_args['ID'];
720+
$ids_to_update[] = $item_id;
721+
$case_clauses .= $wpdb->prepare( ' WHEN %d THEN %d', $item_id, $update_args['menu_order'] );
722+
}
723+
724+
$ids_sql = implode( ',', $ids_to_update );
725+
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $case_clauses and $ids_sql are constructed from prepared/safe integer values.
726+
$wpdb->query( "UPDATE {$wpdb->posts} SET menu_order = CASE ID {$case_clauses} END WHERE ID IN ({$ids_sql})" );
727+
728+
foreach ( $ids_to_update as $id ) {
729+
clean_post_cache( $id );
730+
}
731+
}
648732

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

0 commit comments

Comments
 (0)