BIT-1301 Adding icons to collection items (#840)

This commit is contained in:
Oleg Semenenko 2024-01-28 23:31:20 -06:00 committed by Álison Fernandes
parent 82ef39e15d
commit 2d652c8a2e
12 changed files with 247 additions and 10 deletions

View file

@ -19,6 +19,7 @@ import com.x8bit.bitwarden.ui.platform.feature.search.SearchTypeData
import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import com.x8bit.bitwarden.ui.tools.feature.send.util.toLabelIcons
import com.x8bit.bitwarden.ui.tools.feature.send.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.util.toLabelIcons
import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import java.time.Clock
@ -171,7 +172,7 @@ private fun CipherView.toDisplayItem(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
),
extraIconList = emptyList(),
extraIconList = toLabelIcons(),
overflowOptions = toOverflowActions(),
totpCode = login?.totp,
)

View file

@ -17,6 +17,7 @@ import com.x8bit.bitwarden.ui.platform.util.toFormattedPattern
import com.x8bit.bitwarden.ui.tools.feature.send.util.toLabelIcons
import com.x8bit.bitwarden.ui.tools.feature.send.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
import com.x8bit.bitwarden.ui.vault.feature.util.toLabelIcons
import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
import java.time.Clock
@ -210,7 +211,7 @@ private fun CipherView.toDisplayItem(
baseIconUrl = baseIconUrl,
isIconLoadingDisabled = isIconLoadingDisabled,
),
extraIconList = emptyList(),
extraIconList = toLabelIcons(),
overflowOptions = toOverflowActions(),
)

View file

@ -2,7 +2,9 @@ package com.x8bit.bitwarden.ui.vault.feature.util
import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.model.VaultTrailingIcon
/**
* Creates the list of overflow actions to be displayed for a [CipherView].
@ -36,3 +38,18 @@ fun CipherView.toOverflowActions(): List<ListingItemOverflowAction.VaultAction>
)
}
.orEmpty()
/**
* Checks if the list is empty and if not returns an icon in a list.
*/
fun CipherView.toLabelIcons(): List<IconRes> {
return listOfNotNull(
VaultTrailingIcon.COLLECTION.takeIf {
this.collectionIds.isNotEmpty() || this.organizationId?.isNotEmpty() == true
},
VaultTrailingIcon.ATTACHMENT.takeIf { this.attachments?.isNotEmpty() == true },
)
.map {
IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription)
}
}

View file

