Skip to content

Conversation

@nbradbury
Copy link
Contributor

@nbradbury nbradbury commented Feb 2, 2026

Description

Partially addresses CMM-1166.

This PR adds a new experimental Navigation Menus feature that allows users to view and edit their site's navigation menus directly from the app. The feature supports both self-hosted WordPress sites and WordPress.com sites with app passwords enabled.

Key changes:

  • New Navigation Menus UI: Complete Jetpack Compose implementation with screens for menu list, menu detail, menu items list, and menu item detail
  • REST Client: New NavMenuRestClient using wordpress-rs for all menu CRUD operations (menus, menu items, locations)
  • Capability Checking: Added SiteCapabilityChecker with caching to verify users have edit_theme_options capability before showing the menus option
  • Menu Item Types: Support for adding pages, posts, categories, tags, and custom links as menu items
  • Pull-to-refresh: Added refresh functionality to the menu list screen
  • Animated reordering: Menu items can be reordered with smooth animations
  • My Site Integration: New "Menus" item in the My Site menu for sites that support the feature

Limitations

The goal with this PR was to provide basic menu creation and editing and is not fully-featured. Notably, the ability to assign a pre-existing post, page, or category to a menu item is not available yet (see here), nor is pagination. These will be added later.

Testing instructions

Prerequisites:

  • Experimental feature flag enabled
  • A self-hosted WordPress site with app password configured, OR
  • A WordPress.com site with app password enabled
  • The logged-in user must have the edit_theme_options capability (typically admin users)

Test viewing menus:

  1. Open the app and navigate to My Site
  2. Tap on the site menu (three dots or overflow menu)
  • Verify "Menus" option appears for eligible sites
  1. Tap on "Menus"
  • Verify the menu list loads and displays existing menus with item counts

Test creating a new menu:

  1. On the menu list screen, tap the FAB (+) button
  2. Enter a menu name and optionally a description
  3. Tap Save
  • Verify the new menu appears in the list

Test adding menu items:

  1. Tap on a menu to view its details
  2. Tap "Manage Items"
  3. Tap the FAB (+) button to add a new item
  4. Select item type (Page, Post, Category, Tag, or Custom Link)
  5. Fill in the required fields
  6. Tap the checkmark to save
  • Verify the item appears in the menu items list

Test reordering menu items:

  1. Open a menu with multiple items
  2. Long-press on an item and drag to reorder
  • Verify items animate smoothly during reorder
  • Verify the new order is saved

Test pull-to-refresh:

  1. On the menu list screen, pull down to refresh
  • Verify the refresh indicator appears and menus reload

Test deleting a menu:

  1. Open a menu's detail screen
  2. Tap the delete icon in the toolbar
  3. Confirm the deletion
  • Verify the menu is removed from the list

Test capability restriction:

  1. Log in with a user that does NOT have edit_theme_options capability (e.g., Contributor role)
  2. Navigate to My Site menu
  • Verify "Menus" option does NOT appear
menu.mp4

nbradbury and others added 30 commits January 29, 2026 09:23
Implement the ability to create, edit, and delete WordPress navigation
menus for self-hosted sites using the WP REST API with Application
Passwords. This feature includes:

- FluxC data layer with NavMenuModel, NavMenuItemModel, NavMenuLocationModel
- NavMenuStore with CRUD operations via NavMenuWPAPIRestClient
- Compose-based UI with menu list, detail, item list, and item detail screens
- Hierarchical menu item support with parent selection and reordering
- Menu assignment to theme locations
- Feature flag (ENABLE_NAV_MENUS) for controlled rollout
- Visibility limited to self-hosted sites with edit_theme_options capability

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Convert NavMenuModel, NavMenuItemModel, and NavMenuLocationModel from
  data classes to regular classes to fix mutable property warnings
- Extract screen composables from NavMenusActivity to separate files
  (MenuListScreen, MenuDetailScreen, MenuItemListScreen, MenuItemDetailScreen)
- Refactor reorderMenuItem() to reduce return statements
- Add logging for caught exceptions in parseJsonArray()
- Remove unused imports in NavMenuWPAPIRestClient

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Enable feature flag (ENABLE_NAV_MENUS = true)
- Support WordPress.com sites in addition to self-hosted sites
  by checking both isUsingWpComRestApi and isUsingSelfHostedRestApi
- Add OpenMenus handler to MenuActivity for unified menu navigation
- Improve error handling in NavMenusViewModel with try-catch and
  fallback error messages for blank API responses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The menus feature no longer needs a remote feature flag. Remove the
feature config and make menus always available when the site has
REST API access and edit_theme_options capability.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace the FluxC-based NavMenuWPAPIRestClient with NavMenuRsRestClient
that uses the wordpress-rs library for API calls. This is consistent with
the migration of other features (Media, Taxonomy) to wordpress-rs.

- Add NavMenuRsRestClient using wordpress-rs requestBuilder methods
- Update NavMenusViewModel and NavMenuStore to use new client
- Remove old NavMenuWPAPIRestClient and NavMenuApiResponse

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move NavMenuRestClient from FluxC to WordPress module to align with
the goal of phasing out FluxC in favor of wordpress-rs.

- Move REST client to WordPress/ui/navmenus/data/
- Rename NavMenuRsRestClient to NavMenuRestClient
- Delete unused NavMenuStore and NavMenuAction from FluxC
- Keep data models in FluxC for now (needed for WellSql persistence)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Restrict Menus feature to self-hosted sites with Application Passwords.
WordPress.com sites don't have apiRestUsernamePlain/apiRestPasswordPlain
credentials, causing NPE in WpApiClientProvider.getWpApiClient().

