feat: refactor existing shortcuts to use shortcut manager (Phase 2)
- Update v-shortcut directive to support both old format (keys) and new format (actionIds) - Refactor general shortcuts: MenuButton (Ctrl+E), OpenQuickActions (Ctrl+K), ContentAuth (Shift+?) - Refactor navigation shortcuts: all g+key sequences in Navigation.vue now use actionIds - Refactor task detail shortcuts: all 14 task shortcuts + Ctrl+S now use shortcut manager - Update help modal to show effective shortcuts and link to settings page - Add showHelp shortcut action for Shift+? keyboard shortcut help All existing shortcuts now use the shortcut manager system and will respect user customizations. The help modal displays current effective shortcuts (default or customized) and provides a direct link to the keyboard shortcuts settings page for easy customization.
This commit is contained in:
parent
94e334fa71
commit
c16a2cf362
|
|
@ -55,7 +55,7 @@
|
|||
</Modal>
|
||||
|
||||
<BaseButton
|
||||
v-shortcut="'Shift+?'"
|
||||
v-shortcut="'.general.showHelp'"
|
||||
class="keyboard-shortcuts-button d-print-none"
|
||||
@click="showKeyboardShortcuts()"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<BaseButton
|
||||
v-shortcut="'Mod+e'"
|
||||
v-shortcut="'.general.toggleMenu'"
|
||||
class="menu-show-button"
|
||||
:title="$t('keyboardShortcuts.toggleMenu')"
|
||||
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<menu class="menu-list other-menu-items">
|
||||
<li>
|
||||
<RouterLink
|
||||
v-shortcut="'g o'"
|
||||
v-shortcut="'.navigation.goToOverview'"
|
||||
:to="{ name: 'home'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<RouterLink
|
||||
v-shortcut="'g u'"
|
||||
v-shortcut="'.navigation.goToUpcoming'"
|
||||
:to="{ name: 'tasks.range'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<RouterLink
|
||||
v-shortcut="'g p'"
|
||||
v-shortcut="'.navigation.goToProjects'"
|
||||
:to="{ name: 'projects.index'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<RouterLink
|
||||
v-shortcut="'g a'"
|
||||
v-shortcut="'.navigation.goToLabels'"
|
||||
:to="{ name: 'labels.index'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<RouterLink
|
||||
v-shortcut="'g m'"
|
||||
v-shortcut="'.navigation.goToTeams'"
|
||||
:to="{ name: 'teams.index'}"
|
||||
>
|
||||
<span class="menu-item-icon icon">
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
|||
import {useBaseStore} from '@/stores/base'
|
||||
import {onBeforeUnmount, onMounted} from 'vue'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||
import {useShortcutManager} from '@/composables/useShortcutManager'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const shortcutManager = useShortcutManager()
|
||||
|
||||
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
|
||||
function openQuickActionsViaHotkey(event) {
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (!hotkeyString) return
|
||||
|
||||
// On macOS, use Cmd+K (Meta+K), on other platforms use Ctrl+K (Control+K)
|
||||
const expectedHotkey = isAppleDevice() ? 'Meta+k' : 'Control+k'
|
||||
|
||||
const expectedHotkey = shortcutManager.getHotkeyString('general.quickSearch')
|
||||
if (hotkeyString !== expectedHotkey) return
|
||||
|
||||
|
||||
event.preventDefault()
|
||||
|
||||
openQuickActions()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,18 @@
|
|||
:show-close="true"
|
||||
@close="close()"
|
||||
>
|
||||
<template #header>
|
||||
<div class="help-header">
|
||||
<h2>{{ $t('keyboardShortcuts.title') }}</h2>
|
||||
<RouterLink
|
||||
:to="{ name: 'user.settings.keyboardShortcuts' }"
|
||||
class="button is-small"
|
||||
@click="close()"
|
||||
>
|
||||
{{ $t('keyboardShortcuts.customizeShortcuts') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
v-for="(s, i) in shortcuts"
|
||||
:key="i"
|
||||
|
|
@ -39,27 +51,44 @@
|
|||
<Shortcut
|
||||
is="dd"
|
||||
class="shortcut-keys"
|
||||
:keys="sc.keys"
|
||||
:keys="getEffectiveKeys(sc)"
|
||||
:combination="sc.combination && $t(`keyboardShortcuts.${sc.combination}`)"
|
||||
/>
|
||||
</template>
|
||||
</dl>
|
||||
</template>
|
||||
|
||||
<p class="help-text">
|
||||
{{ $t('keyboardShortcuts.helpText') }}
|
||||
</p>
|
||||
</Card>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useShortcutManager} from '@/composables/useShortcutManager'
|
||||
|
||||
import Shortcut from '@/components/misc/Shortcut.vue'
|
||||
import Message from '@/components/misc/Message.vue'
|
||||
|
||||
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'
|
||||
import type {ShortcutAction} from './shortcuts'
|
||||
|
||||
const shortcutManager = useShortcutManager()
|
||||
|
||||
function close() {
|
||||
useBaseStore().setKeyboardShortcutsActive(false)
|
||||
}
|
||||
|
||||
function getEffectiveKeys(shortcut: ShortcutAction): string[] {
|
||||
// For shortcuts with actionId, get effective keys from shortcut manager
|
||||
if (shortcut.actionId) {
|
||||
return shortcutManager.getShortcut(shortcut.actionId) || shortcut.keys
|
||||
}
|
||||
// Fallback to default keys for backwards compatibility
|
||||
return shortcut.keys
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
@ -67,6 +96,25 @@ function close() {
|
|||
text-align: start;
|
||||
}
|
||||
|
||||
.help-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.help-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.help-text {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--grey-200);
|
||||
color: var(--text-light);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message:not(:last-child) {
|
||||
margin-block-end: 1rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ export const KEYBOARD_SHORTCUTS: ShortcutGroup[] = [
|
|||
contexts: ['*'],
|
||||
category: ShortcutCategory.GENERAL,
|
||||
},
|
||||
{
|
||||
actionId: 'general.showHelp',
|
||||
title: 'keyboardShortcuts.showHelp',
|
||||
keys: ['shift', '?'],
|
||||
customizable: true,
|
||||
contexts: ['*'],
|
||||
category: ShortcutCategory.GENERAL,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,12 +1,44 @@
|
|||
import type {Directive} from 'vue'
|
||||
import {install, uninstall} from '@github/hotkey'
|
||||
import {useShortcutManager} from '@/composables/useShortcutManager'
|
||||
|
||||
const directive = <Directive<HTMLElement,string>>{
|
||||
mounted(el, {value}) {
|
||||
if(value === '') {
|
||||
return
|
||||
}
|
||||
install(el, value)
|
||||
|
||||
// Support both old format (direct keys) and new format (actionId)
|
||||
const shortcutManager = useShortcutManager()
|
||||
const hotkeyString = value.startsWith('.')
|
||||
? shortcutManager.getHotkeyString(value.slice(1)) // New format: actionId (remove leading dot)
|
||||
: value // Old format: direct keys (backwards compat)
|
||||
|
||||
if (!hotkeyString) return
|
||||
|
||||
install(el, hotkeyString)
|
||||
|
||||
// Store for cleanup and updates
|
||||
el.dataset.shortcutActionId = value
|
||||
},
|
||||
updated(el, {value, oldValue}) {
|
||||
if (value === oldValue) return
|
||||
|
||||
// Reinstall with new shortcut
|
||||
uninstall(el)
|
||||
|
||||
if(value === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const shortcutManager = useShortcutManager()
|
||||
const hotkeyString = value.startsWith('.')
|
||||
? shortcutManager.getHotkeyString(value.slice(1))
|
||||
: value
|
||||
|
||||
if (!hotkeyString) return
|
||||
install(el, hotkeyString)
|
||||
el.dataset.shortcutActionId = value
|
||||
},
|
||||
beforeUnmount(el) {
|
||||
uninstall(el)
|
||||
|
|
|
|||
|
|
@ -1106,6 +1106,7 @@
|
|||
"somePagesOnly": "These shortcuts work only on some pages.",
|
||||
"toggleMenu": "Toggle The Menu",
|
||||
"quickSearch": "Open the search/quick action bar",
|
||||
"showHelp": "Show keyboard shortcuts help",
|
||||
"then": "then",
|
||||
"fixed": "Fixed",
|
||||
"pressKeys": "Press keys...",
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@
|
|||
>
|
||||
<a
|
||||
v-if="router.options.history.state?.back?.includes('/projects/'+p.id+'/') || false"
|
||||
v-shortcut="p.id === project?.id ? 'u' : ''"
|
||||
v-shortcut="p.id === project?.id ? '.task.openProject' : ''"
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ getProjectTitle(p) }}
|
||||
</a>
|
||||
<RouterLink
|
||||
v-else
|
||||
v-shortcut="p.id === project?.id ? 'u' : ''"
|
||||
v-shortcut="p.id === project?.id ? '.task.openProject' : ''"
|
||||
:to="{ name: 'project.index', params: { projectId: p.id } }"
|
||||
>
|
||||
{{ getProjectTitle(p) }}
|
||||
|
|
@ -416,7 +416,7 @@
|
|||
>
|
||||
<template v-if="canWrite">
|
||||
<XButton
|
||||
v-shortcut="'t'"
|
||||
v-shortcut="'.task.markDone'"
|
||||
:class="{'is-success': !task.done}"
|
||||
:shadow="task.done"
|
||||
class="is-outlined has-no-border"
|
||||
|
|
@ -433,7 +433,7 @@
|
|||
@update:modelValue="sub => task.subscription = sub"
|
||||
/>
|
||||
<XButton
|
||||
v-shortcut="'s'"
|
||||
v-shortcut="'.task.toggleFavorite'"
|
||||
variant="secondary"
|
||||
:icon="task.isFavorite ? 'star' : ['far', 'star']"
|
||||
@click="toggleFavorite"
|
||||
|
|
@ -446,7 +446,7 @@
|
|||
<span class="action-heading">{{ $t('task.detail.organization') }}</span>
|
||||
|
||||
<XButton
|
||||
v-shortcut="'l'"
|
||||
v-shortcut="'.task.labels'"
|
||||
variant="secondary"
|
||||
icon="tags"
|
||||
@click="setFieldActive('labels')"
|
||||
|
|
@ -454,7 +454,7 @@
|
|||
{{ $t('task.detail.actions.label') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
v-shortcut="'p'"
|
||||
v-shortcut="'.task.priority'"
|
||||
variant="secondary"
|
||||
icon="exclamation-circle"
|
||||
@click="setFieldActive('priority')"
|
||||
|
|
@ -469,7 +469,7 @@
|
|||
{{ $t('task.detail.actions.percentDone') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
v-shortcut="'c'"
|
||||
v-shortcut="'.task.color'"
|
||||
variant="secondary"
|
||||
icon="fill-drip"
|
||||
:icon-color="color"
|
||||
|
|
@ -481,7 +481,7 @@
|
|||
<span class="action-heading">{{ $t('task.detail.management') }}</span>
|
||||
|
||||
<XButton
|
||||
v-shortcut="'a'"
|
||||
v-shortcut="'.task.assign'"
|
||||
v-cy="'taskDetail.assign'"
|
||||
variant="secondary"
|
||||
icon="users"
|
||||
|
|
@ -490,7 +490,7 @@
|
|||
{{ $t('task.detail.actions.assign') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
v-shortcut="'f'"
|
||||
v-shortcut="'.task.attachment'"
|
||||
variant="secondary"
|
||||
icon="paperclip"
|
||||
@click="setFieldActive('attachments')"
|
||||
|
|
@ -498,7 +498,7 @@
|
|||
{{ $t('task.detail.actions.attachments') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
v-shortcut="'r'"
|
||||
v-shortcut="'.task.related'"
|
||||
variant="secondary"
|
||||
icon="sitemap"
|
||||
@click="setRelatedTasksActive()"
|
||||
|
|
@ -506,7 +506,7 @@
|
|||
{{ $t('task.detail.actions.relatedTasks') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
v-shortcut="'m'"
|
||||
v-shortcut="'.task.move'"
|
||||
variant="secondary"
|
||||
icon="list"
|
||||
@click="setFieldActive('moveProject')"
|
||||
|
|
@ -517,7 +517,7 @@
|
|||
<span class="action-heading">{{ $t('task.detail.dateAndTime') }}</span>
|
||||
|
||||
<XButton
|
||||
v-shortcut="'d'"
|
||||
v-shortcut="'.task.dueDate'"
|
||||
variant="secondary"
|
||||
icon="calendar"
|
||||
@click="setFieldActive('dueDate')"
|
||||
|
|
@ -539,7 +539,7 @@
|
|||
{{ $t('task.detail.actions.endDate') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
v-shortcut="reminderShortcut"
|
||||
v-shortcut="'.task.reminder'"
|
||||
variant="secondary"
|
||||
:icon="['far', 'clock']"
|
||||
@click="setFieldActive('reminders')"
|
||||
|
|
@ -554,7 +554,7 @@
|
|||
{{ $t('task.detail.actions.repeatAfter') }}
|
||||
</XButton>
|
||||
<XButton
|
||||
v-shortcut="'Shift+Delete'"
|
||||
v-shortcut="'.task.delete'"
|
||||
icon="trash-alt"
|
||||
:shadow="false"
|
||||
class="is-danger is-outlined has-no-border"
|
||||
|
|
@ -604,6 +604,7 @@ import {useI18n} from 'vue-i18n'
|
|||
import {unrefElement, useMediaQuery} from '@vueuse/core'
|
||||
import {klona} from 'klona/lite'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
import {useShortcutManager} from '@/composables/useShortcutManager'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel from '@/models/task'
|
||||
|
|
@ -640,7 +641,7 @@ import Reactions from '@/components/input/Reactions.vue'
|
|||
|
||||
import {uploadFile} from '@/helpers/attachments'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import {isAppleDevice} from '@/helpers/isAppleDevice'
|
||||
// isAppleDevice no longer needed - reminder shortcut handled by shortcut manager
|
||||
import {scrollIntoView} from '@/helpers/scrollIntoView'
|
||||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
|
|
@ -687,9 +688,12 @@ useTitle(taskTitle)
|
|||
function saveTaskViaHotkey(event) {
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (!hotkeyString) return
|
||||
if (hotkeyString !== 'Control+s' && hotkeyString !== 'Meta+s') return
|
||||
event.preventDefault()
|
||||
|
||||
const shortcutManager = useShortcutManager()
|
||||
const expectedHotkey = shortcutManager.getHotkeyString('task.save')
|
||||
if (hotkeyString !== expectedHotkey) return
|
||||
|
||||
event.preventDefault()
|
||||
saveTask()
|
||||
}
|
||||
|
||||
|
|
@ -713,7 +717,7 @@ const lastProjectOrTaskProject = computed(() => lastProject.value ?? project.val
|
|||
|
||||
// Use Shift+R on macOS (Alt+R produces special characters depending on keyboard layout)
|
||||
// Use Alt+r on other platforms
|
||||
const reminderShortcut = computed(() => isAppleDevice() ? 'Shift+R' : 'Alt+r')
|
||||
// Reminder shortcut is now handled by shortcut manager
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', saveTaskViaHotkey)
|
||||
|
|
|
|||
Loading…
Reference in New Issue