diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt index 369b9de617..ff9dec921e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditNavigation.kt @@ -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, ) } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt index 06ad1ddf4a..ed925a1f3c 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModel.kt @@ -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 { /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt index c7097b859a..77c1f5e30b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlocked/VaultUnlockedNavigation.kt @@ -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() }, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt index a6a43bf395..aab5900ee7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarNavigation.kt @@ -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, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt index 19bfc29d05..2ca7624919 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreen.kt @@ -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, diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/VaultItemSelectionDialog.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/VaultItemSelectionDialog.kt new file mode 100644 index 0000000000..ace700a238 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/VaultItemSelectionDialog.kt @@ -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 = 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) + }, + ) + } + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt new file mode 100644 index 0000000000..94c142f106 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/model/CreateVaultItemType.kt @@ -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), +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensions.kt new file mode 100644 index 0000000000..ca90aca03c --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensions.kt @@ -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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt index e447270eb5..89a9b713e2 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingNavigation.kt @@ -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, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt index c81326318d..9015b09d62 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreen.kt @@ -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 } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 76ebc21b49..bcc1d78d01 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt index dc8fa52b20..536d20603b 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensions.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensions.kt index d3f1200c5e..0806aa6ef4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensions.kt @@ -68,18 +68,4 @@ fun List.getFilteredFolders(folderName: String? = null): List): 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() diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt index 40069a2f36..57cf12cf29 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultGraphNavigation.kt @@ -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( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt index 3f87d6f855..d6811e269f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultNavigation.kt @@ -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, ) } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt index 0e727efc5b..d9078fbbcd 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreen.kt @@ -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 } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index d978e7ff56..70f6145838 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -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. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt index 0c257640c2..c3fa2e4578 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/handlers/VaultHandlers.kt @@ -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)) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreenTest.kt index f63c2dc69f..1c49f51593 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditScreenTest.kt @@ -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, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt index cb515a1bf2..fbf6353055 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/folders/addedit/FolderAddEditViewModelTest.kt @@ -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" diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt index ca324e073f..d0eac04636 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/vaultunlockednavbar/VaultUnlockedNavBarScreenTest.kt @@ -56,6 +56,7 @@ class VaultUnlockedNavBarScreenTest : BaseComposeTest() { onNavigateToSetupAutoFillScreen = {}, onNavigateToSetupUnlockScreen = {}, onNavigateToImportLogins = {}, + onNavigateToAddFolderScreen = { _ -> }, ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensionsTest.kt new file mode 100644 index 0000000000..0bb6b1bebe --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/components/util/CreateVaultItemTypeExtensionsTest.kt @@ -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) + } + } + } +} diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt index fb18ce3b42..1d43c64c34 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditViewModelTest.kt @@ -4471,7 +4471,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() { VaultItemCipherType.IDENTITY -> "identity" VaultItemCipherType.SECURE_NOTE -> "secure_note" VaultItemCipherType.SSH_KEY -> "ssh_key" - null -> null + else -> null }, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index 10150e0d09..765cacb35c 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt index 8b73181071..72ddd51863 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModelTest.kt @@ -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() diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt index f0c0de9da0..53e59211f8 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt @@ -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, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensionsTest.kt index 8475352df2..b3b2e0334d 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/util/FolderViewExtensionsTest.kt @@ -63,16 +63,19 @@ class FolderViewExtensionsTest { fun `toFolderDisplayName should return the correct name`() { val folderName = "Folder/test/2" - val folderList: List = 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(), ) } } diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index 69c4deec65..9017407f20 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -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(relaxed = true) private val intentManager = mockk(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( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt index 1d2633ad46..e7a595368a 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModelTest.kt @@ -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,