mirror of
https://github.com/bitwarden/android.git
synced 2026-03-22 06:11:38 -05:00
PM-18370: Allow selecting type of cipher to add from collection list (#4741)
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user