mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1410: Move to organization from edit item screen (#760)
This commit is contained in:
parent
7a416de9c9
commit
c977f7617a
6 changed files with 199 additions and 1 deletions
|
@ -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() },
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue