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(
|
||||
route = Routes.SITE_LIST,
|
||||
arguments = listOf(navArgument("instanceId") { type = NavType.StringType })
|
||||
|
|
@ -80,6 +96,9 @@ fun AppNavHost(
|
|||
popUpTo(Routes.SETUP) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onCloneSite = { instanceId, siteId ->
|
||||
navController.navigate(Routes.setupClone(instanceId, siteId))
|
||||
},
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ package no.naiv.implausibly.ui.navigation
|
|||
object Routes {
|
||||
const val LOADING = "loading"
|
||||
const val SETUP = "setup"
|
||||
const val SETUP_CLONE = "setup?cloneInstanceId={cloneInstanceId}&cloneSiteId={cloneSiteId}"
|
||||
const val SITE_LIST = "site_list/{instanceId}"
|
||||
const val DASHBOARD = "dashboard/{instanceId}/{siteId}"
|
||||
|
||||
fun siteList(instanceId: String) = "site_list/$instanceId"
|
||||
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
|
||||
package no.naiv.implausibly.ui.setup
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
@ -39,16 +40,39 @@ sealed class TestResult {
|
|||
|
||||
@HiltViewModel
|
||||
class SetupViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val instanceRepository: InstanceRepository,
|
||||
private val siteRepository: SiteRepository,
|
||||
private val appPreferences: AppPreferences
|
||||
) : ViewModel() {
|
||||
|
||||
private val cloneInstanceId: String? = savedStateHandle["cloneInstanceId"]
|
||||
private val cloneSiteId: String? = savedStateHandle["cloneSiteId"]
|
||||
|
||||
private val _uiState = MutableStateFlow(SetupUiState())
|
||||
val uiState: StateFlow<SetupUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
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() {
|
||||
|
|
@ -95,7 +119,6 @@ class SetupViewModel @Inject constructor(
|
|||
isTesting = false,
|
||||
testResult = if (error == null) TestResult.Success else TestResult.Failure(error),
|
||||
name = if (error == null && it.name.isBlank()) {
|
||||
// Auto-fill name from site ID if blank
|
||||
state.siteId
|
||||
} else {
|
||||
it.name
|
||||
|
|
@ -121,10 +144,8 @@ class SetupViewModel @Inject constructor(
|
|||
apiKey = apiKey
|
||||
)
|
||||
|
||||
// Also store the site ID
|
||||
siteRepository.addSite(state.siteId, instance.id)
|
||||
|
||||
// Set as selected
|
||||
appPreferences.setSelectedInstanceId(instance.id)
|
||||
appPreferences.setSelectedSiteId(state.siteId)
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
@Composable
|
||||
fun SiteListScreen(
|
||||
onSiteSelected: (instanceId: String, siteId: String) -> Unit,
|
||||
onCloneSite: (instanceId: String, siteId: String) -> Unit = { _, _ -> },
|
||||
onBack: () -> Unit,
|
||||
viewModel: SiteListViewModel = hiltViewModel()
|
||||
) {
|
||||
|
|
@ -159,7 +160,7 @@ fun SiteListScreen(
|
|||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = { viewModel.cloneSite(site.id) }) {
|
||||
IconButton(onClick = { onCloneSite(uiState.instanceId, site.id) }) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Clone site"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue