feat(cron): confirm before deleting a cron job (#879)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
1e6e248891
commit
d4008b7b31
5 files changed
+85
-1
No files matched your search
@@ -244,7 +244,7 @@ fun CronJobsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.deleteCronJob(job.id) },
|
onClick = { viewModel.requestDeleteJob(job) },
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Delete,
|
imageVector = Icons.Filled.Delete,
|
||||||
@@ -305,6 +305,25 @@ fun CronJobsScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Delete Confirm Dialog ──
|
||||||
|
state.deleteTarget?.let { job ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { viewModel.dismissDeleteDialog() },
|
||||||
|
title = { Text(stringResource(R.string.cron_delete_title)) },
|
||||||
|
text = { Text(stringResource(R.string.cron_delete_message, job.name)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { viewModel.confirmDeleteJob() }) {
|
||||||
|
Text(stringResource(R.string.action_delete))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { viewModel.dismissDeleteDialog() }) {
|
||||||
|
Text(stringResource(R.string.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ data class CronJobsUiState(
|
|||||||
val jobs: List<CronJob> = emptyList(),
|
val jobs: List<CronJob> = emptyList(),
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
|
// Delete-confirm state (non-null while the confirm dialog is up)
|
||||||
|
val deleteTarget: CronJob? = null,
|
||||||
// Editor state
|
// Editor state
|
||||||
val editorState: CronJobEditorState = CronJobEditorState(),
|
val editorState: CronJobEditorState = CronJobEditorState(),
|
||||||
)
|
)
|
||||||
@@ -196,6 +198,20 @@ class CronJobsViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun requestDeleteJob(job: CronJob) {
|
||||||
|
_uiState.update { it.copy(deleteTarget = job) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dismissDeleteDialog() {
|
||||||
|
_uiState.update { it.copy(deleteTarget = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun confirmDeleteJob() {
|
||||||
|
val target = _uiState.value.deleteTarget ?: return
|
||||||
|
_uiState.update { it.copy(deleteTarget = null) }
|
||||||
|
deleteCronJob(target.id)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Editor ──
|
// ── Editor ──
|
||||||
|
|
||||||
fun openNewJobDialog() {
|
fun openNewJobDialog() {
|
||||||
|
|||||||
@@ -586,6 +586,8 @@
|
|||||||
<string name="cron_edit_action_save">저장</string>
|
<string name="cron_edit_action_save">저장</string>
|
||||||
<string name="cron_discard_title">변경사항을 취소할까요?</string>
|
<string name="cron_discard_title">변경사항을 취소할까요?</string>
|
||||||
<string name="cron_discard_message">저장되지 않은 변경사항이 있습니다. 취소하시겠습니까?</string>
|
<string name="cron_discard_message">저장되지 않은 변경사항이 있습니다. 취소하시겠습니까?</string>
|
||||||
|
<string name="cron_delete_title">크론 작업을 삭제할까요?</string>
|
||||||
|
<string name="cron_delete_message">\"%1$s\" 작업을 삭제하시겠습니까? 되돌릴 수 없습니다.</string>
|
||||||
|
|
||||||
<!-- Achievements Screen -->
|
<!-- Achievements Screen -->
|
||||||
<string name="achievements_empty_title">아직 업적이 없습니다</string>
|
<string name="achievements_empty_title">아직 업적이 없습니다</string>
|
||||||
|
|||||||
@@ -828,6 +828,8 @@
|
|||||||
<string name="cron_edit_action_save">Save</string>
|
<string name="cron_edit_action_save">Save</string>
|
||||||
<string name="cron_discard_title">Discard changes?</string>
|
<string name="cron_discard_title">Discard changes?</string>
|
||||||
<string name="cron_discard_message">You have unsaved changes. Discard them?</string>
|
<string name="cron_discard_message">You have unsaved changes. Discard them?</string>
|
||||||
|
<string name="cron_delete_title">Delete job?</string>
|
||||||
|
<string name="cron_delete_message">Delete \"%1$s\"? This can\'t be undone.</string>
|
||||||
|
|
||||||
<!-- Achievements Screen -->
|
<!-- Achievements Screen -->
|
||||||
<string name="achievements_empty_title">No achievements yet</string>
|
<string name="achievements_empty_title">No achievements yet</string>
|
||||||
|
|||||||
@@ -226,6 +226,51 @@ class CronJobsViewModelTest {
|
|||||||
assertFalse(vm.uiState.value.editorState.isOpen)
|
assertFalse(vm.uiState.value.editorState.isOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `requestDeleteJob sets the delete target without deleting`() {
|
||||||
|
val vm = createViewModel()
|
||||||
|
val job = CronJob(id = "j-del", name = "Doomed")
|
||||||
|
|
||||||
|
vm.requestDeleteJob(job)
|
||||||
|
|
||||||
|
assertEquals(job, vm.uiState.value.deleteTarget)
|
||||||
|
coVerify(exactly = 0) { mockApi.deleteCronJob(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `dismissDeleteDialog clears the target without deleting`() {
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.requestDeleteJob(CronJob(id = "j-del", name = "Doomed"))
|
||||||
|
|
||||||
|
vm.dismissDeleteDialog()
|
||||||
|
|
||||||
|
assertNull(vm.uiState.value.deleteTarget)
|
||||||
|
coVerify(exactly = 0) { mockApi.deleteCronJob(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `confirmDeleteJob clears the target and deletes the job`() {
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.requestDeleteJob(CronJob(id = "j-del", name = "Doomed"))
|
||||||
|
|
||||||
|
vm.confirmDeleteJob()
|
||||||
|
settle()
|
||||||
|
|
||||||
|
assertNull(vm.uiState.value.deleteTarget)
|
||||||
|
coVerify { mockApi.deleteCronJob("j-del") }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `confirmDeleteJob without a target is a no-op`() {
|
||||||
|
val vm = createViewModel()
|
||||||
|
|
||||||
|
vm.confirmDeleteJob()
|
||||||
|
settle()
|
||||||
|
|
||||||
|
assertNull(vm.uiState.value.deleteTarget)
|
||||||
|
coVerify(exactly = 0) { mockApi.deleteCronJob(any()) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `setMonitorMode switching source clears the other`() {
|
fun `setMonitorMode switching source clears the other`() {
|
||||||
val vm = createViewModel()
|
val vm = createViewModel()
|
||||||
|
|||||||
Reference in new issue
Block a user