mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Add initial attachments screen shell (#745)
This commit is contained in:
parent
30ab22f826
commit
96201fd34c
15 changed files with 454 additions and 8 deletions
|
@ -24,6 +24,8 @@ import com.x8bit.bitwarden.ui.tools.feature.send.addsend.model.AddSendType
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend
|
import com.x8bit.bitwarden.ui.tools.feature.send.addsend.navigateToAddSend
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.navigateToVaultAddEdit
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.vaultAddEditDestination
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.attachmentDestination
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.navigateToAttachment
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
|
import com.x8bit.bitwarden.ui.vault.feature.item.navigateToVaultItem
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
|
import com.x8bit.bitwarden.ui.vault.feature.item.vaultItemDestination
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen
|
import com.x8bit.bitwarden.ui.vault.feature.manualcodeentry.navigateToManualCodeEntryScreen
|
||||||
|
@ -90,6 +92,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
},
|
},
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) },
|
onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) },
|
||||||
|
onNavigateToAttachments = { navController.navigateToAttachment(it) },
|
||||||
)
|
)
|
||||||
vaultMoveToOrganizationDestination(
|
vaultMoveToOrganizationDestination(
|
||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
@ -108,6 +111,7 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
onNavigateToMoveToOrganization = {
|
onNavigateToMoveToOrganization = {
|
||||||
navController.navigateToVaultMoveToOrganization(it)
|
navController.navigateToVaultMoveToOrganization(it)
|
||||||
},
|
},
|
||||||
|
onNavigateToAttachments = { navController.navigateToAttachment(it) },
|
||||||
)
|
)
|
||||||
vaultQrCodeScanDestination(
|
vaultQrCodeScanDestination(
|
||||||
onNavigateToManualCodeEntryScreen = {
|
onNavigateToManualCodeEntryScreen = {
|
||||||
|
@ -136,6 +140,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
|
||||||
},
|
},
|
||||||
onNavigateToViewCipher = { navController.navigateToVaultItem(it) },
|
onNavigateToViewCipher = { navController.navigateToVaultItem(it) },
|
||||||
)
|
)
|
||||||
|
attachmentDestination(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
|
||||||
onNavigateToManualCodeEntryScreen: () -> Unit,
|
onNavigateToManualCodeEntryScreen: () -> Unit,
|
||||||
onNavigateToQrCodeScanScreen: () -> Unit,
|
onNavigateToQrCodeScanScreen: () -> Unit,
|
||||||
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
|
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
|
||||||
|
onNavigateToAttachments: (cipherId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
route = ADD_EDIT_ITEM_ROUTE,
|
route = ADD_EDIT_ITEM_ROUTE,
|
||||||
|
@ -59,6 +60,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
|
||||||
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
|
onNavigateToManualCodeEntryScreen = onNavigateToManualCodeEntryScreen,
|
||||||
onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen,
|
onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen,
|
||||||
onNavigateToGeneratorModal = onNavigateToGeneratorModal,
|
onNavigateToGeneratorModal = onNavigateToGeneratorModal,
|
||||||
|
onNavigateToAttachments = onNavigateToAttachments,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,15 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenErrorContent
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingContent
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenLoadingDialog
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenOverflowActionItem
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
import com.x8bit.bitwarden.ui.platform.components.LoadingDialogState
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.OverflowMenuItemData
|
||||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||||
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
|
import com.x8bit.bitwarden.ui.platform.theme.LocalPermissionsManager
|
||||||
|
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
|
||||||
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
|
||||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
|
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCommonHandlers
|
||||||
|
@ -50,6 +53,7 @@ fun VaultAddEditScreen(
|
||||||
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
permissionsManager: PermissionsManager = LocalPermissionsManager.current,
|
||||||
onNavigateToManualCodeEntryScreen: () -> Unit,
|
onNavigateToManualCodeEntryScreen: () -> Unit,
|
||||||
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
|
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
|
||||||
|
onNavigateToAttachments: (cipherId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -73,6 +77,8 @@ fun VaultAddEditScreen(
|
||||||
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultAddEditEvent.NavigateToAttachments -> onNavigateToAttachments(event.cipherId)
|
||||||
|
|
||||||
VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke()
|
VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,6 +127,21 @@ fun VaultAddEditScreen(
|
||||||
{ viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) }
|
{ viewModel.trySendAction(VaultAddEditAction.Common.SaveClick) }
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
BitwardenOverflowActionItem(
|
||||||
|
menuItemDataList = persistentListOfNotNull(
|
||||||
|
OverflowMenuItemData(
|
||||||
|
text = stringResource(id = R.string.attachments),
|
||||||
|
onClick = remember(viewModel) {
|
||||||
|
{
|
||||||
|
viewModel.trySendAction(
|
||||||
|
VaultAddEditAction.Common.AttachmentsClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.takeUnless { state.isAddItemMode },
|
||||||
|
),
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -141,6 +141,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
handleToggleMasterPasswordReprompt(action)
|
handleToggleMasterPasswordReprompt(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is VaultAddEditAction.Common.AttachmentsClick -> handleAttachmentsClick()
|
||||||
is VaultAddEditAction.Common.CloseClick -> handleCloseClick()
|
is VaultAddEditAction.Common.CloseClick -> handleCloseClick()
|
||||||
is VaultAddEditAction.Common.DismissDialog -> handleDismissDialog()
|
is VaultAddEditAction.Common.DismissDialog -> handleDismissDialog()
|
||||||
is VaultAddEditAction.Common.SaveClick -> handleSaveClick()
|
is VaultAddEditAction.Common.SaveClick -> handleSaveClick()
|
||||||
|
@ -257,6 +258,10 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleAttachmentsClick() {
|
||||||
|
onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleCloseClick() {
|
private fun handleCloseClick() {
|
||||||
sendEvent(
|
sendEvent(
|
||||||
event = VaultAddEditEvent.NavigateBack,
|
event = VaultAddEditEvent.NavigateBack,
|
||||||
|
@ -1007,6 +1012,12 @@ class VaultAddEditViewModel @Inject constructor(
|
||||||
(state.viewState as? VaultAddEditState.ViewState.Content)?.let(block)
|
(state.viewState as? VaultAddEditState.ViewState.Content)?.let(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun onEdit(
|
||||||
|
crossinline block: (VaultAddEditType.EditItem) -> Unit,
|
||||||
|
) {
|
||||||
|
(state.vaultAddEditType as? VaultAddEditType.EditItem)?.let(block)
|
||||||
|
}
|
||||||
|
|
||||||
private inline fun updateContent(
|
private inline fun updateContent(
|
||||||
crossinline block: (
|
crossinline block: (
|
||||||
VaultAddEditState.ViewState.Content,
|
VaultAddEditState.ViewState.Content,
|
||||||
|
@ -1424,6 +1435,13 @@ sealed class VaultAddEditEvent {
|
||||||
*/
|
*/
|
||||||
data object NavigateBack : VaultAddEditEvent()
|
data object NavigateBack : VaultAddEditEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to attachments screen.
|
||||||
|
*/
|
||||||
|
data class NavigateToAttachments(
|
||||||
|
val cipherId: String,
|
||||||
|
) : VaultAddEditEvent()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to the QR code scan screen.
|
* Navigate to the QR code scan screen.
|
||||||
*/
|
*/
|
||||||
|
@ -1468,6 +1486,11 @@ sealed class VaultAddEditAction {
|
||||||
*/
|
*/
|
||||||
data object DismissDialog : Common()
|
data object DismissDialog : Common()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has clicked the attachments overflow option.
|
||||||
|
*/
|
||||||
|
data object AttachmentsClick : Common()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the action when a type option is selected.
|
* Represents the action when a type option is selected.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.attachments
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||||
|
|
||||||
|
private const val ATTACHMENTS_CIPHER_ID = "cipher_id"
|
||||||
|
private const val ATTACHMENTS_ROUTE_PREFIX = "attachments"
|
||||||
|
private const val ATTACHMENTS_ROUTE = "$ATTACHMENTS_ROUTE_PREFIX/{$ATTACHMENTS_CIPHER_ID}"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to retrieve arguments from the [SavedStateHandle].
|
||||||
|
*/
|
||||||
|
@OmitFromCoverage
|
||||||
|
data class AttachmentsArgs(val cipherId: String) {
|
||||||
|
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||||
|
cipherId = checkNotNull(savedStateHandle.get<String>(ATTACHMENTS_CIPHER_ID)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the attachments screen to the nav graph.
|
||||||
|
*/
|
||||||
|
fun NavGraphBuilder.attachmentDestination(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
composableWithSlideTransitions(
|
||||||
|
route = ATTACHMENTS_ROUTE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(ATTACHMENTS_CIPHER_ID) { type = NavType.StringType },
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
AttachmentsScreen(
|
||||||
|
onNavigateBack = onNavigateBack,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to the attachments screen.
|
||||||
|
*/
|
||||||
|
fun NavController.navigateToAttachment(
|
||||||
|
cipherId: String,
|
||||||
|
navOptions: NavOptions? = null,
|
||||||
|
) {
|
||||||
|
navigate(
|
||||||
|
route = "$ATTACHMENTS_ROUTE_PREFIX/$cipherId",
|
||||||
|
navOptions = navOptions,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.attachments
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.x8bit.bitwarden.R
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenScaffold
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButton
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.BitwardenTopAppBar
|
||||||
|
import com.x8bit.bitwarden.ui.platform.components.NavigationIcon
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.handlers.AttachmentsHandlers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the attachments screen.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AttachmentsScreen(
|
||||||
|
viewModel: AttachmentsViewModel = hiltViewModel(),
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
|
val context = LocalContext.current
|
||||||
|
EventsEffect(viewModel = viewModel) { event ->
|
||||||
|
when (event) {
|
||||||
|
AttachmentsEvent.NavigateBack -> onNavigateBack()
|
||||||
|
|
||||||
|
is AttachmentsEvent.ShowToast -> {
|
||||||
|
Toast
|
||||||
|
.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val attachmentHandlers = remember(viewModel) { AttachmentsHandlers.create(viewModel) }
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
BitwardenScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.fillMaxSize(),
|
||||||
|
topBar = {
|
||||||
|
BitwardenTopAppBar(
|
||||||
|
title = stringResource(id = R.string.attachments),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = NavigationIcon(
|
||||||
|
navigationIcon = painterResource(id = R.drawable.ic_back),
|
||||||
|
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||||
|
onNavigationIconClick = attachmentHandlers.onBackClick,
|
||||||
|
),
|
||||||
|
actions = {
|
||||||
|
BitwardenTextButton(
|
||||||
|
label = stringResource(id = R.string.save),
|
||||||
|
onClick = attachmentHandlers.onSaveClick,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(innerPadding),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Text(text = "Not Yet Implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.attachments
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val KEY_STATE = "state"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ViewModel responsible for handling user interactions in the attachments screen.
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class AttachmentsViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
) : BaseViewModel<AttachmentsState, AttachmentsEvent, AttachmentsAction>(
|
||||||
|
// We load the state from the savedStateHandle for testing purposes.
|
||||||
|
initialState = savedStateHandle[KEY_STATE]
|
||||||
|
?: AttachmentsState(
|
||||||
|
cipherId = AttachmentsArgs(savedStateHandle).cipherId,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
override fun handleAction(action: AttachmentsAction) {
|
||||||
|
when (action) {
|
||||||
|
AttachmentsAction.BackClick -> handleBackClick()
|
||||||
|
AttachmentsAction.SaveClick -> handleSaveClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleBackClick() {
|
||||||
|
sendEvent(AttachmentsEvent.NavigateBack)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleSaveClick() {
|
||||||
|
sendEvent(AttachmentsEvent.ShowToast("Not Yet Implemented".asText()))
|
||||||
|
// TODO: Handle saving the attachments (BIT-522)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state for viewing attachments.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class AttachmentsState(
|
||||||
|
val cipherId: String,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a set of events related attachments.
|
||||||
|
*/
|
||||||
|
sealed class AttachmentsEvent {
|
||||||
|
/**
|
||||||
|
* Navigates back.
|
||||||
|
*/
|
||||||
|
data object NavigateBack : AttachmentsEvent()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the given [message] as a toast.
|
||||||
|
*/
|
||||||
|
data class ShowToast(
|
||||||
|
val message: Text,
|
||||||
|
) : AttachmentsEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a set of actions related to attachments.
|
||||||
|
*/
|
||||||
|
sealed class AttachmentsAction {
|
||||||
|
/**
|
||||||
|
* User clicked the back button.
|
||||||
|
*/
|
||||||
|
data object BackClick : AttachmentsAction()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User clicked the save button.
|
||||||
|
*/
|
||||||
|
data object SaveClick : AttachmentsAction()
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.attachments.handlers
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsAction
|
||||||
|
import com.x8bit.bitwarden.ui.vault.feature.attachments.AttachmentsViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection of handler functions for managing actions within the context of viewing attachments.
|
||||||
|
*/
|
||||||
|
data class AttachmentsHandlers(
|
||||||
|
val onBackClick: () -> Unit,
|
||||||
|
val onSaveClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates the [AttachmentsHandlers] using the [AttachmentsViewModel] to send desired
|
||||||
|
* actions.
|
||||||
|
*/
|
||||||
|
fun create(viewModel: AttachmentsViewModel): AttachmentsHandlers =
|
||||||
|
AttachmentsHandlers(
|
||||||
|
onBackClick = { viewModel.trySendAction(AttachmentsAction.BackClick) },
|
||||||
|
onSaveClick = { viewModel.trySendAction(AttachmentsAction.SaveClick) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ fun NavGraphBuilder.vaultItemDestination(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
||||||
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
||||||
|
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
composableWithSlideTransitions(
|
composableWithSlideTransitions(
|
||||||
route = VAULT_ITEM_ROUTE,
|
route = VAULT_ITEM_ROUTE,
|
||||||
|
@ -41,6 +42,7 @@ fun NavGraphBuilder.vaultItemDestination(
|
||||||
onNavigateBack = onNavigateBack,
|
onNavigateBack = onNavigateBack,
|
||||||
onNavigateToVaultAddEditItem = onNavigateToVaultEditItem,
|
onNavigateToVaultAddEditItem = onNavigateToVaultEditItem,
|
||||||
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
|
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
|
||||||
|
onNavigateToAttachments = onNavigateToAttachments,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,7 @@ fun VaultItemScreen(
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
|
||||||
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
|
||||||
|
onNavigateToAttachments: (vaultItemId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
@ -90,10 +91,7 @@ fun VaultItemScreen(
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
|
is VaultItemEvent.NavigateToUri -> intentManager.launchUri(event.uri.toUri())
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToAttachments -> {
|
is VaultItemEvent.NavigateToAttachments -> onNavigateToAttachments(event.itemId)
|
||||||
// TODO implement attachments in BIT-522
|
|
||||||
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
is VaultItemEvent.NavigateToMoveToOrganization -> {
|
is VaultItemEvent.NavigateToMoveToOrganization -> {
|
||||||
onNavigateToMoveToOrganization(event.itemId)
|
onNavigateToMoveToOrganization(event.itemId)
|
||||||
|
|
|
@ -60,6 +60,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
private var onNavigateQrCodeScanScreenCalled = false
|
private var onNavigateQrCodeScanScreenCalled = false
|
||||||
private var onNavigateToManualCodeEntryScreenCalled = false
|
private var onNavigateToManualCodeEntryScreenCalled = false
|
||||||
private var onNavigateToGeneratorModalType: GeneratorMode.Modal? = null
|
private var onNavigateToGeneratorModalType: GeneratorMode.Modal? = null
|
||||||
|
private var onNavigateToAttachmentsId: String? = null
|
||||||
|
|
||||||
private val mutableEventFlow = bufferedMutableSharedFlow<VaultAddEditEvent>()
|
private val mutableEventFlow = bufferedMutableSharedFlow<VaultAddEditEvent>()
|
||||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN)
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN)
|
||||||
|
@ -76,13 +77,12 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
composeTestRule.setContent {
|
composeTestRule.setContent {
|
||||||
VaultAddEditScreen(
|
VaultAddEditScreen(
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
onNavigateToQrCodeScanScreen = {
|
onNavigateToQrCodeScanScreen = { onNavigateQrCodeScanScreenCalled = true },
|
||||||
onNavigateQrCodeScanScreenCalled = true
|
|
||||||
},
|
|
||||||
onNavigateToManualCodeEntryScreen = {
|
onNavigateToManualCodeEntryScreen = {
|
||||||
onNavigateToManualCodeEntryScreenCalled = true
|
onNavigateToManualCodeEntryScreenCalled = true
|
||||||
},
|
},
|
||||||
onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it },
|
onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it },
|
||||||
|
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
permissionsManager = fakePermissionManager,
|
permissionsManager = fakePermissionManager,
|
||||||
)
|
)
|
||||||
|
@ -120,6 +120,13 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||||
assertEquals(GeneratorMode.Modal.Password, onNavigateToGeneratorModalType)
|
assertEquals(GeneratorMode.Modal.Password, onNavigateToGeneratorModalType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `on NavigateToAttachments event should invoke onNavigateToAttachments`() {
|
||||||
|
val cipherId = "cipherId-1234"
|
||||||
|
mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToAttachments(cipherId))
|
||||||
|
assertEquals(cipherId, onNavigateToAttachmentsId)
|
||||||
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
@Test
|
@Test
|
||||||
fun `on NavigateToGeneratorModal event in username mode should invoke NavigateToGeneratorModal with Username Generator Mode `() {
|
fun `on NavigateToGeneratorModal event in username mode should invoke NavigateToGeneratorModal with Username Generator Mode `() {
|
||||||
|
|
|
@ -161,6 +161,25 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `AttachmentsClick should emit NavigateToAttachments`() = runTest {
|
||||||
|
val vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID)
|
||||||
|
val initState = createVaultAddItemState(vaultAddEditType = vaultAddEditType)
|
||||||
|
val viewModel = createAddVaultItemViewModel(
|
||||||
|
savedStateHandle = createSavedStateHandleWithState(
|
||||||
|
state = initState,
|
||||||
|
vaultAddEditType = vaultAddEditType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.actionChannel.trySend(VaultAddEditAction.Common.AttachmentsClick)
|
||||||
|
assertEquals(
|
||||||
|
VaultAddEditEvent.NavigateToAttachments(DEFAULT_EDIT_ITEM_ID),
|
||||||
|
awaitItem(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `in add mode, SaveClick should show dialog, and remove it once an item is saved`() =
|
fun `in add mode, SaveClick should show dialog, and remove it once an item is saved`() =
|
||||||
runTest {
|
runTest {
|
||||||
|
@ -1526,7 +1545,8 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||||
"TestId 1",
|
"TestId 1",
|
||||||
"Boolean Field",
|
"Boolean Field",
|
||||||
true,
|
true,
|
||||||
), VaultAddEditState.Custom.BooleanField(
|
),
|
||||||
|
VaultAddEditState.Custom.BooleanField(
|
||||||
"TestId 3",
|
"TestId 3",
|
||||||
"Boolean Field",
|
"Boolean Field",
|
||||||
true,
|
true,
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.attachments
|
||||||
|
|
||||||
|
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.junit.Assert
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AttachmentsScreenTest : BaseComposeTest() {
|
||||||
|
private var onNavigateBackCalled = false
|
||||||
|
|
||||||
|
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||||
|
private val mutableEventFlow = bufferedMutableSharedFlow<AttachmentsEvent>()
|
||||||
|
val viewModel: AttachmentsViewModel = mockk {
|
||||||
|
every { stateFlow } returns mutableStateFlow
|
||||||
|
every { eventFlow } returns mutableEventFlow
|
||||||
|
every { trySendAction(any()) } just runs
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
composeTestRule.setContent {
|
||||||
|
AttachmentsScreen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateBack should call onNavigateBack`() {
|
||||||
|
mutableEventFlow.tryEmit(AttachmentsEvent.NavigateBack)
|
||||||
|
Assert.assertTrue(onNavigateBackCalled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: AttachmentsState = AttachmentsState(
|
||||||
|
cipherId = "cipherId-1234",
|
||||||
|
)
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.x8bit.bitwarden.ui.vault.feature.attachments
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import app.cash.turbine.test
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||||
|
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class AttachmentsViewModelTest : BaseViewModelTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when state is null`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
assertEquals(DEFAULT_STATE, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `initial state should be correct when state is set`() = runTest {
|
||||||
|
val initialState = DEFAULT_STATE.copy(cipherId = "123456789")
|
||||||
|
val viewModel = createViewModel(initialState)
|
||||||
|
assertEquals(initialState, viewModel.stateFlow.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `BackClick should emit NavigateBack`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(AttachmentsAction.BackClick)
|
||||||
|
assertEquals(AttachmentsEvent.NavigateBack, awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SaveClick should emit ShowToast`() = runTest {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
viewModel.eventFlow.test {
|
||||||
|
viewModel.trySendAction(AttachmentsAction.SaveClick)
|
||||||
|
assertEquals(AttachmentsEvent.ShowToast("Not Yet Implemented".asText()), awaitItem())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createViewModel(
|
||||||
|
initialState: AttachmentsState? = null,
|
||||||
|
): AttachmentsViewModel = AttachmentsViewModel(
|
||||||
|
savedStateHandle = SavedStateHandle().apply {
|
||||||
|
set("state", initialState)
|
||||||
|
set("cipher_id", initialState?.cipherId ?: "cipherId-1234")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val DEFAULT_STATE: AttachmentsState = AttachmentsState(
|
||||||
|
cipherId = "cipherId-1234",
|
||||||
|
)
|
|
@ -57,6 +57,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
private var onNavigateBackCalled = false
|
private var onNavigateBackCalled = false
|
||||||
private var onNavigateToVaultEditItemId: String? = null
|
private var onNavigateToVaultEditItemId: String? = null
|
||||||
private var onNavigateToMoveToOrganizationItemId: String? = null
|
private var onNavigateToMoveToOrganizationItemId: String? = null
|
||||||
|
private var onNavigateToAttachmentsId: String? = null
|
||||||
|
|
||||||
private val intentManager = mockk<IntentManager>()
|
private val intentManager = mockk<IntentManager>()
|
||||||
|
|
||||||
|
@ -75,6 +76,7 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
onNavigateBack = { onNavigateBackCalled = true },
|
onNavigateBack = { onNavigateBackCalled = true },
|
||||||
onNavigateToVaultAddEditItem = { id, _ -> onNavigateToVaultEditItemId = id },
|
onNavigateToVaultAddEditItem = { id, _ -> onNavigateToVaultEditItemId = id },
|
||||||
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
|
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
|
||||||
|
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
|
||||||
intentManager = intentManager,
|
intentManager = intentManager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -95,6 +97,13 @@ class VaultItemScreenTest : BaseComposeTest() {
|
||||||
assertEquals(id, onNavigateToMoveToOrganizationItemId)
|
assertEquals(id, onNavigateToMoveToOrganizationItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `NavigateToMoveToOrganization event should invoke onNavigateToAttachments`() {
|
||||||
|
val id = "id1234"
|
||||||
|
mutableEventFlow.tryEmit(VaultItemEvent.NavigateToAttachments(itemId = id))
|
||||||
|
assertEquals(id, onNavigateToAttachmentsId)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `on close click should send CloseClick`() {
|
fun `on close click should send CloseClick`() {
|
||||||
composeTestRule.onNodeWithContentDescription(label = "Close").performClick()
|
composeTestRule.onNodeWithContentDescription(label = "Close").performClick()
|
||||||
|
|
Loading…
Reference in a new issue