mirror of
https://github.com/bitwarden/android.git
synced 2025-03-15 18:58:59 +03:00
BIT-1575: Collection Screen (#878)
This commit is contained in:
parent
96401aba79
commit
034284fad4
14 changed files with 194 additions and 64 deletions
|
@ -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) },
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -89,7 +89,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
|||
},
|
||||
onNavigateToGeneratorModal = { onNavigateToGeneratorModalType = it },
|
||||
onNavigateToAttachments = { onNavigateToAttachmentsId = it },
|
||||
onNavigateToMoveToOrganization = { onNavigateToMoveToOrganizationId = it },
|
||||
onNavigateToMoveToOrganization = { id, _ -> onNavigateToMoveToOrganizationId = id },
|
||||
viewModel = viewModel,
|
||||
permissionsManager = fakePermissionManager,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue