feat: clone instance with full details for editing
Clone button on site list now navigates to setup screen pre-filled with the source instance's URL, API key, and site ID. User can edit all fields and save as a new instance. API key is resolved from encrypted storage via the ViewModel, never passed through nav args. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4eef2ac168
commit
1434f2f842
4 changed files with 47 additions and 4 deletions
|
|
@ -70,6 +70,22 @@ fun AppNavHost(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(
|
||||||
|
route = Routes.SETUP_CLONE,
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument("cloneInstanceId") { type = NavType.StringType },
|
||||||
|
navArgument("cloneSiteId") { type = NavType.StringType }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
SetupScreen(
|
||||||
|
onInstanceAdded = { instanceId ->
|
||||||
|
navController.navigate(Routes.siteList(instanceId)) {
|
||||||
|
popUpTo(0) { inclusive = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composable(
|
composable(
|
||||||
route = Routes.SITE_LIST,
|
route = Routes.SITE_LIST,
|
||||||
arguments = listOf(navArgument("instanceId") { type = NavType.StringType })
|
arguments = listOf(navArgument("instanceId") { type = NavType.StringType })
|
||||||
|
|
@ -80,6 +96,9 @@ fun AppNavHost(
|
||||||
popUpTo(Routes.SETUP) { inclusive = true }
|
popUpTo(Routes.SETUP) { inclusive = true }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onCloneSite = { instanceId, siteId ->
|
||||||
|
navController.navigate(Routes.setupClone(instanceId, siteId))
|
||||||
|
},
|
||||||
onBack = { navController.popBackStack() }
|
onBack = { navController.popBackStack() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,11 @@ package no.naiv.implausibly.ui.navigation
|
||||||
object Routes {
|
object Routes {
|
||||||
const val LOADING = "loading"
|
const val LOADING = "loading"
|
||||||
const val SETUP = "setup"
|
const val SETUP = "setup"
|
||||||
|
const val SETUP_CLONE = "setup?cloneInstanceId={cloneInstanceId}&cloneSiteId={cloneSiteId}"
|
||||||
const val SITE_LIST = "site_list/{instanceId}"
|
const val SITE_LIST = "site_list/{instanceId}"
|
||||||
const val DASHBOARD = "dashboard/{instanceId}/{siteId}"
|
const val DASHBOARD = "dashboard/{instanceId}/{siteId}"
|
||||||
|
|
||||||
fun siteList(instanceId: String) = "site_list/$instanceId"
|
fun siteList(instanceId: String) = "site_list/$instanceId"
|
||||||
fun dashboard(instanceId: String, siteId: String) = "dashboard/$instanceId/$siteId"
|
fun dashboard(instanceId: String, siteId: String) = "dashboard/$instanceId/$siteId"
|
||||||
|
fun setupClone(instanceId: String, siteId: String) = "setup?cloneInstanceId=$instanceId&cloneSiteId=$siteId"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
package no.naiv.implausibly.ui.setup
|
package no.naiv.implausibly.ui.setup
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
|
@ -39,16 +40,39 @@ sealed class TestResult {
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SetupViewModel @Inject constructor(
|
class SetupViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
private val instanceRepository: InstanceRepository,
|
private val instanceRepository: InstanceRepository,
|
||||||
private val siteRepository: SiteRepository,
|
private val siteRepository: SiteRepository,
|
||||||
private val appPreferences: AppPreferences
|
private val appPreferences: AppPreferences
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val cloneInstanceId: String? = savedStateHandle["cloneInstanceId"]
|
||||||
|
private val cloneSiteId: String? = savedStateHandle["cloneSiteId"]
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(SetupUiState())
|
private val _uiState = MutableStateFlow(SetupUiState())
|
||||||
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadExistingInstances()
|
loadExistingInstances()
|
||||||
|
if (cloneInstanceId != null) {
|
||||||
|
loadCloneData(cloneInstanceId, cloneSiteId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadCloneData(instanceId: String, siteId: String?) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val instance = instanceRepository.getById(instanceId) ?: return@launch
|
||||||
|
val apiKey = instanceRepository.getApiKey(instance) ?: ""
|
||||||
|
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
baseUrl = instance.baseUrl,
|
||||||
|
apiKey = apiKey,
|
||||||
|
name = "",
|
||||||
|
siteId = siteId ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadExistingInstances() {
|
private fun loadExistingInstances() {
|
||||||
|
|
@ -95,7 +119,6 @@ class SetupViewModel @Inject constructor(
|
||||||
isTesting = false,
|
isTesting = false,
|
||||||
testResult = if (error == null) TestResult.Success else TestResult.Failure(error),
|
testResult = if (error == null) TestResult.Success else TestResult.Failure(error),
|
||||||
name = if (error == null && it.name.isBlank()) {
|
name = if (error == null && it.name.isBlank()) {
|
||||||
// Auto-fill name from site ID if blank
|
|
||||||
state.siteId
|
state.siteId
|
||||||
} else {
|
} else {
|
||||||
it.name
|
it.name
|
||||||
|
|
@ -121,10 +144,8 @@ class SetupViewModel @Inject constructor(
|
||||||
apiKey = apiKey
|
apiKey = apiKey
|
||||||
)
|
)
|
||||||
|
|
||||||
// Also store the site ID
|
|
||||||
siteRepository.addSite(state.siteId, instance.id)
|
siteRepository.addSite(state.siteId, instance.id)
|
||||||
|
|
||||||
// Set as selected
|
|
||||||
appPreferences.setSelectedInstanceId(instance.id)
|
appPreferences.setSelectedInstanceId(instance.id)
|
||||||
appPreferences.setSelectedSiteId(state.siteId)
|
appPreferences.setSelectedSiteId(state.siteId)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
@Composable
|
@Composable
|
||||||
fun SiteListScreen(
|
fun SiteListScreen(
|
||||||
onSiteSelected: (instanceId: String, siteId: String) -> Unit,
|
onSiteSelected: (instanceId: String, siteId: String) -> Unit,
|
||||||
|
onCloneSite: (instanceId: String, siteId: String) -> Unit = { _, _ -> },
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
viewModel: SiteListViewModel = hiltViewModel()
|
viewModel: SiteListViewModel = hiltViewModel()
|
||||||
) {
|
) {
|
||||||
|
|
@ -159,7 +160,7 @@ fun SiteListScreen(
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
IconButton(onClick = { viewModel.cloneSite(site.id) }) {
|
IconButton(onClick = { onCloneSite(uiState.instanceId, site.id) }) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.ContentCopy,
|
Icons.Default.ContentCopy,
|
||||||
contentDescription = "Clone site"
|
contentDescription = "Clone site"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue