Skip to content

CMM-1146 stats cards handling i2#22555

Merged
adalpari merged 12 commits intotrunkfrom
feat/CMM-1146-stats-cards-handling-i2
Feb 3, 2026
Merged

CMM-1146 stats cards handling i2#22555
adalpari merged 12 commits intotrunkfrom
feat/CMM-1146-stats-cards-handling-i2

Conversation

@adalpari
Copy link
Copy Markdown
Contributor

@adalpari adalpari commented Feb 3, 2026

Description

This PR adds card position management functionality to stats cards, allowing users to reorder cards via a "Move Card" sub-menu. It also adds no-connection handling and displays
the selected period in the top bar.

Changes:

  • Move Card sub-menu: Added Move Up, Move to Top, Move Down, and Move to Bottom options with descriptive icons
    • Top card hides up/top options; bottom card hides down/bottom options
    • Single card hides all move options
  • No-connection handling: When offline, cards are hidden and a message with Retry button is shown
    • Retry shows loading state on cards (not error state)
  • Period in top bar: The selected stats period (e.g., "Last 7 days") is now displayed in the top bar next to the date picker icon
    • Custom date ranges show formatted dates (e.g., "Jan 15 - Jan 22")
    • Clicking the period label opens the period selection menu
  • Code cleanup: Extracted common move logic into moveCardToIndex helper method in repository

Testing instructions

Screen_recording_20260203_131420.mp4

Move card functionality:

  1. Open Stats screen with multiple cards visible
  2. Tap the three-dot menu on any card
  3. Tap "Move Card" to open the sub-menu
  • Verify first card only shows "Move Down" and "Move to Bottom"
  • Verify last card only shows "Move Up" and "Move to Top"
  • Verify middle cards show all four options
  1. Test each move action
  • Verify cards reorder correctly
  • Verify order persists after app restart

No-connection handling:

  1. Enable airplane mode
  2. Open Stats screen (or pull to refresh)
  • Verify cards are hidden and no-connection message is shown
  1. Disable airplane mode and tap "Retry"
  • Verify cards show loading state (not error state)
  • Verify data loads successfully

Period display in top bar:

  1. Open Stats screen
  • Verify the selected period (e.g., "Last 7 days") is shown in the top bar
  1. Tap the period label or date icon
  • Verify period selection menu opens
  1. Select a different period
  • Verify the top bar updates to show the new period
  1. Select "Custom" and choose a date range
  • Verify the top bar shows the formatted date range (e.g., "Jan 15 - Jan 22")

Single card scenario:

  1. Remove all cards except one
  2. Tap the three-dot menu on the remaining card
  • Verify "Move Card" option is not shown

@dangermattic
Copy link
Copy Markdown
Collaborator

dangermattic commented Feb 3, 2026

2 Warnings
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ PR is not assigned to a milestone.

Generated by 🚫 Danger

@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Feb 3, 2026

@claude

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Feb 3, 2026

Outdated

saveConfiguration(siteId, current.copy(visibleCards = newVisibleCards))
}

@Suppress("TooGenericExceptionCaught")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider catching specific exceptions instead of generic Exception

While the @Suppress makes the linter happy, catching all exceptions here could hide important bugs during development. Consider:

private fun loadConfiguration(siteId: Long): StatsCardsConfiguration {
    val json = appPrefsWrapper.getStatsCardsConfigurationJson(siteId) ?: return StatsCardsConfiguration()
    
    return try {
        val config = gson.fromJson(json, StatsCardsConfiguration::class.java)
        if (isValidConfiguration(config)) {
            config
        } else {
            AppLog.w(AppLog.T.STATS, "Stats cards configuration contains invalid card types")
            resetToDefault(siteId)
        }
    } catch (e: JsonSyntaxException) {
        AppLog.e(AppLog.T.STATS, "Failed to parse stats cards configuration", e)
        resetToDefault(siteId)
    }
}

This way, unexpected exceptions (like NPEs from bugs) won't be silently swallowed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working as expected because we want to reset to default, no matter the error type, to avoid configuration crashes. The code is properly logging the error

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Feb 3, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
FlavorJalapeno
Build TypeDebug
Versionpr22555-dedd990
Build Number1483
Application IDcom.jetpack.android.prealpha
Commitdedd990
Installation URL4ai0mgakblgeg
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Feb 3, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
FlavorJalapeno
Build TypeDebug
Versionpr22555-dedd990
Build Number1483
Application IDorg.wordpress.android.prealpha
Commitdedd990
Installation URL7e9jc6hvp1csg
Note: Google Login is not supported on these builds.

@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Feb 3, 2026

@claude

@claude
Copy link
Copy Markdown
Contributor

