[PM-25394] Sort default user collections by Organization name (#5819)

This commit is contained in:
Patrick Honkonen 2025-09-03 10:04:45 -04:00 committed by GitHub
parent 9adc25471e
commit 1c4e4dcaf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 62 additions and 16 deletions

View File

@ -82,7 +82,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.UpdateSendResult
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.data.vault.repository.model.VaultUnlockResult
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabetically
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByType
import com.x8bit.bitwarden.data.vault.repository.util.sortAlphabeticallyByTypeAndOrganization
import com.x8bit.bitwarden.data.vault.repository.util.toDomainsData
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkFolder
import com.x8bit.bitwarden.data.vault.repository.util.toEncryptedNetworkSend
@ -1168,7 +1168,11 @@ class VaultRepositoryImpl(
.fold(
onSuccess = { collections ->
DataState.Loaded(
collections.sortAlphabeticallyByType(),
collections.sortAlphabeticallyByTypeAndOrganization(
userOrganizations = authDiskSource
.getOrganizations(userId = userId)
.orEmpty(),
),
)
},
onFailure = { throwable -> DataState.Error(throwable) },

View File

@ -32,21 +32,45 @@ fun List<SyncResponseJson.Collection>.toEncryptedSdkCollectionList(): List<Colle
map { it.toEncryptedSdkCollection() }
/**
* Sorts the collections, grouping them by type, with `DEFAULT_USER_COLLECTION` types displayed
* first. Within each group, collections are sorted alphabetically by name.
* Sorts a list of [CollectionView] objects based on a multi-level sorting logic.
*
* The sorting criteria are as follows, in order of precedence:
* 1. Collections of type `DEFAULT_USER_COLLECTION` are placed first.
* 2. All other collections are grouped by their `CollectionType`.
* 3. Within each group, collections are sorted alphabetically by name. For collections of
* type `DEFAULT_USER_COLLECTION`, the corresponding organization's name is used for sorting
* instead of the collection's own name.
*
* This function uses a [SpecialCharWithPrecedenceComparator] for the alphabetical sort.
*
* @param userOrganizations A list of the user's organizations, used to find the name for
* `DEFAULT_USER_COLLECTION` types.
* @return A new list containing the sorted [CollectionView] objects.
*/
@JvmName("toAlphabeticallySortedCollectionList")
fun List<CollectionView>.sortAlphabeticallyByType(): List<CollectionView> {
fun List<CollectionView>.sortAlphabeticallyByTypeAndOrganization(
userOrganizations: List<SyncResponseJson.Profile.Organization>,
): List<CollectionView> {
return this.sortedWith(
// DEFAULT_USER_COLLECTION come first
comparator = compareBy<CollectionView> { it.type != CollectionType.DEFAULT_USER_COLLECTION }
// Then sort by other CollectionType ordinals
.thenBy { it.type }
// Finally, sort by name within each group
.thenComparing(
CollectionView::name,
SpecialCharWithPrecedenceComparator,
),
// Finally, sort within each group. For default collections, use the
// organization's name; for others, use the collection's name.
.thenBy(SpecialCharWithPrecedenceComparator) {
if (it.type == CollectionType.DEFAULT_USER_COLLECTION) {
// For default collections, sort by the organization's name
userOrganizations
.find { org -> org.id == it.organizationId }
?.name
?: it.name
} else {
// For other collections, sort by the collection's name
it.name
}
}
// As a final fallback if names are identical, sort by ID to ensure a stable order
.thenBy { it.id },
)
}

View File

@ -4,6 +4,7 @@ import com.bitwarden.collections.Collection
import com.bitwarden.collections.CollectionType
import com.bitwarden.network.model.CollectionTypeJson
import com.bitwarden.network.model.SyncResponseJson
import com.bitwarden.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCollectionView
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
@ -103,7 +104,7 @@ class VaultSdkCollectionExtensionsTest {
@Suppress("MaxLineLength")
@Test
fun `toSortAlphabetically should sort collections by type and name`() {
fun `toSortAlphabeticallyByTypeAndOrganization should sort collections by type and name`() {
val list = listOf(
createMockCollectionView(1).copy(name = "c"),
createMockCollectionView(1).copy(name = "B"),
@ -111,16 +112,28 @@ class VaultSdkCollectionExtensionsTest {
createMockCollectionView(1).copy(name = "4"),
createMockCollectionView(1).copy(name = "A"),
createMockCollectionView(1).copy(name = "#"),
createMockCollectionView(1).copy(
name = "D",
createMockCollectionView(2).copy(
name = "Org2 items",
type = CollectionType.DEFAULT_USER_COLLECTION,
organizationId = "mockId-2",
),
createMockCollectionView(1).copy(
name = "Org1 items",
type = CollectionType.DEFAULT_USER_COLLECTION,
organizationId = "mockId-1",
),
)
val expected = listOf(
createMockCollectionView(1).copy(
name = "D",
name = "Org1 items",
type = CollectionType.DEFAULT_USER_COLLECTION,
organizationId = "mockId-1",
),
createMockCollectionView(2).copy(
name = "Org2 items",
type = CollectionType.DEFAULT_USER_COLLECTION,
organizationId = "mockId-2",
),
createMockCollectionView(1).copy(name = "#"),
createMockCollectionView(1).copy(name = "4"),
@ -132,7 +145,12 @@ class VaultSdkCollectionExtensionsTest {
assertEquals(
expected,
list.sortAlphabeticallyByType(),
list.sortAlphabeticallyByTypeAndOrganization(
userOrganizations = listOf(
createMockOrganization(number = 1),
createMockOrganization(number = 2),
),
),
)
}