Skip to content

CMM-1146 stats create cards handling#22552

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

CMM-1146 stats create cards handling#22552
adalpari merged 19 commits intotrunkfrom
feat/CMM-1146-stats-Create-cards-handling

Conversation

@adalpari
Copy link
Contributor

@adalpari adalpari commented Feb 2, 2026

Description

Implements stats cards configuration handling for the new stats screen. Users can now customize which
stats cards are visible by removing cards via menu and adding them back through a bottom sheet.
Configuration is persisted per-site using SharedPreferences.

NEXT: allow user to move cards and fix the individual "Retry" button

Key changes:

  • Added StatsCardsConfigurationRepository to manage per-site card visibility preferences
  • Created explicit card types (MOST_VIEWED_POSTS_AND_PAGES, MOST_VIEWED_REFERRERS) instead of
    using a single MOST_VIEWED type with subtypes, simplifying the architecture
  • Added AddStatsCardBottomSheet for re-adding hidden cards
  • Added configuration validation to handle migration from old saved configurations containing
    invalid/removed card types
  • Added card menu with "Remove card" option to all stats cards

Testing instructions

Screen_recording_20260202_162331.mp4

Card removal:

  1. Open the new stats screen on any site
  2. Tap the menu icon on any stats card (e.g., "Today's Stats")
  3. Tap "Remove card"
  • Verify the card is removed from the screen
  • Verify the change persists after navigating away and returning

Adding cards back:

  1. Remove at least one card using the steps above
  2. Scroll to the bottom of the stats screen
  3. Tap the "Add card" button
  • Verify a bottom sheet appears showing the removed card(s)
  1. Tap on a card to add it back
  • Verify the card reappears on the stats screen

Empty state:

  1. Remove all cards from the stats screen
  • Verify an empty state message is displayed
  • Verify the "Add card" option is still available

Configuration persistence:

  1. Customize the visible cards (remove some, keep others)
  2. Force close the app completely
  3. Reopen the app and navigate to the stats screen
  • Verify the card configuration is preserved

Multi-site configuration:

  1. On Site A, remove some cards
  2. Switch to Site B
  • Verify Site B shows its own card configuration (default if not customized)
  1. Customize Site B's cards differently
  2. Switch back to Site A
  • Verify Site A still has its original customization

@adalpari
Copy link
Contributor Author

adalpari commented Feb 2, 2026

@claude

@claude
Copy link
Contributor

claude bot commented Feb 2, 2026

Outdated

@dangermattic
Copy link
Collaborator

dangermattic commented Feb 2, 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
Contributor Author

adalpari commented Feb 2, 2026

3. Missing Synchronization in NewStatsViewModel

File: NewStatsViewModel.kt:29-30

private val siteId: Long
    get() = selectedSiteRepository.getSelectedSite()?.siteId ?: 0L

Issue: If the user switches sites while operations are in progress, operations could be applied to the wrong site. The siteId is read fresh each time but there's no synchronization with the ongoing operations.

Recommendation: Consider capturing the siteId at the start of each operation or using a StateFlow for the selected site.

Impact: Medium - Could save/load configuration to wrong site during site switching

The user can only change the site by closing the screen. So, there's no sync problem.

@adalpari
Copy link
Contributor Author

adalpari commented Feb 2, 2026

4. Configuration Validation Incomplete

File: StatsCardsConfigurationRepository.kt:90-94

Issue: The validation only checks for null values but doesn't validate:

  • Duplicate cards in visibleCards
  • Card order consistency
  • Whether the configuration is logically valid (e.g., not all cards hidden for first-time users)

We don't want to test/handle those cases, because they are perfectly fine.

@adalpari
Copy link
Contributor Author

adalpari commented Feb 2, 2026

6. Inconsistent String Resource Usage

Files: Multiple card files

Issue: In MostViewedCard.kt:214, the column header uses cardType.displayNameResId which shows "Most Viewed Posts and Pages" as a column header, but it should likely be a more generic label like "Title" or "Content".

Text(
    text = stringResource(cardType.displayNameResId),  // Shows full card name
    style = MaterialTheme.typography.labelMedium,
    color = MaterialTheme.colorScheme.onSurfaceVariant
)

Recommendation: Use a separate string resource for column headers.

It shows the card name, which is expected. It was previously a selector. We may just remove it.

@adalpari
Copy link
Contributor Author

