mirror of
https://github.com/bitwarden/android.git
synced 2026-08-28 22:45:25 -05:00
PM-18032 Adding a new folder while adding or editing an item. (#4731)
This commit is contained in:
+6
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.platform.components.bottomsheet
|
||||
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -30,6 +31,7 @@ import kotlinx.coroutines.launch
|
||||
* @param onDismiss The action to perform when the bottom sheet is dismissed will also be performed
|
||||
* when the "close" icon is clicked, caller must handle any desired animation or hiding of the
|
||||
* bottom sheet. This will be invoked _after_ the sheet has been animated away.
|
||||
* @param topBarActions Row of actions to add the top bar of the bottom sheet.
|
||||
* @param showBottomSheet Whether or not to show the bottom sheet, by default this is true assuming
|
||||
* the showing/hiding will be handled by the caller.
|
||||
* @param sheetContent Content to display in the bottom sheet. The content is passed the padding
|
||||
@@ -42,6 +44,7 @@ fun BitwardenModalBottomSheet(
|
||||
sheetTitle: String,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
topBarActions: @Composable RowScope.(animatedOnDismiss: () -> Unit) -> Unit = {},
|
||||
showBottomSheet: Boolean = true,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
sheetContent: @Composable (animatedOnDismiss: () -> Unit) -> Unit,
|
||||
@@ -68,6 +71,9 @@ fun BitwardenModalBottomSheet(
|
||||
onNavigationIconClick = animatedOnDismiss,
|
||||
navigationIconContentDescription = stringResource(R.string.close),
|
||||
),
|
||||
actions = {
|
||||
topBarActions(animatedOnDismiss)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
minimumHeight = 64.dp,
|
||||
)
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package com.x8bit.bitwarden.ui.platform.components.button
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag
|
||||
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
|
||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* A button which uses a read-only text field for layout and style purposes.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BitwardenTextSelectionButton(
|
||||
label: String,
|
||||
selectedOption: String?,
|
||||
onClick: () -> Unit,
|
||||
cardStyle: CardStyle?,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = false,
|
||||
tooltipEnabled: Boolean = true,
|
||||
supportingText: String? = null,
|
||||
tooltip: TooltipData? = null,
|
||||
insets: PaddingValues = PaddingValues(),
|
||||
textFieldTestTag: String? = null,
|
||||
semanticRole: Role = Role.Button,
|
||||
semanticContentDescription: String = supportingText
|
||||
?.let { "$selectedOption. $label. $it" }
|
||||
?: "$selectedOption. $label",
|
||||
customAccessibilityActions: List<CustomAccessibilityAction> = listOfNotNull(
|
||||
tooltip?.let {
|
||||
CustomAccessibilityAction(
|
||||
label = it.contentDescription,
|
||||
action = {
|
||||
it.onClick()
|
||||
true
|
||||
},
|
||||
)
|
||||
},
|
||||
),
|
||||
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clearAndSetSemantics {
|
||||
role = semanticRole
|
||||
contentDescription = semanticContentDescription
|
||||
customActions = customAccessibilityActions
|
||||
}
|
||||
.cardStyle(
|
||||
cardStyle = cardStyle,
|
||||
paddingTop = 6.dp,
|
||||
paddingBottom = 0.dp,
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(paddingValues = insets),
|
||||
) {
|
||||
TextField(
|
||||
textStyle = BitwardenTheme.typography.bodyLarge,
|
||||
readOnly = true,
|
||||
label = {
|
||||
Row {
|
||||
Text(
|
||||
text = label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
tooltip?.let {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = R.drawable.ic_question_circle_small,
|
||||
contentDescription = it.contentDescription,
|
||||
onClick = it.onClick,
|
||||
isEnabled = tooltipEnabled,
|
||||
contentColor = BitwardenTheme.colorScheme.icon.secondary,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
BitwardenRowOfActions(
|
||||
modifier = Modifier.padding(paddingValues = actionsPadding),
|
||||
actions = {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_chevron_down),
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.minimumInteractiveComponentSize(),
|
||||
)
|
||||
actions()
|
||||
},
|
||||
)
|
||||
},
|
||||
value = selectedOption.orEmpty(),
|
||||
onValueChange = {},
|
||||
enabled = enabled,
|
||||
colors = bitwardenTextFieldButtonColors(),
|
||||
modifier = Modifier
|
||||
.nullableTestTag(tag = textFieldTestTag)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
supportingText
|
||||
?.let { content ->
|
||||
Spacer(modifier = Modifier.height(height = 6.dp))
|
||||
BitwardenHorizontalDivider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp),
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
content = {
|
||||
Text(
|
||||
text = content,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
?: Spacer(modifier = Modifier.height(height = cardStyle?.let { 6.dp } ?: 0.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun BitwardenTextSelectionButton_preview() {
|
||||
BitwardenTheme {
|
||||
BitwardenTextSelectionButton(
|
||||
label = "Folder",
|
||||
selectedOption = "No Folder",
|
||||
onClick = {},
|
||||
cardStyle = CardStyle.Full,
|
||||
)
|
||||
}
|
||||
}
|
||||
+20
-126
@@ -1,50 +1,23 @@
|
||||
package com.x8bit.bitwarden.ui.platform.components.dropdown
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.nullableTestTag
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenSelectionDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.row.BitwardenSelectionRow
|
||||
import com.x8bit.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.color.bitwardenTextFieldButtonColors
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.TooltipData
|
||||
import com.x8bit.bitwarden.ui.platform.components.row.BitwardenRowOfActions
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@@ -73,7 +46,6 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
* in the app bar's trailing side. This lambda extends [RowScope], allowing flexibility in
|
||||
* defining the layout of the actions.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun BitwardenMultiSelectButton(
|
||||
label: String,
|
||||
@@ -92,103 +64,25 @@ fun BitwardenMultiSelectButton(
|
||||
) {
|
||||
var shouldShowDialog by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.clearAndSetSemantics {
|
||||
role = Role.DropdownList
|
||||
contentDescription = supportingText
|
||||
?.let { "$selectedOption. $label. $it" }
|
||||
?: "$selectedOption. $label"
|
||||
customActions = listOfNotNull(
|
||||
tooltip?.let {
|
||||
CustomAccessibilityAction(
|
||||
label = it.contentDescription,
|
||||
action = {
|
||||
it.onClick()
|
||||
true
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
.cardStyle(
|
||||
cardStyle = cardStyle,
|
||||
paddingTop = 6.dp,
|
||||
paddingBottom = 0.dp,
|
||||
onClick = { shouldShowDialog = !shouldShowDialog },
|
||||
)
|
||||
.padding(paddingValues = insets),
|
||||
) {
|
||||
TextField(
|
||||
textStyle = BitwardenTheme.typography.bodyLarge,
|
||||
readOnly = true,
|
||||
label = {
|
||||
Row {
|
||||
Text(
|
||||
text = label,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
tooltip?.let {
|
||||
Spacer(modifier = Modifier.width(3.dp))
|
||||
BitwardenStandardIconButton(
|
||||
vectorIconRes = R.drawable.ic_question_circle_small,
|
||||
contentDescription = it.contentDescription,
|
||||
onClick = it.onClick,
|
||||
isEnabled = isEnabled,
|
||||
contentColor = BitwardenTheme.colorScheme.icon.secondary,
|
||||
modifier = Modifier.size(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingIcon = {
|
||||
BitwardenRowOfActions(
|
||||
modifier = Modifier.padding(paddingValues = actionsPadding),
|
||||
actions = {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_chevron_down),
|
||||
contentDescription = null,
|
||||
tint = BitwardenTheme.colorScheme.icon.primary,
|
||||
modifier = Modifier.minimumInteractiveComponentSize(),
|
||||
)
|
||||
actions()
|
||||
},
|
||||
)
|
||||
},
|
||||
value = selectedOption.orEmpty(),
|
||||
onValueChange = onOptionSelected,
|
||||
enabled = shouldShowDialog,
|
||||
colors = bitwardenTextFieldButtonColors(),
|
||||
modifier = Modifier
|
||||
.nullableTestTag(tag = textFieldTestTag)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
supportingText
|
||||
?.let { content ->
|
||||
Spacer(modifier = Modifier.height(height = 6.dp))
|
||||
BitwardenHorizontalDivider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp),
|
||||
)
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 48.dp)
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||
content = {
|
||||
Text(
|
||||
text = content,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.secondary,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
?: Spacer(modifier = Modifier.height(height = cardStyle?.let { 6.dp } ?: 0.dp))
|
||||
}
|
||||
BitwardenTextSelectionButton(
|
||||
label = label,
|
||||
selectedOption = selectedOption,
|
||||
onClick = {
|
||||
shouldShowDialog = true
|
||||
},
|
||||
cardStyle = cardStyle,
|
||||
enabled = shouldShowDialog,
|
||||
tooltipEnabled = isEnabled,
|
||||
supportingText = supportingText,
|
||||
tooltip = tooltip,
|
||||
insets = insets,
|
||||
textFieldTestTag = textFieldTestTag,
|
||||
actionsPadding = actionsPadding,
|
||||
actions = actions,
|
||||
semanticRole = Role.DropdownList,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
if (shouldShowDialog) {
|
||||
BitwardenSelectionDialog(
|
||||
title = label,
|
||||
|
||||
+3
-7
@@ -18,6 +18,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenActionCard
|
||||
import com.x8bit.bitwarden.ui.platform.components.card.BitwardenInfoCalloutCard
|
||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkScope
|
||||
@@ -149,15 +150,10 @@ fun CoachMarkScope<AddEditItemCoachMark>.VaultAddEditContent(
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenMultiSelectButton(
|
||||
BitwardenTextSelectionButton(
|
||||
label = stringResource(id = R.string.folder),
|
||||
options = state.common.availableFolders.map { it.name }.toImmutableList(),
|
||||
selectedOption = state.common.selectedFolder?.name,
|
||||
onOptionSelected = { selectedFolderName ->
|
||||
commonTypeHandlers.onFolderSelected(
|
||||
state.common.availableFolders.first { it.name == selectedFolderName },
|
||||
)
|
||||
},
|
||||
onClick = commonTypeHandlers.onSelectOrAddFolderForItem,
|
||||
cardStyle = if (isAddItemMode && state.common.hasOrganizations) {
|
||||
CardStyle.Top(dividerPadding = 0.dp)
|
||||
} else {
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -14,11 +27,14 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -26,10 +42,13 @@ import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.cardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.action.BitwardenOverflowActionItem
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.action.OverflowMenuItemData
|
||||
import com.x8bit.bitwarden.ui.platform.components.bottomsheet.BitwardenModalBottomSheet
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.CoachMarkContainer
|
||||
import com.x8bit.bitwarden.ui.platform.components.coachmark.rememberLazyListCoachMarkState
|
||||
@@ -41,7 +60,11 @@ import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenMasterPassword
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenOverwritePasskeyConfirmationDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenPinDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenTwoButtonDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
|
||||
import com.x8bit.bitwarden.ui.platform.components.radio.BitwardenRadioButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
|
||||
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalBiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.composition.LocalExitManager
|
||||
@@ -53,6 +76,7 @@ import com.x8bit.bitwarden.ui.platform.manager.biometrics.BiometricsManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.exit.ExitManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.platform.manager.permissions.PermissionsManager
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.platform.util.persistentListOfNotNull
|
||||
import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditCardTypeHandlers
|
||||
@@ -61,6 +85,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditIdentit
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditLoginTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditSshKeyTypeHandlers
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.handlers.VaultAddEditUserVerificationHandlers
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -394,6 +420,11 @@ fun VaultAddEditScreen(
|
||||
.imePadding()
|
||||
.fillMaxSize(),
|
||||
)
|
||||
FolderSelectionBottomSheet(
|
||||
state = viewState.common,
|
||||
handlers = commonTypeHandlers,
|
||||
showBottomSheet = state.shouldShowFolderSelectionBottomSheet,
|
||||
)
|
||||
}
|
||||
|
||||
is VaultAddEditState.ViewState.Error -> {
|
||||
@@ -517,3 +548,153 @@ private fun VaultAddEditItemDialogs(
|
||||
null -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun FolderSelectionBottomSheet(
|
||||
state: VaultAddEditState.ViewState.Content.Common,
|
||||
handlers: VaultAddEditCommonHandlers,
|
||||
showBottomSheet: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var selectedOptionState by rememberSaveable {
|
||||
mutableStateOf(state.selectedFolder?.name.orEmpty())
|
||||
}
|
||||
BitwardenModalBottomSheet(
|
||||
sheetTitle = stringResource(R.string.folders),
|
||||
onDismiss = handlers.onDismissFolderSelectionSheet,
|
||||
topBarActions = { animatedOnDismiss ->
|
||||
BitwardenTextButton(
|
||||
label = stringResource(R.string.save),
|
||||
onClick = {
|
||||
handlers.onDismissFolderSelectionSheet()
|
||||
state
|
||||
.availableFolders
|
||||
.firstOrNull {
|
||||
it.name == selectedOptionState
|
||||
}
|
||||
?.run {
|
||||
handlers.onChangeToExistingFolder(this.id)
|
||||
}
|
||||
?: run {
|
||||
handlers.onOnAddFolder(selectedOptionState)
|
||||
}
|
||||
animatedOnDismiss()
|
||||
},
|
||||
isEnabled = selectedOptionState.isNotBlank(),
|
||||
)
|
||||
},
|
||||
showBottomSheet = showBottomSheet,
|
||||
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
) {
|
||||
FolderSelectionBottomSheetContent(
|
||||
options = state.availableFolders.map { it.name }.toImmutableList(),
|
||||
selectedOption = selectedOptionState,
|
||||
onOptionSelected = {
|
||||
selectedOptionState = it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun FolderSelectionBottomSheetContent(
|
||||
options: ImmutableList<String>,
|
||||
selectedOption: String,
|
||||
onOptionSelected: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier
|
||||
.standardHorizontalMargin(),
|
||||
) {
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
itemsIndexed(options) { index, option ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.cardStyle(
|
||||
cardStyle = if (index == 0) {
|
||||
CardStyle.Top()
|
||||
} else {
|
||||
CardStyle.Middle()
|
||||
},
|
||||
onClick = {
|
||||
onOptionSelected(option)
|
||||
},
|
||||
),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = option,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
style = BitwardenTheme.typography.bodyLarge,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
BitwardenRadioButton(
|
||||
isSelected = selectedOption == option,
|
||||
onClick = {
|
||||
onOptionSelected(option)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
var inEditMode by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
var addFolderText by rememberSaveable {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val cardStyle = if (options.isEmpty()) CardStyle.Full else CardStyle.Bottom
|
||||
if (inEditMode) {
|
||||
BitwardenTextField(
|
||||
label = stringResource(R.string.add_folder),
|
||||
value = addFolderText,
|
||||
onValueChange = {
|
||||
addFolderText = it
|
||||
onOptionSelected(it)
|
||||
},
|
||||
autoFocus = true,
|
||||
cardStyle = cardStyle,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
actions = {
|
||||
BitwardenRadioButton(
|
||||
isSelected = selectedOption == addFolderText,
|
||||
onClick = {
|
||||
onOptionSelected(addFolderText)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
BitwardenClickableText(
|
||||
label = stringResource(id = R.string.add_folder),
|
||||
onClick = {
|
||||
onOptionSelected(addFolderText)
|
||||
inEditMode = true
|
||||
},
|
||||
leadingIcon = painterResource(id = R.drawable.ic_plus_small),
|
||||
style = BitwardenTheme.typography.labelMedium,
|
||||
innerPadding = PaddingValues(all = 16.dp),
|
||||
cornerSize = 0.dp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.cardStyle(cardStyle = cardStyle, paddingVertical = 0.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+120
-4
@@ -3,7 +3,9 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.FolderView
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.BreachCountResult
|
||||
@@ -35,6 +37,7 @@ import com.x8bit.bitwarden.data.tools.generator.repository.model.GeneratorResult
|
||||
import com.x8bit.bitwarden.data.vault.datasource.network.model.PolicyTypeJson
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
@@ -50,6 +53,7 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.appendFolderAndOwnerData
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toAvailableFolders
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toItemType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
|
||||
@@ -176,6 +180,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
shouldExitOnSave = shouldExitOnSave,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldClearSpecialCircumstance = autofillSelectionData == null,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
},
|
||||
) {
|
||||
@@ -206,6 +211,12 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
vaultRepository
|
||||
.foldersStateFlow
|
||||
.map { VaultAddEditAction.Internal.AvailableFoldersReceive(folderData = it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
generatorRepository
|
||||
.generatorResultFlow
|
||||
.map { result ->
|
||||
@@ -251,7 +262,6 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
handleCustomFieldValueChange(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Common.FolderChange -> handleFolderTextInputChange(action)
|
||||
is VaultAddEditAction.Common.NameTextChange -> handleNameTextInputChange(action)
|
||||
is VaultAddEditAction.Common.NotesTextChange -> handleNotesTextInputChange(action)
|
||||
is VaultAddEditAction.Common.OwnershipChange -> handleOwnershipTextInputChange(action)
|
||||
@@ -335,6 +345,19 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
VaultAddEditAction.Common.DismissFido2VerificationDialogClick -> {
|
||||
handleDismissFido2VerificationDialogClick()
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Common.FolderChange -> handleFolderTextInputChange(action)
|
||||
VaultAddEditAction.Common.DismissFolderSelectionBottomSheet -> {
|
||||
handleDismissFolderSelectionBottomSheet()
|
||||
}
|
||||
|
||||
VaultAddEditAction.Common.SelectOrAddFolderForItem -> {
|
||||
handleSelectOrAddFolderForItem()
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Common.AddNewFolder -> {
|
||||
handleAddNewFolder(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +717,36 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
showFido2ErrorDialog()
|
||||
}
|
||||
|
||||
private fun handleDismissFolderSelectionBottomSheet() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowFolderSelectionBottomSheet = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSelectOrAddFolderForItem() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowFolderSelectionBottomSheet = true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddNewFolder(action: VaultAddEditAction.Common.AddNewFolder) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = VaultAddEditState.DialogState.Loading(R.string.saving.asText()),
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result = vaultRepository.createFolder(
|
||||
FolderView(
|
||||
name = action.newFolderName,
|
||||
id = null,
|
||||
revisionDate = DateTime.now(),
|
||||
),
|
||||
)
|
||||
sendAction(VaultAddEditAction.Internal.AddFolderResultReceive(result = result))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddNewCustomFieldClick(
|
||||
action: VaultAddEditAction.Common.AddNewCustomFieldClick,
|
||||
) {
|
||||
@@ -792,7 +845,7 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
action: VaultAddEditAction.Common.FolderChange,
|
||||
) {
|
||||
updateCommonContent { commonContent ->
|
||||
commonContent.copy(selectedFolderId = action.folder.id)
|
||||
commonContent.copy(selectedFolderId = action.folderId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1427,9 +1480,29 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
is VaultAddEditAction.Internal.ShouldShowAddLoginCoachMarkValueChangeReceive -> {
|
||||
handleShouldShowAddLoginCoachMarkValueChange(action)
|
||||
}
|
||||
|
||||
is VaultAddEditAction.Internal.AddFolderResultReceive -> handleAddFolderResult(action)
|
||||
is VaultAddEditAction.Internal.AvailableFoldersReceive -> {
|
||||
handleAvailableFoldersReceive(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAvailableFoldersReceive(
|
||||
action: VaultAddEditAction.Internal.AvailableFoldersReceive,
|
||||
) {
|
||||
action
|
||||
.folderData
|
||||
.data
|
||||
?.let {
|
||||
updateCommonContent { commonContent ->
|
||||
commonContent.copy(
|
||||
availableFolders = it.toAvailableFolders(resourceManager = resourceManager),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleShouldShowAddLoginCoachMarkValueChange(
|
||||
action: VaultAddEditAction.Internal.ShouldShowAddLoginCoachMarkValueChangeReceive,
|
||||
) {
|
||||
@@ -1440,6 +1513,19 @@ class VaultAddEditViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddFolderResult(action: VaultAddEditAction.Internal.AddFolderResultReceive) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
dialog = null,
|
||||
)
|
||||
}
|
||||
updateCommonContent {
|
||||
it.copy(
|
||||
selectedFolderId = (action.result as? CreateFolderResult.Success)?.folderView?.id,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCreateCipherResultReceive(
|
||||
action: VaultAddEditAction.Internal.CreateCipherResultReceive,
|
||||
) {
|
||||
@@ -1943,6 +2029,7 @@ data class VaultAddEditState(
|
||||
val vaultAddEditType: VaultAddEditType,
|
||||
val viewState: ViewState,
|
||||
val dialog: DialogState?,
|
||||
val shouldShowFolderSelectionBottomSheet: Boolean,
|
||||
val shouldShowCloseButton: Boolean = true,
|
||||
// Internal
|
||||
val shouldExitOnSave: Boolean = false,
|
||||
@@ -2621,9 +2708,9 @@ sealed class VaultAddEditAction {
|
||||
/**
|
||||
* Fired when the folder text input is changed.
|
||||
*
|
||||
* @property folder The new folder text.
|
||||
* @property folderId The new folder id.
|
||||
*/
|
||||
data class FolderChange(val folder: VaultAddEditState.Folder) : Common()
|
||||
data class FolderChange(val folderId: String?) : Common()
|
||||
|
||||
/**
|
||||
* Fired when the Favorite toggle is changed.
|
||||
@@ -2767,6 +2854,21 @@ sealed class VaultAddEditAction {
|
||||
* The user has clicked to dismiss the FIDO 2 password or PIN verification dialog.
|
||||
*/
|
||||
data object DismissFido2VerificationDialogClick : Common()
|
||||
|
||||
/**
|
||||
* The user has clicked on folder selection card for the item.
|
||||
*/
|
||||
data object SelectOrAddFolderForItem : Common()
|
||||
|
||||
/**
|
||||
* The user has dismissed the folder selection bottom sheet.
|
||||
*/
|
||||
data object DismissFolderSelectionBottomSheet : Common()
|
||||
|
||||
/**
|
||||
* The user has selected to add a new folder to associate with the item.
|
||||
*/
|
||||
data class AddNewFolder(val newFolderName: String) : Common()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3164,5 +3266,19 @@ sealed class VaultAddEditAction {
|
||||
data class ShouldShowAddLoginCoachMarkValueChangeReceive(
|
||||
val shouldShowCoachMarkTour: Boolean,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Received a result for attempting to add a folder.
|
||||
*/
|
||||
data class AddFolderResultReceive(
|
||||
val result: CreateFolderResult,
|
||||
) : Internal()
|
||||
|
||||
/**
|
||||
* Received an update to the available folders.
|
||||
*/
|
||||
data class AvailableFoldersReceive(
|
||||
val folderData: DataState<List<FolderView>>,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
+26
-6
@@ -12,7 +12,7 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCollection
|
||||
* within the context of adding items to a vault.
|
||||
*
|
||||
* @property onNameTextChange Handles the action when the name text is changed.
|
||||
* @property onFolderSelected Handles the action when a folder is selected.
|
||||
* @property onSelectOrAddFolderForItem Handles the action when a folder is selected.
|
||||
* @property onToggleFavorite Handles the action when the favorite toggle is changed.
|
||||
* @property onToggleMasterPasswordReprompt Handles the action when the master password
|
||||
* reprompt toggle is changed.
|
||||
@@ -27,7 +27,6 @@ import com.x8bit.bitwarden.ui.vault.model.VaultCollection
|
||||
@Suppress("LongParameterList")
|
||||
data class VaultAddEditCommonHandlers(
|
||||
val onNameTextChange: (String) -> Unit,
|
||||
val onFolderSelected: (VaultAddEditState.Folder) -> Unit,
|
||||
val onToggleFavorite: (Boolean) -> Unit,
|
||||
val onToggleMasterPasswordReprompt: (Boolean) -> Unit,
|
||||
val onNotesTextChange: (String) -> Unit,
|
||||
@@ -38,6 +37,10 @@ data class VaultAddEditCommonHandlers(
|
||||
val onCustomFieldActionSelect: (CustomFieldAction, VaultAddEditState.Custom) -> Unit,
|
||||
val onCollectionSelect: (VaultCollection) -> Unit,
|
||||
val onHiddenFieldVisibilityChange: (Boolean) -> Unit,
|
||||
val onSelectOrAddFolderForItem: () -> Unit,
|
||||
val onDismissFolderSelectionSheet: () -> Unit,
|
||||
val onChangeToExistingFolder: (String?) -> Unit,
|
||||
val onOnAddFolder: (String) -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
@@ -54,11 +57,9 @@ data class VaultAddEditCommonHandlers(
|
||||
VaultAddEditAction.Common.NameTextChange(newName),
|
||||
)
|
||||
},
|
||||
onFolderSelected = { newFolder ->
|
||||
onSelectOrAddFolderForItem = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.FolderChange(
|
||||
newFolder,
|
||||
),
|
||||
VaultAddEditAction.Common.SelectOrAddFolderForItem,
|
||||
)
|
||||
},
|
||||
onToggleFavorite = { isFavorite ->
|
||||
@@ -123,6 +124,25 @@ data class VaultAddEditCommonHandlers(
|
||||
VaultAddEditAction.Common.HiddenFieldVisibilityChange(isVisible = it),
|
||||
)
|
||||
},
|
||||
onDismissFolderSelectionSheet = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.DismissFolderSelectionBottomSheet,
|
||||
)
|
||||
},
|
||||
onChangeToExistingFolder = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.FolderChange(
|
||||
folderId = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onOnAddFolder = {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.AddNewFolder(
|
||||
newFolderName = it,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-2
@@ -178,7 +178,11 @@ private fun List<FolderView>.toSelectedFolderId(cipherView: CipherView?): String
|
||||
?.folderId
|
||||
?.takeIf { id -> id in map { it.id } }
|
||||
|
||||
private fun List<FolderView>.toAvailableFolders(
|
||||
/**
|
||||
* Maps a list of [FolderView]s to a list of available [VaultAddEditState.Folder]s with
|
||||
* a default first item of "None."
|
||||
*/
|
||||
fun List<FolderView>.toAvailableFolders(
|
||||
resourceManager: ResourceManager,
|
||||
): List<VaultAddEditState.Folder> =
|
||||
listOf(
|
||||
@@ -188,7 +192,7 @@ private fun List<FolderView>.toAvailableFolders(
|
||||
),
|
||||
)
|
||||
.plus(
|
||||
map { VaultAddEditState.Folder(name = it.name, id = it.id.toString()) },
|
||||
map { VaultAddEditState.Folder(name = it.name, id = it.id) },
|
||||
)
|
||||
|
||||
private fun UserState.Account.toSelectedOwnerId(cipherView: CipherView?): String? =
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="TypographyEllipsis,TypographyDashes">
|
||||
<string name="about">About</string>
|
||||
<string name="add">Add</string>
|
||||
<string name="add_folder">Add Folder</string>
|
||||
<string name="add_folder">Add folder</string>
|
||||
<string name="add_item">Add Item</string>
|
||||
<string name="an_error_has_occurred">An error has occurred.</string>
|
||||
<string name="back">Back</string>
|
||||
|
||||
+143
-14
@@ -1,6 +1,9 @@
|
||||
package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
@@ -31,6 +34,7 @@ import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performSemanticsAction
|
||||
import androidx.compose.ui.test.performTextClearance
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
@@ -38,6 +42,7 @@ import androidx.core.net.toUri
|
||||
import com.bitwarden.vault.UriMatchType
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2RegisterCredentialResult
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
|
||||
import com.x8bit.bitwarden.ui.autofill.fido2.manager.Fido2CompletionManager
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
@@ -2466,7 +2471,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking a Folder Option should send FolderChange action`() {
|
||||
fun `clicking a Folder Option should send SelectOrAddFolderForItem action`() {
|
||||
updateStateWithFolders()
|
||||
|
||||
// Opens the menu
|
||||
@@ -2474,21 +2479,9 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
.onNodeWithContentDescriptionAfterScroll(label = "No Folder. Folder")
|
||||
.performClick()
|
||||
|
||||
// Choose the option from the menu
|
||||
composeTestRule
|
||||
.onAllNodesWithText(text = "mockFolderName-1")
|
||||
.onLast()
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
VaultAddEditAction.Common.FolderChange(
|
||||
VaultAddEditState.Folder(
|
||||
id = "mockFolderId-1",
|
||||
name = "mockFolderName-1",
|
||||
),
|
||||
),
|
||||
VaultAddEditAction.Common.SelectOrAddFolderForItem,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2510,6 +2503,135 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should show folder selection bottom sheet when state updates to true`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowFolderSelectionBottomSheet = true)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Folders")
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Add folder")
|
||||
.assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DismissFolderSelectionBottomSheet action sent when bottom sheet close button click`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowFolderSelectionBottomSheet = true)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Folders")
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithContentDescription("Close")
|
||||
.filterToOne(hasAnySibling(hasText("Folders")))
|
||||
.assertIsDisplayed()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
dispatcher.advanceTimeByAndRunCurrent(1000L)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.DismissFolderSelectionBottomSheet)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `Clicking add folder button in bottom sheet hides add button and replaced with TextField`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowFolderSelectionBottomSheet = true)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Folders")
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Add folder")
|
||||
.assertIsDisplayed()
|
||||
.assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.EditableText))
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Add folder")
|
||||
.assertIsDisplayed()
|
||||
.assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.EditableText))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Editing the add folder text and clicking save send AddFolder action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(shouldShowFolderSelectionBottomSheet = true)
|
||||
}
|
||||
val newFolderName = "newFolderName"
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Folders")
|
||||
.assertIsDisplayed()
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Add folder")
|
||||
.assertIsDisplayed()
|
||||
.assert(SemanticsMatcher.keyNotDefined(SemanticsProperties.EditableText))
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText("Add folder")
|
||||
.assertIsDisplayed()
|
||||
.assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.EditableText))
|
||||
.performTextInput(newFolderName)
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Save")
|
||||
.filterToOne(hasAnySibling(hasText("Folders")))
|
||||
.assertIsDisplayed()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.AddNewFolder(newFolderName))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Selecting existing option and clicking save on folder sheet sends FolderChange action`() {
|
||||
val folderId = "1234"
|
||||
val folderName = "name"
|
||||
mutableStateFlow.update { currentState ->
|
||||
updateCommonContent(currentState) {
|
||||
copy(
|
||||
availableFolders =
|
||||
listOf(
|
||||
VaultAddEditState.Folder(
|
||||
id = folderId,
|
||||
name = folderName,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
.copy(shouldShowFolderSelectionBottomSheet = true)
|
||||
}
|
||||
|
||||
composeTestRule
|
||||
.onNodeWithText(folderName)
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
composeTestRule
|
||||
.onAllNodesWithText("Save")
|
||||
.filterToOne(hasAnySibling(hasText("Folders")))
|
||||
.assertIsDisplayed()
|
||||
.performSemanticsAction(SemanticsActions.OnClick)
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.FolderChange(folderId = folderId))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `toggling the favorite toggle should send ToggleFavorite action`() {
|
||||
mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES
|
||||
@@ -3885,6 +4007,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
dialog = VaultAddEditState.DialogState.Generic(message = "test".asText()),
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.LOGIN),
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_LOGIN = VaultAddEditState(
|
||||
@@ -3896,6 +4019,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
),
|
||||
dialog = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_IDENTITY = VaultAddEditState(
|
||||
@@ -3907,6 +4031,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
),
|
||||
dialog = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_CARD = VaultAddEditState(
|
||||
@@ -3918,6 +4043,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
),
|
||||
dialog = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SECURE_NOTES_CUSTOM_FIELDS = VaultAddEditState(
|
||||
@@ -3939,6 +4065,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
dialog = null,
|
||||
vaultAddEditType = VaultAddEditType.AddItem(VaultItemCipherType.SECURE_NOTE),
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SECURE_NOTES = VaultAddEditState(
|
||||
@@ -3950,6 +4077,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
),
|
||||
dialog = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
private val DEFAULT_STATE_SSH_KEYS = VaultAddEditState(
|
||||
@@ -3961,6 +4089,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
|
||||
),
|
||||
dialog = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
private val ALTERED_COLLECTIONS = listOf(
|
||||
|
||||
+146
-7
@@ -3,6 +3,7 @@ package com.x8bit.bitwarden.ui.vault.feature.addedit
|
||||
import android.content.pm.SigningInfo
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.send.SendView
|
||||
import com.bitwarden.vault.CipherView
|
||||
import com.bitwarden.vault.CollectionView
|
||||
@@ -54,6 +55,7 @@ import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createViewCollectionV
|
||||
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createViewExceptPasswordsCollectionView
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.CreateFolderResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.DeleteCipherResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.TotpCodeResult
|
||||
import com.x8bit.bitwarden.data.vault.repository.model.UpdateCipherResult
|
||||
@@ -66,7 +68,6 @@ import com.x8bit.bitwarden.ui.tools.feature.generator.model.GeneratorMode
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldAction
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.CustomFieldType
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.toCustomField
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.createMockPasskeyAttestationOptions
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDefaultAddTypeContent
|
||||
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toViewState
|
||||
@@ -133,6 +134,9 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
private val totpTestCodeFlow: MutableSharedFlow<TotpCodeResult> = bufferedMutableSharedFlow()
|
||||
|
||||
private val mutableVaultDataFlow = MutableStateFlow<DataState<VaultData>>(DataState.Loading)
|
||||
private val mutableFolderStateFlow = MutableStateFlow<DataState<List<FolderView>>>(
|
||||
DataState.Loading,
|
||||
)
|
||||
private val resourceManager: ResourceManager = mockk {
|
||||
every { getString(R.string.folder_none) } returns "No Folder"
|
||||
}
|
||||
@@ -154,6 +158,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
private val vaultRepository: VaultRepository = mockk {
|
||||
every { vaultDataStateFlow } returns mutableVaultDataFlow
|
||||
every { totpCodeFlow } returns totpTestCodeFlow
|
||||
every { foldersStateFlow } returns mutableFolderStateFlow
|
||||
}
|
||||
|
||||
private val mockAuthRepository = mockk<AuthRepository>(relaxed = true)
|
||||
@@ -187,7 +192,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
unmockkStatic(CipherView::toViewState)
|
||||
unmockkStatic(CustomFieldType::toCustomField)
|
||||
unmockkStatic(UUID::randomUUID)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -204,6 +209,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
shouldShowCloseButton = true,
|
||||
shouldExitOnSave = false,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
val viewModel = createAddVaultItemViewModel(
|
||||
savedStateHandle = createSavedStateHandleWithState(
|
||||
@@ -285,6 +291,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
),
|
||||
dialog = null,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
@@ -3151,10 +3158,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `FolderChange should update folder`() = runTest {
|
||||
val action = VaultAddEditAction.Common.FolderChange(
|
||||
VaultAddEditState.Folder(
|
||||
id = "mockId-1",
|
||||
name = "Folder 1",
|
||||
),
|
||||
folderId = "mockId-1",
|
||||
)
|
||||
|
||||
viewModel.trySendAction(action)
|
||||
@@ -3171,6 +3175,139 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SelectOrAddFolderFoItem should update state to show bottom sheet`() = runTest {
|
||||
val action = VaultAddEditAction.Common.SelectOrAddFolderForItem
|
||||
|
||||
viewModel.trySendAction(action)
|
||||
|
||||
val expectedState = vaultAddItemInitialState.copy(
|
||||
shouldShowFolderSelectionBottomSheet = true,
|
||||
)
|
||||
|
||||
assertEquals(expectedState, viewModel.stateFlow.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DismissFolderSelectionBottomSheet should update state to hide bottom sheet`() =
|
||||
runTest {
|
||||
val action = VaultAddEditAction.Common.DismissFolderSelectionBottomSheet
|
||||
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.SelectOrAddFolderForItem)
|
||||
|
||||
assertEquals(
|
||||
vaultAddItemInitialState.copy(
|
||||
shouldShowFolderSelectionBottomSheet = true,
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
val expectedState = vaultAddItemInitialState.copy(
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
viewModel.trySendAction(action)
|
||||
assertEquals(
|
||||
expectedState,
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddNewFolder action calls create folder from vault repository`() = runTest {
|
||||
mockkStatic(DateTime::class)
|
||||
every { DateTime.now() } returns Instant.MIN
|
||||
|
||||
val folderName = "folderName"
|
||||
val expectedFolderResult = FolderView(
|
||||
id = "123",
|
||||
name = folderName,
|
||||
revisionDate = DateTime.now(),
|
||||
)
|
||||
coEvery {
|
||||
vaultRepository.createFolder(any())
|
||||
} returns CreateFolderResult.Success(expectedFolderResult)
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.AddNewFolder(folderName))
|
||||
coVerify {
|
||||
vaultRepository.createFolder(
|
||||
FolderView(
|
||||
name = folderName,
|
||||
id = null,
|
||||
revisionDate = Instant.MIN,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
unmockkStatic(DateTime::class)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AddNewFolder updates dialog states and selected folder id on success`() = runTest {
|
||||
|
||||
val folderId = "123"
|
||||
val folderName = "folderName"
|
||||
val expectedFolderResult = FolderView(
|
||||
id = folderId,
|
||||
name = folderName,
|
||||
revisionDate = DateTime.now(),
|
||||
)
|
||||
coEvery {
|
||||
vaultRepository.createFolder(any())
|
||||
} returns CreateFolderResult.Success(expectedFolderResult)
|
||||
|
||||
viewModel.stateFlow.test {
|
||||
awaitItem() // initial state.
|
||||
viewModel.trySendAction(VaultAddEditAction.Common.AddNewFolder(folderName))
|
||||
assertEquals(
|
||||
vaultAddItemInitialState.copy(
|
||||
dialog = VaultAddEditState.DialogState.Loading(R.string.saving.asText()),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
assertEquals(
|
||||
createVaultAddItemState(
|
||||
dialogState = null,
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
selectedFolderId = folderId,
|
||||
),
|
||||
),
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `State updates when available folders state is updated`() {
|
||||
mutableFolderStateFlow.update {
|
||||
DataState.Loaded(
|
||||
data = listOf(
|
||||
FolderView(
|
||||
name = "folder",
|
||||
revisionDate = DateTime.now(),
|
||||
id = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
val folderList = listOf(
|
||||
VaultAddEditState.Folder(
|
||||
id = null,
|
||||
name = "No Folder",
|
||||
),
|
||||
VaultAddEditState.Folder(
|
||||
id = null,
|
||||
name = "folder",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
createVaultAddItemState(
|
||||
commonContentViewState = createCommonContentViewState(
|
||||
availableFolders = folderList.toList(),
|
||||
),
|
||||
),
|
||||
viewModel.stateFlow.value,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ToggleFavorite should update favorite`() = runTest {
|
||||
val action = VaultAddEditAction.Common.ToggleFavorite(true)
|
||||
@@ -4277,6 +4414,7 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
totpData = totpData,
|
||||
shouldShowCoachMarkTour = false,
|
||||
shouldClearSpecialCircumstance = shouldClearSpecialCircumstance,
|
||||
shouldShowFolderSelectionBottomSheet = false,
|
||||
)
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@@ -4298,10 +4436,11 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
|
||||
hasOrganizations: Boolean = true,
|
||||
canDelete: Boolean = true,
|
||||
canAssociateToCollections: Boolean = true,
|
||||
selectedFolderId: String? = null,
|
||||
): VaultAddEditState.ViewState.Content.Common =
|
||||
VaultAddEditState.ViewState.Content.Common(
|
||||
name = name,
|
||||
selectedFolderId = null,
|
||||
selectedFolderId = selectedFolderId,
|
||||
favorite = favorite,
|
||||
customFieldData = customFieldData,
|
||||
masterPasswordReprompt = masterPasswordReprompt,
|
||||
|
||||
Reference in New Issue
Block a user