diff --git a/docs/de/navigation.yml b/docs/de/navigation.yml
index dea28ffb8a..0ff8cc2a19 100644
--- a/docs/de/navigation.yml
+++ b/docs/de/navigation.yml
@@ -7,3 +7,4 @@
- /user/profile/
- /user/notification/
- /user/cli/
+ - /user/shortcuts/
diff --git a/docs/de/user/shortcuts/index.md b/docs/de/user/shortcuts/index.md
new file mode 100644
index 0000000000..626927c78e
--- /dev/null
+++ b/docs/de/user/shortcuts/index.md
@@ -0,0 +1,37 @@
+---
+title: Tastaturkürzel
+---
+Der SCM-Manager unterstützt Tastaturinteraktion und -navigation durch zusätzliche Tastenkürzel.
+
+### Übersicht
+
+Während sie den SCM-Manager verwenden, können sie eine Übersicht aller dem aktiven Benutzer auf der aktuellen Seite verfügbaren Tastenkürzel mittels der `?`-Taste aufrufen.
+
+### Globale Tastenkürzel
+
+| Key Combination | Description |
+|-----------------|-------------------------------------|
+| ? | Öffne die Tastaturkürzelübersicht |
+| / | Fokussiere die globale Schnellsuche |
+| alt r | Navigiere zur Repositoryübersicht |
+| alt u | Navigiere zur Benutzerübersicht |
+| alt g | Navigiere zur Gruppenübersicht |
+| alt a | Navigiere zur Administration |
+
+### Repositoryspezifische Tastenkürzel
+
+| Key Combination | Description |
+|-----------------|---------------|
+| g i | Info |
+| g b | Branches |
+| g t | Tags |
+| g c | Code |
+| g s | Einstellungen |
+
+### Tastenkürzel aus Plugin
+
+Plugins können selbst neue Tastenkürzel definieren.
+Diese können global oder repository-spezifisch sein oder in einem komplett anderen Kontext angewandt werden.
+Sie werden automatisch in der Übersicht im SCM-Manager mit aufgelistet.
+Um die Tastenkürzel eines Plugins innerhalb der Benutzerdokumentation zu finden, verweisen wir hier auf die Dokumentation
+des jeweiligen Plugins.
diff --git a/docs/en/navigation.yml b/docs/en/navigation.yml
index 98507e081f..7152f930a7 100644
--- a/docs/en/navigation.yml
+++ b/docs/en/navigation.yml
@@ -16,6 +16,7 @@
- /user/profile/
- /user/notification/
- /user/cli/
+ - /user/shortcuts/
- section: Administration
entries:
diff --git a/docs/en/user/shortcuts/index.md b/docs/en/user/shortcuts/index.md
new file mode 100644
index 0000000000..adaa139fc6
--- /dev/null
+++ b/docs/en/user/shortcuts/index.md
@@ -0,0 +1,37 @@
+---
+title: Shortcuts
+---
+The SCM-Manager enhances keyboard interaction and navigation through additional shortcuts.
+
+### Summary
+
+While using the SCM-Manager, a summary of all shortcuts available to the active user on the current page can be opened
+from anywhere by pressing the `?` key.
+
+### Global Shortcuts
+
+| Key Combination | Description |
+|-----------------|----------------------------|
+| ? | Open the shortcut summary |
+| / | Focus global quick search |
+| alt r | Navigate to Repositories |
+| alt u | Navigate to Users |
+| alt g | Navigate to Groups |
+| alt a | Navigate to Administration |
+
+### Repository-specific Shortcuts
+
+| Key Combination | Description |
+|-----------------|-------------|
+| g i | Info |
+| g b | Branches |
+| g t | Tags |
+| g c | Code |
+| g s | Settings |
+
+### Plugin Shortcuts
+
+Plugins can introduce new shortcuts.
+They may be global, repository-specific or connected to an entirely different context.
+They will automatically be included in the summary generated within the SCM-Manager.
+To find the shortcuts outside the SCM-Manager, please refer to the documentation of the plugin.
diff --git a/gradle/changelog/shortcuts_docs.yaml b/gradle/changelog/shortcuts_docs.yaml
new file mode 100644
index 0000000000..8e7f8ef06a
--- /dev/null
+++ b/gradle/changelog/shortcuts_docs.yaml
@@ -0,0 +1,2 @@
+- type: added
+ description: Keyboard shortcuts documentation ([#2129](https://github.com/scm-manager/scm-manager/pull/2129))
diff --git a/scm-ui/ui-webapp/package.json b/scm-ui/ui-webapp/package.json
index 51ad0b7199..90f49c89dd 100644
--- a/scm-ui/ui-webapp/package.json
+++ b/scm-ui/ui-webapp/package.json
@@ -27,7 +27,8 @@
"string_score": "^0.1.22",
"styled-components": "^5.3.5",
"systemjs": "0.21.6",
- "mousetrap": "^1.6.5"
+ "mousetrap": "^1.6.5",
+ "ua-parser-js": "^1.0.2"
},
"scripts": {
"test": "jest",
@@ -50,6 +51,7 @@
"@types/styled-components": "^5.1.25",
"@types/systemjs": "^0.20.6",
"@types/mousetrap": "^1.6.9",
+ "@types/ua-parser-js": "^0.7.36",
"fetch-mock": "^7.5.1",
"react-test-renderer": "^17.0.1"
},
@@ -68,4 +70,4 @@
"publishConfig": {
"access": "public"
}
-}
\ No newline at end of file
+}
diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json
index 5a039f59bb..c44a317c2e 100644
--- a/scm-ui/ui-webapp/public/locales/de/commons.json
+++ b/scm-ui/ui-webapp/public/locales/de/commons.json
@@ -261,5 +261,23 @@
},
"tag": {
"delete": "Löschen"
+ },
+ "shortcutDocsModal": {
+ "title": "Tastaturkürzel",
+ "description": "Die folgende Tabelle listet alle für Sie auf der aktuellen Seite verfügbaren Tastaturkürzel auf.",
+ "table": {
+ "headers": {
+ "keyCombination": "Tastenkombination",
+ "description": "Beschreibung"
+ }
+ }
+ },
+ "shortcuts": {
+ "search": "Fokussiere die globale Schnellsuche",
+ "repositories": "Navigiere zur Repositoryübersicht",
+ "users": "Navigiere zur Benutzerübersicht",
+ "groups": "Navigiere zur Gruppenübersicht",
+ "admin": "Navigiere zur Administration",
+ "docs": "Öffne die Tastaturkürzelübersicht"
}
}
diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json
index a71ece8ff3..744da2f5da 100644
--- a/scm-ui/ui-webapp/public/locales/de/repos.json
+++ b/scm-ui/ui-webapp/public/locales/de/repos.json
@@ -538,5 +538,12 @@
"queryToShort": "Tippe mindestens 2 Zeichen ein, um die Suche zu starten",
"emptyResult": "Es wurden keine Ergebnisse für <0>{{query}}0> gefunden"
}
+ },
+ "shortcuts": {
+ "info": "Info",
+ "branches": "Branches",
+ "tags": "Tags",
+ "code": "Code",
+ "settings": "Einstellungen"
}
}
diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json
index 8d72828333..f808cc39fe 100644
--- a/scm-ui/ui-webapp/public/locales/en/commons.json
+++ b/scm-ui/ui-webapp/public/locales/en/commons.json
@@ -262,5 +262,23 @@
},
"tag": {
"delete": "Delete"
+ },
+ "shortcutDocsModal": {
+ "title": "Keyboard Shortcuts",
+ "description": "The following table lists all keyboard shortcuts available to you on the current page.",
+ "table": {
+ "headers": {
+ "keyCombination": "Key Combination",
+ "description": "Description"
+ }
+ }
+ },
+ "shortcuts": {
+ "search": "Focus global quick search",
+ "repositories": "Navigate to Repositories",
+ "users": "Navigate to Users",
+ "groups": "Navigate to Groups",
+ "admin": "Navigate to Administration",
+ "docs": "Open the shortcut summary"
}
}
diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json
index 778854e0cf..9acd935234 100644
--- a/scm-ui/ui-webapp/public/locales/en/repos.json
+++ b/scm-ui/ui-webapp/public/locales/en/repos.json
@@ -545,5 +545,12 @@
"queryToShort": "Type at least two characters to start the search",
"emptyResult": "Nothing found for query <0>{{query}}0>"
}
+ },
+ "shortcuts": {
+ "info": "Info",
+ "branches": "Branches",
+ "tags": "Tags",
+ "code": "Code",
+ "settings": "Settings"
}
}
diff --git a/scm-ui/ui-webapp/src/containers/App.tsx b/scm-ui/ui-webapp/src/containers/App.tsx
index 3c274564bf..73140f9aef 100644
--- a/scm-ui/ui-webapp/src/containers/App.tsx
+++ b/scm-ui/ui-webapp/src/containers/App.tsx
@@ -34,6 +34,7 @@ import useShortcut from "../shortcuts/useShortcut";
import Login from "./Login";
import NavigationBar from "./NavigationBar";
import styled from "styled-components";
+import ShortcutDocsModal from "../shortcuts/ShortcutDocsModal";
const AppWrapper = styled.div`
min-height: 100vh;
@@ -47,25 +48,18 @@ const App: FC = () => {
usePauseShortcutsWhenModalsActive();
const history = useHistory();
- useShortcut("option+r", () => {
- if (index && index._links["repositories"]) {
- history.push("/repos/");
- }
+
+ useShortcut("option+r", () => history.push("/repos/"), t("shortcuts.repositories"), {
+ active: !!index?._links["repositories"],
});
- useShortcut("option+u", () => {
- if (index && index._links["users"]) {
- history.push("/users/");
- }
+ useShortcut("option+u", () => history.push("/users/"), t("shortcuts.users"), {
+ active: !!index?._links["users"],
});
- useShortcut("option+g", () => {
- if (index && index._links["groups"]) {
- history.push("/groups/");
- }
+ useShortcut("option+g", () => history.push("/groups/"), t("shortcuts.groups"), {
+ active: !!index?._links["groups"],
});
- useShortcut("option+a", () => {
- if (index && index._links["config"]) {
- history.push("/admin/");
- }
+ useShortcut("option+a", () => history.push("/admin/"), t("shortcuts.admin"), {
+ active: !!index?._links["config"],
});
if (!index) {
@@ -95,6 +89,7 @@ const App: FC = () => {
+
{content}
diff --git a/scm-ui/ui-webapp/src/containers/Index.tsx b/scm-ui/ui-webapp/src/containers/Index.tsx
index 8942fcabb7..edba6e458d 100644
--- a/scm-ui/ui-webapp/src/containers/Index.tsx
+++ b/scm-ui/ui-webapp/src/containers/Index.tsx
@@ -33,6 +33,7 @@ import i18next from "i18next";
import { binder, extensionPoints } from "@scm-manager/ui-extensions";
import InitializationAdminAccountStep from "./InitializationAdminAccountStep";
import InitializationPluginWizardStep from "./InitializationPluginWizardStep";
+import { ShortcutDocsContextProvider } from "../shortcuts/useShortcutDocs";
const Index: FC = () => {
const { isLoading, error, data } = useIndex();
@@ -60,13 +61,15 @@ const Index: FC = () => {
return (
-
-
- setPluginsLoaded(true)}>
-
-
-
-
+
+
+
+ setPluginsLoaded(true)}>
+
+
+
+
+
);
diff --git a/scm-ui/ui-webapp/src/containers/OmniSearch.tsx b/scm-ui/ui-webapp/src/containers/OmniSearch.tsx
index 3c530479ab..5c0e8a3649 100644
--- a/scm-ui/ui-webapp/src/containers/OmniSearch.tsx
+++ b/scm-ui/ui-webapp/src/containers/OmniSearch.tsx
@@ -30,7 +30,8 @@ import React, {
SetStateAction,
useCallback,
useEffect,
- useMemo, useRef,
+ useMemo,
+ useRef,
useState,
} from "react";
import { Hit, Links, Repository, ValueHitField } from "@scm-manager/ui-types";
@@ -357,7 +358,7 @@ const OmniSearch: FC = () => {
searchTypes.sort(orderTypes(t));
const id = useCallback(namespaceAndName, []);
- useShortcut("/", () => searchInputRef.current?.focus());
+ useShortcut("/", () => searchInputRef.current?.focus(), t("shortcuts.search"));
const entries = useMemo(() => {
const newEntries = [];
diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx
index 2bdecf8b4a..d79ce7e41c 100644
--- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx
+++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import { match as Match } from "react-router";
import { Link as RouteLink, Redirect, Route, RouteProps, Switch, useHistory, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
@@ -101,27 +101,27 @@ const RepositoryRoot = () => {
const url = urls.matchedUrlFromMatch(match);
- useShortcut("g i", () => {
- history.push(`${url}/info`);
- });
- useShortcut("g b", () => {
- if (repository && repository._links["branches"]) {
- history.push(`${url}/branches/`);
+ const codeLinkname = useMemo(() => {
+ if (repository?._links?.sources) {
+ return "sources";
}
- });
- useShortcut("g t", () => {
- if (repository && repository._links["tags"]) {
- history.push(`${url}/tags/`);
+ if (repository?._links?.changesets) {
+ return "changesets";
}
+ return "";
+ }, [repository]);
+
+ useShortcut("g i", () => history.push(`${url}/info`), t("shortcuts.info"));
+ useShortcut("g b", () => history.push(`${url}/branches/`), t("shortcuts.branches"), {
+ active: !!repository?._links["branches"],
});
- useShortcut("g c", () => {
- if (repository && repository._links[getCodeLinkname()]) {
- history.push(evaluateDestinationForCodeLink());
- }
+ useShortcut("g t", () => history.push(`${url}/tags/`), t("shortcuts.tags"), {
+ active: !!repository?._links["tags"],
});
- useShortcut("g s", () => {
- history.push(`${url}/settings/general`);
+ useShortcut("g c", () => history.push(evaluateDestinationForCodeLink()), t("shortcuts.code"), {
+ active: !!repository?._links[codeLinkname],
});
+ useShortcut("g s", () => history.push(`${url}/settings/general`), t("shortcuts.settings"));
useEffect(() => {
if (repository) {
@@ -239,16 +239,6 @@ const RepositoryRoot = () => {
return !!route.location.pathname.match(regex);
};
- const getCodeLinkname = () => {
- if (repository?._links?.sources) {
- return "sources";
- }
- if (repository?._links?.changesets) {
- return "changesets";
- }
- return "";
- };
-
const evaluateDestinationForCodeLink = () => {
if (repository?._links?.sources) {
return `${url}/code/sources/`;
@@ -381,7 +371,7 @@ const RepositoryRoot = () => {
/>
(a > b ? 1 : -1);
+
+const ShortcutDocsModal = () => {
+ const { docs } = useShortcutDocs();
+ const [open, setOpen] = useState(false);
+ const [t] = useTranslation("commons");
+ useShortcut("?", () => setOpen(true), t("shortcuts.docs"));
+
+ return (
+ setOpen(false)} active={open}>
+ {t("shortcutDocsModal.description")}
+
+
+ {([key]: [string, string]) =>
+ splitKeyCombination(key).map((k, i) => (
+ 0,
+ }
+ )}
+ >
+ {k}
+
+ ))
+ }
+
+
+ {([, description]: [string, string]) => description}
+
+
+
+ );
+};
+
+export default ShortcutDocsModal;
diff --git a/scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.test.ts b/scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.test.ts
new file mode 100644
index 0000000000..26d48faa06
--- /dev/null
+++ b/scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.test.ts
@@ -0,0 +1,56 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import splitKeyCombination from "./splitKeyCombination";
+
+const MAC_USER_AGENT =
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/600.7.12 (KHTML, like Gecko) Version/8.0.7 Safari/600.7.12";
+const WINDOWS_USER_AGENT =
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36";
+const LINUX_USER_AGENT =
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36";
+
+describe("splitKeyCombination", () => {
+ it("should split and replace correctly", () => {
+ expect(splitKeyCombination("alt+a meta+b meta+c", WINDOWS_USER_AGENT)).toEqual([
+ "alt",
+ "a",
+ "meta",
+ "b",
+ "meta",
+ "c",
+ ]);
+ expect(splitKeyCombination("option+a command+b command+c mod+d", LINUX_USER_AGENT)).toEqual([
+ "alt",
+ "a",
+ "meta",
+ "b",
+ "meta",
+ "c",
+ "ctrl",
+ "d",
+ ]);
+ expect(splitKeyCombination("alt+a meta+b mod+c", MAC_USER_AGENT)).toEqual(["option", "a", "⌘", "b", "⌘", "c"]);
+ });
+});
diff --git a/scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.ts b/scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.ts
new file mode 100644
index 0000000000..6b000043ca
--- /dev/null
+++ b/scm-ui/ui-webapp/src/shortcuts/splitKeyCombination.ts
@@ -0,0 +1,37 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import parser from "ua-parser-js";
+
+export default function splitKeyCombination(key: string, userAgent = window.navigator.userAgent) {
+ const {
+ os: { name: osName },
+ } = parser(userAgent);
+ const isMacOS = osName === "Mac OS";
+ return key
+ .replace(/(option|alt)/g, isMacOS ? "option" : "alt")
+ .replace(/(command|meta)/g, isMacOS ? "⌘" : "meta")
+ .replace("mod", isMacOS ? "⌘" : "ctrl")
+ .split(/[+ ]/);
+}
diff --git a/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts b/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts
index 059423497f..2e0238ea32 100644
--- a/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts
+++ b/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts
@@ -24,6 +24,16 @@
import { useEffect } from "react";
import Mousetrap from "mousetrap";
+import useShortcutDocs from "./useShortcutDocs";
+
+export type UseShortcutOptions = {
+ /**
+ * Whether the shortcut is currently active
+ *
+ * @default true
+ */
+ active?: boolean;
+};
/**
* ## Summary
@@ -46,29 +56,45 @@ import Mousetrap from "mousetrap";
*
* ## Combinations
*
- * Keys can be combined with the "+" separator, but without extra whitespaces.
+ * Keys can be combined by separating them with a whitespace.
+ * For using modifiers, prefix the key with the modifier and concat them with a "+".
*
- * @param key
- * @param callback
- * @example useShortcut("/", ...)
+ * Please also refer to the examples.
+ *
+ * @param key The keycode combination that triggers the callback
+ * @param callback The function that is executed when the key combination is pressed
+ * @param description The translated description used for the shortcut documentation
+ * @param options Whether the shortcut is currently active, defaults to true
+ * @example useShortcut("a b", ...)
* @example useShortcut("ctrl+shift+k", ...)
* @see https://github.com/ccampbell/mousetrap
* @see https://craig.is/killing/mice
*/
-export default function useShortcut(key: string, callback: (e: KeyboardEvent) => void) {
+export default function useShortcut(
+ key: string,
+ callback: (e: KeyboardEvent) => void,
+ description: string,
+ options?: UseShortcutOptions
+) {
+ const { add, remove } = useShortcutDocs();
useEffect(() => {
- Mousetrap.bind(key, (e) => {
- callback(e);
- /*
- * Returning false disables default event behaviour and stops event bubbling.
- * Otherwise, a shortcut that moves focus to an input field would cause the key to be entered into the input at the same time.
- * We could move the decision to the callback, but this behaviour is an implementation detail of Mousetrap which we would like to hide.
- */
- return false;
- });
+ const active = !options || options.active === undefined || options.active;
+ if (active) {
+ add(key, description);
+ Mousetrap.bind(key, (e) => {
+ callback(e);
+ /*
+ * Returning false disables default event behaviour and stops event bubbling.
+ * Otherwise, a shortcut that moves focus to an input field would cause the key to be entered into the input at the same time.
+ * We could move the decision to the callback, but this behaviour is an implementation detail of Mousetrap which we would like to hide.
+ */
+ return false;
+ });
+ }
return () => {
+ remove(key);
Mousetrap.unbind(key);
};
- }, [key, callback]);
+ }, [key, callback, add, remove, options, description]);
}
diff --git a/scm-ui/ui-webapp/src/shortcuts/useShortcutDocs.tsx b/scm-ui/ui-webapp/src/shortcuts/useShortcutDocs.tsx
new file mode 100644
index 0000000000..4ae9cbe6fa
--- /dev/null
+++ b/scm-ui/ui-webapp/src/shortcuts/useShortcutDocs.tsx
@@ -0,0 +1,53 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+import React, { FC, useContext, useMemo, useRef } from "react";
+
+export type ShortcutDocsContextType = {
+ docs: Readonly>;
+ add: (key: string, description: string) => void;
+ remove: (key: string) => void;
+};
+
+const ShortcutDocsContext = React.createContext({} as ShortcutDocsContextType);
+
+export const ShortcutDocsContextProvider: FC = ({ children }) => {
+ const docs = useRef>({});
+ const value = useMemo(
+ () => ({
+ docs: docs.current,
+ add: (key: string, description: string) => (docs.current[key] = description),
+ remove: (key: string) => {
+ delete docs.current[key];
+ },
+ }),
+ []
+ );
+
+ return {children};
+};
+
+export default function useShortcutDocs() {
+ return useContext(ShortcutDocsContext);
+}
diff --git a/yarn.lock b/yarn.lock
index 00c49705e2..61435887e3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4169,6 +4169,11 @@
resolved "https://registry.yarnpkg.com/@types/to-camel-case/-/to-camel-case-1.0.0.tgz#927ef0a7294d90b1835466c29b64b8ad2a32d8b5"
integrity sha512-LXJOP0xvOUB4dKu+t7EVhSsM2NauLSZSOGkBS7Wqz3lWHIseCJnMDG+HrZHLFZQ39Fq3jr4RErJyQzfsoOlXSA==
+"@types/ua-parser-js@^0.7.36":
+ version "0.7.36"
+ resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"
+ integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==
+
"@types/uglify-js@*":
version "3.16.0"
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.16.0.tgz#2cf74a0e6ebb6cd54c0d48e509d5bd91160a9602"
@@ -18717,6 +18722,11 @@ typescript@^4.0.5, typescript@^4.6.0:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
+ua-parser-js@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775"
+ integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==
+
uglify-js@^3.1.4:
version "3.16.3"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.3.tgz#94c7a63337ee31227a18d03b8a3041c210fd1f1d"