adalpari commented Feb 2, 2026

7. Potential Memory Leak in Repository

File: StatsCardsConfigurationRepository.kt:28

private val configurationCache = mutableMapOf<Long, StatsCardsConfiguration>()

Issue: The cache grows indefinitely. If a user has many sites or switches frequently, this could consume memory unnecessarily.

Recommendation: Implement an LRU cache or clear cache when appropriate:

companion object {
    private const val MAX_CACHE_SIZE = 10
}

private val configurationCache = object : LinkedHashMap<Long, StatsCardsConfiguration>(
    MAX_CACHE_SIZE, 0.75f, true
) {
    override fun removeEldestEntry(eldest: Map.Entry<Long, StatsCardsConfiguration>): Boolean {
        return size > MAX_CACHE_SIZE
    }
}

Impact: Low - Only affects users with many sites

I've removed the cache now since it's not adding much value.

@adalpari
Copy link
Contributor Author

adalpari commented Feb 2, 2026

@claude

@claude
Copy link
Contributor

claude bot commented Feb 2, 2026

Claude finished @adalpari's task in 2m 4s —— View job


Code Review Complete ✅

I've reviewed this PR implementing stats cards configuration handling. Overall, this is a well-structured implementation with good test coverage. Here are my findings:


🎯 Architecture & Design

Strengths:

  • Clean separation of concerns with dedicated repository for configuration management
  • Good use of Kotlin Flow for reactive state management
  • Proper dependency injection with Hilt
  • Explicit card types (e.g., MOST_VIEWED_POSTS_AND_PAGES vs single MOST_VIEWED with subtypes) simplifies the architecture significantly

Observations:

  • The configuration validation logic (isValidConfiguration) is clever - it handles the migration case where old enum values might deserialize as nulls. This is well-documented.

🔒 Security

No critical security issues found.

The implementation properly:

  • Uses per-site configuration isolation
  • Validates configuration data before use
  • Handles JSON parsing errors gracefully

⚡ Performance

Minor Considerations:

  1. Repository cache removed - I see from the comments you removed the cache. This is fine, but note that getConfiguration() now hits SharedPreferences on every call. For the current usage pattern this should be acceptable since:

    • Calls are suspended on IO dispatcher
    • Configuration changes are infrequent
    • StateFlow provides in-memory caching at the ViewModel level
  2. StateFlow emission - StatsCardsConfigurationRepository:42 emits on every save. This is good for multi-site scenarios but consider that if the same configuration is saved repeatedly, observers will be notified each time. This is likely fine given current usage.


🧪 Test Coverage

Excellent test coverage! The test files cover:

  • Default configuration loading (StatsCardsConfigurationRepositoryTest:38)
  • JSON parsing and validation (StatsCardsConfigurationRepositoryTest:47,61)
  • Card add/remove operations (StatsCardsConfigurationRepositoryTest:83,101)
  • Migration handling for invalid card types (StatsCardsConfigurationRepositoryTest:131)
  • ViewModel state management (NewStatsViewModelTest)
  • Multi-site configuration isolation (NewStatsViewModelTest:110)

📝 Code Quality Issues

1. Potential Site ID Synchronization Issue

File: NewStatsViewModel.kt:30-31

private val siteId: Long
    get() = selectedSiteRepository.getSelectedSite()?.siteId ?: 0L

Issue: While you mentioned users can only change sites by closing the screen, the siteId property is read fresh on each access. If an async operation is in progress and the ViewModel is not destroyed (e.g., configuration change), there's a theoretical race condition.

Recommendation: Consider capturing siteId at operation start:

fun removeCard(cardType: StatsCardType) {
    viewModelScope.launch {
        val currentSiteId = siteId
        cardConfigurationRepository.removeCard(currentSiteId, cardType)
    }
}

Impact: Low - Current implementation is likely safe given usage patterns, but defensive coding would prevent future issues.


2. Missing Input Validation

File: StatsCardsConfigurationRepository.kt:55

suspend fun addCard(siteId: Long, cardType: StatsCardType): Unit = withContext(ioDispatcher) {
    val current = getConfiguration(siteId)
    val newVisibleCards = current.visibleCards + cardType
    saveConfiguration(siteId, current.copy(visibleCards = newVisibleCards))
}

Issue: No check for duplicate cards. If addCard() is called twice with the same card type, it will be added twice to the list.

Recommendation: Add duplicate check:

suspend fun addCard(siteId: Long, cardType: StatsCardType): Unit = withContext(ioDispatcher) {
    val current = getConfiguration(siteId)
    if (cardType in current.visibleCards) return@withContext
    val newVisibleCards = current.visibleCards + cardType
    saveConfiguration(siteId, current.copy(visibleCards = newVisibleCards))
}

Impact: Medium - Could lead to duplicate cards in the UI if the method is called multiple times.

Fix this →


3. Inconsistent Error Handling

Files: NewStatsViewModel.kt:38-48 and StatsCardsConfigurationRepository.kt:59

Issue: The repository's loadConfiguration catches all exceptions and returns default config silently, while the ViewModel's loadConfiguration also has a try-catch that returns default config. This creates double error handling.

Current flow:

loadConfiguration (VM) 
  -> try 
    -> getConfiguration (repo) 
      -> loadConfiguration (repo) 
        -> try/catch -> logs + returns default
    -> catch -> logs + returns default

Recommendation: Since the repository already handles errors gracefully, the ViewModel's try-catch could be simplified or removed. The current approach works but is redundant.

Impact: Low - No functional issue, just unnecessary complexity.


4. Card Order Not Preserved on Add

File: StatsCardsConfigurationRepository.kt:55

val newVisibleCards = current.visibleCards + cardType

Issue: When a card is added back, it's appended to the end rather than being inserted at its original position (based on defaultOrder). This means if a user removes the second card and adds it back, it appears at the end instead of its original position.

Recommendation: Consider inserting cards at their proper position:

suspend fun addCard(siteId: Long, cardType: StatsCardType): Unit = withContext(ioDispatcher) {
    val current = getConfiguration(siteId)
    if (cardType in current.visibleCards) return@withContext
    
    // Insert at proper position based on defaultOrder
    val newVisibleCards = (current.visibleCards + cardType)
        .sortedBy { it.defaultOrder }
    
    saveConfiguration(siteId, current.copy(visibleCards = newVisibleCards))
}

Impact: Low - UX preference; current behavior might be intentional to let users customize order.

Fix this →


5. Empty State Accessibility

File: NewStatsActivity.kt:272-286

if (visibleCards.isEmpty()) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .padding(32.dp),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = stringResource(R.string.stats_no_cards_message),
            style = MaterialTheme.typography.bodyLarge,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )
    }
}

Issue: The empty state text doesn't have a semantic role or content description for accessibility.

Recommendation: Consider adding semantic properties:

Text(
    text = stringResource(R.string.stats_no_cards_message),
    style = MaterialTheme.typography.bodyLarge,
    color = MaterialTheme.colorScheme.onSurfaceVariant,
    modifier = Modifier.semantics { 
        heading()
    }
)

Impact: Low - Accessibility improvement for screen readers.


6. Unused Configuration Fields

File: StatsCardsConfiguration.kt:9-11

val selectedPeriodType: String? = null,
val customPeriodStartDate: Long? = null,
val customPeriodEndDate: Long? = null

Issue: These fields are defined but never used in the current PR. They appear to be for future functionality.

Recommendation: Either:

  • Add a comment explaining they're for future use
  • Remove them until needed (YAGNI principle)
  • Implement the functionality now

Impact: Very Low - Just code cleanliness; no functional issue.


🎨 UI/UX Observations

  1. Bottom Sheet UX - The AddStatsCardBottomSheet automatically dismisses when a card is selected (AddStatsCardBottomSheet.kt:75). This is good for single-selection but might be annoying if users want to add multiple cards. Consider keeping it open or showing a snackbar confirmation.

  2. Menu Icon - Using Icons.Default.Delete for "Remove Card" (StatsCardMenu.kt:64) is semantically correct but might be perceived as permanent deletion rather than hiding. This is likely fine, just noting it.

  3. Empty State Flow - When all cards are removed, the empty state appears but the "Add Card" button is below it (NewStatsActivity.kt:360-363). This is good - users can immediately fix the empty state.


📦 Documentation

Strengths:

  • KDoc comments on complex functions (e.g., isValidConfiguration)
  • Clear parameter descriptions
  • Good use of inline comments explaining business logic

Suggestions:

  • Consider adding a module-level doc comment explaining the card configuration system
  • The PR description is excellent and serves as good documentation

✅ Summary