WordPress.com menu editing would require a separate implementation using
the WordPress.com REST API with OAuth authentication.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Catch specific exceptions instead of generic Exception:
- Rethrow CancellationException to support coroutine cancellation
- Catch IOException for network errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use CREATE TABLE IF NOT EXISTS for NavMenu tables in migration 211.
This prevents errors when tables already exist from WellSql's
automatic table creation during database initialization.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use default background color for Card containers instead of the
Material 3 surface container color, which looked inconsistent.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Ensure menu_order is always >= 1 to fix HTTP 400 error
- Update empty state message for menu items
- Show "Items" suffix in menu items screen title

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Display the menu description below the title with a maximum of 3 lines.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Show menu item description (max 3 lines) in list
- Add URL validation when saving menu items
- Fix URL field not showing when editing custom links
- Hide reorder buttons when there's only one menu item
- Map typeLabel to type code for correct item type detection

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add description field to create and update params for menu items.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The wordpress-rs library returns non-nullable types for most fields.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use underscore for unused exception parameter
- Add suppress annotation for uniffi HashMap cast

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These models are used as simple DTOs and are not stored in the database.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This file is no longer needed since nav menus are not stored in SQLite.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Display item count next to each menu name in the list
- Add fetchAllMenuItems to fetch all items for counting
- Add plurals resource for proper singular/plural handling
- Log exception in isValidUrl for debugging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract menu ID from item response instead of using 0L
- Move item count display next to the Items button for better layout

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reorganizes the menu item detail screen to place description
directly after title for better UX, and removes the CSS classes
field which is not commonly used.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace the delete button with a trash icon in the toolbar for
a cleaner UI. The icon only appears when editing existing items.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removes unused ViewModel functions (updateMenuItemCssClasses,
updateMenuItemAttrTitle), unused UI events (ShowMessage, NavigateBack),
and orphaned string resources that remained after UI fields were removed.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extract shared parseJsonLocationArray() extension function to eliminate
  duplicated JSON array parsing logic in ViewModel, UiState, and RestClient
- Consolidate showDeleteMenuConfirmation and showDeleteMenuItemConfirmation
  into a single parameterized showDeleteConfirmation function

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add toJsonStringArray() extension function as the inverse of
  parseJsonStringArray() for consistent JSON array handling
- Rename parseJsonLocationArray to parseJsonStringArray for generality
- Replace verbose joinToString patterns with the new extension
- Simplify NavMenuItemWithEditContext.toNavMenuItemModel() by using
  default parameter instead of overload

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
nbradbury and others added 9 commits February 3, 2026 08:07
- First item no longer shows move up icon
- Last item no longer shows move down icon
- Items can only swap with siblings at the same indent level

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Refactor updateMenuItemOrder to reduce return count
- Suppress LargeClass warning (splitting would be a larger refactor)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The nav menu tables were created but never used - the feature fetches
data directly from the REST API without local caching. Removing the
migration to avoid creating unused tables.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Explicitly reject javascript:, data:, and vbscript: URLs to prevent
potential XSS attacks if menu URLs are rendered in a WebView.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Catch generic Exception instead of just IOException since wordpress-rs
  can throw various exceptions from the Rust/uniffi layer
- Add @Suppress annotation for detekt compliance
- Replace hardcoded error string with string resource

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add logging when fetching edit_theme_options capability fails instead
of silently swallowing the exception.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change from contains() to equals() to avoid matching unintended
capability names like "cant_edit_theme_options".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove the linkable items loading (pages, posts, categories, tags) from
NavMenusViewModel to be implemented separately. This removes:
- loadLinkableItems, loadPages, loadPosts, loadCategories, loadTags,
  buildHierarchicalList functions
- pageStore, postStore, taxonomyStore dependencies

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add NAV_MENUS to ExperimentalFeatures so users must opt-in via
Experimental Features settings before the menus item appears.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@wordpress-mobile wordpress-mobile deleted a comment from claude bot Feb 3, 2026
nbradbury and others added 6 commits February 3, 2026 10:16
Replace Patterns.WEB_URL with URLUtil.isValidUrl() while keeping
the dangerous schemes check for XSS protection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Modifier.semantics with content description to convey the
hierarchy level to screen readers, since visual padding alone
doesn't communicate the parent-child relationships.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@nbradbury nbradbury changed the title Add navigation menus feature Add experimental navigation menus feature Feb 3, 2026
nbradbury and others added 3 commits February 3, 2026 11:06
Replace naive split(",") with proper JSONArray parsing to correctly
handle values containing commas.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace space-based indentation with proper padding and add semantics
for screen readers in LinkableItemDropdown and ParentDropdown.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Trim whitespace and enforce length limits on menu name, description,
menu item title, and menu item description fields.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@wordpress-mobile wordpress-mobile deleted a comment from claude bot Feb 3, 2026
nbradbury and others added 3 commits February 3, 2026 11:23
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The error is guaranteed non-null in this branch of the when expression.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This fixes the screen jumping issue when tapping input fields on Android 15+.
Compose-based activities handle insets automatically, so the base activity's
custom inset handling was conflicting with Compose's built-in behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@nbradbury nbradbury requested a review from adalpari February 3, 2026 17:33
@nbradbury nbradbury marked this pull request as ready for review February 3, 2026 17:33
The reorder logic was checking only the immediately adjacent item
for the same indent level. When an item has children, siblings are
not adjacent in the flat list, so arrows wouldn't appear.

Updated both the UI (MenuItemListScreen) and ViewModel to properly
search for siblings by scanning through the list until finding an
item at the same indent level or hitting a parent boundary. Also
updated the reorder operation to move entire subtrees when swapping.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 3, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants