Skip to content

Commit

Permalink
Merge pull request #387 from Assocify-Team/sk/create-select-errors
Browse files Browse the repository at this point in the history
Error messages in create and select association
  • Loading branch information
SekoiaTree authored Jun 2, 2024
2 parents 7473917 + 372204e commit b39e2ac
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class SelectAssociationScreenTest(semanticsProvider: SemanticsNodeInteractionsPr
val createOrgaButton: KNode = child { hasTestTag("CreateNewOrganizationButton") }
val searchOrgaButton: KNode = onNode { hasTestTag("SOB") }
val arrowBackButton: KNode = onNode { hasTestTag("ArrowBackButton") }
val snackbar: KNode = onNode { hasTestTag("snackbar") }
}

/**
Expand Down Expand Up @@ -222,4 +223,22 @@ class SelectAssociationTest : TestCase(kaspressoBuilder = Kaspresso.Builder.with
arrowBackButton { assertIsNotDisplayed() }
}
}

@Test
fun testSelectAssocFailure() {
every { mockUserAPI.requestJoin(any(), any(), any()) } answers
{
val onFailureCallback = thirdArg<(Exception) -> Unit>()
onFailureCallback(Exception("Test exception"))
}

val model = SelectAssociationViewModel(mockAssocAPI, mockUserAPI, mockNavActions)

composeTestRule.setContent { SelectAssociationScreen(mockNavActions, model) }
composeTestRule.onNodeWithTag("DisplayOrganizationScreen-${testAssociation.uid}").performClick()

ComposeScreen.onComposeScreen<SelectAssociationScreenTest>(composeTestRule) {
snackbar.assertIsDisplayed()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ class UserAPI(private val db: SupabaseClient, cachePath: Path) : SupabaseApi() {
fun requestJoin(associationId: String, onSuccess: () -> Unit, onFailure: (Exception) -> Unit) {
tryAsync(onFailure) {
db.from("applicant")
.insert(
.upsert(
Json.decodeFromString<JsonElement>(
"""{"association_id": "$associationId", "user_id": "${CurrentUser.userUid}"}"""))
onSuccess()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.github.se.assocify.model.entities.PermissionRole
import com.github.se.assocify.model.entities.RoleType
import com.github.se.assocify.model.entities.User
import com.github.se.assocify.navigation.NavigationActions
import com.github.se.assocify.ui.util.SnackbarSystem
import java.time.LocalDate
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
Expand All @@ -30,6 +31,8 @@ class CreateAssociationViewmodel(
private val _uiState = MutableStateFlow(CreateAssoUIState())
val uiState: StateFlow<CreateAssoUIState> = _uiState

private val snackbarSystem = SnackbarSystem(uiState.value.snackbarHostState)

private var association =
Association(UUID.randomUUID().toString(), _uiState.value.name, "", LocalDate.now())
private val roles =
Expand Down Expand Up @@ -177,10 +180,14 @@ class CreateAssociationViewmodel(
_uiState.value.nameError == null)
}

var currentlySaving = false
/*
* Saves the association in the database
*/
fun saveAsso() {
if (currentlySaving) return

currentlySaving = true
assoAPI.addAssociation(
association,
onSuccess = {
Expand All @@ -189,9 +196,12 @@ class CreateAssociationViewmodel(
_uiState.value.members,
{
CurrentUser.associationUid = association.uid
userAPI.updateCurrentUserAssociationCache({ navActions.goFromCreateAsso() }, {})
currentlySaving = false
// Navigate in both cases, so that the error shows up in the profile
userAPI.updateCurrentUserAssociationCache(
{ navActions.goFromCreateAsso() }, { navActions.goFromCreateAsso() })
},
{})
{}) // Exception not dealt with, to do in v1.1 (use server funcs, it's atomic)
if (_uiState.value.imageUri != null) {
assoAPI.setLogo(
association.uid,
Expand All @@ -204,7 +214,9 @@ class CreateAssociationViewmodel(
}
},
onFailure = { exception ->
currentlySaving = false
Log.e("CreateAssoViewModel", "Failed to add asso: ${exception.message}")
snackbarSystem.showSnackbar("Failed to create association.", "Retry", this::saveAsso)
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SearchBar
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
Expand Down Expand Up @@ -85,6 +87,13 @@ fun SelectAssociationScreen(navActions: NavigationActions, viewModel: SelectAsso
contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.primary))
},
snackbarHost = {
SnackbarHost(
state.value.snackbarHostState,
snackbar = { snackbarData ->
Snackbar(snackbarData = snackbarData, modifier = Modifier.testTag("snackbar"))
})
},
contentWindowInsets = WindowInsets(20.dp, 10.dp, 20.dp, 20.dp)) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
SearchBar(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.github.se.assocify.ui.screens.selectAssociation

import android.util.Log
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.ViewModel
import com.github.se.assocify.model.CurrentUser
import com.github.se.assocify.model.database.AssociationAPI
Expand All @@ -8,6 +10,7 @@ import com.github.se.assocify.model.entities.Association
import com.github.se.assocify.model.entities.RoleType
import com.github.se.assocify.model.entities.User
import com.github.se.assocify.navigation.NavigationActions
import com.github.se.assocify.ui.util.SnackbarSystem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

Expand All @@ -24,10 +27,11 @@ class SelectAssociationViewModel(
) : ViewModel() {
private val _uiState: MutableStateFlow<SelectAssociationState> =
MutableStateFlow(SelectAssociationState())
val uiState: StateFlow<SelectAssociationState>
val uiState: StateFlow<SelectAssociationState> = _uiState

private val snackbarSystem = SnackbarSystem(_uiState.value.snackbarHostState)

init {
uiState = _uiState
updateDatabaseValues()
}

Expand All @@ -50,6 +54,12 @@ class SelectAssociationViewModel(
/** Confirms selection of an association and moves to the home screen. */
fun selectAssoc(uid: String) {
CurrentUser.associationUid = uid
val selectError = { e: Exception ->
Log.e("SelectAssociationViewModel", "Error selecting association: $e")
val association = uiState.value.associations.find { it.uid == uid }?.name ?: "Unknown"
snackbarSystem.showSnackbar(
"Error joining association \"$association\"", "Retry", { selectAssoc(uid) })
}
userAPI.requestJoin(
uid,
{
Expand All @@ -62,22 +72,21 @@ class SelectAssociationViewModel(
CurrentUser.userUid!!,
role,
{
userAPI.updateCurrentUserAssociationCache(
{
if (navActions.backFromSelectAsso()) {
navActions.back()
} else {
navActions.onLogin(true)
}
},
{})
val exit = {
if (navActions.backFromSelectAsso()) {
navActions.back()
} else {
navActions.onLogin(true)
}
}
userAPI.updateCurrentUserAssociationCache({ exit() }, { exit() })
},
{})
selectError)
}
},
{})
selectError)
},
{})
selectError)
}
}

Expand All @@ -93,5 +102,6 @@ data class SelectAssociationState(
val associations: List<Association> = emptyList(),
val searchQuery: String = "",
val user: User = User(),
val searchState: Boolean = false
val searchState: Boolean = false,
val snackbarHostState: SnackbarHostState = SnackbarHostState()
)
86 changes: 45 additions & 41 deletions app/src/main/java/com/github/se/assocify/ui/util/SnackbarSystem.kt
Original file line number Diff line number Diff line change
@@ -1,41 +1,45 @@
package com.github.se.assocify.ui.util

import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/** A system to show snackbars in the app. */
class SnackbarSystem(private val snackbarHostState: SnackbarHostState) {
/**
* Shows a snackbar with the given message.
*
* Note : Persistent snackbars with no action will have a cross to dismiss them even if dismiss is
* false
*
* @param message the message to show
* @param actionLabel the label for the action button
* @param actionHandler the handler for the action button
* @param dismiss whether the snackbar should have a cross to dismiss it
* @param persistent whether the snackbar should remain until explicitely dismissed (otherwise,
* times out on its own)
*/
fun showSnackbar(
message: String,
actionLabel: String? = null,
actionHandler: (() -> Unit) = {},
dismiss: Boolean = false,
persistent: Boolean = false,
) {

CoroutineScope(Dispatchers.Main).launch {
val result =
snackbarHostState.showSnackbar(
message, actionLabel, dismiss || (persistent && actionLabel == null))
if (result == SnackbarResult.ActionPerformed) {
actionHandler()
}
}
}
}
package com.github.se.assocify.ui.util

import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

/** A system to show snackbars in the app. */
class SnackbarSystem(private val snackbarHostState: SnackbarHostState) {
/**
* Shows a snackbar with the given message.
*
* Note : Persistent snackbars with no action will have a cross to dismiss them even if dismiss is
* false
*
* @param message the message to show
* @param actionLabel the label for the action button
* @param actionHandler the handler for the action button
* @param dismiss whether the snackbar should have a cross to dismiss it
* @param persistent whether the snackbar should remain until explicitely dismissed (otherwise,
* times out on its own)
*/
fun showSnackbar(
message: String,
actionLabel: String? = null,
actionHandler: (() -> Unit) = {},
dismiss: Boolean = false,
persistent: Boolean = false,
) {

CoroutineScope(Dispatchers.Main).launch {
val result =
snackbarHostState.showSnackbar(
message,
actionLabel,
dismiss || (persistent && actionLabel == null),
if (persistent) SnackbarDuration.Indefinite else SnackbarDuration.Short)
if (result == SnackbarResult.ActionPerformed) {
actionHandler()
}
}
}
}

0 comments on commit b39e2ac

Please sign in to comment.