feat: replace confirm dialogs with custom small layout dialogs

Use showSmallLayoutDialog pattern (like missing models dialog) for
unsaved changes and delete preset dialogs with explicit action buttons.
This commit is contained in:
Johnpaul
2026-03-10 03:13:44 +01:00
parent 2d9d699f33
commit 0fd9989e3c
7 changed files with 165 additions and 16 deletions

View File

@@ -0,0 +1,31 @@
<template>
<div
class="flex w-full max-w-[420px] flex-col border-t border-border-default"
>
<div class="flex flex-col gap-4 p-4">
<p class="m-0 text-sm text-muted-foreground">
{{ $t('g.keybindingPresets.deletePresetWarning') }}
</p>
<div class="flex justify-end gap-2">
<Button
variant="textonly"
class="text-muted-foreground"
@click="onResult(false)"
>
{{ $t('g.close') }}
</Button>
<Button variant="secondary" @click="onResult(true)">
{{ $t('g.delete') }}
</Button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
const { onResult } = defineProps<{
onResult: (result: boolean) => void
}>()
</script>

View File

@@ -0,0 +1,7 @@
<template>
<div class="flex w-full items-center p-4">
<p class="m-0 text-sm font-medium">
{{ $t('g.keybindingPresets.deletePresetTitle') }}
</p>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<template>
<div
class="flex w-full max-w-[420px] flex-col border-t border-border-default"
>
<div class="flex flex-col gap-4 p-4">
<p class="m-0 text-sm text-muted-foreground">
{{ $t('g.keybindingPresets.unsavedChangesMessage') }}
</p>
<div class="flex justify-end gap-2">
<Button
variant="textonly"
class="text-muted-foreground"
@click="onResult(null)"
>
{{ $t('g.cancel') }}
</Button>
<Button
variant="secondary"
class="bg-secondary-background"
@click="onResult(false)"
>
{{ $t('g.keybindingPresets.discardAndSwitch') }}
</Button>
<Button
variant="secondary"
class="bg-base-foreground text-base-background"
@click="onResult(true)"
>
{{ $t('g.keybindingPresets.saveAndSwitch') }}
</Button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Button from '@/components/ui/button/Button.vue'
const { onResult } = defineProps<{
onResult: (result: boolean | null) => void
}>()
</script>

View File

@@ -0,0 +1,13 @@
<template>
<div class="flex w-full items-center p-4">
<p class="m-0 text-sm font-medium">
{{ $t('g.keybindingPresets.unsavedChangesTo', { name: presetName }) }}
</p>
</div>
</template>
<script setup lang="ts">
const { presetName } = defineProps<{
presetName: string
}>()
</script>

View File

@@ -269,10 +269,12 @@
"saveAsNewPreset": "Save as new preset",
"resetToDefault": "Reset to default",
"deletePreset": "Delete preset",
"unsavedChangesTitle": "Unsaved Changes",
"unsavedChangesMessage": "You have unsaved changes to the current keybinding preset. Would you like to save them before switching?",
"deletePresetTitle": "Delete Preset",
"deletePresetMessage": "Are you sure you want to delete preset \"{name}\"?",
"unsavedChangesTo": "Unsaved changes to {name}",
"unsavedChangesMessage": "You have unsaved changes that will be lost if you switch without saving.",
"discardAndSwitch": "Discard and Switch",
"saveAndSwitch": "Save and Switch",
"deletePresetTitle": "Delete the current preset?",
"deletePresetWarning": "This preset will be deleted. This cannot be undone.",
"presetSaved": "Preset \"{name}\" saved",
"presetDeleted": "Preset \"{name}\" deleted",
"presetImported": "Keybinding preset imported",
@@ -283,7 +285,7 @@
"overwritePresetTitle": "Overwrite Preset",
"overwritePresetMessage": "A preset named \"{name}\" already exists. Overwrite it?",
"presetNamePrompt": "Enter a name for the preset",
"default": "Default"
"default": "Default Preset"
},
"commandProhibited": "Command {command} is prohibited. Contact an administrator for more information.",
"startRecording": "Start Recording",

View File