claude bot commented Feb 3, 2026

Claude finished @adalpari's task in 1m 41s —— View job


Code Review Complete

I've conducted a comprehensive review of this PR. Overall, the implementation is well-structured with good test coverage. Here are my findings organized by category:


✅ Strengths

1. Excellent Test Coverage

  • Comprehensive unit tests for move operations (8 test cases)
  • Network availability tests properly covered
  • Repository tests validate all edge cases including invalid configurations

2. Code Organization

  • Clean separation of concerns (Activity → ViewModel → Repository)
  • Proper use of Kotlin coroutines and StateFlow
  • Good reusability with CardPosition data class and helper methods

3. User Experience

  • Smart handling of edge cases (first/last card movement restrictions)
  • Proper loading states and error handling
  • Network connectivity awareness

🔍 Issues Found

Critical: Network Status Race Condition

File: WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt:294-300

The retry logic has a race condition. checkNetworkStatus() updates the flow asynchronously, but the code immediately checks the network state, which may still be stale.

Current implementation:

onRetry = {
    newStatsViewModel.checkNetworkStatus()
    // Flow may not have updated yet when LaunchedEffect checks it
}

Recommended fix:
Option 1 - Always load data on retry (simplest):

onRetry = {
    newStatsViewModel.checkNetworkStatus()
    showNoConnectionScreen = false  // Trust the retry and show loading states
    todaysStatsViewModel.loadData()
    viewsStatsViewModel.loadData()
    mostViewedViewModel.loadData()
    countriesViewModel.loadData()
}

Option 2 - Make checkNetworkStatus() return the result:

fun checkNetworkStatus(): Boolean {
    val isAvailable = networkUtilsWrapper.isNetworkAvailable()
    _isNetworkAvailable.value = isAvailable
    return isAvailable
}

Accessibility: Missing Content Descriptions

File: WordPress/src/main/java/org/wordpress/android/ui/newstats/components/StatsCardMenu.kt:108-164

All four move action icons are missing contentDescription for screen reader accessibility:

Icon(
    imageVector = Icons.Default.KeyboardArrowUp,
    contentDescription = stringResource(R.string.stats_card_move_up) // Currently null
)

This applies to all four icons: KeyboardArrowUp, VerticalAlignTop, KeyboardArrowDown, VerticalAlignBottom.

Fix: Add proper content descriptions to all four Icon composables in the move submenu.


Code Quality: Overly Broad Exception Handling

File: WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsCardsConfigurationRepository.kt:103-120

While the @Suppress("TooGenericExceptionCaught") annotation addresses the linter warning, catching all exceptions can hide bugs during development.

Current:

catch (e: Exception) {
    AppLog.e(AppLog.T.STATS, "Failed to parse stats cards configuration, resetting to default", e)
    resetToDefault(siteId)
}

Recommended:

catch (e: JsonSyntaxException) {
    AppLog.e(AppLog.T.STATS, "Failed to parse stats cards configuration", e)
    resetToDefault(siteId)
} catch (e: JsonParseException) {
    AppLog.e(AppLog.T.STATS, "Failed to parse stats cards configuration", e)
    resetToDefault(siteId)
}

Note: I see the PR author commented that catching all exceptions is intentional to avoid configuration crashes. This is a valid design decision, though catching specific exceptions would be more defensive during development.


📝 Minor Observations

1. Network State Management (NewStatsActivity.kt:274-289)
The showNoConnectionScreen state management is clever but complex. Consider documenting the state machine behavior with comments explaining when cards vs. no-connection screen are shown.

2. Card Position Memoization (NewStatsActivity.kt:344-348)
Good use of remember to avoid recalculation on recomposition.

3. Move Logic Consolidation (StatsCardsConfigurationRepository.kt:91-101)
Excellent refactoring - the moveCardToIndex helper eliminates code duplication across all four move operations.


✨ Best Practices Followed

  • ✅ MVVM architecture properly implemented
  • ✅ Compose best practices (state hoisting, recomposition optimization)
  • ✅ Kotlin conventions (no empty lines after braces, proper companion object placement)
  • ✅ Proper coroutine usage with appropriate dispatchers
  • ✅ Test coverage for business logic
  • ✅ User-facing strings properly externalized to resources

📊 Summary

Must Fix (2):

  1. Network status race condition in retry logic
  2. Missing accessibility content descriptions on move icons

Should Consider (1):

  1. More specific exception handling (though current approach is defensible)

Overall Assessment: The PR demonstrates solid engineering with good separation of concerns, comprehensive testing, and thoughtful UX. The critical issue with the network retry logic should be addressed before merging.


