diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt index badfbf1000c..9f30d4d28f1 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier @@ -33,7 +34,7 @@ import kotlinx.collections.immutable.toPersistentList /** * The contents state for the search screen. */ -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun SearchContent( viewState: SearchState.ViewState.Content, @@ -41,43 +42,28 @@ fun SearchContent( searchType: SearchTypeData, modifier: Modifier = Modifier, ) { - var showConfirmationDialog: ListingItemOverflowAction? by rememberSaveable { - mutableStateOf(null) + var overflowSpeedBumpAction by remember { + mutableStateOf(value = null) } - when (val option = showConfirmationDialog) { - is ListingItemOverflowAction.SendAction.DeleteClick -> { - BitwardenTwoButtonDialog( - title = stringResource(id = BitwardenString.delete), - message = stringResource(id = BitwardenString.are_you_sure_delete_send), - confirmButtonText = stringResource(id = BitwardenString.yes), - dismissButtonText = stringResource(id = BitwardenString.cancel), - onConfirmClick = { - showConfirmationDialog = null - searchHandlers.onOverflowItemClick(option) - }, - onDismissClick = { showConfirmationDialog = null }, - onDismissRequest = { showConfirmationDialog = null }, - ) - } - - is ListingItemOverflowAction.SendAction.CopyUrlClick, - is ListingItemOverflowAction.SendAction.EditClick, - is ListingItemOverflowAction.SendAction.RemovePasswordClick, - is ListingItemOverflowAction.SendAction.ShareUrlClick, - is ListingItemOverflowAction.SendAction.ViewClick, - is ListingItemOverflowAction.VaultAction.CopyNoteClick, - is ListingItemOverflowAction.VaultAction.CopyNumberClick, - is ListingItemOverflowAction.VaultAction.CopyPasswordClick, - is ListingItemOverflowAction.VaultAction.CopyTotpClick, - is ListingItemOverflowAction.VaultAction.CopySecurityCodeClick, - is ListingItemOverflowAction.VaultAction.CopyUsernameClick, - is ListingItemOverflowAction.VaultAction.EditClick, - is ListingItemOverflowAction.VaultAction.LaunchClick, - is ListingItemOverflowAction.VaultAction.ViewClick, - is ListingItemOverflowAction.VaultAction.ArchiveClick, - is ListingItemOverflowAction.VaultAction.UnarchiveClick, - null, - -> Unit + overflowSpeedBumpAction?.let { action -> + action + .speedBump + ?.let { speedBump -> + BitwardenTwoButtonDialog( + twoButtonDialogData = speedBump, + onConfirmClick = { + overflowSpeedBumpAction = null + searchHandlers.onOverflowItemClick(action) + }, + onDismissClick = { overflowSpeedBumpAction = null }, + onDismissRequest = { overflowSpeedBumpAction = null }, + ) + } + ?: run { + // If we somehow get here and there is no speed bump, then we should keep on going. + overflowSpeedBumpAction = null + searchHandlers.onOverflowItemClick(action) + } } var autofillSelectionOptionsItem by rememberSaveable { @@ -143,8 +129,12 @@ fun SearchContent( contentDescription = option.contentDescription(), onClick = { when (option) { - is ListingItemOverflowAction.SendAction.DeleteClick -> { - showConfirmationDialog = option + is ListingItemOverflowAction.SendAction -> { + if (option.speedBump != null) { + overflowSpeedBumpAction = option + } else { + searchHandlers.onOverflowItemClick(option) + } } is ListingItemOverflowAction.VaultAction -> { @@ -155,12 +145,12 @@ fun SearchContent( MasterPasswordRepromptData.OverflowItem( action = option, ) + } else if (option.speedBump != null) { + overflowSpeedBumpAction = option } else { searchHandlers.onOverflowItemClick(option) } } - - else -> searchHandlers.onOverflowItemClick(option) } }, ) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt index f2ddc022303..06e71681c2b 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingContent.kt @@ -31,7 +31,7 @@ import com.x8bit.bitwarden.ui.platform.components.listitem.BitwardenListItem import com.x8bit.bitwarden.ui.platform.components.listitem.SelectionItemData import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction -import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toImmutableList /** * Content view for the [VaultItemListingScreen]. @@ -46,43 +46,29 @@ fun VaultItemListingContent( vaultItemListingHandlers: VaultItemListingHandlers, modifier: Modifier = Modifier, ) { - var showConfirmationDialog: ListingItemOverflowAction? by rememberSaveable { + var overflowSpeedBumpAction: ListingItemOverflowAction? by rememberSaveable { mutableStateOf(null) } - when (val option = showConfirmationDialog) { - is ListingItemOverflowAction.SendAction.DeleteClick -> { - BitwardenTwoButtonDialog( - title = stringResource(id = BitwardenString.delete), - message = stringResource(id = BitwardenString.are_you_sure_delete_send), - confirmButtonText = stringResource(id = BitwardenString.yes), - dismissButtonText = stringResource(id = BitwardenString.cancel), - onConfirmClick = { - showConfirmationDialog = null - vaultItemListingHandlers.overflowItemClick(option) - }, - onDismissClick = { showConfirmationDialog = null }, - onDismissRequest = { showConfirmationDialog = null }, - ) - } - is ListingItemOverflowAction.SendAction.CopyUrlClick, - is ListingItemOverflowAction.SendAction.EditClick, - is ListingItemOverflowAction.SendAction.ViewClick, - is ListingItemOverflowAction.SendAction.RemovePasswordClick, - is ListingItemOverflowAction.SendAction.ShareUrlClick, - is ListingItemOverflowAction.VaultAction.CopyNoteClick, - is ListingItemOverflowAction.VaultAction.CopyNumberClick, - is ListingItemOverflowAction.VaultAction.CopyPasswordClick, - is ListingItemOverflowAction.VaultAction.CopySecurityCodeClick, - is ListingItemOverflowAction.VaultAction.CopyUsernameClick, - is ListingItemOverflowAction.VaultAction.EditClick, - is ListingItemOverflowAction.VaultAction.LaunchClick, - is ListingItemOverflowAction.VaultAction.ViewClick, - is ListingItemOverflowAction.VaultAction.CopyTotpClick, - is ListingItemOverflowAction.VaultAction.ArchiveClick, - is ListingItemOverflowAction.VaultAction.UnarchiveClick, - null, - -> Unit + overflowSpeedBumpAction?.let { action -> + action + .speedBump + ?.let { speedBump -> + BitwardenTwoButtonDialog( + twoButtonDialogData = speedBump, + onConfirmClick = { + overflowSpeedBumpAction = null + vaultItemListingHandlers.overflowItemClick(action) + }, + onDismissClick = { overflowSpeedBumpAction = null }, + onDismissRequest = { overflowSpeedBumpAction = null }, + ) + } + ?: run { + // If we somehow get here and there is no speed bump, then we should keep on going. + overflowSpeedBumpAction = null + vaultItemListingHandlers.overflowItemClick(action) + } } var masterPasswordRepromptData by remember { mutableStateOf(null) } @@ -264,8 +250,12 @@ fun VaultItemListingContent( contentDescription = option.contentDescription(), onClick = { when (option) { - is ListingItemOverflowAction.SendAction.DeleteClick -> { - showConfirmationDialog = option + is ListingItemOverflowAction.SendAction -> { + if (option.speedBump != null) { + overflowSpeedBumpAction = option + } else { + vaultItemListingHandlers.overflowItemClick(option) + } } is ListingItemOverflowAction.VaultAction -> { @@ -276,19 +266,19 @@ fun VaultItemListingContent( MasterPasswordRepromptData.OverflowItem( action = option, ) + } else if (option.speedBump != null) { + overflowSpeedBumpAction = option } else { vaultItemListingHandlers.overflowItemClick(option) } } - - else -> vaultItemListingHandlers.overflowItemClick(option) } }, ) } // Only show options if allowed .filter { !policyDisablesSend } - .toPersistentList(), + .toImmutableList(), cardStyle = state .displayItemList .toListItemCardStyle(index = index, dividerPadding = 56.dp), diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt index c6b7eb87147..1841612bba6 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/model/ListingItemOverflowAction.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.model import android.os.Parcelable import com.bitwarden.send.SendType +import com.bitwarden.ui.platform.components.dialog.model.BitwardenTwoButtonDialogData import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.util.Text import com.bitwarden.ui.util.asText @@ -23,6 +24,11 @@ sealed class ListingItemOverflowAction : Parcelable { */ abstract val contentDescription: Text + /** + * The data to be displayed for an optional speed bump dialog. + */ + abstract val speedBump: BitwardenTwoButtonDialogData? + /** * Represents the send actions. */ @@ -37,6 +43,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : SendAction() { override val title: Text get() = BitwardenString.view.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -49,6 +56,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : SendAction() { override val title: Text get() = BitwardenString.edit.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -58,6 +66,7 @@ sealed class ListingItemOverflowAction : Parcelable { data class CopyUrlClick(val sendUrl: String) : SendAction() { override val title: Text get() = BitwardenString.copy_link.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -68,6 +77,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val title: Text get() = BitwardenString.share_link.asText() override val contentDescription: Text get() = BitwardenString.external_link_format.asText(title) + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -77,6 +87,7 @@ sealed class ListingItemOverflowAction : Parcelable { data class RemovePasswordClick(val sendId: String) : SendAction() { override val title: Text get() = BitwardenString.remove_password.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -86,6 +97,13 @@ sealed class ListingItemOverflowAction : Parcelable { data class DeleteClick(val sendId: String) : SendAction() { override val title: Text get() = BitwardenString.delete.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? + get() = BitwardenTwoButtonDialogData( + title = BitwardenString.delete.asText(), + message = BitwardenString.are_you_sure_delete_send.asText(), + confirmButtonText = BitwardenString.yes.asText(), + dismissButtonText = BitwardenString.cancel.asText(), + ) } } @@ -110,6 +128,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : VaultAction() { override val title: Text get() = BitwardenString.view.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -123,6 +142,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : VaultAction() { override val title: Text get() = BitwardenString.edit.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -133,6 +153,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val title: Text get() = BitwardenString.copy_username.asText() override val requiresPasswordReprompt: Boolean get() = false override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -145,6 +166,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : VaultAction() { override val title: Text get() = BitwardenString.copy_password.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -157,6 +179,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : VaultAction() { override val title: Text get() = BitwardenString.copy_totp.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -169,6 +192,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : VaultAction() { override val title: Text get() = BitwardenString.copy_number.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -181,6 +205,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : VaultAction() { override val title: Text get() = BitwardenString.copy_security_code.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -193,6 +218,7 @@ sealed class ListingItemOverflowAction : Parcelable { ) : VaultAction() { override val title: Text get() = BitwardenString.copy_notes.asText() override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -204,6 +230,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val requiresPasswordReprompt: Boolean get() = false override val contentDescription: Text get() = BitwardenString.external_link_format.asText(title) + override val speedBump: BitwardenTwoButtonDialogData? get() = null } /** @@ -214,6 +241,13 @@ sealed class ListingItemOverflowAction : Parcelable { override val title: Text get() = BitwardenString.archive_verb.asText() override val requiresPasswordReprompt: Boolean get() = true override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? + get() = BitwardenTwoButtonDialogData( + title = BitwardenString.archive_item.asText(), + message = BitwardenString.once_archived_this_item_will_be_excluded.asText(), + confirmButtonText = BitwardenString.archive_verb.asText(), + dismissButtonText = BitwardenString.cancel.asText(), + ) } /** @@ -224,6 +258,7 @@ sealed class ListingItemOverflowAction : Parcelable { override val title: Text get() = BitwardenString.unarchive.asText() override val requiresPasswordReprompt: Boolean get() = true override val contentDescription: Text get() = title + override val speedBump: BitwardenTwoButtonDialogData? get() = null } } } diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt index c874954bdff..05ab64734ef 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultContent.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.base.util.standardHorizontalMargin import com.bitwarden.ui.platform.base.util.toListItemCardStyle import com.bitwarden.ui.platform.components.card.BitwardenActionCard +import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText import com.bitwarden.ui.platform.components.icon.model.IconData import com.bitwarden.ui.platform.components.model.CardStyle @@ -73,6 +74,31 @@ fun VaultContent( onDismissRequest = { overflowMasterPasswordRepromptAction = null }, ) } + + var overflowSpeedBumpAction by remember { + mutableStateOf(value = null) + } + overflowSpeedBumpAction?.let { action -> + action + .speedBump + ?.let { speedBump -> + BitwardenTwoButtonDialog( + twoButtonDialogData = speedBump, + onConfirmClick = { + overflowSpeedBumpAction = null + vaultHandlers.overflowOptionClick(action) + }, + onDismissClick = { overflowSpeedBumpAction = null }, + onDismissRequest = { overflowSpeedBumpAction = null }, + ) + } + ?: run { + // If we somehow get here and there is no speed bump, then we should keep on going. + overflowSpeedBumpAction = null + vaultHandlers.overflowOptionClick(action) + } + } + LazyColumn( modifier = modifier, ) { @@ -162,6 +188,8 @@ fun VaultContent( action.requiresPasswordReprompt ) { overflowMasterPasswordRepromptAction = action + } else if (action.speedBump != null) { + overflowSpeedBumpAction = action } else { vaultHandlers.overflowOptionClick(action) } @@ -364,6 +392,8 @@ fun VaultContent( action.requiresPasswordReprompt ) { overflowMasterPasswordRepromptAction = action + } else if (action.speedBump != null) { + overflowSpeedBumpAction = action } else { vaultHandlers.overflowOptionClick(action) } diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt index ac211e3c09f..fdcc11be905 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/platform/feature/search/SearchScreenTest.kt @@ -728,26 +728,6 @@ class SearchScreenTest : BitwardenComposeTest() { ) } - composeTestRule - .onNodeWithContentDescription("More options") - .assertIsDisplayed() - .performClick() - composeTestRule - .onNodeWithText("Archive") - .assert(hasAnyAncestor(isDialog())) - .performScrollTo() - .assertIsDisplayed() - .performClick() - verify(exactly = 1) { - viewModel.trySendAction( - SearchAction.OverflowOptionClick( - overflowAction = ListingItemOverflowAction.VaultAction.ArchiveClick( - cipherId = "mockId-1", - ), - ), - ) - } - composeTestRule.assertNoDialogExists() } @@ -959,7 +939,52 @@ class SearchScreenTest : BitwardenComposeTest() { @Suppress("MaxLineLength") @Test - fun `on send item delete overflow option click should display delete confirmation dialog and emits DeleteSendConfirmClick on confirmation`() { + fun `on vault item archive overflow option click should display archive confirmation dialog and emits ArchiveClick on confirmation`() { + val itemId = "mockId-1" + val title = "Archive item" + mutableStateFlow.update { + it.copy( + viewState = SearchState.ViewState.Content( + displayItems = persistentListOf(createMockDisplayItemForCipher(number = 1)), + ), + ) + } + composeTestRule.onNode(isDialog()).assertDoesNotExist() + composeTestRule.onNodeWithText(text = title).assertDoesNotExist() + + composeTestRule + .onNodeWithContentDescription(label = "More options") + .assertIsDisplayed() + .performClick() + composeTestRule + .onNodeWithText(text = "Archive") + .performScrollTo() + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule + .onNodeWithText(title) + .assertIsDisplayed() + .assert(hasAnyAncestor(isDialog())) + composeTestRule + .onNodeWithText(text = "Archive") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction( + SearchAction.OverflowOptionClick( + overflowAction = ListingItemOverflowAction.VaultAction.ArchiveClick( + cipherId = itemId, + ), + ), + ) + } + } + + @Suppress("MaxLineLength") + @Test + fun `on send item delete overflow option click should display delete confirmation dialog and emits DeleteClick on confirmation`() { val sendId = "mockId-1" val message = "Are you sure you want to delete this Send?" mutableStateFlow.update { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt index 668f4607bb2..ee26ce6bf7b 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingScreenTest.kt @@ -1441,6 +1441,55 @@ class VaultItemListingScreenTest : BitwardenComposeTest() { composeTestRule.assertNoDialogExists() } + @Suppress("MaxLineLength") + @Test + fun `on item archive overflow option click should display archive confirmation dialog and emits ArchiveClick on confirmation`() { + val cipherId = "mockId-1" + val archiveAction = ListingItemOverflowAction.VaultAction.ArchiveClick(cipherId = cipherId) + mutableStateFlow.update { + it.copy( + itemListingType = VaultItemListingState.ItemListingType.Vault.Login, + viewState = VaultItemListingState.ViewState.Content( + displayCollectionList = emptyList(), + displayItemList = listOf( + createCipherDisplayItem(number = 1).copy( + overflowOptions = listOf(archiveAction), + ), + ), + displayFolderList = emptyList(), + ), + ) + } + + composeTestRule + .onNodeWithText(text = "mockTitle-1") + .onChildren() + .filterToOne(hasContentDescription(value = "More options")) + .assertIsDisplayed() + .performClick() + + composeTestRule + .onNodeWithText(text = "Archive") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule + .onAllNodesWithText(text = "Archive item") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + composeTestRule + .onAllNodesWithText(text = "Archive") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction( + action = VaultItemListingsAction.OverflowOptionClick(action = archiveAction), + ) + } + } + @Suppress("MaxLineLength") @Test fun `on cipher item overflow option click when reprompt is required should show the master password dialog`() { diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt index 70057205bb0..8e5ec203d21 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/vault/VaultScreenTest.kt @@ -1329,6 +1329,57 @@ class VaultScreenTest : BitwardenComposeTest() { } } + @Suppress("MaxLineLength") + @Test + fun `on vault item archive overflow option click should display archive confirmation dialog and emits ArchiveClick on confirmation`() { + val itemText = "Test Item" + val userName = "Bitwarden" + val cipherId = "12345" + val archiveAction = ListingItemOverflowAction.VaultAction.ArchiveClick(cipherId = cipherId) + val vaultItem = VaultState.ViewState.VaultItem.Login( + id = cipherId, + name = itemText.asText(), + username = userName.asText(), + overflowOptions = persistentListOf(archiveAction), + shouldShowMasterPasswordReprompt = false, + hasDecryptionError = false, + ) + mutableStateFlow.update { + it.copy( + viewState = DEFAULT_CONTENT_VIEW_STATE.copy( + favoriteItems = listOf(vaultItem), + ), + ) + } + + composeTestRule.onNode(hasScrollToNodeAction()).performScrollToNode(hasText(itemText)) + composeTestRule + .onNodeWithText(text = itemText) + .onChildren() + .filterToOne(hasContentDescription(value = "More options")) + .assertIsDisplayed() + .performClick() + + composeTestRule + .onNodeWithText(text = "Archive") + .assert(hasAnyAncestor(isDialog())) + .performClick() + + composeTestRule + .onAllNodesWithText(text = "Archive item") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + composeTestRule + .onAllNodesWithText(text = "Archive") + .filterToOne(hasAnyAncestor(isDialog())) + .assertIsDisplayed() + .performClick() + + verify(exactly = 1) { + viewModel.trySendAction(VaultAction.OverflowOptionClick(overflowAction = archiveAction)) + } + } + @Suppress("MaxLineLength") @Test fun `clicking a favorite item overflow with password prompt should prompt for password before dismissing upon Cancel`() { diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenTwoButtonDialog.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenTwoButtonDialog.kt index 5592258914d..cd128a1df2f 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenTwoButtonDialog.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenTwoButtonDialog.kt @@ -28,11 +28,51 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.bitwarden.ui.platform.base.util.toAnnotatedString import com.bitwarden.ui.platform.components.button.BitwardenTextButton +import com.bitwarden.ui.platform.components.dialog.model.BitwardenTwoButtonDialogData import com.bitwarden.ui.platform.components.dialog.util.maxDialogHeight import com.bitwarden.ui.platform.components.dialog.util.maxDialogWidth import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider import com.bitwarden.ui.platform.theme.BitwardenTheme +/** + * Represents a Bitwarden-styled dialog with two buttons. + * + * @param twoButtonDialogData The data to be displayed. + * @param onConfirmClick Called when the confirmation button is clicked. + * @param onDismissClick Called when the dismiss button is clicked. + * @param onDismissRequest Called when the user attempts to dismiss the dialog (for example by + * tapping outside of it). + * @param confirmTextColor The color of the confirmation text. + * @param dismissTextColor The color of the dismiss text. + * @param dismissOnBackPress Indicates that the back button should dismiss the dialog. + * @param dismissOnClickOutside Indicates that tapping outside the dialog should dismiss the dialog. + */ +@Composable +fun BitwardenTwoButtonDialog( + twoButtonDialogData: BitwardenTwoButtonDialogData, + onConfirmClick: () -> Unit, + onDismissClick: () -> Unit, + onDismissRequest: () -> Unit, + confirmTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground, + dismissTextColor: Color = BitwardenTheme.colorScheme.outlineButton.foreground, + dismissOnBackPress: Boolean = true, + dismissOnClickOutside: Boolean = true, +) { + BitwardenTwoButtonDialog( + title = twoButtonDialogData.title?.invoke(), + message = twoButtonDialogData.message(), + confirmButtonText = twoButtonDialogData.confirmButtonText(), + dismissButtonText = twoButtonDialogData.dismissButtonText(), + onConfirmClick = onConfirmClick, + onDismissClick = onDismissClick, + onDismissRequest = onDismissRequest, + confirmTextColor = confirmTextColor, + dismissTextColor = dismissTextColor, + dismissOnBackPress = dismissOnBackPress, + dismissOnClickOutside = dismissOnClickOutside, + ) +} + /** * Represents a Bitwarden-styled dialog with two buttons. * @@ -44,8 +84,10 @@ import com.bitwarden.ui.platform.theme.BitwardenTheme * @param onDismissClick called when the dismiss button is clicked. * @param onDismissRequest called when the user attempts to dismiss the dialog (for example by * tapping outside of it). - * @param confirmTextColor The color of the confirm text. + * @param confirmTextColor The color of the confirmation text. * @param dismissTextColor The color of the dismiss text. + * @param dismissOnBackPress Indicates that the back button should dismiss the dialog. + * @param dismissOnClickOutside Indicates that tapping outside the dialog should dismiss the dialog. */ @Composable fun BitwardenTwoButtonDialog( @@ -87,8 +129,10 @@ fun BitwardenTwoButtonDialog( * @param onDismissClick called when the dismiss button is clicked. * @param onDismissRequest called when the user attempts to dismiss the dialog (for example by * tapping outside of it). - * @param confirmTextColor The color of the confirm text. + * @param confirmTextColor The color of the confirmation text. * @param dismissTextColor The color of the dismiss text. + * @param dismissOnBackPress Indicates that the back button should dismiss the dialog. + * @param dismissOnClickOutside Indicates that tapping outside the dialog should dismiss the dialog. */ @Composable @Suppress("LongMethod") diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/model/BitwardenTwoButtonDialogData.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/model/BitwardenTwoButtonDialogData.kt new file mode 100644 index 00000000000..3163a437286 --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/model/BitwardenTwoButtonDialogData.kt @@ -0,0 +1,19 @@ +package com.bitwarden.ui.platform.components.dialog.model + +import com.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog +import com.bitwarden.ui.util.Text + +/** + * Contains the data for displaying a [BitwardenTwoButtonDialog]. + * + * @property title The optional title to show. + * @property message The message to show. + * @property confirmButtonText The text to show on confirmation button. + * @property dismissButtonText The text to show on dismiss button. + */ +class BitwardenTwoButtonDialogData( + val title: Text?, + val message: Text, + val confirmButtonText: Text, + val dismissButtonText: Text, +) diff --git a/ui/src/main/res/values/strings_non_localized.xml b/ui/src/main/res/values/strings_non_localized.xml index a95fc6f3e11..e5a3cb6e62d 100644 --- a/ui/src/main/res/values/strings_non_localized.xml +++ b/ui/src/main/res/values/strings_non_localized.xml @@ -42,6 +42,8 @@ Avoid logout on KDF change Migrate My Vault to My Items Archive Items + Archive item + Once archived, this item will be excluded from search results and autofill suggestions. Send Email Verification Trigger cookie acquisition Clear SSO cookies