diff --git a/src/components/dialog/content/setting/KeybindingPanel.test.ts b/src/components/dialog/content/setting/KeybindingPanel.test.ts
deleted file mode 100644
index 948faaf50..000000000
--- a/src/components/dialog/content/setting/KeybindingPanel.test.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-import { mount } from '@vue/test-utils'
-import { createPinia, setActivePinia } from 'pinia'
-import PrimeVue from 'primevue/config'
-import Tooltip from 'primevue/tooltip'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { nextTick } from 'vue'
-import { createI18n } from 'vue-i18n'
-
-import KeybindingPanel from './KeybindingPanel.vue'
-import {
- KeyComboImpl,
- KeybindingImpl,
- useKeybindingStore
-} from '@/stores/keybindingStore'
-
-const i18n = createI18n({
- legacy: false,
- locale: 'en',
- messages: { en: {} }
-})
-
-const mockPersist = vi.fn().mockResolvedValue(undefined)
-vi.mock('@/services/keybindingService', () => ({
- useKeybindingService: () => ({
- persistUserKeybindings: mockPersist
- })
-}))
-
-vi.mock('primevue/usetoast', () => ({
- useToast: () => ({
- add: vi.fn()
- })
-}))
-
-vi.mock('@/stores/commandStore', () => ({
- useCommandStore: () => ({
- commands: {
- 'test-command': { id: 'test-command', label: 'Test Command' },
- 'command-a': { id: 'command-a', label: 'Command A' },
- 'command-b': { id: 'command-b', label: 'Command B' }
- }
- })
-}))
-
-describe('KeybindingPanel', () => {
- beforeEach(() => {
- setActivePinia(createPinia())
- vi.clearAllMocks()
- })
-
- const mountPanel = () => {
- return mount(KeybindingPanel, {
- global: {
- plugins: [PrimeVue, i18n],
- directives: { tooltip: Tooltip },
- stubs: {
- DataTable: true,
- Column: true,
- Dialog: {
- template:
- '
',
- props: ['visible', 'header']
- },
- InputText: true,
- Message: true,
- Tag: true,
- Button: true,
- SearchBox: true,
- PanelTemplate: {
- template: '
'
- },
- KeyComboDisplay: true
- }
- }
- })
- }
-
- describe('saveKeybinding', () => {
- it('should close dialog before updating store to prevent warning flash', async () => {
- const keybindingStore = useKeybindingStore()
- let dialogVisibleWhenStoreUpdated: boolean | undefined
-
- const wrapper = mountPanel()
- const vm = wrapper.vm as InstanceType
-
- // Patch keybindingStore to capture dialog state when store is updated
- const originalUpdate = keybindingStore.updateKeybindingOnCommand
- keybindingStore.updateKeybindingOnCommand = (keybinding) => {
- dialogVisibleWhenStoreUpdated = (
- vm as unknown as { editDialogVisible: boolean }
- ).editDialogVisible
- return originalUpdate.call(keybindingStore, keybinding)
- }
-
- // Setup editing state
- ;(
- vm as unknown as { currentEditingCommand: { id: string } }
- ).currentEditingCommand = { id: 'test-command' }
- ;(
- vm as unknown as { newBindingKeyCombo: KeyComboImpl }
- ).newBindingKeyCombo = new KeyComboImpl({ key: 's', ctrl: true })
- ;(vm as unknown as { editDialogVisible: boolean }).editDialogVisible =
- true
-
- await (
- vm as unknown as { saveKeybinding: () => Promise }
- ).saveKeybinding()
-
- // Dialog should be closed when store updates (prevents warning flash)
- expect(dialogVisibleWhenStoreUpdated).toBe(false)
- })
-
- it('should persist keybinding after closing dialog', async () => {
- const wrapper = mountPanel()
- const vm = wrapper.vm as InstanceType
-
- // Setup editing state
- ;(
- vm as unknown as { currentEditingCommand: { id: string } }
- ).currentEditingCommand = { id: 'test-command' }
- ;(
- vm as unknown as { newBindingKeyCombo: KeyComboImpl }
- ).newBindingKeyCombo = new KeyComboImpl({ key: 's', ctrl: true })
- ;(vm as unknown as { editDialogVisible: boolean }).editDialogVisible =
- true
-
- await (
- vm as unknown as { saveKeybinding: () => Promise }
- ).saveKeybinding()
-
- expect(mockPersist).toHaveBeenCalled()
- expect(
- (vm as unknown as { editDialogVisible: boolean }).editDialogVisible
- ).toBe(false)
- })
-
- it('should not show warning after save button is clicked', async () => {
- const keybindingStore = useKeybindingStore()
-
- // Setup existing keybinding that will conflict
- keybindingStore.addUserKeybinding(
- new KeybindingImpl({
- commandId: 'existing-command',
- combo: new KeyComboImpl({ key: 's', ctrl: true })
- })
- )
-
- const wrapper = mountPanel()
- const vm = wrapper.vm as InstanceType
-
- // Setup editing with conflicting combo
- ;(
- vm as unknown as {
- currentEditingCommand: { id: string; keybinding: null }
- }
- ).currentEditingCommand = {
- id: 'new-command',
- keybinding: null
- }
- ;(
- vm as unknown as { newBindingKeyCombo: KeyComboImpl }
- ).newBindingKeyCombo = new KeyComboImpl({ key: 's', ctrl: true })
- ;(vm as unknown as { editDialogVisible: boolean }).editDialogVisible =
- true
- await nextTick()
-
- // Verify warning is shown before save
- expect(
- (vm as unknown as { existingKeybindingOnCombo: KeybindingImpl | null })
- .existingKeybindingOnCombo
- ).not.toBeNull()
-
- // Click save
- await (
- vm as unknown as { saveKeybinding: () => Promise }
- ).saveKeybinding()
- await nextTick()
-
- // After save, dialog should be closed AND state should be cleared
- expect(
- (vm as unknown as { editDialogVisible: boolean }).editDialogVisible
- ).toBe(false)
- expect(
- (vm as unknown as { currentEditingCommand: null }).currentEditingCommand
- ).toBeNull()
- expect(
- (vm as unknown as { newBindingKeyCombo: null }).newBindingKeyCombo
- ).toBeNull()
- })
- })
-
- describe('keybinding overwrite flow', () => {
- it('should successfully overwrite existing keybinding without flash', async () => {
- const keybindingStore = useKeybindingStore()
-
- // Setup existing keybinding
- keybindingStore.addUserKeybinding(
- new KeybindingImpl({
- commandId: 'command-a',
- combo: new KeyComboImpl({ key: 'a', ctrl: true })
- })
- )
-
- const wrapper = mountPanel()
- const vm = wrapper.vm as InstanceType
-
- // Open edit for command-b with same combo (conflict)
- ;(
- vm as unknown as {
- currentEditingCommand: { id: string; keybinding: null }
- }
- ).currentEditingCommand = {
- id: 'command-b',
- keybinding: null
- }
- ;(
- vm as unknown as { newBindingKeyCombo: KeyComboImpl }
- ).newBindingKeyCombo = new KeyComboImpl({ key: 'a', ctrl: true })
- ;(vm as unknown as { editDialogVisible: boolean }).editDialogVisible =
- true
- await nextTick()
-
- // Verify conflict detected
- expect(
- (vm as unknown as { existingKeybindingOnCombo: KeybindingImpl })
- .existingKeybindingOnCombo?.commandId
- ).toBe('command-a')
-
- // Save (overwrite)
- await (
- vm as unknown as { saveKeybinding: () => Promise }
- ).saveKeybinding()
-
- // Verify dialog closed
- expect(
- (vm as unknown as { editDialogVisible: boolean }).editDialogVisible
- ).toBe(false)
-
- // Verify new keybinding saved
- expect(
- keybindingStore.getKeybindingByCommandId('command-b')
- ).toBeDefined()
-
- // Verify combo now belongs to command-b
- const combo = new KeyComboImpl({ key: 'a', ctrl: true })
- expect(keybindingStore.getKeybinding(combo)?.commandId).toBe('command-b')
-
- // Verify persistence called
- expect(mockPersist).toHaveBeenCalled()
- })
- })
-})
diff --git a/src/components/dialog/content/setting/KeybindingPanel.vue b/src/components/dialog/content/setting/KeybindingPanel.vue
index 4e4b141e7..7724245d5 100644
--- a/src/components/dialog/content/setting/KeybindingPanel.vue
+++ b/src/components/dialog/content/setting/KeybindingPanel.vue
@@ -197,24 +197,20 @@ const currentEditingCommand = ref(null)
const keybindingInput = ref()
const existingKeybindingOnCombo = computed(() => {
- if (!currentEditingCommand.value) {
- return null
- }
-
- // If the new keybinding is the same as the current editing command, then don't show the error
if (
- currentEditingCommand.value.keybinding?.combo?.equals(
- newBindingKeyCombo.value
- )
+ !editDialogVisible.value ||
+ !currentEditingCommand.value ||
+ !newBindingKeyCombo.value
) {
return null
}
- if (!newBindingKeyCombo.value) {
+ const existing = keybindingStore.getKeybinding(newBindingKeyCombo.value)
+ if (!existing || existing.commandId === currentEditingCommand.value.id) {
return null
}
- return keybindingStore.getKeybinding(newBindingKeyCombo.value)
+ return existing
})
function editKeybinding(commandData: ICommandData) {
@@ -265,18 +261,15 @@ function cancelEdit() {
async function saveKeybinding() {
if (!currentEditingCommand.value || !newBindingKeyCombo.value) return
- // Capture values before closing dialog
- const commandId = currentEditingCommand.value.id
- const combo = newBindingKeyCombo.value
-
- // Close dialog FIRST to prevent warning flash during store update
- cancelEdit()
-
- // Update store after dialog is closed
const updated = keybindingStore.updateKeybindingOnCommand(
- new KeybindingImpl({ commandId, combo })
+ new KeybindingImpl({
+ commandId: currentEditingCommand.value.id,
+ combo: newBindingKeyCombo.value
+ })
)
+ cancelEdit()
+
if (updated) {
await keybindingService.persistUserKeybindings()
}