BIT-1575: Collection Screen (#878)

This commit is contained in:
Ramsey Smith 2024-01-30 16:01:20 -07:00 committed by Álison Fernandes
parent 96401aba79
commit 034284fad4
14 changed files with 194 additions and 64 deletions

View file

@ -108,8 +108,11 @@ fun NavGraphBuilder.vaultUnlockedGraph(
onNavigateBack = { navController.popBackStack() },
onNavigateToGeneratorModal = { navController.navigateToGeneratorModal(mode = it) },
onNavigateToAttachments = { navController.navigateToAttachment(it) },
onNavigateToMoveToOrganization = {
navController.navigateToVaultMoveToOrganization(it)
onNavigateToMoveToOrganization = { vaultItemId, showOnlyCollections ->
navController.navigateToVaultMoveToOrganization(
vaultItemId = vaultItemId,
showOnlyCollections = showOnlyCollections,
)
},
)
vaultMoveToOrganizationDestination(
@ -126,8 +129,11 @@ fun NavGraphBuilder.vaultUnlockedGraph(
},
)
},
onNavigateToMoveToOrganization = {
navController.navigateToVaultMoveToOrganization(it)
onNavigateToMoveToOrganization = { vaultItemId, showOnlyCollections ->
navController.navigateToVaultMoveToOrganization(
vaultItemId = vaultItemId,
showOnlyCollections = showOnlyCollections,
)
},
onNavigateToAttachments = { navController.navigateToAttachment(it) },
)

View file

@ -24,15 +24,19 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCollection
fun LazyListScope.collectionItemsSelector(
collectionList: List<VaultCollection>?,
onCollectionSelect: (VaultCollection) -> Unit,
isCollectionsTitleVisible: Boolean = true,
) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.collections),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
if (isCollectionsTitleVisible) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenListHeaderText(
label = stringResource(id = R.string.collections),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}
if (collectionList?.isNotEmpty() == true) {
@ -54,14 +58,14 @@ fun LazyListScope.collectionItemsSelector(
Spacer(modifier = Modifier.height(8.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
Text(
text = stringResource(id = R.string.no_collections_to_list),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
}
}

View file

@ -49,7 +49,7 @@ fun NavGraphBuilder.vaultAddEditDestination(
onNavigateToQrCodeScanScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String, showOnlyCollections: Boolean) -> Unit,
) {
composableWithSlideTransitions(
route = ADD_EDIT_ITEM_ROUTE,

View file

@ -57,7 +57,7 @@ fun VaultAddEditScreen(
onNavigateToManualCodeEntryScreen: () -> Unit,
onNavigateToGeneratorModal: (GeneratorMode.Modal) -> Unit,
onNavigateToAttachments: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String) -> Unit,
onNavigateToMoveToOrganization: (cipherId: String, showOnlyCollections: Boolean) -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
@ -83,12 +83,11 @@ fun VaultAddEditScreen(
is VaultAddEditEvent.NavigateToAttachments -> onNavigateToAttachments(event.cipherId)
is VaultAddEditEvent.NavigateToMoveToOrganization -> {
onNavigateToMoveToOrganization(event.cipherId)
onNavigateToMoveToOrganization(event.cipherId, false)
}
is VaultAddEditEvent.NavigateToCollections -> {
// TODO implement Collections in BIT-1575
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
onNavigateToMoveToOrganization(event.cipherId, true)
}
VaultAddEditEvent.NavigateBack -> onNavigateBack.invoke()

View file

@ -29,7 +29,7 @@ data class VaultItemArgs(val vaultItemId: String) {
fun NavGraphBuilder.vaultItemDestination(
onNavigateBack: () -> Unit,
onNavigateToVaultEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
onNavigateToAttachments: (vaultItemId: String) -> Unit,
) {
composableWithSlideTransitions(

View file

@ -61,7 +61,7 @@ fun VaultItemScreen(
intentManager: IntentManager = LocalIntentManager.current,
onNavigateBack: () -> Unit,
onNavigateToVaultAddEditItem: (vaultItemId: String, isClone: Boolean) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String) -> Unit,
onNavigateToMoveToOrganization: (vaultItemId: String, showOnlyCollections: Boolean) -> Unit,
onNavigateToAttachments: (vaultItemId: String) -> Unit,
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@ -94,12 +94,11 @@ fun VaultItemScreen(
is VaultItemEvent.NavigateToAttachments -> onNavigateToAttachments(event.itemId)
is VaultItemEvent.NavigateToMoveToOrganization -> {
onNavigateToMoveToOrganization(event.itemId)
onNavigateToMoveToOrganization(event.itemId, false)
}
is VaultItemEvent.NavigateToCollections -> {
// TODO implement Collections in BIT-1575
Toast.makeText(context, "Not yet implemented.", Toast.LENGTH_SHORT).show()
onNavigateToMoveToOrganization(event.itemId, true)
}
is VaultItemEvent.ShowToast -> {

View file

@ -27,6 +27,7 @@ import kotlinx.collections.immutable.toImmutableList
@Composable
fun VaultMoveToOrganizationContent(
state: VaultMoveToOrganizationState.ViewState.Content,
showOnlyCollections: Boolean,
organizationSelect: (VaultMoveToOrganizationState.ViewState.Content.Organization) -> Unit,
collectionSelect: (VaultCollection) -> Unit,
modifier: Modifier = Modifier,
@ -34,44 +35,47 @@ fun VaultMoveToOrganizationContent(
LazyColumn(
modifier = modifier,
) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.organization),
options = state
.organizations
.map { it.name }
.toImmutableList(),
selectedOption = state.selectedOrganization.name,
onOptionSelected = { selectedString ->
organizationSelect(
state
.organizations
.first { it.name == selectedString },
)
},
modifier = Modifier.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(id = R.string.move_to_org_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
if (!showOnlyCollections) {
item {
Spacer(modifier = Modifier.height(8.dp))
BitwardenMultiSelectButton(
label = stringResource(id = R.string.organization),
options = state
.organizations
.map { it.name }
.toImmutableList(),
selectedOption = state.selectedOrganization.name,
onOptionSelected = { selectedString ->
organizationSelect(
state
.organizations
.first { it.name == selectedString },
)
},
modifier = Modifier.padding(horizontal = 16.dp),
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(id = R.string.move_to_org_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 12.sp,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 32.dp),
)
}
}
}
collectionItemsSelector(
collectionList = state.selectedOrganization.collections,
onCollectionSelect = collectionSelect,
isCollectionsTitleVisible = !showOnlyCollections,
)
}
}

View file

@ -11,16 +11,26 @@ import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val VAULT_MOVE_TO_ORGANIZATION_PREFIX = "vault_move_to_organization"
private const val VAULT_MOVE_TO_ORGANIZATION_ID = "vault_move_to_organization_id"
private const val VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS =
"vault_move_to_organization_only_collections"
private const val VAULT_MOVE_TO_ORGANIZATION_ROUTE =
"$VAULT_MOVE_TO_ORGANIZATION_PREFIX/{$VAULT_MOVE_TO_ORGANIZATION_ID}"
VAULT_MOVE_TO_ORGANIZATION_PREFIX +
"/{$VAULT_MOVE_TO_ORGANIZATION_ID}" +
"/{$VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS}"
/**
* Class to retrieve vault move to organization arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class VaultMoveToOrganizationArgs(val vaultItemId: String) {
data class VaultMoveToOrganizationArgs(
val vaultItemId: String,
val showOnlyCollections: Boolean,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
checkNotNull(savedStateHandle[VAULT_MOVE_TO_ORGANIZATION_ID]) as String,
vaultItemId = checkNotNull(savedStateHandle[VAULT_MOVE_TO_ORGANIZATION_ID]) as String,
showOnlyCollections =
(checkNotNull(savedStateHandle[VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS]) as String)
.toBoolean(),
)
}
@ -34,6 +44,9 @@ fun NavGraphBuilder.vaultMoveToOrganizationDestination(
route = VAULT_MOVE_TO_ORGANIZATION_ROUTE,
arguments = listOf(
navArgument(VAULT_MOVE_TO_ORGANIZATION_ID) { type = NavType.StringType },
navArgument(VAULT_MOVE_TO_ORGANIZATION_ONLY_COLLECTIONS) {
type = NavType.StringType
},
),
) {
VaultMoveToOrganizationScreen(
@ -47,10 +60,11 @@ fun NavGraphBuilder.vaultMoveToOrganizationDestination(
*/
fun NavController.navigateToVaultMoveToOrganization(
vaultItemId: String,
showOnlyCollections: Boolean,
navOptions: NavOptions? = null,
) {
navigate(
route = "$VAULT_MOVE_TO_ORGANIZATION_PREFIX/$vaultItemId",
route = "$VAULT_MOVE_TO_ORGANIZATION_PREFIX/$vaultItemId/$showOnlyCollections",
navOptions = navOptions,
)
}

View file

@ -109,14 +109,14 @@ private fun VaultMoveToOrganizationScaffold(
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.move_to_organization),
title = state.appBarText(),
scrollBehavior = scrollBehavior,
navigationIcon = painterResource(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = closeClick,
actions = {
BitwardenTextButton(
label = stringResource(id = R.string.move),
label = state.appBarButtonText(),
onClick = moveClick,
isEnabled = state.viewState is
VaultMoveToOrganizationState.ViewState.Content,
@ -134,6 +134,7 @@ private fun VaultMoveToOrganizationScaffold(
is VaultMoveToOrganizationState.ViewState.Content -> {
VaultMoveToOrganizationContent(
state = state.viewState,
showOnlyCollections = state.onlyShowCollections,
organizationSelect = organizationSelect,
collectionSelect = collectionSelect,
modifier = modifier,

View file

@ -45,6 +45,7 @@ class VaultMoveToOrganizationViewModel @Inject constructor(
?: run {
VaultMoveToOrganizationState(
vaultItemId = VaultMoveToOrganizationArgs(savedStateHandle).vaultItemId,
onlyShowCollections = VaultMoveToOrganizationArgs(savedStateHandle).showOnlyCollections,
viewState = VaultMoveToOrganizationState.ViewState.Loading,
dialogState = null,
)
@ -306,10 +307,25 @@ class VaultMoveToOrganizationViewModel @Inject constructor(
@Parcelize
data class VaultMoveToOrganizationState(
val vaultItemId: String,
val onlyShowCollections: Boolean,
val viewState: ViewState,
val dialogState: DialogState?,
) : Parcelable {
val appBarText: Text
get() = if (onlyShowCollections) {
R.string.collections.asText()
} else {
R.string.move_to_organization.asText()
}
val appBarButtonText: Text
get() = if (onlyShowCollections) {
R.string.save.asText()
} else {
R.string.move.asText()
}
/**
* Represents the current state of any dialogs on the screen.
*/

View file

@ -89,7 +89,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
},
onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it },
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationId = it },
onNavigateToMoveToOrganization = { id, _ -> onNavigateToMoveToOrganizationId = id },
viewModel = viewModel,
permissionsManager = fakePermissionManager,
)

View file

@ -76,7 +76,9 @@ class VaultItemScreenTest : BaseComposeTest() {
viewModel = viewModel,
onNavigateBack = { onNavigateBackCalled = true },
onNavigateToVaultAddEditItem = { id, _ -> onNavigateToVaultEditItemId = id },
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationItemId = it },
onNavigateToMoveToOrganization = { id, _ ->
onNavigateToMoveToOrganizationItemId = id
},
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
intentManager = intentManager,
)

View file

@ -50,6 +50,87 @@ class VaultMoveToOrganizationScreenTest : BaseComposeTest() {
}
}
@Test
fun `the app bar title should display according to state`() {
mutableStateFlow.update { currentState ->
currentState.copy(viewState = VaultMoveToOrganizationState.ViewState.Loading)
}
composeTestRule
.onNodeWithText(text = "Collections")
.assertIsNotDisplayed()
composeTestRule
.onNodeWithText(text = "Move to Organization")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
currentState.copy(onlyShowCollections = true)
}
composeTestRule
.onNodeWithText(text = "Move to Organization")
.assertIsNotDisplayed()
composeTestRule
.onNodeWithText(text = "Collections")
.assertIsDisplayed()
}
@Test
fun `the app bar button text should display according to state`() {
mutableStateFlow.update { currentState ->
currentState.copy(viewState = VaultMoveToOrganizationState.ViewState.Loading)
}
composeTestRule
.onNodeWithText(text = "Save")
.assertIsNotDisplayed()
composeTestRule
.onNodeWithText(text = "Move")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
currentState.copy(onlyShowCollections = true)
}
composeTestRule
.onNodeWithText(text = "Move")
.assertIsNotDisplayed()
composeTestRule
.onNodeWithText(text = "Save")
.assertIsDisplayed()
}
@Test
fun `the organization option field should update according to state`() {
composeTestRule
.onNodeWithContentDescription(label = "Organization, mockOrganizationName-1")
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
currentState.copy(onlyShowCollections = true)
}
composeTestRule
.onNodeWithContentDescription(label = "Organization, mockOrganizationName-1")
.assertIsNotDisplayed()
}
@Test
fun `the organization option field description should update according to state`() {
composeTestRule
.onNodeWithText(text = "Choose an organization that", substring = true)
.assertIsDisplayed()
mutableStateFlow.update { currentState ->
currentState.copy(onlyShowCollections = true)
}
composeTestRule
composeTestRule
.onNodeWithText(text = "Choose an organization that", substring = true)
.assertIsNotDisplayed()
}
@Test
fun `on NavigateBack event should invoke onNavigateBack`() {
mutableEventFlow.tryEmit(VaultMoveToOrganizationEvent.NavigateBack)
@ -235,4 +316,5 @@ private fun createVaultMoveToOrganizationState(): VaultMoveToOrganizationState =
selectedOrganizationId = "mockOrganizationId-1",
),
dialogState = null,
onlyShowCollections = false,
)

View file

@ -391,9 +391,11 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
private fun createSavedStateHandleWithState(
state: VaultMoveToOrganizationState? = null,
vaultItemId: String = "mockCipherId",
showOnlyCollections: Boolean = false,
) = SavedStateHandle().apply {
set("state", state)
set("vault_move_to_organization_id", vaultItemId)
set("vault_move_to_organization_only_collections", "$showOnlyCollections")
}
@Suppress("MaxLineLength")
@ -405,6 +407,7 @@ class VaultMoveToOrganizationViewModelTest : BaseViewModelTest() {
vaultItemId = vaultItemId,
viewState = viewState,
dialogState = dialogState,
onlyShowCollections = false,
)
}