Merged in feature/improved-navi (pull request #152)

Feature/improved navi
This commit is contained in:
Sebastian Sdorra
2019-02-06 14:10:37 +00:00
64 changed files with 1385 additions and 1296 deletions

View File

@@ -127,6 +127,7 @@ class RepositoryConfig extends React.Component<Props, State> {
disabled={!this.state.selectedBranchName}
/>
</form>
<hr />
</>
);
} else {

View File

@@ -27,14 +27,9 @@ binder.bind(
);
binder.bind("repos.repository-avatar", GitAvatar, gitPredicate);
cfgBinder.bindRepository(
"/configuration",
"scm-git-plugin.repo-config.link",
"configuration",
RepositoryConfig
);
// global config
binder.bind("repo-config.route", RepositoryConfig, gitPredicate);
// global config
cfgBinder.bindGlobal(
"/git",
"scm-git-plugin.config.link",

View File

@@ -72,6 +72,33 @@ class ConfigurationBinder {
binder.bind("repository.route", RepoRoute, repoPredicate);
}
bindRepositorySetting(to: string, labelI18nKey: string, linkName: string, RepositoryComponent: any) {
// create predicate based on the link name of the current repository route
// if the linkname is not available, the navigation link and the route are not bound to the extension points
const repoPredicate = (props: Object) => {
return props.repository && props.repository._links && props.repository._links[linkName];
};
// create NavigationLink with translated label
const RepoNavLink = translate(this.i18nNamespace)(({t, url}) => {
return this.navLink(url + "/settings" + to, labelI18nKey, t);
});
// bind navigation link to extension point
binder.bind("repository.subnavigation", RepoNavLink, repoPredicate);
// route for global configuration, passes the current repository to component
const RepoRoute = ({url, repository, ...additionalProps}) => {
const link = repository._links[linkName].href;
return this.route(url + "/settings" + to, <RepositoryComponent repository={repository} link={link} {...additionalProps}/>);
};
// bind config route to extension point
binder.bind("repository.route", RepoRoute, repoPredicate);
}
}
export default new ConfigurationBinder();

View File

@@ -28,7 +28,7 @@ class NavLink extends React.Component<Props> {
let showIcon = null;
if (icon) {
showIcon = (<><i className={icon}></i>{" "}</>);
showIcon = (<><i className={icon} />{" "}</>);
}
return (

View File

@@ -0,0 +1,65 @@
//@flow
import * as React from "react";
import { Link, Route } from "react-router-dom";
type Props = {
to: string,
icon?: string,
label: string,
activeOnlyWhenExact?: boolean,
activeWhenMatch?: (route: any) => boolean,
children?: React.Node
};
class SubNavigation extends React.Component<Props> {
static defaultProps = {
activeOnlyWhenExact: false
};
isActive(route: any) {
const { activeWhenMatch } = this.props;
return route.match || (activeWhenMatch && activeWhenMatch(route));
}
renderLink = (route: any) => {
const { to, icon, label } = this.props;
let defaultIcon = "fas fa-cog";
if (icon) {
defaultIcon = icon;
}
let children = null;
if (this.isActive(route)) {
children = <ul className="sub-menu">{this.props.children}</ul>;
}
return (
<li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
<i className={defaultIcon} /> {label}
</Link>
{children}
</li>
);
};
render() {
const { to, activeOnlyWhenExact } = this.props;
// removes last part of url
let parents = to.split("/");
parents.splice(-1, 1);
let parent = parents.join("/");
return (
<Route
path={parent}
exact={activeOnlyWhenExact}
children={this.renderLink}
/>
);
}
}
export default SubNavigation;

View File

@@ -3,6 +3,7 @@
export { default as NavAction } from "./NavAction.js";
export { default as NavLink } from "./NavLink.js";
export { default as Navigation } from "./Navigation.js";
export { default as SubNavigation } from "./SubNavigation.js";
export { default as PrimaryNavigation } from "./PrimaryNavigation.js";
export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink.js";
export { default as Section } from "./Section.js";

View File

@@ -25,7 +25,7 @@ class ChangesetDiff extends React.Component<Props> {
render() {
const { changeset, t } = this.props;
if (!this.isDiffSupported(changeset)) {
return <Notification type="danger">{t("changesets.diff.not-supported")}</Notification>;
return <Notification type="danger">{t("changesets.changeset.diffNotSupported")}</Notification>;
} else {
const url = this.createUrl(changeset);
return <LoadingDiff url={url} />;

View File

@@ -43,8 +43,10 @@
"previous": "Zurück"
},
"profile": {
"navigation-label": "Navigation",
"actions-label": "Aktionen",
"navigationLabel": "Profil Navigation",
"informationNavLink": "Information",
"changePasswordNavLink": "Passwort ändern",
"settingsNavLink": "Einstellungen",
"username": "Benutzername",
"displayName": "Anzeigename",
"mail": "E-Mail",

View File

@@ -1,12 +1,10 @@
{
"config": {
"navigation-title": "Navigation"
},
"global-config": {
"navigationLabel": "Einstellungs Navigation",
"globalConfigurationNavLink": "Globale Einstellungen",
"title": "Einstellungen",
"navigation-label": "Globale Einstellungen",
"error-title": "Fehler",
"error-subtitle": "Unbekannter Einstellungen Fehler"
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Einstellungen Fehler"
},
"config-form": {
"submit": "Speichern",

View File

@@ -11,13 +11,16 @@
"title": "Gruppen",
"subtitle": "Verwaltung der Gruppen"
},
"single-group": {
"error-title": "Fehler",
"error-subtitle": "Unbekannter Gruppen Fehler",
"navigation-label": "Navigation",
"actions-label": "Aktionen",
"information-label": "Informationen",
"back-label": "Zurück"
"singleGroup": {
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Gruppen Fehler",
"menu": {
"navigationLabel": "Gruppen Navigation",
"informationNavLink": "Informationen",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",
"setPermissionsNavLink": "Berechtigungen"
}
},
"add-group": {
"title": "Gruppe erstellen",
@@ -44,27 +47,25 @@
"loading": "Suche...",
"no-options": "Kein Vorschlag für Benutzername verfügbar"
},
"group-form": {
"groupForm": {
"subtitle": "Gruppe bearbeiten",
"submit": "Speichern",
"name-error": "Name ist ungültig",
"description-error": "Beschreibung ist ungültig",
"nameError": "Name ist ungültig",
"descriptionError": "Beschreibung ist ungültig",
"help": {
"nameHelpText": "Eindeutiger Name der Gruppe",
"descriptionHelpText": "Eine kurze Beschreibung der Gruppe",
"memberHelpText": "Benutzername des Mitglieds der Gruppe"
}
},
"delete-group-button": {
"label": "Löschen",
"confirm-alert": {
"deleteGroup": {
"subtitle": "Gruppe löschen",
"button": "Löschen",
"confirmAlert": {
"title": "Gruppe löschen",
"message": "Soll die Gruppe wirklich gelöscht werden?",
"submit": "Ja",
"cancel": "Nein"
}
},
"set-permissions-button": {
"label": "Berechtigungen ändern"
}
}

View File

@@ -1,8 +1,6 @@
{
"form": {
"submit-button": {
"label": "Berechtigungen speichern"
},
"set-permissions-successful": "Berechtigungen erfolgreich gespeichert"
"setPermissions": {
"button": "Berechtigungen speichern",
"setPermissionsSuccessful": "Berechtigungen erfolgreich gespeichert"
}
}

View File

@@ -11,41 +11,55 @@
"name-invalid": "Der Name des Repository ist ungültig",
"contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein"
},
"help": {
"nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.",
"typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).",
"contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.",
"descriptionHelpText": "Eine kurze Beschreibung des Repository."
},
"repositoryRoot": {
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Repository Fehler",
"menu": {
"navigationLabel": "Repository Navigation",
"informationNavLink": "Informationen",
"historyNavLink": "Commits",
"sourcesNavLink": "Sources",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",
"permissionsNavLink": "Berechtigungen"
}
},
"overview": {
"title": "Repositories",
"subtitle": "Übersicht aller verfügbaren Repositories",
"create-button": "Repository erstellen"
},
"repository-root": {
"error-title": "Fehler",
"error-subtitle": "Unbekannter Repository Fehler",
"actions-label": "Aktionen",
"back-label": "Zurück",
"navigation-label": "Navigation",
"history": "Commits",
"information": "Informationen",
"permissions": "Berechtigungen",
"sources": "Sources"
"createButton": "Repository erstellen"
},
"create": {
"title": "Repository erstellen",
"subtitle": "Erstellen eines neuen Repository"
},
"repository-form": {
"submit": "Speichern"
"changesets": {
"errorTitle": "Fehler",
"errorSubtitle": "Changesets konnten nicht abgerufen werden",
"branchSelectorLabel": "Branches",
"changeset": {
"description": "Beschreibung",
"summary": "Changeset {{id}} wurde committet {{time}}",
"diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt",
"id": "ID",
"contact": "Kontakt",
"date": "Datum"
},
"edit-nav-link": {
"label": "Bearbeiten"
},
"delete-nav-action": {
"label": "Löschen",
"confirm-alert": {
"title": "Repository löschen",
"message": "Soll das Repository wirklich gelöscht werden?",
"submit": "Ja",
"cancel": "Nein"
"author": {
"name": "Autor",
"mail": "Mail"
}
},
"repositoryForm": {
"subtitle": "Repository bearbeiten",
"submit": "Speichern"
},
"sources": {
"file-tree": {
"name": "Name",
@@ -65,27 +79,6 @@
"size": "Größe"
}
},
"changesets": {
"diff": {
"not-supported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt"
},
"error-title": "Fehler",
"error-subtitle": "Changesets konnten nicht abgerufen werden",
"changeset": {
"id": "ID",
"description": "Beschreibung",
"contact": "Kontakt",
"date": "Datum",
"summary": "Changeset {{id}} wurde committet {{time}}"
},
"author": {
"name": "Autor",
"mail": "Mail"
}
},
"branch-selector": {
"label": "Branches"
},
"permission": {
"user": "Benutzer",
"group": "Gruppe",
@@ -98,7 +91,7 @@
"user-permission": "Benutzerberechtigung",
"edit-permission": {
"delete-button": "Löschen",
"save-button": "Änderungen Speichern"
"save-button": "Änderungen speichern"
},
"advanced-button": {
"label": "Erweitert"
@@ -138,10 +131,14 @@
}
}
},
"help": {
"nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.",
"typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).",
"contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.",
"descriptionHelpText": "Eine kurze Beschreibung des Repository."
"deleteRepo": {
"subtitle": "Repository löschen",
"button": "Löschen",
"confirmAlert": {
"title": "Repository löschen",
"message": "Soll das Repository wirklich gelöscht werden?",
"submit": "Ja",
"cancel": "Nein"
}
}
}

View File

@@ -10,59 +10,55 @@
"creationDate": "Erstellt",
"lastModified": "Zuletzt bearbeitet"
},
"users": {
"title": "Benutzer",
"subtitle": "Verwaltung der Benutzer"
},
"create-user-button": {
"label": "Benutzer erstellen"
},
"delete-user-button": {
"label": "Löschen",
"confirm-alert": {
"title": "Benutzer löschen",
"message": "Soll der Benutzer wirklich gelöscht werden?",
"submit": "Ja",
"cancel": "Nein"
}
},
"edit-user-button": {
"label": "Bearbeiten"
},
"set-password-button": {
"label": "Passwort ändern"
},
"set-permissions-button": {
"label": "Berechtigungen ändern"
},
"user-form": {
"submit": "Speichern"
},
"add-user": {
"title": "Benutzer erstellen",
"subtitle": "Erstellen eines neuen Benutzers"
},
"single-user": {
"error-title": "Fehler",
"error-subtitle": "Unbekannter Benutzer Fehler",
"navigation-label": "Navigation",
"actions-label": "Aktionen",
"information-label": "Informationen",
"back-label": "Zurück"
},
"validation": {
"mail-invalid": "Diese E-Mail ist ungültig",
"name-invalid": "Dieser Name ist ungültig",
"displayname-invalid": "Dieser Anzeigename ist ungültig"
},
"password": {
"set-password-successful": "Das Passwort wurde erfolgreich gespeichert."
},
"help": {
"usernameHelpText": "Einzigartiger Name des Benutzers",
"displayNameHelpText": "Anzeigename des Benutzers",
"mailHelpText": "E-Mail Adresse des Benutzers",
"adminHelpText": "Ein Administrator kann Repositories, Gruppen und Benutzer erstellen, bearbeiten und löschen.",
"activeHelpText": "Aktivierung oder Deaktivierung eines Benutzers"
},
"users": {
"title": "Benutzer",
"subtitle": "Verwaltung der Benutzer",
"createButton": "Benutzer erstellen"
},
"singleUser": {
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Benutzer Fehler",
"menu": {
"navigationLabel": "Benutzer Navigation",
"informationNavLink": "Informationen",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",
"setPasswordNavLink": "Passwort",
"setPermissionsNavLink": "Berechtigungen"
}
},
"addUser": {
"title": "Benutzer erstellen",
"subtitle": "Erstellen eines neuen Benutzers"
},
"deleteUser": {
"subtitle": "Benutzer löschen",
"button": "Löschen",
"confirmAlert": {
"title": "Benutzer löschen",
"message": "Soll der Benutzer wirklich gelöscht werden?",
"submit": "Ja",
"cancel": "Nein"
}
},
"singleUserPassword": {
"button": "Passwort setzen",
"setPasswordSuccessful": "Das Passwort wurde erfolgreich gespeichert."
},
"userForm": {
"subtitle": "Benutzer bearbeiten",
"button": "Speichern"
}
}

View File

@@ -43,8 +43,10 @@
"previous": "Previous"
},
"profile": {
"navigation-label": "Navigation",
"actions-label": "Actions",
"navigationLabel": "Profile Navigation",
"informationNavLink": "Information",
"changePasswordNavLink": "Change password",
"settingsNavLink": "Settings",
"username": "Username",
"displayName": "Display Name",
"mail": "E-Mail",
@@ -67,6 +69,6 @@
"passwordInvalid": "Password has to be between 6 and 32 characters",
"passwordConfirmFailed": "Passwords have to be identical",
"submit": "Submit",
"changedSuccessfully": "Password successfully changed"
"changedSuccessfully": "Password changed successfully"
}
}

View File

@@ -1,12 +1,10 @@
{
"config": {
"navigation-title": "Navigation"
},
"global-config": {
"navigationLabel": "Configuration Navigation",
"globalConfigurationNavLink": "Global Configuration",
"title": "Configuration",
"navigation-label": "Global Configuration",
"error-title": "Error",
"error-subtitle": "Unknown Config Error"
"errorTitle": "Error",
"errorSubtitle": "Unknown Config Error"
},
"config-form": {
"submit": "Submit",

View File

@@ -11,13 +11,16 @@
"title": "Groups",
"subtitle": "Create, read, update and delete groups"
},
"single-group": {
"error-title": "Error",
"error-subtitle": "Unknown group error",
"navigation-label": "Navigation",
"actions-label": "Actions",
"information-label": "Information",
"back-label": "Back"
"singleGroup": {
"errorTitle": "Error",
"errorSubtitle": "Unknown group error",
"menu": {
"navigationLabel": "Group Navigation",
"informationNavLink": "Information",
"settingsNavLink": "Settings",
"generalNavLink": "General",
"setPermissionsNavLink": "Permissions"
}
},
"add-group": {
"title": "Create Group",
@@ -44,27 +47,25 @@
"loading": "Loading...",
"no-options": "No suggestion available"
},
"group-form": {
"groupForm": {
"subtitle": "Edit Group",
"submit": "Submit",
"name-error": "Group name is invalid",
"description-error": "Description is invalid",
"nameError": "Group name is invalid",
"descriptionError": "Description is invalid",
"help": {
"nameHelpText": "Unique name of the group",
"descriptionHelpText": "A short description of the group",
"memberHelpText": "Usernames of the group members"
}
},
"delete-group-button": {
"label": "Delete",
"confirm-alert": {
"deleteGroup": {
"subtitle": "Delete Group",
"button": "Delete",
"confirmAlert": {
"title": "Delete Group",
"message": "Do you really want to delete the group?",
"submit": "Yes",
"cancel": "No"
}
},
"set-permissions-button": {
"label": "Set Permissions"
}
}

View File

@@ -1,8 +1,6 @@
{
"form": {
"submit-button": {
"label": "Set Permissions"
},
"set-permissions-successful": "Permissions set successfully"
"setPermissions": {
"button": "Set permissions",
"setPermissionsSuccessful": "Permissions set successfully"
}
}

View File

@@ -11,41 +11,55 @@
"name-invalid": "The repository name is invalid",
"contact-invalid": "Contact must be a valid mail address"
},
"help": {
"nameHelpText": "The name of the repository. This name will be part of the repository url.",
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
"contactHelpText": "Email address of the person who is responsible for this repository.",
"descriptionHelpText": "A short description of the repository."
},
"repositoryRoot": {
"errorTitle": "Error",
"errorSubtitle": "Unknown repository error",
"menu": {
"navigationLabel": "Repository Navigation",
"informationNavLink": "Information",
"historyNavLink": "Commits",
"sourcesNavLink": "Sources",
"settingsNavLink": "Settings",
"generalNavLink": "General",
"permissionsNavLink": "Permissions"
}
},
"overview": {
"title": "Repositories",
"subtitle": "Overview of available repositories",
"create-button": "Create Repository"
},
"repository-root": {
"error-title": "Error",
"error-subtitle": "Unknown repository error",
"actions-label": "Actions",
"back-label": "Back",
"navigation-label": "Navigation",
"history": "Commits",
"information": "Information",
"permissions": "Permissions",
"sources": "Sources"
"createButton": "Create Repository"
},
"create": {
"title": "Create Repository",
"subtitle": "Create a new repository"
},
"repository-form": {
"submit": "Save"
"changesets": {
"errorTitle": "Error",
"errorSubtitle": "Could not fetch changesets",
"branchSelectorLabel": "Branches",
"changeset": {
"description": "Description",
"summary": "Changeset {{id}} was committed {{time}}",
"diffNotSupported": "Diff of changesets is not supported by the type of repository",
"id": "ID",
"contact": "Contact",
"date": "Date"
},
"edit-nav-link": {
"label": "Edit"
},
"delete-nav-action": {
"label": "Delete",
"confirm-alert": {
"title": "Delete Repository",
"message": "Do you really want to delete the repository?",
"submit": "Yes",
"cancel": "No"
"author": {
"name": "Author",
"mail": "Mail"
}
},
"repositoryForm": {
"subtitle": "Edit Repository",
"submit": "Save"
},
"sources": {
"file-tree": {
"name": "Name",
@@ -65,27 +79,6 @@
"size": "Size"
}
},
"changesets": {
"diff": {
"not-supported": "Diff of changesets is not supported by the type of repository"
},
"error-title": "Error",
"error-subtitle": "Could not fetch changesets",
"changeset": {
"id": "ID",
"description": "Description",
"contact": "Contact",
"date": "Date",
"summary": "Changeset {{id}} was committed {{time}}"
},
"author": {
"name": "Author",
"mail": "Mail"
}
},
"branch-selector": {
"label": "Branches"
},
"permission": {
"user": "User",
"group": "Group",
@@ -106,7 +99,7 @@
"delete-permission-button": {
"label": "Delete",
"confirm-alert": {
"title": "Delete Permission",
"title": "Delete permission",
"message": "Do you really want to delete the permission?",
"submit": "Yes",
"cancel": "No"
@@ -138,10 +131,14 @@
}
}
},
"help": {
"nameHelpText": "The name of the repository. This name will be part of the repository url.",
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
"contactHelpText": "Email address of the person who is responsible for this repository.",
"descriptionHelpText": "A short description of the repository."
"deleteRepo": {
"subtitle": "Delete Repository",
"button": "Delete",
"confirmAlert": {
"title": "Delete repository",
"message": "Do you really want to delete the repository?",
"submit": "Yes",
"cancel": "No"
}
}
}

View File

@@ -10,59 +10,55 @@
"creationDate": "Creation Date",
"lastModified": "Last Modified"
},
"users": {
"title": "Users",
"subtitle": "Create, read, update and delete users"
},
"create-user-button": {
"label": "Create User"
},
"delete-user-button": {
"label": "Delete",
"confirm-alert": {
"title": "Delete User",
"message": "Do you really want to delete the user?",
"submit": "Yes",
"cancel": "No"
}
},
"edit-user-button": {
"label": "Edit"
},
"set-password-button": {
"label": "Set Password"
},
"set-permissions-button": {
"label": "Set Permissions"
},
"user-form": {
"submit": "Submit"
},
"add-user": {
"title": "Create User",
"subtitle": "Create a new user"
},
"single-user": {
"error-title": "Error",
"error-subtitle": "Unknown user error",
"navigation-label": "Navigation",
"actions-label": "Actions",
"information-label": "Information",
"back-label": "Back"
},
"validation": {
"mail-invalid": "This email is invalid",
"name-invalid": "This name is invalid",
"displayname-invalid": "This displayname is invalid"
},
"password": {
"set-password-successful": "Password successfully set"
},
"help": {
"usernameHelpText": "Unique name of the user.",
"displayNameHelpText": "Display name of the user.",
"mailHelpText": "Email address of the user.",
"adminHelpText": "An administrator is able to create, modify and delete repositories, groups and users.",
"activeHelpText": "Activate or deactivate the user."
},
"users": {
"title": "Users",
"subtitle": "Create, read, update and delete users",
"createButton": "Create User"
},
"singleUser": {
"errorTitle": "Error",
"errorSubtitle": "Unknown user error",
"menu": {
"navigationLabel": "User Navigation",
"informationNavLink": "Information",
"settingsNavLink": "Settings",
"generalNavLink": "General",
"setPasswordNavLink": "Password",
"setPermissionsNavLink": "Permissions"
}
},
"addUser": {
"title": "Create User",
"subtitle": "Create a new user"
},
"deleteUser": {
"subtitle": "Delete User",
"button": "Delete",
"confirmAlert": {
"title": "Delete user",
"message": "Do you really want to delete the user?",
"submit": "Yes",
"cancel": "No"
}
},
"singleUserPassword": {
"button": "Set password",
"setPasswordSuccessful": "Password successfully set"
},
"userForm": {
"subtitle": "Edit User",
"button": "Submit"
}
}

View File

@@ -8,8 +8,8 @@ import type { Links } from "@scm-manager/ui-types";
import { Page, Navigation, NavLink, Section } from "@scm-manager/ui-components";
import GlobalConfig from "./GlobalConfig";
import type { History } from "history";
import {connect} from "react-redux";
import {compose} from "redux";
import { connect } from "react-redux";
import { compose } from "redux";
import { getLinks } from "../../modules/indexResource";
type Props = {
@@ -47,19 +47,21 @@ class Config extends React.Component<Props> {
<div className="columns">
<div className="column is-three-quarters">
<Route path={url} exact component={GlobalConfig} />
<ExtensionPoint name="config.route"
<ExtensionPoint
name="config.route"
props={extensionProps}
renderAll={true}
/>
</div>
<div className="column is-one-quarter">
<Navigation>
<Section label={t("config.navigation-title")}>
<Section label={t("config.navigationLabel")}>
<NavLink
to={`${url}`}
label={t("global-config.navigation-label")}
label={t("config.globalConfigurationNavLink")}
/>
<ExtensionPoint name="config.navigation"
<ExtensionPoint
name="config.navigation"
props={extensionProps}
renderAll={true}
/>
@@ -83,4 +85,3 @@ export default compose(
connect(mapStateToProps),
translate("config")
)(Config);

View File

@@ -78,8 +78,8 @@ class GlobalConfig extends React.Component<Props, State> {
if (error) {
return (
<ErrorPage
title={t("global-config.error-title")}
subtitle={t("global-config.error-subtitle")}
title={t("config.errorTitle")}
subtitle={t("config.errorSubtitle")}
error={error}
configUpdatePermission={configUpdatePermission}
/>
@@ -91,7 +91,7 @@ class GlobalConfig extends React.Component<Props, State> {
return (
<div>
<Title title={t("global-config.title")} />
<Title title={t("config.title")} />
{this.renderConfigChangedNotification()}
<ConfigForm
submitForm={config => this.modifyConfig(config)}

View File

@@ -12,11 +12,13 @@ import {
ErrorPage,
Page,
Navigation,
SubNavigation,
Section,
NavLink
} from "@scm-manager/ui-components";
import ChangeUserPassword from "./ChangeUserPassword";
import ProfileInfo from "./ProfileInfo";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
type Props = {
me: Me,
@@ -57,26 +59,43 @@ class Profile extends React.Component<Props, State> {
);
}
const extensionProps = {
me,
url
};
return (
<Page title={me.displayName}>
<div className="columns">
<div className="column is-three-quarters">
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
<Route
path={`${url}/password`}
path={`${url}/settings/password`}
render={() => <ChangeUserPassword me={me} />}
/>
</div>
<div className="column">
<Navigation>
<Section label={t("profile.navigation-label")}>
<NavLink to={`${url}`} icon="fas fa-info-circle" label={t("profile.information")} />
</Section>
<Section label={t("profile.actions-label")}>
<Section label={t("profile.navigationLabel")}>
<NavLink
to={`${url}/password`}
label={t("profile.change-password")}
to={`${url}`}
icon="fas fa-info-circle"
label={t("profile.informationNavLink")}
/>
<SubNavigation
to={`${url}/settings/password`}
label={t("profile.settingsNavLink")}
>
<NavLink
to={`${url}/settings/password`}
label={t("profile.changePasswordNavLink")}
/>
<ExtensionPoint
name="profile.subnavigation"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</Section>
</Navigation>
</div>

View File

@@ -42,16 +42,6 @@ class ProfileInfo extends React.Component<Props, State> {
<MailLink address={me.mail} />
</td>
</tr>
<tr>
<td className="has-text-weight-semibold">{t("profile.groups")}</td>
<td className="content">
<ul>
{me.groups.map((group) => {
return <li>{group}</li>;
})}
</ul>
</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -2,6 +2,7 @@
import React from "react";
import { translate } from "react-i18next";
import {
Subtitle,
AutocompleteAddEntryToTableField,
LabelWithHelpIcon,
MemberNameTable,
@@ -71,36 +72,43 @@ class GroupForm extends React.Component<Props, State> {
};
render() {
const { t, loading } = this.props;
const { loading, t } = this.props;
const { group } = this.state;
let nameField = null;
let subtitle = null;
if (!this.props.group) {
// create new group
nameField = (
<InputField
label={t("group.name")}
errorMessage={t("group-form.name-error")}
errorMessage={t("groupForm.nameError")}
onChange={this.handleGroupNameChange}
value={group.name}
validationError={this.state.nameValidationError}
helpText={t("group-form.help.nameHelpText")}
helpText={t("groupForm.help.nameHelpText")}
/>
);
} else {
// edit existing group
subtitle = <Subtitle subtitle={t("groupForm.subtitle")} />;
}
return (
<>
{subtitle}
<form onSubmit={this.submit}>
{nameField}
<Textarea
label={t("group.description")}
errorMessage={t("group-form.description-error")}
errorMessage={t("groupForm.descriptionError")}
onChange={this.handleDescriptionChange}
value={group.description}
validationError={false}
helpText={t("group-form.help.descriptionHelpText")}
helpText={t("groupForm.help.descriptionHelpText")}
/>
<LabelWithHelpIcon
label={t("group.members")}
helpText={t("group-form.help.memberHelpText")}
helpText={t("groupForm.help.memberHelpText")}
/>
<MemberNameTable
members={group.members}
@@ -120,10 +128,11 @@ class GroupForm extends React.Component<Props, State> {
/>
<SubmitButton
disabled={!this.isValid()}
label={t("group-form.submit")}
label={t("groupForm.submit")}
loading={loading}
/>
</form>
</>
);
}

View File

@@ -1,56 +0,0 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { Group } from "@scm-manager/ui-types";
import { NavAction, confirmAlert } from "@scm-manager/ui-components";
type Props = {
group: Group,
confirmDialog?: boolean,
t: string => string,
deleteGroup: (group: Group) => void
};
export class DeleteGroupNavLink extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
deleteGroup = () => {
this.props.deleteGroup(this.props.group);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("delete-group-button.confirm-alert.title"),
message: t("delete-group-button.confirm-alert.message"),
buttons: [
{
label: t("delete-group-button.confirm-alert.submit"),
onClick: () => this.deleteGroup(),
},
{
label: t("delete-group-button.confirm-alert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.group._links.delete;
};
render() {
const { confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteGroup;
if (!this.isDeletable()) {
return null;
}
return <NavAction icon="fas fa-times" label={t("delete-group-button.label")} action={action} />;
}
}
export default translate("groups")(DeleteGroupNavLink);

View File

@@ -1,82 +0,0 @@
import React from "react";
import { mount, shallow } from "enzyme";
import "../../../tests/enzyme";
import "../../../tests/i18n";
import DeleteGroupNavLink from "./DeleteGroupNavLink";
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
NavAction: require.requireActual("@scm-manager/ui-components").NavAction
}));
describe("DeleteGroupNavLink", () => {
it("should render nothing, if the delete link is missing", () => {
const group = {
_links: {}
};
const navLink = shallow(
<DeleteGroupNavLink group={group} deleteGroup={() => {}} />
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const group = {
_links: {
delete: {
href: "/groups"
}
}
};
const navLink = mount(
<DeleteGroupNavLink group={group} deleteGroup={() => {}} />
);
expect(navLink.text()).not.toBe("");
});
it("should open the confirm dialog on navLink click", () => {
const group = {
_links: {
delete: {
href: "/groups"
}
}
};
const navLink = mount(
<DeleteGroupNavLink group={group} deleteGroup={() => {}} />
);
navLink.find("a").simulate("click");
expect(confirmAlert.mock.calls.length).toBe(1);
});
it("should call the delete group function with delete url", () => {
const group = {
_links: {
delete: {
href: "/groups"
}
}
};
let calledUrl = null;
function capture(group) {
calledUrl = group._links.delete.href;
}
const navLink = mount(
<DeleteGroupNavLink
group={group}
confirmDialog={false}
deleteGroup={capture}
/>
);
navLink.find("a").simulate("click");
expect(calledUrl).toBe("/groups");
});
});

View File

@@ -1,29 +1,28 @@
//@flow
import React from "react";
import type { Group } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import type { Group } from "@scm-manager/ui-types";
type Props = {
t: string => string,
group: Group,
editUrl: string,
group: Group
t: string => string
};
type State = {};
class EditGroupNavLink extends React.Component<Props, State> {
render() {
const { t, editUrl } = this.props;
if (!this.isEditable()) {
return null;
}
return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-group-button.label")} />;
}
class EditGroupNavLink extends React.Component<Props> {
isEditable = () => {
return this.props.group._links.update;
};
render() {
const { t, editUrl } = this.props;
if (!this.isEditable()) {
return null;
}
return <NavLink to={editUrl} label={t("singleGroup.menu.generalNavLink")} />;
}
}
export default translate("groups")(EditGroupNavLink);

View File

@@ -17,7 +17,7 @@ class ChangePermissionNavLink extends React.Component<Props> {
if (!this.hasPermissionToSetPermission()) {
return null;
}
return <NavLink to={permissionsUrl} label={t("set-permissions-button.label")} />;
return <NavLink to={permissionsUrl} label={t("singleGroup.menu.setPermissionsNavLink")} />;
}
hasPermissionToSetPermission = () => {

View File

@@ -1,3 +1,2 @@
export { default as DeleteGroupNavLink } from "./DeleteGroupNavLink";
export { default as EditGroupNavLink } from "./EditGroupNavLink";
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";

View File

@@ -0,0 +1,113 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { Group } from "@scm-manager/ui-types";
import {
Subtitle,
DeleteButton,
confirmAlert,
ErrorNotification
} from "@scm-manager/ui-components";
import {
deleteGroup,
getDeleteGroupFailure,
isDeleteGroupPending
} from "../modules/groups";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import type { History } from "history";
type Props = {
loading: boolean,
error: Error,
group: Group,
confirmDialog?: boolean,
deleteGroup: (group: Group, callback?: () => void) => void,
// context props
history: History,
t: string => string
};
export class DeleteGroup extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
deleteGroup = () => {
this.props.deleteGroup(this.props.group, this.groupDeleted);
};
groupDeleted = () => {
this.props.history.push("/groups");
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteGroup.confirmAlert.title"),
message: t("deleteGroup.confirmAlert.message"),
buttons: [
{
label: t("deleteGroup.confirmAlert.submit"),
onClick: () => this.deleteGroup()
},
{
label: t("deleteGroup.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.group._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteGroup;
if (!this.isDeletable()) {
return null;
}
return (
<>
<Subtitle subtitle={t("deleteGroup.subtitle")} />
<ErrorNotification error={error} />
<div className="columns">
<div className="column">
<DeleteButton
label={t("deleteGroup.button")}
action={action}
loading={loading}
/>
</div>
</div>
</>
);
}
}
const mapStateToProps = (state, ownProps) => {
const loading = isDeleteGroupPending(state, ownProps.group.name);
const error = getDeleteGroupFailure(state, ownProps.group.name);
return {
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
deleteGroup: (group: Group, callback?: () => void) => {
dispatch(deleteGroup(group, callback));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(translate("groups")(DeleteGroup)));

View File

@@ -3,9 +3,9 @@ import React from "react";
import { connect } from "react-redux";
import GroupForm from "../components/GroupForm";
import {
modifyGroup,
getModifyGroupFailure,
isModifyGroupPending,
modifyGroup,
modifyGroupReset
} from "../modules/groups";
import type { History } from "history";
@@ -13,12 +13,13 @@ import { withRouter } from "react-router-dom";
import type { Group } from "@scm-manager/ui-types";
import { ErrorNotification } from "@scm-manager/ui-components";
import { getUserAutoCompleteLink } from "../../modules/indexResource";
import DeleteGroup from "./DeleteGroup";
type Props = {
group: Group,
fetchGroup: (name: string) => void,
modifyGroup: (group: Group, callback?: () => void) => void,
modifyGroupReset: Group => void,
fetchGroup: (name: string) => void,
autocompleteLink: string,
history: History,
loading?: boolean,
@@ -54,7 +55,7 @@ class EditGroup extends React.Component<Props> {
};
render() {
const { group, loading, error } = this.props;
const { loading, error, group } = this.props;
return (
<div>
<ErrorNotification error={error} />
@@ -66,6 +67,8 @@ class EditGroup extends React.Component<Props> {
loading={loading}
loadUserSuggestions={this.loadUserAutocompletion}
/>
<hr />
<DeleteGroup group={group} />
</div>
);
}

View File

@@ -6,33 +6,30 @@ import {
ErrorPage,
Loading,
Navigation,
SubNavigation,
Section,
NavLink
} from "@scm-manager/ui-components";
import { Route } from "react-router";
import { Details } from "./../components/table";
import {
DeleteGroupNavLink,
EditGroupNavLink,
SetPermissionsNavLink
} from "./../components/navLinks";
import type { Group } from "@scm-manager/ui-types";
import type { History } from "history";
import {
deleteGroup,
fetchGroupByName,
getGroupByName,
isFetchGroupPending,
getFetchGroupFailure,
getDeleteGroupFailure,
isDeleteGroupPending
getFetchGroupFailure
} from "../modules/groups";
import { translate } from "react-i18next";
import EditGroup from "./EditGroup";
import { getGroupsLink } from "../../modules/indexResource";
import SetPermissions from "../../permissions/components/SetPermissions";
import {ExtensionPoint} from "@scm-manager/ui-extensions";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
type Props = {
name: string,
@@ -42,7 +39,6 @@ type Props = {
groupLink: string,
// dispatcher functions
deleteGroup: (group: Group, callback?: () => void) => void,
fetchGroupByName: (string, string) => void,
// context objects
@@ -63,14 +59,6 @@ class SingleGroup extends React.Component<Props> {
return url;
};
deleteGroup = (group: Group) => {
this.props.deleteGroup(group, this.groupDeleted);
};
groupDeleted = () => {
this.props.history.push("/groups");
};
matchedUrl = () => {
return this.stripEndingSlash(this.props.match.url);
};
@@ -81,8 +69,8 @@ class SingleGroup extends React.Component<Props> {
if (error) {
return (
<ErrorPage
title={t("single-group.error-title")}
subtitle={t("single-group.error-subtitle")}
title={t("singleGroup.errorTitle")}
subtitle={t("singleGroup.errorSubtitle")}
error={error}
/>
);
@@ -109,15 +97,17 @@ class SingleGroup extends React.Component<Props> {
component={() => <Details group={group} />}
/>
<Route
path={`${url}/edit`}
path={`${url}/settings/general`}
exact
component={() => <EditGroup group={group} />}
/>
<Route
path={`${url}/permissions`}
path={`${url}/settings/permissions`}
exact
component={() => (
<SetPermissions selectedPermissionsLink={group._links.permissions} />
<SetPermissions
selectedPermissionsLink={group._links.permissions}
/>
)}
/>
<ExtensionPoint
@@ -128,33 +118,35 @@ class SingleGroup extends React.Component<Props> {
</div>
<div className="column">
<Navigation>
<Section label={t("single-group.navigation-label")}>
<Section label={t("singleGroup.menu.navigationLabel")}>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("single-group.information-label")}
/>
<SetPermissionsNavLink
group={group}
permissionsUrl={`${url}/permissions`}
label={t("singleGroup.menu.informationNavLink")}
/>
<ExtensionPoint
name="group.navigation"
props={extensionProps}
renderAll={true}
/>
</Section>
<Section label={t("single-group.actions-label")}>
<DeleteGroupNavLink
<SubNavigation
to={`${url}/settings/general`}
label={t("singleGroup.menu.settingsNavLink")}
>
<EditGroupNavLink
group={group}
deleteGroup={this.deleteGroup}
editUrl={`${url}/settings/general`}
/>
<EditGroupNavLink group={group} editUrl={`${url}/edit`} />
<NavLink
to="/groups"
icon="fas fa-undo-alt"
label={t("single-group.back-label")}
<SetPermissionsNavLink
group={group}
permissionsUrl={`${url}/settings/permissions`}
/>
<ExtensionPoint
name="group.subnavigation"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</Section>
</Navigation>
</div>
@@ -167,10 +159,8 @@ class SingleGroup extends React.Component<Props> {
const mapStateToProps = (state, ownProps) => {
const name = ownProps.match.params.name;
const group = getGroupByName(state, name);
const loading =
isFetchGroupPending(state, name) || isDeleteGroupPending(state, name);
const error =
getFetchGroupFailure(state, name) || getDeleteGroupFailure(state, name);
const loading = isFetchGroupPending(state, name);
const error = getFetchGroupFailure(state, name);
const groupLink = getGroupsLink(state);
return {
@@ -186,9 +176,6 @@ const mapDispatchToProps = dispatch => {
return {
fetchGroupByName: (link: string, name: string) => {
dispatch(fetchGroupByName(link, name));
},
deleteGroup: (group: Group, callback?: () => void) => {
dispatch(deleteGroup(group, callback));
}
};
};

View File

@@ -113,7 +113,7 @@ class SetPermissions extends React.Component<Props, State> {
message = (
<Notification
type={"success"}
children={t("form.set-permissions-successful")}
children={t("setPermissions.setPermissionsSuccessful")}
onClose={() => this.onClose()}
/>
);
@@ -128,7 +128,7 @@ class SetPermissions extends React.Component<Props, State> {
<SubmitButton
disabled={!this.state.permissionsChanged}
loading={loading}
label={t("form.submit-button.label")}
label={t("setPermissions.button")}
/>
</form>
);

View File

@@ -1,58 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { NavAction, confirmAlert } from "@scm-manager/ui-components";
import type { Repository } from "@scm-manager/ui-types";
type Props = {
repository: Repository,
confirmDialog?: boolean,
delete: Repository => void,
// context props
t: string => string
};
class DeleteNavAction extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
delete = () => {
this.props.delete(this.props.repository);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("delete-nav-action.confirm-alert.title"),
message: t("delete-nav-action.confirm-alert.message"),
buttons: [
{
label: t("delete-nav-action.confirm-alert.submit"),
onClick: () => this.delete()
},
{
label: t("delete-nav-action.confirm-alert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.repository._links.delete;
};
render() {
const { confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.delete();
if (!this.isDeletable()) {
return null;
}
return <NavAction action={action} icon="fas fa-times" label={t("delete-nav-action.label")} />;
}
}
export default translate("repos")(DeleteNavAction);

View File

@@ -1,82 +0,0 @@
import React from "react";
import { mount, shallow } from "enzyme";
import "../../tests/enzyme";
import "../../tests/i18n";
import DeleteNavAction from "./DeleteNavAction";
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
NavAction: require.requireActual("@scm-manager/ui-components").NavAction
}));
describe("DeleteNavAction", () => {
it("should render nothing, if the delete link is missing", () => {
const repository = {
_links: {}
};
const navLink = shallow(
<DeleteNavAction repository={repository} delete={() => {}} />
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const repository = {
_links: {
delete: {
href: "/repositories"
}
}
};
const navLink = mount(
<DeleteNavAction repository={repository} delete={() => {}} />
);
expect(navLink.text()).not.toBe("");
});
it("should open the confirm dialog on navLink click", () => {
const repository = {
_links: {
delete: {
href: "/repositorys"
}
}
};
const navLink = mount(
<DeleteNavAction repository={repository} delete={() => {}} />
);
navLink.find("a").simulate("click");
expect(confirmAlert.mock.calls.length).toBe(1);
});
it("should call the delete repository function with delete url", () => {
const repository = {
_links: {
delete: {
href: "/repos"
}
}
};
let calledUrl = null;
function capture(repository) {
calledUrl = repository._links.delete.href;
}
const navLink = mount(
<DeleteNavAction
repository={repository}
confirmDialog={false}
delete={capture}
/>
);
navLink.find("a").simulate("click");
expect(calledUrl).toBe("/repos");
});
});

View File

@@ -1,22 +1,28 @@
//@flow
import React from "react";
import type { Repository } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import type { Repository } from "@scm-manager/ui-types";
type Props = { editUrl: string, t: string => string, repository: Repository };
type Props = {
repository: Repository,
editUrl: string,
t: string => string
};
class EditNavLink extends React.Component<Props> {
class EditRepoNavLink extends React.Component<Props> {
isEditable = () => {
return this.props.repository._links.update;
};
render() {
const { editUrl, t } = this.props;
if (!this.isEditable()) {
return null;
}
const { editUrl, t } = this.props;
return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-nav-link.label")} />;
return <NavLink to={editUrl} label={t("repositoryRoot.menu.generalNavLink")} />;
}
}
export default translate("repos")(EditNavLink);
export default translate("repos")(EditRepoNavLink);

View File

@@ -3,9 +3,9 @@ import { shallow, mount } from "enzyme";
import "../../tests/enzyme";
import "../../tests/i18n";
import ReactRouterEnzymeContext from "react-router-enzyme-context";
import EditNavLink from "./EditNavLink";
import EditRepoNavLink from "./EditRepoNavLink";
describe("EditNavLink", () => {
describe("GeneralNavLink", () => {
const options = new ReactRouterEnzymeContext();
it("should render nothing, if the modify link is missing", () => {
@@ -14,7 +14,7 @@ describe("EditNavLink", () => {
};
const navLink = shallow(
<EditNavLink repository={repository} editUrl="" />,
<EditRepoNavLink repository={repository} editUrl="" />,
options.get()
);
expect(navLink.text()).toBe("");
@@ -30,9 +30,9 @@ describe("EditNavLink", () => {
};
const navLink = mount(
<EditNavLink repository={repository} editUrl="" />,
<EditRepoNavLink repository={repository} editUrl="" />,
options.get()
);
expect(navLink.text()).toBe(" edit-nav-link.label");
expect(navLink.text()).toBe("repositoryRoot.menu.generalNavLink");
});
});

View File

@@ -20,7 +20,7 @@ class PermissionsNavLink extends React.Component<Props> {
}
const { permissionUrl, t } = this.props;
return (
<NavLink to={permissionUrl} icon="fas fa-lock" label={t("repository-root.permissions")} />
<NavLink to={permissionUrl} label={t("repositoryRoot.menu.permissionsNavLink")} />
);
}
}

View File

@@ -33,6 +33,6 @@ describe("PermissionsNavLink", () => {
<PermissionsNavLink repository={repository} permissionUrl="" />,
options.get()
);
expect(navLink.text()).toBe(" repository-root.permissions");
expect(navLink.text()).toBe("repositoryRoot.menu.permissionsNavLink");
});
});

View File

@@ -2,6 +2,7 @@
import React from "react";
import { translate } from "react-i18next";
import {
Subtitle,
InputField,
Select,
SubmitButton,
@@ -81,7 +82,15 @@ class RepositoryForm extends React.Component<Props, State> {
const { loading, t } = this.props;
const repository = this.state.repository;
let subtitle = null;
if (this.props.repository) {
// edit existing repo
subtitle = <Subtitle subtitle={t("repositoryForm.subtitle")} />;
}
return (
<>
{subtitle}
<form onSubmit={this.submit}>
{this.renderCreateOnlyFields()}
<InputField
@@ -102,9 +111,10 @@ class RepositoryForm extends React.Component<Props, State> {
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("repository-form.submit")}
label={t("repositoryForm.submit")}
/>
</form>
</>
);
}

View File

@@ -64,7 +64,7 @@ class RepositoryEntry extends React.Component<Props> {
return (
<RepositoryEntryLink
iconClass="fa-cog fa-lg"
to={repositoryLink + "/edit"}
to={repositoryLink + "/settings/general"}
/>
);
}

View File

@@ -37,8 +37,8 @@ class ChangesetView extends React.Component<Props> {
if (error) {
return (
<ErrorPage
title={t("changeset-error.title")}
subtitle={t("changeset-error.subtitle")}
title={t("changesets.errorTitle")}
subtitle={t("changesets.errorSubtitle")}
error={error}
/>
);

View File

@@ -101,7 +101,7 @@ class BranchRoot extends React.Component<Props> {
if (repository._links.branches) {
return (
<BranchSelector
label={t("branch-selector.label")}
label={t("changesets.branchSelectorLabel")}
branches={branches}
selectedBranch={selected}
selected={(b: Branch) => {

View File

@@ -0,0 +1,114 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { Repository } from "@scm-manager/ui-types";
import {
Subtitle,
DeleteButton,
confirmAlert,
ErrorNotification
} from "@scm-manager/ui-components";
import {
deleteRepo,
getDeleteRepoFailure,
isDeleteRepoPending
} from "../modules/repos";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import type { History } from "history";
type Props = {
loading: boolean,
error: Error,
repository: Repository,
confirmDialog?: boolean,
deleteRepo: (Repository, () => void) => void,
// context props
history: History,
t: string => string
};
class DeleteRepo extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
deleted = () => {
this.props.history.push("/repos");
};
deleteRepo = () => {
this.props.deleteRepo(this.props.repository, this.deleted);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteRepo.confirmAlert.title"),
message: t("deleteRepo.confirmAlert.message"),
buttons: [
{
label: t("deleteRepo.confirmAlert.submit"),
onClick: () => this.deleteRepo()
},
{
label: t("deleteRepo.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.repository._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteRepo;
if (!this.isDeletable()) {
return null;
}
return (
<>
<Subtitle subtitle={t("deleteRepo.subtitle")} />
<ErrorNotification error={error} />
<div className="columns">
<div className="column">
<DeleteButton
label={t("deleteRepo.button")}
action={action}
loading={loading}
/>
</div>
</div>
</>
);
}
}
const mapStateToProps = (state, ownProps) => {
const { namespace, name } = ownProps.repository;
const loading = isDeleteRepoPending(state, namespace, name);
const error = getDeleteRepoFailure(state, namespace, name);
return {
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
deleteRepo: (repo: Repository, callback: () => void) => {
dispatch(deleteRepo(repo, callback));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(translate("repos")(DeleteRepo)));

View File

@@ -1,8 +1,9 @@
// @flow
import React from "react";
import { connect } from "react-redux";
import { translate } from "react-i18next";
import { withRouter } from "react-router-dom";
import RepositoryForm from "../components/form";
import DeleteRepo from "./DeleteRepo";
import type { Repository } from "@scm-manager/ui-types";
import {
modifyRepo,
@@ -10,34 +11,55 @@ import {
getModifyRepoFailure,
modifyRepoReset
} from "../modules/repos";
import { withRouter } from "react-router-dom";
import type { History } from "history";
import { ErrorNotification } from "@scm-manager/ui-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
type Props = {
repository: Repository,
modifyRepo: (Repository, () => void) => void,
modifyRepoReset: Repository => void,
loading: boolean,
error: Error,
modifyRepo: (Repository, () => void) => void,
modifyRepoReset: Repository => void,
// context props
t: string => string,
history: History
repository: Repository,
history: History,
match: any
};
class Edit extends React.Component<Props> {
class EditRepo extends React.Component<Props> {
componentDidMount() {
const { modifyRepoReset, repository } = this.props;
modifyRepoReset(repository);
}
repoModified = () => {
const { history, repository } = this.props;
history.push(`/repo/${repository.namespace}/${repository.name}`);
};
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 2);
}
return url;
};
matchedUrl = () => {
return this.stripEndingSlash(this.props.match.url);
};
render() {
const { loading, error } = this.props;
const { loading, error, repository } = this.props;
const url = this.matchedUrl();
const extensionProps = {
repository,
url
};
return (
<div>
<ErrorNotification error={error} />
@@ -48,6 +70,13 @@ class Edit extends React.Component<Props> {
this.props.modifyRepo(repo, this.repoModified);
}}
/>
<hr />
<ExtensionPoint
name="repo-config.route"
props={extensionProps}
renderAll={true}
/>
<DeleteRepo repository={repository} />
</div>
);
}
@@ -77,4 +106,4 @@ const mapDispatchToProps = dispatch => {
export default connect(
mapStateToProps,
mapDispatchToProps
)(translate("repos")(withRouter(Edit)));
)(withRouter(EditRepo));

View File

@@ -90,7 +90,7 @@ class Overview extends React.Component<Props> {
if (showCreateButton) {
return (
<CreateButton
label={t("overview.create-button")}
label={t("overview.createButton")}
link="/repos/create"
/>
);

View File

@@ -1,20 +1,32 @@
//@flow
import React from "react";
import {deleteRepo, fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending} from "../modules/repos";
import {
fetchRepoByName,
getFetchRepoFailure,
getRepository,
isFetchRepoPending
} from "../modules/repos";
import {connect} from "react-redux";
import {Route, Switch} from "react-router-dom";
import type {Repository} from "@scm-manager/ui-types";
import { connect } from "react-redux";
import { Route, Switch } from "react-router-dom";
import type { Repository } from "@scm-manager/ui-types";
import {ErrorPage, Loading, Navigation, NavLink, Page, Section} from "@scm-manager/ui-components";
import {translate} from "react-i18next";
import {
ErrorPage,
Loading,
Navigation,
SubNavigation,
NavLink,
Page,
Section
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import RepositoryDetails from "../components/RepositoryDetails";
import DeleteNavAction from "../components/DeleteNavAction";
import Edit from "../containers/Edit";
import EditRepo from "./EditRepo";
import Permissions from "../permissions/containers/Permissions";
import type {History} from "history";
import EditNavLink from "../components/EditNavLink";
import type { History } from "history";
import EditRepoNavLink from "../components/EditRepoNavLink";
import BranchRoot from "./ChangesetsRoot";
import ChangesetView from "./ChangesetView";
@@ -35,7 +47,6 @@ type Props = {
// dispatch functions
fetchRepoByName: (link: string, namespace: string, name: string) => void,
deleteRepo: (repository: Repository, () => void) => void,
// context props
t: string => string,
@@ -61,14 +72,6 @@ class RepositoryRoot extends React.Component<Props> {
return this.stripEndingSlash(this.props.match.url);
};
deleted = () => {
this.props.history.push("/repos");
};
delete = (repository: Repository) => {
this.props.deleteRepo(repository, this.deleted);
};
matches = (route: any) => {
const url = this.matchedUrl();
const regex = new RegExp(`${url}(/branches)?/?[^/]*/changesets?.*`);
@@ -81,8 +84,8 @@ class RepositoryRoot extends React.Component<Props> {
if (error) {
return (
<ErrorPage
title={t("repository-root.error-title")}
subtitle={t("repository-root.error-subtitle")}
title={t("repositoryRoot.errorTitle")}
subtitle={t("repositoryRoot.errorSubtitle")}
error={error}
/>
);
@@ -111,11 +114,11 @@ class RepositoryRoot extends React.Component<Props> {
component={() => <RepositoryDetails repository={repository} />}
/>
<Route
path={`${url}/edit`}
component={() => <Edit repository={repository} />}
path={`${url}/settings/general`}
component={() => <EditRepo repository={repository} />}
/>
<Route
path={`${url}/permissions`}
path={`${url}/settings/permissions`}
render={() => (
<Permissions
namespace={this.props.repository.namespace}
@@ -170,14 +173,18 @@ class RepositoryRoot extends React.Component<Props> {
</div>
<div className="column">
<Navigation>
<Section label={t("repository-root.navigation-label")}>
<NavLink to={url} icon="fas fa-info-circle" label={t("repository-root.information")} />
<Section label={t("repositoryRoot.menu.navigationLabel")}>
<NavLink
to={url}
icon="fas fa-info-circle"
label={t("repositoryRoot.menu.informationNavLink")}
/>
<RepositoryNavLink
repository={repository}
linkName="changesets"
to={`${url}/changesets/`}
icon="fas fa-code-branch"
label={t("repository-root.history")}
label={t("repositoryRoot.menu.historyNavLink")}
activeWhenMatch={this.matches}
activeOnlyWhenExact={false}
/>
@@ -186,23 +193,32 @@ class RepositoryRoot extends React.Component<Props> {
linkName="sources"
to={`${url}/sources`}
icon="fas fa-code"
label={t("repository-root.sources")}
label={t("repositoryRoot.menu.sourcesNavLink")}
activeOnlyWhenExact={false}
/>
<PermissionsNavLink
permissionUrl={`${url}/permissions`}
repository={repository}
/>
<ExtensionPoint
name="repository.navigation"
props={extensionProps}
renderAll={true}
/>
</Section>
<Section label={t("repository-root.actions-label")}>
<DeleteNavAction repository={repository} delete={this.delete} />
<EditNavLink repository={repository} editUrl={`${url}/edit`} />
<NavLink to="/repos" icon="fas fa-undo" label={t("repository-root.back-label")} />
<SubNavigation
to={`${url}/settings/general`}
label={t("repositoryRoot.menu.settingsNavLink")}
>
<EditRepoNavLink
repository={repository}
editUrl={`${url}/settings/general`}
/>
<PermissionsNavLink
permissionUrl={`${url}/settings/permissions`}
repository={repository}
/>
<ExtensionPoint
name="repository.subnavigation"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</Section>
</Navigation>
</div>
@@ -234,9 +250,6 @@ const mapDispatchToProps = dispatch => {
return {
fetchRepoByName: (link: string, namespace: string, name: string) => {
dispatch(fetchRepoByName(link, namespace, name));
},
deleteRepo: (repository: Repository, callback: () => void) => {
dispatch(deleteRepo(repository, callback));
}
};
};

View File

@@ -118,7 +118,7 @@ class Sources extends React.Component<Props> {
<BranchSelector
branches={branches}
selectedBranch={revision}
label={t("branch-selector.label")}
label={t("changesets.branchSelectorLabel")}
selected={(b: Branch) => {
this.branchSelected(b);
}}

View File

@@ -90,7 +90,7 @@ class SetUserPassword extends React.Component<Props, State> {
message = (
<Notification
type={"success"}
children={t("password.set-password-successful")}
children={t("singleUserPassword.setPasswordSuccessful")}
onClose={() => this.onClose()}
/>
);
@@ -108,7 +108,7 @@ class SetUserPassword extends React.Component<Props, State> {
<SubmitButton
disabled={!this.state.passwordValid}
loading={loading}
label={t("user-form.submit")}
label={t("singleUserPassword.button")}
/>
</form>
);

View File

@@ -3,6 +3,7 @@ import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import {
Subtitle,
Checkbox,
InputField,
PasswordConfirmation,
@@ -113,7 +114,9 @@ class UserForm extends React.Component<Props, State> {
let nameField = null;
let passwordChangeField = null;
let subtitle = null;
if (!this.props.user) {
// create new user
nameField = (
<div className="column is-half">
<InputField
@@ -130,8 +133,13 @@ class UserForm extends React.Component<Props, State> {
passwordChangeField = (
<PasswordConfirmation passwordChanged={this.handlePasswordChange} />
);
} else {
// edit existing user
subtitle = <Subtitle subtitle={t("userForm.subtitle")} />;
}
return (
<>
{subtitle}
<form onSubmit={this.submit}>
<div className="columns is-multiline">
{nameField}
@@ -178,11 +186,12 @@ class UserForm extends React.Component<Props, State> {
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("user-form.submit")}
label={t("userForm.button")}
/>
</div>
</div>
</form>
</>
);
}

View File

@@ -1,20 +0,0 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { CreateButton } from "@scm-manager/ui-components";
// TODO remove
type Props = {
t: string => string
};
class CreateUserButton extends React.Component<Props> {
render() {
const { t } = this.props;
return (
<CreateButton label={t("create-user-button.label")} link="/users/add" />
);
}
}
export default translate("users")(CreateUserButton);

View File

@@ -1,56 +0,0 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import { NavAction, confirmAlert } from "@scm-manager/ui-components";
type Props = {
user: User,
confirmDialog?: boolean,
t: string => string,
deleteUser: (user: User) => void
};
class DeleteUserNavLink extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
deleteUser = () => {
this.props.deleteUser(this.props.user);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("delete-user-button.confirm-alert.title"),
message: t("delete-user-button.confirm-alert.message"),
buttons: [
{
label: t("delete-user-button.confirm-alert.submit"),
onClick: () => this.deleteUser()
},
{
label: t("delete-user-button.confirm-alert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.user._links.delete;
};
render() {
const { confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteUser;
if (!this.isDeletable()) {
return null;
}
return <NavAction icon="fas fa-times" label={t("delete-user-button.label")} action={action} />;
}
}
export default translate("users")(DeleteUserNavLink);

View File

@@ -1,82 +0,0 @@
import React from "react";
import { mount, shallow } from "enzyme";
import "../../../tests/enzyme";
import "../../../tests/i18n";
import DeleteUserNavLink from "./DeleteUserNavLink";
import { confirmAlert } from "@scm-manager/ui-components";
jest.mock("@scm-manager/ui-components", () => ({
confirmAlert: jest.fn(),
NavAction: require.requireActual("@scm-manager/ui-components").NavAction
}));
describe("DeleteUserNavLink", () => {
it("should render nothing, if the delete link is missing", () => {
const user = {
_links: {}
};
const navLink = shallow(
<DeleteUserNavLink user={user} deleteUser={() => {}} />
);
expect(navLink.text()).toBe("");
});
it("should render the navLink", () => {
const user = {
_links: {
delete: {
href: "/users"
}
}
};
const navLink = mount(
<DeleteUserNavLink user={user} deleteUser={() => {}} />
);
expect(navLink.text()).not.toBe("");
});
it("should open the confirm dialog on navLink click", () => {
const user = {
_links: {
delete: {
href: "/users"
}
}
};
const navLink = mount(
<DeleteUserNavLink user={user} deleteUser={() => {}} />
);
navLink.find("a").simulate("click");
expect(confirmAlert.mock.calls.length).toBe(1);
});
it("should call the delete user function with delete url", () => {
const user = {
_links: {
delete: {
href: "/users"
}
}
};
let calledUrl = null;
function capture(user) {
calledUrl = user._links.delete.href;
}
const navLink = mount(
<DeleteUserNavLink
user={user}
confirmDialog={false}
deleteUser={capture}
/>
);
navLink.find("a").simulate("click");
expect(calledUrl).toBe("/users");
});
});

View File

@@ -1,28 +1,28 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import { NavLink } from "@scm-manager/ui-components";
import { translate } from "react-i18next";
type Props = {
t: string => string,
user: User,
editUrl: String
editUrl: String,
t: string => string
};
class EditUserNavLink extends React.Component<Props> {
isEditable = () => {
return this.props.user._links.update;
};
render() {
const { t, editUrl } = this.props;
if (!this.isEditable()) {
return null;
}
return <NavLink to={editUrl} icon="fas fa-cog" label={t("edit-user-button.label")} />;
return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} />;
}
isEditable = () => {
return this.props.user._links.update;
};
}
export default translate("users")(EditUserNavLink);

View File

@@ -17,7 +17,7 @@ class ChangePasswordNavLink extends React.Component<Props> {
if (!this.hasPermissionToSetPassword()) {
return null;
}
return <NavLink to={passwordUrl} label={t("set-password-button.label")} />;
return <NavLink to={passwordUrl} label={t("singleUser.menu.setPasswordNavLink")} />;
}
hasPermissionToSetPassword = () => {

View File

@@ -17,7 +17,7 @@ class ChangePermissionNavLink extends React.Component<Props> {
if (!this.hasPermissionToSetPermission()) {
return null;
}
return <NavLink to={permissionsUrl} label={t("set-permissions-button.label")} />;
return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} />;
}
hasPermissionToSetPermission = () => {

View File

@@ -1,4 +1,3 @@
export { default as DeleteUserNavLink } from "./DeleteUserNavLink";
export { default as EditUserNavLink } from "./EditUserNavLink";
export { default as SetPasswordNavLink } from "./SetPasswordNavLink";
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";

View File

@@ -49,8 +49,8 @@ class AddUser extends React.Component<Props> {
return (
<Page
title={t("add-user.title")}
subtitle={t("add-user.subtitle")}
title={t("addUser.title")}
subtitle={t("addUser.subtitle")}
error={error}
showContentOnError={true}
>

View File

@@ -0,0 +1,113 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import type { User } from "@scm-manager/ui-types";
import {
Subtitle,
DeleteButton,
confirmAlert,
ErrorNotification
} from "@scm-manager/ui-components";
import {
deleteUser,
getDeleteUserFailure,
isDeleteUserPending
} from "../modules/users";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import type { History } from "history";
type Props = {
loading: boolean,
error: Error,
user: User,
confirmDialog?: boolean,
deleteUser: (user: User, callback?: () => void) => void,
// context props
history: History,
t: string => string
};
class DeleteUser extends React.Component<Props> {
static defaultProps = {
confirmDialog: true
};
userDeleted = () => {
this.props.history.push("/users");
};
deleteUser = () => {
this.props.deleteUser(this.props.user, this.userDeleted);
};
confirmDelete = () => {
const { t } = this.props;
confirmAlert({
title: t("deleteUser.confirmAlert.title"),
message: t("deleteUser.confirmAlert.message"),
buttons: [
{
label: t("deleteUser.confirmAlert.submit"),
onClick: () => this.deleteUser()
},
{
label: t("deleteUser.confirmAlert.cancel"),
onClick: () => null
}
]
});
};
isDeletable = () => {
return this.props.user._links.delete;
};
render() {
const { loading, error, confirmDialog, t } = this.props;
const action = confirmDialog ? this.confirmDelete : this.deleteUser;
if (!this.isDeletable()) {
return null;
}
return (
<>
<Subtitle subtitle={t("deleteUser.subtitle")} />
<ErrorNotification error={error} />
<div className="columns">
<div className="column">
<DeleteButton
label={t("deleteUser.button")}
action={action}
loading={loading}
/>
</div>
</div>
</>
);
}
}
const mapStateToProps = (state, ownProps) => {
const loading = isDeleteUserPending(state, ownProps.user.name);
const error = getDeleteUserFailure(state, ownProps.user.name);
return {
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
deleteUser: (user: User, callback?: () => void) => {
dispatch(deleteUser(user, callback));
}
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(translate("users")(DeleteUser)));

View File

@@ -2,7 +2,8 @@
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import UserForm from "./../components/UserForm";
import UserForm from "../components/UserForm";
import DeleteUser from "./DeleteUser";
import type { User } from "@scm-manager/ui-types";
import {
modifyUser,
@@ -31,6 +32,7 @@ class EditUser extends React.Component<Props> {
const { modifyUserReset, user } = this.props;
modifyUserReset(user);
}
userModified = (user: User) => () => {
this.props.history.push(`/user/${user.name}`);
};
@@ -49,11 +51,22 @@ class EditUser extends React.Component<Props> {
user={user}
loading={loading}
/>
<hr />
<DeleteUser user={user} />
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const loading = isModifyUserPending(state, ownProps.user.name);
const error = getModifyUserFailure(state, ownProps.user.name);
return {
loading,
error
};
};
const mapDispatchToProps = dispatch => {
return {
modifyUser: (user: User, callback?: () => void) => {
@@ -65,15 +78,6 @@ const mapDispatchToProps = dispatch => {
};
};
const mapStateToProps = (state, ownProps) => {
const loading = isModifyUserPending(state, ownProps.user.name);
const error = getModifyUserFailure(state, ownProps.user.name);
return {
loading,
error
};
};
export default connect(
mapStateToProps,
mapDispatchToProps

View File

@@ -5,6 +5,7 @@ import {
Page,
Loading,
Navigation,
SubNavigation,
Section,
NavLink,
ErrorPage
@@ -16,24 +17,16 @@ import type { User } from "@scm-manager/ui-types";
import type { History } from "history";
import {
fetchUserByName,
deleteUser,
getUserByName,
isFetchUserPending,
getFetchUserFailure,
isDeleteUserPending,
getDeleteUserFailure
getFetchUserFailure
} from "../modules/users";
import {
DeleteUserNavLink,
EditUserNavLink,
SetPasswordNavLink,
SetPermissionsNavLink
} from "./../components/navLinks";
import { EditUserNavLink, SetPasswordNavLink, SetPermissionsNavLink } from "./../components/navLinks";
import { translate } from "react-i18next";
import { getUsersLink } from "../../modules/indexResource";
import SetUserPassword from "../components/SetUserPassword";
import SetPermissions from "../../permissions/components/SetPermissions";
import {ExtensionPoint} from "@scm-manager/ui-extensions";
type Props = {
name: string,
@@ -42,8 +35,7 @@ type Props = {
error: Error,
usersLink: string,
// dispatcher functions
deleteUser: (user: User, callback?: () => void) => void,
// dispatcher function
fetchUserByName: (string, string) => void,
// context objects
@@ -57,14 +49,6 @@ class SingleUser extends React.Component<Props> {
this.props.fetchUserByName(this.props.usersLink, this.props.name);
}
userDeleted = () => {
this.props.history.push("/users");
};
deleteUser = (user: User) => {
this.props.deleteUser(user, this.userDeleted);
};
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 2);
@@ -82,8 +66,8 @@ class SingleUser extends React.Component<Props> {
if (error) {
return (
<ErrorPage
title={t("single-user.error-title")}
subtitle={t("single-user.error-subtitle")}
title={t("singleUser.errorTitle")}
subtitle={t("singleUser.errorSubtitle")}
error={error}
/>
);
@@ -95,21 +79,26 @@ class SingleUser extends React.Component<Props> {
const url = this.matchedUrl();
const extensionProps = {
user,
url
};
return (
<Page title={user.displayName}>
<div className="columns">
<div className="column is-three-quarters">
<Route path={url} exact component={() => <Details user={user} />} />
<Route
path={`${url}/edit`}
path={`${url}/settings/general`}
component={() => <EditUser user={user} />}
/>
<Route
path={`${url}/password`}
path={`${url}/settings/password`}
component={() => <SetUserPassword user={user} />}
/>
<Route
path={`${url}/permissions`}
path={`${url}/settings/permissions`}
component={() => (
<SetPermissions
selectedPermissionsLink={user._links.permissions}
@@ -119,25 +108,34 @@ class SingleUser extends React.Component<Props> {
</div>
<div className="column">
<Navigation>
<Section label={t("single-user.navigation-label")}>
<Section label={t("singleUser.menu.navigationLabel")}>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("single-user.information-label")}
label={t("singleUser.menu.informationNavLink")}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleUser.menu.settingsNavLink")}
>
<EditUserNavLink
user={user}
editUrl={`${url}/settings/general`}
/>
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
<SetPasswordNavLink
user={user}
passwordUrl={`${url}/password`}
passwordUrl={`${url}/settings/password`}
/>
<SetPermissionsNavLink
user={user}
permissionsUrl={`${url}/permissions`}
permissionsUrl={`${url}/settings/permissions`}
/>
</Section>
<Section label={t("single-user.actions-label")}>
<DeleteUserNavLink user={user} deleteUser={this.deleteUser} />
<NavLink to="/users" icon="fas fa-undo" label={t("single-user.back-label")} />
<ExtensionPoint
name="user.subnavigation"
props={extensionProps}
renderAll={true}
/>
</SubNavigation>
</Section>
</Navigation>
</div>
@@ -150,10 +148,8 @@ class SingleUser extends React.Component<Props> {
const mapStateToProps = (state, ownProps) => {
const name = ownProps.match.params.name;
const user = getUserByName(state, name);
const loading =
isFetchUserPending(state, name) || isDeleteUserPending(state, name);
const error =
getFetchUserFailure(state, name) || getDeleteUserFailure(state, name);
const loading = isFetchUserPending(state, name);
const error = getFetchUserFailure(state, name);
const usersLink = getUsersLink(state);
return {
usersLink,
@@ -168,9 +164,6 @@ const mapDispatchToProps = dispatch => {
return {
fetchUserByName: (link: string, name: string) => {
dispatch(fetchUserByName(link, name));
},
deleteUser: (user: User, callback?: () => void) => {
dispatch(deleteUser(user, callback));
}
};
};

View File

@@ -14,10 +14,9 @@ import {
getFetchUsersFailure
} from "../modules/users";
import { Page, Paginator } from "@scm-manager/ui-components";
import { Page, CreateButton, Paginator } from "@scm-manager/ui-components";
import { UserTable } from "./../components/table";
import type { User, PagedCollection } from "@scm-manager/ui-types";
import CreateUserButton from "../components/buttons/CreateUserButton";
import { getUsersLink } from "../../modules/indexResource";
type Props = {
@@ -86,8 +85,9 @@ class Users extends React.Component<Props> {
}
renderCreateButton() {
const { t } = this.props;
if (this.props.canAddUsers) {
return <CreateUserButton />;
return <CreateButton label={t("users.createButton")} link="/users/add" />;
} else {
return;
}

View File

@@ -283,18 +283,29 @@ $fa-font-path: "webfonts";
}
.menu-list {
a {
border-radius: 0;
color: #333;
padding: 1rem;
border-top: 1px solid #eee;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
&.is-active {
color: $blue;
background-color: #fff;
}
}
&:before {
> li {
ul {
margin: 0;
border-top: 1px solid #eee;
li {
border-right: none;
}
li:last-child {
border-bottom: none;
}
}
> a.is-active:before {
position: relative;
content: " ";
background: $blue;
@@ -305,12 +316,43 @@ $fa-font-path: "webfonts";
float: left;
top: -16px;
}
border-radius: 0;
border-top: 1px solid #eee;
border-left: 1px solid #eee;
border-right: 1px solid #eee;
}
}
> li:first-child > a {
> li:first-child {
border-top: none;
}
li:last-child > a {
li:last-child {
border-bottom: 1px solid #eee;
}
div {
margin-bottom: 0;
}
}
.sub-menu li {
line-height: 1;
a {
padding: 0.75rem 1rem;
}
a:before {
font-family: "Font Awesome 5 Free";
font-weight: 900;
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
content: "\f105";
padding-right: 5px;
}
i {
display: none;
}
}