From 6b4e367c66af3f3ee981b1dde6b9388ce649b938 Mon Sep 17 00:00:00 2001 From: Ramsey Smith <142836716+ramsey-livefront@users.noreply.github.com> Date: Thu, 15 Feb 2024 16:03:24 -0700 Subject: [PATCH] BIT-1724: No folder vault section (#1021) --- .../itemlisting/VaultItemListingViewModel.kt | 5 +- .../ui/vault/feature/vault/VaultViewModel.kt | 8 --- .../feature/vault/util/VaultDataExtensions.kt | 51 ++++++++++++++----- .../datasource/sdk/model/CipherViewUtil.kt | 3 +- .../VaultItemListingDataExtensionsTest.kt | 30 +++++++++++ .../vault/util/VaultDataExtensionsTest.kt | 47 +++++++++++++++++ 6 files changed, 120 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt index 6eecf35316..2538877e42 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/VaultItemListingViewModel.kt @@ -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 } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt index 92d99bb054..56c1661485 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/VaultViewModel.kt @@ -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 - } } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt index 844041b3cd..a162e8bd60 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensions.kt @@ -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 -> diff --git a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt index 030d3c86a6..1ddf056d29 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/vault/datasource/sdk/model/CipherViewUtil.kt @@ -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", diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt index 425cf34cc0..1904212689 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/itemlisting/util/VaultItemListingDataExtensionsTest.kt @@ -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`() { diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt index fb3ba27a69..25df223f15 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/vault/feature/vault/util/VaultDataExtensionsTest.kt @@ -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() + 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) + } }