Tasks

  • Read repository guidelines (CLAUDE.md)
  • Review main activity changes (NewStatsActivity.kt)
  • Review view model changes (NewStatsViewModel.kt)
  • Review card menu implementation (StatsCardMenu.kt)
  • Review repository changes (StatsCardsConfigurationRepository.kt)
  • Review individual card implementations
  • Review test coverage
  • Compile findings and provide feedback

Comment on lines +108 to +110
Icon(
imageVector = Icons.Default.KeyboardArrowUp,
contentDescription = stringResource(R.string.stats_card_move_up)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accessibility: Missing content description

All four move action icons should have content descriptions for screen reader users:

Icon(
    imageVector = Icons.Default.KeyboardArrowUp,
    contentDescription = stringResource(R.string.stats_card_move_up)
)

This applies to all four icons in this submenu (lines 109, 128, 145, 163).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive, already solved

@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Feb 3, 2026

ile: WordPress/src/main/java/org/wordpress/android/ui/newstats/components/StatsCardMenu.kt:108-164

All four move action icons are missing contentDescription for screen reader accessibility:

Icon(
    imageVector = Icons.Default.KeyboardArrowUp,
    contentDescription = stringResource(R.string.stats_card_move_up) // Currently null
)

This applies to all four icons: KeyboardArrowUp, VerticalAlignTop, KeyboardArrowDown, VerticalAlignBottom.

False positive. This was already fixed...

@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Feb 3, 2026

Critical: Network Status Race Condition

File: WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt:294-300

The retry logic has a race condition. checkNetworkStatus() updates the flow asynchronously, but the code immediately checks the network state, which may still be stale.

Current implementation:

onRetry = {
    newStatsViewModel.checkNetworkStatus()
    // Flow may not have updated yet when LaunchedEffect checks it
}

Recommended fix: Option 1 - Always load data on retry (simplest):

onRetry = {
    newStatsViewModel.checkNetworkStatus()
    showNoConnectionScreen = false  // Trust the retry and show loading states
    todaysStatsViewModel.loadData()
    viewsStatsViewModel.loadData()
    mostViewedViewModel.loadData()
    countriesViewModel.loadData()
}

Option 2 - Make checkNetworkStatus() return the result:

fun checkNetworkStatus(): Boolean {
    val isAvailable = networkUtilsWrapper.isNetworkAvailable()
    _isNetworkAvailable.value = isAvailable
    return isAvailable
}

I already worked on it here, but I took the chance to make further improvements.

@adalpari adalpari marked this pull request as ready for review February 3, 2026 12:13
@adalpari adalpari requested a review from nbradbury February 3, 2026 12:14
@nbradbury nbradbury self-assigned this Feb 3, 2026
@nbradbury
Copy link
Copy Markdown
Contributor

NP: If there are only two stats cards, we probably shouldn't show both "Move up" and "Move to top" (likewise for down/bottom).

top

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 3, 2026

Codecov Report

❌ Patch coverage is 14.12429% with 304 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.57%. Comparing base (f0c33e7) to head (dedd990).
⚠️ Report is 1 commits behind head on trunk.

Files with missing lines Patch % Lines
.../wordpress/android/ui/newstats/NewStatsActivity.kt 0.00% 113 Missing ⚠️
...ss/android/ui/newstats/components/StatsCardMenu.kt 9.75% 74 Missing ⚠️
...s/android/ui/newstats/mostviewed/MostViewedCard.kt 0.00% 35 Missing ⚠️
...s/android/ui/newstats/viewsstats/ViewsStatsCard.kt 0.00% 30 Missing ⚠️
...ess/android/ui/newstats/countries/CountriesCard.kt 0.00% 26 Missing ⚠️
...android/ui/newstats/todaysstats/TodaysStatsCard.kt 0.00% 24 Missing ⚠️
...ts/repository/StatsCardsConfigurationRepository.kt 91.66% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22555      +/-   ##
==========================================
- Coverage   38.64%   38.57%   -0.07%     
==========================================
  Files        2225     2225              
  Lines      109329   109650     +321     
  Branches    15327    15349      +22     
==========================================
+ Hits        42248    42297      +49     
- Misses      63574    63844     +270     
- Partials     3507     3509       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@adalpari
Copy link
Copy Markdown
Contributor Author

adalpari commented Feb 3, 2026

NP: If there are only two stats cards, we probably shouldn't show both "Move up" and "Move to top" (likewise for down/bottom).

Good point! Done here

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

@nbradbury nbradbury left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! :shipit:

@adalpari adalpari merged commit 6e9045a into trunk Feb 3, 2026
24 checks passed
@adalpari adalpari deleted the feat/CMM-1146-stats-cards-handling-i2 branch February 3, 2026 14:03
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