@ -16,7 +16,9 @@ import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenGroupItem
import com.x8bit.bitwarden.ui.platform.components.BitwardenListHeaderTextWithSupportLabel
import com.x8bit.bitwarden.ui.platform.components.model.toIconResources
import com.x8bit.bitwarden.ui.vault.feature.vault.handlers.VaultHandlers
import kotlinx.collections.immutable.toPersistentList
/**
* Content view for the [VaultScreen].
@ -76,6 +78,10 @@ fun VaultContent(
items(state.favoriteItems) { favoriteItem ->
VaultEntryListItem(
startIcon = favoriteItem.startIcon,
trailingLabelIcons = favoriteItem
.extraIconList
.toIconResources()
.toPersistentList(),
label = favoriteItem.name(),
supportingLabel = favoriteItem.supportingLabel?.invoke(),
onClick = { vaultHandlers.vaultItemClick(favoriteItem) },
@ -231,6 +237,10 @@ fun VaultContent(
items(state.noFolderItems) { noFolderItem ->
VaultEntryListItem(
startIcon = noFolderItem.startIcon,
trailingLabelIcons = noFolderItem
.extraIconList
.toIconResources()
.toPersistentList(),
label = noFolderItem.name(),
supportingLabel = noFolderItem.supportingLabel?.invoke(),
onClick = { vaultHandlers.vaultItemClick(noFolderItem) },

View file

@ -7,8 +7,11 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.components.BitwardenListItem
import com.x8bit.bitwarden.ui.platform.components.SelectionItemData
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
/**
@ -30,6 +33,7 @@ fun VaultEntryListItem(
overflowOptions: List<ListingItemOverflowAction.VaultAction>,
onOverflowOptionClick: (ListingItemOverflowAction.VaultAction) -> Unit,
modifier: Modifier = Modifier,
trailingLabelIcons: ImmutableList<IconResource> = persistentListOf(),
supportingLabel: String? = null,
) {
BitwardenListItem(
@ -37,6 +41,7 @@ fun VaultEntryListItem(
label = label,
supportingLabel = supportingLabel,
startIcon = startIcon,
trailingLabelIcons = trailingLabelIcons,
onClick = onClick,
selectionDataList = overflowOptions
.map { option ->

View file

@ -20,6 +20,7 @@ import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterData
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
@ -655,8 +656,16 @@ data class VaultState(
*/
abstract val name: Text
/**
* The icon at the start of the item.
*/
abstract val startIcon: IconData
/**
* The icons shown after the item name.
*/
abstract val extraIconList: List<IconRes>
/**
* An optional supporting label for the vault item that provides additional information.
* This property is open to be overridden by subclasses that can provide their own
@ -679,6 +688,7 @@ data class VaultState(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
val username: Text?,
) : VaultItem() {
@ -696,6 +706,7 @@ data class VaultState(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_card_item),
override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
val brand: Text? = null,
val lastFourDigits: Text? = null,
@ -723,6 +734,7 @@ data class VaultState(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_identity_item),
override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
val firstName: Text?,
) : VaultItem() {
@ -738,6 +750,7 @@ data class VaultState(
override val id: String,
override val name: Text,
override val startIcon: IconData = IconData.Local(R.drawable.ic_secure_note_item),
override val extraIconList: List<IconRes> = emptyList(),
override val overflowOptions: List<ListingItemOverflowAction.VaultAction>,
) : VaultItem() {
override val supportingLabel: Text? get() = null

View file

@ -10,6 +10,7 @@ import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.model.IconData
import com.x8bit.bitwarden.ui.vault.feature.util.toLabelIcons
import com.x8bit.bitwarden.ui.vault.feature.util.toOverflowActions
import com.x8bit.bitwarden.ui.vault.feature.vault.VaultState
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
@ -158,12 +159,14 @@ private fun CipherView.toVaultItemOrNull(
baseIconUrl = baseIconUrl,
),
overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(),
)
CipherType.SECURE_NOTE -> VaultState.ViewState.VaultItem.SecureNote(
id = id,
name = name.asText(),
overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(),
)
CipherType.CARD -> VaultState.ViewState.VaultItem.Card(
@ -174,6 +177,7 @@ private fun CipherView.toVaultItemOrNull(
?.takeLast(4)
?.asText(),
overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(),
)
CipherType.IDENTITY -> VaultState.ViewState.VaultItem.Identity(
@ -181,6 +185,7 @@ private fun CipherView.toVaultItemOrNull(
name = name.asText(),
firstName = identity?.firstName?.asText(),
overflowOptions = toOverflowActions(),
extraIconList = toLabelIcons(),
)
}
}

View file

@ -0,0 +1,23 @@
package com.x8bit.bitwarden.ui.vault.model
import androidx.annotation.DrawableRes
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* Represents the icons displayed after the cipher name.
*/
enum class VaultTrailingIcon(
@DrawableRes val iconRes: Int,
val contentDescription: Text,
) {
COLLECTION(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
ATTACHMENT(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M17.722,12.752L7.418,2.326C7.081,1.981 6.679,1.707 6.236,1.523C5.792,1.338 5.316,1.245 4.837,1.25C4.357,1.255 3.883,1.357 3.443,1.551C3.003,1.745 2.607,2.026 2.277,2.378C1.606,3.101 1.238,4.058 1.25,5.048C1.263,6.039 1.654,6.986 2.343,7.692L10.483,15.93C10.536,15.985 10.599,16.027 10.669,16.055C10.74,16.082 10.815,16.095 10.89,16.091C10.958,16.086 11.024,16.068 11.084,16.036C11.144,16.004 11.197,15.96 11.239,15.906C11.338,15.803 11.392,15.664 11.39,15.521C11.388,15.377 11.331,15.24 11.23,15.139L3.09,6.904C2.603,6.406 2.326,5.737 2.318,5.038C2.309,4.339 2.569,3.663 3.043,3.153C3.276,2.905 3.556,2.707 3.866,2.571C4.177,2.435 4.511,2.363 4.849,2.359C5.187,2.356 5.523,2.422 5.836,2.552C6.148,2.682 6.432,2.875 6.669,3.119L16.974,13.542C17.312,13.851 17.551,14.255 17.661,14.701C17.695,14.906 17.678,15.116 17.613,15.313C17.547,15.509 17.434,15.687 17.284,15.828C17.119,16.003 16.922,16.143 16.704,16.24C16.485,16.336 16.249,16.387 16.01,16.389C15.772,16.392 15.535,16.346 15.315,16.254C15.094,16.162 14.894,16.026 14.726,15.855L4.49,5.498C4.426,5.43 4.376,5.35 4.342,5.263C4.308,5.176 4.293,5.083 4.296,4.989C4.299,4.895 4.32,4.804 4.358,4.719C4.397,4.633 4.452,4.557 4.52,4.493C4.574,4.441 4.639,4.401 4.709,4.374C4.779,4.348 4.854,4.336 4.929,4.339C5.004,4.343 5.077,4.361 5.145,4.394C5.213,4.426 5.274,4.472 5.323,4.529L13.399,12.7C13.448,12.75 13.505,12.789 13.569,12.816C13.633,12.844 13.702,12.857 13.771,12.857C13.841,12.86 13.911,12.846 13.974,12.818C14.037,12.789 14.095,12.746 14.139,12.693C14.241,12.588 14.3,12.448 14.305,12.302C14.305,12.23 14.292,12.158 14.265,12.092C14.238,12.026 14.198,11.965 14.148,11.914L6.07,3.737C5.919,3.581 5.739,3.458 5.541,3.373C5.342,3.288 5.13,3.243 4.914,3.241C4.699,3.239 4.485,3.28 4.286,3.362C4.086,3.444 3.904,3.565 3.751,3.717C3.461,4.08 3.301,4.532 3.298,4.999C3.296,5.465 3.451,5.92 3.737,6.286L13.974,16.65C14.241,16.923 14.56,17.139 14.911,17.285C15.262,17.43 15.639,17.504 16.019,17.5C16.399,17.496 16.775,17.415 17.123,17.262C17.472,17.109 17.786,16.887 18.048,16.609C18.304,16.358 18.499,16.051 18.618,15.711C18.737,15.371 18.776,15.008 18.733,14.651C18.615,13.922 18.258,13.254 17.722,12.752Z"
android:fillColor="#000000"/>
</vector>

View file

@ -26,7 +26,16 @@ fun createMockDisplayItemForCipher(
uri = "https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item,
),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
@ -50,7 +59,16 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number",
subtitle = null,
iconData = IconData.Local(R.drawable.ic_secure_note_item),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
@ -68,7 +86,16 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number",
subtitle = "er-$number",
iconData = IconData.Local(R.drawable.ic_card_item),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
@ -89,7 +116,16 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number",
subtitle = "mockFirstName-${number}mockLastName-$number",
iconData = IconData.Local(R.drawable.ic_identity_item),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),

View file

@ -27,7 +27,16 @@ fun createMockDisplayItemForCipher(
"https://vault.bitwarden.com/icons/www.mockuri.com/icon.png",
fallbackIconRes = R.drawable.ic_login_item,
),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
@ -50,7 +59,16 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number",
subtitle = subtitle,
iconData = IconData.Local(R.drawable.ic_secure_note_item),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
@ -67,7 +85,16 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number",
subtitle = subtitle,
iconData = IconData.Local(R.drawable.ic_card_item),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),
@ -87,7 +114,16 @@ fun createMockDisplayItemForCipher(
title = "mockName-$number",
subtitle = subtitle,
iconData = IconData.Local(R.drawable.ic_identity_item),
extraIconList = emptyList(),
extraIconList = listOf(
IconRes(
iconRes = R.drawable.ic_collection,
contentDescription = R.string.collections.asText(),
),
IconRes(
iconRes = R.drawable.ic_attachment,
contentDescription = R.string.attachments.asText(),
),
),
overflowOptions = listOf(
ListingItemOverflowAction.VaultAction.ViewClick(cipherId = "mockId-$number"),
ListingItemOverflowAction.VaultAction.EditClick(cipherId = "mockId-$number"),

View file

@ -7,7 +7,9 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockIdentityVie
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockLoginView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockSecureNoteView
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockUriView
import com.x8bit.bitwarden.ui.platform.components.model.IconRes
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
import com.x8bit.bitwarden.ui.vault.model.VaultTrailingIcon
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@ -202,4 +204,83 @@ class CipherViewExtensionsTest {
result,
)
}
@Test
fun `toTrailingIcons should return collection icon if collectionId is not empty`() {
val cipher = createMockCipherView(1).copy(
organizationId = null,
attachments = null,
)
val expected = listOf(VaultTrailingIcon.COLLECTION).map {
IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription)
}
val result = cipher.toLabelIcons()
assertEquals(expected, result)
}
@Test
fun `toTrailingIcons should return collection icon if organizationId is not null`() {
val cipher = createMockCipherView(1).copy(
collectionIds = listOf(),
attachments = null,
)
val expected = listOf(VaultTrailingIcon.COLLECTION).map {
IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription)
}
val result = cipher.toLabelIcons()
assertEquals(expected, result)
}
@Test
fun `toTrailingIcons should return attachment icon if attachments is not null`() {
val cipher = createMockCipherView(1).copy(
collectionIds = listOf(),
organizationId = null,
)
val expected = listOf(VaultTrailingIcon.ATTACHMENT).map {
IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription)
}
val result = cipher.toLabelIcons()
assertEquals(expected, result)
}
@Test
fun `toTrailingIcons should return trailing icons if cipher has correct data`() {
val cipher = createMockCipherView(1)
val expected = listOf(
VaultTrailingIcon.COLLECTION,
VaultTrailingIcon.ATTACHMENT,
).map {
IconRes(iconRes = it.iconRes, contentDescription = it.contentDescription)
}
val result = cipher.toLabelIcons()
assertEquals(expected, result)
}
@Test
fun `toTrailingIcons should return empty list if no data requires an extra icon`() {
val cipher = createMockCipherView(1).copy(
collectionIds = listOf(),
organizationId = null,
attachments = null,
)
val expected = listOf<IconRes>()
val result = cipher.toLabelIcons()
assertEquals(expected, result)
}
}