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;