Add keyboard shortcuts documentation (#2129)

There is currently no documentation which shortcuts are available to the end user, neither within the application nor the documentation published on scm-manager.org. This PR adds the missing documentation in both places and introduces a new api for developers to add documentation when using `useShortcut`. It also improves the api for conditional shortcuts significantly.
This commit is contained in:
Konstantin Schaper
2022-10-11 16:29:33 +02:00
committed by GitHub
parent 33f7dd994a
commit 65f111b8b4
20 changed files with 439 additions and 70 deletions

View File

@@ -7,3 +7,4 @@
- /user/profile/
- /user/notification/
- /user/cli/
- /user/shortcuts/

View File

@@ -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.

View File

@@ -16,6 +16,7 @@
- /user/profile/
- /user/notification/
- /user/cli/
- /user/shortcuts/
- section: Administration
entries:

View File

@@ -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.

View File

@@ -0,0 +1,2 @@
- type: added
description: Keyboard shortcuts documentation ([#2129](https://github.com/scm-manager/scm-manager/pull/2129))

View File

@@ -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"
},

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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 = () => {
<Header authenticated={authenticatedOrAnonymous}>
<NavigationBar links={index._links} />
</Header>
<ShortcutDocsModal />
<div className="is-flex-grow-1">{content}</div>
<Footer me={me} version={index.version} links={index._links} />
</AppWrapper>

View File

@@ -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,6 +61,7 @@ const Index: FC = () => {
return (
<ErrorBoundary fallback={IndexErrorPage}>
<ScrollToTop>
<ShortcutDocsContextProvider>
<ActiveModalCountContextProvider>
<NamespaceAndNameContextProvider>
<PluginLoader link={link} loaded={pluginsLoaded} callback={() => setPluginsLoaded(true)}>
@@ -67,6 +69,7 @@ const Index: FC = () => {
</PluginLoader>
</NamespaceAndNameContextProvider>
</ActiveModalCountContextProvider>
</ShortcutDocsContextProvider>
</ScrollToTop>
</ErrorBoundary>
);

View File

@@ -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 = [];

View File

@@ -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 = () => {
/>
<RepositoryNavLink
repository={repository}
linkName={getCodeLinkname()}
linkName={codeLinkname}
to={evaluateDestinationForCodeLink()}
icon="fas fa-code"
label={t("repositoryRoot.menu.sourcesNavLink")}

View File

@@ -0,0 +1,68 @@
/*
* 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, { useState } from "react";
import useShortcutDocs from "./useShortcutDocs";
import { Column, Modal, Table } from "@scm-manager/ui-components";
import useShortcut from "./useShortcut";
import { useTranslation } from "react-i18next";
import classNames from "classnames";
import splitKeyCombination from "./splitKeyCombination";
const keyComparator = ([a]: [string, string], [b]: [string, string]) => (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 (
<Modal title={t("shortcutDocsModal.title")} closeFunction={() => setOpen(false)} active={open}>
<p className="mb-2">{t("shortcutDocsModal.description")}</p>
<Table data={Object.entries(docs).sort(keyComparator)}>
<Column header={t("shortcutDocsModal.table.headers.keyCombination")}>
{([key]: [string, string]) =>
splitKeyCombination(key).map((k, i) => (
<span
className={classNames(
"has-background-secondary-less has-text-secondary-most py-1 px-2 has-rounded-border",
{
"ml-1": i > 0,
}
)}
>
{k}
</span>
))
}
</Column>
<Column header={t("shortcutDocsModal.table.headers.description")}>
{([, description]: [string, string]) => description}
</Column>
</Table>
</Modal>
);
};
export default ShortcutDocsModal;

View File

@@ -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"]);
});
});

View File

@@ -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(/[+ ]/);
}

View File

@@ -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,17 +56,31 @@ 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(() => {
const active = !options || options.active === undefined || options.active;
if (active) {
add(key, description);
Mousetrap.bind(key, (e) => {
callback(e);
/*
@@ -66,9 +90,11 @@ export default function useShortcut(key: string, callback: (e: KeyboardEvent) =>
*/
return false;
});
}
return () => {
remove(key);
Mousetrap.unbind(key);
};
}, [key, callback]);
}, [key, callback, add, remove, options, description]);
}

View File

@@ -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<Record<string, string>>;
add: (key: string, description: string) => void;
remove: (key: string) => void;
};
const ShortcutDocsContext = React.createContext<ShortcutDocsContextType>({} as ShortcutDocsContextType);
export const ShortcutDocsContextProvider: FC = ({ children }) => {
const docs = useRef<Record<string, string>>({});
const value = useMemo(
() => ({
docs: docs.current,
add: (key: string, description: string) => (docs.current[key] = description),
remove: (key: string) => {
delete docs.current[key];
},
}),
[]
);
return <ShortcutDocsContext.Provider value={value}>{children}</ShortcutDocsContext.Provider>;
};
export default function useShortcutDocs() {
return useContext(ShortcutDocsContext);
}

View File

@@ -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"