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) }, onCopyClick = { sendHandlers.onCopySendClick(it) },
onEditClick = { sendHandlers.onEditSendClick(it) }, onEditClick = { sendHandlers.onEditSendClick(it) },
onShareClick = { sendHandlers.onShareSendClick(it) }, onShareClick = { sendHandlers.onShareSendClick(it) },
onDeleteClick = { sendHandlers.onDeleteSendClick(it) },
onRemovePasswordClick = if (it.hasPassword) {
{ sendHandlers.onRemovePasswordClick(it) }
} else {
null
},
modifier = Modifier modifier = Modifier
.padding( .padding(
start = 16.dp, start = 16.dp,

View file

@ -1,6 +1,10 @@
package com.x8bit.bitwarden.ui.tools.feature.send package com.x8bit.bitwarden.ui.tools.feature.send
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -8,11 +12,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem 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.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme 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 com.x8bit.bitwarden.ui.tools.feature.send.model.SendStatusIcon
import kotlinx.collections.immutable.persistentListOf
/** /**
* A Composable function that displays a row send item. * 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 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 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 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. * @param modifier An optional [Modifier] for this Composable, defaulting to an empty Modifier.
* This allows the caller to specify things like padding, size, etc. * This allows the caller to specify things like padding, size, etc.
*/ */
@ -38,8 +46,11 @@ fun SendListItem(
onEditClick: () -> Unit, onEditClick: () -> Unit,
onCopyClick: () -> Unit, onCopyClick: () -> Unit,
onShareClick: () -> Unit, onShareClick: () -> Unit,
onDeleteClick: () -> Unit,
onRemovePasswordClick: (() -> Unit)?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var shouldShowDeleteConfirmationDialog by rememberSaveable { mutableStateOf(false) }
BitwardenListItem( BitwardenListItem(
label = label, label = label,
supportingLabel = supportingLabel, supportingLabel = supportingLabel,
@ -51,7 +62,7 @@ fun SendListItem(
) )
}, },
onClick = onClick, onClick = onClick,
selectionDataList = persistentListOf( selectionDataList = persistentListOfNotNull(
SelectionItemData( SelectionItemData(
text = stringResource(id = R.string.edit), text = stringResource(id = R.string.edit),
onClick = onEditClick, onClick = onEditClick,
@ -64,9 +75,33 @@ fun SendListItem(
text = stringResource(id = R.string.share_link), text = stringResource(id = R.string.share_link),
onClick = onShareClick, 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, 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) @Preview(showBackground = true)
@ -82,6 +117,8 @@ private fun SendListItem_preview() {
onCopyClick = {}, onCopyClick = {},
onEditClick = {}, onEditClick = {},
onShareClick = {}, onShareClick = {},
onDeleteClick = {},
onRemovePasswordClick = null,
) )
} }
} }

View file

@ -67,6 +67,8 @@ class SendViewModel @Inject constructor(
is SendAction.SendClick -> handleSendClick(action) is SendAction.SendClick -> handleSendClick(action)
is SendAction.ShareClick -> handleShareClick(action) is SendAction.ShareClick -> handleShareClick(action)
SendAction.TextTypeClick -> handleTextTypeClick() SendAction.TextTypeClick -> handleTextTypeClick()
is SendAction.DeleteSendClick -> handleDeleteSendClick(action)
is SendAction.RemovePasswordClick -> handleRemovePasswordClick(action)
is SendAction.Internal -> handleInternalAction(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) // TODO: Navigate to the text type send list screen (BIT-1388)
sendEvent(SendEvent.ShowToast("Not yet implemented".asText())) 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 type: Type,
val iconList: List<SendStatusIcon>, val iconList: List<SendStatusIcon>,
val shareUrl: String, val shareUrl: String,
val hasPassword: Boolean,
) : Parcelable { ) : Parcelable {
/** /**
* Indicates the type of send this, a text or file. * Indicates the type of send this, a text or file.
@ -345,6 +358,20 @@ sealed class SendAction {
val sendItem: SendState.ViewState.Content.SendItem, val sendItem: SendState.ViewState.Content.SendItem,
) : SendAction() ) : 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. * 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 onEditSendClick: (SendState.ViewState.Content.SendItem) -> Unit,
val onCopySendClick: (SendState.ViewState.Content.SendItem) -> Unit, val onCopySendClick: (SendState.ViewState.Content.SendItem) -> Unit,
val onShareSendClick: (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 { companion object {
/** /**
@ -30,6 +32,10 @@ data class SendHandlers(
onEditSendClick = { viewModel.trySendAction(SendAction.SendClick(it)) }, onEditSendClick = { viewModel.trySendAction(SendAction.SendClick(it)) },
onCopySendClick = { viewModel.trySendAction(SendAction.CopyClick(it)) }, onCopySendClick = { viewModel.trySendAction(SendAction.CopyClick(it)) },
onShareSendClick = { viewModel.trySendAction(SendAction.ShareClick(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), shareUrl = sendView.toSendUrl(baseWebSendUrl),
hasPassword = sendView.hasPassword,
) )
} }
.sortedBy { it.name }, .sortedBy { it.name },

View file

@ -470,6 +470,100 @@ class SendScreenTest : BaseComposeTest() {
composeTestRule.assertNoDialogExists() 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 @Test
fun `on send item overflow dialog cancel click should close the dialog`() { fun `on send item overflow dialog cancel click should close the dialog`() {
mutableStateFlow.update { mutableStateFlow.update {
@ -525,6 +619,7 @@ private val DEFAULT_SEND_ITEM: SendState.ViewState.Content.SendItem =
type = SendState.ViewState.Content.SendItem.Type.FILE, type = SendState.ViewState.Content.SendItem.Type.FILE,
iconList = emptyList(), iconList = emptyList(),
shareUrl = "www.test.com/#/send/mockAccessId-1/mockKey-1", shareUrl = "www.test.com/#/send/mockAccessId-1/mockKey-1",
hasPassword = true,
) )
private val DEFAULT_CONTENT_VIEW_STATE: SendState.ViewState.Content = SendState.ViewState.Content( 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, type = SendState.ViewState.Content.SendItem.Type.TEXT,
iconList = emptyList(), iconList = emptyList(),
shareUrl = "www.test.com/#/send/mockAccessId-1/mockKey-1", 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 @Test
fun `SyncClick should call sync`() { fun `SyncClick should call sync`() {
val viewModel = createViewModel() val viewModel = createViewModel()

View file

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