BIT-406: Allow item listing screen to display Collections data (#394)

This commit is contained in:
Brian Yencho 2023-12-14 15:38:46 -06:00 committed by Álison Fernandes
parent 41a0817c5a
commit 9a05b7168e
10 changed files with 129 additions and 18 deletions

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.ui.platform.theme.TransitionProviders
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
private const val CARD: String = "card"
private const val COLLECTION: String = "collection"
private const val FOLDER: String = "folder"
private const val IDENTITY: String = "identity"
private const val LOGIN: String = "login"
@ -92,6 +93,7 @@ fun NavController.navigateToVaultItemListing(
private fun VaultItemListingType.toTypeString(): String {
return when (this) {
is VaultItemListingType.Card -> CARD
is VaultItemListingType.Collection -> COLLECTION
is VaultItemListingType.Folder -> FOLDER
is VaultItemListingType.Identity -> IDENTITY
is VaultItemListingType.Login -> LOGIN
@ -102,6 +104,7 @@ private fun VaultItemListingType.toTypeString(): String {
private fun VaultItemListingType.toIdOrNull(): String? =
when (this) {
is VaultItemListingType.Collection -> collectionId
is VaultItemListingType.Folder -> folderId
is VaultItemListingType.Card -> null
is VaultItemListingType.Identity -> null
@ -121,6 +124,7 @@ private fun determineVaultItemListingType(
SECURE_NOTE -> VaultItemListingType.SecureNote
TRASH -> VaultItemListingType.Trash
FOLDER -> VaultItemListingType.Folder(folderId = id)
COLLECTION -> VaultItemListingType.Collection(collectionId = requireNotNull(id))
// This should never occur, vaultItemListingTypeString must match
else -> throw IllegalStateException()
}

View file

@ -151,6 +151,8 @@ class VaultItemListingViewModel @Inject constructor(
.updateWithAdditionalDataIfNecessary(
folderList = vaultData
.folderViewList,
collectionList = vaultData
.collectionViewList,
),
viewState = vaultData
.cipherViewList
@ -305,6 +307,23 @@ data class VaultItemListingState(
override val hasFab: Boolean
get() = false
}
/**
* A Collection item listing.
*
* @property collectionId the ID of the collection.
* @property collectionName the name of the collection.
*/
data class Collection(
val collectionId: String,
// The collectionName will always initially be an empty string
val collectionName: String = "",
) : ItemListingType() {
override val titleText: Text
get() = collectionName.asText()
override val hasFab: Boolean
get() = false
}
}
}

View file

@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import androidx.annotation.DrawableRes
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.bitwarden.core.CollectionView
import com.bitwarden.core.FolderView
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
@ -19,6 +20,10 @@ fun CipherView.determineListingPredicate(
type == CipherType.CARD && deletedDate == null
}
is VaultItemListingState.ItemListingType.Collection -> {
itemListingType.collectionId in this.collectionIds && deletedDate == null
}
is VaultItemListingState.ItemListingType.Folder -> {
folderId == itemListingType.folderId && deletedDate == null
}
@ -53,9 +58,17 @@ fun List<CipherView>.toViewState(): VaultItemListingState.ViewState =
/** * Updates a [VaultItemListingState.ItemListingType] with the given data if necessary. */
fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
folderList: List<FolderView>,
collectionList: List<CollectionView>,
): VaultItemListingState.ItemListingType =
when (this) {
is VaultItemListingState.ItemListingType.Card -> this
is VaultItemListingState.ItemListingType.Collection -> copy(
collectionName = collectionList
.find { it.id == collectionId }
?.name
.orEmpty(),
)
is VaultItemListingState.ItemListingType.Folder -> copy(
folderName = folderList
.find { it.id == folderId }

View file

@ -17,4 +17,7 @@ fun VaultItemListingType.toItemListingType(): VaultItemListingState.ItemListingT
is VaultItemListingType.Login -> VaultItemListingState.ItemListingType.Login
is VaultItemListingType.SecureNote -> VaultItemListingState.ItemListingType.SecureNote
is VaultItemListingType.Trash -> VaultItemListingState.ItemListingType.Trash
is VaultItemListingType.Collection -> {
VaultItemListingState.ItemListingType.Collection(collectionId = collectionId)
}
}

View file

@ -123,9 +123,10 @@ class VaultViewModel @Inject constructor(
}
private fun handleCollectionItemClick(action: VaultAction.CollectionClick) {
// TODO: Navigate to the listing screen for collections (BIT-406).
sendEvent(
VaultEvent.ShowToast(message = "Not yet implemented."),
VaultEvent.NavigateToItemListing(
VaultItemListingType.Collection(action.collectionItem.id),
),
)
}

View file

@ -36,4 +36,11 @@ sealed class VaultItemListingType {
* @param folderId the id of the folder, a null value indicates a, "no folder" grouping.
*/
data class Folder(val folderId: String?) : VaultItemListingType()
/**
* A Collection listing.
*
* @param collectionId the ID of the collection.
*/
data class Collection(val collectionId: String) : VaultItemListingType()
}

View file

@ -421,6 +421,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
)
}
@Suppress("CyclomaticComplexMethod")
private fun createSavedStateHandleWithVaultItemListingType(
vaultItemListingType: VaultItemListingType,
) = SavedStateHandle().apply {
@ -428,6 +429,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
"vault_item_listing_type",
when (vaultItemListingType) {
is VaultItemListingType.Card -> "card"
is VaultItemListingType.Collection -> "collection"
is VaultItemListingType.Folder -> "folder"
is VaultItemListingType.Identity -> "identity"
is VaultItemListingType.Login -> "login"
@ -439,6 +441,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
"id",
when (vaultItemListingType) {
is VaultItemListingType.Card -> null
is VaultItemListingType.Collection -> vaultItemListingType.collectionId
is VaultItemListingType.Folder -> vaultItemListingType.folderId
is VaultItemListingType.Identity -> null
is VaultItemListingType.Login -> null

View file

@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.itemlisting.util
import com.bitwarden.core.CipherType
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockFolderView
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import org.junit.Assert.assertEquals
@ -25,6 +26,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -53,6 +55,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -81,6 +84,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -109,6 +113,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -137,6 +142,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to true,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -165,6 +171,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -193,6 +200,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to false,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to true,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -221,6 +229,7 @@ class VaultItemListingDataExtensionsTest {
VaultItemListingState.ItemListingType.Identity to false,
VaultItemListingState.ItemListingType.Trash to true,
VaultItemListingState.ItemListingType.Folder(folderId = "mockId-1") to false,
VaultItemListingState.ItemListingType.Collection(collectionId = "mockId-1") to false,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
@ -292,12 +301,20 @@ class VaultItemListingDataExtensionsTest {
createMockFolderView(number = 2),
createMockFolderView(number = 3),
)
val collectionViewList = listOf(
createMockCollectionView(number = 1),
createMockCollectionView(number = 2),
createMockCollectionView(number = 3),
)
val result = VaultItemListingState.ItemListingType.Folder(
folderId = "mockId-1",
folderName = "wrong name",
)
.updateWithAdditionalDataIfNecessary(folderList = folderViewList)
.updateWithAdditionalDataIfNecessary(
folderList = folderViewList,
collectionList = collectionViewList,
)
assertEquals(
VaultItemListingState.ItemListingType.Folder(
@ -309,15 +326,55 @@ class VaultItemListingDataExtensionsTest {
}
@Test
fun `updateWithAdditionalDataIfNecessary should not change a non folder itemListingType`() {
fun `updateWithAdditionalDataIfNecessary should update a collection itemListingType`() {
val folderViewList = listOf(
createMockFolderView(number = 1),
createMockFolderView(number = 2),
createMockFolderView(number = 3),
)
val collectionViewList = listOf(
createMockCollectionView(number = 1),
createMockCollectionView(number = 2),
createMockCollectionView(number = 3),
)
val result = VaultItemListingState.ItemListingType.Collection(
collectionId = "mockId-1",
collectionName = "wrong name",
)
.updateWithAdditionalDataIfNecessary(
folderList = folderViewList,
collectionList = collectionViewList,
)
assertEquals(
VaultItemListingState.ItemListingType.Collection(
collectionId = "mockId-1",
collectionName = "mockName-1",
),
result,
)
}
@Suppress("MaxLineLength")
@Test
fun `updateWithAdditionalDataIfNecessary should not change a non-folder or non-collection itemListingType`() {
val folderViewList = listOf(
createMockFolderView(number = 1),
createMockFolderView(number = 2),
createMockFolderView(number = 3),
)
val collectionViewList = listOf(
createMockCollectionView(number = 1),
createMockCollectionView(number = 2),
createMockCollectionView(number = 3),
)
val result = VaultItemListingState.ItemListingType.Login
.updateWithAdditionalDataIfNecessary(folderList = folderViewList)
.updateWithAdditionalDataIfNecessary(
folderList = folderViewList,
collectionList = collectionViewList,
)
assertEquals(
VaultItemListingState.ItemListingType.Login,

View file

@ -13,6 +13,7 @@ class VaultItemListingTypeExtensionsTest {
val itemListingTypeList = listOf(
VaultItemListingType.Folder(folderId = "mock"),
VaultItemListingType.Trash,
VaultItemListingType.Collection(collectionId = "collectionId"),
)
val result = itemListingTypeList.map { it.toItemListingType() }
@ -21,6 +22,7 @@ class VaultItemListingTypeExtensionsTest {
listOf(
VaultItemListingState.ItemListingType.Folder(folderId = "mock"),
VaultItemListingState.ItemListingType.Trash,
VaultItemListingState.ItemListingType.Collection(collectionId = "collectionId"),
),
result,
)

View file

@ -594,21 +594,23 @@ class VaultViewModelTest : BaseViewModelTest() {
}
}
@Suppress("MaxLineLength")
@Test
fun `CollectionClick should emit ShowToast`() = runTest {
val viewModel = createViewModel()
val collectionId = "12345"
val collection = mockk<VaultState.ViewState.CollectionItem> {
every { id } returns collectionId
fun `CollectionClick should emit NavigateToItemListing event with Collection type with the correct collection ID`() =
runTest {
val viewModel = createViewModel()
val collectionId = "12345"
val collection = mockk<VaultState.ViewState.CollectionItem> {
every { id } returns collectionId
}
viewModel.eventFlow.test {
viewModel.trySendAction(VaultAction.CollectionClick(collection))
assertEquals(
VaultEvent.NavigateToItemListing(VaultItemListingType.Collection(collectionId)),
awaitItem(),
)
}
}
viewModel.eventFlow.test {
viewModel.trySendAction(VaultAction.CollectionClick(collection))
assertEquals(
VaultEvent.ShowToast(message = "Not yet implemented."),
awaitItem(),
)
}
}
@Test
fun `IdentityGroupClick should emit NavigateToItemListing event with Identity type`() =