From 3c0ad46f070d7662c0d84d4911a7e2aa2b890f8c Mon Sep 17 00:00:00 2001 From: Florian Scholdei Date: Tue, 10 Dec 2024 16:41:01 +0100 Subject: [PATCH] Set descriptive document titles Document titles represent the pages, for example in lists with bookmarks. They are important for navigation and orientation in websites. If the offer or the content of the page is not labeled, orientation is impaired. This changes the behavior for setting document titles. The functionality has been removed from the Page and Title components and is now represented by `useDocumentTitle` hook to better describe the content of inividual pages. Co-authored-by: Anna Vetcininova --- gradle/changelog/document_title.yaml | 2 + .../src/main/js/GitGlobalConfiguration.tsx | 14 ++- .../src/main/js/HgGlobalConfiguration.tsx | 33 +++--- .../src/main/js/SvnGlobalConfiguration.tsx | 33 +++--- scm-ui/ui-components/src/ErrorBoundary.tsx | 6 +- scm-ui/ui-components/src/ErrorPage.tsx | 38 +++---- scm-ui/ui-components/src/layout/Page.tsx | 92 +++++---------- scm-ui/ui-components/src/layout/Title.tsx | 14 +-- scm-ui/ui-core/src/base/helpers/index.ts | 1 + .../src/base/helpers/useDocumentTitle.test.ts | 48 ++++++++ .../src/base/helpers/useDocumentTitle.ts | 51 +++++++++ scm-ui/ui-core/src/base/misc/Title.tsx | 12 +- scm-ui/ui-extensions/src/extensionPoints.tsx | 5 + scm-ui/ui-webapp/public/locales/de/admin.json | 4 +- .../ui-webapp/public/locales/de/commons.json | 3 + .../ui-webapp/public/locales/de/groups.json | 1 + scm-ui/ui-webapp/public/locales/de/repos.json | 42 ++++++- scm-ui/ui-webapp/public/locales/de/users.json | 1 + scm-ui/ui-webapp/public/locales/en/admin.json | 4 +- .../ui-webapp/public/locales/en/commons.json | 3 + .../ui-webapp/public/locales/en/groups.json | 1 + scm-ui/ui-webapp/public/locales/en/repos.json | 57 +++++++--- scm-ui/ui-webapp/public/locales/en/users.json | 1 + .../src/admin/containers/AdminDetails.tsx | 4 +- .../src/admin/containers/GlobalConfig.tsx | 2 + .../plugins/containers/PluginsOverview.tsx | 2 + .../components/PermissionRoleDetails.tsx | 63 ++++++----- .../roles/containers/CreateRepositoryRole.tsx | 2 + .../roles/containers/EditRepositoryRole.tsx | 5 +- .../roles/containers/RepositoryRoles.tsx | 8 +- .../roles/containers/SingleRepositoryRole.tsx | 3 +- scm-ui/ui-webapp/src/components/LoginInfo.tsx | 12 +- .../src/containers/Accessibility.tsx | 11 +- .../src/containers/ChangeUserPassword.tsx | 6 +- .../src/containers/ExternalError.tsx | 2 + scm-ui/ui-webapp/src/containers/Index.tsx | 3 +- scm-ui/ui-webapp/src/containers/Logout.tsx | 8 +- scm-ui/ui-webapp/src/containers/Profile.tsx | 4 +- .../ui-webapp/src/containers/ProfileInfo.tsx | 6 +- scm-ui/ui-webapp/src/containers/Theme.tsx | 17 ++- .../src/groups/components/table/Details.tsx | 105 +++++++++--------- .../src/groups/containers/CreateGroup.tsx | 2 + .../src/groups/containers/EditGroup.tsx | 8 +- .../src/groups/containers/Groups.tsx | 4 +- .../components/SetGroupPermissions.tsx | 18 ++- .../components/SetUserPermissions.tsx | 14 ++- .../branches/components/BranchDetail.tsx | 15 ++- .../repos/branches/components/BranchView.tsx | 43 ++++--- .../branches/containers/BranchesOverview.tsx | 4 + .../branches/containers/CreateBranch.tsx | 10 +- .../codeSection/containers/FileSearch.tsx | 10 +- .../src/repos/compare/CompareSelectBar.tsx | 15 ++- .../src/repos/compare/CompareSelector.tsx | 13 +-- .../repos/components/RepositoryDetails.tsx | 44 ++++---- .../src/repos/containers/ChangesetView.tsx | 15 ++- .../src/repos/containers/Changesets.tsx | 49 +++++++- .../repos/containers/CreateRepositoryRoot.tsx | 23 ++-- .../src/repos/containers/EditRepo.tsx | 20 ++-- .../src/repos/containers/Overview.tsx | 15 +++ .../src/repos/importlog/ImportLog.tsx | 6 +- .../containers/NamespaceInformation.tsx | 7 +- .../permissions/containers/Permissions.tsx | 9 +- .../src/repos/sources/containers/Sources.tsx | 27 ++++- .../src/repos/tags/components/TagDetail.tsx | 8 ++ .../src/repos/tags/container/TagsOverview.tsx | 2 + scm-ui/ui-webapp/src/search/Search.tsx | 27 +++-- scm-ui/ui-webapp/src/search/Syntax.tsx | 78 +++++++------ .../src/users/components/SetUserPassword.tsx | 4 +- .../users/components/apiKeys/SetApiKeys.tsx | 2 + .../components/publicKeys/SetPublicKeys.tsx | 2 + .../src/users/components/table/Details.tsx | 14 ++- .../src/users/containers/CreateUser.tsx | 3 +- .../src/users/containers/EditUser.tsx | 14 ++- .../ui-webapp/src/users/containers/Users.tsx | 3 +- 74 files changed, 812 insertions(+), 445 deletions(-) create mode 100644 gradle/changelog/document_title.yaml create mode 100644 scm-ui/ui-core/src/base/helpers/useDocumentTitle.test.ts create mode 100644 scm-ui/ui-core/src/base/helpers/useDocumentTitle.ts diff --git a/gradle/changelog/document_title.yaml b/gradle/changelog/document_title.yaml new file mode 100644 index 0000000000..873a13e1c1 --- /dev/null +++ b/gradle/changelog/document_title.yaml @@ -0,0 +1,2 @@ +- type: changed + description: Replace title behavior with `useDocumentTitle` hook for setting descriptive document titles diff --git a/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.tsx b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.tsx index 64a41077c9..9bff653d04 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.tsx +++ b/scm-plugins/scm-git-plugin/src/main/js/GitGlobalConfiguration.tsx @@ -15,11 +15,12 @@ */ import React, { FC, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { Title, ConfigurationForm, InputField, Checkbox, validation } from "@scm-manager/ui-components"; -import { useConfigLink } from "@scm-manager/ui-api"; -import { HalRepresentation } from "@scm-manager/ui-types"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { useConfigLink } from "@scm-manager/ui-api"; +import { ConfigurationForm, InputField, Checkbox, validation } from "@scm-manager/ui-components"; +import { Title, useDocumentTitle } from "@scm-manager/ui-core"; +import { HalRepresentation } from "@scm-manager/ui-types"; type Props = { link: string; @@ -37,6 +38,7 @@ type Configuration = HalRepresentation & { const GitGlobalConfiguration: FC = ({ link }) => { const [t] = useTranslation("plugins"); + useDocumentTitle(t("scm-git-plugin.config.title")); const { initialConfiguration, isReadOnly, update, ...formProps } = useConfigLink(link); const { formState, handleSubmit, register, reset } = useForm({ mode: "onChange" }); @@ -45,7 +47,7 @@ const GitGlobalConfiguration: FC = ({ link }) => { if (initialConfiguration) { reset(initialConfiguration); } - }, [initialConfiguration]); + }, [initialConfiguration, reset]); const isValidDefaultBranch = (value: string) => { return validation.isBranchValid(value); @@ -58,7 +60,7 @@ const GitGlobalConfiguration: FC = ({ link }) => { onSubmit={handleSubmit(update)} {...formProps} > - + <Title>{t("scm-git-plugin.config.title")} { - render() { - const { link, t } = this.props; - return ( -
- - <Configuration link={link} render={(props: any) => <HgConfigurationForm {...props} />} /> - </div> - ); - } -} +const HgGlobalConfiguration: FC<Props> = ({ link }) => { + const [t] = useTranslation("plugins"); + useDocumentTitle(t("scm-hg-plugin.config.title")); -export default withTranslation("plugins")(HgGlobalConfiguration); + return ( + <div> + <Title>{t("scm-hg-plugin.config.title")} + } /> +
+ ); +}; + +export default HgGlobalConfiguration; diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.tsx b/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.tsx index e8571f9c72..f3c7fbae44 100644 --- a/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.tsx +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnGlobalConfiguration.tsx @@ -14,25 +14,26 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React from "react"; -import { WithTranslation, withTranslation } from "react-i18next"; -import { Title, Configuration } from "@scm-manager/ui-components"; +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; +import { Configuration } from "@scm-manager/ui-components"; +import { Title, useDocumentTitle } from "@scm-manager/ui-core"; import SvnConfigurationForm from "./SvnConfigurationForm"; -type Props = WithTranslation & { +type Props = { link: string; }; -class SvnGlobalConfiguration extends React.Component { - render() { - const { link, t } = this.props; - return ( -
- - <Configuration link={link} render={(props: any) => <SvnConfigurationForm {...props} />} /> - </div> - ); - } -} +const SvnGlobalConfiguration: FC<Props> = ({ link }) => { + const [t] = useTranslation("plugins"); + useDocumentTitle(t("scm-svn-plugin.config.title")); -export default withTranslation("plugins")(SvnGlobalConfiguration); + return ( + <div> + <Title>{t("scm-svn-plugin.config.title")} + } /> +
+ ); +}; + +export default SvnGlobalConfiguration; diff --git a/scm-ui/ui-components/src/ErrorBoundary.tsx b/scm-ui/ui-components/src/ErrorBoundary.tsx index bad10f2011..8fbc94474d 100644 --- a/scm-ui/ui-components/src/ErrorBoundary.tsx +++ b/scm-ui/ui-components/src/ErrorBoundary.tsx @@ -20,6 +20,7 @@ import { useTranslation } from "react-i18next"; import classNames from "classnames"; import styled from "styled-components"; import { MissingLinkError, urls, useIndexLink } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import ErrorNotification from "./ErrorNotification"; import ErrorPage from "./ErrorPage"; import { Subtitle, Title } from "./layout"; @@ -53,6 +54,7 @@ const RedirectIconContainer = styled.div` const RedirectPage = () => { const [t] = useTranslation("commons"); + useDocumentTitle(t("errorNotification.prefix")); // we use an icon instead of loading spinner, // because a redirect is synchron and a spinner does not spin on a synchron action return ( @@ -106,7 +108,7 @@ const ErrorDisplay: FC = ({ error, errorInfo, fallback: Fallb const fallbackProps = { error, - errorInfo + errorInfo, }; return ; @@ -128,7 +130,7 @@ class ErrorBoundary extends React.Component { componentDidCatch(error: Error, errorInfo: ErrorInfo) { this.setState({ error, - errorInfo + errorInfo, }); } diff --git a/scm-ui/ui-components/src/ErrorPage.tsx b/scm-ui/ui-components/src/ErrorPage.tsx index c15f7d38d1..f9b8dd1aeb 100644 --- a/scm-ui/ui-components/src/ErrorPage.tsx +++ b/scm-ui/ui-components/src/ErrorPage.tsx @@ -14,9 +14,11 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React from "react"; -import ErrorNotification from "./ErrorNotification"; +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; import { BackendError, ForbiddenError } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; +import ErrorNotification from "./ErrorNotification"; type Props = { error: Error; @@ -24,28 +26,26 @@ type Props = { subtitle: string; }; -class ErrorPage extends React.Component { - render() { - const { title, error } = this.props; +const ErrorPage: FC = ({ error, title, subtitle }) => { + const [t] = useTranslation("commons"); + useDocumentTitle(t("errorNotification.prefix")); - return ( -
-
-

{title}

- {this.renderSubtitle()} - -
-
- ); - } - - renderSubtitle = () => { - const { error, subtitle } = this.props; + const renderSubtitle = () => { if (error instanceof BackendError || error instanceof ForbiddenError) { return null; } return

{subtitle}

; }; -} + + return ( +
+
+

{title}

+ {renderSubtitle()} + +
+
+ ); +}; export default ErrorPage; diff --git a/scm-ui/ui-components/src/layout/Page.tsx b/scm-ui/ui-components/src/layout/Page.tsx index 63c1b9cdb6..69389cfde6 100644 --- a/scm-ui/ui-components/src/layout/Page.tsx +++ b/scm-ui/ui-components/src/layout/Page.tsx @@ -14,20 +14,19 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React, { ReactNode } from "react"; +import React, { FC, ReactNode } from "react"; import classNames from "classnames"; import styled from "styled-components"; import Loading from "./../Loading"; import ErrorNotification from "./../ErrorNotification"; +import ErrorBoundary from "../ErrorBoundary"; import Title from "./Title"; import Subtitle from "./Subtitle"; import PageActions from "./PageActions"; -import ErrorBoundary from "../ErrorBoundary"; type Props = { title?: ReactNode; - // Use the documentTitle to set the document title (browser title) whenever you want to set one - // and use something different than a string for the title property. + // [DEPRECATED] Use useDocumentTitle hook inside the component instead. documentTitle?: string; afterTitle?: ReactNode; subtitle?: ReactNode; @@ -44,43 +43,19 @@ const PageActionContainer = styled.div` } `; -export default class Page extends React.Component { - componentDidUpdate() { - const textualTitle = this.getTextualTitle(); - if (textualTitle && textualTitle !== document.title) { - document.title = textualTitle; - } - } - - render() { - const { error } = this.props; - return ( -
-
- {this.renderPageHeader()} - - - {this.renderContent()} - -
-
- ); - } - - isPageAction(node: any) { +const Page: FC = ({ title, afterTitle, subtitle, loading, error, showContentOnError, children }) => { + const isPageAction = (node: any) => { return ( node.displayName === PageActions.displayName || (node.type && node.type.displayName === PageActions.displayName) ); - } - - renderPageHeader() { - const { error, afterTitle, title, subtitle, children } = this.props; + }; + const renderPageHeader = () => { let pageActions = null; let pageActionsExists = false; - React.Children.forEach(children, child => { + React.Children.forEach(children, (child) => { if (child && !error) { - if (this.isPageAction(child)) { + if (isPageAction(child)) { pageActions = ( { } } }); + const underline = pageActionsExists ?
: null; if (title || subtitle) { @@ -107,9 +83,7 @@ export default class Page extends React.Component {
- - {this.getTitleComponent()} - + {title} {afterTitle}
{subtitle ? {subtitle} : null} @@ -121,11 +95,9 @@ export default class Page extends React.Component { ); } return null; - } - - renderContent() { - const { loading, children, showContentOnError, error } = this.props; + }; + const renderContent = () => { if (error && !showContentOnError) { return null; } @@ -134,33 +106,27 @@ export default class Page extends React.Component { } const content: ReactNode[] = []; - React.Children.forEach(children, child => { + React.Children.forEach(children, (child) => { if (child) { - if (!this.isPageAction(child)) { + if (!isPageAction(child)) { content.push(child); } } }); return content; - } - - getTextualTitle: () => string | undefined = () => { - const { title, documentTitle } = this.props; - if (documentTitle) { - return documentTitle; - } else if (typeof title === "string") { - return title; - } else { - return undefined; - } }; - getTitleComponent = () => { - const { title } = this.props; - if (title && typeof title !== "string") { - return title; - } else { - return undefined; - } - }; -} + return ( +
+
+ {renderPageHeader()} + + + {renderContent()} + +
+
+ ); +}; + +export default Page; diff --git a/scm-ui/ui-components/src/layout/Title.tsx b/scm-ui/ui-components/src/layout/Title.tsx index 96fb5dac14..cbbc039154 100644 --- a/scm-ui/ui-components/src/layout/Title.tsx +++ b/scm-ui/ui-components/src/layout/Title.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React, { FC, useEffect } from "react"; +import React, { FC } from "react"; import classNames from "classnames"; type Props = { @@ -28,17 +28,7 @@ type Props = { * @deprecated Please import the identical module from "@scm-manager/ui-core" */ -const Title: FC = ({ title, preventRefreshingPageTitle, customPageTitle, className, children }) => { - useEffect(() => { - if (!preventRefreshingPageTitle) { - if (customPageTitle) { - document.title = customPageTitle; - } else if (title) { - document.title = title; - } - } - }, [title, preventRefreshingPageTitle, customPageTitle]); - +const Title: FC = ({ title, className, children }) => { if (children) { return

{children}

; } else if (title) { diff --git a/scm-ui/ui-core/src/base/helpers/index.ts b/scm-ui/ui-core/src/base/helpers/index.ts index 5cc604faba..d10b604f84 100644 --- a/scm-ui/ui-core/src/base/helpers/index.ts +++ b/scm-ui/ui-core/src/base/helpers/index.ts @@ -15,4 +15,5 @@ */ export { default as useAriaId } from "./useAriaId"; +export { default as useDocumentTitle, useDocumentTitleForRepository } from "./useDocumentTitle"; export { createAttributesForTesting, isDevBuild } from "./devbuild"; diff --git a/scm-ui/ui-core/src/base/helpers/useDocumentTitle.test.ts b/scm-ui/ui-core/src/base/helpers/useDocumentTitle.test.ts new file mode 100644 index 0000000000..a2b139cfbb --- /dev/null +++ b/scm-ui/ui-core/src/base/helpers/useDocumentTitle.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2020 - present Cloudogu GmbH + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { renderHook } from "@testing-library/react-hooks"; +import { Repository } from "@scm-manager/ui-types"; +import { binder } from "@scm-manager/ui-extensions"; +import useDocumentTitle, { useDocumentTitleForRepository } from "./useDocumentTitle"; + +describe("useDocumentTitle", () => { + it("should set document title", () => { + renderHook(() => useDocumentTitle("Part1", "Part2")); + expect(document.title).toBe("Part1 - Part2 - SCM-Manager"); + }); + + it("should append title if extension is a string", () => { + binder.getExtension = () => ({ documentTitle: "myInstance" }); + renderHook(() => useDocumentTitle("Part1", "Part2")); + expect(document.title).toBe("Part1 - Part2 - SCM-Manager (myInstance)"); + }); + + it("should modify title if extension is a function", () => { + binder.getExtension = () => ({ documentTitle: (title: string) => `Modified: ${title}` }); + renderHook(() => useDocumentTitle("Part1", "Part2")); + expect(document.title).toBe("Modified: Part1 - Part2 - SCM-Manager"); + }); +}); + +describe("useDocumentTitleForRepository", () => { + const repository: Repository = { namespace: "namespace", name: "name" } as Repository; + + it("should set the document title for a repository", () => { + renderHook(() => useDocumentTitleForRepository(repository, "Part1", "Part2")); + expect(document.title).toBe("Part1 - Part2 - namespace/name - SCM-Manager"); + }); +}); diff --git a/scm-ui/ui-core/src/base/helpers/useDocumentTitle.ts b/scm-ui/ui-core/src/base/helpers/useDocumentTitle.ts new file mode 100644 index 0000000000..f117fd6480 --- /dev/null +++ b/scm-ui/ui-core/src/base/helpers/useDocumentTitle.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2020 - present Cloudogu GmbH + * + * This program is free software: you can redistribute it and/or modify it under + * the terms of the GNU Affero General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see https://www.gnu.org/licenses/. + */ + +import { useEffect } from "react"; +import { binder, extensionPoints } from "@scm-manager/ui-extensions"; +import { Repository } from "@scm-manager/ui-types"; + +/** + * Hook to set the document title. + * + * @param titleParts - An array of title parts to be joined. + * Title parts should be sorted with the highest specificity first. + */ +export default function useDocumentTitle(...titleParts: string[]) { + useEffect(() => { + const extension = binder.getExtension("document.title"); + let title = `${titleParts.join(" - ")} - SCM-Manager`; + if (extension) { + if (typeof extension.documentTitle === "string") { + title += ` (${extension.documentTitle})`; + } else if (typeof extension.documentTitle === "function") { + title = extension.documentTitle(title); + } + } + document.title = title; + }, [titleParts]); +} + +/** + * Hook to set the document title for a repository. + * + * @param repository - The repository for which the title should be set. + * @param titleParts - An array of title parts to be joined. + * Title parts should be sorted with the highest specificity first. + */ +export function useDocumentTitleForRepository(repository: Repository, ...titleParts: string[]) { + useDocumentTitle(...titleParts, repository.namespace + "/" + repository.name); +} diff --git a/scm-ui/ui-core/src/base/misc/Title.tsx b/scm-ui/ui-core/src/base/misc/Title.tsx index 8e63a06622..2ef907ddfa 100644 --- a/scm-ui/ui-core/src/base/misc/Title.tsx +++ b/scm-ui/ui-core/src/base/misc/Title.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React, { HTMLAttributes, useEffect } from "react"; +import React, { HTMLAttributes } from "react"; import classNames from "classnames"; type Props = { @@ -24,16 +24,6 @@ type Props = { }; const Title = React.forwardRef & Props>( ({ title, customPageTitle, preventRefreshingPageTitle, children, className, ...props }, ref) => { - useEffect(() => { - if (!preventRefreshingPageTitle) { - if (customPageTitle) { - document.title = customPageTitle; - } else if (title) { - document.title = title; - } - } - }, [title, preventRefreshingPageTitle, customPageTitle]); - if (children || title) { return (

diff --git a/scm-ui/ui-extensions/src/extensionPoints.tsx b/scm-ui/ui-extensions/src/extensionPoints.tsx index 0a7b47871a..dd5b329742 100644 --- a/scm-ui/ui-extensions/src/extensionPoints.tsx +++ b/scm-ui/ui-extensions/src/extensionPoints.tsx @@ -257,6 +257,11 @@ export type RepositoryOverviewListOptionsExtensionPoint = ExtensionPointDefiniti () => { pageSize?: number; showArchived?: boolean } >; +export type DocumentTitleExtensionPoint = ExtensionPointDefinition< + "document.title", + { documentTitle: string | ((originalTitle: string) => string) } +>; + // From docs export type AdminNavigation = RenderableExtensionPointDefinition<"admin.navigation", { links: Links; url: string }>; diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json index 04856ca764..bdbdd29659 100644 --- a/scm-ui/ui-webapp/public/locales/de/admin.json +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -114,12 +114,14 @@ "repositoryRole": { "navLink": "Berechtigungsrollen", "title": "Berechtigungsrollen", + "titleWithPage": "Berechtigungsrollen Seite {{page}} von {{total}}", "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Berechtigungsrollen Fehler", + "detailsTitle": "Berechtigungsrolle", "createSubtitle": "Berechtigungsrolle erstellen", "editSubtitle": "Berechtigungsrolle bearbeiten", "overview": { - "title": "Übersicht aller verfügbaren Berechtigungsrollen", + "subtitle": "Übersicht aller verfügbaren Berechtigungsrollen", "noPermissionRoles": "Keine Berechtigungsrollen gefunden.", "createButton": "Berechtigungsrolle erstellen" }, diff --git a/scm-ui/ui-webapp/public/locales/de/commons.json b/scm-ui/ui-webapp/public/locales/de/commons.json index d83b92e1ae..682b335d6e 100644 --- a/scm-ui/ui-webapp/public/locales/de/commons.json +++ b/scm-ui/ui-webapp/public/locales/de/commons.json @@ -67,6 +67,7 @@ "loading": "Lade Daten ..." }, "logout": { + "title": "Abmeldung", "error": { "title": "Abmeldung fehlgeschlagen", "subtitle": "Während der Abmeldung ist ein Fehler aufgetreten." @@ -135,6 +136,7 @@ "previous": "Zurück" }, "profile": { + "subtitle": "Information", "navigationLabel": "Profil", "informationNavLink": "Information", "changePasswordNavLink": "Passwort ändern", @@ -263,6 +265,7 @@ "ariaLabel": "Globale Suche", "placeholder": "Suche...", "title": "Suche", + "titleWithPage": "Suche Seite {{page}} von {{total}}", "subtitle": "{{type}} Ergebnisse", "subtitleWithContext": "{{type}} Ergebnisse für in \"{{context}}\"", "withQueryType": " mit {{queryType}}", diff --git a/scm-ui/ui-webapp/public/locales/de/groups.json b/scm-ui/ui-webapp/public/locales/de/groups.json index 3957e2df33..0aaa067e8e 100644 --- a/scm-ui/ui-webapp/public/locales/de/groups.json +++ b/scm-ui/ui-webapp/public/locales/de/groups.json @@ -16,6 +16,7 @@ "createButton": "Gruppe erstellen" }, "singleGroup": { + "settingsTitle": "Generelle Einstellungen", "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Gruppen Fehler", "menu": { diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index 4874624937..ed255214ce 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -34,6 +34,7 @@ "skipLfsHelpText": "Ist diese Option aktiviert, werden beim Import keine (potentiell vorhandenen) LFS-Dateien in den SCM-Manager geladen. Diese Option ist nur für Git Repositories relevant." }, "repositoryRoot": { + "settingsTitle": "Generelle Einstellungen", "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Repository Fehler", "menu": { @@ -56,6 +57,9 @@ }, "overview": { "title": "Repositories", + "titleWithPage": "Repositories Seite {{page}} von {{total}}", + "titleWithNamespace": "Repositories in {{namespace}}", + "titleWithNamespaceAndPage": "Repositories Seite {{page}} von {{total}} in {{namespace}}", "subtitle": "Übersicht aller verfügbaren Repositories", "noRepositories": "Keine Repositories gefunden.", "invalidNamespace": "Keine Repositories gefunden. Möglicherweise existiert der ausgewählte Namespace nicht.", @@ -172,7 +176,8 @@ "cancel": "Nein", "submit": "Ja" } - } + }, + "branchWithNamespaceName": "Branch {{branch}} in {{namespace}}/{{name}}" }, "compare": { "title": "Vergleiche Änderungen", @@ -183,6 +188,16 @@ "target": "Target", "with": "Vergleiche Änderungen mit...", "filter": "Auswahl filtern...", + "typeTitle": { + "b": "Branch", + "t": "Tag", + "r": "Revision" + }, + "type": { + "b": "Branch", + "t": "Tag", + "r": "Revision" + }, "emptyResult": "Es wurden keine dem Filter entsprechenden Ergebnisse gefunden.", "tabs": { "b": "Branches", @@ -197,7 +212,8 @@ "tabs": { "diff": "Diff", "commits": "Commits" - } + }, + "compareSourceAndTargetWithNamespaceName": "Vergleiche {{sourceType}} {{source}} mit {{targetType}} {{target}} in {{namespace}}/{{name}}" }, "tags": { "overview": { @@ -249,7 +265,8 @@ "cancel": "Nein", "submit": "Ja" } - } + }, + "tagWithNamespaceName": "Tag {{tag}} in {{namespace}}/{{name}}" }, "code": { "sources": "Sources", @@ -258,6 +275,11 @@ "noBranches": "Keine Sources für das Repository gefunden." }, "changesets": { + "commitsWithNamespaceName": "Commits in {{namespace}}/{{name}}", + "commitsWithPageAndNamespaceName": "Commits Seite {{page}} von {{total}} in {{namespace}}/{{name}}", + "commitsWithPageRevisionAndNamespaceName": "Commits Seite {{page}} von {{total}} auf {{revision}} in {{namespace}}/{{name}}", + "commitsWithRevisionAndNamespaceName": "Commits auf {{revision}} in {{namespace}}/{{name}}", + "idWithNamespaceName": "{{id}} in {{namespace}}/{{name}}", "errorTitle": "Fehler", "errorSubtitle": "Changesets konnten nicht abgerufen werden", "noChangesets": "Keine Changesets in diesem Branch gefunden. Die Commits könnten gelöscht worden sein.", @@ -420,7 +442,9 @@ "notBound": "Keine Erweiterung angebunden." }, "loadMore": "Laden", - "moreFilesAvailable": "Es werden nur die ersten {{count}} Dateien angezeigt. Es sind weitere Dateien vorhanden." + "moreFilesAvailable": "Es werden nur die ersten {{count}} Dateien angezeigt. Es sind weitere Dateien vorhanden.", + "pathWithRevisionAndNamespaceName": "{{path}} von {{revision}} in {{namespace}}/{{name}}", + "sourcesWithRevisionAndNamespaceName": "Sources von {{revision}} in {{namespace}}/{{name}}" }, "permission": { "title": "Berechtigungen", @@ -547,6 +571,13 @@ "started": "Die Reindizierung wurde erfolgreich gestartet. Dies ist eine asynchrone Operation und kann einige Zeit in Anspruch nehmen." }, "diff": { + "changes": { + "add": "added", + "delete": "deleted", + "modify": "modified", + "rename": "renamed", + "copy": "copied" + }, "jumpToSource": "Zur Quelldatei springen", "jumpToTarget": "Zur vorherigen Version der Datei springen", "sideBySide": "Zur zweispaltigen Ansicht wechseln", @@ -587,7 +618,8 @@ "notifications": { "queryToShort": "Tippe mindestens 2 Zeichen ein, um die Suche zu starten", "emptyResult": "Es wurden keine Ergebnisse für <0>{{query}} gefunden" - } + }, + "searchWithRevisionAndNamespaceName": "Suche auf {{revision}} in {{namespace}}/{{name}}" }, "shortcuts": { "info": "Wechsel zur Repository-Info", diff --git a/scm-ui/ui-webapp/public/locales/de/users.json b/scm-ui/ui-webapp/public/locales/de/users.json index 0f61709f3a..a55f413cf4 100644 --- a/scm-ui/ui-webapp/public/locales/de/users.json +++ b/scm-ui/ui-webapp/public/locales/de/users.json @@ -31,6 +31,7 @@ "createButton": "Benutzer erstellen" }, "singleUser": { + "settingsTitle": "Generelle Einstellungen", "errorTitle": "Fehler", "errorSubtitle": "Unbekannter Benutzer Fehler", "menu": { diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json index 5f6147e559..a0e3d8099b 100644 --- a/scm-ui/ui-webapp/public/locales/en/admin.json +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -114,12 +114,14 @@ "repositoryRole": { "navLink": "Permission Roles", "title": "Permission Roles", + "titleWithPage": "Permission Roles page {{page}} of {{total}}", "errorTitle": "Error", "errorSubtitle": "Unknown Permission Role Error", + "detailsTitle": "Permission Role", "createSubtitle": "Create Permission Role", "editSubtitle": "Edit Permission Role", "overview": { - "title": "Overview of all Permission Roles", + "subtitle": "Overview of all Permission Roles", "noPermissionRoles": "No Permission Roles found.", "createButton": "Create Permission Role" }, diff --git a/scm-ui/ui-webapp/public/locales/en/commons.json b/scm-ui/ui-webapp/public/locales/en/commons.json index fada7c04d8..429d0cf87e 100644 --- a/scm-ui/ui-webapp/public/locales/en/commons.json +++ b/scm-ui/ui-webapp/public/locales/en/commons.json @@ -68,6 +68,7 @@ "error": "Error" }, "logout": { + "title": "Logout", "error": { "title": "Logout Failed", "subtitle": "Something went wrong during logout" @@ -136,6 +137,7 @@ "previous": "Previous" }, "profile": { + "subtitle": "Information", "navigationLabel": "Profile", "informationNavLink": "Information", "changePasswordNavLink": "Change Password", @@ -264,6 +266,7 @@ "ariaLabel": "Global search", "placeholder": "Search...", "title": "Search", + "titleWithPage": "Search page {{page}} of {{total}}", "subtitle": "{{type}} results", "subtitleWithContext": "{{type}} results in \"{{context}}\"", "withQueryType": " with {{queryType}}", diff --git a/scm-ui/ui-webapp/public/locales/en/groups.json b/scm-ui/ui-webapp/public/locales/en/groups.json index b057c3a314..2e50fd4b2f 100644 --- a/scm-ui/ui-webapp/public/locales/en/groups.json +++ b/scm-ui/ui-webapp/public/locales/en/groups.json @@ -16,6 +16,7 @@ "createButton": "Create Group" }, "singleGroup": { + "settingsTitle": "General Settings", "errorTitle": "Error", "errorSubtitle": "Unknown group error", "menu": { diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index d468a65001..44b0df9e97 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -34,6 +34,7 @@ "skipLfsHelpText": "Check this if (potentially available) LFS files shall not be loaded into SCM-Manager during the import. This option is relevant only for git repositories." }, "repositoryRoot": { + "settingsTitle": "General Settings", "errorTitle": "Error", "errorSubtitle": "Unknown repository error", "menu": { @@ -56,6 +57,9 @@ }, "overview": { "title": "Repositories", + "titleWithPage": "Repositories page {{page}} of {{total}}", + "titleWithNamespace": "Repositories in {{namespace}}", + "titleWithNamespaceAndPage": "Repositories page {{page}} of {{total}} in {{namespace}}", "subtitle": "Overview of available repositories", "noRepositories": "No repositories found.", "invalidNamespace": "No repositories found. It's likely that the selected namespace does not exist.", @@ -172,7 +176,8 @@ "cancel": "No", "submit": "Yes" } - } + }, + "branchWithNamespaceName": "Branch {{branch}} in {{namespace}}/{{name}}" }, "compare": { "title": "Compare Changes", @@ -183,6 +188,16 @@ "target": "Target", "with": "Compare changes with...", "filter": "Filter selection...", + "typeTitle": { + "b": "Branch", + "t": "Tag", + "r": "Revision" + }, + "type": { + "b": "branch", + "t": "tag", + "r": "revision" + }, "emptyResult": "No results matching the filter were found.", "tabs": { "b": "Branches", @@ -197,7 +212,8 @@ "tabs": { "diff": "Diff", "commits": "Commits" - } + }, + "compareSourceAndTargetWithNamespaceName": "Compare {{sourceType}} {{source}} with {{targetType}} {{target}} in {{namespace}}/{{name}}" }, "tags": { "overview": { @@ -249,7 +265,8 @@ "cancel": "No", "submit": "Yes" } - } + }, + "tagWithNamespaceName": "Tag {{tag}} in {{namespace}}/{{name}}" }, "code": { "sources": "Sources", @@ -258,6 +275,11 @@ "noBranches": "No sources found for this repository." }, "changesets": { + "commitsWithNamespaceName": "Commits in {{namespace}}/{{name}}", + "commitsWithPageAndNamespaceName": "Commits page {{page}} of {{total}} in {{namespace}}/{{name}}", + "commitsWithPageRevisionAndNamespaceName": "Commits page {{page}} of {{total}} on {{revision}} in {{namespace}}/{{name}}", + "commitsWithRevisionAndNamespaceName": "Commits on {{revision}} in {{namespace}}/{{name}}", + "idWithNamespaceName": "{{id}} in {{namespace}}/{{name}}", "errorTitle": "Error", "errorSubtitle": "Could not fetch changesets", "noChangesets": "No changesets found for this branch. The commits could have been removed.", @@ -420,7 +442,9 @@ "notBound": "No extension bound." }, "loadMore": "Load", - "moreFilesAvailable": "These are just the first {{count}} files. There are more files available." + "moreFilesAvailable": "These are just the first {{count}} files. There are more files available.", + "pathWithRevisionAndNamespaceName": "{{path}} on {{revision}} in {{namespace}}/{{name}}", + "sourcesWithRevisionAndNamespaceName": "Sources on {{revision}} in {{namespace}}/{{name}}" }, "permission": { "title": "Permissions", @@ -523,6 +547,17 @@ "cancel": "No" } }, + "archive": { + "tooltip": "Read only. The archive cannot be changed." + }, + "exporting": { + "tooltip": "Read only. The repository is currently being exported." + }, + "healthCheckFailure": { + "tooltip": "This repository has health check failures. Click to get details.", + "title": "Health Check Failures", + "close": "Close" + }, "runHealthCheck": { "button": "Run Health Checks", "subtitle": "Health Checks", @@ -535,17 +570,6 @@ "description": "Deletes all existing search indices for this repository and recreates them from scratch. This may take a while.", "started": "Reindexing has been started successfully. This is an asynchronous operation and may take a while." }, - "archive": { - "tooltip": "Read only. The archive cannot be changed." - }, - "exporting": { - "tooltip": "Read only. The repository is currently being exported." - }, - "healthCheckFailure": { - "tooltip": "This repository has health check failures. Click to get details.", - "title": "Health Check Failures", - "close": "Close" - }, "diff": { "changes": { "add": "added", @@ -594,7 +618,8 @@ "notifications": { "queryToShort": "Type at least two characters to start the search", "emptyResult": "Nothing found for query <0>{{query}}" - } + }, + "searchWithRevisionAndNamespaceName": "Search on {{revision}} in {{namespace}}/{{name}}" }, "shortcuts": { "info": "Switch to repository info", diff --git a/scm-ui/ui-webapp/public/locales/en/users.json b/scm-ui/ui-webapp/public/locales/en/users.json index dda433e532..acf37078ea 100644 --- a/scm-ui/ui-webapp/public/locales/en/users.json +++ b/scm-ui/ui-webapp/public/locales/en/users.json @@ -31,6 +31,7 @@ "createButton": "Create User" }, "singleUser": { + "settingsTitle": "General Settings", "errorTitle": "Error", "errorSubtitle": "Unknown user error", "menu": { diff --git a/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx b/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx index 852fe63853..137cb2a5e8 100644 --- a/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx +++ b/scm-ui/ui-webapp/src/admin/containers/AdminDetails.tsx @@ -19,6 +19,7 @@ import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { devices, ErrorNotification, Image, Loading, Subtitle, Title } from "@scm-manager/ui-components"; import { useUpdateInfo, useVersion } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; const BoxShadowBox = styled.div` box-shadow: 0 2px 3px rgba(40, 177, 232, 0.1), 0 0 0 2px rgba(40, 177, 232, 0.2); @@ -41,6 +42,7 @@ const MobileWrapped = styled.article` const AdminDetails: FC = () => { const [t] = useTranslation("admin"); + useDocumentTitle(t("admin.info.title")); const version = useVersion(); const { data: updateInfo, error, isLoading } = useUpdateInfo(); @@ -64,7 +66,7 @@ const AdminDetails: FC = () => {

{t("admin.info.newRelease.title")}

{t("admin.info.newRelease.description", { - version: updateInfo?.latestVersion + version: updateInfo?.latestVersion, })}

diff --git a/scm-ui/ui-webapp/src/admin/containers/GlobalConfig.tsx b/scm-ui/ui-webapp/src/admin/containers/GlobalConfig.tsx index 062d4917c5..c26152db6c 100644 --- a/scm-ui/ui-webapp/src/admin/containers/GlobalConfig.tsx +++ b/scm-ui/ui-webapp/src/admin/containers/GlobalConfig.tsx @@ -20,6 +20,7 @@ import { Link } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, Title } from "@scm-manager/ui-components"; import ConfigForm from "../components/form/ConfigForm"; import { useConfig, useIndexLinks, useNamespaceStrategies, useUpdateConfig } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; const GlobalConfig: FC = () => { const indexLinks = useIndexLinks(); @@ -31,6 +32,7 @@ const GlobalConfig: FC = () => { isLoading: isLoadingNamespaceStrategies, } = useNamespaceStrategies(); const [t] = useTranslation("config"); + useDocumentTitle(t("config.title")); const error = configLoadingError || namespaceStrategiesLoadingError || updateError || undefined; const isLoading = isLoadingNamespaceStrategies || isLoadingConfig; const canUpdateConfig = !!(config && (config._links.update as Link).href); diff --git a/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx b/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx index 5614b37b71..40bc04660e 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx @@ -36,6 +36,7 @@ import CloudoguPlatformBanner from "../components/CloudoguPlatformBanner"; import PluginCenterAuthInfo from "../components/PluginCenterAuthInfo"; import styled from "styled-components"; import { Button } from "@scm-manager/ui-buttons"; +import { useDocumentTitle } from "@scm-manager/ui-core"; export enum PluginAction { INSTALL = "install", @@ -70,6 +71,7 @@ const StickyHeader = styled.div` const PluginsOverview: FC = ({ installed }) => { const [t] = useTranslation("admin"); + useDocumentTitle(installed ? t("plugins.installedSubtitle") : t("plugins.availableSubtitle")); const { data: availablePlugins, isLoading: isLoadingAvailablePlugins, diff --git a/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx b/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx index 0e5c18ac19..63d862f21d 100644 --- a/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx +++ b/scm-ui/ui-webapp/src/admin/roles/components/PermissionRoleDetails.tsx @@ -14,49 +14,54 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React from "react"; -import { WithTranslation, withTranslation } from "react-i18next"; +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { RepositoryRole } from "@scm-manager/ui-types"; -import { Button, Level } from "@scm-manager/ui-components"; +import { Level, LinkButton, Title, useDocumentTitle } from "@scm-manager/ui-core"; import PermissionRoleDetailsTable from "./PermissionRoleDetailsTable"; -type Props = WithTranslation & { +type Props = { role: RepositoryRole; url: string; }; -class PermissionRoleDetails extends React.Component { - renderEditButton() { - const { t, url } = this.props; - if (!!this.props.role._links.update) { +const PermissionRoleDetails: FC = ({ role, url }) => { + const [t] = useTranslation("admin"); + useDocumentTitle(t("repositoryRole.detailsTitle")); + + const renderEditButton = () => { + if (!!role._links.update) { return ( <>
- } /> + + {t("repositoryRole.editButton")} + + } + /> ); } return null; - } + }; - render() { - const { role } = this.props; + return ( + <> + {t("repositoryRole.detailsTitle")} + + {renderEditButton()} + + name="repositoryRole.role-details.information" + renderAll={true} + props={{ + role, + }} + /> + + ); +}; - return ( - <> - - {this.renderEditButton()} - - name="repositoryRole.role-details.information" - renderAll={true} - props={{ - role - }} - /> - - ); - } -} - -export default withTranslation("admin")(PermissionRoleDetails); +export default PermissionRoleDetails; diff --git a/scm-ui/ui-webapp/src/admin/roles/containers/CreateRepositoryRole.tsx b/scm-ui/ui-webapp/src/admin/roles/containers/CreateRepositoryRole.tsx index 8a2f3a95f1..72c1939136 100644 --- a/scm-ui/ui-webapp/src/admin/roles/containers/CreateRepositoryRole.tsx +++ b/scm-ui/ui-webapp/src/admin/roles/containers/CreateRepositoryRole.tsx @@ -20,9 +20,11 @@ import { ErrorNotification, Loading, Subtitle, Title } from "@scm-manager/ui-com import RepositoryRoleForm from "./RepositoryRoleForm"; import { useCreateRepositoryRole } from "@scm-manager/ui-api"; import { Redirect } from "react-router-dom"; +import { useDocumentTitle } from "@scm-manager/ui-core"; const CreateRepositoryRole: FC = () => { const [t] = useTranslation("admin"); + useDocumentTitle(t("repositoryRole.createSubtitle")); const { error, isLoading: loading, create, repositoryRole: created } = useCreateRepositoryRole(); if (created) { diff --git a/scm-ui/ui-webapp/src/admin/roles/containers/EditRepositoryRole.tsx b/scm-ui/ui-webapp/src/admin/roles/containers/EditRepositoryRole.tsx index 484864cc1d..5e8a87d45c 100644 --- a/scm-ui/ui-webapp/src/admin/roles/containers/EditRepositoryRole.tsx +++ b/scm-ui/ui-webapp/src/admin/roles/containers/EditRepositoryRole.tsx @@ -17,11 +17,12 @@ import React, { FC } from "react"; import RepositoryRoleForm from "./RepositoryRoleForm"; import { useTranslation } from "react-i18next"; -import { ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components"; +import { ErrorNotification, Loading, Subtitle, Title } from "@scm-manager/ui-components"; import { RepositoryRole } from "@scm-manager/ui-types"; import DeleteRepositoryRole from "./DeleteRepositoryRole"; import { useUpdateRepositoryRole } from "@scm-manager/ui-api"; import { Redirect } from "react-router-dom"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Props = { role: RepositoryRole; @@ -29,6 +30,7 @@ type Props = { const EditRepositoryRole: FC = ({ role }) => { const [t] = useTranslation("admin"); + useDocumentTitle(t("repositoryRole.editSubtitle")); const { isUpdated, update, error, isLoading: loading } = useUpdateRepositoryRole(); if (isUpdated) { @@ -43,6 +45,7 @@ const EditRepositoryRole: FC = ({ role }) => { return ( <> + <Subtitle subtitle={t("repositoryRole.editSubtitle")} /> <RepositoryRoleForm role={role} submitForm={update} /> <DeleteRepositoryRole role={role} /> diff --git a/scm-ui/ui-webapp/src/admin/roles/containers/RepositoryRoles.tsx b/scm-ui/ui-webapp/src/admin/roles/containers/RepositoryRoles.tsx index b771abc878..b1b9db0a51 100644 --- a/scm-ui/ui-webapp/src/admin/roles/containers/RepositoryRoles.tsx +++ b/scm-ui/ui-webapp/src/admin/roles/containers/RepositoryRoles.tsx @@ -30,6 +30,7 @@ import { } from "@scm-manager/ui-components"; import PermissionRoleTable from "../components/PermissionRoleTable"; import { useRepositoryRoles } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type RepositoryRolesPageProps = { data?: RepositoryRoleCollection; @@ -62,6 +63,11 @@ const RepositoryRoles: FC<Props> = ({ baseUrl }) => { const page = urls.getPageFromMatch({ params }); const { isLoading: loading, error, data } = useRepositoryRoles({ page: page - 1 }); const [t] = useTranslation("admin"); + useDocumentTitle( + data?.pageTotal && data.pageTotal > 1 && page + ? t("repositoryRole.titleWithPage", { page, total: data.pageTotal }) + : t("repositoryRole.title") + ); const canAddRoles = !!data?._links.create; if (error) { @@ -79,7 +85,7 @@ const RepositoryRoles: FC<Props> = ({ baseUrl }) => { return ( <> <Title title={t("repositoryRole.title")} /> - <Subtitle subtitle={t("repositoryRole.overview.title")} /> + <Subtitle subtitle={t("repositoryRole.overview.subtitle")} /> <RepositoryRolesPage data={data} page={page} baseUrl={baseUrl} /> {canAddRoles ? ( <CreateButton label={t("repositoryRole.overview.createButton")} link={`${baseUrl}/create`} /> diff --git a/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx b/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx index f5621d694e..47898e936a 100644 --- a/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx +++ b/scm-ui/ui-webapp/src/admin/roles/containers/SingleRepositoryRole.tsx @@ -44,12 +44,11 @@ const SingleRepositoryRole: FC = () => { const extensionProps = { role, - url: escapedUrl + url: escapedUrl, }; return ( <> - <Title title={t("repositoryRole.title")} /> <Route path={`${escapedUrl}/info`}> <PermissionRoleDetail role={role} url={url} /> </Route> diff --git a/scm-ui/ui-webapp/src/components/LoginInfo.tsx b/scm-ui/ui-webapp/src/components/LoginInfo.tsx index cb6d13abdf..2ac39dfaca 100644 --- a/scm-ui/ui-webapp/src/components/LoginInfo.tsx +++ b/scm-ui/ui-webapp/src/components/LoginInfo.tsx @@ -15,13 +15,14 @@ */ import React, { FC } from "react"; -import InfoBox from "./InfoBox"; -import LoginForm from "./LoginForm"; -import { Image, Loading } from "@scm-manager/ui-components"; -import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; +import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { useLoginInfo } from "@scm-manager/ui-api"; -import { useTranslation } from "react-i18next"; +import { Image, Loading } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; +import InfoBox from "./InfoBox"; +import LoginForm from "./LoginForm"; const TopMarginBox = styled.div` margin-top: 5rem; @@ -57,6 +58,7 @@ type Props = { const LoginInfo: FC<Props> = (props) => { const { isLoading: isLoadingLoginInfo, data: info } = useLoginInfo(); const [t] = useTranslation("commons"); + useDocumentTitle(t("login.title")); if (isLoadingLoginInfo) { return <Loading />; diff --git a/scm-ui/ui-webapp/src/containers/Accessibility.tsx b/scm-ui/ui-webapp/src/containers/Accessibility.tsx index e9f774c9be..f49d630197 100644 --- a/scm-ui/ui-webapp/src/containers/Accessibility.tsx +++ b/scm-ui/ui-webapp/src/containers/Accessibility.tsx @@ -15,13 +15,20 @@ */ import React, { FC } from "react"; -import { ButtonGroup, Checkbox, SubmitButton, Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; import { useForm } from "react-hook-form"; +import { Me } from "@scm-manager/ui-types"; +import { useDocumentTitle } from "@scm-manager/ui-core"; +import { ButtonGroup, Checkbox, SubmitButton, Subtitle } from "@scm-manager/ui-components"; import { AccessibilityConfig, useAccessibilityConfig } from "../accessibilityConfig"; -const Accessibility: FC = () => { +type Props = { + me: Me; +}; + +const Accessibility: FC<Props> = ({ me }) => { const [t] = useTranslation("commons"); + useDocumentTitle(t("profile.accessibility.subtitle"), me.displayName); const { value: accessibilityConfig, setValue: setAccessibilityConfig, isLoading } = useAccessibilityConfig(); const { register, diff --git a/scm-ui/ui-webapp/src/containers/ChangeUserPassword.tsx b/scm-ui/ui-webapp/src/containers/ChangeUserPassword.tsx index 361fa88244..e1f01570e1 100644 --- a/scm-ui/ui-webapp/src/containers/ChangeUserPassword.tsx +++ b/scm-ui/ui-webapp/src/containers/ChangeUserPassword.tsx @@ -15,6 +15,7 @@ */ import React, { FC, FormEvent, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { ErrorNotification, InputField, @@ -22,9 +23,9 @@ import { Notification, PasswordConfirmation, SubmitButton, - Subtitle + Subtitle, } from "@scm-manager/ui-components"; -import { useTranslation } from "react-i18next"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import { Me } from "@scm-manager/ui-types"; import { useChangeUserPassword } from "@scm-manager/ui-api"; @@ -34,6 +35,7 @@ type Props = { const ChangeUserPassword: FC<Props> = ({ me }) => { const [t] = useTranslation("commons"); + useDocumentTitle(t("password.subtitle"), me.displayName); const { isLoading, error, passwordChanged, changePassword, reset } = useChangeUserPassword(me); const [oldPassword, setOldPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); diff --git a/scm-ui/ui-webapp/src/containers/ExternalError.tsx b/scm-ui/ui-webapp/src/containers/ExternalError.tsx index ff0957a268..1d015f37ad 100644 --- a/scm-ui/ui-webapp/src/containers/ExternalError.tsx +++ b/scm-ui/ui-webapp/src/containers/ExternalError.tsx @@ -18,6 +18,7 @@ import React from "react"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Notification, Page } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type LocationState = { code: string; @@ -26,6 +27,7 @@ type LocationState = { const ExternalError = () => { const { code } = useParams<LocationState>(); const [t] = useTranslation(["commons", "plugins"]); + useDocumentTitle(`${t("app.error.title")}: ${t(`plugins:errors.${code}.displayName`)}`); return ( <Page title={t("app.error.title")} subtitle={t(`plugins:errors.${code}.displayName`)}> diff --git a/scm-ui/ui-webapp/src/containers/Index.tsx b/scm-ui/ui-webapp/src/containers/Index.tsx index a54ac66f47..15e657b660 100644 --- a/scm-ui/ui-webapp/src/containers/Index.tsx +++ b/scm-ui/ui-webapp/src/containers/Index.tsx @@ -16,7 +16,8 @@ import React, { FC, useState } from "react"; import App from "./App"; -import { ErrorBoundary, Header, Loading } from "@scm-manager/ui-components"; +import { ErrorBoundary, Header } from "@scm-manager/ui-components"; +import { Loading } from "@scm-manager/ui-core"; import PluginLoader from "./PluginLoader"; import ScrollToTop from "./ScrollToTop"; import IndexErrorPage from "./IndexErrorPage"; diff --git a/scm-ui/ui-webapp/src/containers/Logout.tsx b/scm-ui/ui-webapp/src/containers/Logout.tsx index 99c69425cc..0ce0f9e9ca 100644 --- a/scm-ui/ui-webapp/src/containers/Logout.tsx +++ b/scm-ui/ui-webapp/src/containers/Logout.tsx @@ -15,15 +15,17 @@ */ import React, { FC, useEffect } from "react"; -import { useTranslation } from "react-i18next"; - -import { ErrorPage, Loading } from "@scm-manager/ui-components"; import { Redirect } from "react-router-dom"; +import { useTranslation } from "react-i18next"; import { useLogout } from "@scm-manager/ui-api"; +import { ErrorPage, Loading } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; const Logout: FC = () => { const { error, logout } = useLogout(); const [t] = useTranslation("commons"); + useDocumentTitle(t("logout.title")); + useEffect(() => { if (logout) { logout(); diff --git a/scm-ui/ui-webapp/src/containers/Profile.tsx b/scm-ui/ui-webapp/src/containers/Profile.tsx index b743182424..38606ed5f2 100644 --- a/scm-ui/ui-webapp/src/containers/Profile.tsx +++ b/scm-ui/ui-webapp/src/containers/Profile.tsx @@ -75,10 +75,10 @@ const Profile: FC = () => { <ProfileInfo me={me} /> </Route> <Route path={`${url}/settings/theme`} exact> - <Theme /> + <Theme me={me} /> </Route> <Route path={`${url}/settings/accessibility`} exact> - <Accessibility /> + <Accessibility me={me} /> </Route> {mayChangePassword && ( <Route path={`${url}/settings/password`}> diff --git a/scm-ui/ui-webapp/src/containers/ProfileInfo.tsx b/scm-ui/ui-webapp/src/containers/ProfileInfo.tsx index 8d455e3895..8aecf8f10a 100644 --- a/scm-ui/ui-webapp/src/containers/ProfileInfo.tsx +++ b/scm-ui/ui-webapp/src/containers/ProfileInfo.tsx @@ -22,8 +22,9 @@ import { AvatarWrapper, createAttributesForTesting, InfoTable, - MailLink + MailLink, } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Props = { me: Me; @@ -31,6 +32,7 @@ type Props = { const ProfileInfo: FC<Props> = ({ me }) => { const [t] = useTranslation("commons"); + useDocumentTitle(t("profile.subtitle"), me.displayName); const renderGroups = () => { let groups = null; if (me.groups.length > 0) { @@ -39,7 +41,7 @@ const ProfileInfo: FC<Props> = ({ me }) => { <th>{t("profile.groups")}</th> <td className="p-0"> <ul> - {me.groups.map(group => { + {me.groups.map((group) => { return <li>{group}</li>; })} </ul> diff --git a/scm-ui/ui-webapp/src/containers/Theme.tsx b/scm-ui/ui-webapp/src/containers/Theme.tsx index bda0ba8aa6..fbdcb3ddb1 100644 --- a/scm-ui/ui-webapp/src/containers/Theme.tsx +++ b/scm-ui/ui-webapp/src/containers/Theme.tsx @@ -20,6 +20,8 @@ import { useForm } from "react-hook-form"; import styled from "styled-components"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; +import { Me } from "@scm-manager/ui-types"; +import { useDocumentTitle } from "@scm-manager/ui-core"; const LS_KEY = "scm.theme"; @@ -36,6 +38,10 @@ export const useThemeState = () => { return { theme, setTheme, isLoading }; }; +type Props = { + me: Me; +}; + type ThemeForm = { theme: string; }; @@ -47,21 +53,22 @@ const RadioColumn = styled.div` width: 2rem; `; -const Theme: FC = () => { +const Theme: FC<Props> = ({ me }) => { const { theme, setTheme, isLoading } = useThemeState(); const { register, setValue, handleSubmit, formState: { isDirty }, - watch + watch, } = useForm<ThemeForm>({ mode: "onChange", defaultValues: { - theme - } + theme, + }, }); const [t] = useTranslation("commons"); + useDocumentTitle(t("profile.theme.subtitle"), me.displayName); const onSubmit = (values: ThemeForm) => { setTheme(values.theme); @@ -71,7 +78,7 @@ const Theme: FC = () => { <> <Subtitle>{t("profile.theme.subtitle")}</Subtitle> <form className="is-flex is-flex-direction-column" onSubmit={handleSubmit(onSubmit)}> - {themes.map(theme => { + {themes.map((theme) => { const a11yId = createA11yId("theme"); return ( <div diff --git a/scm-ui/ui-webapp/src/groups/components/table/Details.tsx b/scm-ui/ui-webapp/src/groups/components/table/Details.tsx index 276a11d0f6..66aa69c19a 100644 --- a/scm-ui/ui-webapp/src/groups/components/table/Details.tsx +++ b/scm-ui/ui-webapp/src/groups/components/table/Details.tsx @@ -14,63 +14,23 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React from "react"; -import { WithTranslation, withTranslation } from "react-i18next"; +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; import { Group } from "@scm-manager/ui-types"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import { Checkbox, DateFromNow, InfoTable } from "@scm-manager/ui-components"; import GroupMember from "./GroupMember"; import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; -type Props = WithTranslation & { +type Props = { group: Group; }; -class Details extends React.Component<Props> { - render() { - const { group, t } = this.props; - return ( - <InfoTable className="content"> - <tbody> - <tr> - <th>{t("group.name")}</th> - <td>{group.name}</td> - </tr> - <tr> - <th>{t("group.description")}</th> - <td>{group.description}</td> - </tr> - <tr> - <th>{t("group.external")}</th> - <td> - <Checkbox checked={group.external} readOnly={true} /> - </td> - </tr> - <tr> - <th>{t("group.type")}</th> - <td>{group.type}</td> - </tr> - <tr> - <th>{t("group.creationDate")}</th> - <td> - <DateFromNow date={group.creationDate} /> - </td> - </tr> - <tr> - <th>{t("group.lastModified")}</th> - <td> - <DateFromNow date={group.lastModified} /> - </td> - </tr> - {this.renderMembers()} - <ExtensionPoint<extensionPoints.GroupInformationTableBottom> name="group.information.table.bottom" props={{group}} renderAll={true} /> - </tbody> - </InfoTable> - ); - } - - renderMembers() { - const { group, t } = this.props; +const Details: FC<Props> = ({ group }) => { + const [t] = useTranslation("groups"); + useDocumentTitle(t("singleGroup.menu.informationNavLink"), group.name); + const renderMembers = () => { let member = null; if (group.members.length > 0) { member = ( @@ -87,7 +47,50 @@ class Details extends React.Component<Props> { ); } return member; - } -} + }; -export default withTranslation("groups")(Details); + return ( + <InfoTable className="content"> + <tbody> + <tr> + <th>{t("group.name")}</th> + <td>{group.name}</td> + </tr> + <tr> + <th>{t("group.description")}</th> + <td>{group.description}</td> + </tr> + <tr> + <th>{t("group.external")}</th> + <td> + <Checkbox checked={group.external} readOnly={true} /> + </td> + </tr> + <tr> + <th>{t("group.type")}</th> + <td>{group.type}</td> + </tr> + <tr> + <th>{t("group.creationDate")}</th> + <td> + <DateFromNow date={group.creationDate} /> + </td> + </tr> + <tr> + <th>{t("group.lastModified")}</th> + <td> + <DateFromNow date={group.lastModified} /> + </td> + </tr> + {renderMembers()} + <ExtensionPoint<extensionPoints.GroupInformationTableBottom> + name="group.information.table.bottom" + props={{ group }} + renderAll={true} + /> + </tbody> + </InfoTable> + ); +}; + +export default Details; diff --git a/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx index 01c574d888..4ad7ec60aa 100644 --- a/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/CreateGroup.tsx @@ -18,11 +18,13 @@ import React, { FC } from "react"; import { Redirect, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useCreateGroup, urls } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import { Page } from "@scm-manager/ui-components"; import GroupForm from "../components/GroupForm"; const CreateGroup: FC = () => { const [t] = useTranslation("groups"); + useDocumentTitle(t("addGroup.title")); const { isLoading, create, error, group } = useCreateGroup(); const location = useLocation(); diff --git a/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx b/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx index 266bcd036a..6f28e74e1f 100644 --- a/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/EditGroup.tsx @@ -15,18 +15,22 @@ */ import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; import { Group } from "@scm-manager/ui-types"; -import { useUpdateGroup, useUserSuggestions } from "@scm-manager/ui-api"; +import { useUpdateGroup } from "@scm-manager/ui-api"; import { ErrorNotification } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; +import UpdateNotification from "../../components/UpdateNotification"; import GroupForm from "../components/GroupForm"; import DeleteGroup from "./DeleteGroup"; -import UpdateNotification from "../../components/UpdateNotification"; type Props = { group: Group; }; const EditGroup: FC<Props> = ({ group }) => { + const [t] = useTranslation("groups"); + useDocumentTitle(t("singleGroup.settingsTitle"), group.name); const { error, isLoading, update, isUpdated } = useUpdateGroup(); return ( diff --git a/scm-ui/ui-webapp/src/groups/containers/Groups.tsx b/scm-ui/ui-webapp/src/groups/containers/Groups.tsx index a3f69c5991..22a0be1e10 100644 --- a/scm-ui/ui-webapp/src/groups/containers/Groups.tsx +++ b/scm-ui/ui-webapp/src/groups/containers/Groups.tsx @@ -17,8 +17,9 @@ import React, { FC } from "react"; import { Redirect, useLocation, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useGroups } from "@scm-manager/ui-api"; import { Group, GroupCollection } from "@scm-manager/ui-types"; +import { useGroups } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import { CreateButton, LinkPaginator, @@ -59,6 +60,7 @@ const Groups: FC = () => { const page = urls.getPageFromMatch({ params }); const { isLoading, error, data } = useGroups({ search, page: page - 1 }); const [t] = useTranslation("groups"); + useDocumentTitle(t("groups.title")); const groups = data?._embedded?.groups; const canCreateGroups = !!data?._links.create; if (data && data.pageTotal < page && page > 1) { diff --git a/scm-ui/ui-webapp/src/permissions/components/SetGroupPermissions.tsx b/scm-ui/ui-webapp/src/permissions/components/SetGroupPermissions.tsx index 4e7d158315..1c74973532 100644 --- a/scm-ui/ui-webapp/src/permissions/components/SetGroupPermissions.tsx +++ b/scm-ui/ui-webapp/src/permissions/components/SetGroupPermissions.tsx @@ -14,24 +14,30 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { Group } from "@scm-manager/ui-types"; import React, { FC } from "react"; -import SetPermissions from "./SetPermissions"; +import { useTranslation } from "react-i18next"; +import { Group } from "@scm-manager/ui-types"; import { useGroupPermissions, useSetGroupPermissions } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; +import SetPermissions from "./SetPermissions"; type Props = { group: Group; }; const SetGroupPermissions: FC<Props> = ({ group }) => { - const { data: selectedPermissions, isLoading: loadingPermissions, error: permissionsLoadError } = useGroupPermissions( - group - ); + const [t] = useTranslation("groups"); + useDocumentTitle(t("singleGroup.menu.setPermissionsNavLink"), group.name); + const { + data: selectedPermissions, + isLoading: loadingPermissions, + error: permissionsLoadError, + } = useGroupPermissions(group); const { isLoading: isUpdatingPermissions, isUpdated: permissionsUpdated, setPermissions, - error: permissionsUpdateError + error: permissionsUpdateError, } = useSetGroupPermissions(group, selectedPermissions); return ( <SetPermissions diff --git a/scm-ui/ui-webapp/src/permissions/components/SetUserPermissions.tsx b/scm-ui/ui-webapp/src/permissions/components/SetUserPermissions.tsx index 04993295a0..55583cdb68 100644 --- a/scm-ui/ui-webapp/src/permissions/components/SetUserPermissions.tsx +++ b/scm-ui/ui-webapp/src/permissions/components/SetUserPermissions.tsx @@ -16,22 +16,28 @@ import { User } from "@scm-manager/ui-types"; import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; import SetPermissions from "./SetPermissions"; import { useSetUserPermissions, useUserPermissions } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Props = { user: User; }; const SetUserPermissions: FC<Props> = ({ user }) => { - const { data: selectedPermissions, isLoading: loadingPermissions, error: permissionsLoadError } = useUserPermissions( - user - ); + const [t] = useTranslation("users"); + useDocumentTitle(t("singleUser.menu.setPermissionsNavLink"), user.displayName); + const { + data: selectedPermissions, + isLoading: loadingPermissions, + error: permissionsLoadError, + } = useUserPermissions(user); const { isLoading: isUpdatingPermissions, isUpdated: permissionsUpdated, setPermissions, - error: permissionsUpdateError + error: permissionsUpdateError, } = useSetUserPermissions(user, selectedPermissions); return ( <SetPermissions diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx index 2918d83f3c..c8944c220f 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchDetail.tsx @@ -18,7 +18,8 @@ import React, { FC } from "react"; import { useTranslation } from "react-i18next"; import classNames from "classnames"; import { Branch, Repository } from "@scm-manager/ui-types"; -import {SmallLoadingSpinner, Subtitle, useGeneratedId} from "@scm-manager/ui-components"; +import { SmallLoadingSpinner, Subtitle, useGeneratedId } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import BranchButtonGroup from "./BranchButtonGroup"; import DefaultBranchTag from "./DefaultBranchTag"; import AheadBehindTag from "./AheadBehindTag"; @@ -33,6 +34,13 @@ type Props = { const BranchDetail: FC<Props> = ({ repository, branch }) => { const [t] = useTranslation("repos"); + useDocumentTitle( + t("branch.branchWithNamespaceName", { + branch: branch.name, + namespace: repository.namespace, + name: repository.name, + }) + ); const { data, isLoading } = useBranchDetails(repository, branch); const labelId = useGeneratedId(); let aheadBehind; @@ -70,7 +78,10 @@ const BranchDetail: FC<Props> = ({ repository, branch }) => { <BranchButtonGroup repository={repository} branch={branch} /> </div> </div> - <span id={labelId} className="is-size-7 has-text-secondary">{t("branch.aheadBehind.label")}</span>{aheadBehind} + <span id={labelId} className="is-size-7 has-text-secondary"> + {t("branch.aheadBehind.label")} + </span> + {aheadBehind} </> ); }; diff --git a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx index 2a01dfa6b6..a0d220abee 100644 --- a/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/components/BranchView.tsx @@ -14,7 +14,7 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React from "react"; +import React, { FC } from "react"; import BranchDetail from "./BranchDetail"; import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; import { Branch, Repository } from "@scm-manager/ui-types"; @@ -25,27 +25,24 @@ type Props = { branch: Branch; }; -class BranchView extends React.Component<Props> { - render() { - const { repository, branch } = this.props; - return ( - <> - <BranchDetail repository={repository} branch={branch} /> - <hr /> - <div className="content"> - <ExtensionPoint<extensionPoints.ReposBranchDetailsInformation> - name="repos.branch-details.information" - renderAll={true} - props={{ - repository, - branch - }} - /> - </div> - <BranchDangerZone repository={repository} branch={branch} /> - </> - ); - } -} +const BranchView: FC<Props> = ({ repository, branch }) => { + return ( + <> + <BranchDetail repository={repository} branch={branch} /> + <hr /> + <div className="content"> + <ExtensionPoint<extensionPoints.ReposBranchDetailsInformation> + name="repos.branch-details.information" + renderAll={true} + props={{ + repository, + branch, + }} + /> + </div> + <BranchDangerZone repository={repository} branch={branch} /> + </> + ); +}; export default BranchView; diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx index 411378e63e..d309456843 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchesOverview.tsx @@ -15,8 +15,10 @@ */ import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; import { Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading } from "@scm-manager/ui-components"; +import { useDocumentTitleForRepository } from "@scm-manager/ui-core"; import { useBranches } from "@scm-manager/ui-api"; import BranchTableWrapper from "./BranchTableWrapper"; @@ -27,6 +29,8 @@ type Props = { const BranchesOverview: FC<Props> = ({ repository, baseUrl }) => { const { isLoading, error, data } = useBranches(repository); + const [t] = useTranslation("repos"); + useDocumentTitleForRepository(repository, t("branches.overview.title")); if (error) { return <ErrorNotification error={error} />; diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx index d00c154be0..6657f3a946 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/CreateBranch.tsx @@ -19,10 +19,11 @@ import { Redirect, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import queryString from "query-string"; import { Repository } from "@scm-manager/ui-types"; -import { ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components"; -import BranchForm from "../components/BranchForm"; import { useBranches, useCreateBranch } from "@scm-manager/ui-api"; +import { ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components"; +import { useDocumentTitleForRepository } from "@scm-manager/ui-core"; import { encodePart } from "../../sources/components/content/FileLink"; +import BranchForm from "../components/BranchForm"; type Props = { repository: Repository; @@ -33,6 +34,7 @@ const CreateBranch: FC<Props> = ({ repository }) => { const { isLoading: isLoadingList, error: errorList, data: branches } = useBranches(repository); const location = useLocation(); const [t] = useTranslation("repos"); + useDocumentTitleForRepository(repository, t("branches.create.title")); const transmittedName = (url: string): string | undefined => { const paramsName = queryString.parse(url).name; @@ -48,7 +50,9 @@ const CreateBranch: FC<Props> = ({ repository }) => { if (createdBranch) { return ( <Redirect - to={`/repo/${repository.namespace}/${repository.name}/branch/${encodeURIComponent(encodePart(createdBranch.name))}/info`} + to={`/repo/${repository.namespace}/${repository.name}/branch/${encodeURIComponent( + encodePart(createdBranch.name) + )}/info`} /> ); } diff --git a/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx b/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx index 3ab22de1f6..1863a963f6 100644 --- a/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx +++ b/scm-ui/ui-webapp/src/repos/codeSection/containers/FileSearch.tsx @@ -22,6 +22,7 @@ import styled from "styled-components"; import { Branch, Repository } from "@scm-manager/ui-types"; import { urls, usePaths } from "@scm-manager/ui-api"; import { createA11yId, ErrorNotification, FilterInput, Help, Icon, Loading } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import CodeActionBar from "../components/CodeActionBar"; import FileSearchResults from "../components/FileSearchResults"; import { filepathSearch } from "../utils/filepathSearch"; @@ -60,7 +61,14 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch } const query = urls.getQueryStringFromLocation(location) || ""; const prevSourcePath = urls.getPrevSourcePathFromLocation(location) || ""; const [t] = useTranslation("repos"); - const [firstSelectedBranch, setBranchChanged] = useState<string | undefined>(selectedBranch); + useDocumentTitle( + t("fileSearch.searchWithRevisionAndNamespaceName", { + revision: decodeURIComponent(revision), + namespace: repository.namespace, + name: repository.name, + }) + ); + const [firstSelectedBranch] = useState<string | undefined>(selectedBranch); useEffect(() => { if (query.length > 1 && data) { diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx index 2936c08002..5ff7280b44 100644 --- a/scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx +++ b/scm-ui/ui-webapp/src/repos/compare/CompareSelectBar.tsx @@ -20,6 +20,7 @@ import { useTranslation } from "react-i18next"; import styled from "styled-components"; import { Repository } from "@scm-manager/ui-types"; import { devices, Icon } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import CompareSelector from "./CompareSelector"; import { CompareBranchesParams } from "./CompareView"; @@ -59,12 +60,22 @@ const CompareSelectBar: FC<Props> = ({ repository, baseUrl }) => { const history = useHistory(); const [source, setSource] = useState<CompareProps>({ type: params?.sourceType, - name: decodeURIComponent(params?.sourceName) + name: decodeURIComponent(params?.sourceName), }); const [target, setTarget] = useState<CompareProps>({ type: params?.targetType, - name: decodeURIComponent(params?.targetName) + name: decodeURIComponent(params?.targetName), }); + useDocumentTitle( + t("compare.compareSourceAndTargetWithNamespaceName", { + sourceType: t(`compare.selector.type.${source.type}`), + source: source.name, + targetType: t(`compare.selector.type.${target.type}`), + target: target.name, + namespace: repository.namespace, + name: repository.name, + }) + ); useEffect(() => { const tabUriComponent = location.pathname.split("/")[9]; diff --git a/scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx b/scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx index bbaa338549..52d38b4893 100644 --- a/scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx +++ b/scm-ui/ui-webapp/src/repos/compare/CompareSelector.tsx @@ -85,17 +85,6 @@ const CompareSelector: FC<Props> = ({ onSelect, selected, label, repository }) = }; }); - const getActionTypeName = (type: CompareTypes) => { - switch (type) { - case "b": - return "Branch"; - case "t": - return "Tag"; - case "r": - return "Revision"; - } - }; - return ( <ResponsiveWrapper className="field mb-0 is-flex is-flex-direction-column is-fullwidth"> <label className="label">{label}</label> @@ -107,7 +96,7 @@ const CompareSelector: FC<Props> = ({ onSelect, selected, label, repository }) = onClick={() => setShowDropdown(!showDropdown)} > <span className="is-ellipsis-overflow"> - <strong>{getActionTypeName(selection.type)}:</strong> {selection.name} + <strong>{t(`compare.selector.typeTitle.${selection.type}`)}:</strong> {selection.name} </span> <span className="icon is-small"> <Icon>angle-down</Icon> diff --git a/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx b/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx index 727a70c4f8..65abded555 100644 --- a/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx +++ b/scm-ui/ui-webapp/src/repos/components/RepositoryDetails.tsx @@ -14,34 +14,36 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import React from "react"; +import React, { FC } from "react"; +import { useTranslation } from "react-i18next"; import { Repository } from "@scm-manager/ui-types"; -import RepositoryDetailTable from "./RepositoryDetailTable"; import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; +import { useDocumentTitleForRepository } from "@scm-manager/ui-core"; +import RepositoryDetailTable from "./RepositoryDetailTable"; type Props = { repository: Repository; }; -class RepositoryDetails extends React.Component<Props> { - render() { - const { repository } = this.props; - return ( - <div> - <RepositoryDetailTable repository={repository} /> - <hr /> - <div className="content"> - <ExtensionPoint<extensionPoints.RepositoryDetailsInformation> - name="repos.repository-details.information" - renderAll={true} - props={{ - repository - }} - /> - </div> +const RepositoryDetails: FC<Props> = ({ repository }) => { + const [t] = useTranslation("repos"); + useDocumentTitleForRepository(repository, t("repositoryRoot.menu.informationNavLink")); + + return ( + <div> + <RepositoryDetailTable repository={repository} /> + <hr /> + <div className="content"> + <ExtensionPoint<extensionPoints.RepositoryDetailsInformation> + name="repos.repository-details.information" + renderAll={true} + props={{ + repository, + }} + /> </div> - ); - } -} + </div> + ); +}; export default RepositoryDetails; diff --git a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx index 92ada76344..d1f2c80c1b 100644 --- a/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/ChangesetView.tsx @@ -15,13 +15,14 @@ */ import React, { FC } from "react"; +import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Changeset, Repository } from "@scm-manager/ui-types"; import { ErrorPage, Loading } from "@scm-manager/ui-components"; import ChangesetDetails from "../components/changesets/ChangesetDetails"; import { FileControlFactory } from "@scm-manager/ui-components"; import { RepositoryRevisionContextProvider, useChangeset } from "@scm-manager/ui-api"; -import { useParams } from "react-router-dom"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Props = { repository: Repository; @@ -36,6 +37,18 @@ const ChangesetView: FC<Props> = ({ repository, fileControlFactoryFactory }) => const { id } = useParams<Params>(); const { isLoading, error, data: changeset } = useChangeset(repository, id); const [t] = useTranslation("repos"); + useDocumentTitle( + changeset?.id + ? t("changesets.idWithNamespaceName", { + id: changeset.id.slice(0, 7), + namespace: repository.namespace, + name: repository.name, + }) + : t("changesets.changesetsWithNamespaceName", { + namespace: repository.namespace, + name: repository.name, + }) + ); if (error) { return <ErrorPage title={t("changesets.errorTitle")} subtitle={t("changesets.errorSubtitle")} error={error} />; diff --git a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx index d9ded614d3..276915bece 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Changesets.tsx @@ -27,6 +27,7 @@ import { Notification, urls, } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; export const usePage = () => { const match = useRouteMatch(); @@ -39,12 +40,54 @@ type Props = { url: string; }; -const Changesets: FC<Props> = ({ repository, branch, url }) => { +const Changesets: FC<Props> = ({ repository, branch, ...props }) => { const page = usePage(); const { isLoading, error, data } = useChangesets(repository, { branch, page: page - 1 }); + const [t] = useTranslation("repos"); + const getDocumentTitle = () => { + if (data?.pageTotal && data.pageTotal > 1 && page) { + if (branch) { + return t("changesets.commitsWithPageRevisionAndNamespaceName", { + page, + total: data.pageTotal, + revision: branch.name, + namespace: repository.namespace, + name: repository.name, + }); + } else { + return t("changesets.commitsWithPageAndNamespaceName", { + page, + total: data.pageTotal, + namespace: repository.namespace, + name: repository.name, + }); + } + } else if (branch) { + return t("changesets.commitsWithRevisionAndNamespaceName", { + revision: branch.name, + namespace: repository.namespace, + name: repository.name, + }); + } else { + return t("changesets.commitsWithNamespaceName", { + namespace: repository.namespace, + name: repository.name, + }); + } + }; + useDocumentTitle(getDocumentTitle()); - return <ChangesetsPanel repository={repository} error={error} isLoading={isLoading} data={data} url={url} />; + return ( + <ChangesetsPanel + isLoading={isLoading} + error={error} + data={data} + repository={repository} + branch={branch} + {...props} + /> + ); }; type ChangesetsPanelProps = Props & { @@ -53,7 +96,7 @@ type ChangesetsPanelProps = Props & { data?: ChangesetCollection; }; -export const ChangesetsPanel: FC<ChangesetsPanelProps> = ({ repository, error, isLoading, data, url }) => { +export const ChangesetsPanel: FC<ChangesetsPanelProps> = ({ repository, error, isLoading, data, url, branch }) => { const page = usePage(); const [t] = useTranslation("repos"); const changesets = data?._embedded?.changesets; diff --git a/scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx b/scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx index 30d48cdfc1..284672c878 100644 --- a/scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/CreateRepositoryRoot.tsx @@ -16,16 +16,16 @@ import React, { FC } from "react"; import { Route, Switch } from "react-router-dom"; -import CreateRepository from "./CreateRepository"; -import ImportRepository from "./ImportRepository"; -import { useBinder } from "@scm-manager/ui-extensions"; import { useTranslation } from "react-i18next"; -import { Notification, Page, urls } from "@scm-manager/ui-components"; -import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher"; import { useIndex, useNamespaceStrategies, useRepositoryTypes } from "@scm-manager/ui-api"; +import { Page, urls } from "@scm-manager/ui-components"; +import { Notification, useDocumentTitle } from "@scm-manager/ui-core"; +import { extensionPoints, useBinder } from "@scm-manager/ui-extensions"; import NamespaceAndNameFields from "../components/NamespaceAndNameFields"; import RepositoryInformationForm from "../components/RepositoryInformationForm"; -import { extensionPoints } from "@scm-manager/ui-extensions/"; +import RepositoryFormSwitcher from "../components/form/RepositoryFormSwitcher"; +import CreateRepository from "./CreateRepository"; +import ImportRepository from "./ImportRepository"; type CreatorRouteProps = { creator: extensionPoints.RepositoryCreatorExtension; @@ -41,13 +41,14 @@ const useCreateRepositoryData = () => { pageLoadingError: errorNS || errorRT || errorIdx || undefined, namespaceStrategies, repositoryTypes, - index + index, }; }; const CreatorRoute: FC<CreatorRouteProps> = ({ creator, creators }) => { const { isPageLoading, pageLoadingError, namespaceStrategies, repositoryTypes, index } = useCreateRepositoryData(); const [t] = useTranslation(["repos", "plugins"]); + useDocumentTitle(creator.subtitle); const Component = creator.component; @@ -88,15 +89,15 @@ const CreateRepositoryRoot: FC = () => { path: "", icon: "plus", label: t("repositoryForm.createButton"), - component: CreateRepository + component: CreateRepository, }, { subtitle: t("import.subtitle"), path: "import", icon: "file-upload", label: t("repositoryForm.importButton"), - component: ImportRepository - } + component: ImportRepository, + }, ]; const extCreators = binder.getExtensions<extensionPoints.RepositoryCreator>("repos.creator"); @@ -106,7 +107,7 @@ const CreateRepositoryRoot: FC = () => { return ( <Switch> - {creators.map(creator => ( + {creators.map((creator) => ( <Route key={creator.path} exact path={urls.concat("/repos/create", creator.path)}> <CreatorRoute creator={creator} creators={creators} /> </Route> diff --git a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx index 7d6ae606e8..767e5c9446 100644 --- a/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/EditRepo.tsx @@ -16,18 +16,19 @@ import React, { FC } from "react"; import { useRouteMatch } from "react-router-dom"; -import RepositoryForm from "../components/form"; -import { Repository } from "@scm-manager/ui-types"; -import { ErrorNotification, Subtitle, urls } from "@scm-manager/ui-components"; -import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; -import RepositoryDangerZone from "./RepositoryDangerZone"; import { useTranslation } from "react-i18next"; -import ExportRepository from "./ExportRepository"; +import { Repository } from "@scm-manager/ui-types"; import { useUpdateRepository } from "@scm-manager/ui-api"; +import { ErrorNotification, Subtitle, urls } from "@scm-manager/ui-components"; +import { useDocumentTitleForRepository } from "@scm-manager/ui-core"; +import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; +import UpdateNotification from "../../components/UpdateNotification"; +import RepositoryForm from "../components/form"; +import Reindex from "../components/Reindex"; +import RepositoryDangerZone from "./RepositoryDangerZone"; +import ExportRepository from "./ExportRepository"; import HealthCheckWarning from "./HealthCheckWarning"; import RunHealthCheck from "./RunHealthCheck"; -import UpdateNotification from "../../components/UpdateNotification"; -import Reindex from "../components/Reindex"; type Props = { repository: Repository; @@ -37,11 +38,12 @@ const EditRepo: FC<Props> = ({ repository }) => { const match = useRouteMatch(); const { isLoading, error, update, isUpdated } = useUpdateRepository(); const [t] = useTranslation("repos"); + useDocumentTitleForRepository(repository, t("repositoryRoot.settingsTitle")); const url = urls.matchedUrlFromMatch(match); const extensionProps = { repository, - url + url, }; return ( diff --git a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx index d67aea76aa..11f6de0097 100644 --- a/scm-ui/ui-webapp/src/repos/containers/Overview.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/Overview.tsx @@ -29,6 +29,7 @@ import { PageActions, urls, } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import RepositoryList from "../components/list"; import { useNamespaceAndNameContext, useNamespaces, useRepositories } from "@scm-manager/ui-api"; import { NamespaceCollection, RepositoryCollection } from "@scm-manager/ui-types"; @@ -170,6 +171,20 @@ const Overview: FC = () => { const { isLoading, error, namespace, namespaces, repositories, search, page } = useOverviewData(); const history = useHistory(); const [t] = useTranslation("repos"); + const getDocumentTitle = () => { + if (repositories?.pageTotal && repositories.pageTotal > 1 && page) { + if (namespace) { + return t("overview.titleWithNamespaceAndPage", { page, total: repositories.pageTotal, namespace }); + } else { + return t("overview.titleWithPage", { page, total: repositories.pageTotal }); + } + } else if (namespace) { + return t("overview.titleWithNamespace", { namespace }); + } else { + return t("overview.title"); + } + }; + useDocumentTitle(getDocumentTitle()); const binder = useBinder(); const context = useNamespaceAndNameContext(); useEffect(() => { diff --git a/scm-ui/ui-webapp/src/repos/importlog/ImportLog.tsx b/scm-ui/ui-webapp/src/repos/importlog/ImportLog.tsx index e2f0b41782..8eb6e7deaf 100644 --- a/scm-ui/ui-webapp/src/repos/importlog/ImportLog.tsx +++ b/scm-ui/ui-webapp/src/repos/importlog/ImportLog.tsx @@ -15,10 +15,11 @@ */ import React, { FC } from "react"; -import { useImportLog } from "@scm-manager/ui-api"; import { useParams } from "react-router-dom"; -import { Page } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; +import { useImportLog } from "@scm-manager/ui-api"; +import { Page } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Params = { logId: string; @@ -28,6 +29,7 @@ const ImportLog: FC = () => { const { logId } = useParams<Params>(); const { isLoading, data, error } = useImportLog(logId); const [t] = useTranslation("commons"); + useDocumentTitle(t("importLog.title")); return ( <Page title={t("importLog.title")} loading={isLoading} error={error}> diff --git a/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceInformation.tsx b/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceInformation.tsx index 5cc2edf0b6..ebf3d241d2 100644 --- a/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceInformation.tsx +++ b/scm-ui/ui-webapp/src/repos/namespaces/containers/NamespaceInformation.tsx @@ -14,13 +14,13 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -import { Namespace } from "@scm-manager/ui-types"; import React, { FC } from "react"; -import { ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-core"; +import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { useRepositories } from "@scm-manager/ui-api"; import { DateFromNow } from "@scm-manager/ui-components"; -import { Link } from "react-router-dom"; +import { ErrorNotification, Loading, Subtitle, useDocumentTitle } from "@scm-manager/ui-core"; +import { Namespace } from "@scm-manager/ui-types"; type Props = { namespace: Namespace; @@ -28,6 +28,7 @@ type Props = { const NamespaceInformation: FC<Props> = ({ namespace }) => { const [t] = useTranslation("namespaces"); + useDocumentTitle(t("namespaceRoot.infoPage.subtitle"), namespace.namespace); const { data: repositories, error, isLoading } = useRepositories({ namespace: namespace, pageSize: 9999, page: 0 }); if (error) { diff --git a/scm-ui/ui-webapp/src/repos/permissions/containers/Permissions.tsx b/scm-ui/ui-webapp/src/repos/permissions/containers/Permissions.tsx index 18daaebfe3..9145606716 100644 --- a/scm-ui/ui-webapp/src/repos/permissions/containers/Permissions.tsx +++ b/scm-ui/ui-webapp/src/repos/permissions/containers/Permissions.tsx @@ -16,11 +16,12 @@ import React, { FC } from "react"; import { useTranslation } from "react-i18next"; +import { useAvailablePermissions, usePermissions } from "@scm-manager/ui-api"; import { ErrorPage, Loading, Subtitle } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import { Namespace, Repository } from "@scm-manager/ui-types"; import CreatePermissionForm from "./CreatePermissionForm"; import PermissionsTable from "../components/PermissionsTable"; -import { useAvailablePermissions, usePermissions } from "@scm-manager/ui-api"; type Props = { namespaceOrRepository: Namespace | Repository; @@ -37,13 +38,17 @@ const usePermissionData = (namespaceOrRepository: Namespace | Repository) => { isLoading: permissions.isLoading || availablePermissions.isLoading, error: permissions.error || availablePermissions.error, permissions: permissions.data, - availablePermissions: availablePermissions.data + availablePermissions: availablePermissions.data, }; }; const Permissions: FC<Props> = ({ namespaceOrRepository }) => { const { isLoading, error, permissions, availablePermissions } = usePermissionData(namespaceOrRepository); const [t] = useTranslation("repos"); + useDocumentTitle( + t("repositoryRoot.menu.permissionsNavLink"), + namespaceOrRepository.namespace + (isRepository(namespaceOrRepository) ? "/" + namespaceOrRepository.name : "") + ); if (error) { return <ErrorPage title={t("permission.error-title")} subtitle={t("permission.error-subtitle")} error={error} />; diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx index 131a44f123..f25009fcfa 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/Sources.tsx @@ -20,7 +20,7 @@ import { useHistory, useLocation, useParams } from "react-router-dom"; import { RepositoryRevisionContextProvider, urls, useSources } from "@scm-manager/ui-api"; import { Branch, Repository } from "@scm-manager/ui-types"; import { Breadcrumb } from "@scm-manager/ui-components"; -import { Notification, ErrorNotification, Loading } from "@scm-manager/ui-core"; +import { Notification, ErrorNotification, Loading, useDocumentTitle } from "@scm-manager/ui-core"; import FileTree from "../components/FileTree"; import Content from "./Content"; import CodeActionBar from "../../codeSection/components/CodeActionBar"; @@ -57,6 +57,30 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) = const history = useHistory(); const location = useLocation(); const [t] = useTranslation("repos"); + const getDocumentTitle = () => { + if (revision) { + const getRevision = () => { + return branches?.some((branch) => branch.name === revision) ? revision : revision.slice(0, 7); + }; + if (path) { + return t("sources.pathWithRevisionAndNamespaceName", { + path: path, + revision: getRevision(), + namespace: repository.namespace, + name: repository.name, + }); + } else { + return t("sources.sourcesWithRevisionAndNamespaceName", { + revision: getRevision(), + namespace: repository.namespace, + name: repository.name, + }); + } + } else { + return repository.namespace + "/" + repository.name; + } + }; + useDocumentTitle(getDocumentTitle()); const [contentRef, setContentRef] = useState<HTMLElement | null>(); useScrollToElement(contentRef, () => location.hash, location.hash); @@ -72,7 +96,6 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) = ); } }, [branches, selectedBranch, history, baseUrl, location.hash]); - const { isLoading, error, diff --git a/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx b/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx index 563645caf1..e2a896e47e 100644 --- a/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx +++ b/scm-ui/ui-webapp/src/repos/tags/components/TagDetail.tsx @@ -19,6 +19,7 @@ import { useTranslation } from "react-i18next"; import classNames from "classnames"; import { Repository, Tag } from "@scm-manager/ui-types"; import { Subtitle, DateFromNow, SignatureIcon } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; import TagButtonGroup from "./TagButtonGroup"; import CompareLink from "../../compare/CompareLink"; @@ -29,6 +30,13 @@ type Props = { const TagDetail: FC<Props> = ({ repository, tag }) => { const [t] = useTranslation("repos"); + useDocumentTitle( + t("tag.tagWithNamespaceName", { + tag: tag.name, + namespace: repository.namespace, + name: repository.name, + }) + ); const encodedTag = encodeURIComponent(tag.name); diff --git a/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx b/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx index fd00176409..07a22a9f53 100644 --- a/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx +++ b/scm-ui/ui-webapp/src/repos/tags/container/TagsOverview.tsx @@ -17,6 +17,7 @@ import React, { FC, useMemo, useState } from "react"; import { Repository } from "@scm-manager/ui-types"; import { ErrorNotification, Loading, Notification, Subtitle } from "@scm-manager/ui-components"; +import { useDocumentTitleForRepository } from "@scm-manager/ui-core"; import { useTranslation } from "react-i18next"; import orderTags, { SORT_OPTIONS, SortOption } from "../orderTags"; import TagTable from "../components/TagTable"; @@ -31,6 +32,7 @@ type Props = { const TagsOverview: FC<Props> = ({ repository, baseUrl }) => { const { isLoading, error, data } = useTags(repository); const [t] = useTranslation("repos"); + useDocumentTitleForRepository(repository, t("tags.overview.title")); const [sort, setSort] = useState<SortOption | undefined>(); const tags = useMemo(() => orderTags(data?._embedded?.tags || [], sort), [data, sort]); diff --git a/scm-ui/ui-webapp/src/search/Search.tsx b/scm-ui/ui-webapp/src/search/Search.tsx index 0b5130bf04..1cbc87c5d5 100644 --- a/scm-ui/ui-webapp/src/search/Search.tsx +++ b/scm-ui/ui-webapp/src/search/Search.tsx @@ -24,7 +24,7 @@ import { Tag, urls, } from "@scm-manager/ui-components"; -import { Notification, Level } from "@scm-manager/ui-core"; +import { Notification, Level, useDocumentTitle } from "@scm-manager/ui-core"; import { Link, useLocation, useParams } from "react-router-dom"; import { useIndex, useNamespaceAndNameContext, useSearch, useSearchCounts, useSearchTypes } from "@scm-manager/ui-api"; import Results from "./Results"; @@ -163,9 +163,22 @@ const InvalidSearch: FC = () => { const Search: FC = () => { const { data: index } = useIndex(); - const [t] = useTranslation(["commons", "plugins"]); - const [showHelp, setShowHelp] = useState(false); const { query, selectedType, page, namespace, name } = usePageParams(); + const searchOptions = { + type: selectedType, + page: page - 1, + pageSize: 25, + namespaceContext: namespace, + repositoryNameContext: name, + }; + const { data, isLoading, error } = useSearch(query, searchOptions); + const [t] = useTranslation(["commons", "plugins"]); + useDocumentTitle( + data?.pageTotal && data.pageTotal > 1 && page + ? t("search.titleWithPage", { page, total: data.pageTotal }) + : t("search.title") + ); + const [showHelp, setShowHelp] = useState(false); const context = useNamespaceAndNameContext(); useEffect(() => { context.setNamespace(namespace || ""); @@ -176,14 +189,6 @@ const Search: FC = () => { context.setName(""); }; }, [namespace, name, context]); - const searchOptions = { - type: selectedType, - page: page - 1, - pageSize: 25, - namespaceContext: namespace, - repositoryNameContext: name, - }; - const { data, isLoading, error } = useSearch(query, searchOptions); const types = useSearchTypes(searchOptions); types.sort(orderTypes(t)); diff --git a/scm-ui/ui-webapp/src/search/Syntax.tsx b/scm-ui/ui-webapp/src/search/Syntax.tsx index 2f7afb7e13..e64e4ce4b9 100644 --- a/scm-ui/ui-webapp/src/search/Syntax.tsx +++ b/scm-ui/ui-webapp/src/search/Syntax.tsx @@ -15,13 +15,13 @@ */ import React, { FC, useState } from "react"; -import { useSearchableTypes, useSearchSyntaxContent } from "@scm-manager/ui-api"; import { useTranslation } from "react-i18next"; -import { copyToClipboard, InputField, Loading, MarkdownView, Page } from "@scm-manager/ui-components"; -import { ErrorNotification, Icon, Tooltip, Button } from "@scm-manager/ui-core"; +import classNames from "classnames"; import { parse } from "date-fns"; import styled from "styled-components"; -import classNames from "classnames"; +import { useSearchableTypes, useSearchSyntaxContent } from "@scm-manager/ui-api"; +import { copyToClipboard, InputField, MarkdownView, Page } from "@scm-manager/ui-components"; +import { ErrorNotification, Icon, Loading, Tooltip, Button, useDocumentTitle } from "@scm-manager/ui-core"; import { SearchableType } from "@scm-manager/ui-types"; const StyledTooltip = styled(Tooltip)` @@ -41,7 +41,9 @@ type ExpandableProps = { const Expandable: FC<ExpandableProps> = ({ header, children, className }) => { const [t] = useTranslation("commons"); + useDocumentTitle(t("search.syntax.title")); const [expanded, setExpanded] = useState(false); + return ( <div className={classNames("card search-syntax-accordion", className)}> <header> @@ -88,18 +90,20 @@ const Examples: FC<ExampleProps> = ({ searchableType }) => { <h5 className="title mt-5">{t("search.syntax.exampleQueries.title")}</h5> <div className="mb-2">{t("search.syntax.exampleQueries.description")}</div> <table> - <tr> - <th>{t("search.syntax.exampleQueries.table.description")}</th> - <th>{t("search.syntax.exampleQueries.table.query")}</th> - <th>{t("search.syntax.exampleQueries.table.explanation")}</th> - </tr> - {examples.map((example) => ( - <tr key={example.description}> - <td>{example.description}</td> - <td>{example.query}</td> - <td>{example.explanation}</td> + <tbody> + <tr> + <th>{t("search.syntax.exampleQueries.table.description")}</th> + <th>{t("search.syntax.exampleQueries.table.query")}</th> + <th>{t("search.syntax.exampleQueries.table.explanation")}</th> </tr> - ))} + {examples.map((example) => ( + <tr key={example.description}> + <td>{example.description}</td> + <td>{example.query}</td> + <td>{example.explanation}</td> + </tr> + ))} + </tbody> </table> </> ); @@ -126,28 +130,30 @@ const SearchableTypes: FC = () => { header={t(`plugins:search.types.${searchableType.name}.title`, searchableType.name)} > <table> - <tr> - <th>{t("search.syntax.fields.name")}</th> - <th>{t("search.syntax.fields.type")}</th> - <th>{t("search.syntax.fields.exampleValue")}</th> - <th>{t("search.syntax.fields.hints")}</th> - </tr> - {searchableType.fields.map((searchableField) => ( - <tr key={searchableField.name}> - <th>{searchableField.name}</th> - <td>{searchableField.type}</td> - <td> - {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.exampleValue`, { - defaultValue: "", - })} - </td> - <td> - {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.hints`, { - defaultValue: "", - })} - </td> + <tbody> + <tr> + <th>{t("search.syntax.fields.name")}</th> + <th>{t("search.syntax.fields.type")}</th> + <th>{t("search.syntax.fields.exampleValue")}</th> + <th>{t("search.syntax.fields.hints")}</th> </tr> - ))} + {searchableType.fields.map((searchableField) => ( + <tr key={searchableField.name}> + <th>{searchableField.name}</th> + <td>{searchableField.type}</td> + <td> + {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.exampleValue`, { + defaultValue: "", + })} + </td> + <td> + {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.hints`, { + defaultValue: "", + })} + </td> + </tr> + ))} + </tbody> </table> <Examples searchableType={searchableType} /> </Expandable> diff --git a/scm-ui/ui-webapp/src/users/components/SetUserPassword.tsx b/scm-ui/ui-webapp/src/users/components/SetUserPassword.tsx index e147b4aeb0..cb1aae8291 100644 --- a/scm-ui/ui-webapp/src/users/components/SetUserPassword.tsx +++ b/scm-ui/ui-webapp/src/users/components/SetUserPassword.tsx @@ -23,9 +23,10 @@ import { Notification, PasswordConfirmation, SubmitButton, - Subtitle + Subtitle, } from "@scm-manager/ui-components"; import { useSetUserPassword } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Props = { user: User; @@ -33,6 +34,7 @@ type Props = { const SetUserPassword: FC<Props> = ({ user }) => { const [t] = useTranslation("users"); + useDocumentTitle(t("singleUser.menu.setPasswordNavLink"), user.displayName); const { passwordOverwritten, setPassword, error, isLoading, reset } = useSetUserPassword(user); const [newPassword, setNewPassword] = useState(""); const [passwordValid, setPasswordValid] = useState(false); diff --git a/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx index 870e7cc895..cb6ee31386 100644 --- a/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx +++ b/scm-ui/ui-webapp/src/users/components/apiKeys/SetApiKeys.tsx @@ -22,6 +22,7 @@ import AddApiKey from "./AddApiKey"; import { useTranslation } from "react-i18next"; import { useApiKeys, useDeleteApiKey } from "@scm-manager/ui-api"; import { Link as RouterLink } from "react-router-dom"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Props = { user: User | Me; @@ -29,6 +30,7 @@ type Props = { const SetApiKeys: FC<Props> = ({ user }) => { const [t] = useTranslation("users"); + useDocumentTitle(t("singleUser.menu.setApiKeyNavLink"), user.displayName); const { isLoading, data: apiKeys, error: fetchError } = useApiKeys(user); const { error: deletionError, remove } = useDeleteApiKey(user); const error = deletionError || fetchError; diff --git a/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx b/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx index 0fdd1a7479..261dd4690a 100644 --- a/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx +++ b/scm-ui/ui-webapp/src/users/components/publicKeys/SetPublicKeys.tsx @@ -21,6 +21,7 @@ import AddPublicKey from "./AddPublicKey"; import PublicKeyTable from "./PublicKeyTable"; import { ErrorNotification, Loading, Subtitle } from "@scm-manager/ui-components"; import { useDeletePublicKey, usePublicKeys } from "@scm-manager/ui-api"; +import { useDocumentTitle } from "@scm-manager/ui-core"; type Props = { user: User | Me; @@ -28,6 +29,7 @@ type Props = { const SetPublicKeys: FC<Props> = ({ user }) => { const [t] = useTranslation("users"); + useDocumentTitle(t("singleUser.menu.setPublicKeyNavLink"), user.displayName); const { error: fetchingError, isLoading, data: publicKeys } = usePublicKeys(user); const { error: deletionError, remove } = useDeletePublicKey(user); const error = fetchingError || deletionError; diff --git a/scm-ui/ui-webapp/src/users/components/table/Details.tsx b/scm-ui/ui-webapp/src/users/components/table/Details.tsx index 4f9825b78f..f4839a1204 100644 --- a/scm-ui/ui-webapp/src/users/components/table/Details.tsx +++ b/scm-ui/ui-webapp/src/users/components/table/Details.tsx @@ -15,7 +15,7 @@ */ import React, { FC, useState } from "react"; -import { useTranslation, WithTranslation } from "react-i18next"; +import { useTranslation } from "react-i18next"; import { User } from "@scm-manager/ui-types"; import { Checkbox, @@ -23,18 +23,20 @@ import { DateFromNow, Help, InfoTable, - MailLink + MailLink, } from "@scm-manager/ui-components"; import { Icon } from "@scm-manager/ui-components"; import PermissionOverview from "../PermissionOverview"; import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions"; +import { useDocumentTitle } from "@scm-manager/ui-core"; -type Props = WithTranslation & { +type Props = { user: User; }; const Details: FC<Props> = ({ user }) => { const [t] = useTranslation("users"); + useDocumentTitle(t("singleUser.menu.informationNavLink"), user.displayName); const [collapsed, setCollapsed] = useState(true); const toggleCollapse = () => setCollapsed(!collapsed); @@ -96,7 +98,11 @@ const Details: FC<Props> = ({ user }) => { <DateFromNow date={user.lastModified} /> </td> </tr> - <ExtensionPoint<extensionPoints.UserInformationTableBottom> name="user.information.table.bottom" props={{user}} renderAll={true} /> + <ExtensionPoint<extensionPoints.UserInformationTableBottom> + name="user.information.table.bottom" + props={{ user }} + renderAll={true} + /> </tbody> </InfoTable> {permissionOverview} diff --git a/scm-ui/ui-webapp/src/users/containers/CreateUser.tsx b/scm-ui/ui-webapp/src/users/containers/CreateUser.tsx index 4be073e466..e2b1ffdd8e 100644 --- a/scm-ui/ui-webapp/src/users/containers/CreateUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/CreateUser.tsx @@ -21,7 +21,7 @@ import { Page } from "@scm-manager/ui-components"; import { Form, useCreateResource } from "@scm-manager/ui-forms"; import * as userValidator from "../components/userValidation"; import { Link, User, UserCollection, UserCreation } from "@scm-manager/ui-types"; -import { ErrorNotification, Loading, Notification } from "@scm-manager/ui-core"; +import { ErrorNotification, Loading, Notification, useDocumentTitle } from "@scm-manager/ui-core"; import { useUsers } from "@scm-manager/ui-api"; type UserCreationForm = Pick<UserCreation, "password" | "name" | "displayName" | "active" | "external" | "mail"> & { @@ -30,6 +30,7 @@ type UserCreationForm = Pick<UserCreation, "password" | "name" | "displayName" | const CreateUserForm: FC<{ users: UserCollection }> = ({ users }) => { const [t] = useTranslation("users"); + useDocumentTitle(t("createUser.title")); const { submit, submissionResult: createdUser } = useCreateResource<UserCreationForm, User>( (users._links.create as Link).href, ["user", "users"], diff --git a/scm-ui/ui-webapp/src/users/containers/EditUser.tsx b/scm-ui/ui-webapp/src/users/containers/EditUser.tsx index 3c3e0752ef..97cc072348 100644 --- a/scm-ui/ui-webapp/src/users/containers/EditUser.tsx +++ b/scm-ui/ui-webapp/src/users/containers/EditUser.tsx @@ -15,13 +15,14 @@ */ import React, { FC } from "react"; -import DeleteUser from "./DeleteUser"; -import { User } from "@scm-manager/ui-types"; -import UserConverter from "../components/UserConverter"; -import { Form, useUpdateResource } from "@scm-manager/ui-forms"; -import * as userValidator from "../components/userValidation"; -import { Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; +import { User } from "@scm-manager/ui-types"; +import { Subtitle } from "@scm-manager/ui-components"; +import { useDocumentTitle } from "@scm-manager/ui-core"; +import { Form, useUpdateResource } from "@scm-manager/ui-forms"; +import UserConverter from "../components/UserConverter"; +import * as userValidator from "../components/userValidation"; +import DeleteUser from "./DeleteUser"; type Props = { user: User; @@ -29,6 +30,7 @@ type Props = { const EditUser: FC<Props> = ({ user }) => { const [t] = useTranslation("users"); + useDocumentTitle(t("singleUser.settingsTitle"), user.displayName); const { submit } = useUpdateResource<User>(user, (user) => user.name, { contentType: "application/vnd.scmm-user+json;v=2", collectionName: ["user", "users"], diff --git a/scm-ui/ui-webapp/src/users/containers/Users.tsx b/scm-ui/ui-webapp/src/users/containers/Users.tsx index 0bf9cb69bd..4120fde361 100644 --- a/scm-ui/ui-webapp/src/users/containers/Users.tsx +++ b/scm-ui/ui-webapp/src/users/containers/Users.tsx @@ -20,7 +20,7 @@ import { useTranslation } from "react-i18next"; import { useUsers } from "@scm-manager/ui-api"; import { User, UserCollection } from "@scm-manager/ui-types"; import { CreateButton, LinkPaginator, OverviewPageActions, Page, PageActions, urls } from "@scm-manager/ui-components"; -import { Notification } from "@scm-manager/ui-core"; +import { Notification, useDocumentTitle } from "@scm-manager/ui-core"; import { UserTable } from "./../components/table"; type UserPageProps = { @@ -52,6 +52,7 @@ const Users: FC = () => { const page = urls.getPageFromMatch({ params }); const { isLoading, error, data } = useUsers({ page: page - 1, search }); const [t] = useTranslation("users"); + useDocumentTitle(t("users.title")); const users = data?._embedded?.users; const canAddUsers = !!data?._links.create;