mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1115 Add nested folder support (#1072)
This commit is contained in:
parent
5c4a7310d4
commit
c3b422e46f
15 changed files with 705 additions and 98 deletions
|
@ -72,8 +72,8 @@ fun NavGraphBuilder.composableWithPushTransitions(
|
|||
arguments = arguments,
|
||||
deepLinks = deepLinks,
|
||||
enterTransition = TransitionProviders.Enter.pushLeft,
|
||||
exitTransition = TransitionProviders.Exit.pushLeft,
|
||||
popEnterTransition = TransitionProviders.Enter.pushLeft,
|
||||
exitTransition = TransitionProviders.Exit.stay,
|
||||
popEnterTransition = TransitionProviders.Enter.stay,
|
||||
popExitTransition = TransitionProviders.Exit.pushRight,
|
||||
content = content,
|
||||
)
|
||||
|
|
|
@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.navigationBarsPadding
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
@ -14,9 +16,11 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.BitwardenListItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPolicyWarningText
|
||||
|
@ -35,6 +39,7 @@ import kotlinx.collections.immutable.toPersistentList
|
|||
fun VaultItemListingContent(
|
||||
state: VaultItemListingState.ViewState.Content,
|
||||
policyDisablesSend: Boolean,
|
||||
folderClick: (id: String) -> Unit,
|
||||
vaultItemClick: (id: String) -> Unit,
|
||||
masterPasswordRepromptSubmit: (password: String, data: MasterPasswordRepromptData) -> Unit,
|
||||
onOverflowItemClick: (action: ListingItemOverflowAction) -> Unit,
|
||||
|
@ -106,75 +111,116 @@ fun VaultItemListingContent(
|
|||
}
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenListHeaderTextWithSupportLabel(
|
||||
label = stringResource(id = R.string.items),
|
||||
supportingLabel = state.displayItemList.size.toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
if (state.displayFolderList.isNotEmpty()) {
|
||||
item {
|
||||
BitwardenListHeaderTextWithSupportLabel(
|
||||
label = stringResource(id = R.string.folders),
|
||||
supportingLabel = state.displayFolderList.count().toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
items(state.displayFolderList) { folder ->
|
||||
BitwardenGroupItem(
|
||||
startIcon = painterResource(id = R.drawable.ic_folder),
|
||||
label = folder.name,
|
||||
supportingLabel = folder.count.toString(),
|
||||
onClick = { folderClick(folder.id) },
|
||||
showDivider = false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
items(state.displayItemList) {
|
||||
BitwardenListItem(
|
||||
startIcon = it.iconData,
|
||||
label = it.title,
|
||||
supportingLabel = it.subtitle,
|
||||
onClick = {
|
||||
if (it.isAutofill && it.shouldShowMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData =
|
||||
MasterPasswordRepromptData.Autofill(
|
||||
cipherId = it.id,
|
||||
)
|
||||
} else {
|
||||
vaultItemClick(it.id)
|
||||
}
|
||||
},
|
||||
trailingLabelIcons = it
|
||||
.extraIconList
|
||||
.toIconResources()
|
||||
.toPersistentList(),
|
||||
selectionDataList = it
|
||||
.overflowOptions
|
||||
.map { option ->
|
||||
SelectionItemData(
|
||||
text = option.title(),
|
||||
onClick = {
|
||||
when (option) {
|
||||
is ListingItemOverflowAction.SendAction.DeleteClick -> {
|
||||
showConfirmationDialog = option
|
||||
}
|
||||
|
||||
is ListingItemOverflowAction.VaultAction -> {
|
||||
if (option.requiresPasswordReprompt &&
|
||||
it.shouldShowMasterPasswordReprompt
|
||||
) {
|
||||
masterPasswordRepromptData =
|
||||
MasterPasswordRepromptData.OverflowItem(
|
||||
action = option,
|
||||
)
|
||||
} else {
|
||||
onOverflowItemClick(option)
|
||||
if (state.displayItemList.isNotEmpty()) {
|
||||
item {
|
||||
HorizontalDivider(
|
||||
thickness = 1.dp,
|
||||
color = MaterialTheme.colorScheme.outlineVariant,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
BitwardenListHeaderTextWithSupportLabel(
|
||||
label = stringResource(id = R.string.items),
|
||||
supportingLabel = state.displayItemList.size.toString(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
items(state.displayItemList) {
|
||||
BitwardenListItem(
|
||||
startIcon = it.iconData,
|
||||
label = it.title,
|
||||
supportingLabel = it.subtitle,
|
||||
onClick = {
|
||||
if (it.isAutofill && it.shouldShowMasterPasswordReprompt) {
|
||||
masterPasswordRepromptData =
|
||||
MasterPasswordRepromptData.Autofill(
|
||||
cipherId = it.id,
|
||||
)
|
||||
} else {
|
||||
vaultItemClick(it.id)
|
||||
}
|
||||
},
|
||||
trailingLabelIcons = it
|
||||
.extraIconList
|
||||
.toIconResources()
|
||||
.toPersistentList(),
|
||||
selectionDataList = it
|
||||
.overflowOptions
|
||||
.map { option ->
|
||||
SelectionItemData(
|
||||
text = option.title(),
|
||||
onClick = {
|
||||
when (option) {
|
||||
is ListingItemOverflowAction.SendAction.DeleteClick -> {
|
||||
showConfirmationDialog = option
|
||||
}
|
||||
}
|
||||
|
||||
else -> onOverflowItemClick(option)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
// Only show options if allowed
|
||||
.filter { !policyDisablesSend }
|
||||
.toPersistentList(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
// There is some built-in padding to the menu button that makes up
|
||||
// the visual difference here.
|
||||
end = 12.dp,
|
||||
),
|
||||
)
|
||||
is ListingItemOverflowAction.VaultAction -> {
|
||||
if (option.requiresPasswordReprompt &&
|
||||
it.shouldShowMasterPasswordReprompt
|
||||
) {
|
||||
masterPasswordRepromptData =
|
||||
MasterPasswordRepromptData.OverflowItem(
|
||||
action = option,
|
||||
)
|
||||
} else {
|
||||
onOverflowItemClick(option)
|
||||
}
|
||||
}
|
||||
|
||||
else -> onOverflowItemClick(option)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
// Only show options if allowed
|
||||
.filter { !policyDisablesSend }
|
||||
.toPersistentList(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
// There is some built-in padding to the menu button that makes up
|
||||
// the visual difference here.
|
||||
end = 12.dp,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
|
|
|
@ -56,10 +56,12 @@ data class VaultItemListingArgs(
|
|||
/**
|
||||
* Add the [VaultItemListingScreen] to the nav graph.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
fun NavGraphBuilder.vaultItemListingDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVaultItemScreen: (id: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
|
||||
onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit,
|
||||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToSearchVault: (searchType: SearchType.Vault) -> Unit,
|
||||
) {
|
||||
|
@ -69,6 +71,7 @@ fun NavGraphBuilder.vaultItemListingDestination(
|
|||
onNavigateToAddSendItem = { },
|
||||
onNavigateToEditSendItem = { },
|
||||
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
|
||||
onNavigateToVaultItemListing = onNavigateToVaultItemListing,
|
||||
onNavigateToVaultItemScreen = onNavigateToVaultItemScreen,
|
||||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
|
||||
|
@ -102,6 +105,7 @@ fun NavGraphBuilder.vaultItemListingDestinationAsRoot(
|
|||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
|
||||
onNavigateToSearch = { onNavigateToSearchVault(it as SearchType.Vault) },
|
||||
onNavigateToVaultItemListing = {},
|
||||
onNavigateToAddSendItem = {},
|
||||
onNavigateToEditSendItem = {},
|
||||
)
|
||||
|
@ -125,6 +129,7 @@ fun NavGraphBuilder.sendItemListingDestination(
|
|||
onNavigateToVaultAddItemScreen = { },
|
||||
onNavigateToVaultItemScreen = { },
|
||||
onNavigateToVaultEditItemScreen = { },
|
||||
onNavigateToVaultItemListing = { },
|
||||
onNavigateToSearch = { onNavigateToSearchSend(it as SearchType.Sends) },
|
||||
)
|
||||
}
|
||||
|
@ -138,6 +143,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination(
|
|||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVaultItemScreen: (id: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (cipherId: String) -> Unit,
|
||||
onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit,
|
||||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToAddSendItem: () -> Unit,
|
||||
onNavigateToEditSendItem: (sendId: String) -> Unit,
|
||||
|
@ -168,6 +174,7 @@ private fun NavGraphBuilder.internalVaultItemListingDestination(
|
|||
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
|
||||
onNavigateToAddSendItem = onNavigateToAddSendItem,
|
||||
onNavigateToEditSendItem = onNavigateToEditSendItem,
|
||||
onNavigateToVaultItemListing = onNavigateToVaultItemListing,
|
||||
onNavigateToSearch = onNavigateToSearch,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
|||
import com.x8bit.bitwarden.ui.platform.theme.LocalIntentManager
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.handlers.VaultItemListingHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.initials
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
|
@ -55,10 +56,12 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
fun VaultItemListingScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToVaultItem: (id: String) -> Unit,
|
||||
onNavigateToVaultEditItemScreen: (cipherVaultId: String) -> Unit,
|
||||
onNavigateToVaultItemListing: (vaultItemListingType: VaultItemListingType) -> Unit,
|
||||
onNavigateToVaultAddItemScreen: () -> Unit,
|
||||
onNavigateToAddSendItem: () -> Unit,
|
||||
onNavigateToEditSendItem: (sendId: String) -> Unit,
|
||||
|
@ -117,6 +120,10 @@ fun VaultItemListingScreen(
|
|||
is VaultItemListingEvent.NavigateToSearchScreen -> {
|
||||
onNavigateToSearch(event.searchType)
|
||||
}
|
||||
|
||||
is VaultItemListingEvent.NavigateToFolderItem -> {
|
||||
onNavigateToVaultItemListing(VaultItemListingType.Folder(event.folderId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -237,6 +244,7 @@ private fun VaultItemListingScaffold(
|
|||
policyDisablesSend = state.policyDisablesSend &&
|
||||
state.itemListingType is VaultItemListingState.ItemListingType.Send,
|
||||
vaultItemClick = vaultItemListingHandlers.itemClick,
|
||||
folderClick = vaultItemListingHandlers.folderClick,
|
||||
masterPasswordRepromptSubmit =
|
||||
vaultItemListingHandlers.masterPasswordRepromptSubmit,
|
||||
onOverflowItemClick = vaultItemListingHandlers.overflowItemClick,
|
||||
|
|
|
@ -138,6 +138,7 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
is VaultItemListingsAction.SwitchAccountClick -> handleSwitchAccountClick(action)
|
||||
is VaultItemListingsAction.DismissDialogClick -> handleDismissDialogClick()
|
||||
is VaultItemListingsAction.BackClick -> handleBackClick()
|
||||
is VaultItemListingsAction.FolderClick -> handleFolderClick(action)
|
||||
is VaultItemListingsAction.LockClick -> handleLockClick()
|
||||
is VaultItemListingsAction.SyncClick -> handleSyncClick()
|
||||
is VaultItemListingsAction.SearchIconClick -> handleSearchIconClick()
|
||||
|
@ -167,6 +168,10 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
authRepository.switchAccount(userId = action.accountSummary.userId)
|
||||
}
|
||||
|
||||
private fun handleFolderClick(action: VaultItemListingsAction.FolderClick) {
|
||||
sendEvent(VaultItemListingEvent.NavigateToFolderItem(action.id))
|
||||
}
|
||||
|
||||
private fun handleRefreshClick() {
|
||||
vaultRepository.sync()
|
||||
}
|
||||
|
@ -671,18 +676,13 @@ class VaultItemListingViewModel @Inject constructor(
|
|||
),
|
||||
viewState = when (val listingType = currentState.itemListingType) {
|
||||
is VaultItemListingState.ItemListingType.Vault -> {
|
||||
vaultData
|
||||
.cipherViewList
|
||||
.filter { cipherView ->
|
||||
cipherView.determineListingPredicate(listingType)
|
||||
}
|
||||
.toFilteredList(state.vaultFilterType)
|
||||
.toViewState(
|
||||
itemListingType = listingType,
|
||||
baseIconUrl = state.baseIconUrl,
|
||||
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
||||
autofillSelectionData = state.autofillSelectionData,
|
||||
)
|
||||
vaultData.toViewState(
|
||||
vaultFilterType = state.vaultFilterType,
|
||||
itemListingType = listingType,
|
||||
baseIconUrl = state.baseIconUrl,
|
||||
isIconLoadingDisabled = state.isIconLoadingDisabled,
|
||||
autofillSelectionData = state.autofillSelectionData,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultItemListingState.ItemListingType.Send -> {
|
||||
|
@ -842,6 +842,7 @@ data class VaultItemListingState(
|
|||
*/
|
||||
data class Content(
|
||||
val displayItemList: List<DisplayItem>,
|
||||
val displayFolderList: List<FolderDisplayItem>,
|
||||
) : ViewState() {
|
||||
override val isPullToRefreshEnabled: Boolean get() = true
|
||||
}
|
||||
|
@ -881,6 +882,19 @@ data class VaultItemListingState(
|
|||
val shouldShowMasterPasswordReprompt: Boolean,
|
||||
)
|
||||
|
||||
/**
|
||||
* The folder that is displayed to the user on the ItemListingScreen.
|
||||
*
|
||||
* @property id the id of the folder.
|
||||
* @property name the name of the folder.
|
||||
* @property count the amount of ciphers in the folder.
|
||||
*/
|
||||
data class FolderDisplayItem(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val count: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Represents different types of item listing.
|
||||
*/
|
||||
|
@ -1017,6 +1031,11 @@ sealed class VaultItemListingEvent {
|
|||
*/
|
||||
data object NavigateToAddVaultItem : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the folder.
|
||||
*/
|
||||
data class NavigateToFolderItem(val folderId: String) : VaultItemListingEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the AddSendItemScreen.
|
||||
*/
|
||||
|
@ -1146,6 +1165,13 @@ sealed class VaultItemListingsAction {
|
|||
*/
|
||||
data class ItemClick(val id: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* Click on the folder.
|
||||
*
|
||||
* @property id the id of the folder that has been clicked
|
||||
*/
|
||||
data class FolderClick(val id: String) : VaultItemListingsAction()
|
||||
|
||||
/**
|
||||
* A master password prompt was encountered when trying to perform a senstive action described
|
||||
* by the given [masterPasswordRepromptData] and the given [password] was submitted.
|
||||
|
|
|
@ -18,6 +18,7 @@ data class VaultItemListingHandlers(
|
|||
val searchIconClick: () -> Unit,
|
||||
val addVaultItemClick: () -> Unit,
|
||||
val itemClick: (id: String) -> Unit,
|
||||
val folderClick: (id: String) -> Unit,
|
||||
val masterPasswordRepromptSubmit: (password: String, MasterPasswordRepromptData) -> Unit,
|
||||
val refreshClick: () -> Unit,
|
||||
val syncClick: () -> Unit,
|
||||
|
@ -50,6 +51,7 @@ data class VaultItemListingHandlers(
|
|||
viewModel.trySendAction(VaultItemListingsAction.AddVaultItemClick)
|
||||
},
|
||||
itemClick = { viewModel.trySendAction(VaultItemListingsAction.ItemClick(it)) },
|
||||
folderClick = { viewModel.trySendAction(VaultItemListingsAction.FolderClick(it)) },
|
||||
masterPasswordRepromptSubmit = { password, data ->
|
||||
viewModel.trySendAction(
|
||||
VaultItemListingsAction.MasterPasswordRepromptSubmit(
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.bitwarden.core.SendView
|
|||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
import com.x8bit.bitwarden.data.platform.util.subtitle
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.toHostOrPathOrNull
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
|
@ -18,8 +19,12 @@ 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.getFolders
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.toFolderDisplayName
|
||||
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.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toFilteredList
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.util.toLoginIconData
|
||||
import java.time.Clock
|
||||
|
||||
|
@ -82,19 +87,47 @@ fun SendView.determineListingPredicate(
|
|||
/**
|
||||
* Transforms a list of [CipherView] into [VaultItemListingState.ViewState].
|
||||
*/
|
||||
fun List<CipherView>.toViewState(
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
fun VaultData.toViewState(
|
||||
itemListingType: VaultItemListingState.ItemListingType.Vault,
|
||||
vaultFilterType: VaultFilterType,
|
||||
baseIconUrl: String,
|
||||
isIconLoadingDisabled: Boolean,
|
||||
autofillSelectionData: AutofillSelectionData?,
|
||||
): VaultItemListingState.ViewState =
|
||||
if (isNotEmpty()) {
|
||||
): VaultItemListingState.ViewState {
|
||||
val filteredCipherViewList = cipherViewList
|
||||
.filter { cipherView ->
|
||||
cipherView.determineListingPredicate(itemListingType)
|
||||
}
|
||||
.toFilteredList(vaultFilterType)
|
||||
|
||||
val folderList = if (itemListingType is VaultItemListingState.ItemListingType.Vault.Folder &&
|
||||
!itemListingType.folderId.isNullOrBlank()
|
||||
) {
|
||||
folderViewList.getFolders(itemListingType.folderId)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return if (folderList.isNotEmpty() || filteredCipherViewList.isNotEmpty()) {
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayItemList = toDisplayItemList(
|
||||
displayItemList = filteredCipherViewList.toDisplayItemList(
|
||||
baseIconUrl = baseIconUrl,
|
||||
isIconLoadingDisabled = isIconLoadingDisabled,
|
||||
isAutofill = autofillSelectionData != null,
|
||||
),
|
||||
displayFolderList = folderList.map { folderView ->
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
id = requireNotNull(folderView.id),
|
||||
name = folderView.name,
|
||||
count = this.cipherViewList
|
||||
.count {
|
||||
it.deletedDate == null &&
|
||||
!it.id.isNullOrBlank() &&
|
||||
folderView.id == it.folderId
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// Use the autofill empty message if necessary, otherwise use normal type-specific message
|
||||
|
@ -128,6 +161,7 @@ fun List<CipherView>.toViewState(
|
|||
shouldShowAddButton = shouldShowAddButton,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a list of [CipherView] into [VaultItemListingState.ViewState].
|
||||
|
@ -142,6 +176,7 @@ fun List<SendView>.toViewState(
|
|||
baseWebSendUrl = baseWebSendUrl,
|
||||
clock = clock,
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
)
|
||||
} else {
|
||||
VaultItemListingState.ViewState.NoItems(
|
||||
|
@ -168,6 +203,7 @@ fun VaultItemListingState.ItemListingType.updateWithAdditionalDataIfNecessary(
|
|||
folderName = folderList
|
||||
.find { it.id == folderId }
|
||||
?.name
|
||||
?.toFolderDisplayName(folderList)
|
||||
.orEmpty(),
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.util
|
||||
|
||||
import com.bitwarden.core.FolderView
|
||||
|
||||
private const val FOLDER_DIVIDER: String = "/"
|
||||
|
||||
/**
|
||||
* Retrieves the subfolders of a given [folderId] and updates their names to proper display names.
|
||||
* This function is necessary if we want to show the subfolders for a specific folder.
|
||||
*/
|
||||
@Suppress("ReturnCount")
|
||||
fun List<FolderView>.getFolders(folderId: String): List<FolderView> {
|
||||
val currentFolder = this.find { it.id == folderId } ?: return emptyList()
|
||||
|
||||
// If two folders have the same name the second folder should have no nested folders
|
||||
val firstFolderWithName = this.first { it.name == currentFolder.name }
|
||||
if (firstFolderWithName.id != folderId) return emptyList()
|
||||
|
||||
val folderList = this
|
||||
.getFilteredFolders(currentFolder.name)
|
||||
.map {
|
||||
it.copy(name = it.name.substringAfter(currentFolder.name + FOLDER_DIVIDER))
|
||||
}
|
||||
|
||||
return folderList
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out subfolders of subfolders from the given list. If a [folderName] is provided,
|
||||
* folders that are not subfolders of the specified [folderName] will be filtered out.
|
||||
*/
|
||||
fun List<FolderView>.getFilteredFolders(folderName: String? = null): List<FolderView> =
|
||||
this.filter { folderView ->
|
||||
// If the folder name is not null we filter out folders that are not subfolders.
|
||||
if (folderName != null &&
|
||||
!folderView.name.startsWith(folderName + FOLDER_DIVIDER)
|
||||
) {
|
||||
return@filter false
|
||||
}
|
||||
|
||||
this.forEach {
|
||||
val firstFolder = folderName
|
||||
?.let { name -> folderView.name.substringAfter(name + FOLDER_DIVIDER) }
|
||||
?: folderView.name
|
||||
|
||||
val secondFolder = folderName
|
||||
?.let { name -> it.name.substringAfter(name + FOLDER_DIVIDER) }
|
||||
?: it.name
|
||||
|
||||
// We don't want to compare the folder to itself or itself plus a slash.
|
||||
if (firstFolder == secondFolder || firstFolder == secondFolder + FOLDER_DIVIDER) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
// If the first folder name is blank or the first folder is a subfolder of the second
|
||||
// folder, we want to filter it out.
|
||||
if (firstFolder.isEmpty() ||
|
||||
firstFolder.startsWith(secondFolder + FOLDER_DIVIDER)
|
||||
) {
|
||||
return@filter false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a folder name to a user-friendly display name. This function is necessary because the
|
||||
* folder name we receive is often nested, and we want to extract just the relevant name for
|
||||
* display to the user.
|
||||
*/
|
||||
fun String.toFolderDisplayName(list: List<FolderView>): String {
|
||||
var folderName = this
|
||||
|
||||
// cycle through the list and determine the correct display name of the folder.
|
||||
list.forEach { folderView ->
|
||||
if (this.startsWith(folderView.name + FOLDER_DIVIDER)) {
|
||||
val newName = this.substringAfter(folderView.name + FOLDER_DIVIDER)
|
||||
if (newName.isNotBlank() && newName.length < folderName.length) {
|
||||
folderName = newName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return folderName
|
||||
}
|
|
@ -45,6 +45,9 @@ fun NavGraphBuilder.vaultGraph(
|
|||
onNavigateToVaultAddItemScreen = onNavigateToVaultAddItemScreen,
|
||||
onNavigateToSearchVault = onNavigateToSearchVault,
|
||||
onNavigateToVaultEditItemScreen = onNavigateToVaultEditItemScreen,
|
||||
onNavigateToVaultItemListing = {
|
||||
navController.navigateToVaultItemListing(it)
|
||||
},
|
||||
)
|
||||
|
||||
vaultVerificationCodeDestination(
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
|||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.orNullIfBlank
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.IconData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.util.getFilteredFolders
|
||||
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
|
||||
|
@ -43,7 +44,9 @@ fun VaultData.toViewState(
|
|||
|
||||
val filteredCipherViewList = filteredCipherViewListWithDeletedItems
|
||||
.filter { it.deletedDate == null }
|
||||
val filteredFolderViewList = folderViewList.toFilteredList(vaultFilterType)
|
||||
|
||||
val filteredFolderViewList = folderViewList.toFilteredList(vaultFilterType).getFilteredFolders()
|
||||
|
||||
val filteredCollectionViewList = collectionViewList.toFilteredList(vaultFilterType)
|
||||
val noFolderItems = filteredCipherViewList
|
||||
.filter { it.folderId.isNullOrBlank() }
|
||||
|
|
|
@ -47,6 +47,7 @@ import com.x8bit.bitwarden.ui.util.performLogoutAccountClick
|
|||
import com.x8bit.bitwarden.ui.util.performLogoutAccountConfirmationClick
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.model.ListingItemOverflowAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import com.x8bit.bitwarden.ui.vault.model.VaultItemListingType
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -72,6 +73,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
private var onNavigateToVaultItemId: String? = null
|
||||
private var onNavigateToVaultEditItemScreenId: String? = null
|
||||
private var onNavigateToSearchType: SearchType? = null
|
||||
private var onNavigateToVaultItemListingScreenType: VaultItemListingType? = null
|
||||
|
||||
private val intentManager: IntentManager = mockk {
|
||||
every { shareText(any()) } just runs
|
||||
|
@ -99,6 +101,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
onNavigateToEditSendItem = { onNavigateToEditSendItemId = it },
|
||||
onNavigateToSearch = { onNavigateToSearchType = it },
|
||||
onNavigateToVaultEditItemScreen = { onNavigateToVaultEditItemScreenId = it },
|
||||
onNavigateToVaultItemListing = { this.onNavigateToVaultItemListingScreenType = it },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -403,6 +406,13 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
assertEquals(id, onNavigateToVaultItemId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToFolderItem should call onNavigateToVaultItemListing`() {
|
||||
val itemListingType = VaultItemListingType.Folder("testId")
|
||||
mutableEventFlow.tryEmit(VaultItemListingEvent.NavigateToFolderItem("testId"))
|
||||
assertEquals(itemListingType, onNavigateToVaultItemListingScreenType)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToUrl should call launchUri on the IntentManager`() {
|
||||
val url = "www.test.com"
|
||||
|
@ -539,6 +549,122 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Folders text should be displayed according to state`() {
|
||||
val folders = "Folders"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
name = "test", id = "1", count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(folders))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Folders text count should be displayed according to state`() {
|
||||
val folders = "Folders"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(name = "test", id = "1", count = 0),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(folders))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(folders, 1.toString())
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
name = "test",
|
||||
id = "1",
|
||||
count = 0,
|
||||
),
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
name = "test1",
|
||||
id = "2",
|
||||
count = 0,
|
||||
),
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
name = "test2",
|
||||
id = "3",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNode(hasScrollToNodeAction())
|
||||
.performScrollToNode(hasText(folders))
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folders)
|
||||
.assertIsDisplayed()
|
||||
.assertTextEquals(folders, 3.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `folderDisplayItems should be displayed according to state`() {
|
||||
val folderName = "TestFolder"
|
||||
mutableStateFlow.update { DEFAULT_STATE }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folderName)
|
||||
.assertDoesNotExist()
|
||||
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = emptyList(),
|
||||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
name = folderName,
|
||||
id = "1",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(text = folderName)
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Items text should be displayed according to state`() {
|
||||
val items = "Items"
|
||||
|
@ -553,6 +679,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -578,6 +705,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -598,6 +726,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
createDisplayItem(number = 3),
|
||||
createDisplayItem(number = 4),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -619,6 +748,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
displayItemList = listOf(
|
||||
createDisplayItem(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -644,6 +774,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
shouldShowMasterPasswordReprompt = false,
|
||||
),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -670,6 +801,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
shouldShowMasterPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -720,6 +852,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
shouldShowMasterPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -749,6 +882,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
shouldShowMasterPasswordReprompt = true,
|
||||
),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -882,6 +1016,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(createCipherDisplayItem(number = 1)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -921,6 +1056,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
createCipherDisplayItem(number = 1)
|
||||
.copy(shouldShowMasterPasswordReprompt = true),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -948,6 +1084,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
itemListingType = VaultItemListingState.ItemListingType.Send.SendFile,
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(createDisplayItem(number = 1)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -973,6 +1110,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(createDisplayItem(number = number)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -996,6 +1134,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(createDisplayItem(number = number)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@ -1084,6 +1223,7 @@ class VaultItemListingScreenTest : BaseComposeTest() {
|
|||
it.copy(
|
||||
viewState = VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(createDisplayItem(number = 1)),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -308,6 +308,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
assertEquals(
|
||||
|
@ -356,6 +357,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -472,6 +474,17 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FolderClick for vault item should emit NavigateToFolderItem`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
val testId = "1"
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(VaultItemListingsAction.FolderClick(testId))
|
||||
assertEquals(VaultItemListingEvent.NavigateToFolderItem(testId), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshClick should sync`() = runTest {
|
||||
val viewModel = createVaultItemListingViewModel()
|
||||
|
@ -832,6 +845,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -878,6 +892,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1).copy(isAutofill = true),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
)
|
||||
.copy(
|
||||
|
@ -978,6 +993,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -1084,6 +1100,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
|
@ -1197,6 +1214,7 @@ class VaultItemListingViewModelTest : BaseViewModelTest() {
|
|||
displayItemList = listOf(
|
||||
createMockDisplayItemForCipher(number = 1),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.net.Uri
|
|||
import com.bitwarden.core.CipherRepromptType
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.FolderView
|
||||
import com.bitwarden.core.SendType
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
|
||||
|
@ -15,8 +16,10 @@ 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.data.vault.datasource.sdk.model.createMockSendView
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.VaultData
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.vault.feature.itemlisting.VaultItemListingState
|
||||
import com.x8bit.bitwarden.ui.vault.feature.vault.model.VaultFilterType
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
|
@ -28,6 +31,7 @@ import java.time.Clock
|
|||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class VaultItemListingDataExtensionsTest {
|
||||
|
||||
private val clock: Clock = Clock.fixed(
|
||||
|
@ -355,27 +359,37 @@ class VaultItemListingDataExtensionsTest {
|
|||
number = 1,
|
||||
isDeleted = false,
|
||||
cipherType = CipherType.LOGIN,
|
||||
folderId = "mockId-1",
|
||||
)
|
||||
.copy(reprompt = CipherRepromptType.PASSWORD),
|
||||
createMockCipherView(
|
||||
number = 2,
|
||||
isDeleted = false,
|
||||
cipherType = CipherType.CARD,
|
||||
folderId = "mockId-1",
|
||||
),
|
||||
createMockCipherView(
|
||||
number = 3,
|
||||
isDeleted = false,
|
||||
cipherType = CipherType.SECURE_NOTE,
|
||||
folderId = "mockId-1",
|
||||
),
|
||||
createMockCipherView(
|
||||
number = 4,
|
||||
isDeleted = false,
|
||||
cipherType = CipherType.IDENTITY,
|
||||
folderId = "mockId-1",
|
||||
),
|
||||
)
|
||||
|
||||
val result = cipherViewList.toViewState(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
val result = VaultData(
|
||||
cipherViewList = cipherViewList,
|
||||
collectionViewList = listOf(),
|
||||
folderViewList = listOf(),
|
||||
sendViewList = listOf(),
|
||||
).toViewState(
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder("mockId-1"),
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
autofillSelectionData = null,
|
||||
|
@ -406,6 +420,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
subtitle = null,
|
||||
),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
result,
|
||||
)
|
||||
|
@ -425,17 +440,25 @@ class VaultItemListingDataExtensionsTest {
|
|||
number = 1,
|
||||
isDeleted = false,
|
||||
cipherType = CipherType.LOGIN,
|
||||
folderId = "mockId-1",
|
||||
)
|
||||
.copy(reprompt = CipherRepromptType.PASSWORD),
|
||||
createMockCipherView(
|
||||
number = 2,
|
||||
isDeleted = false,
|
||||
cipherType = CipherType.CARD,
|
||||
folderId = "mockId-1",
|
||||
),
|
||||
)
|
||||
|
||||
val result = cipherViewList.toViewState(
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
val result = VaultData(
|
||||
cipherViewList = cipherViewList,
|
||||
collectionViewList = listOf(),
|
||||
folderViewList = listOf(),
|
||||
sendViewList = listOf(),
|
||||
).toViewState(
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder("mockId-1"),
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
autofillSelectionData = AutofillSelectionData(
|
||||
|
@ -463,6 +486,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
)
|
||||
.copy(isAutofill = true),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
result,
|
||||
)
|
||||
|
@ -471,7 +495,12 @@ class VaultItemListingDataExtensionsTest {
|
|||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState should transform an empty list of CipherViews into a NoItems ViewState with the appropriate data`() {
|
||||
val cipherViewList = emptyList<CipherView>()
|
||||
val vaultData = VaultData(
|
||||
cipherViewList = listOf(),
|
||||
collectionViewList = listOf(),
|
||||
folderViewList = listOf(),
|
||||
sendViewList = listOf(),
|
||||
)
|
||||
|
||||
// Trash
|
||||
assertEquals(
|
||||
|
@ -479,7 +508,8 @@ class VaultItemListingDataExtensionsTest {
|
|||
message = R.string.no_items_trash.asText(),
|
||||
shouldShowAddButton = false,
|
||||
),
|
||||
cipherViewList.toViewState(
|
||||
vaultData.toViewState(
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Trash,
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
|
@ -493,7 +523,8 @@ class VaultItemListingDataExtensionsTest {
|
|||
message = R.string.no_items_folder.asText(),
|
||||
shouldShowAddButton = false,
|
||||
),
|
||||
cipherViewList.toViewState(
|
||||
vaultData.toViewState(
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder(
|
||||
folderId = "folderId",
|
||||
),
|
||||
|
@ -509,7 +540,8 @@ class VaultItemListingDataExtensionsTest {
|
|||
message = R.string.no_items.asText(),
|
||||
shouldShowAddButton = true,
|
||||
),
|
||||
cipherViewList.toViewState(
|
||||
vaultData.toViewState(
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
|
@ -523,7 +555,8 @@ class VaultItemListingDataExtensionsTest {
|
|||
message = R.string.no_items_for_uri.asText("www.test.com"),
|
||||
shouldShowAddButton = true,
|
||||
),
|
||||
cipherViewList.toViewState(
|
||||
vaultData.toViewState(
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Login,
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
|
@ -553,6 +586,7 @@ class VaultItemListingDataExtensionsTest {
|
|||
createMockDisplayItemForSend(number = 1, sendType = SendType.FILE),
|
||||
createMockDisplayItemForSend(number = 2, sendType = SendType.TEXT),
|
||||
),
|
||||
displayFolderList = emptyList(),
|
||||
),
|
||||
result,
|
||||
)
|
||||
|
@ -645,4 +679,42 @@ class VaultItemListingDataExtensionsTest {
|
|||
result,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should properly filter and return the correct folders`() {
|
||||
val vaultData = VaultData(
|
||||
listOf(createMockCipherView(number = 1)),
|
||||
collectionViewList = emptyList(),
|
||||
folderViewList = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
FolderView("2", "test/test", clock.instant()),
|
||||
FolderView("3", "test/", clock.instant()),
|
||||
FolderView("4", "test/test/test/", clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
),
|
||||
sendViewList = emptyList(),
|
||||
)
|
||||
|
||||
val actual = vaultData.toViewState(
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
autofillSelectionData = null,
|
||||
itemListingType = VaultItemListingState.ItemListingType.Vault.Folder("1"),
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultItemListingState.ViewState.Content(
|
||||
displayItemList = listOf(),
|
||||
displayFolderList = listOf(
|
||||
VaultItemListingState.FolderDisplayItem(
|
||||
name = "test",
|
||||
id = "2",
|
||||
count = 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
actual,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
package com.x8bit.bitwarden.ui.vault.feature.util
|
||||
|
||||
import com.bitwarden.core.FolderView
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class FolderViewExtensionsTest {
|
||||
|
||||
private val clock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `getFolders should get the folders for a folderId with the correct names`() {
|
||||
val folderList: List<FolderView> = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
FolderView("2", "test/test", clock.instant()),
|
||||
FolderView("2", "test/Folder", clock.instant()),
|
||||
FolderView("3", "test//", clock.instant()),
|
||||
FolderView("4", "test/test/test/", clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
FolderView("2", "test", clock.instant()),
|
||||
FolderView("2", "Folder", clock.instant()),
|
||||
FolderView("3", "/", clock.instant()),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
folderList.getFolders(1.toString()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getFilteredFolders should properly filter out sub folders in a list`() {
|
||||
val folderList: List<FolderView> = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
FolderView("2", "test/test", clock.instant()),
|
||||
FolderView("3", "test/", clock.instant()),
|
||||
FolderView("4", "test/test/test/", clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
)
|
||||
|
||||
val expected = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
FolderView("3", "test/", clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected,
|
||||
folderList.getFilteredFolders(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toFolderDisplayName should return the correct name`() {
|
||||
val folderName = "Folder/test/2"
|
||||
|
||||
val folderList: List<FolderView> = listOf(
|
||||
FolderView("2", "Folder/test", clock.instant()),
|
||||
FolderView("3", "test/", clock.instant()),
|
||||
FolderView("4", folderName, clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"2",
|
||||
folderName.toFolderDisplayName(folderList),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.vault.feature.vault.util
|
|||
|
||||
import android.net.Uri
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.FolderView
|
||||
import com.bitwarden.core.LoginUriView
|
||||
import com.bitwarden.core.UriMatchType
|
||||
import com.x8bit.bitwarden.R
|
||||
|
@ -22,16 +23,30 @@ import io.mockk.mockkStatic
|
|||
import io.mockk.unmockkStatic
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
|
||||
class VaultDataExtensionsTest {
|
||||
|
||||
private val clock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
ZoneOffset.UTC,
|
||||
)
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `toViewState for AllVaults should transform full VaultData into ViewState Content without filtering`() {
|
||||
val vaultData = VaultData(
|
||||
cipherViewList = listOf(createMockCipherView(number = 1)),
|
||||
collectionViewList = listOf(createMockCollectionView(number = 1)),
|
||||
folderViewList = listOf(createMockFolderView(number = 1)),
|
||||
folderViewList = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
FolderView("2", "test/test", clock.instant()),
|
||||
FolderView("3", "test/", clock.instant()),
|
||||
FolderView("4", "test/test/test/", clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
),
|
||||
sendViewList = listOf(createMockSendView(number = 1)),
|
||||
)
|
||||
|
||||
|
@ -51,11 +66,22 @@ class VaultDataExtensionsTest {
|
|||
favoriteItems = listOf(),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "mockId-1",
|
||||
name = "mockName-1".asText(),
|
||||
itemCount = 1,
|
||||
id = "1",
|
||||
name = "test".asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "3",
|
||||
name = "test/".asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "5",
|
||||
name = "Folder".asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
|
||||
),
|
||||
),
|
||||
collectionItems = listOf(
|
||||
VaultState.ViewState.CollectionItem(
|
||||
id = "mockId-1",
|
||||
|
@ -466,4 +492,60 @@ class VaultDataExtensionsTest {
|
|||
)
|
||||
unmockkStatic(Uri::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toViewState should properly filter nested folders out`() {
|
||||
val vaultData = VaultData(
|
||||
listOf(createMockCipherView(number = 1)),
|
||||
collectionViewList = emptyList(),
|
||||
folderViewList = listOf(
|
||||
FolderView("1", "test", clock.instant()),
|
||||
FolderView("2", "test/test", clock.instant()),
|
||||
FolderView("3", "test/", clock.instant()),
|
||||
FolderView("4", "test/test/test/", clock.instant()),
|
||||
FolderView("5", "Folder", clock.instant()),
|
||||
),
|
||||
sendViewList = emptyList(),
|
||||
)
|
||||
|
||||
val actual = vaultData.toViewState(
|
||||
isPremium = true,
|
||||
isIconLoadingDisabled = false,
|
||||
baseIconUrl = Environment.Us.environmentUrlData.baseIconUrl,
|
||||
vaultFilterType = VaultFilterType.AllVaults,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
VaultState.ViewState.Content(
|
||||
loginItemsCount = 1,
|
||||
cardItemsCount = 0,
|
||||
identityItemsCount = 0,
|
||||
secureNoteItemsCount = 0,
|
||||
favoriteItems = listOf(),
|
||||
folderItems = listOf(
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "1",
|
||||
name = "test".asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "3",
|
||||
name = "test/".asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
VaultState.ViewState.FolderItem(
|
||||
id = "5",
|
||||
name = "Folder".asText(),
|
||||
itemCount = 0,
|
||||
),
|
||||
|
||||
),
|
||||
collectionItems = listOf(),
|
||||
noFolderItems = listOf(),
|
||||
trashItemsCount = 0,
|
||||
totpItemsCount = 1,
|
||||
),
|
||||
actual,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue