From 05be524e75c1edad42ff83759753bd127f38e569 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Wed, 20 Aug 2025 00:09:12 +0300 Subject: [PATCH 1/4] chore: improve delete confirmation dialog The message is now english friendly and more clear. See issue #178 --- CHANGELOG.md | 1 + src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e87ca97..dc0da95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - workspaces status is now refresh every time Coder Toolbox becomes visible +- improved workspace delete confirmation dialog ### Fixed diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index f8b3a17..8dbfb11 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -277,8 +277,8 @@ class CoderRemoteEnvironment( override val cancelButtonText: String = "Cancel" override val confirmButtonText: String = "Delete" override val message: String = - if (wsRawStatus.canStop()) "Workspace will be closed and all the information will be lost, including all files, unsaved changes, historical info and usage data." - else "All the information in this workspace will be lost, including all files, unsaved changes, historical info and usage data." + if (wsRawStatus.canStop()) "This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data." + else "This will remove all information from the workspace, including files, unsaved changes, history, and usage data." override val title: String = if (wsRawStatus.canStop()) "Delete running workspace?" else "Delete workspace?" } } From e63fd0784ee952359decc1f06d3e0af82a3546c8 Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 21 Aug 2025 21:54:12 +0300 Subject: [PATCH 2/4] impl: confirmation dialog for workspace deletion Users are now required to confirm the workspace name if they want to delete a workspace. This is in order to avoid any accidental removals. --- CHANGELOG.md | 2 +- .../coder/toolbox/CoderRemoteEnvironment.kt | 74 ++++++++++--------- .../com/coder/toolbox/CoderRemoteProvider.kt | 6 +- .../com/coder/toolbox/views/CoderPage.kt | 3 + .../resources/localization/defaultMessages.po | 17 ++++- 5 files changed, 63 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc0da95..982be3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ ### Changed - workspaces status is now refresh every time Coder Toolbox becomes visible -- improved workspace delete confirmation dialog +- workspaces can no longer be removed by accident - users are now required to input the workspace name. ### Fixed diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 8dbfb11..5e69faf 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -12,17 +12,18 @@ import com.coder.toolbox.sdk.v2.models.WorkspaceAgent import com.coder.toolbox.util.waitForFalseWithTimeout import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action +import com.coder.toolbox.views.CoderDelimiter import com.coder.toolbox.views.EnvironmentView import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.remoteDev.AfterDisconnectHook import com.jetbrains.toolbox.api.remoteDev.BeforeConnectionHook -import com.jetbrains.toolbox.api.remoteDev.DeleteEnvironmentConfirmationParams import com.jetbrains.toolbox.api.remoteDev.EnvironmentVisibilityState import com.jetbrains.toolbox.api.remoteDev.RemoteProviderEnvironment import com.jetbrains.toolbox.api.remoteDev.environments.EnvironmentContentsView import com.jetbrains.toolbox.api.remoteDev.states.EnvironmentDescription import com.jetbrains.toolbox.api.remoteDev.states.RemoteEnvironmentState import com.jetbrains.toolbox.api.ui.actions.ActionDescription +import com.jetbrains.toolbox.api.ui.components.TextType import com.squareup.moshi.Moshi import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -78,7 +79,7 @@ class CoderRemoteEnvironment( fun asPairOfWorkspaceAndAgent(): Pair = Pair(workspace, agent) private fun getAvailableActions(): List { - val actions = mutableListOf() + val actions = mutableListOf() if (wsRawStatus.canStop()) { actions.add(Action(context.i18n.ptrl("Open web terminal")) { context.cs.launch { @@ -143,6 +144,24 @@ class CoderRemoteEnvironment( } }) } + actions.add(CoderDelimiter(context.i18n.pnotr(""))) + actions.add(Action(context.i18n.ptrl("Delete workspace")) { + context.cs.launch { + val confirmation = context.ui.showTextInputPopup( + if (wsRawStatus.canStop()) context.i18n.ptrl("Delete running workspace?") else context.i18n.ptrl("Delete workspace?"), + if (wsRawStatus.canStop()) context.i18n.ptrl("This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data.") + else context.i18n.ptrl("This will remove all information from the workspace, including files, unsaved changes, history, and usage data."), + context.i18n.ptrl("Workspace name"), + TextType.General, + context.i18n.ptrl("OK"), + context.i18n.ptrl("Cancel") + ) + if (confirmation != workspace.name) { + return@launch + } + deleteWorkspace() + } + }) return actions } @@ -272,43 +291,32 @@ class CoderRemoteEnvironment( return false } - override fun getDeleteEnvironmentConfirmationParams(): DeleteEnvironmentConfirmationParams? { - return object : DeleteEnvironmentConfirmationParams { - override val cancelButtonText: String = "Cancel" - override val confirmButtonText: String = "Delete" - override val message: String = - if (wsRawStatus.canStop()) "This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data." - else "This will remove all information from the workspace, including files, unsaved changes, history, and usage data." - override val title: String = if (wsRawStatus.canStop()) "Delete running workspace?" else "Delete workspace?" - } - } + override val deleteActionFlow: StateFlow<(() -> Unit)?> = MutableStateFlow(null) - override val deleteActionFlow: StateFlow<(() -> Unit)?> = MutableStateFlow { - context.cs.launch { - try { - client.removeWorkspace(workspace) - // mark the env as deleting otherwise we will have to - // wait for the poller to update the status in the next 5 seconds - state.update { - WorkspaceAndAgentStatus.DELETING.toRemoteEnvironmentState(context) - } + suspend fun deleteWorkspace() { + try { + client.removeWorkspace(workspace) + // mark the env as deleting otherwise we will have to + // wait for the poller to update the status in the next 5 seconds + state.update { + WorkspaceAndAgentStatus.DELETING.toRemoteEnvironmentState(context) + } - context.cs.launch { - withTimeout(5.minutes) { - var workspaceStillExists = true - while (context.cs.isActive && workspaceStillExists) { - if (wsRawStatus == WorkspaceAndAgentStatus.DELETING || wsRawStatus == WorkspaceAndAgentStatus.DELETED) { - workspaceStillExists = false - context.envPageManager.showPluginEnvironmentsPage() - } else { - delay(1.seconds) - } + context.cs.launch { + withTimeout(5.minutes) { + var workspaceStillExists = true + while (context.cs.isActive && workspaceStillExists) { + if (wsRawStatus == WorkspaceAndAgentStatus.DELETING || wsRawStatus == WorkspaceAndAgentStatus.DELETED) { + workspaceStillExists = false + context.envPageManager.showPluginEnvironmentsPage() + } else { + delay(1.seconds) } } } - } catch (e: APIResponseException) { - context.ui.showErrorInfoPopup(e) } + } catch (e: APIResponseException) { + context.ui.showErrorInfoPopup(e) } } diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt index 596255e..2584117 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt @@ -11,6 +11,7 @@ import com.coder.toolbox.util.waitForTrue import com.coder.toolbox.util.withPath import com.coder.toolbox.views.Action import com.coder.toolbox.views.CoderCliSetupWizardPage +import com.coder.toolbox.views.CoderDelimiter import com.coder.toolbox.views.CoderSettingsPage import com.coder.toolbox.views.NewEnvironmentPage import com.coder.toolbox.views.state.CoderCliSetupWizardState @@ -21,7 +22,6 @@ import com.jetbrains.toolbox.api.core.util.LoadableState import com.jetbrains.toolbox.api.localization.LocalizableString import com.jetbrains.toolbox.api.remoteDev.ProviderVisibilityState import com.jetbrains.toolbox.api.remoteDev.RemoteProvider -import com.jetbrains.toolbox.api.ui.actions.ActionDelimiter import com.jetbrains.toolbox.api.ui.actions.ActionDescription import com.jetbrains.toolbox.api.ui.components.UiPage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -416,6 +416,4 @@ class CoderRemoteProvider( LoadableState.Loading } } -} - -private class CoderDelimiter(override val label: LocalizableString) : ActionDelimiter \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index 363d618..b65013f 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -4,6 +4,7 @@ import com.coder.toolbox.CoderToolboxContext import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon import com.jetbrains.toolbox.api.core.ui.icons.SvgIcon.IconType import com.jetbrains.toolbox.api.localization.LocalizableString +import com.jetbrains.toolbox.api.ui.actions.ActionDelimiter import com.jetbrains.toolbox.api.ui.actions.RunnableActionDescription import com.jetbrains.toolbox.api.ui.components.UiPage import kotlinx.coroutines.flow.MutableStateFlow @@ -67,3 +68,5 @@ class Action( actionBlock() } } + +class CoderDelimiter(override val label: LocalizableString) : ActionDelimiter \ No newline at end of file diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 8aabe3f..3ef44a6 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -179,4 +179,19 @@ msgid "Headers" msgstr "" msgid "Body" -msgstr "" \ No newline at end of file +msgstr "" + +msgid "Delete workspace" +msgstr "" + +msgid "Delete running workspace?" +msgstr "" + +msgid "This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data." +msgstr "" + +msgid "This will remove all information from the workspace, including files, unsaved changes, history, and usage data." +msgstr "" + +msgid "Workspace name" +msgstr "" From ad053d1d12375740b1c3c38bb118d8d4fccb3f7f Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 21 Aug 2025 22:48:29 +0300 Subject: [PATCH 3/4] impl: highlight in red the "Delete workspace" action Similar to the original "Delete" button provided by JetBrains. --- src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt | 2 +- src/main/kotlin/com/coder/toolbox/views/CoderPage.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 5e69faf..552a3fa 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -145,7 +145,7 @@ class CoderRemoteEnvironment( }) } actions.add(CoderDelimiter(context.i18n.pnotr(""))) - actions.add(Action(context.i18n.ptrl("Delete workspace")) { + actions.add(Action(context.i18n.ptrl("Delete workspace"), highlightInRed = true) { context.cs.launch { val confirmation = context.ui.showTextInputPopup( if (wsRawStatus.canStop()) context.i18n.ptrl("Delete running workspace?") else context.i18n.ptrl("Delete workspace?"), diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt index b65013f..1fd4cc3 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderPage.kt @@ -58,12 +58,14 @@ abstract class CoderPage( class Action( description: LocalizableString, closesPage: Boolean = false, + highlightInRed: Boolean = false, enabled: () -> Boolean = { true }, private val actionBlock: () -> Unit, ) : RunnableActionDescription { override val label: LocalizableString = description override val shouldClosePage: Boolean = closesPage override val isEnabled: Boolean = enabled() + override val isDangerous: Boolean = highlightInRed override fun run() { actionBlock() } From 3df7c526b161643456c279b240167d889f6f0ccc Mon Sep 17 00:00:00 2001 From: Faur Ioan-Aurel Date: Thu, 21 Aug 2025 23:01:11 +0300 Subject: [PATCH 4/4] chore: improve delete dialog message Make it clear to the user what does he need to type. --- .../kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt | 8 ++++++-- src/main/resources/localization/defaultMessages.po | 6 ------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt index 552a3fa..91553e0 100644 --- a/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt +++ b/src/main/kotlin/com/coder/toolbox/CoderRemoteEnvironment.kt @@ -147,10 +147,14 @@ class CoderRemoteEnvironment( actions.add(CoderDelimiter(context.i18n.pnotr(""))) actions.add(Action(context.i18n.ptrl("Delete workspace"), highlightInRed = true) { context.cs.launch { + var dialogText = + if (wsRawStatus.canStop()) "This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data." + else "This will remove all information from the workspace, including files, unsaved changes, history, and usage data." + dialogText += "\n\nType \"${workspace.name}\" below to confirm:" + val confirmation = context.ui.showTextInputPopup( if (wsRawStatus.canStop()) context.i18n.ptrl("Delete running workspace?") else context.i18n.ptrl("Delete workspace?"), - if (wsRawStatus.canStop()) context.i18n.ptrl("This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data.") - else context.i18n.ptrl("This will remove all information from the workspace, including files, unsaved changes, history, and usage data."), + context.i18n.pnotr(dialogText), context.i18n.ptrl("Workspace name"), TextType.General, context.i18n.ptrl("OK"), diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index 3ef44a6..29351e3 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -187,11 +187,5 @@ msgstr "" msgid "Delete running workspace?" msgstr "" -msgid "This will close the workspace and remove all its information, including files, unsaved changes, history, and usage data." -msgstr "" - -msgid "This will remove all information from the workspace, including files, unsaved changes, history, and usage data." -msgstr "" - msgid "Workspace name" msgstr ""