mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
Add issue-detail g i inbox shortcut
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
ede3206423
commit
69ff793c6a
3 changed files with 163 additions and 1 deletions
|
|
@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
hasBlockingShortcutDialog,
|
||||
isKeyboardShortcutTextInputTarget,
|
||||
resolveGoToInboxKeyAction,
|
||||
resolveInboxQuickArchiveKeyAction,
|
||||
} from "./keyboardShortcuts";
|
||||
|
||||
|
|
@ -109,4 +110,49 @@ describe("keyboardShortcuts helpers", () => {
|
|||
hasOpenDialog: false,
|
||||
})).toBe("disarm");
|
||||
});
|
||||
|
||||
it("arms go-to-inbox on a clean g press", () => {
|
||||
const button = document.createElement("button");
|
||||
|
||||
expect(resolveGoToInboxKeyAction({
|
||||
armed: false,
|
||||
defaultPrevented: false,
|
||||
key: "g",
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
target: button,
|
||||
hasOpenDialog: false,
|
||||
})).toBe("arm");
|
||||
});
|
||||
|
||||
it("navigates to inbox on i after g", () => {
|
||||
const button = document.createElement("button");
|
||||
|
||||
expect(resolveGoToInboxKeyAction({
|
||||
armed: true,
|
||||
defaultPrevented: false,
|
||||
key: "i",
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
target: button,
|
||||
hasOpenDialog: false,
|
||||
})).toBe("navigate");
|
||||
});
|
||||
|
||||
it("disarms go-to-inbox instead of firing from an editor", () => {
|
||||
const input = document.createElement("textarea");
|
||||
|
||||
expect(resolveGoToInboxKeyAction({
|
||||
armed: true,
|
||||
defaultPrevented: false,
|
||||
key: "i",
|
||||
metaKey: false,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
target: input,
|
||||
hasOpenDialog: false,
|
||||
})).toBe("disarm");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const KEYBOARD_SHORTCUT_TEXT_INPUT_SELECTOR = [
|
|||
const MODIFIER_ONLY_KEYS = new Set(["Shift", "Meta", "Control", "Alt"]);
|
||||
|
||||
export type InboxQuickArchiveKeyAction = "ignore" | "archive" | "disarm";
|
||||
export type GoToInboxKeyAction = "ignore" | "arm" | "navigate" | "disarm";
|
||||
|
||||
export function isKeyboardShortcutTextInputTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false;
|
||||
|
|
@ -52,3 +53,35 @@ export function resolveInboxQuickArchiveKeyAction({
|
|||
if (key === "y") return "archive";
|
||||
return "disarm";
|
||||
}
|
||||
|
||||
export function resolveGoToInboxKeyAction({
|
||||
armed,
|
||||
defaultPrevented,
|
||||
key,
|
||||
metaKey,
|
||||
ctrlKey,
|
||||
altKey,
|
||||
target,
|
||||
hasOpenDialog,
|
||||
}: {
|
||||
armed: boolean;
|
||||
defaultPrevented: boolean;
|
||||
key: string;
|
||||
metaKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
altKey: boolean;
|
||||
target: EventTarget | null;
|
||||
hasOpenDialog: boolean;
|
||||
}): GoToInboxKeyAction {
|
||||
if (defaultPrevented) return armed ? "disarm" : "ignore";
|
||||
if (metaKey || ctrlKey || altKey || isModifierOnlyKey(key)) return "ignore";
|
||||
if (hasOpenDialog || isKeyboardShortcutTextInputTarget(target)) {
|
||||
return armed ? "disarm" : "ignore";
|
||||
}
|
||||
|
||||
const normalizedKey = key.toLowerCase();
|
||||
if (!armed) return normalizedKey === "g" ? "arm" : "ignore";
|
||||
if (normalizedKey === "i") return "navigate";
|
||||
if (normalizedKey === "g") return "arm";
|
||||
return "disarm";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ import {
|
|||
rememberIssueDetailLocationState,
|
||||
shouldArmIssueDetailInboxQuickArchive,
|
||||
} from "../lib/issueDetailBreadcrumb";
|
||||
import { hasBlockingShortcutDialog, resolveInboxQuickArchiveKeyAction } from "../lib/keyboardShortcuts";
|
||||
import {
|
||||
hasBlockingShortcutDialog,
|
||||
resolveGoToInboxKeyAction,
|
||||
resolveInboxQuickArchiveKeyAction,
|
||||
} from "../lib/keyboardShortcuts";
|
||||
import {
|
||||
applyOptimisticIssueFieldUpdate,
|
||||
applyOptimisticIssueFieldUpdateToCollection,
|
||||
|
|
@ -1232,6 +1236,8 @@ export function IssueDetail() {
|
|||
}, [closePanel, handleIssuePropertiesUpdate, issuePanelKey, openNewSubIssue, openPanel]);
|
||||
|
||||
const inboxQuickArchiveArmedRef = useRef(false);
|
||||
const goToInboxShortcutArmedRef = useRef(false);
|
||||
const goToInboxShortcutTimeoutRef = useRef<number | null>(null);
|
||||
const canQuickArchiveFromInbox =
|
||||
keyboardShortcutsEnabled &&
|
||||
!issue?.hiddenAt &&
|
||||
|
|
@ -1301,6 +1307,83 @@ export function IssueDetail() {
|
|||
};
|
||||
}, [archiveFromInbox, canQuickArchiveFromInbox, issue?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!keyboardShortcutsEnabled) {
|
||||
goToInboxShortcutArmedRef.current = false;
|
||||
if (goToInboxShortcutTimeoutRef.current !== null) {
|
||||
window.clearTimeout(goToInboxShortcutTimeoutRef.current);
|
||||
goToInboxShortcutTimeoutRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const clearArmTimeout = () => {
|
||||
if (goToInboxShortcutTimeoutRef.current !== null) {
|
||||
window.clearTimeout(goToInboxShortcutTimeoutRef.current);
|
||||
goToInboxShortcutTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const disarm = () => {
|
||||
goToInboxShortcutArmedRef.current = false;
|
||||
clearArmTimeout();
|
||||
};
|
||||
|
||||
const arm = () => {
|
||||
goToInboxShortcutArmedRef.current = true;
|
||||
clearArmTimeout();
|
||||
goToInboxShortcutTimeoutRef.current = window.setTimeout(() => {
|
||||
goToInboxShortcutArmedRef.current = false;
|
||||
goToInboxShortcutTimeoutRef.current = null;
|
||||
}, 1200);
|
||||
};
|
||||
|
||||
const handlePointerDown = () => {
|
||||
disarm();
|
||||
};
|
||||
|
||||
const handleFocusIn = (event: FocusEvent) => {
|
||||
if (event.target instanceof HTMLElement && event.target !== document.body) {
|
||||
disarm();
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
const action = resolveGoToInboxKeyAction({
|
||||
armed: goToInboxShortcutArmedRef.current,
|
||||
defaultPrevented: event.defaultPrevented,
|
||||
key: event.key,
|
||||
metaKey: event.metaKey,
|
||||
ctrlKey: event.ctrlKey,
|
||||
altKey: event.altKey,
|
||||
target: event.target,
|
||||
hasOpenDialog: hasBlockingShortcutDialog(document),
|
||||
});
|
||||
|
||||
if (action === "ignore") return;
|
||||
if (action === "arm") {
|
||||
arm();
|
||||
return;
|
||||
}
|
||||
|
||||
disarm();
|
||||
if (action !== "navigate") return;
|
||||
|
||||
event.preventDefault();
|
||||
navigate(sourceBreadcrumb.href.startsWith("/inbox") ? sourceBreadcrumb.href : "/inbox");
|
||||
};
|
||||
|
||||
document.addEventListener("pointerdown", handlePointerDown, true);
|
||||
document.addEventListener("focusin", handleFocusIn, true);
|
||||
document.addEventListener("keydown", handleKeyDown, true);
|
||||
return () => {
|
||||
disarm();
|
||||
document.removeEventListener("pointerdown", handlePointerDown, true);
|
||||
document.removeEventListener("focusin", handleFocusIn, true);
|
||||
document.removeEventListener("keydown", handleKeyDown, true);
|
||||
};
|
||||
}, [keyboardShortcutsEnabled, navigate, sourceBreadcrumb.href]);
|
||||
|
||||
const isImageAttachment = (attachment: IssueAttachment) => attachment.contentType.startsWith("image/");
|
||||
const attachmentList = attachments ?? [];
|
||||
const imageAttachments = attachmentList.filter(isImageAttachment);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue