mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-14 09:25:43 +01:00
restructure ui for users
This commit is contained in:
@@ -2,13 +2,15 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
label: string,
|
label?: string,
|
||||||
checked: boolean,
|
checked: boolean,
|
||||||
onChange: boolean => void
|
onChange?: boolean => void
|
||||||
};
|
};
|
||||||
class Checkbox extends React.Component<Props> {
|
class Checkbox extends React.Component<Props> {
|
||||||
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||||
this.props.onChange(event.target.checked);
|
if (this.props.onChange) {
|
||||||
|
this.props.onChange(event.target.checked);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
20
scm-ui/src/components/SecondaryNavigation/NavAction.js
Normal file
20
scm-ui/src/components/SecondaryNavigation/NavAction.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string,
|
||||||
|
action: () => void
|
||||||
|
};
|
||||||
|
|
||||||
|
class NavAction extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { label, action } = this.props;
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<a onClick={action}>{label}</a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavAction;
|
||||||
37
scm-ui/src/components/SecondaryNavigation/NavLink.js
Normal file
37
scm-ui/src/components/SecondaryNavigation/NavLink.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
//@flow
|
||||||
|
import * as React from "react";
|
||||||
|
import { Route, Link } from "react-router-dom";
|
||||||
|
|
||||||
|
// TODO mostly copy of PrimaryNavigationLink
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
to: string,
|
||||||
|
label: string,
|
||||||
|
activeOnlyWhenExact?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class NavLink extends React.Component<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
activeOnlyWhenExact: true
|
||||||
|
};
|
||||||
|
|
||||||
|
renderLink = (route: any) => {
|
||||||
|
const { to, label } = this.props;
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<Link className={route.match ? "is-active" : ""} to={to}>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { to, activeOnlyWhenExact } = this.props;
|
||||||
|
return (
|
||||||
|
<Route path={to} exact={activeOnlyWhenExact} children={this.renderLink} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NavLink;
|
||||||
14
scm-ui/src/components/SecondaryNavigation/Navigation.js
Normal file
14
scm-ui/src/components/SecondaryNavigation/Navigation.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//@flow
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children?: React.Node
|
||||||
|
};
|
||||||
|
|
||||||
|
class Navigation extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
return <aside className="menu">{this.props.children}</aside>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navigation;
|
||||||
21
scm-ui/src/components/SecondaryNavigation/Section.js
Normal file
21
scm-ui/src/components/SecondaryNavigation/Section.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//@flow
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
label: string,
|
||||||
|
children?: React.Node
|
||||||
|
};
|
||||||
|
|
||||||
|
class Section extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { label, children } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p className="menu-label">{label}</p>
|
||||||
|
<ul className="menu-list">{children}</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Section;
|
||||||
4
scm-ui/src/components/SecondaryNavigation/index.js
Normal file
4
scm-ui/src/components/SecondaryNavigation/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { default as Navigation } from "./Navigation";
|
||||||
|
export { default as Section } from "./Section";
|
||||||
|
export { default as NavLink } from "./NavLink";
|
||||||
|
export { default as NavAction } from "./NavAction";
|
||||||
@@ -10,8 +10,8 @@ import Logout from "../containers/Logout";
|
|||||||
|
|
||||||
import { Switch } from "react-router-dom";
|
import { Switch } from "react-router-dom";
|
||||||
import ProtectedRoute from "../components/ProtectedRoute";
|
import ProtectedRoute from "../components/ProtectedRoute";
|
||||||
import EditUser from "../users/containers/EditUser";
|
|
||||||
import AddUser from "../users/containers/AddUser";
|
import AddUser from "../users/containers/AddUser";
|
||||||
|
import SingleUser from "../users/containers/SingleUser";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
authenticated?: boolean
|
authenticated?: boolean
|
||||||
@@ -39,13 +39,13 @@ class Main extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
path="/users/edit/:name"
|
path="/users/add"
|
||||||
component={EditUser}
|
component={AddUser}
|
||||||
/>
|
/>
|
||||||
<ProtectedRoute
|
<ProtectedRoute
|
||||||
authenticated={authenticated}
|
authenticated={authenticated}
|
||||||
path="/users/add"
|
path="/user/:name"
|
||||||
component={AddUser}
|
component={SingleUser}
|
||||||
/>
|
/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,33 +3,48 @@ import React from "react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import UserForm from "./UserForm";
|
import UserForm from "./UserForm";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
|
import type { History } from "history";
|
||||||
import { createUser } from "../modules/users";
|
import { createUser } from "../modules/users";
|
||||||
|
import Page from "../../components/Page";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
addUser: User => void,
|
addUser: (user: User, callback?: () => void) => void,
|
||||||
loading?: boolean
|
loading?: boolean,
|
||||||
|
error?: Error,
|
||||||
|
history: History
|
||||||
};
|
};
|
||||||
|
|
||||||
class AddUser extends React.Component<Props> {
|
class AddUser extends React.Component<Props> {
|
||||||
render() {
|
userCreated = () => {
|
||||||
const addUser = this.props.addUser;
|
const { history } = this.props;
|
||||||
|
history.push("/users");
|
||||||
|
};
|
||||||
|
|
||||||
|
createUser = (user: User) => {
|
||||||
|
this.props.addUser(user, this.userCreated);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, error } = this.props;
|
||||||
|
|
||||||
|
// TODO i18n
|
||||||
return (
|
return (
|
||||||
<div>
|
<Page
|
||||||
<UserForm
|
title="Create User"
|
||||||
submitForm={user => addUser(user)}
|
subtitle="Create a new user"
|
||||||
loading={this.props.loading}
|
loading={loading}
|
||||||
/>
|
error={error}
|
||||||
</div>
|
>
|
||||||
|
<UserForm submitForm={user => this.createUser(user)} />
|
||||||
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
addUser: (user: User) => {
|
addUser: (user: User, callback?: () => void) => {
|
||||||
dispatch(createUser(user));
|
dispatch(createUser(user, callback));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
import type { UserEntry } from "../types/UserEntry";
|
|
||||||
import { confirmAlert } from "../../components/ConfirmAlert";
|
import { confirmAlert } from "../../components/ConfirmAlert";
|
||||||
import DeleteButton from "../../components/DeleteButton";
|
import { NavAction } from "../../components/SecondaryNavigation";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
entry: UserEntry,
|
user: User,
|
||||||
confirmDialog?: boolean,
|
confirmDialog?: boolean,
|
||||||
t: string => string,
|
t: string => string,
|
||||||
deleteUser: (user: User) => void
|
deleteUser: (user: User) => void
|
||||||
@@ -19,7 +18,7 @@ class DeleteUserButton extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
deleteUser = () => {
|
deleteUser = () => {
|
||||||
this.props.deleteUser(this.props.entry.entry);
|
this.props.deleteUser(this.props.user);
|
||||||
};
|
};
|
||||||
|
|
||||||
confirmDelete = () => {
|
confirmDelete = () => {
|
||||||
@@ -41,23 +40,17 @@ class DeleteUserButton extends React.Component<Props> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
isDeletable = () => {
|
isDeletable = () => {
|
||||||
return this.props.entry.entry._links.delete;
|
return this.props.user._links.delete;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { confirmDialog, entry, t } = this.props;
|
const { confirmDialog, t } = this.props;
|
||||||
const action = confirmDialog ? this.confirmDelete : this.deleteUser;
|
const action = confirmDialog ? this.confirmDelete : this.deleteUser;
|
||||||
|
|
||||||
if (!this.isDeletable()) {
|
if (!this.isDeletable()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return <NavAction label={t("delete-user-button.label")} action={action} />;
|
||||||
<DeleteButton
|
|
||||||
label={t("delete-user-button.label")}
|
|
||||||
action={action}
|
|
||||||
loading={entry.loading}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,61 +9,53 @@ jest.mock("../../components/ConfirmAlert");
|
|||||||
|
|
||||||
describe("DeleteUserButton", () => {
|
describe("DeleteUserButton", () => {
|
||||||
it("should render nothing, if the delete link is missing", () => {
|
it("should render nothing, if the delete link is missing", () => {
|
||||||
const entry = {
|
const user = {
|
||||||
entry: {
|
_links: {}
|
||||||
_links: {}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const button = shallow(
|
const button = shallow(
|
||||||
<DeleteUserButton entry={entry} deleteUser={() => {}} />
|
<DeleteUserButton user={user} deleteUser={() => {}} />
|
||||||
);
|
);
|
||||||
expect(button.text()).toBe("");
|
expect(button.text()).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the button", () => {
|
it("should render the button", () => {
|
||||||
const entry = {
|
const user = {
|
||||||
entry: {
|
_links: {
|
||||||
_links: {
|
delete: {
|
||||||
delete: {
|
href: "/users"
|
||||||
href: "/users"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const button = mount(
|
const button = mount(
|
||||||
<DeleteUserButton entry={entry} deleteUser={() => {}} />
|
<DeleteUserButton user={user} deleteUser={() => {}} />
|
||||||
);
|
);
|
||||||
expect(button.text()).not.toBe("");
|
expect(button.text()).not.toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should open the confirm dialog on button click", () => {
|
it("should open the confirm dialog on button click", () => {
|
||||||
const entry = {
|
const user = {
|
||||||
entry: {
|
_links: {
|
||||||
_links: {
|
delete: {
|
||||||
delete: {
|
href: "/users"
|
||||||
href: "/users"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const button = mount(
|
const button = mount(
|
||||||
<DeleteUserButton entry={entry} deleteUser={() => {}} />
|
<DeleteUserButton user={user} deleteUser={() => {}} />
|
||||||
);
|
);
|
||||||
button.simulate("click");
|
button.find("a").simulate("click");
|
||||||
|
|
||||||
expect(confirmAlert.mock.calls.length).toBe(1);
|
expect(confirmAlert.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call the delete user function with delete url", () => {
|
it("should call the delete user function with delete url", () => {
|
||||||
const entry = {
|
const user = {
|
||||||
entry: {
|
_links: {
|
||||||
_links: {
|
delete: {
|
||||||
delete: {
|
href: "/users"
|
||||||
href: "/users"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -75,12 +67,12 @@ describe("DeleteUserButton", () => {
|
|||||||
|
|
||||||
const button = mount(
|
const button = mount(
|
||||||
<DeleteUserButton
|
<DeleteUserButton
|
||||||
entry={entry}
|
user={user}
|
||||||
confirmDialog={false}
|
confirmDialog={false}
|
||||||
deleteUser={capture}
|
deleteUser={capture}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
button.simulate("click");
|
button.find("a").simulate("click");
|
||||||
|
|
||||||
expect(calledUrl).toBe("/users");
|
expect(calledUrl).toBe("/users");
|
||||||
});
|
});
|
||||||
|
|||||||
48
scm-ui/src/users/containers/Details.js
Normal file
48
scm-ui/src/users/containers/Details.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import type { User } from "../types/User";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import Checkbox from "../../components/Checkbox";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: User,
|
||||||
|
t: string => string
|
||||||
|
};
|
||||||
|
|
||||||
|
class Details extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { user, t } = this.props;
|
||||||
|
return (
|
||||||
|
<table className="table">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>{t("user.name")}</td>
|
||||||
|
<td>{user.name}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("user.displayName")}</td>
|
||||||
|
<td>{user.displayName}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("user.mail")}</td>
|
||||||
|
<td>{user.mail}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("user.admin")}</td>
|
||||||
|
<td>
|
||||||
|
<Checkbox checked={user.admin} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("user.active")}</td>
|
||||||
|
<td>
|
||||||
|
<Checkbox checked={user.active} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("users")(Details);
|
||||||
@@ -3,50 +3,23 @@ import React from "react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import UserForm from "./UserForm";
|
import UserForm from "./UserForm";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
import type { UserEntry } from "../types/UserEntry";
|
import { modifyUser } from "../modules/users";
|
||||||
import Loading from "../../components/Loading";
|
|
||||||
|
|
||||||
import { modifyUser, fetchUser } from "../modules/users";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: string,
|
user: User,
|
||||||
fetchUser: string => void,
|
|
||||||
userEntry?: UserEntry,
|
|
||||||
updateUser: User => void,
|
updateUser: User => void,
|
||||||
loading: boolean
|
loading: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
class EditUser extends React.Component<Props> {
|
class EditUser extends React.Component<Props> {
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchUser(this.props.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const submitUser = this.props.updateUser;
|
const { user, updateUser } = this.props;
|
||||||
|
return <UserForm submitForm={user => updateUser(user)} user={user} />;
|
||||||
const { userEntry } = this.props;
|
|
||||||
|
|
||||||
if (!userEntry || userEntry.loading) {
|
|
||||||
return <Loading />;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<UserForm
|
|
||||||
submitForm={user => submitUser(user)}
|
|
||||||
user={userEntry.entry}
|
|
||||||
loading={userEntry.loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => {
|
const mapDispatchToProps = dispatch => {
|
||||||
return {
|
return {
|
||||||
fetchUser: (name: string) => {
|
|
||||||
dispatch(fetchUser(name));
|
|
||||||
},
|
|
||||||
updateUser: (user: User) => {
|
updateUser: (user: User) => {
|
||||||
dispatch(modifyUser(user));
|
dispatch(modifyUser(user));
|
||||||
}
|
}
|
||||||
@@ -54,16 +27,7 @@ const mapDispatchToProps = dispatch => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const name = ownProps.match.params.name;
|
return {};
|
||||||
let userEntry;
|
|
||||||
if (state.users && state.users.usersByNames) {
|
|
||||||
userEntry = state.users.usersByNames[name];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
userEntry
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|||||||
118
scm-ui/src/users/containers/SingleUser.js
Normal file
118
scm-ui/src/users/containers/SingleUser.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import Page from "../../components/Page";
|
||||||
|
import { Route } from "react-router";
|
||||||
|
import Details from "./Details";
|
||||||
|
import EditUser from "./EditUser";
|
||||||
|
import type { User } from "../types/User";
|
||||||
|
import type { UserEntry } from "../types/UserEntry";
|
||||||
|
import { fetchUser, deleteUser } from "../modules/users";
|
||||||
|
import Loading from "../../components/Loading";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Navigation,
|
||||||
|
Section,
|
||||||
|
NavLink
|
||||||
|
} from "../../components/SecondaryNavigation";
|
||||||
|
import DeleteUserButton from "./DeleteUserButton";
|
||||||
|
import ErrorPage from "../../components/ErrorPage";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
name: string,
|
||||||
|
userEntry?: UserEntry,
|
||||||
|
match: any,
|
||||||
|
deleteUser: (user: User) => void,
|
||||||
|
fetchUser: string => void
|
||||||
|
};
|
||||||
|
|
||||||
|
class SingleUser extends React.Component<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchUser(this.props.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
stripEndingSlash = (url: string) => {
|
||||||
|
if (url.endsWith("/")) {
|
||||||
|
return url.substring(0, url.length - 2);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { userEntry, match, deleteUser } = this.props;
|
||||||
|
|
||||||
|
if (!userEntry || userEntry.loading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userEntry.error) {
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
title="Error"
|
||||||
|
subtitle="Unknown user error"
|
||||||
|
error={userEntry.error}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userEntry.entry;
|
||||||
|
const url = this.stripEndingSlash(match.url);
|
||||||
|
|
||||||
|
// TODO i18n
|
||||||
|
|
||||||
|
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`}
|
||||||
|
component={() => <EditUser user={user} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="column">
|
||||||
|
<Navigation>
|
||||||
|
<Section label="Navigation">
|
||||||
|
<NavLink to={`${url}`} label="Information" />
|
||||||
|
<NavLink to={`${url}/edit`} label="Edit" />
|
||||||
|
</Section>
|
||||||
|
<Section label="Actions">
|
||||||
|
<DeleteUserButton user={user} deleteUser={deleteUser} />
|
||||||
|
<NavLink to="/users" label="Back" />
|
||||||
|
</Section>
|
||||||
|
</Navigation>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
const name = ownProps.match.params.name;
|
||||||
|
let userEntry;
|
||||||
|
if (state.users && state.users.usersByNames) {
|
||||||
|
userEntry = state.users.usersByNames[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
userEntry
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
fetchUser: (name: string) => {
|
||||||
|
dispatch(fetchUser(name));
|
||||||
|
},
|
||||||
|
deleteUser: (user: User) => {
|
||||||
|
dispatch(deleteUser(user));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(SingleUser);
|
||||||
@@ -42,45 +42,43 @@ class UserForm extends React.Component<Props, User> {
|
|||||||
const user = this.state;
|
const user = this.state;
|
||||||
if (user) {
|
if (user) {
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<form onSubmit={this.submit}>
|
||||||
<form onSubmit={this.submit}>
|
<InputField
|
||||||
<InputField
|
label={t("user.name")}
|
||||||
label={t("user.name")}
|
onChange={this.handleUsernameChange}
|
||||||
onChange={this.handleUsernameChange}
|
value={user ? user.name : ""}
|
||||||
value={user ? user.name : ""}
|
/>
|
||||||
/>
|
<InputField
|
||||||
<InputField
|
label={t("user.displayName")}
|
||||||
label={t("user.displayName")}
|
onChange={this.handleDisplayNameChange}
|
||||||
onChange={this.handleDisplayNameChange}
|
value={user ? user.displayName : ""}
|
||||||
value={user ? user.displayName : ""}
|
/>
|
||||||
/>
|
<InputField
|
||||||
<InputField
|
label={t("user.mail")}
|
||||||
label={t("user.mail")}
|
onChange={this.handleEmailChange}
|
||||||
onChange={this.handleEmailChange}
|
value={user ? user.mail : ""}
|
||||||
value={user ? user.mail : ""}
|
/>
|
||||||
/>
|
<InputField
|
||||||
<InputField
|
label={t("user.password")}
|
||||||
label={t("user.password")}
|
type="password"
|
||||||
type="password"
|
onChange={this.handlePasswordChange}
|
||||||
onChange={this.handlePasswordChange}
|
value={user ? user.password : ""}
|
||||||
value={user ? user.password : ""}
|
/>
|
||||||
/>
|
<Checkbox
|
||||||
<Checkbox
|
label={t("user.admin")}
|
||||||
label={t("user.admin")}
|
onChange={this.handleAdminChange}
|
||||||
onChange={this.handleAdminChange}
|
checked={user ? user.admin : false}
|
||||||
checked={user ? user.admin : false}
|
/>
|
||||||
/>
|
<Checkbox
|
||||||
<Checkbox
|
label={t("user.active")}
|
||||||
label={t("user.active")}
|
onChange={this.handleActiveChange}
|
||||||
onChange={this.handleActiveChange}
|
checked={user ? user.active : false}
|
||||||
checked={user ? user.active : false}
|
/>
|
||||||
/>
|
<SubmitButton
|
||||||
<SubmitButton
|
label={t("user-form.submit")}
|
||||||
label={t("user-form.submit")}
|
loading={this.props.loading}
|
||||||
loading={this.props.loading}
|
/>
|
||||||
/>
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
|
|||||||
@@ -1,33 +1,30 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import DeleteUserButton from "./DeleteUserButton";
|
import { Link } from "react-router-dom";
|
||||||
import EditUserButton from "./EditUserButton";
|
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
import type { UserEntry } from "../types/UserEntry";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
entry: UserEntry,
|
user: User
|
||||||
deleteUser: User => void
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class UserRow extends React.Component<Props> {
|
export default class UserRow extends React.Component<Props> {
|
||||||
|
renderLink(to: string, label: string) {
|
||||||
|
return <Link to={to}>{label}</Link>;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { entry, deleteUser } = this.props;
|
const { user } = this.props;
|
||||||
const user = entry.entry;
|
const to = `/user/${user.name}`;
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{user.name}</td>
|
<td className="is-hidden-mobile">{this.renderLink(to, user.name)}</td>
|
||||||
<td>{user.displayName}</td>
|
<td>{this.renderLink(to, user.displayName)}</td>
|
||||||
<td>{user.mail}</td>
|
|
||||||
<td>
|
<td>
|
||||||
|
<a href={`mailto: ${user.mail}`}>{user.mail}</a>
|
||||||
|
</td>
|
||||||
|
<td className="is-hidden-mobile">
|
||||||
<input type="checkbox" id="admin" checked={user.admin} readOnly />
|
<input type="checkbox" id="admin" checked={user.admin} readOnly />
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<DeleteUserButton entry={entry} deleteUser={deleteUser} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<EditUserButton entry={entry} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,36 +2,29 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import UserRow from "./UserRow";
|
import UserRow from "./UserRow";
|
||||||
import type { User } from "../types/User";
|
|
||||||
import type { UserEntry } from "../types/UserEntry";
|
import type { UserEntry } from "../types/UserEntry";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
t: string => string,
|
t: string => string,
|
||||||
entries: Array<UserEntry>,
|
entries: Array<UserEntry>
|
||||||
deleteUser: User => void
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserTable extends React.Component<Props> {
|
class UserTable extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const { deleteUser, t } = this.props;
|
const { entries, t } = this.props;
|
||||||
const entries = this.props.entries;
|
|
||||||
return (
|
return (
|
||||||
<table className="table is-hoverable is-fullwidth">
|
<table className="table is-hoverable is-fullwidth">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("user.name")}</th>
|
<th className="is-hidden-mobile">{t("user.name")}</th>
|
||||||
<th>{t("user.displayName")}</th>
|
<th>{t("user.displayName")}</th>
|
||||||
<th>{t("user.mail")}</th>
|
<th>{t("user.mail")}</th>
|
||||||
<th>{t("user.admin")}</th>
|
<th className="is-hidden-mobile">{t("user.admin")}</th>
|
||||||
<th />
|
|
||||||
<th />
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{entries.map((entry, index) => {
|
{entries.map((entry, index) => {
|
||||||
return (
|
return <UserRow key={index} user={entry.entry} />;
|
||||||
<UserRow key={index} entry={entry} deleteUser={deleteUser} />
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
|
|
||||||
import { fetchUsers, deleteUser, getUsersFromState } from "../modules/users";
|
import { fetchUsers, getUsersFromState } from "../modules/users";
|
||||||
import Page from "../../components/Page";
|
import Page from "../../components/Page";
|
||||||
import UserTable from "./UserTable";
|
import UserTable from "./UserTable";
|
||||||
import type { User } from "../types/User";
|
import type { User } from "../types/User";
|
||||||
@@ -16,7 +16,6 @@ type Props = {
|
|||||||
t: string => string,
|
t: string => string,
|
||||||
userEntries: Array<UserEntry>,
|
userEntries: Array<UserEntry>,
|
||||||
fetchUsers: () => void,
|
fetchUsers: () => void,
|
||||||
deleteUser: User => void,
|
|
||||||
canAddUsers: boolean
|
canAddUsers: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ class Users extends React.Component<Props, User> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { userEntries, deleteUser, loading, t, error } = this.props;
|
const { userEntries, loading, t, error } = this.props;
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
title={t("users.title")}
|
title={t("users.title")}
|
||||||
@@ -34,7 +33,7 @@ class Users extends React.Component<Props, User> {
|
|||||||
loading={loading || !userEntries}
|
loading={loading || !userEntries}
|
||||||
error={error}
|
error={error}
|
||||||
>
|
>
|
||||||
<UserTable entries={userEntries} deleteUser={deleteUser} />
|
<UserTable entries={userEntries} />
|
||||||
{this.renderAddButton()}
|
{this.renderAddButton()}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
@@ -76,9 +75,6 @@ const mapDispatchToProps = dispatch => {
|
|||||||
return {
|
return {
|
||||||
fetchUsers: () => {
|
fetchUsers: () => {
|
||||||
dispatch(fetchUsers());
|
dispatch(fetchUsers());
|
||||||
},
|
|
||||||
deleteUser: (user: User) => {
|
|
||||||
dispatch(deleteUser(user));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export function fetchUsersFailure(url: string, error: Error): Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//fetch user
|
//fetch user
|
||||||
//TODO: fetchUsersPending and FetchUsersFailure are the wrong functions here!
|
|
||||||
export function fetchUser(name: string) {
|
export function fetchUser(name: string) {
|
||||||
const userUrl = USERS_URL + "/" + name;
|
const userUrl = USERS_URL + "/" + name;
|
||||||
return function(dispatch: any) {
|
return function(dispatch: any) {
|
||||||
@@ -98,7 +97,7 @@ export function fetchUser(name: string) {
|
|||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(cause => {
|
||||||
const error = new Error(`could not fetch user: ${cause.message}`);
|
const error = new Error(`could not fetch user: ${cause.message}`);
|
||||||
dispatch(fetchUserFailure(USERS_URL, error));
|
dispatch(fetchUserFailure(name, error));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -129,14 +128,16 @@ export function fetchUserFailure(username: string, error: Error): Action {
|
|||||||
|
|
||||||
//create user
|
//create user
|
||||||
|
|
||||||
export function createUser(user: User) {
|
export function createUser(user: User, callback?: () => void) {
|
||||||
return function(dispatch: Dispatch) {
|
return function(dispatch: Dispatch) {
|
||||||
dispatch(createUserPending(user));
|
dispatch(createUserPending(user));
|
||||||
return apiClient
|
return apiClient
|
||||||
.postWithContentType(USERS_URL, user, CONTENT_TYPE_USER)
|
.postWithContentType(USERS_URL, user, CONTENT_TYPE_USER)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(createUserSuccess());
|
dispatch(createUserSuccess());
|
||||||
dispatch(fetchUsers());
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(err =>
|
.catch(err =>
|
||||||
dispatch(
|
dispatch(
|
||||||
@@ -224,7 +225,6 @@ export function deleteUser(user: User) {
|
|||||||
.delete(user._links.delete.href)
|
.delete(user._links.delete.href)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(deleteUserSuccess(user));
|
dispatch(deleteUserSuccess(user));
|
||||||
dispatch(fetchUsers());
|
|
||||||
})
|
})
|
||||||
.catch(cause => {
|
.catch(cause => {
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
@@ -382,7 +382,7 @@ export default function reducer(state: any = {}, action: any = {}) {
|
|||||||
|
|
||||||
case FETCH_USER_FAILURE:
|
case FETCH_USER_FAILURE:
|
||||||
return reduceUsersByNames(state, action.payload.username, {
|
return reduceUsersByNames(state, action.payload.username, {
|
||||||
loading: true,
|
loading: false,
|
||||||
error: action.payload.error
|
error: action.payload.error
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -411,18 +411,12 @@ export default function reducer(state: any = {}, action: any = {}) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
case DELETE_USER_FAILURE:
|
case DELETE_USER_FAILURE:
|
||||||
const newState = reduceUsersByNames(state, action.payload.user.name, {
|
return reduceUsersByNames(state, action.payload.user.name, {
|
||||||
loading: false,
|
loading: false,
|
||||||
error: action.payload.error,
|
error: action.payload.error,
|
||||||
entry: action.payload.user
|
entry: action.payload.user
|
||||||
});
|
});
|
||||||
return {
|
|
||||||
...newState,
|
|
||||||
users: {
|
|
||||||
...newState.users,
|
|
||||||
error: action.payload.error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Add single user cases
|
// Add single user cases
|
||||||
case CREATE_USER_PENDING:
|
case CREATE_USER_PENDING:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -209,15 +209,11 @@ describe("users fetch()", () => {
|
|||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
|
|
||||||
// after create, the users are fetched again
|
|
||||||
fetchMock.getOnce(USERS_URL, response);
|
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(createUser(userZaphod)).then(() => {
|
return store.dispatch(createUser(userZaphod)).then(() => {
|
||||||
const actions = store.getActions();
|
const actions = store.getActions();
|
||||||
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
expect(actions[0].type).toEqual(CREATE_USER_PENDING);
|
||||||
expect(actions[1].type).toEqual(CREATE_USER_SUCCESS);
|
expect(actions[1].type).toEqual(CREATE_USER_SUCCESS);
|
||||||
expect(actions[2].type).toEqual(FETCH_USERS_PENDING);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -235,6 +231,24 @@ describe("users fetch()", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should call the callback after user successfully created", () => {
|
||||||
|
// unmatched
|
||||||
|
fetchMock.postOnce(USERS_URL, {
|
||||||
|
status: 204
|
||||||
|
});
|
||||||
|
|
||||||
|
let callMe = "not yet";
|
||||||
|
|
||||||
|
const callback = () => {
|
||||||
|
callMe = "yeah";
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = mockStore({});
|
||||||
|
return store.dispatch(createUser(userZaphod, callback)).then(() => {
|
||||||
|
expect(callMe).toBe("yeah");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("successfully update user", () => {
|
it("successfully update user", () => {
|
||||||
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", {
|
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", {
|
||||||
status: 204
|
status: 204
|
||||||
@@ -269,8 +283,6 @@ describe("users fetch()", () => {
|
|||||||
fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", {
|
fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", {
|
||||||
status: 204
|
status: 204
|
||||||
});
|
});
|
||||||
// after update, the users are fetched again
|
|
||||||
fetchMock.getOnce(USERS_URL, response);
|
|
||||||
|
|
||||||
const store = mockStore({});
|
const store = mockStore({});
|
||||||
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
return store.dispatch(deleteUser(userZaphod)).then(() => {
|
||||||
@@ -278,7 +290,6 @@ describe("users fetch()", () => {
|
|||||||
expect(actions[0].type).toEqual(DELETE_USER);
|
expect(actions[0].type).toEqual(DELETE_USER);
|
||||||
expect(actions[0].payload).toBe(userZaphod);
|
expect(actions[0].payload).toBe(userZaphod);
|
||||||
expect(actions[1].type).toEqual(DELETE_USER_SUCCESS);
|
expect(actions[1].type).toEqual(DELETE_USER_SUCCESS);
|
||||||
expect(actions[2].type).toEqual(FETCH_USERS_PENDING);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -458,11 +469,19 @@ describe("users reducer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update state according to FETCH_USER_FAILURE action", () => {
|
it("should update state according to FETCH_USER_FAILURE action", () => {
|
||||||
|
const error = new Error("kaputt!");
|
||||||
const newState = reducer(
|
const newState = reducer(
|
||||||
{},
|
{
|
||||||
fetchUserFailure(userFord.name, new Error("kaputt!"))
|
usersByNames: {
|
||||||
|
ford: {
|
||||||
|
loading: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchUserFailure(userFord.name, error)
|
||||||
);
|
);
|
||||||
expect(newState.usersByNames["ford"].error).toBeTruthy();
|
expect(newState.usersByNames["ford"].error).toBe(error);
|
||||||
|
expect(newState.usersByNames["ford"].loading).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update state according to FETCH_USER_SUCCESS action", () => {
|
it("should update state according to FETCH_USER_SUCCESS action", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user