mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-23 01:32:40 +00:00
fix(app): require a number when recording index shortcuts (#7365)
Tab and workspace navigation need a final 1–9 index. Prevent saving an incomplete recorded sequence and explain the requirement while preserving arbitrary chord prefixes. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -2333,6 +2333,7 @@ export const ar: TranslationResources = {
|
||||
searchPlaceholder: "البحث في الاختصارات",
|
||||
unavailableOnMobile: "اختصارات لوحة المفاتيح متاحة فقط على سطح المكتب",
|
||||
capturePrompt: "اضغط على الاختصار...",
|
||||
captureIndexPrompt: "اختم برقم من 1 إلى 9.",
|
||||
unassigned: "غير معين",
|
||||
actions: {
|
||||
menu: "إجراءات {{name}}",
|
||||
|
||||
@@ -2456,6 +2456,7 @@ export const en = {
|
||||
searchPlaceholder: "Search shortcuts",
|
||||
unavailableOnMobile: "Keyboard shortcuts are only available on desktop",
|
||||
capturePrompt: "Press shortcut...",
|
||||
captureIndexPrompt: "End with a number from 1 to 9.",
|
||||
unassigned: "Not set",
|
||||
actions: {
|
||||
menu: "Actions for {{name}}",
|
||||
|
||||
@@ -2387,6 +2387,7 @@ export const es: TranslationResources = {
|
||||
searchPlaceholder: "Buscar atajos",
|
||||
unavailableOnMobile: "Los atajos de teclado solo están disponibles en el escritorio",
|
||||
capturePrompt: "Presione el acceso directo...",
|
||||
captureIndexPrompt: "Termina con un número del 1 al 9.",
|
||||
unassigned: "Sin asignar",
|
||||
actions: {
|
||||
menu: "Acciones para {{name}}",
|
||||
|
||||
@@ -2387,6 +2387,7 @@ export const fr: TranslationResources = {
|
||||
searchPlaceholder: "Rechercher des raccourcis",
|
||||
unavailableOnMobile: "Les raccourcis clavier ne sont disponibles que sur l’app de bureau",
|
||||
capturePrompt: "Appuyez sur le raccourci…",
|
||||
captureIndexPrompt: "Terminez par un chiffre de 1 à 9.",
|
||||
unassigned: "Non attribué",
|
||||
actions: {
|
||||
menu: "Actions pour {{name}}",
|
||||
|
||||
@@ -2354,6 +2354,7 @@ export const ja: TranslationResources = {
|
||||
searchPlaceholder: "ショートカットを検索",
|
||||
unavailableOnMobile: "キーボードショートカットはデスクトップでのみ利用できます",
|
||||
capturePrompt: "ショートカットを押してください...",
|
||||
captureIndexPrompt: "最後に1〜9の数字を入力してください。",
|
||||
unassigned: "未設定",
|
||||
actions: {
|
||||
menu: "{{name}} のアクション",
|
||||
|
||||
@@ -2343,6 +2343,7 @@ export const ko: TranslationResources = {
|
||||
searchPlaceholder: "검색 단축키",
|
||||
unavailableOnMobile: "키보드 단축키는 데스크톱에서만 사용할 수 있습니다",
|
||||
capturePrompt: "단축키를 누르세요...",
|
||||
captureIndexPrompt: "마지막에 1부터 9까지의 숫자를 입력하세요.",
|
||||
unassigned: "설정되지 않음",
|
||||
actions: {
|
||||
menu: "{{name}} 작업",
|
||||
|
||||
@@ -2368,6 +2368,7 @@ export const ptBR: TranslationResources = {
|
||||
searchPlaceholder: "Pesquisar atalhos",
|
||||
unavailableOnMobile: "Atalhos de teclado estão disponíveis apenas no desktop",
|
||||
capturePrompt: "Pressione o atalho...",
|
||||
captureIndexPrompt: "Termine com um número de 1 a 9.",
|
||||
unassigned: "Não definido",
|
||||
actions: {
|
||||
menu: "Ações para {{name}}",
|
||||
|
||||
@@ -2373,6 +2373,7 @@ export const ru: TranslationResources = {
|
||||
searchPlaceholder: "Поиск сочетаний клавиш",
|
||||
unavailableOnMobile: "Сочетания клавиш доступны только на рабочем столе.",
|
||||
capturePrompt: "Нажмите сочетание клавиш...",
|
||||
captureIndexPrompt: "Завершите цифрой от 1 до 9.",
|
||||
unassigned: "Не задано",
|
||||
actions: {
|
||||
menu: "Действия для {{name}}",
|
||||
|
||||
@@ -2305,6 +2305,7 @@ export const zhCN: TranslationResources = {
|
||||
searchPlaceholder: "搜索快捷键",
|
||||
unavailableOnMobile: "键盘快捷键仅在桌面端可用",
|
||||
capturePrompt: "按下快捷键...",
|
||||
captureIndexPrompt: "请以 1 到 9 的数字结尾。",
|
||||
unassigned: "未设置",
|
||||
actions: {
|
||||
menu: "{{name}} 的操作",
|
||||
|
||||
@@ -4,7 +4,9 @@ import { keyboardEventToComboString } from "./shortcut-string";
|
||||
import {
|
||||
buildKeyboardShortcutHelpSections,
|
||||
buildEffectiveBindings,
|
||||
DEFAULT_BINDINGS,
|
||||
normalizeCapturedShortcutCombo,
|
||||
isShortcutCaptureComplete,
|
||||
getBindingIdForAction,
|
||||
getDefaultKeysForAction,
|
||||
getWorkspaceIndexJumpModifierKey,
|
||||
@@ -1390,3 +1392,22 @@ describe("number-group shortcut capture", () => {
|
||||
expect(normalizeCapturedShortcutCombo(tabBindingId, "Ctrl+9")).toBe("Ctrl+Digit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shortcut capture completion", () => {
|
||||
const indexBindings = DEFAULT_BINDINGS.filter((binding) => binding.payload?.type === "index");
|
||||
it.each(indexBindings)("requires a final 1–9 number for $id", ({ id }) => {
|
||||
for (const combos of [[], ["Ctrl+K"], ["Ctrl+0"], ["Ctrl+Digit", "Ctrl+K"]]) {
|
||||
expect(isShortcutCaptureComplete(id, combos)).toBe(false);
|
||||
}
|
||||
for (let index = 1; index <= 9; index++) {
|
||||
const combo = normalizeCapturedShortcutCombo(id, `Ctrl+${index}`);
|
||||
expect(isShortcutCaptureComplete(id, [combo])).toBe(true);
|
||||
expect(isShortcutCaptureComplete(id, ["Ctrl+K", combo])).toBe(true);
|
||||
}
|
||||
});
|
||||
it("keeps ordinary shortcuts available without a number", () => {
|
||||
expect(isShortcutCaptureComplete("show-shortcuts", [])).toBe(false);
|
||||
expect(isShortcutCaptureComplete("show-shortcuts", ["Ctrl+K"])).toBe(true);
|
||||
expect(isShortcutCaptureComplete("show-shortcuts", ["Ctrl+0"])).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1217,6 +1217,14 @@ export function normalizeCapturedShortcutCombo(bindingId: string, combo: string)
|
||||
return combo.replace(/(^|\+)[1-9]$/, "$1Digit");
|
||||
}
|
||||
|
||||
/** Index navigation needs a final number; earlier combos may be any chord prefix. */
|
||||
export function isShortcutCaptureComplete(bindingId: string, combos: readonly string[]): boolean {
|
||||
const lastCombo = combos.at(-1);
|
||||
if (!lastCombo) return false;
|
||||
const binding = DEFAULT_BINDINGS.find((candidate) => candidate.id === bindingId);
|
||||
return binding?.payload?.type !== "index" || /(^|\+)(Digit|[1-9])$/.test(lastCombo);
|
||||
}
|
||||
|
||||
export function buildEffectiveBindings(overrides: ShortcutOverrides): ParsedShortcutBinding[] {
|
||||
return DEFAULT_BINDINGS.map(function (binding) {
|
||||
const override = overrides[binding.id];
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
buildKeyboardShortcutHelpSections,
|
||||
getBindingIdForAction,
|
||||
normalizeCapturedShortcutCombo,
|
||||
isShortcutCaptureComplete,
|
||||
getDefaultKeysForAction,
|
||||
resolveShortcutKeysForAction,
|
||||
type KeyboardShortcutHelpRow,
|
||||
@@ -269,6 +270,8 @@ function ShortcutRow({
|
||||
|
||||
const isBindable = bindingId !== null;
|
||||
const showDone = isCapturing && capturedCombos.length > 0;
|
||||
const captureComplete =
|
||||
bindingId !== null && isShortcutCaptureComplete(bindingId, capturedCombos);
|
||||
const showClear = displayChord !== null;
|
||||
// Reset restores the default, so it is only meaningful when there is a
|
||||
// default to restore. A binding that ships without one would otherwise show a
|
||||
@@ -288,11 +291,14 @@ function ShortcutRow({
|
||||
capturedCombos={capturedCombos}
|
||||
heldModifiers={heldModifiers}
|
||||
/>
|
||||
{showDone && !captureComplete && (
|
||||
<Text style={styles.capturingText}>{t("settings.shortcuts.captureIndexPrompt")}</Text>
|
||||
)}
|
||||
</View>
|
||||
{isCapturing ? (
|
||||
<>
|
||||
{showDone && (
|
||||
<Button variant="ghost" size="sm" onPress={onDone}>
|
||||
<Button variant="ghost" size="sm" disabled={!captureComplete} onPress={onDone}>
|
||||
{t("settings.shortcuts.actions.done")}
|
||||
</Button>
|
||||
)}
|
||||
@@ -357,7 +363,10 @@ export function KeyboardShortcutsSection() {
|
||||
);
|
||||
|
||||
const saveCapture = useCallback(() => {
|
||||
if (capturingBindingId === null || capturedCombos.length === 0) {
|
||||
if (
|
||||
capturingBindingId === null ||
|
||||
!isShortcutCaptureComplete(capturingBindingId, capturedCombos)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void setOverride(capturingBindingId, capturedCombos.join(" "));
|
||||
|
||||
@@ -261,3 +261,35 @@ test("rebinding Jump to tab preserves the number group after reload", async ({
|
||||
await openSettingsSection(page, "shortcuts");
|
||||
await expectTabNumberRange(page);
|
||||
});
|
||||
|
||||
async function expectNumberRequiredBeforeSaving(page: Page, label: string) {
|
||||
const row = page.getByText(label, { exact: true }).locator("..");
|
||||
await expect(row.getByRole("button", { name: "Done", exact: true })).toBeDisabled();
|
||||
await expect(row.getByText("End with a number from 1 to 9.", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
async function completeNumberShortcut(page: Page, label: string) {
|
||||
const row = page.getByText(label, { exact: true }).locator("..");
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.keyboard.press("Control+2");
|
||||
await expect(row.getByRole("button", { name: "Done", exact: true })).toBeEnabled();
|
||||
await row.getByRole("button", { name: "Done", exact: true }).click();
|
||||
await expect(row.getByText("⌃K", { exact: true })).toBeVisible();
|
||||
await expect(row.getByText("⌃1-9", { exact: true })).toBeVisible();
|
||||
}
|
||||
|
||||
for (const { label, action } of [
|
||||
{ label: "Jump to tab", action: "workspace-tab-jump-index" },
|
||||
{ label: "Jump to workspace", action: "workspace-jump-index" },
|
||||
]) {
|
||||
test(`${label} requires a final number before a recorded shortcut can be saved`, async ({
|
||||
page,
|
||||
}) => {
|
||||
await openShortcutsSettings(page);
|
||||
await captureShortcut(page, label, action, "Control+K");
|
||||
await expectNumberRequiredBeforeSaving(page, label);
|
||||
await page.keyboard.press("Control+0");
|
||||
await expectNumberRequiredBeforeSaving(page, label);
|
||||
await completeNumberShortcut(page, label);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user