From 840c35a7af3de17c7bfc7111a66201266f986092 Mon Sep 17 00:00:00 2001 From: Trey T Date: Sat, 18 Apr 2026 14:50:07 -0500 Subject: [PATCH] P7 Stream Y: empty/error/loading state audit (iOS parity) Audits every list + detail screen (non-document) for empty/error/loading state parity with iOS. Reuses StandardEmptyState / StandardErrorState where possible; adds missing states where screens currently show blank on error. - Add StandardErrorState and CompactErrorState components under ui/components/common/ (mirrors iOS ErrorView pattern: icon + title + message + Retry). - ManageUsersScreen: error state previously had no retry button; now uses StandardErrorState with a Retry CTA matching iOS ManageUsersView. - ResidenceDetailScreen: task and contractor sub-section error cards now use CompactErrorState with inline retry (previously plain error text). Other audited screens (ResidencesScreen, TasksScreen, AllTasksScreen, ContractorsScreen, ContractorDetailScreen, EditTaskScreen, CompleteTaskScreen, TaskTemplatesBrowserScreen, TaskSuggestionsScreen, OnboardingFirstTaskContent) already had loading + error + empty parity via ApiResultHandler / HandleErrors / inline state machines; no changes needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/common/StandardErrorState.kt | 120 ++++++++++++++++++ .../honeyDue/ui/screens/ManageUsersScreen.kt | 40 +++--- .../ui/screens/ResidenceDetailScreen.kt | 31 ++--- 3 files changed, 145 insertions(+), 46 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/StandardErrorState.kt diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/StandardErrorState.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/StandardErrorState.kt new file mode 100644 index 0000000..ad13154 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/components/common/StandardErrorState.kt @@ -0,0 +1,120 @@ +package com.tt.honeyDue.ui.components.common + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ErrorOutline +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tt.honeyDue.ui.theme.AppSpacing + +/** + * StandardErrorState - Inline error state with retry button + * Matches iOS ErrorView pattern + * + * Use this for inline error states where the caller wants a retry CTA + * directly in the content area (e.g. list/detail load failure). + * For modal error presentation, keep using ErrorDialog / HandleErrors. + * + * Usage: + * ``` + * StandardErrorState( + * title = "Couldn't load users", + * message = errorMessage, + * onRetry = { viewModel.refresh() } + * ) + * ``` + */ +@Composable +fun StandardErrorState( + message: String, + onRetry: () -> Unit, + modifier: Modifier = Modifier, + title: String = "Something went wrong", + icon: ImageVector = Icons.Default.ErrorOutline, + retryLabel: String = "Retry" +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(AppSpacing.xl), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(AppSpacing.md, Alignment.CenterVertically) + ) { + Icon( + imageVector = icon, + contentDescription = null, // decorative + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.error + ) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center + ) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.widthIn(max = 320.dp) + ) + OutlinedButton(onClick = onRetry) { + Text(retryLabel) + } + } +} + +/** + * Compact inline error for embedded sub-sections (e.g. inside a LazyColumn item) + * where we don't have fillMaxSize available. Shows the message + inline retry. + */ +@Composable +fun CompactErrorState( + message: String, + onRetry: () -> Unit, + modifier: Modifier = Modifier, + retryLabel: String = "Retry" +) { + Card( + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ), + modifier = modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(AppSpacing.lg), + verticalArrangement = Arrangement.spacedBy(AppSpacing.sm) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm) + ) { + Icon( + imageVector = Icons.Default.ErrorOutline, + contentDescription = null, // decorative + tint = MaterialTheme.colorScheme.onErrorContainer + ) + Text( + text = message, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.weight(1f) + ) + } + TextButton( + onClick = onRetry, + modifier = Modifier.align(Alignment.End) + ) { + Text(retryLabel) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ManageUsersScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ManageUsersScreen.kt index 6876f6f..f1396ed 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ManageUsersScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ManageUsersScreen.kt @@ -25,6 +25,7 @@ import com.tt.honeyDue.models.ResidenceShareCode import com.tt.honeyDue.network.ApiResult import com.tt.honeyDue.network.APILayer import com.tt.honeyDue.testing.AccessibilityIds +import com.tt.honeyDue.ui.components.common.StandardErrorState import com.tt.honeyDue.ui.theme.* import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource @@ -51,8 +52,10 @@ fun ManageUsersScreen( val clipboardManager = LocalClipboardManager.current val snackbarHostState = remember { SnackbarHostState() } - LaunchedEffect(residenceId) { - shareCode = null + // Extracted so retry can reuse it. + suspend fun loadUsers() { + isLoading = true + error = null when (val result = APILayer.getResidenceUsers(residenceId)) { is ApiResult.Success -> { users = result.data @@ -66,6 +69,11 @@ fun ManageUsersScreen( } } + LaunchedEffect(residenceId) { + shareCode = null + loadUsers() + } + WarmGradientBackground { Scaffold( modifier = Modifier.semantics { testTagsAsResourceId = true }, @@ -107,28 +115,12 @@ fun ManageUsersScreen( CircularProgressIndicator() } } else if (error != null) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md) - ) { - Icon( - Icons.Default.Error, - contentDescription = null, // decorative - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.error - ) - Text( - text = error ?: "Unknown error", - color = MaterialTheme.colorScheme.error - ) - } - } + StandardErrorState( + title = "Couldn't load users", + message = error ?: "Unknown error", + onRetry = { scope.launch { loadUsers() } }, + modifier = Modifier.padding(paddingValues) + ) } else { LazyColumn( modifier = Modifier diff --git a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ResidenceDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ResidenceDetailScreen.kt index 26852f8..46688f5 100644 --- a/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ResidenceDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/tt/honeyDue/ui/screens/ResidenceDetailScreen.kt @@ -25,6 +25,7 @@ import com.tt.honeyDue.ui.components.ApiResultHandler import com.tt.honeyDue.ui.components.CompleteTaskDialog import com.tt.honeyDue.ui.components.HandleErrors import com.tt.honeyDue.ui.components.ManageUsersDialog +import com.tt.honeyDue.ui.components.common.CompactErrorState import com.tt.honeyDue.ui.components.common.InfoCard import com.tt.honeyDue.ui.components.residence.PropertyDetailItem import com.tt.honeyDue.ui.components.residence.DetailRow @@ -874,17 +875,10 @@ fun ResidenceDetailScreen( } is ApiResult.Error -> { item { - OrganicCard( - modifier = Modifier.fillMaxWidth(), - accentColor = MaterialTheme.colorScheme.error, - showBlob = false - ) { - Text( - text = "Error loading tasks: ${com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}", - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(OrganicSpacing.cozy) - ) - } + CompactErrorState( + message = "Error loading tasks: ${com.tt.honeyDue.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}", + onRetry = { residenceViewModel.loadResidenceTasks(residenceId) } + ) } } is ApiResult.Success -> { @@ -1016,17 +1010,10 @@ fun ResidenceDetailScreen( } is ApiResult.Error -> { item { - OrganicCard( - modifier = Modifier.fillMaxWidth(), - accentColor = MaterialTheme.colorScheme.error, - showBlob = false - ) { - Text( - text = "Error loading contractors: ${com.tt.honeyDue.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}", - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(OrganicSpacing.cozy) - ) - } + CompactErrorState( + message = "Error loading contractors: ${com.tt.honeyDue.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}", + onRetry = { residenceViewModel.loadResidenceContractors(residenceId) } + ) } } is ApiResult.Success -> {