Fix node-details remove action to preserve confirmation flow (#5192)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jamesarich <2199651+jamesarich@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot 2026-04-20 10:59:20 -05:00 committed by GitHub
parent 2b47da3b61
commit 7492a33cf8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 119 additions and 8 deletions

View file

@ -43,10 +43,7 @@ internal fun handleNodeAction(
val route = viewModel.getDirectMessageRoute(menuAction.node, uiState.ourNode)
navigateToMessages(route)
}
is NodeMenuAction.Remove -> {
viewModel.handleNodeMenuAction(menuAction)
onNavigateUp()
}
is NodeMenuAction.Remove -> viewModel.handleNodeMenuAction(menuAction, onNavigateUp)
else -> viewModel.handleNodeMenuAction(menuAction)
}
}

View file

@ -89,9 +89,10 @@ class NodeDetailViewModel(
}
/** Dispatches high-level node management actions like removal, muting, or favoriting. */
fun handleNodeMenuAction(action: NodeMenuAction) {
fun handleNodeMenuAction(action: NodeMenuAction, onAfterRemove: () -> Unit = {}) {
when (action) {
is NodeMenuAction.Remove -> nodeManagementActions.requestRemoveNode(viewModelScope, action.node)
is NodeMenuAction.Remove ->
nodeManagementActions.requestRemoveNode(viewModelScope, action.node, onAfterRemove)
is NodeMenuAction.Ignore -> nodeManagementActions.requestIgnoreNode(viewModelScope, action.node)
is NodeMenuAction.Mute -> nodeManagementActions.requestMuteNode(viewModelScope, action.node)
is NodeMenuAction.Favorite -> nodeManagementActions.requestFavoriteNode(viewModelScope, action.node)

View file

@ -50,11 +50,14 @@ constructor(
private val radioController: RadioController,
private val alertManager: AlertManager,
) {
open fun requestRemoveNode(scope: CoroutineScope, node: Node) {
open fun requestRemoveNode(scope: CoroutineScope, node: Node, onAfterRemove: () -> Unit = {}) {
alertManager.showAlert(
titleRes = Res.string.remove,
messageRes = Res.string.remove_node_text,
onConfirm = { removeNode(scope, node.num) },
onConfirm = {
removeNode(scope, node.num)
onAfterRemove()
},
)
}

View file

@ -0,0 +1,90 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.feature.node.detail
import androidx.lifecycle.SavedStateHandle
import dev.mokkery.answering.returns
import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.meshtastic.core.model.Node
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.feature.node.component.NodeMenuAction
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
import org.meshtastic.feature.node.model.NodeDetailAction
import org.meshtastic.proto.User
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertFalse
@OptIn(ExperimentalCoroutinesApi::class)
class HandleNodeActionTest {
private val testDispatcher = UnconfinedTestDispatcher()
private val nodeManagementActions: NodeManagementActions = mock()
private val nodeRequestActions: NodeRequestActions = mock()
private val serviceRepository: ServiceRepository = mock()
private val getNodeDetailsUseCase: GetNodeDetailsUseCase = mock()
@BeforeTest
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { getNodeDetailsUseCase(any()) } returns emptyFlow()
}
@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `remove action delegates to viewModel and does not navigate up immediately`() = runTest(testDispatcher) {
val node = Node(num = 1234, user = User(id = "!1234"))
every { nodeManagementActions.requestRemoveNode(any(), any(), any()) } returns Unit
val viewModel = createViewModel()
var navigateUpCalled = false
handleNodeAction(
action = NodeDetailAction.HandleNodeMenuAction(NodeMenuAction.Remove(node)),
uiState = NodeDetailUiState(),
navigateToMessages = {},
onNavigateUp = { navigateUpCalled = true },
onNavigate = {},
viewModel = viewModel,
)
verify { nodeManagementActions.requestRemoveNode(any(), node, any()) }
assertFalse(navigateUpCalled)
}
private fun createViewModel() = NodeDetailViewModel(
savedStateHandle = SavedStateHandle(mapOf("destNum" to 1234)),
nodeManagementActions = nodeManagementActions,
nodeRequestActions = nodeRequestActions,
serviceRepository = serviceRepository,
getNodeDetailsUseCase = getNodeDetailsUseCase,
)
}

View file

@ -30,6 +30,7 @@ import org.meshtastic.core.testing.FakeRadioController
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.proto.User
import kotlin.test.Test
import kotlin.test.assertTrue
@OptIn(ExperimentalCoroutinesApi::class)
class NodeManagementActionsTest {
@ -69,4 +70,23 @@ class NodeManagementActionsTest {
)
}
}
@Test
fun requestRemoveNode_invokes_onAfterRemove_when_user_confirms() {
val realAlertManager = AlertManager()
val actionsWithRealAlert =
NodeManagementActions(
nodeRepository = nodeRepository,
serviceRepository = serviceRepository,
radioController = radioController,
alertManager = realAlertManager,
)
val node = Node(num = 123, user = User(long_name = "Test Node"))
var afterRemoveCalled = false
actionsWithRealAlert.requestRemoveNode(testScope, node) { afterRemoveCalled = true }
realAlertManager.currentAlert.value?.onConfirm?.invoke()
assertTrue(afterRemoveCalled)
}
}