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() },
onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) },
onNavigateToAttachments = { navController.navigateToAttachment(it) },
onNavigateToMoveToOrganization = {
navController.navigateToVaultMoveToOrganization(it)
},
)
vaultMoveToOrganizationDestination(
onNavigateBack = { navController.popBackStack() },

View file

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

View file

@ -54,6 +54,7 @@ fun VaultAddEditScreen(
onNavigateToManualCodeEntryScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String) -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
@ -78,7 +79,13 @@ fun VaultAddEditScreen(
}
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()
}
}
@ -140,6 +147,28 @@ fun VaultAddEditScreen(
},
)
.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.MoveToOrganizationClick -> handleMoveToOrganizationClick()
is VaultAddEditAction.Common.CollectionsClick -> handleCollectionsClick()
is VaultAddEditAction.Common.CloseClick -> handleCloseClick()
is VaultAddEditAction.Common.DismissDialog -> handleDismissDialog()
is VaultAddEditAction.Common.SaveClick -> handleSaveClick()
@ -262,6 +264,14 @@ class VaultAddEditViewModel @Inject constructor(
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() {
sendEvent(
event = VaultAddEditEvent.NavigateBack,
@ -1136,6 +1146,17 @@ data class VaultAddEditState(
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.
*/
@ -1442,6 +1463,20 @@ sealed class VaultAddEditEvent {
val cipherId: String,
) : 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.
*/
@ -1491,6 +1526,16 @@ sealed class VaultAddEditAction {
*/
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.
*

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.test.hasAnyAncestor
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.hasSetTextAction
import androidx.compose.ui.test.isDialog
import androidx.compose.ui.test.isPopup
import androidx.compose.ui.test.onAllNodesWithContentDescription
import androidx.compose.ui.test.onAllNodesWithText
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.performTouchInput
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.util.asText
import com.x8bit.bitwarden.ui.platform.manager.permissions.FakePermissionManager
@ -61,6 +63,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
private var onNavigateToManualCodeEntryScreenCalled = false
private var onNavigateToGeneratorModalType: GeneratorMode.Modal? = null
private var onNavigateToAttachmentsId: String? = null
private var onNavigateToMoveToOrganizationId: String? = null
private val mutableEventFlow = bufferedMutableSharedFlow<VaultAddEditEvent>()
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE_LOGIN)
@ -83,6 +86,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
},
onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it },
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationId = it },
viewModel = viewModel,
permissionsManager = fakePermissionManager,
)
@ -127,6 +131,14 @@ class VaultAddEditScreenTest : BaseComposeTest() {
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")
@Test
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
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
fun `in add mode, SaveClick should show dialog, and remove it once an item is saved`() =
runTest {