diff --git a/gradle/changelog/navigation_shortcuts.yaml b/gradle/changelog/navigation_shortcuts.yaml new file mode 100644 index 0000000000..6cd49d785e --- /dev/null +++ b/gradle/changelog/navigation_shortcuts.yaml @@ -0,0 +1,2 @@ +- type: added + description: Keyboard shortcuts for global and repository-specific navigation ([#2122](https://github.com/scm-manager/scm-manager/pull/2122)) diff --git a/scm-ui/ui-webapp/src/containers/App.tsx b/scm-ui/ui-webapp/src/containers/App.tsx index eb881b3519..8231b29994 100644 --- a/scm-ui/ui-webapp/src/containers/App.tsx +++ b/scm-ui/ui-webapp/src/containers/App.tsx @@ -23,15 +23,17 @@ */ import React, { FC } from "react"; import Main from "./Main"; +import { useHistory } from "react-router-dom"; import { useTranslation } from "react-i18next"; +import styled from "styled-components"; +import { useIndex, useSubject } from "@scm-manager/ui-api"; import { ErrorPage, Footer, Header, Loading } from "@scm-manager/ui-components"; import { binder } from "@scm-manager/ui-extensions"; -import Login from "./Login"; -import { useIndex, useSubject } from "@scm-manager/ui-api"; -import NavigationBar from "./NavigationBar"; -import styled from "styled-components"; -import Feedback from "./Feedback"; import usePauseShortcutsWhenModalsActive from "../shortcuts/usePauseShortcutsWhenModalsActive"; +import useShortcut from "../shortcuts/useShortcut"; +import Login from "./Login"; +import NavigationBar from "./NavigationBar"; +import Feedback from "./Feedback"; const AppWrapper = styled.div` min-height: 100vh; @@ -44,6 +46,28 @@ const App: FC = () => { const [t] = useTranslation("commons"); usePauseShortcutsWhenModalsActive(); + const history = useHistory(); + useShortcut("option+r", () => { + if (index && index._links["repositories"]) { + history.push("/repos/"); + } + }); + useShortcut("option+u", () => { + if (index && index._links["users"]) { + history.push("/users/"); + } + }); + useShortcut("option+g", () => { + if (index && index._links["groups"]) { + history.push("/groups/"); + } + }); + useShortcut("option+a", () => { + if (index && index._links["config"]) { + history.push("/admin/"); + } + }); + if (!index) { return null; } diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx index 40af0212a4..2bdecf8b4a 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryRoot.tsx @@ -23,7 +23,7 @@ */ import React, { useEffect, useState } from "react"; import { match as Match } from "react-router"; -import { Link as RouteLink, Redirect, Route, RouteProps, Switch, useRouteMatch } from "react-router-dom"; +import { Link as RouteLink, Redirect, Route, RouteProps, Switch, useHistory, useRouteMatch } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { binder, ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { Changeset, Link } from "@scm-manager/ui-types"; @@ -62,6 +62,7 @@ import CompareRoot from "../compare/CompareRoot"; import TagRoot from "../tags/container/TagRoot"; import { useIndexLinks, useNamespaceAndNameContext, useRepository } from "@scm-manager/ui-api"; import styled from "styled-components"; +import useShortcut from "../../shortcuts/useShortcut"; const TagGroup = styled.span` & > * { @@ -96,6 +97,31 @@ const RepositoryRoot = () => { const [showHealthCheck, setShowHealthCheck] = useState(false); const [t] = useTranslation("repos"); const context = useNamespaceAndNameContext(); + const history = useHistory(); + + const url = urls.matchedUrlFromMatch(match); + + useShortcut("g i", () => { + history.push(`${url}/info`); + }); + useShortcut("g b", () => { + if (repository && repository._links["branches"]) { + history.push(`${url}/branches/`); + } + }); + useShortcut("g t", () => { + if (repository && repository._links["tags"]) { + history.push(`${url}/tags/`); + } + }); + useShortcut("g c", () => { + if (repository && repository._links[getCodeLinkname()]) { + history.push(evaluateDestinationForCodeLink()); + } + }); + useShortcut("g s", () => { + history.push(`${url}/settings/general`); + }); useEffect(() => { if (repository) { @@ -118,8 +144,6 @@ const RepositoryRoot = () => { return ; } - const url = urls.matchedUrlFromMatch(match); - // props used for extensions // most of the props required for compatibility const props = { diff --git a/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts b/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts index 37614eaea1..059423497f 100644 --- a/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts +++ b/scm-ui/ui-webapp/src/shortcuts/useShortcut.ts @@ -53,6 +53,7 @@ import Mousetrap from "mousetrap"; * @example useShortcut("/", ...) * @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) { useEffect(() => {