mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 23:25:45 +03:00
BIT-842, BIT-843: Add Vault Filter and Vault Selection menu UI (#448)
This commit is contained in:
parent
3c29dccf62
commit
3f0e44d42f
11 changed files with 631 additions and 30 deletions
|
@ -0,0 +1,99 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBackground
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the current [selectedVaultFilterType] and allows for a new selection from the
|
||||||
|
* given [vaultFilterTypes].
|
||||||
|
*
|
||||||
|
* @param selectedVaultFilterType The currently selected filter type.
|
||||||
|
* @param vaultFilterTypes The list of possible filter types.
|
||||||
|
* @param onVaultFilterTypeSelect A callback for when a new type is selected.
|
||||||
|
* @param topAppBarScrollBehavior Used to derive the background color of the content and keep it in
|
||||||
|
* sync with the associated app bar.
|
||||||
|
* @param modifier A [Modifier] for the composable.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun VaultFilter(
|
||||||
|
selectedVaultFilterType: VaultFilterType,
|
||||||
|
vaultFilterTypes: ImmutableList<VaultFilterType>,
|
||||||
|
onVaultFilterTypeSelect: (VaultFilterType) -> Unit,
|
||||||
|
topAppBarScrollBehavior: TopAppBarScrollBehavior,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var shouldShowSelectionDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
if (shouldShowSelectionDialog) {
|
||||||
|
BitwardenSelectionDialog(
|
||||||
|
title = stringResource(id = R.string.filter_by_vault),
|
||||||
|
onDismissRequest = { shouldShowSelectionDialog = false },
|
||||||
|
) {
|
||||||
|
vaultFilterTypes.forEach { filterType ->
|
||||||
|
BitwardenSelectionRow(
|
||||||
|
text = filterType.description,
|
||||||
|
isSelected = filterType == selectedVaultFilterType,
|
||||||
|
onClick = {
|
||||||
|
shouldShowSelectionDialog = false
|
||||||
|
onVaultFilterTypeSelect(filterType)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.scrolledContainerBackground(topAppBarScrollBehavior)
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
.then(modifier),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.vault_filter_description,
|
||||||
|
selectedVaultFilterType.name(),
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { shouldShowSelectionDialog = true },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_more_horizontal),
|
||||||
|
contentDescription = stringResource(id = R.string.filter_by_vault),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,10 +5,13 @@ import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
@ -42,6 +45,7 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenSearchActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
@ -90,6 +94,9 @@ fun VaultScreen(
|
||||||
}
|
}
|
||||||
VaultScreenScaffold(
|
VaultScreenScaffold(
|
||||||
state = viewModel.stateFlow.collectAsState().value,
|
state = viewModel.stateFlow.collectAsState().value,
|
||||||
|
vaultFilterTypeSelect = remember(viewModel) {
|
||||||
|
{ viewModel.trySendAction(VaultAction.VaultFilterTypeSelect(it)) }
|
||||||
|
},
|
||||||
addItemClickAction = remember(viewModel) {
|
addItemClickAction = remember(viewModel) {
|
||||||
{ viewModel.trySendAction(VaultAction.AddItemClick) }
|
{ viewModel.trySendAction(VaultAction.AddItemClick) }
|
||||||
},
|
},
|
||||||
|
@ -161,6 +168,7 @@ fun VaultScreen(
|
||||||
@Composable
|
@Composable
|
||||||
private fun VaultScreenScaffold(
|
private fun VaultScreenScaffold(
|
||||||
state: VaultState,
|
state: VaultState,
|
||||||
|
vaultFilterTypeSelect: (VaultFilterType) -> Unit,
|
||||||
addItemClickAction: () -> Unit,
|
addItemClickAction: () -> Unit,
|
||||||
searchIconClickAction: () -> Unit,
|
searchIconClickAction: () -> Unit,
|
||||||
accountLockClickAction: (AccountSummary) -> Unit,
|
accountLockClickAction: (AccountSummary) -> Unit,
|
||||||
|
@ -230,7 +238,7 @@ private fun VaultScreenScaffold(
|
||||||
BitwardenScaffold(
|
BitwardenScaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
BitwardenMediumTopAppBar(
|
BitwardenMediumTopAppBar(
|
||||||
title = stringResource(id = R.string.my_vault),
|
title = state.appBarTitle(),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
actions = {
|
actions = {
|
||||||
BitwardenAccountActionItem(
|
BitwardenAccountActionItem(
|
||||||
|
@ -283,10 +291,33 @@ private fun VaultScreenScaffold(
|
||||||
},
|
},
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val modifier = Modifier
|
Box {
|
||||||
|
val innerModifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
val outerModifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
Box {
|
Column(modifier = outerModifier) {
|
||||||
|
val vaultFilterData = state.vaultFilterData
|
||||||
|
if (state.viewState.hasVaultFilter && vaultFilterData != null) {
|
||||||
|
VaultFilter(
|
||||||
|
selectedVaultFilterType = vaultFilterData.selectedVaultFilterType,
|
||||||
|
vaultFilterTypes = vaultFilterData.vaultFilterTypes.toImmutableList(),
|
||||||
|
onVaultFilterTypeSelect = vaultFilterTypeSelect,
|
||||||
|
topAppBarScrollBehavior = scrollBehavior,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
when (val viewState = state.viewState) {
|
when (val viewState = state.viewState) {
|
||||||
is VaultState.ViewState.Content -> VaultContent(
|
is VaultState.ViewState.Content -> VaultContent(
|
||||||
state = viewState,
|
state = viewState,
|
||||||
|
@ -298,22 +329,23 @@ private fun VaultScreenScaffold(
|
||||||
identityGroupClick = identityGroupClick,
|
identityGroupClick = identityGroupClick,
|
||||||
secureNoteGroupClick = secureNoteGroupClick,
|
secureNoteGroupClick = secureNoteGroupClick,
|
||||||
trashClick = trashClick,
|
trashClick = trashClick,
|
||||||
modifier = modifier,
|
modifier = innerModifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
is VaultState.ViewState.Loading -> VaultLoading(modifier = modifier)
|
is VaultState.ViewState.Loading -> VaultLoading(modifier = innerModifier)
|
||||||
is VaultState.ViewState.NoItems -> VaultNoItems(
|
is VaultState.ViewState.NoItems -> VaultNoItems(
|
||||||
modifier = modifier,
|
modifier = innerModifier,
|
||||||
addItemClickAction = addItemClickAction,
|
addItemClickAction = addItemClickAction,
|
||||||
)
|
)
|
||||||
|
|
||||||
is VaultState.ViewState.Error -> BitwardenErrorContent(
|
is VaultState.ViewState.Error -> BitwardenErrorContent(
|
||||||
message = viewState.message(),
|
message = viewState.message(),
|
||||||
onTryAgainClick = tryAgainClick,
|
onTryAgainClick = tryAgainClick,
|
||||||
modifier = modifier
|
modifier = innerModifier
|
||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BitwardenAccountSwitcher(
|
BitwardenAccountSwitcher(
|
||||||
isVisible = accountMenuVisible,
|
isVisible = accountMenuVisible,
|
||||||
|
@ -324,7 +356,7 @@ private fun VaultScreenScaffold(
|
||||||
onAddAccountClick = addAccountClickAction,
|
onAddAccountClick = addAccountClickAction,
|
||||||
onDismissRequest = { updateAccountMenuVisibility(false) },
|
onDismissRequest = { updateAccountMenuVisibility(false) },
|
||||||
topAppBarScrollBehavior = scrollBehavior,
|
topAppBarScrollBehavior = scrollBehavior,
|
||||||
modifier = modifier,
|
modifier = outerModifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,9 +17,13 @@ import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
|
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAccountSummaries
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toActiveAccountSummary
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toAppBarTitle
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toVaultFilterData
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toViewState
|
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toViewState
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
@ -45,10 +49,14 @@ class VaultViewModel @Inject constructor(
|
||||||
val userState = requireNotNull(authRepository.userStateFlow.value)
|
val userState = requireNotNull(authRepository.userStateFlow.value)
|
||||||
val accountSummaries = userState.toAccountSummaries()
|
val accountSummaries = userState.toAccountSummaries()
|
||||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||||
|
val vaultFilterData = userState.activeAccount.toVaultFilterData()
|
||||||
|
val appBarTitle = vaultFilterData.toAppBarTitle()
|
||||||
VaultState(
|
VaultState(
|
||||||
|
appBarTitle = appBarTitle,
|
||||||
initials = activeAccountSummary.initials,
|
initials = activeAccountSummary.initials,
|
||||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||||
accountSummaries = accountSummaries,
|
accountSummaries = accountSummaries,
|
||||||
|
vaultFilterData = vaultFilterData,
|
||||||
viewState = VaultState.ViewState.Loading,
|
viewState = VaultState.ViewState.Loading,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -96,6 +104,7 @@ class VaultViewModel @Inject constructor(
|
||||||
is VaultAction.SyncClick -> handleSyncClick()
|
is VaultAction.SyncClick -> handleSyncClick()
|
||||||
is VaultAction.LockClick -> handleLockClick()
|
is VaultAction.LockClick -> handleLockClick()
|
||||||
is VaultAction.ExitConfirmationClick -> handleExitConfirmationClick()
|
is VaultAction.ExitConfirmationClick -> handleExitConfirmationClick()
|
||||||
|
is VaultAction.VaultFilterTypeSelect -> handleVaultFilterTypeSelect(action)
|
||||||
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
is VaultAction.SecureNoteGroupClick -> handleSecureNoteClick()
|
||||||
is VaultAction.TrashClick -> handleTrashClick()
|
is VaultAction.TrashClick -> handleTrashClick()
|
||||||
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
is VaultAction.VaultItemClick -> handleVaultItemClick(action)
|
||||||
|
@ -183,6 +192,16 @@ class VaultViewModel @Inject constructor(
|
||||||
sendEvent(VaultEvent.NavigateOutOfApp)
|
sendEvent(VaultEvent.NavigateOutOfApp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleVaultFilterTypeSelect(action: VaultAction.VaultFilterTypeSelect) {
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
vaultFilterData = it.vaultFilterData?.copy(
|
||||||
|
selectedVaultFilterType = action.vaultFilterType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleTrashClick() {
|
private fun handleTrashClick() {
|
||||||
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.Trash))
|
sendEvent(VaultEvent.NavigateToItemListing(VaultItemListingType.Trash))
|
||||||
}
|
}
|
||||||
|
@ -214,13 +233,17 @@ class VaultViewModel @Inject constructor(
|
||||||
// navigating.
|
// navigating.
|
||||||
if (state.isSwitchingAccounts) return
|
if (state.isSwitchingAccounts) return
|
||||||
|
|
||||||
|
val vaultFilterData = userState.activeAccount.toVaultFilterData()
|
||||||
|
val appBarTitle = vaultFilterData.toAppBarTitle()
|
||||||
mutableStateFlow.update {
|
mutableStateFlow.update {
|
||||||
val accountSummaries = userState.toAccountSummaries()
|
val accountSummaries = userState.toAccountSummaries()
|
||||||
val activeAccountSummary = userState.toActiveAccountSummary()
|
val activeAccountSummary = userState.toActiveAccountSummary()
|
||||||
it.copy(
|
it.copy(
|
||||||
|
appBarTitle = appBarTitle,
|
||||||
initials = activeAccountSummary.initials,
|
initials = activeAccountSummary.initials,
|
||||||
avatarColorString = activeAccountSummary.avatarColorHex,
|
avatarColorString = activeAccountSummary.avatarColorHex,
|
||||||
accountSummaries = accountSummaries,
|
accountSummaries = accountSummaries,
|
||||||
|
vaultFilterData = vaultFilterData,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -284,9 +307,11 @@ class VaultViewModel @Inject constructor(
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class VaultState(
|
data class VaultState(
|
||||||
|
val appBarTitle: Text,
|
||||||
private val avatarColorString: String,
|
private val avatarColorString: String,
|
||||||
val initials: String,
|
val initials: String,
|
||||||
val accountSummaries: List<AccountSummary>,
|
val accountSummaries: List<AccountSummary>,
|
||||||
|
val vaultFilterData: VaultFilterData? = null,
|
||||||
val viewState: ViewState,
|
val viewState: ViewState,
|
||||||
val dialog: DialogState? = null,
|
val dialog: DialogState? = null,
|
||||||
// Internal-use properties
|
// Internal-use properties
|
||||||
|
@ -310,12 +335,18 @@ data class VaultState(
|
||||||
*/
|
*/
|
||||||
abstract val hasFab: Boolean
|
abstract val hasFab: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether or not the the Vault Filter may be shown (when applicable).
|
||||||
|
*/
|
||||||
|
abstract val hasVaultFilter: Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading state for the [VaultScreen], signifying that the content is being processed.
|
* Loading state for the [VaultScreen], signifying that the content is being processed.
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object Loading : ViewState() {
|
data object Loading : ViewState() {
|
||||||
override val hasFab: Boolean get() = false
|
override val hasFab: Boolean get() = false
|
||||||
|
override val hasVaultFilter: Boolean get() = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -324,6 +355,7 @@ data class VaultState(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object NoItems : ViewState() {
|
data object NoItems : ViewState() {
|
||||||
override val hasFab: Boolean get() = true
|
override val hasFab: Boolean get() = true
|
||||||
|
override val hasVaultFilter: Boolean get() = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -335,6 +367,7 @@ data class VaultState(
|
||||||
val message: Text,
|
val message: Text,
|
||||||
) : ViewState() {
|
) : ViewState() {
|
||||||
override val hasFab: Boolean get() = false
|
override val hasFab: Boolean get() = false
|
||||||
|
override val hasVaultFilter: Boolean get() = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -363,6 +396,7 @@ data class VaultState(
|
||||||
val trashItemsCount: Int,
|
val trashItemsCount: Int,
|
||||||
) : ViewState() {
|
) : ViewState() {
|
||||||
override val hasFab: Boolean get() = true
|
override val hasFab: Boolean get() = true
|
||||||
|
override val hasVaultFilter: Boolean get() = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -621,6 +655,13 @@ sealed class VaultAction {
|
||||||
*/
|
*/
|
||||||
data object ExitConfirmationClick : VaultAction()
|
data object ExitConfirmationClick : VaultAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User selected a [VaultFilterType] from the Vault Filter menu.
|
||||||
|
*/
|
||||||
|
data class VaultFilterTypeSelect(
|
||||||
|
val vaultFilterType: VaultFilterType,
|
||||||
|
) : VaultAction()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Action to trigger when a specific vault item is clicked.
|
* Action to trigger when a specific vault item is clicked.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.vault.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a currently [selectedVaultFilterType] and a list of possible [vaultFilterTypes].
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class VaultFilterData(
|
||||||
|
val selectedVaultFilterType: VaultFilterType,
|
||||||
|
val vaultFilterTypes: List<VaultFilterType>,
|
||||||
|
) : Parcelable
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.vault.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a way to filter on vaults when more than one may be present in a list of vault items.
|
||||||
|
*/
|
||||||
|
sealed class VaultFilterType : Parcelable {
|
||||||
|
/**
|
||||||
|
* A short name for the filter.
|
||||||
|
*/
|
||||||
|
abstract val name: Text
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A potentially longer description of the filter. This may be the same as the [name] when there
|
||||||
|
* is no distinction necessary.
|
||||||
|
*/
|
||||||
|
abstract val description: Text
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data from all vaults should be present (i.e. there is no filtering).
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object AllVaults : VaultFilterType() {
|
||||||
|
override val name: Text get() = R.string.all.asText()
|
||||||
|
override val description: Text get() = R.string.all_vaults.asText()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only data from the user's personal vault shoudl be present.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data object MyVault : VaultFilterType() {
|
||||||
|
override val name: Text get() = R.string.my_vault.asText()
|
||||||
|
override val description: Text get() = R.string.my_vault.asText()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only data from the organization with the given [organizationId] should be present.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class OrganizationVault(
|
||||||
|
val organizationId: String,
|
||||||
|
val organizationName: String,
|
||||||
|
) : VaultFilterType() {
|
||||||
|
override val name: Text get() = organizationName.asText()
|
||||||
|
override val description: Text get() = organizationName.asText()
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,8 @@ package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||||
|
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the given [UserState] to a list of [AccountSummary].
|
* Converts the given [UserState] to a list of [AccountSummary].
|
||||||
|
@ -39,3 +41,30 @@ fun UserState.Account.toAccountSummary(
|
||||||
isActive = isActive,
|
isActive = isActive,
|
||||||
isVaultUnlocked = this.isVaultUnlocked,
|
isVaultUnlocked = this.isVaultUnlocked,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given [UserState.Account] to a [VaultFilterData] (if applicable). Filter data is
|
||||||
|
* only relevant when the given account is associated with one or more organizations.
|
||||||
|
*/
|
||||||
|
fun UserState.Account.toVaultFilterData(): VaultFilterData? =
|
||||||
|
this
|
||||||
|
.organizations
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.let { organizations ->
|
||||||
|
VaultFilterData(
|
||||||
|
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
vaultFilterTypes = listOf(
|
||||||
|
VaultFilterType.AllVaults,
|
||||||
|
VaultFilterType.MyVault,
|
||||||
|
*organizations
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.map { organization ->
|
||||||
|
VaultFilterType.OrganizationVault(
|
||||||
|
organizationId = organization.id,
|
||||||
|
organizationName = organization.name.orEmpty(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toTypedArray(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives an app bar title given the [VaultFilterData].
|
||||||
|
*/
|
||||||
|
fun VaultFilterData?.toAppBarTitle(): Text =
|
||||||
|
if (this != null) {
|
||||||
|
R.string.vaults
|
||||||
|
} else {
|
||||||
|
R.string.my_vault
|
||||||
|
}
|
||||||
|
.asText()
|
|
@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.compose.ui.test.performScrollToNode
|
import androidx.compose.ui.test.performScrollToNode
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
@ -32,6 +33,8 @@ import com.x8bit.bitwarden.ui.util.performAddAccountClick
|
||||||
import com.x8bit.bitwarden.ui.util.performLockAccountClick
|
import com.x8bit.bitwarden.ui.util.performLockAccountClick
|
||||||
import com.x8bit.bitwarden.ui.util.performLogoutAccountClick
|
import com.x8bit.bitwarden.ui.util.performLogoutAccountClick
|
||||||
import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick
|
import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
@ -80,6 +83,135 @@ class VaultScreenTest : BaseComposeTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `app bar title should update according to state`() {
|
||||||
|
composeTestRule.onNodeWithText("My vault").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Vaults").assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(appBarTitle = R.string.vaults.asText())
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("My vault").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Vaults").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vault filter should update according to state`() {
|
||||||
|
composeTestRule.onNodeWithText("Vault: All").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Vault: My vault").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Vault: Test Organization").assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA,
|
||||||
|
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("Vault: All").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Vault: My vault").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Vault: Test Organization").assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA.copy(
|
||||||
|
selectedVaultFilterType = VaultFilterType.MyVault,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("Vault: All").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Vault: My vault").assertIsDisplayed()
|
||||||
|
composeTestRule.onNodeWithText("Vault: Test Organization").assertDoesNotExist()
|
||||||
|
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA.copy(
|
||||||
|
selectedVaultFilterType = ORGANIZATION_VAULT_FILTER,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithText("Vault: All").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Vault: My vault").assertDoesNotExist()
|
||||||
|
composeTestRule.onNodeWithText("Vault: Test Organization").assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `vault filter menu click should display the filter selection dialog`() {
|
||||||
|
// Display the vault filter
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA,
|
||||||
|
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
|
||||||
|
composeTestRule.onNodeWithContentDescription("Filter items by vault").performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("All vaults")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("My vault")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Test Organization")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Cancel")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.assertIsDisplayed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cancel click in the filter selection dialog should close the dialog`() {
|
||||||
|
// Display the vault selection dialog
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA,
|
||||||
|
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithContentDescription("Filter items by vault").performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("Cancel")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `vault filter click in the filter selection dialog should send VaultFilterTypeSelect and close the dialog`() {
|
||||||
|
// Display the vault selection dialog
|
||||||
|
mutableStateFlow.update {
|
||||||
|
it.copy(
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA,
|
||||||
|
viewState = DEFAULT_CONTENT_VIEW_STATE,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
composeTestRule.onNodeWithContentDescription("Filter items by vault").performClick()
|
||||||
|
|
||||||
|
composeTestRule
|
||||||
|
.onAllNodesWithText("All vaults")
|
||||||
|
.filterToOne(hasAnyAncestor(isDialog()))
|
||||||
|
.performClick()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
viewModel.trySendAction(VaultAction.VaultFilterTypeSelect(VaultFilterType.AllVaults))
|
||||||
|
}
|
||||||
|
composeTestRule.assertNoDialogExists()
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `account icon click should show the account switcher and trigger the nav bar dim request`() {
|
fun `account icon click should show the account switcher and trigger the nav bar dim request`() {
|
||||||
|
@ -858,6 +990,20 @@ private val LOCKED_ACCOUNT_SUMMARY = AccountSummary(
|
||||||
isVaultUnlocked = false,
|
isVaultUnlocked = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val ORGANIZATION_VAULT_FILTER = VaultFilterType.OrganizationVault(
|
||||||
|
organizationId = "testOrganizationId",
|
||||||
|
organizationName = "Test Organization",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val VAULT_FILTER_DATA = VaultFilterData(
|
||||||
|
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
vaultFilterTypes = listOf(
|
||||||
|
VaultFilterType.AllVaults,
|
||||||
|
VaultFilterType.MyVault,
|
||||||
|
ORGANIZATION_VAULT_FILTER,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE: VaultState = VaultState(
|
private val DEFAULT_STATE: VaultState = VaultState(
|
||||||
avatarColorString = "#aa00aa",
|
avatarColorString = "#aa00aa",
|
||||||
initials = "AU",
|
initials = "AU",
|
||||||
|
@ -865,6 +1011,7 @@ private val DEFAULT_STATE: VaultState = VaultState(
|
||||||
ACTIVE_ACCOUNT_SUMMARY,
|
ACTIVE_ACCOUNT_SUMMARY,
|
||||||
LOCKED_ACCOUNT_SUMMARY,
|
LOCKED_ACCOUNT_SUMMARY,
|
||||||
),
|
),
|
||||||
|
appBarTitle = R.string.my_vault.asText(),
|
||||||
viewState = VaultState.ViewState.Loading,
|
viewState = VaultState.ViewState.Loading,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.x8bit.bitwarden.R
|
import com.x8bit.bitwarden.R
|
||||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||||
|
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
import com.x8bit.bitwarden.data.auth.repository.model.SwitchAccountResult
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumstance
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState.SpecialCircumstance
|
||||||
|
@ -16,6 +17,8 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
|
@ -126,13 +129,19 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
environment = Environment.Us,
|
environment = Environment.Us,
|
||||||
isPremium = true,
|
isPremium = true,
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
organizations = emptyList(),
|
organizations = listOf(
|
||||||
|
Organization(
|
||||||
|
id = "organiationId",
|
||||||
|
name = "Test Organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
DEFAULT_STATE.copy(
|
DEFAULT_STATE.copy(
|
||||||
|
appBarTitle = R.string.vaults.asText(),
|
||||||
avatarColorString = "#00aaaa",
|
avatarColorString = "#00aaaa",
|
||||||
initials = "OU",
|
initials = "OU",
|
||||||
accountSummaries = listOf(
|
accountSummaries = listOf(
|
||||||
|
@ -146,6 +155,17 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
isVaultUnlocked = true,
|
isVaultUnlocked = true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
vaultFilterData = VaultFilterData(
|
||||||
|
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
vaultFilterTypes = listOf(
|
||||||
|
VaultFilterType.AllVaults,
|
||||||
|
VaultFilterType.MyVault,
|
||||||
|
VaultFilterType.OrganizationVault(
|
||||||
|
organizationId = "organiationId",
|
||||||
|
organizationName = "Test Organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
viewModel.stateFlow.value,
|
viewModel.stateFlow.value,
|
||||||
)
|
)
|
||||||
|
@ -286,6 +306,46 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on VaultFilterTypeSelect should update the selected filter type`() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
// Update to state with filters
|
||||||
|
val initialState = DEFAULT_STATE.copy(
|
||||||
|
appBarTitle = R.string.vaults.asText(),
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA,
|
||||||
|
)
|
||||||
|
mutableUserStateFlow.value = DEFAULT_USER_STATE
|
||||||
|
.copy(
|
||||||
|
accounts = listOf(
|
||||||
|
DEFAULT_USER_STATE.accounts[0].copy(
|
||||||
|
organizations = listOf(
|
||||||
|
Organization(
|
||||||
|
id = "testOrganizationId",
|
||||||
|
name = "Test Organization",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DEFAULT_USER_STATE.accounts[1],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
initialState,
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModel.trySendAction(VaultAction.VaultFilterTypeSelect(VaultFilterType.MyVault))
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
initialState.copy(
|
||||||
|
vaultFilterData = VAULT_FILTER_DATA.copy(
|
||||||
|
selectedVaultFilterType = VaultFilterType.MyVault,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
viewModel.stateFlow.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
|
fun `vaultDataStateFlow Loaded with items should update state to Content`() = runTest {
|
||||||
mutableVaultDataStateFlow.tryEmit(
|
mutableVaultDataStateFlow.tryEmit(
|
||||||
|
@ -760,6 +820,20 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val ORGANIZATION_VAULT_FILTER = VaultFilterType.OrganizationVault(
|
||||||
|
organizationId = "testOrganizationId",
|
||||||
|
organizationName = "Test Organization",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val VAULT_FILTER_DATA = VaultFilterData(
|
||||||
|
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
vaultFilterTypes = listOf(
|
||||||
|
VaultFilterType.AllVaults,
|
||||||
|
VaultFilterType.MyVault,
|
||||||
|
ORGANIZATION_VAULT_FILTER,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
private val DEFAULT_STATE: VaultState =
|
private val DEFAULT_STATE: VaultState =
|
||||||
createMockVaultState(viewState = VaultState.ViewState.Loading)
|
createMockVaultState(viewState = VaultState.ViewState.Loading)
|
||||||
|
|
||||||
|
@ -794,6 +868,7 @@ private fun createMockVaultState(
|
||||||
dialog: VaultState.DialogState? = null,
|
dialog: VaultState.DialogState? = null,
|
||||||
): VaultState =
|
): VaultState =
|
||||||
VaultState(
|
VaultState(
|
||||||
|
appBarTitle = R.string.my_vault.asText(),
|
||||||
avatarColorString = "#aa00aa",
|
avatarColorString = "#aa00aa",
|
||||||
initials = "AU",
|
initials = "AU",
|
||||||
accountSummaries = listOf(
|
accountSummaries = listOf(
|
||||||
|
|
|
@ -5,7 +5,10 @@ import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
class UserStateExtensionsTest {
|
class UserStateExtensionsTest {
|
||||||
|
@ -197,4 +200,63 @@ class UserStateExtensionsTest {
|
||||||
.toActiveAccountSummary(),
|
.toActiveAccountSummary(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toVaultFilterData for an account with no organizations should return a null value`() {
|
||||||
|
assertNull(
|
||||||
|
UserState.Account(
|
||||||
|
userId = "activeUserId",
|
||||||
|
name = "name",
|
||||||
|
email = "email",
|
||||||
|
avatarColorHex = "avatarColorHex",
|
||||||
|
environment = Environment.Us,
|
||||||
|
isPremium = true,
|
||||||
|
isVaultUnlocked = true,
|
||||||
|
organizations = emptyList(),
|
||||||
|
)
|
||||||
|
.toVaultFilterData(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `toVaultFilterData for an account with organizations should return data with the available types in the correct order`() {
|
||||||
|
assertEquals(
|
||||||
|
VaultFilterData(
|
||||||
|
selectedVaultFilterType = VaultFilterType.AllVaults,
|
||||||
|
vaultFilterTypes = listOf(
|
||||||
|
VaultFilterType.AllVaults,
|
||||||
|
VaultFilterType.MyVault,
|
||||||
|
VaultFilterType.OrganizationVault(
|
||||||
|
organizationId = "organizationId-A",
|
||||||
|
organizationName = "Organization A",
|
||||||
|
),
|
||||||
|
VaultFilterType.OrganizationVault(
|
||||||
|
organizationId = "organizationId-B",
|
||||||
|
organizationName = "Organization B",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
UserState.Account(
|
||||||
|
userId = "activeUserId",
|
||||||
|
name = "name",
|
||||||
|
email = "email",
|
||||||
|
avatarColorHex = "avatarColorHex",
|
||||||
|
environment = Environment.Us,
|
||||||
|
isPremium = true,
|
||||||
|
isVaultUnlocked = true,
|
||||||
|
organizations = listOf(
|
||||||
|
Organization(
|
||||||
|
id = "organizationId-B",
|
||||||
|
name = "Organization B",
|
||||||
|
),
|
||||||
|
Organization(
|
||||||
|
id = "organizationId-A",
|
||||||
|
name = "Organization A",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toVaultFilterData(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class VaultFilterDataExtensionsTest {
|
||||||
|
@Test
|
||||||
|
fun `toAppBarTitle for a null value should return My Vault`() {
|
||||||
|
assertEquals(
|
||||||
|
R.string.my_vault.asText(),
|
||||||
|
(null as VaultFilterData?).toAppBarTitle(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `toAppBarTitle for a non-null value should return Vaults`() {
|
||||||
|
assertEquals(
|
||||||
|
R.string.vaults.asText(),
|
||||||
|
VaultFilterData(
|
||||||
|
selectedVaultFilterType = VaultFilterType.MyVault,
|
||||||
|
vaultFilterTypes = listOf(
|
||||||
|
VaultFilterType.AllVaults,
|
||||||
|
VaultFilterType.MyVault,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toAppBarTitle(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue