fix: Use a narrower type for custom shortcut names and reduce hard-coding.

This commit is contained in:
Daniel J. Geiger 2023-11-22 14:47:53 -06:00
parent f71ded4bf9
commit 9642a6e756
6 changed files with 58 additions and 42 deletions

View File

@ -2,11 +2,12 @@ import { isDarwin } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import { SubtypeOf } from "../utility-types"; import { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { ActionName } from "./types"; import { ActionName, CustomActionName } from "./types";
export type ShortcutName = export type ShortcutName =
| SubtypeOf< | SubtypeOf<
ActionName, ActionName,
| CustomActionName
| "toggleTheme" | "toggleTheme"
| "loadScene" | "loadScene"
| "clearCanvas" | "clearCanvas"
@ -40,6 +41,15 @@ export type ShortcutName =
| "saveScene" | "saveScene"
| "imageExport"; | "imageExport";
export const registerCustomShortcuts = (
shortcuts: Record<CustomActionName, string[]>,
) => {
for (const key in shortcuts) {
const shortcut = key as CustomActionName;
shortcutMap[shortcut] = shortcuts[shortcut];
}
};
const shortcutMap: Record<ShortcutName, string[]> = { const shortcutMap: Record<ShortcutName, string[]> = {
toggleTheme: [getShortcutKey("Shift+Alt+D")], toggleTheme: [getShortcutKey("Shift+Alt+D")],
saveScene: [getShortcutKey("CtrlOrCmd+S")], saveScene: [getShortcutKey("CtrlOrCmd+S")],
@ -85,23 +95,8 @@ const shortcutMap: Record<ShortcutName, string[]> = {
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")], toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
}; };
export type CustomShortcutName = string; export const getShortcutFromShortcutName = (name: ShortcutName) => {
const shortcuts = shortcutMap[name];
let customShortcutMap: Record<CustomShortcutName, string[]> = {};
export const registerCustomShortcuts = (
shortcuts: Record<CustomShortcutName, string[]>,
) => {
customShortcutMap = { ...customShortcutMap, ...shortcuts };
};
export const getShortcutFromShortcutName = (
name: ShortcutName | CustomShortcutName,
) => {
const shortcuts =
name in customShortcutMap
? customShortcutMap[name as CustomShortcutName]
: shortcutMap[name as ShortcutName];
// if multiple shortcuts available, take the first one // if multiple shortcuts available, take the first one
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : ""; return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
}; };

View File

@ -44,8 +44,11 @@ export type ActionPredicateFn = (
export type UpdaterFn = (res: ActionResult) => void; export type UpdaterFn = (res: ActionResult) => void;
export type ActionFilterFn = (action: Action) => void; export type ActionFilterFn = (action: Action) => void;
export const makeCustomActionName = (name: string) =>
`custom.${name}` as CustomActionName;
export type CustomActionName = `custom.${string}`;
export type ActionName = export type ActionName =
| `custom.${string}` | CustomActionName
| "copy" | "copy"
| "cut" | "cut"
| "paste" | "paste"

View File

@ -1,6 +1,6 @@
import { getShortcutKey, updateActiveTool } from "../utils"; import { getShortcutKey, updateActiveTool } from "../utils";
import { t } from "../i18n"; import { t } from "../i18n";
import { Action } from "../actions/types"; import { Action, makeCustomActionName } from "../actions/types";
import clsx from "clsx"; import clsx from "clsx";
import { import {
Subtype, Subtype,
@ -29,7 +29,7 @@ export const SubtypeButton = (
const keyTest: Action["keyTest"] = const keyTest: Action["keyTest"] =
key !== undefined ? (event) => event.code === `Key${key}` : undefined; key !== undefined ? (event) => event.code === `Key${key}` : undefined;
const subtypeAction: Action = { const subtypeAction: Action = {
name: `custom.${subtype}`, name: makeCustomActionName(subtype),
trackEvent: false, trackEvent: false,
predicate: (...rest) => rest[4]?.subtype === subtype, predicate: (...rest) => rest[4]?.subtype === subtype,
perform: (elements, appState) => { perform: (elements, appState) => {
@ -159,7 +159,7 @@ export const SubtypeToggles = () => {
> >
{getSubtypeNames().map((subtype) => {getSubtypeNames().map((subtype) =>
am.renderAction( am.renderAction(
`custom.${subtype}`, makeCustomActionName(subtype),
hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {}, hasAlwaysEnabledActions(subtype) ? { onContextMenu } : {},
), ),
)} )}

View File

@ -5,11 +5,14 @@ import { getSelectedElements } from "../../scene";
import { AppState, ExcalidrawImperativeAPI } from "../../types"; import { AppState, ExcalidrawImperativeAPI } from "../../types";
import { registerAuxLangData } from "../../i18n"; import { registerAuxLangData } from "../../i18n";
import { Action, ActionName, ActionPredicateFn } from "../../actions/types";
import { import {
CustomShortcutName, Action,
registerCustomShortcuts, ActionName,
} from "../../actions/shortcuts"; ActionPredicateFn,
CustomActionName,
makeCustomActionName,
} from "../../actions/types";
import { registerCustomShortcuts } from "../../actions/shortcuts";
import { register } from "../../actions/register"; import { register } from "../../actions/register";
import { hasBoundTextElement, isTextElement } from "../typeChecks"; import { hasBoundTextElement, isTextElement } from "../typeChecks";
import { import {
@ -44,7 +47,7 @@ export type SubtypeRecord = Readonly<{
parents: readonly ExcalidrawElement["type"][]; parents: readonly ExcalidrawElement["type"][];
actionNames?: readonly SubtypeActionName[]; actionNames?: readonly SubtypeActionName[];
disabledNames?: readonly DisabledActionName[]; disabledNames?: readonly DisabledActionName[];
shortcutMap?: Record<CustomShortcutName, string[]>; shortcutMap?: Record<string, string[]>;
alwaysEnabledNames?: readonly SubtypeActionName[]; alwaysEnabledNames?: readonly SubtypeActionName[];
}>; }>;
@ -373,8 +376,8 @@ export const prepareSubtype = (
...subtypeActionMap, ...subtypeActionMap,
{ {
subtype, subtype,
actions: record.actionNames.map( actions: record.actionNames.map((actionName) =>
(actionName) => `custom.${actionName}` as ActionName, makeCustomActionName(actionName),
), ),
}, },
]; ];
@ -390,14 +393,19 @@ export const prepareSubtype = (
...alwaysEnabledMap, ...alwaysEnabledMap,
{ {
subtype, subtype,
actions: record.alwaysEnabledNames.map( actions: record.alwaysEnabledNames.map((actionName) =>
(actionName) => `custom.${actionName}` as ActionName, makeCustomActionName(actionName),
), ),
}, },
]; ];
} }
if (record.shortcutMap) { const customShortcutMap = record.shortcutMap;
registerCustomShortcuts(record.shortcutMap); if (customShortcutMap) {
const shortcutMap: Record<CustomActionName, string[]> = {};
for (const key in customShortcutMap) {
shortcutMap[makeCustomActionName(key)] = customShortcutMap[key];
}
registerCustomShortcuts(shortcutMap);
} }
// Prepare the subtype // Prepare the subtype

View File

@ -4,11 +4,16 @@ import { API } from "./helpers/api";
import { render } from "./test-utils"; import { render } from "./test-utils";
import { Excalidraw } from "../packages/excalidraw/index"; import { Excalidraw } from "../packages/excalidraw/index";
import { import {
CustomShortcutName,
getShortcutFromShortcutName, getShortcutFromShortcutName,
registerCustomShortcuts, registerCustomShortcuts,
} from "../actions/shortcuts"; } from "../actions/shortcuts";
import { Action, ActionPredicateFn, ActionResult } from "../actions/types"; import {
Action,
ActionPredicateFn,
ActionResult,
CustomActionName,
makeCustomActionName,
} from "../actions/types";
import { import {
actionChangeFontFamily, actionChangeFontFamily,
actionChangeFontSize, actionChangeFontSize,
@ -19,11 +24,14 @@ const { h } = window;
describe("regression tests", () => { describe("regression tests", () => {
it("should retrieve custom shortcuts", () => { it("should retrieve custom shortcuts", () => {
const shortcuts: Record<CustomShortcutName, string[]> = { const shortcutName = makeCustomActionName("test");
test: [getShortcutKey("CtrlOrCmd+1"), getShortcutKey("CtrlOrCmd+2")], const shortcuts: Record<CustomActionName, string[]> = {};
}; shortcuts[shortcutName] = [
getShortcutKey("CtrlOrCmd+1"),
getShortcutKey("CtrlOrCmd+2"),
];
registerCustomShortcuts(shortcuts); registerCustomShortcuts(shortcuts);
expect(getShortcutFromShortcutName("test")).toBe("Ctrl+1"); expect(getShortcutFromShortcutName(shortcutName)).toBe("Ctrl+1");
}); });
it("should apply universal action predicates", async () => { it("should apply universal action predicates", async () => {

View File

@ -32,7 +32,7 @@ import { getFontString, getShortcutKey } from "../utils";
import * as textElementUtils from "../element/textElement"; import * as textElementUtils from "../element/textElement";
import { isTextElement } from "../element"; import { isTextElement } from "../element";
import { mutateElement, newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
import { Action, ActionName } from "../actions/types"; import { Action, ActionName, makeCustomActionName } from "../actions/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { getShortcutFromShortcutName } from "../actions/shortcuts"; import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { actionChangeSloppiness } from "../actions"; import { actionChangeSloppiness } from "../actions";
@ -81,7 +81,7 @@ const test1: SubtypeRecord = {
}; };
const testAction: Action = { const testAction: Action = {
name: `custom.${TEST_ACTION}`, name: makeCustomActionName(TEST_ACTION),
trackEvent: false, trackEvent: false,
perform: (elements, appState) => { perform: (elements, appState) => {
return { return {
@ -338,7 +338,9 @@ describe("subtypes", () => {
expect(test3Methods?.clean).toBeUndefined(); expect(test3Methods?.clean).toBeUndefined();
}); });
it("should register custom shortcuts", async () => { it("should register custom shortcuts", async () => {
expect(getShortcutFromShortcutName("testShortcut")).toBe("Shift+T"); expect(
getShortcutFromShortcutName(makeCustomActionName("testShortcut")),
).toBe("Shift+T");
}); });
it("should correctly validate", async () => { it("should correctly validate", async () => {
test1.parents.forEach((p) => { test1.parents.forEach((p) => {