@@ -16,6 +16,13 @@ const mockApi = vi.hoisted(() => ({
const mockDownloadBlob = vi.hoisted(() => vi.fn())
const mockUploadFile = vi.hoisted(() => vi.fn())
const mockConfirm = vi.hoisted(() => vi.fn().mockResolvedValue(true))
const mockShowSmallLayoutDialog = vi.hoisted(() =>
vi.fn().mockImplementation((options: Record<string, unknown>) => {
const props = options.props as Record<string, unknown> | undefined
const onResult = props?.onResult as ((v: boolean) => void) | undefined
onResult?.(true)
})
)
const mockSettingSet = vi.hoisted(() => vi.fn())
const mockToastAdd = vi.hoisted(() => vi.fn())
const mockPersistUserKeybindings = vi.hoisted(() =>
@@ -37,7 +44,8 @@ vi.mock('@/scripts/utils', () => ({
vi.mock('@/services/dialogService', () => ({
useDialogService: () => ({
confirm: mockConfirm,
prompt: vi.fn().mockResolvedValue('test-preset')
prompt: vi.fn().mockResolvedValue('test-preset'),
showSmallLayoutDialog: mockShowSmallLayoutDialog
})
}))

View File

@@ -2,6 +2,10 @@ import { toRaw } from 'vue'
import { fromZodError } from 'zod-validation-error'
import { downloadBlob } from '@/base/common/downloadUtil'
import DeletePresetContent from '@/components/dialog/content/setting/keybinding/DeletePresetContent.vue'
import DeletePresetHeader from '@/components/dialog/content/setting/keybinding/DeletePresetHeader.vue'
import UnsavedChangesContent from '@/components/dialog/content/setting/keybinding/UnsavedChangesContent.vue'
import UnsavedChangesHeader from '@/components/dialog/content/setting/keybinding/UnsavedChangesHeader.vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
@@ -9,6 +13,7 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { uploadFile } from '@/scripts/utils'
import { useDialogService } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'
import { KeybindingImpl } from './keybinding'
import { useKeybindingService } from './keybindingService'
@@ -50,9 +55,54 @@ export function useKeybindingPresetService() {
const keybindingService = useKeybindingService()
const settingStore = useSettingStore()
const dialogService = useDialogService()
const dialogStore = useDialogStore()
const toast = useToastStore()
const { wrapWithErrorHandlingAsync } = useErrorHandling()
const DELETE_DIALOG_KEY = 'delete-keybinding-preset'
const UNSAVED_DIALOG_KEY = 'unsaved-keybinding-changes'
function showDeletePresetDialog(): Promise<boolean> {
return new Promise((resolve) => {
dialogService.showSmallLayoutDialog({
key: DELETE_DIALOG_KEY,
headerComponent: DeletePresetHeader,
component: DeletePresetContent,
props: {
onResult: (result: boolean) => {
resolve(result)
dialogStore.closeDialog({ key: DELETE_DIALOG_KEY })
}
},
dialogComponentProps: {
onClose: () => resolve(false)
}
})
})
}
function showUnsavedChangesDialog(
presetName: string
): Promise<boolean | null> {
return new Promise((resolve) => {
dialogService.showSmallLayoutDialog({
key: UNSAVED_DIALOG_KEY,
headerComponent: UnsavedChangesHeader,
headerProps: { presetName },
component: UnsavedChangesContent,
props: {
onResult: (result: boolean | null) => {
resolve(result)
dialogStore.closeDialog({ key: UNSAVED_DIALOG_KEY })
}
},
dialogComponentProps: {
onClose: () => resolve(null)
}
})
})
}
async function listPresets(): Promise<string[]> {
const files = await api.listUserDataFullInfo(PRESETS_DIR)
return files
@@ -106,11 +156,7 @@ export function useKeybindingPresetService() {
}
async function deletePreset(name: string) {
const confirmed = await dialogService.confirm({
title: t('g.keybindingPresets.deletePresetTitle'),
message: t('g.keybindingPresets.deletePresetMessage', { name }),
type: 'delete'
})
const confirmed = await showDeletePresetDialog()
if (!confirmed) return
const resp = await api.deleteUserData(presetFilePath(name))
@@ -176,11 +222,11 @@ export function useKeybindingPresetService() {
async function switchPreset(targetName: string) {
if (keybindingStore.isCurrentPresetModified) {
const result = await dialogService.confirm({
title: t('g.keybindingPresets.unsavedChangesTitle'),
message: t('g.keybindingPresets.unsavedChangesMessage'),
type: 'dirtyClose'
})
const displayName =
keybindingStore.currentPresetName === 'default'
? t('g.keybindingPresets.default')
: keybindingStore.currentPresetName
const result = await showUnsavedChangesDialog(displayName)
if (result === null) return
if (result) {