Add delete and remove password options items for send (#591)

This commit is contained in:
David Perez 2024-01-12 12:53:15 -06:00 committed by Álison Fernandes
parent 197feea56a
commit c06be2b8de
8 changed files with 197 additions and 2 deletions

View file

@ -83,6 +83,12 @@ fun SendContent(
onCopyClick = { sendHandlers.onCopySendClick(it) },
onEditClick = { sendHandlers.onEditSendClick(it) },
onShareClick = { sendHandlers.onShareSendClick(it) },
onDeleteClick = { sendHandlers.onDeleteSendClick(it) },
onRemovePasswordClick = if (it.hasPassword) {
{ sendHandlers.onRemovePasswordClick(it) }
} else {
null
},
modifier = Modifier
.padding(
start = 16.dp,

View file

@ -1,6 +1,10 @@
package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
@ -8,11 +12,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenTwoButtonDialog
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
import com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
import kotlinx.collections.immutable.persistentListOf
/**
* A Composable function that displays a row send item.
@ -24,6 +29,9 @@ import kotlinx.collections.immutable.persistentListOf
* @param onEditClick The lambda to be invoked when the edit option is clicked from the menu.
* @param onCopyClick The lambda to be invoked when the copy option is clicked from the menu.
* @param onShareClick The lambda to be invoked when the share option is clicked from the menu.
* @param onDeleteClick The lambda to be invoked when the delete option is clicked from the menu.
* @param onRemovePasswordClick The lambda to be invoked when the remove password option is clicked
* from the menu, if `null` the remove password button is not displayed.
* @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
* This allows the caller to specify things like padding, size, etc.
*/
@ -38,8 +46,11 @@ fun SendListItem(
onEditClick: () -> Unit,
onCopyClick: () -> Unit,
onShareClick: () -> Unit,
onDeleteClick: () -> Unit,
onRemovePasswordClick: (() -> Unit)?,
modifier: Modifier = Modifier,
) {
var shouldShowDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
BitwardenListItem(
label = label,
supportingLabel = supportingLabel,
@ -51,7 +62,7 @@ fun SendListItem(
)
},
onClick = onClick,
selectionDataList = persistentListOf(
selectionDataList = persistentListOfNotNull(
SelectionItemData(
text = stringResource(id = R.string.edit),
onClick = onEditClick,
@ -64,9 +75,33 @@ fun SendListItem(
text = stringResource(id = R.string.share_link),
onClick = onShareClick,
),
onRemovePasswordClick?.let {
SelectionItemData(
text = stringResource(id = R.string.remove_password),
onClick = it,
)
},
SelectionItemData(
text = stringResource(id = R.string.delete),
onClick = { shouldShowDeleteConfirmationDialog = true },
),
),
modifier = modifier,
)
if (shouldShowDeleteConfirmationDialog) {
BitwardenTwoButtonDialog(
title = stringResource(id = R.string.delete),
message = stringResource(id = R.string.are_you_sure_delete_send),
confirmButtonText = stringResource(id = R.string.yes),
dismissButtonText = stringResource(id = R.string.cancel),
onConfirmClick = {
shouldShowDeleteConfirmationDialog = false
onDeleteClick()
},
onDismissClick = { shouldShowDeleteConfirmationDialog = false },
onDismissRequest = { shouldShowDeleteConfirmationDialog = false },
)
}
}
@Preview(showBackground = true)
@ -82,6 +117,8 @@ private fun SendListItem_preview() {
onCopyClick = {},
onEditClick = {},
onShareClick = {},
onDeleteClick = {},
onRemovePasswordClick = null,
)
}
}

View file

@ -67,6 +67,8 @@ class SendViewModel @Inject constructor(
is SendAction.SendClick -> handleSendClick(action)
is SendAction.ShareClick -> handleShareClick(action)
SendAction.TextTypeClick -> handleTextTypeClick()
is SendAction.DeleteSendClick -> handleDeleteSendClick(action)
is SendAction.RemovePasswordClick -> handleRemovePasswordClick(action)
is SendAction.Internal -> handleInternalAction(action)
}
@ -185,6 +187,16 @@ class SendViewModel @Inject constructor(
// TODO: Navigate to the text type send list screen (BIT-1388)
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
}
private fun handleDeleteSendClick(action: SendAction.DeleteSendClick) {
// TODO: Navigate to the text type send list screen (BIT-1388)
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
}
private fun handleRemovePasswordClick(action: SendAction.RemovePasswordClick) {
// TODO: Navigate to the text type send list screen (BIT-1388)
sendEvent(SendEvent.ShowToast("Not yet implemented".asText()))
}
}
/**
@ -227,6 +239,7 @@ data class SendState(
val type: Type,
val iconList: List<SendStatusIcon>,
val shareUrl: String,
val hasPassword: Boolean,
) : Parcelable {
/**
* Indicates the type of send this, a text or file.
@ -345,6 +358,20 @@ sealed class SendAction {
val sendItem: SendState.ViewState.Content.SendItem,
) : SendAction()
/**
* User clicked the delete item button.
*/
data class DeleteSendClick(
val sendItem: SendState.ViewState.Content.SendItem,
) : SendAction()
/**
* User clicked the remove password item button.
*/
data class RemovePasswordClick(
val sendItem: SendState.ViewState.Content.SendItem,
) : SendAction()
/**
* Models actions that the [SendViewModel] itself will send.
*/

View file

@ -15,6 +15,8 @@ data class SendHandlers(
val onEditSendClick: (SendState.ViewState.Content.SendItem) -> Unit,
val onCopySendClick: (SendState.ViewState.Content.SendItem) -> Unit,
val onShareSendClick: (SendState.ViewState.Content.SendItem) -> Unit,
val onDeleteSendClick: (SendState.ViewState.Content.SendItem) -> Unit,
val onRemovePasswordClick: (SendState.ViewState.Content.SendItem) -> Unit,
) {
companion object {
/**
@ -30,6 +32,10 @@ data class SendHandlers(
onEditSendClick = { viewModel.trySendAction(SendAction.SendClick(it)) },
onCopySendClick = { viewModel.trySendAction(SendAction.CopyClick(it)) },
onShareSendClick = { viewModel.trySendAction(SendAction.ShareClick(it)) },
onDeleteSendClick = { viewModel.trySendAction(SendAction.DeleteSendClick(it)) },
onRemovePasswordClick = {
viewModel.trySendAction(SendAction.RemovePasswordClick(it))
},
)
}
}

View file

@ -54,6 +54,7 @@ private fun List<SendView>.toSendContent(
},
),
shareUrl = sendView.toSendUrl(baseWebSendUrl),
hasPassword = sendView.hasPassword,
)
}
.sortedBy { it.name },

View file

@ -470,6 +470,100 @@ class SendScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists()
}
@Test
fun `on send item overflow dialog remove password click should send RemovePasswordClick`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Remove password")
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(SendAction.RemovePasswordClick(DEFAULT_SEND_ITEM))
}
composeTestRule.assertNoDialogExists()
}
@Test
fun `on send item overflow dialog delete click should show confirmation dialog`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Delete")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onNodeWithText("Are you sure you want to delete this Send?")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Test
fun `on delete confirmation dialog yes click should send DeleteSendClick`() {
mutableStateFlow.update {
it.copy(
viewState = SendState.ViewState.Content(
textTypeCount = 0,
fileTypeCount = 1,
sendItems = listOf(DEFAULT_SEND_ITEM),
),
)
}
composeTestRule.assertNoDialogExists()
composeTestRule
.onNodeWithContentDescription("Options")
.assertIsDisplayed()
.performClick()
composeTestRule
.onNodeWithText("Delete")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onNodeWithText("Yes")
.assert(hasAnyAncestor(isDialog()))
.performClick()
verify {
viewModel.trySendAction(SendAction.DeleteSendClick(DEFAULT_SEND_ITEM))
}
composeTestRule.assertNoDialogExists()
}
@Test
fun `on send item overflow dialog cancel click should close the dialog`() {
mutableStateFlow.update {
@ -525,6 +619,7 @@ private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem =
type = SendState.ViewState.Content.SendItem.Type.FILE,
iconList = emptyList(),
shareUrl = "www.test.com/#/send/mockAccessId-1/mockKey-1",
hasPassword = true,
)
private val DEFAULT_CONTENT_VIEW_STATE: SendState.ViewState.Content = SendState.ViewState.Content(
@ -539,6 +634,7 @@ private val DEFAULT_CONTENT_VIEW_STATE: SendState.ViewState.Content = SendState.
type = SendState.ViewState.Content.SendItem.Type.TEXT,
iconList = emptyList(),
shareUrl = "www.test.com/#/send/mockAccessId-1/mockKey-1",
hasPassword = true,
),
),
)

View file

@ -107,6 +107,26 @@ class SendViewModelTest : BaseViewModelTest() {
}
}
@Test
fun `DeleteSendClick should emit ShowToast`() = runTest {
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.DeleteSendClick(sendItem))
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
}
}
@Test
fun `RemovePasswordClick should emit ShowToast`() = runTest {
val sendItem = mockk<SendState.ViewState.Content.SendItem>()
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(SendAction.RemovePasswordClick(sendItem))
assertEquals(SendEvent.ShowToast("Not yet implemented".asText()), awaitItem())
}
}
@Test
fun `SyncClick should call sync`() {
val viewModel = createViewModel()

View file

@ -72,6 +72,7 @@ class SendDataExtensionsTest {
SendStatusIcon.PENDING_DELETE,
),
shareUrl = "www.test.com/#/send/mockAccessId-1/mockKey-1",
hasPassword = true,
),
SendState.ViewState.Content.SendItem(
id = "mockId-2",
@ -85,6 +86,7 @@ class SendDataExtensionsTest {
SendStatusIcon.PENDING_DELETE,
),
shareUrl = "www.test.com/#/send/mockAccessId-2/mockKey-2",
hasPassword = true,
),
),
),