BIT-1410: Move to organization from edit item screen (#760)

This commit is contained in:
Ramsey Smith 2024-01-24 15:50:47 -07:00 committed by Álison Fernandes
parent 7a416de9c9
commit c977f7617a
6 changed files with 199 additions and 1 deletions

View file

@ -93,6 +93,9 @@ fun NavGraphBuilder.vaultUnlockedGraph(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },
onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) }, onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) },
onNavigateToAttachments = { navController.navigateToAttachment(it) }, onNavigateToAttachments = { navController.navigateToAttachment(it) },
onNavigateToMoveToOrganization = {
navController.navigateToVaultMoveToOrganization(it)
},
) )
vaultMoveToOrganizationDestination( vaultMoveToOrganizationDestination(
onNavigateBack = { navController.popBackStack() }, onNavigateBack = { navController.popBackStack() },

View file

@ -42,12 +42,14 @@ data class VaultAddEditArgs(
/** /**
* Add the vault add & edit screen to the nav graph. * Add the vault add & edit screen to the nav graph.
*/ */
@Suppress("LongParameterList")
fun NavGraphBuilder.vaultAddEditDestination( fun NavGraphBuilder.vaultAddEditDestination(
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onNavigateToManualCodeEntryScreen: () -> Unit, onNavigateToManualCodeEntryScreen: () -> Unit,
onNavigateToQrCodeScanScreen: () -> Unit, onNavigateToQrCodeScanScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit, onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit, onNavigateToAttachments: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String) -> Unit,
) { ) {
composableWithSlideTransitions( composableWithSlideTransitions(
route = ADD_EDIT_ITEM_ROUTE, route = ADD_EDIT_ITEM_ROUTE,
@ -61,6 +63,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen, onNavigateToQrCodeScanScreen = onNavigateToQrCodeScanScreen,
onNavigateToGeneratorModal = onNavigateToGeneratorModal, onNavigateToGeneratorModal = onNavigateToGeneratorModal,
onNavigateToAttachments = onNavigateToAttachments, onNavigateToAttachments = onNavigateToAttachments,
onNavigateToMoveToOrganization = onNavigateToMoveToOrganization,
) )
} }
} }

View file

@ -54,6 +54,7 @@ fun VaultAddEditScreen(
onNavigateToManualCodeEntryScreen: () -> Unit, onNavigateToManualCodeEntryScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit, onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit, onNavigateToAttachments: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String) -> Unit,
) { ) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle() val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current val context = LocalContext.current
@ -78,7 +79,13 @@ fun VaultAddEditScreen(
} }
is VaultAddEditEvent.NavigateToAttachments -> onNavigateToAttachments(event.cipherId) is VaultAddEditEvent.NavigateToAttachments -> onNavigateToAttachments(event.cipherId)
is VaultAddEditEvent.NavigateToMoveToOrganization -> {
onNavigateToMoveToOrganization(event.cipherId)
}
is VaultAddEditEvent.NavigateToCollections -> {
// TODO implement Collections in BIT-1575
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
}
VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke() VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke()
} }
} }
@ -140,6 +147,28 @@ fun VaultAddEditScreen(
}, },
) )
.takeUnless { state.isAddItemMode }, .takeUnless { state.isAddItemMode },
OverflowMenuItemData(
text = stringResource(id = R.string.move_to_organization),
onClick = remember(viewModel) {
{
viewModel.trySendAction(
VaultAddEditAction.Common.MoveToOrganizationClick,
)
}
},
)
.takeUnless { state.isAddItemMode || state.isCipherInCollection },
OverflowMenuItemData(
text = stringResource(id = R.string.collections),
onClick = remember(viewModel) {
{
viewModel.trySendAction(
VaultAddEditAction.Common.CollectionsClick,
)
}
},
)
.takeUnless { state.isAddItemMode || !state.isCipherInCollection },
), ),
) )
}, },

View file

