mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-16 02:06:18 +01:00
merge
This commit is contained in:
@@ -1,86 +1,87 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Route } from "react-router";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
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 { getLinks } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
links: Links,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
match: any,
|
||||
history: History
|
||||
};
|
||||
|
||||
class Config extends React.Component<Props> {
|
||||
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 { links, t } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const extensionProps = {
|
||||
links,
|
||||
url
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route path={url} exact component={GlobalConfig} />
|
||||
<ExtensionPoint name="config.route"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-one-quarter">
|
||||
<Navigation>
|
||||
<Section label={t("config.navigation-title")}>
|
||||
<NavLink
|
||||
to={`${url}`}
|
||||
label={t("global-config.navigation-label")}
|
||||
/>
|
||||
<ExtensionPoint name="config.navigation"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</Section>
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const links = getLinks(state);
|
||||
return {
|
||||
links
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
translate("config")
|
||||
)(Config);
|
||||
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Route } from "react-router";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
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 { getLinks } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
links: Links,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
match: any,
|
||||
history: History
|
||||
};
|
||||
|
||||
class Config extends React.Component<Props> {
|
||||
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 { links, t } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
const extensionProps = {
|
||||
links,
|
||||
url
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route path={url} exact component={GlobalConfig} />
|
||||
<ExtensionPoint
|
||||
name="config.route"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-one-quarter">
|
||||
<Navigation>
|
||||
<Section label={t("config.navigationLabel")}>
|
||||
<NavLink
|
||||
to={`${url}`}
|
||||
label={t("config.globalConfigurationNavLink")}
|
||||
/>
|
||||
<ExtensionPoint
|
||||
name="config.navigation"
|
||||
props={extensionProps}
|
||||
renderAll={true}
|
||||
/>
|
||||
</Section>
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any) => {
|
||||
const links = getLinks(state);
|
||||
return {
|
||||
links
|
||||
};
|
||||
};
|
||||
|
||||
export default compose(
|
||||
connect(mapStateToProps),
|
||||
translate("config")
|
||||
)(Config);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -143,7 +143,6 @@ class Login extends React.Component<Props, State> {
|
||||
/>
|
||||
<SubmitButton
|
||||
label={t("login.submit")}
|
||||
disabled={this.isInValid()}
|
||||
fullWidth={true}
|
||||
loading={loading}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,7 @@ import Login from "../containers/Login";
|
||||
import Logout from "../containers/Logout";
|
||||
|
||||
import { ProtectedRoute } from "@scm-manager/ui-components";
|
||||
import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
import {binder, ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
|
||||
import AddUser from "../users/containers/AddUser";
|
||||
import SingleUser from "../users/containers/SingleUser";
|
||||
@@ -32,10 +32,15 @@ type Props = {
|
||||
class Main extends React.Component<Props> {
|
||||
render() {
|
||||
const { authenticated, links } = this.props;
|
||||
const redirectUrlFactory = binder.getExtension("main.redirect", this.props);
|
||||
let url ="/repos";
|
||||
if (redirectUrlFactory){
|
||||
url = redirectUrlFactory(this.props);
|
||||
}
|
||||
return (
|
||||
<div className="main">
|
||||
<Switch>
|
||||
<Redirect exact path="/" to="/repos" />
|
||||
<Redirect exact path="/" to={url}/>
|
||||
<Route exact path="/login" component={Login} />
|
||||
<Route path="/logout" component={Logout} />
|
||||
<ProtectedRoute
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -28,30 +28,20 @@ class ProfileInfo extends React.Component<Props, State> {
|
||||
<div className="media-content">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="has-text-weight-semibold">{t("profile.username")}</td>
|
||||
<td>{me.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="has-text-weight-semibold">{t("profile.displayName")}</td>
|
||||
<td>{me.displayName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="has-text-weight-semibold">{t("profile.mail")}</td>
|
||||
<td>
|
||||
<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>
|
||||
<tr>
|
||||
<td className="has-text-weight-semibold">{t("profile.username")}</td>
|
||||
<td>{me.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="has-text-weight-semibold">{t("profile.displayName")}</td>
|
||||
<td>{me.displayName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="has-text-weight-semibold">{t("profile.mail")}</td>
|
||||
<td>
|
||||
<MailLink address={me.mail} />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
Subtitle,
|
||||
AutocompleteAddEntryToTableField,
|
||||
LabelWithHelpIcon,
|
||||
MemberNameTable,
|
||||
@@ -71,59 +72,67 @@ 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 (
|
||||
<form onSubmit={this.submit}>
|
||||
{nameField}
|
||||
<Textarea
|
||||
label={t("group.description")}
|
||||
errorMessage={t("group-form.description-error")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={group.description}
|
||||
validationError={false}
|
||||
helpText={t("group-form.help.descriptionHelpText")}
|
||||
/>
|
||||
<LabelWithHelpIcon
|
||||
label={t("group.members")}
|
||||
helpText={t("group-form.help.memberHelpText")}
|
||||
/>
|
||||
<MemberNameTable
|
||||
members={group.members}
|
||||
memberListChanged={this.memberListChanged}
|
||||
/>
|
||||
<>
|
||||
{subtitle}
|
||||
<form onSubmit={this.submit}>
|
||||
{nameField}
|
||||
<Textarea
|
||||
label={t("group.description")}
|
||||
errorMessage={t("groupForm.descriptionError")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={group.description}
|
||||
validationError={false}
|
||||
helpText={t("groupForm.help.descriptionHelpText")}
|
||||
/>
|
||||
<LabelWithHelpIcon
|
||||
label={t("group.members")}
|
||||
helpText={t("groupForm.help.memberHelpText")}
|
||||
/>
|
||||
<MemberNameTable
|
||||
members={group.members}
|
||||
memberListChanged={this.memberListChanged}
|
||||
/>
|
||||
|
||||
<AutocompleteAddEntryToTableField
|
||||
addEntry={this.addMember}
|
||||
disabled={false}
|
||||
buttonLabel={t("add-member-button.label")}
|
||||
fieldLabel={t("add-member-textfield.label")}
|
||||
errorMessage={t("add-member-textfield.error")}
|
||||
loadSuggestions={this.props.loadUserSuggestions}
|
||||
placeholder={t("add-member-autocomplete.placeholder")}
|
||||
loadingMessage={t("add-member-autocomplete.loading")}
|
||||
noOptionsMessage={t("add-member-autocomplete.no-options")}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
label={t("group-form.submit")}
|
||||
loading={loading}
|
||||
/>
|
||||
</form>
|
||||
<AutocompleteAddEntryToTableField
|
||||
addEntry={this.addMember}
|
||||
disabled={false}
|
||||
buttonLabel={t("add-member-button.label")}
|
||||
fieldLabel={t("add-member-textfield.label")}
|
||||
errorMessage={t("add-member-textfield.error")}
|
||||
loadSuggestions={this.props.loadUserSuggestions}
|
||||
placeholder={t("add-member-autocomplete.placeholder")}
|
||||
loadingMessage={t("add-member-autocomplete.loading")}
|
||||
noOptionsMessage={t("add-member-autocomplete.no-options")}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
label={t("groupForm.submit")}
|
||||
loading={loading}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { default as DeleteGroupNavLink } from "./DeleteGroupNavLink";
|
||||
export { default as EditGroupNavLink } from "./EditGroupNavLink";
|
||||
export { default as SetPermissionsNavLink } from "./SetPermissionsNavLink";
|
||||
|
||||
113
scm-ui/src/groups/containers/DeleteGroup.js
Normal file
113
scm-ui/src/groups/containers/DeleteGroup.js
Normal 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)));
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
group={group}
|
||||
deleteGroup={this.deleteGroup}
|
||||
/>
|
||||
<EditGroupNavLink group={group} editUrl={`${url}/edit`} />
|
||||
<NavLink
|
||||
to="/groups"
|
||||
icon="fas fa-undo-alt"
|
||||
label={t("single-group.back-label")}
|
||||
/>
|
||||
<SubNavigation
|
||||
to={`${url}/settings/general`}
|
||||
label={t("singleGroup.menu.settingsNavLink")}
|
||||
>
|
||||
<EditGroupNavLink
|
||||
group={group}
|
||||
editUrl={`${url}/settings/general`}
|
||||
/>
|
||||
<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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -17,6 +17,12 @@ import { ConnectedRouter } from "react-router-redux";
|
||||
|
||||
import { urls } from "@scm-manager/ui-components";
|
||||
|
||||
import jss from "jss";
|
||||
import jssNested from "jss-nested";
|
||||
|
||||
// setup jss and install required plugins
|
||||
jss.setup(jssNested());
|
||||
|
||||
// Create a history of your choosing (we're using a browser history in this case)
|
||||
const history: BrowserHistory = createHistory({
|
||||
basename: urls.contextPath
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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")} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,11 @@ import { ExtensionPoint } from "@scm-manager/ui-extensions";
|
||||
const styles = {
|
||||
spacing: {
|
||||
marginRight: "1em"
|
||||
},
|
||||
tags: {
|
||||
"& .tag": {
|
||||
marginLeft: ".25rem"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,7 +53,7 @@ class ChangesetDetails extends React.Component<Props> {
|
||||
<div className="content">
|
||||
<h4>
|
||||
<ExtensionPoint
|
||||
name="changesets.changeset.description"
|
||||
name="changeset.description"
|
||||
props={{ changeset, value: description.title }}
|
||||
renderAll={false}
|
||||
>
|
||||
@@ -67,7 +72,7 @@ class ChangesetDetails extends React.Component<Props> {
|
||||
</p>
|
||||
<p>
|
||||
<Interpolate
|
||||
i18nKey="changesets.changeset.summary"
|
||||
i18nKey="changeset.summary"
|
||||
id={id}
|
||||
time={date}
|
||||
/>
|
||||
@@ -81,7 +86,7 @@ class ChangesetDetails extends React.Component<Props> {
|
||||
return (
|
||||
<span key={key}>
|
||||
<ExtensionPoint
|
||||
name="changesets.changeset.description"
|
||||
name="changeset.description"
|
||||
props={{ changeset, value: item }}
|
||||
renderAll={false}
|
||||
>
|
||||
@@ -106,10 +111,11 @@ class ChangesetDetails extends React.Component<Props> {
|
||||
};
|
||||
|
||||
renderTags = () => {
|
||||
const { classes } = this.props;
|
||||
const tags = this.getTags();
|
||||
if (tags.length > 0) {
|
||||
return (
|
||||
<div className="level-item">
|
||||
<div className={classNames("level-item", classes.tags)}>
|
||||
{tags.map((tag: Tag) => {
|
||||
return <ChangesetTag key={tag.name} tag={tag} />;
|
||||
})}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import {
|
||||
Subtitle,
|
||||
InputField,
|
||||
Select,
|
||||
SubmitButton,
|
||||
@@ -81,30 +82,39 @@ class RepositoryForm extends React.Component<Props, State> {
|
||||
const { loading, t } = this.props;
|
||||
const repository = this.state.repository;
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
{this.renderCreateOnlyFields()}
|
||||
<InputField
|
||||
label={t("repository.contact")}
|
||||
onChange={this.handleContactChange}
|
||||
value={repository ? repository.contact : ""}
|
||||
validationError={this.state.contactValidationError}
|
||||
errorMessage={t("validation.contact-invalid")}
|
||||
helpText={t("help.contactHelpText")}
|
||||
/>
|
||||
let subtitle = null;
|
||||
if (this.props.repository) {
|
||||
// edit existing repo
|
||||
subtitle = <Subtitle subtitle={t("repositoryForm.subtitle")} />;
|
||||
}
|
||||
|
||||
<Textarea
|
||||
label={t("repository.description")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={repository ? repository.description : ""}
|
||||
helpText={t("help.descriptionHelpText")}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
loading={loading}
|
||||
label={t("repository-form.submit")}
|
||||
/>
|
||||
</form>
|
||||
return (
|
||||
<>
|
||||
{subtitle}
|
||||
<form onSubmit={this.submit}>
|
||||
{this.renderCreateOnlyFields()}
|
||||
<InputField
|
||||
label={t("repository.contact")}
|
||||
onChange={this.handleContactChange}
|
||||
value={repository ? repository.contact : ""}
|
||||
validationError={this.state.contactValidationError}
|
||||
errorMessage={t("validation.contact-invalid")}
|
||||
helpText={t("help.contactHelpText")}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label={t("repository.description")}
|
||||
onChange={this.handleDescriptionChange}
|
||||
value={repository ? repository.description : ""}
|
||||
helpText={t("help.descriptionHelpText")}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
loading={loading}
|
||||
label={t("repositoryForm.submit")}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class RepositoryEntry extends React.Component<Props> {
|
||||
return (
|
||||
<RepositoryEntryLink
|
||||
iconClass="fa-cog fa-lg"
|
||||
to={repositoryLink + "/edit"}
|
||||
to={repositoryLink + "/settings/general"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import {withRouter} from "react-router-dom";
|
||||
import type {Branch, Changeset, PagedCollection, Repository} from "@scm-manager/ui-types";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import type {
|
||||
Branch,
|
||||
Changeset,
|
||||
PagedCollection,
|
||||
Repository
|
||||
} from "@scm-manager/ui-types";
|
||||
import {
|
||||
fetchChangesets,
|
||||
getChangesets,
|
||||
@@ -11,9 +16,15 @@ import {
|
||||
selectListAsCollection
|
||||
} from "../modules/changesets";
|
||||
|
||||
import {connect} from "react-redux";
|
||||
import {ErrorNotification, getPageFromMatch, LinkPaginator, ChangesetList, Loading} from "@scm-manager/ui-components";
|
||||
import {compose} from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import {
|
||||
ErrorNotification,
|
||||
getPageFromMatch,
|
||||
LinkPaginator,
|
||||
ChangesetList,
|
||||
Loading
|
||||
} from "@scm-manager/ui-components";
|
||||
import { compose } from "redux";
|
||||
|
||||
type Props = {
|
||||
repository: Repository,
|
||||
@@ -64,13 +75,21 @@ class Changesets extends React.Component<Props> {
|
||||
|
||||
renderList = () => {
|
||||
const { repository, changesets } = this.props;
|
||||
return <ChangesetList repository={repository} changesets={changesets} />;
|
||||
return (
|
||||
<div className="panel-block">
|
||||
<ChangesetList repository={repository} changesets={changesets} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
renderPaginator = () => {
|
||||
const { page, list } = this.props;
|
||||
if (list) {
|
||||
return <LinkPaginator page={page} collection={list} />;
|
||||
return (
|
||||
<div className="panel-footer">
|
||||
<LinkPaginator page={page} collection={list} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -89,10 +89,12 @@ class BranchRoot extends React.Component<Props> {
|
||||
const changesets = <Changesets repository={repository} branch={branch} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
{this.renderBranchSelector()}
|
||||
</div>
|
||||
<Route path={`${url}/:page?`} component={() => changesets} />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -101,7 +103,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) => {
|
||||
|
||||
114
scm-ui/src/repos/containers/DeleteRepo.js
Normal file
114
scm-ui/src/repos/containers/DeleteRepo.js
Normal 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)));
|
||||
@@ -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));
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React from "react";
|
||||
import { Button, SubmitButton } from "@scm-manager/ui-components";
|
||||
import { Button, SubmitButton, Modal } from "@scm-manager/ui-components";
|
||||
import { translate } from "react-i18next";
|
||||
import PermissionCheckbox from "../components/PermissionCheckbox";
|
||||
|
||||
@@ -38,6 +38,7 @@ class AdvancedPermissionsDialog extends React.Component<Props, State> {
|
||||
|
||||
const verbSelectBoxes = Object.entries(verbs).map(e => (
|
||||
<PermissionCheckbox
|
||||
key={e[0]}
|
||||
disabled={readOnly}
|
||||
name={e[0]}
|
||||
checked={e[1]}
|
||||
@@ -49,38 +50,30 @@ class AdvancedPermissionsDialog extends React.Component<Props, State> {
|
||||
<SubmitButton label={t("permission.advanced.dialog.submit")} />
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="modal is-active">
|
||||
<div className="modal-background" />
|
||||
<div className="modal-card">
|
||||
<header className="modal-card-head">
|
||||
<p className="modal-card-title">
|
||||
{t("permission.advanced.dialog.title")}
|
||||
</p>
|
||||
<button
|
||||
className="delete"
|
||||
aria-label="close"
|
||||
onClick={() => onClose()}
|
||||
const body = <>{verbSelectBoxes}</>;
|
||||
|
||||
const footer = (
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="field is-grouped">
|
||||
<p className="control">{submitButton}</p>
|
||||
<p className="control">
|
||||
<Button
|
||||
label={t("permission.advanced.dialog.abort")}
|
||||
action={onClose}
|
||||
/>
|
||||
</header>
|
||||
<section className="modal-card-body">
|
||||
<div className="content">{verbSelectBoxes}</div>
|
||||
<div className="is-pulled-right">
|
||||
<form onSubmit={this.onSubmit}>
|
||||
<div className="field is-grouped">
|
||||
<p className="control">{submitButton}</p>
|
||||
<p className="control">
|
||||
<Button
|
||||
label={t("permission.advanced.dialog.abort")}
|
||||
action={onClose}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("permission.advanced.dialog.title")}
|
||||
closeFunction={() => onClose()}
|
||||
body={body}
|
||||
footer={footer}
|
||||
active={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import {
|
||||
Loading,
|
||||
ErrorPage,
|
||||
Subtitle,
|
||||
LabelWithHelpIcon
|
||||
} from "@scm-manager/ui-components";
|
||||
import type {
|
||||
@@ -144,6 +145,7 @@ class Permissions extends React.Component<Props> {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subtitle subtitle={t("permission.title")} />
|
||||
<table className="has-background-light table is-hoverable is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
@@ -108,32 +108,34 @@ class FileTree extends React.Component<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={classes.iconColumn} />
|
||||
<th>{t("sources.file-tree.name")}</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.length")}
|
||||
</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.lastModified")}
|
||||
</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.description")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(file => (
|
||||
<FileTreeLeaf
|
||||
key={file.name}
|
||||
file={file}
|
||||
baseUrl={baseUrlWithRevision}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="panel-block">
|
||||
<table className="table table-hover table-sm is-fullwidth">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className={classes.iconColumn} />
|
||||
<th>{t("sources.file-tree.name")}</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.length")}
|
||||
</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.lastModified")}
|
||||
</th>
|
||||
<th className="is-hidden-mobile">
|
||||
{t("sources.file-tree.description")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{files.map(file => (
|
||||
<FileTreeLeaf
|
||||
key={file.name}
|
||||
file={file}
|
||||
baseUrl={baseUrlWithRevision}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { ButtonGroup } from "@scm-manager/ui-components";
|
||||
import { ButtonGroup, Button } from "@scm-manager/ui-components";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
@@ -18,39 +18,32 @@ class FileButtonGroup extends React.Component<Props> {
|
||||
this.props.showHistory(false);
|
||||
};
|
||||
|
||||
color = (selected: boolean) => {
|
||||
return selected ? "link is-selected" : null;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, historyIsSelected } = this.props;
|
||||
|
||||
const sourcesLabel = (
|
||||
<>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile">
|
||||
{t("sources.content.sourcesButton")}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
const historyLabel = (
|
||||
<>
|
||||
<span className="icon">
|
||||
<i className="fas fa-history" />
|
||||
</span>
|
||||
<span className="is-hidden-mobile">
|
||||
{t("sources.content.historyButton")}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<ButtonGroup
|
||||
firstlabel={sourcesLabel}
|
||||
secondlabel={historyLabel}
|
||||
firstAction={this.showSources}
|
||||
secondAction={this.showHistory}
|
||||
firstIsSelected={!historyIsSelected}
|
||||
/>
|
||||
<ButtonGroup>
|
||||
<Button action={this.showSources} color={ this.color(!historyIsSelected) }>
|
||||
<span className="icon">
|
||||
<i className="fas fa-code"/>
|
||||
</span>
|
||||
<span className="is-hidden-mobile">
|
||||
{t("sources.content.sourcesButton")}
|
||||
</span>
|
||||
</Button>
|
||||
<Button action={this.showHistory} color={ this.color(historyIsSelected) }>
|
||||
<span className="icon">
|
||||
<i className="fas fa-history"/>
|
||||
</span>
|
||||
<span className="is-hidden-mobile">
|
||||
{t("sources.content.historyButton")}
|
||||
</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ type State = {
|
||||
};
|
||||
|
||||
const styles = {
|
||||
toCenterContent: {
|
||||
display: "block"
|
||||
},
|
||||
pointer: {
|
||||
cursor: "pointer"
|
||||
},
|
||||
@@ -126,7 +123,6 @@ class Content extends React.Component<Props, State> {
|
||||
<div
|
||||
className={classNames(
|
||||
"panel-block",
|
||||
classes.toCenterContent,
|
||||
classes.hasBackground
|
||||
)}
|
||||
>
|
||||
@@ -161,7 +157,7 @@ class Content extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { file, revision, repository, path, classes } = this.props;
|
||||
const { file, revision, repository, path } = this.props;
|
||||
const { showHistory } = this.state;
|
||||
|
||||
const header = this.showHeader();
|
||||
@@ -180,13 +176,11 @@ class Content extends React.Component<Props, State> {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav className="panel">
|
||||
<article className="panel-heading">{header}</article>
|
||||
<div className="panel">
|
||||
<div className="panel-heading">{header}</div>
|
||||
{moreInformation}
|
||||
<div className={classNames("panel-block", classes.toCenterContent)}>
|
||||
{content}
|
||||
</div>
|
||||
</nav>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,12 +79,16 @@ class HistoryView extends React.Component<Props, State> {
|
||||
const currentPage = page + 1;
|
||||
return (
|
||||
<>
|
||||
<ChangesetList repository={repository} changesets={changesets} />
|
||||
<StatePaginator
|
||||
page={currentPage}
|
||||
collection={pageCollection}
|
||||
updatePage={(newPage: number) => this.updatePage(newPage)}
|
||||
/>
|
||||
<div className="panel-block">
|
||||
<ChangesetList repository={repository} changesets={changesets} />
|
||||
</div>
|
||||
<div className="panel-footer">
|
||||
<StatePaginator
|
||||
page={currentPage}
|
||||
collection={pageCollection}
|
||||
updatePage={(newPage: number) => this.updatePage(newPage)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,8 +93,10 @@ class Sources extends React.Component<Props> {
|
||||
|
||||
if (currentFileIsDirectory) {
|
||||
return (
|
||||
<div className={"has-border-around"}>
|
||||
{this.renderBranchSelector()}
|
||||
<div className="panel">
|
||||
<div className="panel-heading">
|
||||
{this.renderBranchSelector()}
|
||||
</div>
|
||||
<FileTree
|
||||
repository={repository}
|
||||
revision={revision}
|
||||
@@ -118,7 +120,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);
|
||||
}}
|
||||
|
||||
@@ -90,7 +90,7 @@ class SourcesView extends React.Component<Props, State> {
|
||||
|
||||
const sources = this.showSources();
|
||||
|
||||
return <>{sources}</>;
|
||||
return <div className="panel-block">{sources}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,59 +133,65 @@ class UserForm extends React.Component<Props, State> {
|
||||
passwordChangeField = (
|
||||
<PasswordConfirmation passwordChanged={this.handlePasswordChange} />
|
||||
);
|
||||
} else {
|
||||
// edit existing user
|
||||
subtitle = <Subtitle subtitle={t("userForm.subtitle")} />;
|
||||
}
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
<div className="columns is-multiline">
|
||||
{nameField}
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("user.displayName")}
|
||||
onChange={this.handleDisplayNameChange}
|
||||
value={user ? user.displayName : ""}
|
||||
validationError={this.state.displayNameValidationError}
|
||||
errorMessage={t("validation.displayname-invalid")}
|
||||
helpText={t("help.displayNameHelpText")}
|
||||
/>
|
||||
<>
|
||||
{subtitle}
|
||||
<form onSubmit={this.submit}>
|
||||
<div className="columns is-multiline">
|
||||
{nameField}
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("user.displayName")}
|
||||
onChange={this.handleDisplayNameChange}
|
||||
value={user ? user.displayName : ""}
|
||||
validationError={this.state.displayNameValidationError}
|
||||
errorMessage={t("validation.displayname-invalid")}
|
||||
helpText={t("help.displayNameHelpText")}
|
||||
/>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("user.mail")}
|
||||
onChange={this.handleEmailChange}
|
||||
value={user ? user.mail : ""}
|
||||
validationError={this.state.mailValidationError}
|
||||
errorMessage={t("validation.mail-invalid")}
|
||||
helpText={t("help.mailHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-half">
|
||||
<InputField
|
||||
label={t("user.mail")}
|
||||
onChange={this.handleEmailChange}
|
||||
value={user ? user.mail : ""}
|
||||
validationError={this.state.mailValidationError}
|
||||
errorMessage={t("validation.mail-invalid")}
|
||||
helpText={t("help.mailHelpText")}
|
||||
/>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
{passwordChangeField}
|
||||
<Checkbox
|
||||
label={t("user.admin")}
|
||||
onChange={this.handleAdminChange}
|
||||
checked={user ? user.admin : false}
|
||||
helpText={t("help.adminHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("user.active")}
|
||||
onChange={this.handleActiveChange}
|
||||
checked={user ? user.active : false}
|
||||
helpText={t("help.activeHelpText")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
{passwordChangeField}
|
||||
<Checkbox
|
||||
label={t("user.admin")}
|
||||
onChange={this.handleAdminChange}
|
||||
checked={user ? user.admin : false}
|
||||
helpText={t("help.adminHelpText")}
|
||||
/>
|
||||
<Checkbox
|
||||
label={t("user.active")}
|
||||
onChange={this.handleActiveChange}
|
||||
checked={user ? user.active : false}
|
||||
helpText={t("help.activeHelpText")}
|
||||
/>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
loading={loading}
|
||||
label={t("userForm.button")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
loading={loading}
|
||||
label={t("user-form.submit")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
113
scm-ui/src/users/containers/DeleteUser.js
Normal file
113
scm-ui/src/users/containers/DeleteUser.js
Normal 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)));
|
||||
@@ -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
|
||||
|
||||
@@ -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")}
|
||||
/>
|
||||
<EditUserNavLink user={user} editUrl={`${url}/edit`} />
|
||||
<SetPasswordNavLink
|
||||
user={user}
|
||||
passwordUrl={`${url}/password`}
|
||||
/>
|
||||
<SetPermissionsNavLink
|
||||
user={user}
|
||||
permissionsUrl={`${url}/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")} />
|
||||
<SubNavigation
|
||||
to={`${url}/settings/general`}
|
||||
label={t("singleUser.menu.settingsNavLink")}
|
||||
>
|
||||
<EditUserNavLink
|
||||
user={user}
|
||||
editUrl={`${url}/settings/general`}
|
||||
/>
|
||||
<SetPasswordNavLink
|
||||
user={user}
|
||||
passwordUrl={`${url}/settings/password`}
|
||||
/>
|
||||
<SetPermissionsNavLink
|
||||
user={user}
|
||||
permissionsUrl={`${url}/settings/permissions`}
|
||||
/>
|
||||
<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));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,143 +1,143 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { History } from "history";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
import {
|
||||
fetchUsersByPage,
|
||||
fetchUsersByLink,
|
||||
getUsersFromState,
|
||||
selectListAsCollection,
|
||||
isPermittedToCreateUsers,
|
||||
isFetchUsersPending,
|
||||
getFetchUsersFailure
|
||||
} from "../modules/users";
|
||||
|
||||
import { Page, 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 = {
|
||||
users: User[],
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
canAddUsers: boolean,
|
||||
list: PagedCollection,
|
||||
page: number,
|
||||
usersLink: string,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
|
||||
// dispatch functions
|
||||
fetchUsersByPage: (link: string, page: number) => void,
|
||||
fetchUsersByLink: (link: string) => void
|
||||
};
|
||||
|
||||
class Users extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchUsersByPage(this.props.usersLink, this.props.page);
|
||||
}
|
||||
|
||||
onPageChange = (link: string) => {
|
||||
this.props.fetchUsersByLink(link);
|
||||
};
|
||||
|
||||
/**
|
||||
* reflect page transitions in the uri
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
const { page, list } = this.props;
|
||||
if (list && (list.page || list.page === 0)) {
|
||||
// backend starts paging by 0
|
||||
const statePage: number = list.page + 1;
|
||||
if (page !== statePage) {
|
||||
this.props.history.push(`/users/${statePage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { users, loading, error, t } = this.props;
|
||||
return (
|
||||
<Page
|
||||
title={t("users.title")}
|
||||
subtitle={t("users.subtitle")}
|
||||
loading={loading || !users}
|
||||
error={error}
|
||||
>
|
||||
<UserTable users={users} />
|
||||
{this.renderPaginator()}
|
||||
{this.renderCreateButton()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderPaginator() {
|
||||
const { list } = this.props;
|
||||
if (list) {
|
||||
return <Paginator collection={list} onPageChange={this.onPageChange} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderCreateButton() {
|
||||
if (this.props.canAddUsers) {
|
||||
return <CreateUserButton />;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getPageFromProps = props => {
|
||||
let page = props.match.params.page;
|
||||
if (page) {
|
||||
page = parseInt(page, 10);
|
||||
} else {
|
||||
page = 1;
|
||||
}
|
||||
return page;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const users = getUsersFromState(state);
|
||||
const loading = isFetchUsersPending(state);
|
||||
const error = getFetchUsersFailure(state);
|
||||
|
||||
const usersLink = getUsersLink(state);
|
||||
|
||||
const page = getPageFromProps(ownProps);
|
||||
const canAddUsers = isPermittedToCreateUsers(state);
|
||||
const list = selectListAsCollection(state);
|
||||
|
||||
return {
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
canAddUsers,
|
||||
list,
|
||||
page,
|
||||
usersLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchUsersByPage: (link: string, page: number) => {
|
||||
dispatch(fetchUsersByPage(link, page));
|
||||
},
|
||||
fetchUsersByLink: (link: string) => {
|
||||
dispatch(fetchUsersByLink(link));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("users")(Users));
|
||||
// @flow
|
||||
import React from "react";
|
||||
import type { History } from "history";
|
||||
import { connect } from "react-redux";
|
||||
import { translate } from "react-i18next";
|
||||
|
||||
import {
|
||||
fetchUsersByPage,
|
||||
fetchUsersByLink,
|
||||
getUsersFromState,
|
||||
selectListAsCollection,
|
||||
isPermittedToCreateUsers,
|
||||
isFetchUsersPending,
|
||||
getFetchUsersFailure
|
||||
} from "../modules/users";
|
||||
|
||||
import { Page, CreateButton, Paginator } from "@scm-manager/ui-components";
|
||||
import { UserTable } from "./../components/table";
|
||||
import type { User, PagedCollection } from "@scm-manager/ui-types";
|
||||
import { getUsersLink } from "../../modules/indexResource";
|
||||
|
||||
type Props = {
|
||||
users: User[],
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
canAddUsers: boolean,
|
||||
list: PagedCollection,
|
||||
page: number,
|
||||
usersLink: string,
|
||||
|
||||
// context objects
|
||||
t: string => string,
|
||||
history: History,
|
||||
|
||||
// dispatch functions
|
||||
fetchUsersByPage: (link: string, page: number) => void,
|
||||
fetchUsersByLink: (link: string) => void
|
||||
};
|
||||
|
||||
class Users extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.fetchUsersByPage(this.props.usersLink, this.props.page);
|
||||
}
|
||||
|
||||
onPageChange = (link: string) => {
|
||||
this.props.fetchUsersByLink(link);
|
||||
};
|
||||
|
||||
/**
|
||||
* reflect page transitions in the uri
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
const { page, list } = this.props;
|
||||
if (list && (list.page || list.page === 0)) {
|
||||
// backend starts paging by 0
|
||||
const statePage: number = list.page + 1;
|
||||
if (page !== statePage) {
|
||||
this.props.history.push(`/users/${statePage}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { users, loading, error, t } = this.props;
|
||||
return (
|
||||
<Page
|
||||
title={t("users.title")}
|
||||
subtitle={t("users.subtitle")}
|
||||
loading={loading || !users}
|
||||
error={error}
|
||||
>
|
||||
<UserTable users={users} />
|
||||
{this.renderPaginator()}
|
||||
{this.renderCreateButton()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderPaginator() {
|
||||
const { list } = this.props;
|
||||
if (list) {
|
||||
return <Paginator collection={list} onPageChange={this.onPageChange} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderCreateButton() {
|
||||
const { t } = this.props;
|
||||
if (this.props.canAddUsers) {
|
||||
return <CreateButton label={t("users.createButton")} link="/users/add" />;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getPageFromProps = props => {
|
||||
let page = props.match.params.page;
|
||||
if (page) {
|
||||
page = parseInt(page, 10);
|
||||
} else {
|
||||
page = 1;
|
||||
}
|
||||
return page;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const users = getUsersFromState(state);
|
||||
const loading = isFetchUsersPending(state);
|
||||
const error = getFetchUsersFailure(state);
|
||||
|
||||
const usersLink = getUsersLink(state);
|
||||
|
||||
const page = getPageFromProps(ownProps);
|
||||
const canAddUsers = isPermittedToCreateUsers(state);
|
||||
const list = selectListAsCollection(state);
|
||||
|
||||
return {
|
||||
users,
|
||||
loading,
|
||||
error,
|
||||
canAddUsers,
|
||||
list,
|
||||
page,
|
||||
usersLink
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchUsersByPage: (link: string, page: number) => {
|
||||
dispatch(fetchUsersByPage(link, page));
|
||||
},
|
||||
fetchUsersByLink: (link: string) => {
|
||||
dispatch(fetchUsersByLink(link));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("users")(Users));
|
||||
|
||||
Reference in New Issue
Block a user