BIT-1654 Add URI option menu (#877)

This commit is contained in:
Oleg Semenenko 2024-01-30 18:22:03 -06:00 committed by Álison Fernandes
parent a92d9ff823
commit 95b4aaf605
9 changed files with 493 additions and 67 deletions

View file

@ -156,22 +156,10 @@ fun LazyListScope.vaultAddEditLoginItems(
items(loginState.uriList) { uriItem ->
Spacer(modifier = Modifier.height(8.dp))
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.uri),
value = uriItem.uri.orEmpty(),
onValueChange = {
loginItemTypeHandlers.onUriTextChange(uriItem.copy(uri = it))
},
actions = {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_settings),
contentDescription = stringResource(id = R.string.options),
),
onClick = loginItemTypeHandlers.onUriSettingsClick,
)
},
modifier = Modifier.padding(horizontal = 16.dp),
VaultAddEditUriItem(
uriItem = uriItem,
onUriValueChange = loginItemTypeHandlers.onUriValueChange,
onUriItemRemoved = loginItemTypeHandlers.onRemoveUriClick,
)
}

View file

@ -0,0 +1,99 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit
import androidx.compose.foundation.layout.padding
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.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.BitwardenBasicDialogRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenIconButtonWithResource
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionDialog
import com.x8bit.bitwarden.ui.platform.components.BitwardenSelectionRow
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextFieldWithActions
import com.x8bit.bitwarden.ui.platform.components.model.IconResource
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toDisplayMatchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.util.toUriMatchType
/**
* The URI item displayed to the user.
*/
@Suppress("LongMethod")
@Composable
fun VaultAddEditUriItem(
uriItem: UriItem,
onUriItemRemoved: (UriItem) -> Unit,
onUriValueChange: (UriItem) -> Unit,
) {
var shouldShowOptionsDialog by rememberSaveable { mutableStateOf(false) }
var shouldShowMatchDialog by rememberSaveable { mutableStateOf(false) }
BitwardenTextFieldWithActions(
label = stringResource(id = R.string.uri),
value = uriItem.uri.orEmpty(),
onValueChange = { onUriValueChange(uriItem.copy(uri = it)) },
actions = {
BitwardenIconButtonWithResource(
iconRes = IconResource(
iconPainter = painterResource(id = R.drawable.ic_settings),
contentDescription = stringResource(id = R.string.options),
),
onClick = { shouldShowOptionsDialog = true },
)
},
modifier = Modifier.padding(horizontal = 16.dp),
)
if (shouldShowOptionsDialog) {
BitwardenSelectionDialog(
title = stringResource(id = R.string.options),
onDismissRequest = { shouldShowOptionsDialog = false },
) {
BitwardenBasicDialogRow(
text = stringResource(id = R.string.match_detection),
onClick = {
shouldShowOptionsDialog = false
shouldShowMatchDialog = true
},
)
BitwardenBasicDialogRow(
text = stringResource(id = R.string.remove),
onClick = {
shouldShowOptionsDialog = false
onUriItemRemoved(uriItem)
},
)
}
}
if (shouldShowMatchDialog) {
val selectedString = uriItem.match.toDisplayMatchType().text.invoke()
BitwardenSelectionDialog(
title = stringResource(id = R.string.uri_match_detection),
onDismissRequest = { shouldShowMatchDialog = false },
) {
UriMatchDisplayType
.entries
.forEach { matchType ->
BitwardenSelectionRow(
text = matchType.text,
isSelected = matchType.text.invoke() == selectedString,
onClick = {
shouldShowMatchDialog = false
onUriValueChange(
uriItem.copy(match = matchType.toUriMatchType()),
)
},
)
}
}
}
}

View file

