diff --git a/frontend/src/components/ui-new/dialogs/KeyboardShortcutsDialog.tsx b/frontend/src/components/ui-new/dialogs/KeyboardShortcutsDialog.tsx index 790b769138..5fbe7ea1ce 100644 --- a/frontend/src/components/ui-new/dialogs/KeyboardShortcutsDialog.tsx +++ b/frontend/src/components/ui-new/dialogs/KeyboardShortcutsDialog.tsx @@ -29,6 +29,7 @@ interface ShortcutGroup { function useShortcutGroups(): ShortcutGroup[] { const { config } = useUserSystem(); + const { t } = useTranslation('common'); const sendShortcut = config?.send_message_shortcut ?? 'ModifierEnter'; return useMemo(() => { @@ -37,36 +38,43 @@ function useShortcutGroups(): ShortcutGroup[] { // Quick Actions - single key shortcuts const quickActions: ShortcutGroup = { - name: 'Quick Actions', + name: t('shortcuts.groups.quickActions'), shortcuts: [ - { keys: '?', description: 'Show this help' }, - { keys: 'Esc', description: 'Close/cancel' }, - { keys: 'C', description: 'Create new task' }, - { keys: 'D', description: 'Delete selected' }, - { keys: '/', description: 'Focus search' }, + { keys: '?', description: t('shortcuts.actions.showHelp') }, + { keys: 'Esc', description: t('shortcuts.actions.closeCancel') }, + { keys: 'C', description: t('shortcuts.actions.createNewTask') }, + { keys: 'D', description: t('shortcuts.actions.deleteSelected') }, + { keys: '/', description: t('shortcuts.actions.focusSearch') }, ], }; // Navigation - Vim-style const navigation: ShortcutGroup = { - name: 'Navigation', + name: t('shortcuts.groups.navigation'), shortcuts: [ - { keys: 'J', description: 'Move down' }, - { keys: 'K', description: 'Move up' }, - { keys: 'H', description: 'Move left' }, - { keys: 'L', description: 'Move right' }, + { keys: 'J', description: t('shortcuts.actions.moveDown') }, + { keys: 'K', description: t('shortcuts.actions.moveUp') }, + { keys: 'H', description: t('shortcuts.actions.moveLeft') }, + { keys: 'L', description: t('shortcuts.actions.moveRight') }, ], }; const modifiers: ShortcutGroup = { - name: 'Modifiers', + name: t('shortcuts.groups.modifiers'), shortcuts: [ - { keys: [mod, 'K'], description: 'Open command bar' }, + { + keys: [mod, 'K'], + description: t('shortcuts.actions.openCommandBar'), + }, sendShortcut === 'Enter' - ? { keys: enterKey, description: 'Send message', useHintKey: true } + ? { + keys: enterKey, + description: t('shortcuts.actions.sendMessage'), + useHintKey: true, + } : { keys: [mod, enterKey], - description: 'Send message', + description: t('shortcuts.actions.sendMessage'), useHintKey: true, }, ], @@ -84,7 +92,10 @@ function useShortcutGroups(): ShortcutGroup[] { sequentialByFirstKey.get(firstKey)!.push({ keys: formatSequentialKeys(binding.keys), - description: binding.description, + description: t( + `shortcuts.actions.${binding.actionId}`, + binding.description + ), hasScope: hasWorkspaceScope, }); } @@ -92,25 +103,41 @@ function useShortcutGroups(): ShortcutGroup[] { // Create named groups for sequential shortcuts const sequentialGroups: ShortcutGroup[] = [ { - name: 'Go To (G ...)', + name: t('shortcuts.groups.goTo'), shortcuts: sequentialByFirstKey.get('g') || [], }, { - name: 'Workspace (W ...)', + name: t('shortcuts.groups.workspace'), shortcuts: sequentialByFirstKey.get('w') || [], }, - { name: 'View (V ...)', shortcuts: sequentialByFirstKey.get('v') || [] }, - { name: 'Git (X ...)', shortcuts: sequentialByFirstKey.get('x') || [] }, - { name: 'Yank (Y ...)', shortcuts: sequentialByFirstKey.get('y') || [] }, { - name: 'Toggle (T ...)', + name: t('shortcuts.groups.view'), + shortcuts: sequentialByFirstKey.get('v') || [], + }, + { + name: t('shortcuts.groups.issues'), + shortcuts: sequentialByFirstKey.get('i') || [], + }, + { + name: t('shortcuts.groups.git'), + shortcuts: sequentialByFirstKey.get('x') || [], + }, + { + name: t('shortcuts.groups.yank'), + shortcuts: sequentialByFirstKey.get('y') || [], + }, + { + name: t('shortcuts.groups.toggle'), shortcuts: sequentialByFirstKey.get('t') || [], }, - { name: 'Run (R ...)', shortcuts: sequentialByFirstKey.get('r') || [] }, + { + name: t('shortcuts.groups.run'), + shortcuts: sequentialByFirstKey.get('r') || [], + }, ].filter((g) => g.shortcuts.length > 0); return [quickActions, navigation, modifiers, ...sequentialGroups]; - }, [sendShortcut]); + }, [sendShortcut, t]); } function ShortcutRow({ item }: { item: ShortcutItem }) { diff --git a/frontend/src/components/ui-new/scope/NewDesignScope.tsx b/frontend/src/components/ui-new/scope/NewDesignScope.tsx index 31ec7f1cde..ca016f7619 100644 --- a/frontend/src/components/ui-new/scope/NewDesignScope.tsx +++ b/frontend/src/components/ui-new/scope/NewDesignScope.tsx @@ -10,6 +10,7 @@ import { UserProvider } from '@/contexts/remote/UserContext'; import { SequenceTrackerProvider } from '@/keyboard/SequenceTracker'; import { SequenceIndicator } from '@/keyboard/SequenceIndicator'; import { useWorkspaceShortcuts } from '@/keyboard/useWorkspaceShortcuts'; +import { useIssueShortcuts } from '@/keyboard/useIssueShortcuts'; import { ExecutionProcessesProvider } from '@/contexts/ExecutionProcessesContext'; import { LogsPanelProvider } from '@/contexts/LogsPanelContext'; import NiceModal from '@ebay/nice-modal-react'; @@ -46,6 +47,7 @@ function KeyboardShortcutsHandler() { { scope: Scope.GLOBAL } ); useWorkspaceShortcuts(); + useIssueShortcuts(); return null; } diff --git a/frontend/src/components/ui-new/views/KanbanIssuePanel.tsx b/frontend/src/components/ui-new/views/KanbanIssuePanel.tsx index 58b64b7e15..28c869fc5a 100644 --- a/frontend/src/components/ui-new/views/KanbanIssuePanel.tsx +++ b/frontend/src/components/ui-new/views/KanbanIssuePanel.tsx @@ -160,7 +160,18 @@ export function KanbanIssuePanel({ const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Escape') { - onClose(); + const target = e.target as HTMLElement; + const isEditable = + target.tagName === 'INPUT' || + target.tagName === 'TEXTAREA' || + target.isContentEditable; + if (isEditable) { + target.blur(); + (e.currentTarget as HTMLElement).focus(); + e.stopPropagation(); + } else { + onClose(); + } } }; @@ -173,8 +184,9 @@ export function KanbanIssuePanel({ return (
{/* Header */}
diff --git a/frontend/src/i18n/locales/en/common.json b/frontend/src/i18n/locales/en/common.json index dd4bdf4df8..8079b8ffa6 100644 --- a/frontend/src/i18n/locales/en/common.json +++ b/frontend/src/i18n/locales/en/common.json @@ -360,7 +360,66 @@ "title": "Keyboard Shortcuts", "inWorkspace": "(in workspace)", "sequentialHint": "Sequential shortcuts: Press the first key, then the second within 500ms.", - "configurableHint": "Configurable in Settings → General → Message Input" + "configurableHint": "Configurable in Settings → General → Message Input", + "groups": { + "quickActions": "Quick Actions", + "navigation": "Navigation", + "modifiers": "Modifiers", + "goTo": "Go To (G ...)", + "workspace": "Workspace (W ...)", + "view": "View (V ...)", + "issues": "Issues (I ...)", + "git": "Git (X ...)", + "yank": "Yank (Y ...)", + "toggle": "Toggle (T ...)", + "run": "Run (R ...)" + }, + "actions": { + "showHelp": "Show this help", + "closeCancel": "Close/cancel", + "createNewTask": "Create new task", + "deleteSelected": "Delete selected", + "focusSearch": "Focus search", + "moveDown": "Move down", + "moveUp": "Move up", + "moveLeft": "Move left", + "moveRight": "Move right", + "openCommandBar": "Open command bar", + "sendMessage": "Send message", + "settings": "Go to Settings", + "new-workspace": "Go to New Workspace", + "duplicate-workspace": "Duplicate workspace", + "rename-workspace": "Rename workspace", + "pin-workspace": "Pin/Unpin workspace", + "archive-workspace": "Archive workspace", + "delete-workspace": "Delete workspace", + "toggle-changes-mode": "Toggle Changes panel", + "toggle-logs-mode": "Toggle Logs panel", + "toggle-preview-mode": "Toggle Preview panel", + "toggle-left-sidebar": "Toggle Left Sidebar", + "toggle-left-main-panel": "Toggle Chat panel", + "create-issue": "Create Issue", + "change-issue-status": "Change Status", + "change-issue-priority": "Change Priority", + "change-assignees": "Change Assignees", + "make-sub-issue-of": "Make Sub-issue of", + "add-sub-issue": "Add Sub-issue", + "remove-parent-issue": "Remove Parent", + "link-workspace": "Link Workspace", + "duplicate-issue": "Duplicate Issue", + "delete-issue": "Delete Issue", + "git-create-pr": "Create Pull Request", + "git-merge": "Merge branch", + "git-rebase": "Rebase branch", + "git-push": "Push changes", + "copy-path": "Copy path", + "copy-raw-logs": "Copy raw logs", + "toggle-dev-server": "Toggle dev server", + "toggle-wrap-lines": "Toggle line wrapping", + "run-setup-script": "Run setup script", + "run-cleanup-script": "Run cleanup script", + "run-archive-script": "Run archive script" + } }, "typeahead": { "tags": "Tags", diff --git a/frontend/src/i18n/locales/es/common.json b/frontend/src/i18n/locales/es/common.json index 153b00ed9c..852ac8dbf2 100644 --- a/frontend/src/i18n/locales/es/common.json +++ b/frontend/src/i18n/locales/es/common.json @@ -350,7 +350,66 @@ "title": "Atajos de Teclado", "inWorkspace": "(en espacio de trabajo)", "sequentialHint": "Atajos secuenciales: Presiona la primera tecla, luego la segunda en 500ms.", - "configurableHint": "Configurable en Ajustes → General → Entrada de Mensaje" + "configurableHint": "Configurable en Ajustes → General → Entrada de Mensaje", + "groups": { + "quickActions": "Acciones Rápidas", + "navigation": "Navegación", + "modifiers": "Modificadores", + "goTo": "Ir a (G ...)", + "workspace": "Espacio de trabajo (W ...)", + "view": "Vista (V ...)", + "issues": "Issues (I ...)", + "git": "Git (X ...)", + "yank": "Copiar (Y ...)", + "toggle": "Alternar (T ...)", + "run": "Ejecutar (R ...)" + }, + "actions": { + "showHelp": "Mostrar esta ayuda", + "closeCancel": "Cerrar/cancelar", + "createNewTask": "Crear nueva tarea", + "deleteSelected": "Eliminar seleccionado", + "focusSearch": "Enfocar búsqueda", + "moveDown": "Mover abajo", + "moveUp": "Mover arriba", + "moveLeft": "Mover a la izquierda", + "moveRight": "Mover a la derecha", + "openCommandBar": "Abrir barra de comandos", + "sendMessage": "Enviar mensaje", + "settings": "Ir a Ajustes", + "new-workspace": "Ir a Nuevo espacio de trabajo", + "duplicate-workspace": "Duplicar espacio de trabajo", + "rename-workspace": "Renombrar espacio de trabajo", + "pin-workspace": "Fijar/Desfijar espacio de trabajo", + "archive-workspace": "Archivar espacio de trabajo", + "delete-workspace": "Eliminar espacio de trabajo", + "toggle-changes-mode": "Alternar panel de cambios", + "toggle-logs-mode": "Alternar panel de registros", + "toggle-preview-mode": "Alternar panel de vista previa", + "toggle-left-sidebar": "Alternar barra lateral izquierda", + "toggle-left-main-panel": "Alternar panel de chat", + "create-issue": "Crear issue", + "change-issue-status": "Cambiar estado", + "change-issue-priority": "Cambiar prioridad", + "change-assignees": "Cambiar asignados", + "make-sub-issue-of": "Hacer sub-issue de", + "add-sub-issue": "Agregar sub-issue", + "remove-parent-issue": "Quitar padre", + "link-workspace": "Vincular espacio de trabajo", + "duplicate-issue": "Duplicar issue", + "delete-issue": "Eliminar issue", + "git-create-pr": "Crear Pull Request", + "git-merge": "Fusionar rama", + "git-rebase": "Rebase de rama", + "git-push": "Enviar cambios", + "copy-path": "Copiar ruta", + "copy-raw-logs": "Copiar registros sin formato", + "toggle-dev-server": "Alternar servidor de desarrollo", + "toggle-wrap-lines": "Alternar ajuste de línea", + "run-setup-script": "Ejecutar script de configuración", + "run-cleanup-script": "Ejecutar script de limpieza", + "run-archive-script": "Ejecutar script de archivado" + } }, "typeahead": { "tags": "Etiquetas", diff --git a/frontend/src/i18n/locales/fr/common.json b/frontend/src/i18n/locales/fr/common.json index 6cab196727..39891a843d 100644 --- a/frontend/src/i18n/locales/fr/common.json +++ b/frontend/src/i18n/locales/fr/common.json @@ -350,7 +350,66 @@ "title": "Raccourcis Clavier", "inWorkspace": "(dans l'espace de travail)", "sequentialHint": "Raccourcis séquentiels : Appuyez sur la première touche, puis sur la seconde en 500ms.", - "configurableHint": "Configurable dans Paramètres → Général → Saisie de Message" + "configurableHint": "Configurable dans Paramètres → Général → Saisie de Message", + "groups": { + "quickActions": "Actions Rapides", + "navigation": "Navigation", + "modifiers": "Modificateurs", + "goTo": "Aller à (G ...)", + "workspace": "Espace de travail (W ...)", + "view": "Vue (V ...)", + "issues": "Tickets (I ...)", + "git": "Git (X ...)", + "yank": "Copier (Y ...)", + "toggle": "Basculer (T ...)", + "run": "Exécuter (R ...)" + }, + "actions": { + "showHelp": "Afficher cette aide", + "closeCancel": "Fermer/annuler", + "createNewTask": "Créer une nouvelle tâche", + "deleteSelected": "Supprimer la sélection", + "focusSearch": "Aller à la recherche", + "moveDown": "Déplacer vers le bas", + "moveUp": "Déplacer vers le haut", + "moveLeft": "Déplacer vers la gauche", + "moveRight": "Déplacer vers la droite", + "openCommandBar": "Ouvrir la barre de commandes", + "sendMessage": "Envoyer le message", + "settings": "Aller aux Paramètres", + "new-workspace": "Aller au Nouvel espace de travail", + "duplicate-workspace": "Dupliquer l'espace de travail", + "rename-workspace": "Renommer l'espace de travail", + "pin-workspace": "Épingler/Désépingler l'espace de travail", + "archive-workspace": "Archiver l'espace de travail", + "delete-workspace": "Supprimer l'espace de travail", + "toggle-changes-mode": "Basculer le panneau Modifications", + "toggle-logs-mode": "Basculer le panneau Logs", + "toggle-preview-mode": "Basculer le panneau Aperçu", + "toggle-left-sidebar": "Basculer la barre latérale gauche", + "toggle-left-main-panel": "Basculer le panneau Chat", + "create-issue": "Créer un ticket", + "change-issue-status": "Changer le statut", + "change-issue-priority": "Changer la priorité", + "change-assignees": "Changer les assignés", + "make-sub-issue-of": "Transformer en sous-ticket de", + "add-sub-issue": "Ajouter un sous-ticket", + "remove-parent-issue": "Retirer le parent", + "link-workspace": "Lier un espace de travail", + "duplicate-issue": "Dupliquer le ticket", + "delete-issue": "Supprimer le ticket", + "git-create-pr": "Créer une Pull Request", + "git-merge": "Fusionner la branche", + "git-rebase": "Rebaser la branche", + "git-push": "Pousser les modifications", + "copy-path": "Copier le chemin", + "copy-raw-logs": "Copier les logs bruts", + "toggle-dev-server": "Basculer le serveur de développement", + "toggle-wrap-lines": "Basculer le retour à la ligne", + "run-setup-script": "Exécuter le script de configuration", + "run-cleanup-script": "Exécuter le script de nettoyage", + "run-archive-script": "Exécuter le script d'archivage" + } }, "typeahead": { "tags": "Tags", diff --git a/frontend/src/i18n/locales/ja/common.json b/frontend/src/i18n/locales/ja/common.json index 46bbd485f8..118909e162 100644 --- a/frontend/src/i18n/locales/ja/common.json +++ b/frontend/src/i18n/locales/ja/common.json @@ -350,7 +350,66 @@ "title": "キーボードショートカット", "inWorkspace": "(ワークスペース内)", "sequentialHint": "シーケンシャルショートカット:最初のキーを押し、500ms以内に2番目のキーを押してください。", - "configurableHint": "設定 → 一般 → メッセージ入力で設定可能" + "configurableHint": "設定 → 一般 → メッセージ入力で設定可能", + "groups": { + "quickActions": "クイックアクション", + "navigation": "ナビゲーション", + "modifiers": "修飾キー", + "goTo": "移動 (G ...)", + "workspace": "ワークスペース (W ...)", + "view": "表示 (V ...)", + "issues": "課題 (I ...)", + "git": "Git (X ...)", + "yank": "コピー (Y ...)", + "toggle": "切り替え (T ...)", + "run": "実行 (R ...)" + }, + "actions": { + "showHelp": "このヘルプを表示", + "closeCancel": "閉じる/キャンセル", + "createNewTask": "新しいタスクを作成", + "deleteSelected": "選択を削除", + "focusSearch": "検索にフォーカス", + "moveDown": "下に移動", + "moveUp": "上に移動", + "moveLeft": "左に移動", + "moveRight": "右に移動", + "openCommandBar": "コマンドバーを開く", + "sendMessage": "メッセージを送信", + "settings": "設定に移動", + "new-workspace": "新しいワークスペースに移動", + "duplicate-workspace": "ワークスペースを複製", + "rename-workspace": "ワークスペースの名前を変更", + "pin-workspace": "ワークスペースをピン留め/解除", + "archive-workspace": "ワークスペースをアーカイブ", + "delete-workspace": "ワークスペースを削除", + "toggle-changes-mode": "変更パネルの切り替え", + "toggle-logs-mode": "ログパネルの切り替え", + "toggle-preview-mode": "プレビューパネルの切り替え", + "toggle-left-sidebar": "左サイドバーの切り替え", + "toggle-left-main-panel": "チャットパネルの切り替え", + "create-issue": "課題を作成", + "change-issue-status": "ステータスを変更", + "change-issue-priority": "優先度を変更", + "change-assignees": "担当者を変更", + "make-sub-issue-of": "サブ課題にする", + "add-sub-issue": "サブ課題を追加", + "remove-parent-issue": "親課題を解除", + "link-workspace": "ワークスペースをリンク", + "duplicate-issue": "課題を複製", + "delete-issue": "課題を削除", + "git-create-pr": "Pull Requestを作成", + "git-merge": "ブランチをマージ", + "git-rebase": "ブランチをリベース", + "git-push": "変更をプッシュ", + "copy-path": "パスをコピー", + "copy-raw-logs": "生ログをコピー", + "toggle-dev-server": "開発サーバーの切り替え", + "toggle-wrap-lines": "行折り返しの切り替え", + "run-setup-script": "セットアップスクリプトを実行", + "run-cleanup-script": "クリーンアップスクリプトを実行", + "run-archive-script": "アーカイブスクリプトを実行" + } }, "typeahead": { "tags": "タグ", diff --git a/frontend/src/i18n/locales/ko/common.json b/frontend/src/i18n/locales/ko/common.json index bb4b52c698..59c6459068 100644 --- a/frontend/src/i18n/locales/ko/common.json +++ b/frontend/src/i18n/locales/ko/common.json @@ -350,7 +350,66 @@ "title": "키보드 단축키", "inWorkspace": "(워크스페이스 내)", "sequentialHint": "순차 단축키: 첫 번째 키를 누른 후 500ms 이내에 두 번째 키를 누르세요.", - "configurableHint": "설정 → 일반 → 메시지 입력에서 설정 가능" + "configurableHint": "설정 → 일반 → 메시지 입력에서 설정 가능", + "groups": { + "quickActions": "빠른 작업", + "navigation": "탐색", + "modifiers": "수정 키", + "goTo": "이동 (G ...)", + "workspace": "워크스페이스 (W ...)", + "view": "보기 (V ...)", + "issues": "이슈 (I ...)", + "git": "Git (X ...)", + "yank": "복사 (Y ...)", + "toggle": "전환 (T ...)", + "run": "실행 (R ...)" + }, + "actions": { + "showHelp": "도움말 표시", + "closeCancel": "닫기/취소", + "createNewTask": "새 작업 만들기", + "deleteSelected": "선택 항목 삭제", + "focusSearch": "검색에 포커스", + "moveDown": "아래로 이동", + "moveUp": "위로 이동", + "moveLeft": "왼쪽으로 이동", + "moveRight": "오른쪽으로 이동", + "openCommandBar": "명령 바 열기", + "sendMessage": "메시지 보내기", + "settings": "설정으로 이동", + "new-workspace": "새 워크스페이스로 이동", + "duplicate-workspace": "워크스페이스 복제", + "rename-workspace": "워크스페이스 이름 변경", + "pin-workspace": "워크스페이스 고정/해제", + "archive-workspace": "워크스페이스 보관", + "delete-workspace": "워크스페이스 삭제", + "toggle-changes-mode": "변경사항 패널 전환", + "toggle-logs-mode": "로그 패널 전환", + "toggle-preview-mode": "미리보기 패널 전환", + "toggle-left-sidebar": "왼쪽 사이드바 전환", + "toggle-left-main-panel": "채팅 패널 전환", + "create-issue": "이슈 만들기", + "change-issue-status": "상태 변경", + "change-issue-priority": "우선순위 변경", + "change-assignees": "담당자 변경", + "make-sub-issue-of": "하위 이슈로 설정", + "add-sub-issue": "하위 이슈 추가", + "remove-parent-issue": "상위 이슈 해제", + "link-workspace": "워크스페이스 연결", + "duplicate-issue": "이슈 복제", + "delete-issue": "이슈 삭제", + "git-create-pr": "Pull Request 만들기", + "git-merge": "브랜치 병합", + "git-rebase": "브랜치 리베이스", + "git-push": "변경사항 푸시", + "copy-path": "경로 복사", + "copy-raw-logs": "원본 로그 복사", + "toggle-dev-server": "개발 서버 전환", + "toggle-wrap-lines": "줄 바꿈 전환", + "run-setup-script": "설정 스크립트 실행", + "run-cleanup-script": "정리 스크립트 실행", + "run-archive-script": "보관 스크립트 실행" + } }, "typeahead": { "tags": "태그", diff --git a/frontend/src/i18n/locales/zh-Hans/common.json b/frontend/src/i18n/locales/zh-Hans/common.json index 5601555dc9..1ef20df38a 100644 --- a/frontend/src/i18n/locales/zh-Hans/common.json +++ b/frontend/src/i18n/locales/zh-Hans/common.json @@ -350,7 +350,66 @@ "title": "键盘快捷键", "inWorkspace": "(在工作区中)", "sequentialHint": "组合快捷键:先按第一个键,然后在500毫秒内按第二个键。", - "configurableHint": "可在 设置 → 常规 → 消息输入 中配置" + "configurableHint": "可在 设置 → 常规 → 消息输入 中配置", + "groups": { + "quickActions": "快捷操作", + "navigation": "导航", + "modifiers": "修饰键", + "goTo": "前往 (G ...)", + "workspace": "工作区 (W ...)", + "view": "视图 (V ...)", + "issues": "问题 (I ...)", + "git": "Git (X ...)", + "yank": "复制 (Y ...)", + "toggle": "切换 (T ...)", + "run": "运行 (R ...)" + }, + "actions": { + "showHelp": "显示帮助", + "closeCancel": "关闭/取消", + "createNewTask": "创建新任务", + "deleteSelected": "删除所选", + "focusSearch": "聚焦搜索", + "moveDown": "向下移动", + "moveUp": "向上移动", + "moveLeft": "向左移动", + "moveRight": "向右移动", + "openCommandBar": "打开命令栏", + "sendMessage": "发送消息", + "settings": "前往设置", + "new-workspace": "前往新工作区", + "duplicate-workspace": "复制工作区", + "rename-workspace": "重命名工作区", + "pin-workspace": "置顶/取消置顶工作区", + "archive-workspace": "归档工作区", + "delete-workspace": "删除工作区", + "toggle-changes-mode": "切换更改面板", + "toggle-logs-mode": "切换日志面板", + "toggle-preview-mode": "切换预览面板", + "toggle-left-sidebar": "切换左侧边栏", + "toggle-left-main-panel": "切换聊天面板", + "create-issue": "创建问题", + "change-issue-status": "更改状态", + "change-issue-priority": "更改优先级", + "change-assignees": "更改负责人", + "make-sub-issue-of": "设为子问题", + "add-sub-issue": "添加子问题", + "remove-parent-issue": "移除父级", + "link-workspace": "关联工作区", + "duplicate-issue": "复制问题", + "delete-issue": "删除问题", + "git-create-pr": "创建 Pull Request", + "git-merge": "合并分支", + "git-rebase": "变基分支", + "git-push": "推送更改", + "copy-path": "复制路径", + "copy-raw-logs": "复制原始日志", + "toggle-dev-server": "切换开发服务器", + "toggle-wrap-lines": "切换自动换行", + "run-setup-script": "运行设置脚本", + "run-cleanup-script": "运行清理脚本", + "run-archive-script": "运行归档脚本" + } }, "typeahead": { "tags": "标签", diff --git a/frontend/src/i18n/locales/zh-Hant/common.json b/frontend/src/i18n/locales/zh-Hant/common.json index 780685d0a7..37c00ec282 100644 --- a/frontend/src/i18n/locales/zh-Hant/common.json +++ b/frontend/src/i18n/locales/zh-Hant/common.json @@ -350,7 +350,66 @@ "title": "鍵盤快捷鍵", "inWorkspace": "(在工作區中)", "sequentialHint": "組合快捷鍵:先按第一個鍵,然後在500毫秒內按第二個鍵。", - "configurableHint": "可在 設定 → 一般 → 訊息輸入 中設定" + "configurableHint": "可在 設定 → 一般 → 訊息輸入 中設定", + "groups": { + "quickActions": "快捷操作", + "navigation": "導覽", + "modifiers": "修飾鍵", + "goTo": "前往 (G ...)", + "workspace": "工作區 (W ...)", + "view": "檢視 (V ...)", + "issues": "問題 (I ...)", + "git": "Git (X ...)", + "yank": "複製 (Y ...)", + "toggle": "切換 (T ...)", + "run": "執行 (R ...)" + }, + "actions": { + "showHelp": "顯示說明", + "closeCancel": "關閉/取消", + "createNewTask": "建立新任務", + "deleteSelected": "刪除所選", + "focusSearch": "聚焦搜尋", + "moveDown": "向下移動", + "moveUp": "向上移動", + "moveLeft": "向左移動", + "moveRight": "向右移動", + "openCommandBar": "開啟命令列", + "sendMessage": "傳送訊息", + "settings": "前往設定", + "new-workspace": "前往新工作區", + "duplicate-workspace": "複製工作區", + "rename-workspace": "重新命名工作區", + "pin-workspace": "釘選/取消釘選工作區", + "archive-workspace": "封存工作區", + "delete-workspace": "刪除工作區", + "toggle-changes-mode": "切換變更面板", + "toggle-logs-mode": "切換日誌面板", + "toggle-preview-mode": "切換預覽面板", + "toggle-left-sidebar": "切換左側邊欄", + "toggle-left-main-panel": "切換聊天面板", + "create-issue": "建立問題", + "change-issue-status": "變更狀態", + "change-issue-priority": "變更優先順序", + "change-assignees": "變更負責人", + "make-sub-issue-of": "設為子問題", + "add-sub-issue": "新增子問題", + "remove-parent-issue": "移除父級", + "link-workspace": "關聯工作區", + "duplicate-issue": "複製問題", + "delete-issue": "刪除問題", + "git-create-pr": "建立 Pull Request", + "git-merge": "合併分支", + "git-rebase": "變基分支", + "git-push": "推送變更", + "copy-path": "複製路徑", + "copy-raw-logs": "複製原始日誌", + "toggle-dev-server": "切換開發伺服器", + "toggle-wrap-lines": "切換自動換行", + "run-setup-script": "執行設定腳本", + "run-cleanup-script": "執行清理腳本", + "run-archive-script": "執行封存腳本" + } }, "typeahead": { "tags": "標籤", diff --git a/frontend/src/keyboard/index.ts b/frontend/src/keyboard/index.ts index 31101a0126..a5a73c1549 100644 --- a/frontend/src/keyboard/index.ts +++ b/frontend/src/keyboard/index.ts @@ -3,3 +3,4 @@ export * from './registry'; export * from './SequenceTracker'; export * from './SequenceIndicator'; export * from './useWorkspaceShortcuts'; +export * from './useIssueShortcuts'; diff --git a/frontend/src/keyboard/registry.ts b/frontend/src/keyboard/registry.ts index 1628ddd3fa..6966fdca8d 100644 --- a/frontend/src/keyboard/registry.ts +++ b/frontend/src/keyboard/registry.ts @@ -65,6 +65,7 @@ export const SEQUENCE_FIRST_KEYS = new Set([ 'y', // Yank/Copy 't', // Toggle 'r', // Run + 'i', // Issue ]); /** @@ -256,6 +257,78 @@ export const sequentialBindings: SequentialBinding[] = [ group: 'Run', actionId: 'run-archive-script', }, + + // Issue (I) + { + id: 'seq-issue-create', + keys: ['i', 'c'], + description: 'Create Issue', + group: 'Issue', + actionId: 'create-issue', + }, + { + id: 'seq-issue-status', + keys: ['i', 's'], + description: 'Change Status', + group: 'Issue', + actionId: 'change-issue-status', + }, + { + id: 'seq-issue-priority', + keys: ['i', 'p'], + description: 'Change Priority', + group: 'Issue', + actionId: 'change-issue-priority', + }, + { + id: 'seq-issue-assignees', + keys: ['i', 'a'], + description: 'Change Assignees', + group: 'Issue', + actionId: 'change-assignees', + }, + { + id: 'seq-issue-make-sub-issue', + keys: ['i', 'm'], + description: 'Make Sub-issue of', + group: 'Issue', + actionId: 'make-sub-issue-of', + }, + { + id: 'seq-issue-add-sub-issue', + keys: ['i', 'b'], + description: 'Add Sub-issue', + group: 'Issue', + actionId: 'add-sub-issue', + }, + { + id: 'seq-issue-remove-parent', + keys: ['i', 'u'], + description: 'Remove Parent', + group: 'Issue', + actionId: 'remove-parent-issue', + }, + { + id: 'seq-issue-link-workspace', + keys: ['i', 'w'], + description: 'Link Workspace', + group: 'Issue', + actionId: 'link-workspace', + }, + { + id: 'seq-issue-duplicate', + keys: ['i', 'd'], + description: 'Duplicate Issue', + group: 'Issue', + actionId: 'duplicate-issue', + }, + { + id: 'seq-issue-delete', + keys: ['i', 'x'], + description: 'Delete Issue', + group: 'Issue', + actionId: 'delete-issue', + }, ]; export const keyBindings: KeyBinding[] = [ diff --git a/frontend/src/keyboard/useIssueShortcuts.ts b/frontend/src/keyboard/useIssueShortcuts.ts new file mode 100644 index 0000000000..df9b758a6f --- /dev/null +++ b/frontend/src/keyboard/useIssueShortcuts.ts @@ -0,0 +1,141 @@ +import { useCallback, useRef, useEffect, useMemo } from 'react'; +import { useParams, useLocation } from 'react-router-dom'; +import { useHotkeys } from 'react-hotkeys-hook'; +import { useActions } from '@/contexts/ActionsContext'; +import { + Actions, + type ActionDefinition, + ActionTargetType, +} from '@/components/ui-new/actions'; +import { Scope } from './registry'; + +const SEQUENCE_TIMEOUT_MS = 1500; + +const OPTIONS = { + scopes: [Scope.KANBAN], + sequenceTimeout: SEQUENCE_TIMEOUT_MS, +} as const; + +export function useIssueShortcuts() { + const { executeAction } = useActions(); + const { projectId, issueId } = useParams<{ + projectId?: string; + issueId?: string; + }>(); + const location = useLocation(); + + const isKanban = location.pathname.startsWith('/projects'); + // Detect create mode from the URL path (e.g. /projects/:id/issues/new) + // NOT from ?mode=create searchParam which is a legacy format + const isCreatingIssue = location.pathname.endsWith('/issues/new'); + + const executeActionRef = useRef(executeAction); + const projectIdRef = useRef(projectId); + const issueIdRef = useRef(issueId); + const isKanbanRef = useRef(isKanban); + const isCreatingIssueRef = useRef(isCreatingIssue); + + useEffect(() => { + executeActionRef.current = executeAction; + projectIdRef.current = projectId; + issueIdRef.current = issueId; + isKanbanRef.current = isKanban; + isCreatingIssueRef.current = isCreatingIssue; + }); + + const issueIds = useMemo(() => (issueId ? [issueId] : []), [issueId]); + const issueIdsRef = useRef(issueIds); + useEffect(() => { + issueIdsRef.current = issueIds; + }); + + const executeIssueAction = useCallback( + (action: ActionDefinition, e?: KeyboardEvent) => { + if (!isKanbanRef.current) return; + // react-hotkeys-hook does not call preventDefault for sequence hotkeys, + // so we must do it manually to stop the second keystroke from being typed + // into any focused input (e.g. the title field after i>c opens create mode). + e?.preventDefault(); + + const currentProjectId = projectIdRef.current; + const currentIssueIds = issueIdsRef.current; + + if (action.requiresTarget === ActionTargetType.ISSUE) { + if (!currentProjectId || currentIssueIds.length === 0) return; + executeActionRef.current( + action, + undefined, + currentProjectId, + currentIssueIds + ); + } else if (action.requiresTarget === ActionTargetType.NONE) { + executeActionRef.current(action); + } + }, + [] + ); + + const enabled = isKanban; + + useHotkeys('i>c', (e) => executeIssueAction(Actions.CreateIssue, e), { + ...OPTIONS, + enabled, + }); + useHotkeys( + 'i>s', + (e) => { + if (isCreatingIssueRef.current) { + executeIssueAction(Actions.ChangeNewIssueStatus, e); + } else { + executeIssueAction(Actions.ChangeIssueStatus, e); + } + }, + { ...OPTIONS, enabled } + ); + useHotkeys( + 'i>p', + (e) => { + if (isCreatingIssueRef.current) { + executeIssueAction(Actions.ChangeNewIssuePriority, e); + } else { + executeIssueAction(Actions.ChangePriority, e); + } + }, + { ...OPTIONS, enabled } + ); + useHotkeys( + 'i>a', + (e) => { + if (isCreatingIssueRef.current) { + executeIssueAction(Actions.ChangeNewIssueAssignees, e); + } else { + executeIssueAction(Actions.ChangeAssignees, e); + } + }, + { ...OPTIONS, enabled } + ); + useHotkeys('i>m', (e) => executeIssueAction(Actions.MakeSubIssueOf, e), { + ...OPTIONS, + enabled, + }); + useHotkeys('i>b', (e) => executeIssueAction(Actions.AddSubIssue, e), { + ...OPTIONS, + enabled, + }); + useHotkeys('i>u', (e) => executeIssueAction(Actions.RemoveParentIssue, e), { + ...OPTIONS, + enabled, + }); + useHotkeys('i>w', (e) => executeIssueAction(Actions.LinkWorkspace, e), { + ...OPTIONS, + enabled, + }); + useHotkeys('i>d', (e) => executeIssueAction(Actions.DuplicateIssue, e), { + ...OPTIONS, + enabled, + }); + useHotkeys('i>x', (e) => executeIssueAction(Actions.DeleteIssue, e), { + ...OPTIONS, + enabled, + }); +}