This is a high-quality implementation with:

  • ✅ Clean architecture
  • ✅ Excellent test coverage
  • ✅ Good error handling
  • ✅ No security issues
  • ⚠️ A few minor issues that should be addressed

Priority Fixes:

  1. Issue Implementing WordPress.com Notifications Activities #2 - Add duplicate check in addCard() (Medium priority)
  2. Issue Fix for bug #403 #4 - Consider card order preservation when re-adding (Low priority, UX decision)

Optional Improvements:

Great work on this feature! The testing and migration handling are particularly well done.


@adalpari
Copy link
Contributor Author

adalpari commented Feb 2, 2026

4. Card Order Not Preserved on Add

File: StatsCardsConfigurationRepository.kt:55

val newVisibleCards = current.visibleCards + cardType

Issue: When a card is added back, it's appended to the end rather than being inserted at its original position (based on defaultOrder). This means if a user removes the second card and adds it back, it appears at the end instead of its original position.

This is expected

@adalpari adalpari marked this pull request as ready for review February 2, 2026 15:24
@adalpari
Copy link
Contributor Author

adalpari commented Feb 2, 2026

I've now achieved some of the Claude's suggestions, and the PR is ready to be reviewed

@adalpari adalpari requested a review from nbradbury February 2, 2026 15:24
@nbradbury
Copy link
Contributor

@adalpari This is a nitpick, but how about changing this message to simply, "No cards to display" (removing the part about tapping the button, since that seems unnecessary)?

add

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Feb 2, 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
Versionpr22552-4b8e033
Build Number1483
Application IDcom.jetpack.android.prealpha
Commit4b8e033
Installation URL41maohulcth10
Note: Google Login is not supported on these builds.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Feb 2, 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
Versionpr22552-4b8e033
Build Number1483
Application IDorg.wordpress.android.prealpha
Commit4b8e033
Installation URL4ju5nopn5blvg
Note: Google Login is not supported on these builds.

@codecov
Copy link

codecov bot commented Feb 2, 2026

Codecov Report

❌ Patch coverage is 34.05405% with 366 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.64%. Comparing base (c7bafd8) to head (4b8e033).
⚠️ Report is 3 commits behind head on trunk.

Files with missing lines Patch % Lines
.../wordpress/android/ui/newstats/NewStatsActivity.kt 0.00% 99 Missing ⚠️
.../ui/newstats/components/AddStatsCardBottomSheet.kt 0.00% 50 Missing ⚠️
...s/android/ui/newstats/viewsstats/ViewsStatsCard.kt 0.00% 50 Missing ⚠️
...ess/android/ui/newstats/countries/CountriesCard.kt 0.00% 32 Missing ⚠️
...s/android/ui/newstats/mostviewed/MostViewedCard.kt 0.00% 29 Missing ⚠️
...android/ui/newstats/todaysstats/TodaysStatsCard.kt 0.00% 29 Missing ⚠️
...ss/android/ui/newstats/components/StatsCardMenu.kt 0.00% 26 Missing ⚠️
...roid/ui/newstats/viewsstats/ViewsStatsViewModel.kt 46.93% 16 Missing and 10 partials ⚠️
...roid/ui/newstats/mostviewed/MostViewedViewModel.kt 85.86% 10 Missing and 3 partials ⚠️
.../java/org/wordpress/android/ui/prefs/AppPrefs.java 0.00% 8 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##            trunk   #22552      +/-   ##
==========================================
+ Coverage   38.62%   38.64%   +0.01%     
==========================================
  Files        2219     2225       +6     
  Lines      109055   109361     +306     
  Branches    15293    15327      +34     
==========================================
+ Hits        42125    42262     +137     
- Misses      63433    63592     +159     
- Partials     3497     3507      +10     

☔ 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
Contributor Author

adalpari commented Feb 2, 2026

@adalpari This is a nitpick, but how about changing this message to simply, "No cards to display" (removing the part about tapping the button, since that seems unnecessary)?

Sounds reasonable. This is how it looks now:

Screenshot 2026-02-02 at 17 06 58

@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 2, 2026

Copy link
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 good! :shipit:

@adalpari adalpari merged commit 4151e61 into trunk Feb 3, 2026
24 checks passed
@adalpari adalpari deleted the feat/CMM-1146-stats-Create-cards-handling branch February 3, 2026 09:30
@kean
Copy link

kean commented Feb 9, 2026

Great progress!

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.

5 participants