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 9b15ec3d38..08c92395fa 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 @@ -272,9 +272,7 @@ fun VaultItemListingScreen( onVaultItemTypeSelected = remember(viewModel) { { viewModel.trySendAction( - VaultItemListingsAction.ItemToAddToFolderSelected( - itemType = it, - ), + VaultItemListingsAction.ItemTypeToAddSelected(itemType = it), ) } }, @@ -400,6 +398,7 @@ private fun VaultItemListingDialogs( VaultItemSelectionDialog( onDismissRequest = onDismissRequest, onOptionSelected = onVaultItemTypeSelected, + excludedOptions = dialogState.excludedOptions, ) } 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 1490f83160..f3380d98c9 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,7 @@ 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.platform.util.persistentListOfNotNull 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 @@ -79,6 +80,7 @@ import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList import com.x8bit.bitwarden.ui.vault.model.TotpData import com.x8bit.bitwarden.ui.vault.model.VaultItemCipherType import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -271,8 +273,8 @@ class VaultItemListingViewModel @Inject constructor( } is VaultItemListingsAction.Internal -> handleInternalAction(action) - is VaultItemListingsAction.ItemToAddToFolderSelected -> { - handleItemToAddToFolderSelected(action) + is VaultItemListingsAction.ItemTypeToAddSelected -> { + handleItemTypeToAddSelected(action) } } } @@ -547,58 +549,71 @@ class VaultItemListingViewModel @Inject constructor( } } - private fun handleItemToAddToFolderSelected( - action: VaultItemListingsAction.ItemToAddToFolderSelected, + private fun handleItemTypeToAddSelected( + action: VaultItemListingsAction.ItemTypeToAddSelected, ) { - (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 -> { + val listingType = state.itemListingType + val collectionId = (listingType as? VaultItemListingState.ItemListingType.Vault.Collection) + ?.collectionId + val folderId = (listingType as? VaultItemListingState.ItemListingType.Vault.Folder) + ?.folderId + when (val vaultItemType = action.itemType) { + CreateVaultItemType.LOGIN, + CreateVaultItemType.CARD, + CreateVaultItemType.IDENTITY, + CreateVaultItemType.SECURE_NOTE, + CreateVaultItemType.SSH_KEY, + -> { + vaultItemType + .toVaultItemCipherTypeOrNull() + ?.let { sendEvent( - VaultItemListingEvent.NavigateToAddFolder( - parentFolderName = folder.fullyQualifiedName, + VaultItemListingEvent.NavigateToAddVaultItem( + vaultItemCipherType = it, + selectedCollectionId = collectionId, + selectedFolderId = folderId, ), ) } + } + + CreateVaultItemType.FOLDER -> { + if (listingType is VaultItemListingState.ItemListingType.Vault.Folder) { + sendEvent( + VaultItemListingEvent.NavigateToAddFolder( + parentFolderName = listingType.fullyQualifiedName, + ), + ) + } else { + throw IllegalArgumentException("$listingType does not support adding a folder") } } + } } private fun handleAddVaultItemClick() { when (val itemListingType = state.itemListingType) { - is VaultItemListingState.ItemListingType.Vault.Folder -> { + is VaultItemListingState.ItemListingType.Vault.Collection -> { mutableStateFlow.update { it.copy( - dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection, + dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( + excludedOptions = persistentListOfNotNull( + CreateVaultItemType.SSH_KEY, + CreateVaultItemType.FOLDER, + ), + ), ) } } - is VaultItemListingState.ItemListingType.Vault.Collection -> { - sendEvent( - VaultItemListingEvent.NavigateToAddVaultItem( - vaultItemCipherType = itemListingType.toVaultItemCipherType(), - selectedCollectionId = itemListingType.collectionId, - ), - ) + is VaultItemListingState.ItemListingType.Vault.Folder -> { + mutableStateFlow.update { + it.copy( + dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( + excludedOptions = persistentListOfNotNull(CreateVaultItemType.SSH_KEY), + ), + ) + } } is VaultItemListingState.ItemListingType.Vault -> { @@ -2016,7 +2031,9 @@ data class VaultItemListingState( * Represents a selection dialog to choose a vault item type to add to folder. */ @Parcelize - data object VaultItemTypeSelection : DialogState() + data class VaultItemTypeSelection( + val excludedOptions: ImmutableList, + ) : DialogState() } /** @@ -2599,7 +2616,7 @@ sealed class VaultItemListingsAction { /** * Indicated a selection was made to add a new item to the vault. */ - data class ItemToAddToFolderSelected( + data class ItemTypeToAddSelected( val itemType: CreateVaultItemType, ) : VaultItemListingsAction() 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 fb5524c032..64a372d36e 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 @@ -66,6 +66,7 @@ import io.mockk.mockkStatic import io.mockk.runs import io.mockk.unmockkStatic import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import org.junit.After @@ -2137,7 +2138,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { @Test fun `VaultItemTypeSelection dialog state show vault item type selection dialog`() { mutableStateFlow.update { - it.copy(dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection) + it.copy( + dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( + excludedOptions = persistentListOf(CreateVaultItemType.SSH_KEY), + ), + ) } composeTestRule @@ -2153,7 +2158,11 @@ class VaultItemListingScreenTest : BaseComposeTest() { @Test fun `when option is selected in VaultItemTypeSelection dialog add item action is sent`() { mutableStateFlow.update { - it.copy(dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection) + it.copy( + dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( + excludedOptions = persistentListOf(CreateVaultItemType.SSH_KEY), + ), + ) } composeTestRule @@ -2168,7 +2177,7 @@ class VaultItemListingScreenTest : BaseComposeTest() { verify(exactly = 1) { viewModel.trySendAction(VaultItemListingsAction.DismissDialogClick) viewModel.trySendAction( - VaultItemListingsAction.ItemToAddToFolderSelected( + VaultItemListingsAction.ItemTypeToAddSelected( CreateVaultItemType.CARD, ), ) 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 db6acef629..4d74614263 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 @@ -87,6 +87,7 @@ import io.mockk.mockkStatic import io.mockk.runs import io.mockk.unmockkStatic import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.runTest @@ -964,7 +965,6 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } } - @Suppress("MaxLineLength") @Test fun `AddVaultItemClick inside a folder should show item selection dialog state`() { val viewModel = createVaultItemListingViewModel( @@ -978,8 +978,10 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { itemListingType = VaultItemListingState.ItemListingType.Vault.Folder( folderId = "id", ), - ) - .copy(dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection), + dialogState = VaultItemListingState.DialogState.VaultItemTypeSelection( + excludedOptions = persistentListOf(CreateVaultItemType.SSH_KEY), + ), + ), viewModel.stateFlow.value, ) } @@ -1008,7 +1010,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } @Test - fun `ItemToAddToFolderSelected sends NavigateToAddFolder for folder selection`() = runTest { + fun `ItemTypeToAddSelected sends NavigateToAddFolder for folder selection`() = runTest { val viewModel = createVaultItemListingViewModel( savedStateHandle = createSavedStateHandleWithVaultItemListingType( vaultItemListingType = VaultItemListingType.Folder(""), @@ -1016,7 +1018,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) viewModel.eventFlow.test { viewModel.trySendAction( - VaultItemListingsAction.ItemToAddToFolderSelected( + VaultItemListingsAction.ItemTypeToAddSelected( itemType = CreateVaultItemType.FOLDER, ), ) @@ -1030,7 +1032,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { } @Test - fun `ItemToAddToFolderSelected sends NavigateToAddFolder for any other selection`() = runTest { + fun `ItemTypeToAddSelected sends NavigateToAddFolder for any other selection`() = runTest { val viewModel = createVaultItemListingViewModel( savedStateHandle = createSavedStateHandleWithVaultItemListingType( vaultItemListingType = VaultItemListingType.Folder("id"), @@ -1038,7 +1040,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { ) viewModel.eventFlow.test { viewModel.trySendAction( - VaultItemListingsAction.ItemToAddToFolderSelected( + VaultItemListingsAction.ItemTypeToAddSelected( itemType = CreateVaultItemType.CARD, ), ) @@ -3189,9 +3191,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { assertEquals( VaultItemListingState.DialogState.Fido2OperationFail( title = R.string.an_error_has_occurred.asText(), - message = - R.string.passkey_operation_failed_because_the_selected_item_does_not_exist - .asText(), + message = R.string + .passkey_operation_failed_because_the_selected_item_does_not_exist + .asText(), ), viewModel.stateFlow.value.dialogState, ) @@ -3232,9 +3234,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { assertEquals( VaultItemListingState.DialogState.Fido2OperationFail( title = R.string.an_error_has_occurred.asText(), - message = - R.string.passkey_operation_failed_because_the_selected_item_does_not_exist - .asText(), + message = R.string + .passkey_operation_failed_because_the_selected_item_does_not_exist + .asText(), ), viewModel.stateFlow.value.dialogState, ) @@ -3951,9 +3953,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { assertEquals( VaultItemListingState.DialogState.Fido2OperationFail( title = R.string.an_error_has_occurred.asText(), - message = - R.string.passkey_operation_failed_because_user_verification_attempts_exceeded - .asText(), + message = R.string + .passkey_operation_failed_because_user_verification_attempts_exceeded + .asText(), ), viewModel.stateFlow.value.dialogState, ) @@ -4123,9 +4125,9 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { assertEquals( VaultItemListingState.DialogState.Fido2OperationFail( title = R.string.an_error_has_occurred.asText(), - message = - R.string.passkey_operation_failed_because_user_verification_attempts_exceeded - .asText(), + message = R.string + .passkey_operation_failed_because_user_verification_attempts_exceeded + .asText(), ), viewModel.stateFlow.value.dialogState, ) @@ -4444,6 +4446,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { private fun createVaultItemListingState( itemListingType: VaultItemListingState.ItemListingType = VaultItemListingState.ItemListingType.Vault.Login, viewState: VaultItemListingState.ViewState = VaultItemListingState.ViewState.Loading, + dialogState: VaultItemListingState.DialogState? = null, ): VaultItemListingState = VaultItemListingState( itemListingType = itemListingType, @@ -4455,7 +4458,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() { baseIconUrl = environmentRepository.environment.environmentUrlData.baseIconUrl, isIconLoadingDisabled = settingsRepository.isIconLoadingDisabled, isPullToRefreshSettingEnabled = false, - dialogState = null, + dialogState = dialogState, totpData = null, autofillSelectionData = null, policyDisablesSend = false,