@ -142,6 +142,8 @@ class VaultAddEditViewModel @Inject constructor(
} }
is VaultAddEditAction.Common.AttachmentsClick -> handleAttachmentsClick() is VaultAddEditAction.Common.AttachmentsClick -> handleAttachmentsClick()
is VaultAddEditAction.Common.MoveToOrganizationClick -> handleMoveToOrganizationClick()
is VaultAddEditAction.Common.CollectionsClick -> handleCollectionsClick()
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()
@ -262,6 +264,14 @@ class VaultAddEditViewModel @Inject constructor(
onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) } onEdit { sendEvent(VaultAddEditEvent.NavigateToAttachments(it.vaultItemId)) }
} }
private fun handleMoveToOrganizationClick() {
onEdit { sendEvent(VaultAddEditEvent.NavigateToMoveToOrganization(it.vaultItemId)) }
}
private fun handleCollectionsClick() {
onEdit { sendEvent(VaultAddEditEvent.NavigateToCollections(it.vaultItemId)) }
}
private fun handleCloseClick() { private fun handleCloseClick() {
sendEvent( sendEvent(
event = VaultAddEditEvent.NavigateBack, event = VaultAddEditEvent.NavigateBack,
@ -1136,6 +1146,17 @@ data class VaultAddEditState(
is VaultAddEditType.CloneItem -> R.string.add_item.asText() is VaultAddEditType.CloneItem -> R.string.add_item.asText()
} }
/**
* Whether or not the cipher is in a collection.
*/
val isCipherInCollection: Boolean
get() = (viewState as? ViewState.Content)
?.common
?.originalCipher
?.collectionIds
?.isNotEmpty()
?: false
/** /**
* Helper to determine if the UI should display the content in add item mode. * Helper to determine if the UI should display the content in add item mode.
*/ */
@ -1442,6 +1463,20 @@ sealed class VaultAddEditEvent {
val cipherId: String, val cipherId: String,
) : VaultAddEditEvent() ) : VaultAddEditEvent()
/**
* Navigates to the move to organization screen.
*/
data class NavigateToMoveToOrganization(
val cipherId: String,
) : VaultAddEditEvent()
/**
* Navigates to the collections screen.
*/
data class NavigateToCollections(
val cipherId: String,
) : VaultAddEditEvent()
/** /**
* Navigate to the QR code scan screen. * Navigate to the QR code scan screen.
*/ */
@ -1491,6 +1526,16 @@ sealed class VaultAddEditAction {
*/ */
data object AttachmentsClick : Common() data object AttachmentsClick : Common()
/**
* The user has clicked the move to organization overflow option.
*/
data object MoveToOrganizationClick : Common()
/**
* The user has clicked the collections overflow option.
*/
data object CollectionsClick : Common()
/** /**
* Represents the action when a type option is selected. * Represents the action when a type option is selected.
* *

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasContentDescription import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasSetTextAction import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.isDialog import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.isPopup
import androidx.compose.ui.test.onAllNodesWithContentDescription import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst import androidx.compose.ui.test.onFirst
@ -28,6 +29,7 @@ import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.performTouchInput
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
@ -61,6 +63,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
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 var onNavigateToAttachmentsId: String? = null
private var onNavigateToMoveToOrganizationId: 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)
@ -83,6 +86,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}, },
onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it }, onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it },
onNavigateToAttachments = { onNavigateToAttachmentsId = it }, onNavigateToAttachments = { onNavigateToAttachmentsId = it },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationId = it },
viewModel = viewModel, viewModel = viewModel,
permissionsManager = fakePermissionManager, permissionsManager = fakePermissionManager,
) )
@ -127,6 +131,14 @@ class VaultAddEditScreenTest : BaseComposeTest() {
assertEquals(cipherId, onNavigateToAttachmentsId) assertEquals(cipherId, onNavigateToAttachmentsId)
} }
@Test
@Suppress("MaxLineLength")
fun `on NavigateToMoveToOrganization event should invoke onNavigateToMoveToOrganization with the correct ID`() {
val cipherId = "cipherId-1234"
mutableEventFlow.tryEmit(VaultAddEditEvent.NavigateToMoveToOrganization(cipherId))
assertEquals(cipherId, onNavigateToMoveToOrganizationId)
}
@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 `() {
@ -2104,6 +2116,74 @@ class VaultAddEditScreenTest : BaseComposeTest() {
} }
} }
@Test
fun `Menu should display correct items when cipher is in a collection`() {
mutableStateFlow.update {
it.copy(
vaultAddEditType = VaultAddEditType.EditItem(vaultItemId = "mockId-1"),
viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
originalCipher = createMockCipherView(1),
),
type = VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
),
)
}
composeTestRule
.onNodeWithContentDescription("More")
.performClick()
composeTestRule
.onAllNodesWithText("Attachments")
.filterToOne(hasAnyAncestor(isPopup()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Collections")
.filterToOne(hasAnyAncestor(isPopup()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Move to Organization")
.filterToOne(hasAnyAncestor(isPopup()))
.assertDoesNotExist()
}
@Test
fun `Menu should display correct items when cipher is not in a collection`() {
mutableStateFlow.update {
it.copy(
vaultAddEditType = VaultAddEditType.EditItem(vaultItemId = "mockId-1"),
viewState = VaultAddEditState.ViewState.Content(
common = VaultAddEditState.ViewState.Content.Common(
originalCipher = createMockCipherView(1).copy(
collectionIds = emptyList(),
),
),
type = VaultAddEditState.ViewState.Content.ItemType.SecureNotes,
),
)
}
composeTestRule
.onNodeWithContentDescription("More")
.performClick()
composeTestRule
.onAllNodesWithText("Attachments")
.filterToOne(hasAnyAncestor(isPopup()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Move to Organization")
.filterToOne(hasAnyAncestor(isPopup()))
.assertIsDisplayed()
composeTestRule
.onAllNodesWithText("Collections")
.filterToOne(hasAnyAncestor(isPopup()))
.assertDoesNotExist()
}
//region Helper functions //region Helper functions
private fun updateLoginType( private fun updateLoginType(

View file

@ -180,6 +180,44 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
} }
} }
@Test
fun `MoveToOrganizationClick should emit NavigateToMoveToOrganization`() = 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.MoveToOrganizationClick)
assertEquals(
VaultAddEditEvent.NavigateToMoveToOrganization(DEFAULT_EDIT_ITEM_ID),
awaitItem(),
)
}
}
@Test
fun `CollectionsClick should emit NavigateToCollections`() = 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.CollectionsClick)
assertEquals(
VaultAddEditEvent.NavigateToCollections(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 {