PM-18370: Allow selecting type of cipher to add from collection list (#4741)

This commit is contained in:
David Perez
2025-02-18 10:59:31 -06:00
committed by GitHub
parent acc5e30b7a
commit e929ca8a7d
4 changed files with 93 additions and 65 deletions

View File

@@ -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,
)
}

View File

@@ -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<CreateVaultItemType>,
) : 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()

View File

@@ -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,
),
)

View File

@@ -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,