Keep proxy-chain deletion targets tied to stable row keys (#6200)
Store the pending member key instead of its list position and resolve that key when deletion is confirmed. Remove the member and its corresponding row key together; if the target no longer exists, leave the draft unchanged. Extract the deletion fix from the accessibility assembly without importing accessibility actions, dialog wording changes, or drag refactors. Reuse the same key-based removal for blank members. Validation: 75 JVM tests passed, including five deletion regressions; Play Store debug Kotlin compilation and x86_64 assembly passed. Emulator smoke check covered confirmation across rotation and immediate blank-member removal. Keyboard/D-pad and runtime reorder-during-confirmation scenarios were not run.
This commit is contained in:
1 parent
1fd05e16b5
commit
6a6a44a380
3 files changed
+87
-10
No files matched your search
@@ -0,0 +1,14 @@
|
|||||||
|
package com.v2ray.ang.ui.server
|
||||||
|
|
||||||
|
/** Removes one draft member and its row key together, resolving its current position at confirmation. */
|
||||||
|
internal fun withoutProxyChainMember(
|
||||||
|
members: List<String>,
|
||||||
|
memberKeys: List<String>,
|
||||||
|
memberKey: String,
|
||||||
|
): Pair<List<String>, List<String>> {
|
||||||
|
val index = memberKeys.indexOf(memberKey)
|
||||||
|
if (index < 0) return members to memberKeys
|
||||||
|
|
||||||
|
return members.toMutableList().also { it.removeAt(index) } to
|
||||||
|
memberKeys.toMutableList().also { it.removeAt(index) }
|
||||||
|
}
|
||||||
@@ -203,7 +203,7 @@ fun ProxyChainScreen(
|
|||||||
var members by rememberSaveable { mutableStateOf(initialMembers) }
|
var members by rememberSaveable { mutableStateOf(initialMembers) }
|
||||||
var memberKeys by rememberSaveable { mutableStateOf(List(initialMembers.size) { UUID.randomUUID().toString() }) }
|
var memberKeys by rememberSaveable { mutableStateOf(List(initialMembers.size) { UUID.randomUUID().toString() }) }
|
||||||
var showProfileDeleteConfirm by remember { mutableStateOf(false) }
|
var showProfileDeleteConfirm by remember { mutableStateOf(false) }
|
||||||
var memberToDeleteIndex by rememberSaveable { mutableStateOf<Int?>(null) }
|
var memberToDeleteKey by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
val showDelete = editGuid.isNotEmpty() && !isRunning
|
val showDelete = editGuid.isNotEmpty() && !isRunning
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
val lazyListState = rememberLazyListState()
|
||||||
@@ -285,7 +285,8 @@ fun ProxyChainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(items = members, key = { index, _ -> memberKeys[index] }) { index, member ->
|
itemsIndexed(items = members, key = { index, _ -> memberKeys[index] }) { index, member ->
|
||||||
ReorderableItem(reorderableState, key = memberKeys[index]) { isDragging ->
|
val memberKey = memberKeys[index]
|
||||||
|
ReorderableItem(reorderableState, key = memberKey) { isDragging ->
|
||||||
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
|
val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
|
||||||
Surface(shadowElevation = elevation) {
|
Surface(shadowElevation = elevation) {
|
||||||
Row(
|
Row(
|
||||||
@@ -314,10 +315,11 @@ fun ProxyChainScreen(
|
|||||||
)
|
)
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
if (member.isBlank()) {
|
if (member.isBlank()) {
|
||||||
members = members.toMutableList().also { it.removeAt(index) }
|
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey)
|
||||||
memberKeys = memberKeys.toMutableList().also { it.removeAt(index) }
|
members = remainingMembers
|
||||||
|
memberKeys = remainingKeys
|
||||||
} else {
|
} else {
|
||||||
memberToDeleteIndex = index
|
memberToDeleteKey = memberKey
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -339,15 +341,16 @@ fun ProxyChainScreen(
|
|||||||
onDismiss = { showProfileDeleteConfirm = false }
|
onDismiss = { showProfileDeleteConfirm = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
memberToDeleteIndex?.let { index ->
|
memberToDeleteKey?.let { memberKey ->
|
||||||
DeleteConfirmDialog(
|
DeleteConfirmDialog(
|
||||||
message = stringResource(R.string.confirm_delete_proxy_chain_member),
|
message = stringResource(R.string.confirm_delete_proxy_chain_member),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
members = members.toMutableList().also { it.removeAt(index) }
|
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, memberKeys, memberKey)
|
||||||
memberKeys = memberKeys.toMutableList().also { it.removeAt(index) }
|
members = remainingMembers
|
||||||
memberToDeleteIndex = null
|
memberKeys = remainingKeys
|
||||||
|
memberToDeleteKey = null
|
||||||
},
|
},
|
||||||
onDismiss = { memberToDeleteIndex = null }
|
onDismiss = { memberToDeleteKey = null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package com.v2ray.ang.ui.server
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ProxyChainMembersTest {
|
||||||
|
@Test
|
||||||
|
fun removalFollowsThePendingKeyAfterReordering() {
|
||||||
|
val pendingKey = "two"
|
||||||
|
val members = listOf("Two", "One", "Three")
|
||||||
|
val keys = listOf("two", "one", "three")
|
||||||
|
|
||||||
|
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, keys, pendingKey)
|
||||||
|
|
||||||
|
assertEquals(listOf("One", "Three"), remainingMembers)
|
||||||
|
assertEquals(listOf("one", "three"), remainingKeys)
|
||||||
|
assertEquals(listOf("Two", "One", "Three"), members)
|
||||||
|
assertEquals(listOf("two", "one", "three"), keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun duplicateNamesDoNotChangeWhichMemberIsRemoved() {
|
||||||
|
val (members, keys) = withoutProxyChainMember(
|
||||||
|
listOf("Same", "Same", "Other"),
|
||||||
|
listOf("first", "second", "third"),
|
||||||
|
"second",
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(listOf("Same", "Other"), members)
|
||||||
|
assertEquals(listOf("first", "third"), keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun missingMemberLeavesBothListsUnchanged() {
|
||||||
|
val members = listOf("One", "Three")
|
||||||
|
val keys = listOf("one", "three")
|
||||||
|
|
||||||
|
val (remainingMembers, remainingKeys) = withoutProxyChainMember(members, keys, "two")
|
||||||
|
|
||||||
|
assertSame(members, remainingMembers)
|
||||||
|
assertSame(keys, remainingKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun emptyChainRemainsEmpty() {
|
||||||
|
assertEquals(
|
||||||
|
emptyList<String>() to emptyList<String>(),
|
||||||
|
withoutProxyChainMember(emptyList(), emptyList(), "missing"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun blankMemberIsRemovedWithItsOwnKey() {
|
||||||
|
assertEquals(
|
||||||
|
listOf("One") to listOf("one"),
|
||||||
|
withoutProxyChainMember(listOf("One", ""), listOf("one", "blank"), "blank"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user