BIT-1724: No folder vault section (#1021)

This commit is contained in:
Ramsey Smith 2024-02-15 16:03:24 -07:00 committed by Álison Fernandes
parent e9fba5b99c
commit 6b4e367c66
6 changed files with 120 additions and 24 deletions

View file

@ -952,7 +952,10 @@ data class VaultItemListingState(
// The folderName will always initially be an empty string
val folderName: String = "",
) : Vault() {
override val titleText: Text get() = folderName.asText()
override val titleText: Text
get() = folderId
?.let { folderName.asText() }
?: R.string.folder_none.asText()
override val hasFab: Boolean get() = false
}

View file

@ -888,14 +888,6 @@ data class VaultState(
val message: Text,
) : DialogState()
}
companion object {
/**
* The maximum number of no folder items that can be displayed before the UI creates a
* no folder "folder".
*/
private const val NO_FOLDER_ITEM_THRESHOLD: Int = 100
}
}
/**

View file

@ -20,6 +20,12 @@ import com.x8bit.bitwarden.ui.vault.model.findVaultCardBrandWithNameOrNull
private const val ANDROID_URI = "androidapp://"
private const val IOS_URI = "iosapp://"
/**
* The maximum number of no folder items that can be displayed before the UI creates a
* no folder "folder".
*/
private const val NO_FOLDER_ITEM_THRESHOLD: Int = 100
/**
* Transforms [VaultData] into [VaultState.ViewState] using the given [vaultFilterType].
*/
@ -38,6 +44,8 @@ fun VaultData.toViewState(
.filter { it.deletedDate == null }
val filteredFolderViewList = folderViewList.toFilteredList(vaultFilterType)
val filteredCollectionViewList = collectionViewList.toFilteredList(vaultFilterType)
val noFolderItems = filteredCipherViewList
.filter { it.folderId.isNullOrBlank() }
return if (filteredCipherViewList.isEmpty()) {
VaultState.ViewState.NoItems
@ -61,25 +69,40 @@ fun VaultData.toViewState(
baseIconUrl = baseIconUrl,
)
},
folderItems = filteredFolderViewList.map { folderView ->
VaultState.ViewState.FolderItem(
id = folderView.id,
name = folderView.name.asText(),
itemCount = filteredCipherViewList
.count {
!it.id.isNullOrBlank() &&
folderView.id == it.folderId
},
)
},
noFolderItems = filteredCipherViewList
.filter { it.folderId.isNullOrBlank() }
folderItems = filteredFolderViewList
.map { folderView ->
VaultState.ViewState.FolderItem(
id = folderView.id,
name = folderView.name.asText(),
itemCount = filteredCipherViewList
.count {
!it.id.isNullOrBlank() &&
folderView.id == it.folderId
},
)
}
.let { folderItems ->
if (noFolderItems.size < NO_FOLDER_ITEM_THRESHOLD) {
folderItems
} else {
folderItems.plus(
VaultState.ViewState.FolderItem(
id = null,
name = R.string.folder_none.asText(),
itemCount = noFolderItems.size,
),
)
}
},
noFolderItems = noFolderItems
.mapNotNull {
it.toVaultItemOrNull(
isIconLoadingDisabled = isIconLoadingDisabled,
baseIconUrl = baseIconUrl,
)
},
}
.takeIf { it.size < NO_FOLDER_ITEM_THRESHOLD }
.orEmpty(),
collectionItems = filteredCollectionViewList
.filter { it.id != null }
.map { collectionView ->

View file

@ -28,11 +28,12 @@ fun createMockCipherView(
isDeleted: Boolean = false,
cipherType: CipherType = CipherType.LOGIN,
totp: String? = "mockTotp-$number",
folderId: String? = "mockId-$number",
): CipherView =
CipherView(
id = "mockId-$number",
organizationId = "mockOrganizationId-$number",
folderId = "mockId-$number",
folderId = folderId,
collectionIds = listOf("mockId-$number"),
key = "mockKey-$number",
name = "mockName-$number",

View file

@ -244,6 +244,36 @@ class VaultItemListingDataExtensionsTest {
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for item not in a folder`() {
val cipherView = createMockCipherView(
number = 1,
isDeleted = false,
cipherType = CipherType.SECURE_NOTE,
folderId = null,
)
mapOf(
VaultItemListingState.ItemListingType.Vault.Login to false,
VaultItemListingState.ItemListingType.Vault.Card to false,
VaultItemListingState.ItemListingType.Vault.SecureNote to true,
VaultItemListingState.ItemListingType.Vault.Identity to false,
VaultItemListingState.ItemListingType.Vault.Trash to false,
VaultItemListingState.ItemListingType.Vault.Folder(folderId = null) to true,
VaultItemListingState.ItemListingType.Vault.Collection(collectionId = "mockId-1") to true,
)
.forEach { (type, expected) ->
val result = cipherView.determineListingPredicate(
itemListingType = type,
)
assertEquals(
expected,
result,
)
}
}
@Test
@Suppress("MaxLineLength")
fun `determineListingPredicate should return the correct predicate for trash SecureNote cipherView`() {

View file

@ -419,4 +419,51 @@ class VaultDataExtensionsTest {
actual,
)
}
@Suppress("MaxLineLength")
@Test
fun `toViewState with over 100 no folder items should show no folder option`() {
mockkStatic(Uri::class)
val uriMock = mockk<Uri>()
every { Uri.parse(any()) } returns uriMock
every { uriMock.host } returns "www.mockuri1.com"
val vaultData = VaultData(
cipherViewList = List(100) {
createMockCipherView(number = it, folderId = null)
},
collectionViewList = listOf(),
folderViewList = listOf(),
sendViewList = listOf(),
)
val actual = vaultData.toViewState(
isPremium = true,
isIconLoadingDisabled = false,
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
vaultFilterType = VaultFilterType.AllVaults,
)
assertEquals(
VaultState.ViewState.Content(
loginItemsCount = 100,
cardItemsCount = 0,
identityItemsCount = 0,
secureNoteItemsCount = 0,
favoriteItems = listOf(),
folderItems = listOf(
VaultState.ViewState.FolderItem(
id = null,
name = R.string.folder_none.asText(),
itemCount = 100,
),
),
collectionItems = listOf(),
noFolderItems = listOf(),
trashItemsCount = 0,
totpItemsCount = 100,
),
actual,
)
unmockkStatic(Uri::class)
}
}