@ -190,6 +190,7 @@ class VaultAddEditViewModel @Inject constructor(
is VaultAddEditAction.Common.CustomFieldActionSelect -> handleCustomFieldActionSelected(
action,
)
is VaultAddEditAction.Common.CollectionSelect -> handleCollectionSelect(action)
}
}
@ -542,8 +543,8 @@ class VaultAddEditViewModel @Inject constructor(
handleLoginPasswordTextInputChange(action)
}
is VaultAddEditAction.ItemType.LoginType.UriTextChange -> {
handleLoginUriTextInputChange(action)
is VaultAddEditAction.ItemType.LoginType.UriValueChange -> {
handleLoginUriValueInputChange(action)
}
is VaultAddEditAction.ItemType.LoginType.OpenUsernameGeneratorClick -> {
@ -562,8 +563,8 @@ class VaultAddEditViewModel @Inject constructor(
handleLoginSetupTotpClick(action)
}
is VaultAddEditAction.ItemType.LoginType.UriSettingsClick -> {
handleLoginUriSettingsClick()
is VaultAddEditAction.ItemType.LoginType.RemoveUriClick -> {
handleLoginRemoveUriClick(action)
}
is VaultAddEditAction.ItemType.LoginType.AddNewUriClick -> {
@ -596,16 +597,16 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleLoginUriTextInputChange(
action: VaultAddEditAction.ItemType.LoginType.UriTextChange,
private fun handleLoginUriValueInputChange(
action: VaultAddEditAction.ItemType.LoginType.UriValueChange,
) {
updateLoginContent { loginType ->
loginType.copy(
uriList = loginType
.uriList
.map { uriItem ->
if (uriItem.id == action.uri.id) {
action.uri
if (uriItem.id == action.uriItem.id) {
action.uriItem
} else {
uriItem
}
@ -614,6 +615,18 @@ class VaultAddEditViewModel @Inject constructor(
}
}
private fun handleLoginRemoveUriClick(
action: VaultAddEditAction.ItemType.LoginType.RemoveUriClick,
) {
updateLoginContent { loginType ->
loginType.copy(
uriList = loginType.uriList.filter {
it != action.uriItem
},
)
}
}
private fun handleLoginOpenUsernameGeneratorClick() {
sendEvent(event = VaultAddEditEvent.NavigateToGeneratorModal(GeneratorMode.Modal.Username))
}
@ -1899,11 +1912,11 @@ sealed class VaultAddEditAction {
data class PasswordTextChange(val password: String) : LoginType()
/**
* Fired when the URI text input is changed.
* Fired when the URI is changed.
*
* @property uri The new URI text.
* @property uriItem The new URI.
*/
data class UriTextChange(val uri: UriItem) : LoginType()
data class UriValueChange(val uriItem: UriItem) : LoginType()
/**
* Represents the action to set up TOTP.
@ -1940,9 +1953,9 @@ sealed class VaultAddEditAction {
data object OpenPasswordGeneratorClick : LoginType()
/**
* Represents the action of clicking TOTP settings
* Represents the action of removing a URI item.
*/
data object UriSettingsClick : LoginType()
data class RemoveUriClick(val uriItem: UriItem) : LoginType()
/**
* Represents the action to add a new URI field.
@ -2164,8 +2177,8 @@ sealed class VaultAddEditAction {
* Indicates that the vault item data has been received.
*/
data class VaultDataReceive(
val vaultData: DataState<VaultData>,
val userData: UserState?,
val vaultData: DataState<VaultData>,
val userData: UserState?,
) : Internal()
/**

View file

@ -10,8 +10,8 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
*
* @property onUsernameTextChange Handles the action when the username text is changed.
* @property onPasswordTextChange Handles the action when the password text is changed.
* @property onUriTextChange Handles the action when the URI text is changed.
* reprompt toggle is changed.
* @property onRemoveUriClick Handles the action when the URI is removed.
* @property onUriValueChange Handles the action when the URI value is changed.
* @property onOpenUsernameGeneratorClick Handles the action when the username generator
* button is clicked.
* @property onPasswordCheckerClick Handles the action when the password checker
@ -28,14 +28,14 @@ import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriItem
data class VaultAddEditLoginTypeHandlers(
val onUsernameTextChange: (String) -> Unit,
val onPasswordTextChange: (String) -> Unit,
val onUriTextChange: (UriItem) -> Unit,
val onRemoveUriClick: (UriItem) -> Unit,
val onUriValueChange: (UriItem) -> Unit,
val onOpenUsernameGeneratorClick: () -> Unit,
val onPasswordCheckerClick: () -> Unit,
val onOpenPasswordGeneratorClick: () -> Unit,
val onSetupTotpClick: (Boolean) -> Unit,
val onCopyTotpKeyClick: (String) -> Unit,
val onClearTotpKeyClick: () -> Unit,
val onUriSettingsClick: () -> Unit,
val onAddNewUriClick: () -> Unit,
) {
companion object {
@ -60,9 +60,9 @@ data class VaultAddEditLoginTypeHandlers(
VaultAddEditAction.ItemType.LoginType.PasswordTextChange(newPassword),
)
},
onUriTextChange = { newUri ->
onUriValueChange = { newUri ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.UriTextChange(newUri),
VaultAddEditAction.ItemType.LoginType.UriValueChange(newUri),
)
},
onOpenUsernameGeneratorClick = {
@ -85,8 +85,12 @@ data class VaultAddEditLoginTypeHandlers(
VaultAddEditAction.ItemType.LoginType.SetupTotpClick(isGranted),
)
},
onUriSettingsClick = {
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.UriSettingsClick)
onRemoveUriClick = { uriItem ->
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.RemoveUriClick(
uriItem,
),
)
},
onAddNewUriClick = {
viewModel.trySendAction(VaultAddEditAction.ItemType.LoginType.AddNewUriClick)

View file

@ -0,0 +1,50 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.model
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
/**
* The options displayed to the user when choosing a match type
* for their URI.
*/
@Suppress("MagicNumber")
enum class UriMatchDisplayType(
val text: Text,
) {
/**
* the default option for when the user has not chosen one.
*/
DEFAULT(R.string.default_text.asText()),
/**
* The URIs match if their top-level and second-level domains match.
*/
BASE_DOMAIN(R.string.base_domain.asText()),
/**
* The URIs match if their hostnames (and ports if specified) match.
*/
HOST(R.string.host.asText()),
/**
* The URIs match if the "test" URI starts with the known URI.
*/
STARTS_WITH(R.string.starts_with.asText()),
/**
* The URIs match if the "test" URI matches the known URI according to a specified regular
* expression for the item.
*/
REGULAR_EXPRESSION(R.string.reg_ex.asText()),
/**
* The URIs match if they are exactly the same.
*/
EXACT(R.string.exact.asText()),
/**
* The URIs should never match.
*/
NEVER(R.string.never.asText()),
}

View file

@ -0,0 +1,32 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
/**
* Method to convert the SDK match type for display to the user.
*/
fun UriMatchType?.toDisplayMatchType(): UriMatchDisplayType =
when (this) {
UriMatchType.DOMAIN -> UriMatchDisplayType.BASE_DOMAIN
UriMatchType.EXACT -> UriMatchDisplayType.EXACT
UriMatchType.HOST -> UriMatchDisplayType.HOST
UriMatchType.NEVER -> UriMatchDisplayType.NEVER
UriMatchType.REGULAR_EXPRESSION -> UriMatchDisplayType.REGULAR_EXPRESSION
UriMatchType.STARTS_WITH -> UriMatchDisplayType.STARTS_WITH
null -> UriMatchDisplayType.DEFAULT
}
/**
* Method to convert the match display type over to the SDK match type.
*/
fun UriMatchDisplayType.toUriMatchType(): UriMatchType? =
when (this) {
UriMatchDisplayType.DEFAULT -> null
UriMatchDisplayType.BASE_DOMAIN -> UriMatchType.DOMAIN
UriMatchDisplayType.HOST -> UriMatchType.HOST
UriMatchDisplayType.STARTS_WITH -> UriMatchType.STARTS_WITH
UriMatchDisplayType.REGULAR_EXPRESSION -> UriMatchType.REGULAR_EXPRESSION
UriMatchDisplayType.EXACT -> UriMatchType.EXACT
UriMatchDisplayType.NEVER -> UriMatchType.NEVER
}

View file

@ -29,6 +29,7 @@ import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTextClearance
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performTouchInput
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
import com.x8bit.bitwarden.data.vault.datasource.sdk.model.createMockCipherView
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
@ -720,7 +721,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
}
@Test
fun `in ItemType_Login state changing URI text field should trigger UriTextChange`() {
fun `in ItemType_Login state changing URI text field should trigger UriValueChange`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(uriList = listOf(UriItem("TestId", "URI", null)))
@ -733,7 +734,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.UriTextChange(
VaultAddEditAction.ItemType.LoginType.UriValueChange(
UriItem("TestId", "TestURI", null),
),
)
@ -759,20 +760,184 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login state clicking the URI settings action should trigger UriSettingsClick`() {
fun `in ItemType_Login Uri settings dialog should be dismissed on cancel click`() {
composeTestRule
.onNodeWithTextAfterScroll(text = "URI")
.onSiblings()
.filterToOne(hasContentDescription(value = "Options"))
.performClick()
composeTestRule
.onNodeWithText("Cancel")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login Uri settings dialog should send RemoveUriClick action if remove is clicked`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(uriList = listOf(UriItem("TestId", null, null)))
}
}
composeTestRule
.onNodeWithTextAfterScroll(text = "URI")
.onSiblings()
.filterToOne(hasContentDescription(value = "Options"))
.performClick()
composeTestRule
.onNodeWithText("Remove")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.UriSettingsClick,
VaultAddEditAction.ItemType.LoginType.RemoveUriClick(
UriItem(
"TestId",
null,
null,
),
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login Uri settings dialog with open match detection click should open list of options`() {
composeTestRule
.onNodeWithTextAfterScroll(text = "URI")
.onSiblings()
.filterToOne(hasContentDescription(value = "Options"))
.performClick()
composeTestRule
.onNodeWithText("Match detection")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("URI match detection")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Default")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Base domain")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Host")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Starts with")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Regular expression")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Exact")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login on URI settings click and on match detection click and option click should emit UriValueChange action`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(uriList = listOf(UriItem("TestId", null, null)))
}
}
composeTestRule
.onNodeWithTextAfterScroll(text = "URI")
.onSiblings()
.filterToOne(hasContentDescription(value = "Options"))
.performClick()
composeTestRule
.onNodeWithText("Match detection")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onNodeWithText("URI match detection")
.assert(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Exact")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
verify {
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.UriValueChange(
UriItem(
"TestId",
null,
UriMatchType.EXACT,
),
),
)
}
}
@Suppress("MaxLineLength")
@Test
fun `in ItemType_Login on URI settings click and on match detection click and cancel click should dismiss the dialog`() {
mutableStateFlow.update { currentState ->
updateLoginType(currentState) {
copy(uriList = listOf(UriItem("TestId", null, null)))
}
}
composeTestRule
.onNodeWithTextAfterScroll(text = "URI")
.onSiblings()
.filterToOne(hasContentDescription(value = "Options"))
.performClick()
composeTestRule
.onNodeWithText("Match detection")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule
.onAllNodesWithText("URI match detection")
.filterToOne(hasAnyAncestor(isDialog()))
.assertIsDisplayed()
composeTestRule
.onNodeWithText("Cancel")
.assert(hasAnyAncestor(isDialog()))
.performClick()
composeTestRule.assertNoDialogExists()
}
@Test
fun `in ItemType_Login state clicking the New URI button should trigger AddNewUriClick`() {
composeTestRule
@ -1884,7 +2049,7 @@ class VaultAddEditScreenTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `Ownership option should send OwnershipChange action`() {
mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES
mutableStateFlow.value = DEFAULT_STATE_SECURE_NOTES
updateStateWithOwners()

View file

@ -882,23 +882,6 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test
fun `UriTextChange should update uri in LoginItem`() = runTest {
val action = VaultAddEditAction.ItemType.LoginType.UriTextChange(
UriItem("testId", "TestUri", null),
)
viewModel.actionChannel.trySend(action)
val expectedState = createVaultAddItemState(
typeContentViewState = createLoginTypeContentViewState(
uri = listOf(UriItem("testId", "TestUri", null)),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `OpenUsernameGeneratorClick should emit NavigateToGeneratorModal with username GeneratorMode`() =
@ -1110,13 +1093,65 @@ class VaultAddEditViewModelTest : BaseViewModelTest() {
@Suppress("MaxLineLength")
@Test
fun `UriSettingsClick should emit ShowToast with 'URI Settings' message`() = runTest {
val viewModel = createAddVaultItemViewModel()
fun `UriValueChange should update URI value in state`() = runTest {
val viewModel = createAddVaultItemViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultAddItemState(
typeContentViewState = createLoginTypeContentViewState(
uri = listOf(UriItem("testID", null, null)),
),
),
vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID),
),
)
val expectedState = loginInitialState.copy(
viewState = VaultAddEditState.ViewState.Content(
common = createCommonContentViewState(),
type = createLoginTypeContentViewState(
uri = listOf(UriItem("testID", "Test", null)),
),
),
)
viewModel.eventFlow.test {
viewModel.actionChannel.trySend(VaultAddEditAction.ItemType.LoginType.UriSettingsClick)
assertEquals(VaultAddEditEvent.ShowToast("URI Settings".asText()), awaitItem())
}
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.UriValueChange(
uriItem = UriItem("testID", "Test", null),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Suppress("MaxLineLength")
@Test
fun `RemoveUriClick should remove URI value in state`() = runTest {
val viewModel = createAddVaultItemViewModel(
savedStateHandle = createSavedStateHandleWithState(
state = createVaultAddItemState(
typeContentViewState = createLoginTypeContentViewState(
uri = listOf(UriItem("testID", null, null)),
),
),
vaultAddEditType = VaultAddEditType.EditItem(DEFAULT_EDIT_ITEM_ID),
),
)
val expectedState = loginInitialState.copy(
viewState = VaultAddEditState.ViewState.Content(
common = createCommonContentViewState(),
type = createLoginTypeContentViewState(
uri = listOf(),
),
),
)
viewModel.trySendAction(
VaultAddEditAction.ItemType.LoginType.RemoveUriClick(
uriItem = UriItem("testID", null, null),
),
)
assertEquals(expectedState, viewModel.stateFlow.value)
}
@Test

View file

@ -0,0 +1,40 @@
package com.x8bit.bitwarden.ui.vault.feature.addedit.util
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.ui.vault.feature.addedit.model.UriMatchDisplayType
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class UriMatchDisplayUtilTest {
@Test
fun `toDisplayMatchType should correctly convert to UriMatchDisplayType`() {
URI_MATCH_TYPE_MAP.forEach {
assertEquals(
it.key.toDisplayMatchType(),
it.value,
)
}
}
@Test
fun `toUriMatchType should correctly convert to UriMatchType`() {
URI_MATCH_TYPE_MAP.forEach {
assertEquals(
it.key,
it.value.toUriMatchType(),
)
}
}
}
private val URI_MATCH_TYPE_MAP: Map<UriMatchType?, UriMatchDisplayType> =
mapOf(
Pair(null, UriMatchDisplayType.DEFAULT),
Pair(UriMatchType.DOMAIN, UriMatchDisplayType.BASE_DOMAIN),
Pair(UriMatchType.HOST, UriMatchDisplayType.HOST),
Pair(UriMatchType.EXACT, UriMatchDisplayType.EXACT),
Pair(UriMatchType.STARTS_WITH, UriMatchDisplayType.STARTS_WITH),
Pair(UriMatchType.REGULAR_EXPRESSION, UriMatchDisplayType.REGULAR_EXPRESSION),
Pair(UriMatchType.EXACT, UriMatchDisplayType.EXACT),
Pair(UriMatchType.NEVER, UriMatchDisplayType.NEVER),
)