mirror of
https://github.com/bitwarden/android.git
synced 2026-10-23 19:43:26 -05:00
PM-18058 and PM-18059 Choose which type of vault item to add from Vault screen and inside a Folder. (#4703)
This commit is contained in:
1 parent
a68b370cba
commit
00f30c922d
29 files changed
+729
-88
No files matched your search
+16
-3
@@ -13,12 +13,14 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.folders.model.FolderAddE
|
||||
private const val ADD_TYPE: String = "add"
|
||||
private const val EDIT_TYPE: String = "edit"
|
||||
private const val EDIT_ITEM_ID: String = "folder_edit_id"
|
||||
private const val PARENT_FOLDER_NAME: String = "parent_folder_name"
|
||||
|
||||
private const val ADD_EDIT_ITEM_PREFIX: String = "folder_add_edit_item"
|
||||
private const val ADD_EDIT_ITEM_TYPE: String = "folder_add_edit_type"
|
||||
|
||||
private const val ADD_EDIT_ITEM_ROUTE: String =
|
||||
"$ADD_EDIT_ITEM_PREFIX/{$ADD_EDIT_ITEM_TYPE}?$EDIT_ITEM_ID={$EDIT_ITEM_ID}"
|
||||
"$ADD_EDIT_ITEM_PREFIX/{$ADD_EDIT_ITEM_TYPE}" +
|
||||
"?$EDIT_ITEM_ID={$EDIT_ITEM_ID}&$PARENT_FOLDER_NAME={$PARENT_FOLDER_NAME}"
|
||||
|
||||
/**
|
||||
* Class to retrieve folder add & edit arguments from the [SavedStateHandle].
|
||||
@@ -26,6 +28,7 @@ private const val ADD_EDIT_ITEM_ROUTE: String =
|
||||
@OmitFromCoverage
|
||||
data class FolderAddEditArgs(
|
||||
val folderAddEditType: FolderAddEditType,
|
||||
val parentFolderName: String?,
|
||||
) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
folderAddEditType = when (requireNotNull(savedStateHandle[ADD_EDIT_ITEM_TYPE])) {
|
||||
@@ -33,13 +36,13 @@ data class FolderAddEditArgs(
|
||||
EDIT_TYPE -> FolderAddEditType.EditItem(requireNotNull(savedStateHandle[EDIT_ITEM_ID]))
|
||||
else -> throw IllegalStateException("Unknown FolderAddEditType.")
|
||||
},
|
||||
parentFolderName = savedStateHandle[PARENT_FOLDER_NAME],
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the folder add & edit screen to the nav graph.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun NavGraphBuilder.folderAddEditDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
) {
|
||||
@@ -47,6 +50,14 @@ fun NavGraphBuilder.folderAddEditDestination(
|
||||
route = ADD_EDIT_ITEM_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(ADD_EDIT_ITEM_TYPE) { type = NavType.StringType },
|
||||
navArgument(EDIT_ITEM_ID) {
|
||||
nullable = true
|
||||
type = NavType.StringType
|
||||
},
|
||||
navArgument(PARENT_FOLDER_NAME) {
|
||||
nullable = true
|
||||
type = NavType.StringType
|
||||
},
|
||||
),
|
||||
) {
|
||||
FolderAddEditScreen(onNavigateBack = onNavigateBack)
|
||||
@@ -58,11 +69,13 @@ fun NavGraphBuilder.folderAddEditDestination(
|
||||
*/
|
||||
fun NavController.navigateToFolderAddEdit(
|
||||
folderAddEditType: FolderAddEditType,
|
||||
parentFolderName: String? = null,
|
||||
navOptions: NavOptions? = null,
|
||||
) {
|
||||
navigate(
|
||||
route = "$ADD_EDIT_ITEM_PREFIX/${folderAddEditType.toTypeString()}" +
|
||||
"?$EDIT_ITEM_ID=${folderAddEditType.toIdOrNull()}",
|
||||
"?$EDIT_ITEM_ID=${folderAddEditType.toIdOrNull()}" +
|
||||
"&$PARENT_FOLDER_NAME=$parentFolderName",
|
||||
navOptions = navOptions,
|
||||
)
|
||||
}
|
||||
|
||||
+12
-4
@@ -39,14 +39,15 @@ class FolderAddEditViewModel @Inject constructor(
|
||||
// We load the state from the savedStateHandle for testing purposes.
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: run {
|
||||
val folderAddEditType = FolderAddEditArgs(savedStateHandle).folderAddEditType
|
||||
val folderAddEditArgs = FolderAddEditArgs(savedStateHandle)
|
||||
FolderAddEditState(
|
||||
folderAddEditType = folderAddEditType,
|
||||
viewState = when (folderAddEditType) {
|
||||
folderAddEditType = folderAddEditArgs.folderAddEditType,
|
||||
viewState = when (folderAddEditArgs.folderAddEditType) {
|
||||
is FolderAddEditType.AddItem -> FolderAddEditState.ViewState.Content("")
|
||||
is FolderAddEditType.EditItem -> FolderAddEditState.ViewState.Loading
|
||||
},
|
||||
dialog = null,
|
||||
parentFolderName = folderAddEditArgs.parentFolderName?.takeUnless { it.isEmpty() },
|
||||
)
|
||||
},
|
||||
) {
|
||||
@@ -111,7 +112,13 @@ class FolderAddEditViewModel @Inject constructor(
|
||||
FolderAddEditType.AddItem -> {
|
||||
val result = vaultRepository.createFolder(
|
||||
FolderView(
|
||||
name = content.folderName,
|
||||
name = state
|
||||
.parentFolderName
|
||||
?.let {
|
||||
"$it/"
|
||||
}
|
||||
.orEmpty() +
|
||||
content.folderName,
|
||||
id = folderAddEditType.folderId,
|
||||
revisionDate = DateTime.now(),
|
||||
),
|
||||
@@ -327,6 +334,7 @@ data class FolderAddEditState(
|
||||
val folderAddEditType: FolderAddEditType,
|
||||
val viewState: ViewState,
|
||||
val dialog: DialogState?,
|
||||
val parentFolderName: String?,
|
||||
) : Parcelable {
|
||||
|
||||
/**
|
||||
|
||||
+12
@@ -89,6 +89,12 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||
onNavigateToVaultEditItemScreen = {
|
||||
navController.navigateToVaultAddEdit(VaultAddEditType.EditItem(it))
|
||||
},
|
||||
onNavigateToAddFolderScreen = {
|
||||
navController.navigateToFolderAddEdit(
|
||||
folderAddEditType = FolderAddEditType.AddItem,
|
||||
parentFolderName = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
vaultUnlockedNavBarDestination(
|
||||
onNavigateToExportVault = { navController.navigateToExportVault() },
|
||||
@@ -120,6 +126,12 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||
onNavigateToImportLogins = {
|
||||
navController.navigateToImportLoginsScreen(snackbarRelay = it)
|
||||
},
|
||||
onNavigateToAddFolderScreen = {
|
||||
navController.navigateToFolderAddEdit(
|
||||
folderAddEditType = FolderAddEditType.AddItem,
|
||||
parentFolderName = it,
|
||||
)
|
||||
},
|
||||
)
|
||||
deleteAccountDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
||||
+2
@@ -40,6 +40,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderName: String?) -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions(
|
||||
route = VAULT_UNLOCKED_NAV_BAR_ROUTE,
|
||||
@@ -60,6 +61,7 @@ fun NavGraphBuilder.vaultUnlockedNavBarDestination(
|
||||
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
|
||||
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
+4
@@ -78,6 +78,7 @@ fun VaultUnlockedNavBarScreen(
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -144,6 +145,7 @@ fun VaultUnlockedNavBarScreen(
|
||||
onNavigateToSetupUnlockScreen = onNavigateToSetupUnlockScreen,
|
||||
onNavigateToSetupAutoFillScreen = onNavigateToSetupAutoFillScreen,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -174,6 +176,7 @@ private fun VaultUnlockedNavBarScaffold(
|
||||
onNavigateToSetupUnlockScreen: () -> Unit,
|
||||
onNavigateToSetupAutoFillScreen: () -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
) {
|
||||
var shouldDimNavBar by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
@@ -233,6 +236,7 @@ private fun VaultUnlockedNavBarScaffold(
|
||||
shouldDimNavBar = shouldDim
|
||||
},
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
|
||||
)
|
||||
sendGraph(
|
||||
navController = navController,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.x8bit.bitwarden.ui.vault.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenBasicDialogRow
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Reusable dialog for making selections between supported [VaultItemCipherType]s
|
||||
*
|
||||
* @param onOptionSelected Lambda to be called when an item is selected.
|
||||
* @param onDismissRequest Lambda to call when the dialog is dismissed by user. Is automatically
|
||||
* called when a selection is made.
|
||||
* @param excludedOptions List of [VaultItemCipherType] to exclude from the presented list.
|
||||
*/
|
||||
@Composable
|
||||
fun VaultItemSelectionDialog(
|
||||
onOptionSelected: (option: CreateVaultItemType) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
// TODO: PM-TBD possibly remove SSH_KEY as default exclusion once added SSH_KEY is enabled.
|
||||
excludedOptions: ImmutableList<CreateVaultItemType> = persistentListOf(
|
||||
CreateVaultItemType.SSH_KEY,
|
||||
),
|
||||
) {
|
||||
val supportedEntries = CreateVaultItemType
|
||||
.entries
|
||||
.filterNot { excludedOptions.contains(it) }
|
||||
BitwardenSelectionDialog(
|
||||
title = stringResource(R.string.type),
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
supportedEntries.forEach {
|
||||
BitwardenBasicDialogRow(
|
||||
text = stringResource(it.selectionText),
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onOptionSelected(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.x8bit.bitwarden.ui.vault.components.model
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.x8bit.bitwarden.R
|
||||
|
||||
/**
|
||||
* Enumerated values to represent a create vault item option.
|
||||
*/
|
||||
enum class CreateVaultItemType(
|
||||
@StringRes val selectionText: Int,
|
||||
) {
|
||||
/**
|
||||
* A login cipher.
|
||||
*/
|
||||
LOGIN(R.string.log_in_noun),
|
||||
|
||||
/**
|
||||
* A card cipher.
|
||||
*/
|
||||
CARD(R.string.type_card),
|
||||
|
||||
/**
|
||||
* A identity cipher.
|
||||
*/
|
||||
IDENTITY(R.string.type_identity),
|
||||
|
||||
/**
|
||||
* A secure note cipher.
|
||||
*/
|
||||
SECURE_NOTE(R.string.type_secure_note),
|
||||
|
||||
/**
|
||||
* A SSH key cipher.
|
||||
*/
|
||||
SSH_KEY(R.string.type_ssh_key),
|
||||
|
||||
/**
|
||||
* A cipher item folder
|
||||
*/
|
||||
FOLDER(R.string.folder),
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.x8bit.bitwarden.ui.vault.components.util
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
|
||||
/**
|
||||
* Extension function to map [CreateVaultItemType] to a matching [VaultItemCipherType] or
|
||||
* `null` if there is no matching type.
|
||||
*/
|
||||
fun CreateVaultItemType.toVaultItemCipherTypeOrNull(): VaultItemCipherType? = when (this) {
|
||||
CreateVaultItemType.LOGIN -> VaultItemCipherType.LOGIN
|
||||
CreateVaultItemType.CARD -> VaultItemCipherType.CARD
|
||||
CreateVaultItemType.IDENTITY -> VaultItemCipherType.IDENTITY
|
||||
CreateVaultItemType.SECURE_NOTE -> VaultItemCipherType.SECURE_NOTE
|
||||
CreateVaultItemType.SSH_KEY -> VaultItemCipherType.SSH_KEY
|
||||
CreateVaultItemType.FOLDER -> null
|
||||
}
|
||||
+8
@@ -69,6 +69,7 @@ fun NavGraphBuilder.vaultItemListingDestination(
|
||||
selectedFolderId: String?,
|
||||
selectedCollectionId: String?,
|
||||
) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
) {
|
||||
internalVaultItemListingDestination(
|
||||
@@ -81,12 +82,14 @@ fun NavGraphBuilder.vaultItemListingDestination(
|
||||
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
|
||||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
|
||||
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the [VaultItemListingScreen] to the nav graph.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun NavGraphBuilder.vaultItemListingDestinationAsRoot(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVaultItemScreen: (id: String) -> Unit,
|
||||
@@ -96,6 +99,7 @@ fun NavGraphBuilder.vaultItemListingDestinationAsRoot(
|
||||
selectedFolderId: String?,
|
||||
selectedCollectionId: String?,
|
||||
) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
) {
|
||||
composableWithStayTransitions(
|
||||
@@ -115,6 +119,7 @@ fun NavGraphBuilder.vaultItemListingDestinationAsRoot(
|
||||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
|
||||
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
|
||||
onNavigateToAddFolder = onNavigateToAddFolderScreen,
|
||||
onNavigateToVaultItemListing = {},
|
||||
onNavigateToAddSendItem = {},
|
||||
onNavigateToEditSendItem = {},
|
||||
@@ -137,6 +142,7 @@ fun NavGraphBuilder.sendItemListingDestination(
|
||||
onNavigateToAddSendItem = onNavigateToAddSendItem,
|
||||
onNavigateToEditSendItem = onNavigateToEditSendItem,
|
||||
onNavigateToVaultAddItemScreen = { _, _, _ -> },
|
||||
onNavigateToAddFolderScreen = { _ -> },
|
||||
onNavigateToVaultItemScreen = { },
|
||||
onNavigateToVaultEditItemScreen = { },
|
||||
onNavigateToVaultItemListing = { },
|
||||
@@ -159,6 +165,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination(
|
||||
selectedFolderId: String?,
|
||||
selectedCollectionId: String?,
|
||||
) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
onNavigateToAddSendItem: () -> Unit,
|
||||
onNavigateToEditSendItem: (sendId: String) -> Unit,
|
||||
onNavigateToSearch: (searchType: SearchType) -> Unit,
|
||||
@@ -190,6 +197,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination(
|
||||
onNavigateToEditSendItem = onNavigateToEditSendItem,
|
||||
onNavigateToVaultItemListing = onNavigateToVaultItemListing,
|
||||
onNavigateToSearch = onNavigateToSearch,
|
||||
onNavigateToAddFolder = onNavigateToAddFolderScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -51,6 +51,8 @@ import com.x8bit.bitwarden.ui.platform.feature.settings.accountsecurity.PinInput
|
||||
import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.components.VaultItemSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingUserVerificationHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
@@ -74,6 +76,7 @@ fun VaultItemListingScreen(
|
||||
selectedFolderId: String?,
|
||||
selectedCollectionId: String?,
|
||||
) -> Unit,
|
||||
onNavigateToAddFolder: (selectedFolderId: String?) -> Unit,
|
||||
onNavigateToAddSendItem: () -> Unit,
|
||||
onNavigateToEditSendItem: (sendId: String) -> Unit,
|
||||
onNavigateToSearch: (searchType: SearchType) -> Unit,
|
||||
@@ -179,6 +182,10 @@ fun VaultItemListingScreen(
|
||||
}
|
||||
|
||||
VaultItemListingEvent.ExitApp -> exitManager.exitApplication()
|
||||
|
||||
is VaultItemListingEvent.NavigateToAddFolder -> {
|
||||
onNavigateToAddFolder(event.parentFolderName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +268,15 @@ fun VaultItemListingScreen(
|
||||
)
|
||||
}
|
||||
},
|
||||
onVaultItemTypeSelected = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemToAddToFolderSelected(
|
||||
itemType = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val vaultItemListingHandlers = remember(viewModel) {
|
||||
@@ -289,6 +305,7 @@ private fun VaultItemListingDialogs(
|
||||
onSubmitPinSetUpFido2Verification: (pin: String, cipherId: String) -> Unit,
|
||||
onRetryPinSetUpFido2Verification: (cipherId: String) -> Unit,
|
||||
onDismissFido2Verification: () -> Unit,
|
||||
onVaultItemTypeSelected: (CreateVaultItemType) -> Unit,
|
||||
) {
|
||||
when (dialogState) {
|
||||
is VaultItemListingState.DialogState.Error -> BitwardenBasicDialog(
|
||||
@@ -378,6 +395,13 @@ private fun VaultItemListingDialogs(
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemListingState.DialogState.VaultItemTypeSelection -> {
|
||||
VaultItemSelectionDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onOptionSelected = onVaultItemTypeSelected,
|
||||
)
|
||||
}
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
+79
-12
@@ -63,6 +63,8 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.util.filterAndOrganize
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.components.util.toVaultItemCipherTypeOrNull
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.determineListingPredicate
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.util.toItemListingType
|
||||
@@ -269,6 +271,9 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
is VaultItemListingsAction.Internal -> handleInternalAction(action)
|
||||
is VaultItemListingsAction.ItemToAddToFolderSelected -> {
|
||||
handleItemToAddToFolderSelected(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,33 +534,74 @@ class VaultItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleItemToAddToFolderSelected(
|
||||
action: VaultItemListingsAction.ItemToAddToFolderSelected,
|
||||
) {
|
||||
(state.itemListingType as? VaultItemListingState.ItemListingType.Vault.Folder)
|
||||
?.let { folder ->
|
||||
when (val vaultItemType = action.itemType) {
|
||||
CreateVaultItemType.LOGIN,
|
||||
CreateVaultItemType.CARD,
|
||||
CreateVaultItemType.IDENTITY,
|
||||
CreateVaultItemType.SECURE_NOTE,
|
||||
CreateVaultItemType.SSH_KEY,
|
||||
-> {
|
||||
vaultItemType
|
||||
.toVaultItemCipherTypeOrNull()
|
||||
?.let {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = it,
|
||||
selectedFolderId = folder.folderId,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CreateVaultItemType.FOLDER -> {
|
||||
sendEvent(
|
||||
VaultItemListingEvent.NavigateToAddFolder(
|
||||
parentFolderName = folder.fullyQualifiedName,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddVaultItemClick() {
|
||||
val event = when (val itemListingType = state.itemListingType) {
|
||||
when (val itemListingType = state.itemListingType) {
|
||||
is VaultItemListingState.ItemListingType.Vault.Folder -> {
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = itemListingType.toVaultItemCipherType(),
|
||||
selectedFolderId = itemListingType.folderId,
|
||||
)
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is VaultItemListingState.ItemListingType.Vault.Collection -> {
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = itemListingType.toVaultItemCipherType(),
|
||||
selectedCollectionId = itemListingType.collectionId,
|
||||
sendEvent(
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = itemListingType.toVaultItemCipherType(),
|
||||
selectedCollectionId = itemListingType.collectionId,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemListingState.ItemListingType.Vault -> {
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = itemListingType.toVaultItemCipherType(),
|
||||
sendEvent(
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = itemListingType.toVaultItemCipherType(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemListingState.ItemListingType.Send -> {
|
||||
VaultItemListingEvent.NavigateToAddSendItem
|
||||
sendEvent(
|
||||
VaultItemListingEvent.NavigateToAddSendItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
sendEvent(event)
|
||||
}
|
||||
|
||||
private fun handleEditSendClick(action: ListingItemOverflowAction.SendAction.EditClick) {
|
||||
@@ -1925,6 +1971,12 @@ data class VaultItemListingState(
|
||||
val message: Text,
|
||||
val selectedCipherId: String,
|
||||
) : DialogState()
|
||||
|
||||
/**
|
||||
* Represents a selection dialog to choose a vault item type to add to folder.
|
||||
*/
|
||||
@Parcelize
|
||||
data object VaultItemTypeSelection : DialogState()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2129,6 +2181,7 @@ data class VaultItemListingState(
|
||||
val folderId: String?,
|
||||
// The folderName will always initially be an empty string
|
||||
val folderName: String = "",
|
||||
val fullyQualifiedName: String = "",
|
||||
) : Vault() {
|
||||
override val titleText: Text
|
||||
get() = folderId
|
||||
@@ -2199,6 +2252,13 @@ sealed class VaultItemListingEvent {
|
||||
val selectedCollectionId: String? = null,
|
||||
) : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to add the folder item.
|
||||
*/
|
||||
data class NavigateToAddFolder(
|
||||
val parentFolderName: String,
|
||||
) : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the collection.
|
||||
*/
|
||||
@@ -2494,6 +2554,13 @@ sealed class VaultItemListingsAction {
|
||||
val cipherViewId: String,
|
||||
) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Indicated a selection was made to add a new item to the vault.
|
||||
*/
|
||||
data class ItemToAddToFolderSelected(
|
||||
val itemType: CreateVaultItemType,
|
||||
) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [VaultItemListingViewModel] itself might send.
|
||||
*/
|
||||
|
||||
+9
-6
@@ -214,7 +214,6 @@ fun VaultData.toViewState(
|
||||
.asText()
|
||||
}
|
||||
val shouldShowAddButton = when (itemListingType) {
|
||||
is VaultItemListingState.ItemListingType.Vault.Folder,
|
||||
VaultItemListingState.ItemListingType.Vault.Trash,
|
||||
VaultItemListingState.ItemListingType.Vault.SshKey,
|
||||
-> false
|
||||
@@ -306,13 +305,17 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
|
||||
.orEmpty(),
|
||||
)
|
||||
|
||||
is VaultItemListingState.ItemListingType.Vault.Folder -> copy(
|
||||
folderName = folderList
|
||||
is VaultItemListingState.ItemListingType.Vault.Folder -> {
|
||||
val fullyQualifiedName = folderList
|
||||
.find { it.id == folderId }
|
||||
?.name
|
||||
?.toFolderDisplayName(folderList)
|
||||
.orEmpty(),
|
||||
)
|
||||
copy(
|
||||
folderName = fullyQualifiedName
|
||||
?.toFolderDisplayName()
|
||||
.orEmpty(),
|
||||
fullyQualifiedName = fullyQualifiedName.orEmpty(),
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemListingState.ItemListingType.Vault.Identity -> this
|
||||
is VaultItemListingState.ItemListingType.Vault.Login -> this
|
||||
|
||||
+1
-15
@@ -68,18 +68,4 @@ fun List<FolderView>.getFilteredFolders(folderName: String? = null): List<Folder
|
||||
* folder name we receive is often nested, and we want to extract just the relevant name for
|
||||
* display to the user.
|
||||
*/
|
||||
fun String.toFolderDisplayName(list: List<FolderView>): String {
|
||||
var folderName = this
|
||||
|
||||
// cycle through the list and determine the correct display name of the folder.
|
||||
list.forEach { folderView ->
|
||||
if (this.startsWith(folderView.name + FOLDER_DIVIDER)) {
|
||||
val newName = this.substringAfter(folderView.name + FOLDER_DIVIDER)
|
||||
if (newName.isNotBlank() && newName.length < folderName.length) {
|
||||
folderName = newName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return folderName
|
||||
}
|
||||
fun String.toFolderDisplayName(): String = this.split(FOLDER_DIVIDER).last()
|
||||
+4
-1
@@ -30,6 +30,7 @@ fun NavGraphBuilder.vaultGraph(
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
) {
|
||||
navigation(
|
||||
route = VAULT_GRAPH_ROUTE,
|
||||
@@ -37,7 +38,7 @@ fun NavGraphBuilder.vaultGraph(
|
||||
) {
|
||||
vaultDestination(
|
||||
onNavigateToVaultAddItemScreen = {
|
||||
onNavigateToVaultAddItemScreen(VaultItemCipherType.LOGIN, null, null)
|
||||
onNavigateToVaultAddItemScreen(it, null, null)
|
||||
},
|
||||
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
|
||||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
@@ -48,6 +49,7 @@ fun NavGraphBuilder.vaultGraph(
|
||||
onNavigateToSearchVault = onNavigateToSearchVault,
|
||||
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
|
||||
)
|
||||
vaultItemListingDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
@@ -58,6 +60,7 @@ fun NavGraphBuilder.vaultGraph(
|
||||
onNavigateToVaultItemListing = {
|
||||
navController.navigateToVaultItemListing(it)
|
||||
},
|
||||
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
|
||||
)
|
||||
|
||||
vaultVerificationCodeDestination(
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.navigation.NavOptions
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithRootPushTransitions
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
|
||||
const val VAULT_ROUTE: String = "vault"
|
||||
@@ -15,7 +16,7 @@ const val VAULT_ROUTE: String = "vault"
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun NavGraphBuilder.vaultDestination(
|
||||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToVaultAddItemScreen: (VaultItemCipherType) -> Unit,
|
||||
onNavigateToVerificationCodeScreen: () -> Unit,
|
||||
onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
|
||||
@@ -23,6 +24,7 @@ fun NavGraphBuilder.vaultDestination(
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
) {
|
||||
composableWithRootPushTransitions(
|
||||
route = VAULT_ROUTE,
|
||||
@@ -36,6 +38,7 @@ fun NavGraphBuilder.vaultDestination(
|
||||
onNavigateToSearchVault = onNavigateToSearchVault,
|
||||
onDimBottomNavBarRequest = onDimBottomNavBarRequest,
|
||||
onNavigateToImportLogins = onNavigateToImportLogins,
|
||||
onNavigateToAddFolderScreen = onNavigateToAddFolderScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,8 +63,11 @@ import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.review.AppReviewManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.vault.components.VaultItemSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -80,7 +83,9 @@ private const val APP_REVIEW_DELAY = 3000L
|
||||
@Composable
|
||||
fun VaultScreen(
|
||||
viewModel: VaultViewModel = hiltViewModel(),
|
||||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToVaultAddItemScreen: (
|
||||
vaultItemCipherType: VaultItemCipherType,
|
||||
) -> Unit,
|
||||
onNavigateToVaultItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (vaultItemId: String) -> Unit,
|
||||
onNavigateToVerificationCodeScreen: () -> Unit,
|
||||
@@ -88,6 +93,7 @@ fun VaultScreen(
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
onDimBottomNavBarRequest: (shouldDim: Boolean) -> Unit,
|
||||
onNavigateToImportLogins: (SnackbarRelay) -> Unit,
|
||||
onNavigateToAddFolderScreen: (selectedFolderId: String?) -> Unit,
|
||||
exitManager: ExitManager = LocalExitManager.current,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
appReviewManager: AppReviewManager = LocalAppReviewManager.current,
|
||||
@@ -122,7 +128,9 @@ fun VaultScreen(
|
||||
}
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
VaultEvent.NavigateToAddItemScreen -> onNavigateToVaultAddItemScreen()
|
||||
is VaultEvent.NavigateToAddItemScreen -> {
|
||||
onNavigateToVaultAddItemScreen(event.type)
|
||||
}
|
||||
|
||||
VaultEvent.NavigateToVaultSearchScreen -> onNavigateToSearchVault(SearchType.Vault.All)
|
||||
|
||||
@@ -155,6 +163,10 @@ fun VaultScreen(
|
||||
VaultEvent.PromptForAppReview -> {
|
||||
launchPrompt.invoke()
|
||||
}
|
||||
|
||||
VaultEvent.NavigateToAddFolder -> {
|
||||
onNavigateToAddFolderScreen(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
val vaultHandlers = remember(viewModel) { VaultHandlers.create(viewModel) }
|
||||
@@ -297,7 +309,7 @@ private fun VaultScreenScaffold(
|
||||
exit = scaleOut(),
|
||||
) {
|
||||
BitwardenFloatingActionButton(
|
||||
onClick = vaultHandlers.addItemClickAction,
|
||||
onClick = vaultHandlers.selectAddItemTypeClickAction,
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_plus_large),
|
||||
contentDescription = stringResource(id = R.string.add_item),
|
||||
modifier = Modifier.testTag(tag = "AddItemButton"),
|
||||
@@ -364,7 +376,9 @@ private fun VaultScreenScaffold(
|
||||
),
|
||||
buttonText = stringResource(R.string.new_login),
|
||||
policyDisablesSend = false,
|
||||
addItemClickAction = vaultHandlers.addItemClickAction,
|
||||
addItemClickAction = {
|
||||
vaultHandlers.addItemClickAction(CreateVaultItemType.LOGIN)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -395,6 +409,13 @@ private fun VaultDialogs(
|
||||
onDismissRequest = vaultHandlers.dialogDismiss,
|
||||
)
|
||||
|
||||
VaultState.DialogState.SelectVaultAddItemType -> VaultItemSelectionDialog(
|
||||
onOptionSelected = {
|
||||
vaultHandlers.addItemClickAction(it)
|
||||
},
|
||||
onDismissRequest = vaultHandlers.dialogDismiss,
|
||||
)
|
||||
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,8 @@ import com.x8bit.bitwarden.ui.platform.components.model.IconRes
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.components.util.toVaultItemCipherTypeOrNull
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
@@ -48,6 +50,7 @@ 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.vaultFilterDataIfRequired
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultCardBrand
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import com.x8bit.bitwarden.ui.vault.util.shortName
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
@@ -65,7 +68,7 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Manages [VaultState], handles [VaultAction], and launches [VaultEvent] for the [VaultScreen].
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
@Suppress("TooManyFunctions", "LongParameterList", "LargeClass")
|
||||
@HiltViewModel
|
||||
class VaultViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository,
|
||||
@@ -170,7 +173,7 @@ class VaultViewModel @Inject constructor(
|
||||
|
||||
override fun handleAction(action: VaultAction) {
|
||||
when (action) {
|
||||
is VaultAction.AddItemClick -> handleAddItemClick()
|
||||
is VaultAction.AddItemClick -> handleAddItemClick(action)
|
||||
is VaultAction.CardGroupClick -> handleCardClick()
|
||||
is VaultAction.FolderClick -> handleFolderItemClick(action)
|
||||
is VaultAction.CollectionClick -> handleCollectionItemClick(action)
|
||||
@@ -203,6 +206,16 @@ class VaultViewModel @Inject constructor(
|
||||
VaultAction.DismissImportActionCard -> handleDismissImportActionCard()
|
||||
VaultAction.ImportActionCardClick -> handleImportActionCardClick()
|
||||
VaultAction.LifecycleResumed -> handleLifecycleResumed()
|
||||
VaultAction.SelectAddItemType -> handleSelectAddItemType()
|
||||
}
|
||||
}
|
||||
|
||||
//region VaultAction Handlers
|
||||
private fun handleSelectAddItemType() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultState.DialogState.SelectVaultAddItemType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,9 +266,31 @@ class VaultViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
//region VaultAction Handlers
|
||||
private fun handleAddItemClick() {
|
||||
sendEvent(VaultEvent.NavigateToAddItemScreen)
|
||||
private fun handleAddItemClick(action: VaultAction.AddItemClick) {
|
||||
when (val vaultItemType = action.type) {
|
||||
CreateVaultItemType.LOGIN,
|
||||
CreateVaultItemType.CARD,
|
||||
CreateVaultItemType.IDENTITY,
|
||||
CreateVaultItemType.SECURE_NOTE,
|
||||
CreateVaultItemType.SSH_KEY,
|
||||
-> {
|
||||
vaultItemType
|
||||
.toVaultItemCipherTypeOrNull()
|
||||
?.let {
|
||||
sendEvent(
|
||||
VaultEvent.NavigateToAddItemScreen(
|
||||
type = it,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CreateVaultItemType.FOLDER -> {
|
||||
sendEvent(
|
||||
VaultEvent.NavigateToAddFolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCardClick() {
|
||||
@@ -1074,6 +1109,12 @@ data class VaultState(
|
||||
@Parcelize
|
||||
data object Syncing : DialogState()
|
||||
|
||||
/**
|
||||
* Represents a dialog for selecting a vault item type to add.
|
||||
*/
|
||||
@Parcelize
|
||||
data object SelectVaultAddItemType : DialogState()
|
||||
|
||||
/**
|
||||
* Represents an error dialog with the given [title] and [message].
|
||||
*/
|
||||
@@ -1097,7 +1138,9 @@ sealed class VaultEvent {
|
||||
/**
|
||||
* Navigate to the Add Item screen.
|
||||
*/
|
||||
data object NavigateToAddItemScreen : VaultEvent()
|
||||
data class NavigateToAddItemScreen(
|
||||
val type: VaultItemCipherType,
|
||||
) : VaultEvent()
|
||||
|
||||
/**
|
||||
* Navigate to the item details screen.
|
||||
@@ -1156,6 +1199,11 @@ sealed class VaultEvent {
|
||||
* Show a snackbar with the given [data].
|
||||
*/
|
||||
data class ShowSnackbar(val data: BitwardenSnackbarData) : VaultEvent(), BackgroundEvent
|
||||
|
||||
/**
|
||||
* Navigate to the add folder screen
|
||||
*/
|
||||
data object NavigateToAddFolder : VaultEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1171,7 +1219,7 @@ sealed class VaultAction {
|
||||
* Click the add an item button.
|
||||
* This can either be the floating action button or actual add an item button.
|
||||
*/
|
||||
data object AddItemClick : VaultAction()
|
||||
data class AddItemClick(val type: CreateVaultItemType) : VaultAction()
|
||||
|
||||
/**
|
||||
* Click the search icon.
|
||||
@@ -1326,6 +1374,11 @@ sealed class VaultAction {
|
||||
*/
|
||||
data object LifecycleResumed : VaultAction()
|
||||
|
||||
/**
|
||||
* User has clicked button to bring up the add item selection dialog.
|
||||
*/
|
||||
data object SelectAddItemType : VaultAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [VaultViewModel] itself might send.
|
||||
*/
|
||||
|
||||
+7
-2
@@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.vault.handlers
|
||||
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
|
||||
@@ -12,7 +13,8 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
*/
|
||||
data class VaultHandlers(
|
||||
val vaultFilterTypeSelect: (VaultFilterType) -> Unit,
|
||||
val addItemClickAction: () -> Unit,
|
||||
val selectAddItemTypeClickAction: () -> Unit,
|
||||
val addItemClickAction: (CreateVaultItemType) -> Unit,
|
||||
val searchIconClickAction: () -> Unit,
|
||||
val accountLockClickAction: (AccountSummary) -> Unit,
|
||||
val accountLogoutClickAction: (AccountSummary) -> Unit,
|
||||
@@ -49,7 +51,10 @@ data class VaultHandlers(
|
||||
vaultFilterTypeSelect = {
|
||||
viewModel.trySendAction(VaultAction.VaultFilterTypeSelect(it))
|
||||
},
|
||||
addItemClickAction = { viewModel.trySendAction(VaultAction.AddItemClick) },
|
||||
selectAddItemTypeClickAction = {
|
||||
viewModel.trySendAction(VaultAction.SelectAddItemType)
|
||||
},
|
||||
addItemClickAction = { viewModel.trySendAction(VaultAction.AddItemClick(it)) },
|
||||
searchIconClickAction = { viewModel.trySendAction(VaultAction.SearchIconClick) },
|
||||
accountLockClickAction = {
|
||||
viewModel.trySendAction(VaultAction.LockAccountClick(it))
|
||||
|
||||
+2
@@ -246,9 +246,11 @@ private val DEFAULT_STATE_ADD = FolderAddEditState(
|
||||
folderAddEditType = FolderAddEditType.AddItem,
|
||||
viewState = FolderAddEditState.ViewState.Loading,
|
||||
dialog = null,
|
||||
parentFolderName = null,
|
||||
)
|
||||
private val DEFAULT_STATE_EDIT = FolderAddEditState(
|
||||
folderAddEditType = FolderAddEditType.EditItem("1"),
|
||||
viewState = FolderAddEditState.ViewState.Loading,
|
||||
dialog = null,
|
||||
parentFolderName = null,
|
||||
)
|
||||
+90
@@ -18,12 +18,16 @@ import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
|
||||
private val mutableFoldersStateFlow =
|
||||
@@ -132,6 +136,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val stateWithoutDialog = stateWithDialog.copy(
|
||||
@@ -175,6 +180,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Error(
|
||||
R.string.generic_error_message.asText(),
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val viewModel = createViewModel(
|
||||
@@ -201,6 +207,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val stateWithoutDialog = stateWithDialog.copy(
|
||||
@@ -243,6 +250,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = "",
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val stateWithDialog = stateWithoutName.copy(
|
||||
@@ -277,6 +285,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val stateWithoutDialog = stateWithDialog.copy(
|
||||
@@ -301,6 +310,83 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode, SaveClick createFolder with no parentFolderNamePresent should just create folder with entered name`() =
|
||||
runTest {
|
||||
mockkStatic(DateTime::class)
|
||||
every { DateTime.now() } returns Instant.MIN
|
||||
val viewModel =
|
||||
createViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
FolderAddEditState(
|
||||
folderAddEditType = FolderAddEditType.AddItem,
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
dialog = null,
|
||||
parentFolderName = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultRepository.createFolder(any())
|
||||
} returns CreateFolderResult.Success(mockk())
|
||||
viewModel.trySendAction(FolderAddEditAction.SaveClick)
|
||||
coVerify(
|
||||
exactly = 1,
|
||||
) {
|
||||
vaultRepository.createFolder(
|
||||
folderView = FolderView(
|
||||
name = DEFAULT_FOLDER_NAME,
|
||||
id = null,
|
||||
revisionDate = Instant.MIN,
|
||||
),
|
||||
)
|
||||
}
|
||||
unmockkStatic(DateTime::class)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `in add mode, SaveClick createFolder with a parentFolderNamePresent should prepend the parent folder to the entered name`() =
|
||||
runTest {
|
||||
mockkStatic(DateTime::class)
|
||||
every { DateTime.now() } returns Instant.MIN
|
||||
val parentFolderName = "parent/folder"
|
||||
val viewModel =
|
||||
createViewModel(
|
||||
createSavedStateHandleWithState(
|
||||
FolderAddEditState(
|
||||
folderAddEditType = FolderAddEditType.AddItem,
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
dialog = null,
|
||||
parentFolderName = parentFolderName,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
coEvery {
|
||||
vaultRepository.createFolder(any())
|
||||
} returns CreateFolderResult.Success(mockk())
|
||||
viewModel.trySendAction(FolderAddEditAction.SaveClick)
|
||||
coVerify(
|
||||
exactly = 1,
|
||||
) {
|
||||
vaultRepository.createFolder(
|
||||
folderView = FolderView(
|
||||
name = "$parentFolderName/$DEFAULT_FOLDER_NAME",
|
||||
id = null,
|
||||
revisionDate = Instant.MIN,
|
||||
),
|
||||
)
|
||||
}
|
||||
unmockkStatic(DateTime::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `in add mode, SaveClick createFolder error should show an error dialog`() = runTest {
|
||||
val state = FolderAddEditState(
|
||||
@@ -309,6 +395,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val viewModel = createViewModel(
|
||||
@@ -344,6 +431,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val stateWithoutDialog = stateWithDialog.copy(
|
||||
@@ -389,6 +477,7 @@ class FolderAddEditViewModelTest : BaseViewModelTest() {
|
||||
viewState = FolderAddEditState.ViewState.Content(
|
||||
folderName = DEFAULT_FOLDER_NAME,
|
||||
),
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
val viewModel = createViewModel(
|
||||
@@ -664,6 +753,7 @@ private val DEFAULT_STATE = FolderAddEditState(
|
||||
viewState = FolderAddEditState.ViewState.Loading,
|
||||
dialog = FolderAddEditState.DialogState.Loading("Loading".asText()),
|
||||
folderAddEditType = FolderAddEditType.AddItem,
|
||||
parentFolderName = null,
|
||||
)
|
||||
|
||||
private const val DEFAULT_EDIT_ITEM_ID = "edit_id"
|
||||
|
||||
+1
@@ -56,6 +56,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() {
|
||||
onNavigateToSetupAutoFillScreen = {},
|
||||
onNavigateToSetupUnlockScreen = {},
|
||||
onNavigateToImportLogins = {},
|
||||
onNavigateToAddFolderScreen = { _ -> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.x8bit.bitwarden.ui.vault.components.util
|
||||
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CreateVaultItemTypeExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `Add CreateVaultItemType values should map to correct VaultItemCipherType or null`() {
|
||||
CreateVaultItemType.entries.forEach { createVaultItemType ->
|
||||
val actualResult = createVaultItemType.toVaultItemCipherTypeOrNull()
|
||||
when (createVaultItemType) {
|
||||
CreateVaultItemType.LOGIN -> assertEquals(
|
||||
VaultItemCipherType.LOGIN,
|
||||
actualResult,
|
||||
)
|
||||
|
||||
CreateVaultItemType.CARD -> assertEquals(
|
||||
VaultItemCipherType.CARD,
|
||||
actualResult,
|
||||
)
|
||||
|
||||
CreateVaultItemType.IDENTITY -> assertEquals(
|
||||
VaultItemCipherType.IDENTITY,
|
||||
actualResult,
|
||||
)
|
||||
|
||||
CreateVaultItemType.SECURE_NOTE -> assertEquals(
|
||||
VaultItemCipherType.SECURE_NOTE,
|
||||
actualResult,
|
||||
)
|
||||
|
||||
CreateVaultItemType.SSH_KEY -> assertEquals(
|
||||
VaultItemCipherType.SSH_KEY,
|
||||
actualResult,
|
||||
)
|
||||
|
||||
CreateVaultItemType.FOLDER -> assertNull(actualResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4471,7 +4471,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
VaultItemCipherType.IDENTITY -> "identity"
|
||||
VaultItemCipherType.SECURE_NOTE -> "secure_note"
|
||||
VaultItemCipherType.SSH_KEY -> "ssh_key"
|
||||
null -> null
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+61
@@ -53,6 +53,7 @@ import com.x8bit.bitwarden.ui.util.performLockAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performLogoutAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performRemoveAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performYesDialogButtonClick
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
@@ -84,6 +85,8 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
private var onNavigateToVaultEditItemScreenId: String? = null
|
||||
private var onNavigateToSearchType: SearchType? = null
|
||||
private var onNavigateToVaultItemListingScreenType: VaultItemListingType? = null
|
||||
private var onNavigateToAddFolderCalled = false
|
||||
private var onNavigateToAddFolderParentFolderName: String? = null
|
||||
|
||||
private val exitManager: ExitManager = mockk {
|
||||
every { exitApplication() } just runs
|
||||
@@ -126,6 +129,10 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
onNavigateToSearch = { onNavigateToSearchType = it },
|
||||
onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemScreenId = it },
|
||||
onNavigateToVaultItemListing = { this.onNavigateToVaultItemListingScreenType = it },
|
||||
onNavigateToAddFolder = { folderName ->
|
||||
onNavigateToAddFolderCalled = true
|
||||
onNavigateToAddFolderParentFolderName = folderName
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2109,6 +2116,60 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToAddFolder event calls onNavigateToAddFolder callback with parent name`() {
|
||||
val parentFolder = "momNpop"
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultItemListingEvent.NavigateToAddFolder(parentFolderName = parentFolder),
|
||||
)
|
||||
assertTrue(onNavigateToAddFolderCalled)
|
||||
assertEquals(
|
||||
parentFolder,
|
||||
onNavigateToAddFolderParentFolderName,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `VaultItemTypeSelection dialog state show vault item type selection dialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Type")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when option is selected in VaultItemTypeSelection dialog add item action is sent`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Card")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultItemListingsAction.DismissDialogClick)
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemToAddToFolderSelected(
|
||||
CreateVaultItemType.CARD,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||
|
||||
+61
-16
@@ -66,6 +66,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.platform.feature.search.model.SearchType
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAssertionOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
@@ -966,24 +967,23 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `AddVaultItemClick inside a folder should emit NavigateToAddVaultItem with a selected folder id`() =
|
||||
runTest {
|
||||
val viewModel = createVaultItemListingViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType = VaultItemListingType.Folder(folderId = "id"),
|
||||
fun `AddVaultItemClick inside a folder should show item selection dialog state`() {
|
||||
val viewModel = createVaultItemListingViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType = VaultItemListingType.Folder(folderId = "id"),
|
||||
),
|
||||
)
|
||||
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
|
||||
assertEquals(
|
||||
createVaultItemListingState(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder(
|
||||
folderId = "id",
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
VaultItemCipherType.LOGIN,
|
||||
selectedFolderId = "id",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.copy(dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddVaultItemClick for vault item should emit NavigateToAddVaultItem`() = runTest {
|
||||
@@ -1008,6 +1008,51 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ItemToAddToFolderSelected sends NavigateToAddFolder for folder selection`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType = VaultItemListingType.Folder(""),
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemToAddToFolderSelected(
|
||||
itemType = CreateVaultItemType.FOLDER,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.NavigateToAddFolder(
|
||||
parentFolderName = "",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ItemToAddToFolderSelected sends NavigateToAddFolder for any other selection`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithVaultItemListingType(
|
||||
vaultItemListingType = VaultItemListingType.Folder("id"),
|
||||
),
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.ItemToAddToFolderSelected(
|
||||
itemType = CreateVaultItemType.CARD,
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
VaultItemListingEvent.NavigateToAddVaultItem(
|
||||
vaultItemCipherType = VaultItemCipherType.CARD,
|
||||
selectedFolderId = "id",
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FolderClick for vault item should emit NavigateToFolderItem`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
||||
+2
-1
@@ -709,7 +709,7 @@ class VaultItemListingDataExtensionsTest {
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.NoItems(
|
||||
message = R.string.no_items_folder.asText(),
|
||||
shouldShowAddButton = false,
|
||||
shouldShowAddButton = true,
|
||||
buttonText = R.string.new_item.asText(),
|
||||
),
|
||||
vaultData.toViewState(
|
||||
@@ -1002,6 +1002,7 @@ class VaultItemListingDataExtensionsTest {
|
||||
VaultItemListingState.ItemListingType.Vault.Folder(
|
||||
folderId = "mockId-1",
|
||||
folderName = "mockName-1",
|
||||
fullyQualifiedName = "mockName-1",
|
||||
),
|
||||
result,
|
||||
)
|
||||
|
||||
+11
-8
@@ -63,16 +63,19 @@ class FolderViewExtensionsTest {
|
||||
fun `toFolderDisplayName should return the correct name`() {
|
||||
val folderName = "Folder/test/2"
|
||||
|
||||
val folderList: List<FolderView> = listOf(
|
||||
FolderView("2", "Folder/test", clock.instant()),
|
||||
FolderView("3", "test/", clock.instant()),
|
||||
FolderView("4", folderName, clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"2",
|
||||
folderName.toFolderDisplayName(folderList),
|
||||
folderName.toFolderDisplayName(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFolderDisplayName should return folder name unchanged if there are no sub folders`() {
|
||||
val folderName = "test"
|
||||
|
||||
assertEquals(
|
||||
folderName,
|
||||
folderName.toFolderDisplayName(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -50,8 +50,10 @@ import com.x8bit.bitwarden.ui.util.performLockAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performLogoutAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performRemoveAccountClick
|
||||
import com.x8bit.bitwarden.ui.util.performYesDialogButtonClick
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
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.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
@@ -63,6 +65,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -77,6 +80,8 @@ class VaultScreenTest : BaseComposeTest() {
|
||||
private var onDimBottomNavBarRequestCalled = false
|
||||
private var onNavigateToVerificationCodeScreen = false
|
||||
private var onNavigateToSearchScreen = false
|
||||
private var onNavigateToAddFolderCalled = false
|
||||
private var onNavigateToAddFolderParentFolderName: String? = null
|
||||
private val exitManager = mockk<ExitManager>(relaxed = true)
|
||||
private val intentManager = mockk<IntentManager>(relaxed = true)
|
||||
private val appReviewManager: AppReviewManager = mockk {
|
||||
@@ -105,6 +110,10 @@ class VaultScreenTest : BaseComposeTest() {
|
||||
onNavigateToImportLoginsCalled = true
|
||||
assertEquals(SnackbarRelay.MY_VAULT_RELAY, it)
|
||||
},
|
||||
onNavigateToAddFolderScreen = { folderName ->
|
||||
onNavigateToAddFolderCalled = true
|
||||
onNavigateToAddFolderParentFolderName = folderName
|
||||
},
|
||||
exitManager = exitManager,
|
||||
intentManager = intentManager,
|
||||
appReviewManager = appReviewManager,
|
||||
@@ -657,10 +666,10 @@ class VaultScreenTest : BaseComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `floating action button click should send AddItemClick action`() {
|
||||
fun `floating action button click should send SelectAddItemType action`() {
|
||||
mutableStateFlow.update { it.copy(viewState = VaultState.ViewState.NoItems) }
|
||||
composeTestRule.onNodeWithContentDescription("Add Item").performClick()
|
||||
verify { viewModel.trySendAction(VaultAction.AddItemClick) }
|
||||
verify { viewModel.trySendAction(VaultAction.SelectAddItemType) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -670,12 +679,14 @@ class VaultScreenTest : BaseComposeTest() {
|
||||
.onNodeWithText("New login")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(VaultAction.AddItemClick) }
|
||||
verify { viewModel.trySendAction(VaultAction.AddItemClick(CreateVaultItemType.LOGIN)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToAddItemScreen event should call onNavigateToVaultAddItemScreen`() {
|
||||
mutableEventFlow.tryEmit(VaultEvent.NavigateToAddItemScreen)
|
||||
mutableEventFlow.tryEmit(
|
||||
VaultEvent.NavigateToAddItemScreen(type = VaultItemCipherType.LOGIN),
|
||||
)
|
||||
assertTrue(onNavigateToVaultAddItemScreenCalled)
|
||||
}
|
||||
|
||||
@@ -1310,6 +1321,55 @@ class VaultScreenTest : BaseComposeTest() {
|
||||
dispatcher.advanceTimeByAndRunCurrent(4000L)
|
||||
verify(exactly = 1) { appReviewManager.promptForReview() }
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `NavigateToAddItemScreen event calls onNavigateToAddFolder callback when cipher item type is FOLDER`() {
|
||||
mutableEventFlow.tryEmit(VaultEvent.NavigateToAddFolder)
|
||||
assertTrue(onNavigateToAddFolderCalled)
|
||||
assertNull(onNavigateToAddFolderParentFolderName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SelectVaultAddItemType dialog state show vault item type selection dialog`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultState.DialogState.SelectVaultAddItemType)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Type")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when option is selected in SelectVaultAddItemType dialog add item action is sent`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(dialog = VaultState.DialogState.SelectVaultAddItemType)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Card")
|
||||
.filterToOne(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify(exactly = 1) {
|
||||
viewModel.trySendAction(VaultAction.DialogDismiss)
|
||||
viewModel.trySendAction(
|
||||
VaultAction.AddItemClick(
|
||||
CreateVaultItemType.CARD,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ACTIVE_ACCOUNT_SUMMARY = AccountSummary(
|
||||
|
||||
@@ -41,10 +41,12 @@ import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
|
||||
import com.x8bit.bitwarden.ui.platform.components.snackbar.BitwardenSnackbarData
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelay
|
||||
import com.x8bit.bitwarden.ui.platform.manager.snackbar.SnackbarRelayManager
|
||||
import com.x8bit.bitwarden.ui.vault.components.model.CreateVaultItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
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.toViewState
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
@@ -1155,11 +1157,12 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddItemClick should emit NavigateToAddItemScreen`() = runTest {
|
||||
fun `AddItemClick should emit NavigateToAddItemScreen with correct type`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
val cipherType = CreateVaultItemType.CARD
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(VaultAction.AddItemClick)
|
||||
assertEquals(VaultEvent.NavigateToAddItemScreen, awaitItem())
|
||||
viewModel.trySendAction(VaultAction.AddItemClick(cipherType))
|
||||
assertEquals(VaultEvent.NavigateToAddItemScreen(VaultItemCipherType.CARD), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1908,6 +1911,19 @@ class VaultViewModelTest : BaseViewModelTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SelectAddItemType action should set dialog state to SelectVaultAddItemType`() {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.trySendAction(VaultAction.SelectAddItemType)
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
dialog = VaultState.DialogState.SelectVaultAddItemType,
|
||||
)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createViewModel(): VaultViewModel =
|
||||
VaultViewModel(
|
||||
authRepository = authRepository,
|
||||
|
||||
Reference in new issue
Block a user