Set descriptive document titles

Document titles represent the pages, for example in lists
with bookmarks. They are important for navigation and
orientation in websites. If the offer or the content of the
page is not labeled, orientation is impaired.

This changes the behavior for setting document titles.
The functionality has been removed from the Page and
Title components and is now represented by `useDocumentTitle`
hook to better describe the content of inividual pages.

Co-authored-by: Anna Vetcininova<anna.vetcininova@cloudogu.com>
This commit is contained in:
Florian Scholdei
2024-12-10 16:41:01 +01:00
parent 3ed457a891
commit 3c0ad46f07
74 changed files with 812 additions and 445 deletions

View File

@@ -0,0 +1,2 @@
- type: changed
description: Replace title behavior with `useDocumentTitle` hook for setting descriptive document titles

View File

@@ -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<Props> = ({ link }) => {
const [t] = useTranslation("plugins");
useDocumentTitle(t("scm-git-plugin.config.title"));
const { initialConfiguration, isReadOnly, update, ...formProps } = useConfigLink<Configuration>(link);
const { formState, handleSubmit, register, reset } = useForm<Configuration>({ mode: "onChange" });
@@ -45,7 +47,7 @@ const GitGlobalConfiguration: FC<Props> = ({ link }) => {
if (initialConfiguration) {
reset(initialConfiguration);
}
}, [initialConfiguration]);
}, [initialConfiguration, reset]);
const isValidDefaultBranch = (value: string) => {
return validation.isBranchValid(value);
@@ -58,7 +60,7 @@ const GitGlobalConfiguration: FC<Props> = ({ link }) => {
onSubmit={handleSubmit(update)}
{...formProps}
>
<Title title={t("scm-git-plugin.config.title")} />
<Title>{t("scm-git-plugin.config.title")}</Title>
<InputField
label={t("scm-git-plugin.config.gcExpression")}
helpText={t("scm-git-plugin.config.gcExpressionHelpText")}

View File

@@ -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 HgConfigurationForm from "./HgConfigurationForm";
type Props = WithTranslation & {
type Props = {
link: string;
};
class HgGlobalConfiguration extends React.Component<Props> {
render() {
const { link, t } = this.props;
const HgGlobalConfiguration: FC<Props> = ({ link }) => {
const [t] = useTranslation("plugins");
useDocumentTitle(t("scm-hg-plugin.config.title"));
return (
<div>
<Title title={t("scm-hg-plugin.config.title")} />
<Title>{t("scm-hg-plugin.config.title")}</Title>
<Configuration link={link} render={(props: any) => <HgConfigurationForm {...props} />} />
</div>
);
}
}
};
export default withTranslation("plugins")(HgGlobalConfiguration);
export default HgGlobalConfiguration;

View File

@@ -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<Props> {
render() {
const { link, t } = this.props;
const SvnGlobalConfiguration: FC<Props> = ({ link }) => {
const [t] = useTranslation("plugins");
useDocumentTitle(t("scm-svn-plugin.config.title"));
return (
<div>
<Title title={t("scm-svn-plugin.config.title")} />
<Title>{t("scm-svn-plugin.config.title")}</Title>
<Configuration link={link} render={(props: any) => <SvnConfigurationForm {...props} />} />
</div>
);
}
}
};
export default withTranslation("plugins")(SvnGlobalConfiguration);
export default SvnGlobalConfiguration;

View File

@@ -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<ErrorDisplayProps> = ({ error, errorInfo, fallback: Fallb
const fallbackProps = {
error,
errorInfo
errorInfo,
};
return <FallbackComponent {...fallbackProps} />;
@@ -128,7 +130,7 @@ class ErrorBoundary extends React.Component<Props, State> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({
error,
errorInfo
errorInfo,
});
}

View File

@@ -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<Props> {
render() {
const { title, error } = this.props;
const ErrorPage: FC<Props> = ({ error, title, subtitle }) => {
const [t] = useTranslation("commons");
useDocumentTitle(t("errorNotification.prefix"));
return (
<section className="section">
<div className="box column is-4 is-offset-4 container">
<h1 className="title">{title}</h1>
{this.renderSubtitle()}
<ErrorNotification error={error} />
</div>
</section>
);
}
renderSubtitle = () => {
const { error, subtitle } = this.props;
const renderSubtitle = () => {
if (error instanceof BackendError || error instanceof ForbiddenError) {
return null;
}
return <p className="subtitle">{subtitle}</p>;
};
}
return (
<section className="section">
<div className="box column is-4 is-offset-4 container">
<h1 className="title">{title}</h1>
{renderSubtitle()}
<ErrorNotification error={error} />
</div>
</section>
);
};
export default ErrorPage;

View File

@@ -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<Props> {
componentDidUpdate() {
const textualTitle = this.getTextualTitle();
if (textualTitle && textualTitle !== document.title) {
document.title = textualTitle;
}
}
render() {
const { error } = this.props;
return (
<section className="section">
<div className="container">
{this.renderPageHeader()}
<ErrorBoundary>
<ErrorNotification error={error} />
{this.renderContent()}
</ErrorBoundary>
</div>
</section>
);
}
isPageAction(node: any) {
const Page: FC<Props> = ({ 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 = (
<PageActionContainer
className={classNames(
@@ -99,6 +74,7 @@ export default class Page extends React.Component<Props> {
}
}
});
const underline = pageActionsExists ? <hr className="header-with-actions" /> : null;
if (title || subtitle) {
@@ -107,9 +83,7 @@ export default class Page extends React.Component<Props> {
<div className="columns">
<div className="column">
<div className="is-flex is-flex-wrap-wrap is-align-items-center">
<Title className="mb-0 mr-2" title={this.getTextualTitle()}>
{this.getTitleComponent()}
</Title>
<Title className="mb-0 mr-2">{title}</Title>
{afterTitle}
</div>
{subtitle ? <Subtitle>{subtitle}</Subtitle> : null}
@@ -121,11 +95,9 @@ export default class Page extends React.Component<Props> {
);
}
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<Props> {
}
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 (
<section className="section">
<div className="container">
{renderPageHeader()}
<ErrorBoundary>
<ErrorNotification error={error} />
{renderContent()}
</ErrorBoundary>
</div>
</section>
);
};
export default Page;

View File

@@ -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<Props> = ({ 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<Props> = ({ title, className, children }) => {
if (children) {
return <h1 className={classNames("title", className)}>{children}</h1>;
} else if (title) {

View File

@@ -15,4 +15,5 @@
*/
export { default as useAriaId } from "./useAriaId";
export { default as useDocumentTitle, useDocumentTitleForRepository } from "./useDocumentTitle";
export { createAttributesForTesting, isDevBuild } from "./devbuild";

View File

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

View File

@@ -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<extensionPoints.DocumentTitleExtensionPoint>("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);
}

View File

@@ -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<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement> & 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 (
<h1 className={classNames("title", className)} {...props} ref={ref}>

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
"createButton": "Gruppe erstellen"
},
"singleGroup": {
"settingsTitle": "Generelle Einstellungen",
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Gruppen Fehler",
"menu": {

View File

@@ -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}}</0> gefunden"
}
},
"searchWithRevisionAndNamespaceName": "Suche auf {{revision}} in {{namespace}}/{{name}}"
},
"shortcuts": {
"info": "Wechsel zur Repository-Info",

View File

@@ -31,6 +31,7 @@
"createButton": "Benutzer erstellen"
},
"singleUser": {
"settingsTitle": "Generelle Einstellungen",
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Benutzer Fehler",
"menu": {

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
"createButton": "Create Group"
},
"singleGroup": {
"settingsTitle": "General Settings",
"errorTitle": "Error",
"errorSubtitle": "Unknown group error",
"menu": {

View File

@@ -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}}</0>"
}
},
"searchWithRevisionAndNamespaceName": "Search on {{revision}} in {{namespace}}/{{name}}"
},
"shortcuts": {
"info": "Switch to repository info",

View File

@@ -31,6 +31,7 @@
"createButton": "Create User"
},
"singleUser": {
"settingsTitle": "General Settings",
"errorTitle": "Error",
"errorSubtitle": "Unknown user error",
"menu": {

View File

@@ -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 = () => {
<h3 className="has-text-weight-medium">{t("admin.info.newRelease.title")}</h3>
<p>
{t("admin.info.newRelease.description", {
version: updateInfo?.latestVersion
version: updateInfo?.latestVersion,
})}
</p>
<a className="button is-warning is-pulled-right" target="_blank" href={updateInfo?.link} rel="noreferrer">

View File

@@ -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);

View File

@@ -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<Props> = ({ installed }) => {
const [t] = useTranslation("admin");
useDocumentTitle(installed ? t("plugins.installedSubtitle") : t("plugins.availableSubtitle"));
const {
data: availablePlugins,
isLoading: isLoadingAvailablePlugins,

View File

@@ -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<Props> {
renderEditButton() {
const { t, url } = this.props;
if (!!this.props.role._links.update) {
const PermissionRoleDetails: FC<Props> = ({ role, url }) => {
const [t] = useTranslation("admin");
useDocumentTitle(t("repositoryRole.detailsTitle"));
const renderEditButton = () => {
if (!!role._links.update) {
return (
<>
<hr />
<Level right={<Button label={t("repositoryRole.editButton")} link={`${url}/edit`} color="primary" />} />
</>
);
<Level
right={
<LinkButton to={`${url}/edit`} variant="primary" color="primary">
{t("repositoryRole.editButton")}
</LinkButton>
}
return null;
}
render() {
const { role } = this.props;
return (
<>
<PermissionRoleDetailsTable role={role} />
{this.renderEditButton()}
<ExtensionPoint<extensionPoints.RepositoryRoleDetailsInformation>
name="repositoryRole.role-details.information"
renderAll={true}
props={{
role
}}
/>
</>
);
}
}
return null;
};
export default withTranslation("admin")(PermissionRoleDetails);
return (
<>
<Title>{t("repositoryRole.detailsTitle")}</Title>
<PermissionRoleDetailsTable role={role} />
{renderEditButton()}
<ExtensionPoint<extensionPoints.RepositoryRoleDetailsInformation>
name="repositoryRole.role-details.information"
renderAll={true}
props={{
role,
}}
/>
</>
);
};
export default PermissionRoleDetails;

View File

@@ -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) {

View File

@@ -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<Props> = ({ 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<Props> = ({ role }) => {
return (
<>
<Title title={t("repositoryRole.detailsTitle")} />
<Subtitle subtitle={t("repositoryRole.editSubtitle")} />
<RepositoryRoleForm role={role} submitForm={update} />
<DeleteRepositoryRole role={role} />

View File

@@ -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`} />

View File

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

View File

@@ -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 />;

View File

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

View File

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

View File

@@ -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`)}>

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

@@ -14,20 +14,41 @@
* 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;
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 = (
<tr>
<th>{t("group.members")}</th>
<td className="p-0">
<ul className="ml-4">
{group._embedded.members.map((member, index) => {
return <GroupMember key={index} member={member} />;
})}
</ul>
</td>
</tr>
);
}
return member;
};
return (
<InfoTable className="content">
<tbody>
@@ -61,33 +82,15 @@ class Details extends React.Component<Props> {
<DateFromNow date={group.lastModified} />
</td>
</tr>
{this.renderMembers()}
<ExtensionPoint<extensionPoints.GroupInformationTableBottom> name="group.information.table.bottom" props={{group}} renderAll={true} />
{renderMembers()}
<ExtensionPoint<extensionPoints.GroupInformationTableBottom>
name="group.information.table.bottom"
props={{ group }}
renderAll={true}
/>
</tbody>
</InfoTable>
);
}
};
renderMembers() {
const { group, t } = this.props;
let member = null;
if (group.members.length > 0) {
member = (
<tr>
<th>{t("group.members")}</th>
<td className="p-0">
<ul className="ml-4">
{group._embedded.members.map((member, index) => {
return <GroupMember key={index} member={member} />;
})}
</ul>
</td>
</tr>
);
}
return member;
}
}
export default withTranslation("groups")(Details);
export default Details;

View File

@@ -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();

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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,9 +25,7 @@ type Props = {
branch: Branch;
};
class BranchView extends React.Component<Props> {
render() {
const { repository, branch } = this.props;
const BranchView: FC<Props> = ({ repository, branch }) => {
return (
<>
<BranchDetail repository={repository} branch={branch} />
@@ -38,14 +36,13 @@ class BranchView extends React.Component<Props> {
renderAll={true}
props={{
repository,
branch
branch,
}}
/>
</div>
<BranchDangerZone repository={repository} branch={branch} />
</>
);
}
}
};
export default BranchView;

View File

@@ -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} />;

View File

@@ -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`}
/>
);
}

View File

@@ -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) {

View File

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

View File

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

View File

@@ -14,18 +14,21 @@
* 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;
const RepositoryDetails: FC<Props> = ({ repository }) => {
const [t] = useTranslation("repos");
useDocumentTitleForRepository(repository, t("repositoryRoot.menu.informationNavLink"));
return (
<div>
<RepositoryDetailTable repository={repository} />
@@ -35,13 +38,12 @@ class RepositoryDetails extends React.Component<Props> {
name="repos.repository-details.information"
renderAll={true}
props={{
repository
repository,
}}
/>
</div>
</div>
);
}
}
};
export default RepositoryDetails;

View File

@@ -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} />;

View File

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

View File

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

View File

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

View File

@@ -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(() => {

View File

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

View File

@@ -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) {

View File

@@ -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} />;

View File

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

View File

@@ -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);

View File

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

View File

@@ -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));

View File

@@ -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,6 +90,7 @@ 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>
<tbody>
<tr>
<th>{t("search.syntax.exampleQueries.table.description")}</th>
<th>{t("search.syntax.exampleQueries.table.query")}</th>
@@ -100,6 +103,7 @@ const Examples: FC<ExampleProps> = ({ searchableType }) => {
<td>{example.explanation}</td>
</tr>
))}
</tbody>
</table>
</>
);
@@ -126,6 +130,7 @@ const SearchableTypes: FC = () => {
header={t(`plugins:search.types.${searchableType.name}.title`, searchableType.name)}
>
<table>
<tbody>
<tr>
<th>{t("search.syntax.fields.name")}</th>
<th>{t("search.syntax.fields.type")}</th>
@@ -148,6 +153,7 @@ const SearchableTypes: FC = () => {
</td>
</tr>
))}
</tbody>
</table>
<Examples searchableType={searchableType} />
</Expandable>

View File

@@ -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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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