diff --git a/scm-ui/src/users/components/UserForm.js b/scm-ui/src/users/components/UserForm.js index 40ecf4504a..54c5e04dde 100644 --- a/scm-ui/src/users/components/UserForm.js +++ b/scm-ui/src/users/components/UserForm.js @@ -1,10 +1,9 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; -import type { User } from "../types/User"; -import { InputField, Checkbox } from "../../components/forms"; -import { SubmitButton } from "../../components/buttons"; -import Loading from "../../components/Loading"; +import {translate} from "react-i18next"; +import type {User} from "../types/User"; +import {Checkbox, InputField} from "../../components/forms"; +import {SubmitButton} from "../../components/buttons"; type Props = { submitForm: User => void, diff --git a/scm-ui/src/users/containers/EditUser.js b/scm-ui/src/users/containers/EditUser.js index bb07424b14..819d734dad 100644 --- a/scm-ui/src/users/containers/EditUser.js +++ b/scm-ui/src/users/containers/EditUser.js @@ -1,27 +1,38 @@ //@flow import React from "react"; -import { connect } from "react-redux"; +import {connect} from "react-redux"; +import {withRouter} from "react-router-dom"; import UserForm from "./../components/UserForm"; -import type { User } from "../types/User"; -import { modifyUser } from "../modules/users"; +import type {User} from "../types/User"; +import {modifyUser} from "../modules/users"; +import type {History} from "history"; type Props = { user: User, - updateUser: User => void, - loading: boolean + modifyUser: (user: User, callback?: () => void) => void, + loading: boolean, + history: History }; class EditUser extends React.Component { + userModified = (user: User) => () => { + this.props.history.push(`/user/${user.name}`); + }; + + modifyUser = (user: User) => { + this.props.modifyUser(user, this.userModified(user)); + }; + render() { - const { user, updateUser } = this.props; - return updateUser(user)} user={user} />; + const { user } = this.props; + return this.modifyUser(user)} user={user} />; } } const mapDispatchToProps = dispatch => { return { - updateUser: (user: User) => { - dispatch(modifyUser(user)); + modifyUser: (user: User, callback?: () => void) => { + dispatch(modifyUser(user, callback)); } }; }; @@ -33,4 +44,4 @@ const mapStateToProps = (state, ownProps) => { export default connect( mapStateToProps, mapDispatchToProps -)(EditUser); +)(withRouter(EditUser)); diff --git a/scm-ui/src/users/containers/SingleUser.js b/scm-ui/src/users/containers/SingleUser.js index cac857a68a..f8e5c1ee49 100644 --- a/scm-ui/src/users/containers/SingleUser.js +++ b/scm-ui/src/users/containers/SingleUser.js @@ -1,25 +1,27 @@ //@flow import React from "react"; -import { connect } from "react-redux"; -import { Page } from "../../components/layout"; -import { Route } from "react-router"; -import { Details } from "./../components/table"; +import {connect} from "react-redux"; +import {Page} from "../../components/layout"; +import {Route} from "react-router"; +import {Details} from "./../components/table"; import EditUser from "./EditUser"; -import type { User } from "../types/User"; -import type { UserEntry } from "../types/UserEntry"; -import { fetchUser, deleteUser } from "../modules/users"; +import type {User} from "../types/User"; +import type {UserEntry} from "../types/UserEntry"; +import type {History} from "history"; +import {deleteUser, fetchUser} from "../modules/users"; import Loading from "../../components/Loading"; -import { Navigation, Section, NavLink } from "../../components/navigation"; -import { DeleteUserButton } from "./../components/buttons"; +import {Navigation, NavLink, Section} from "../../components/navigation"; +import {DeleteUserButton} from "./../components/buttons"; import ErrorPage from "../../components/ErrorPage"; type Props = { name: string, userEntry?: UserEntry, match: any, - deleteUser: (user: User) => void, - fetchUser: string => void + deleteUser: (user: User, callback?: () => void) => void, + fetchUser: string => void, + history: History }; class SingleUser extends React.Component { @@ -27,6 +29,14 @@ class SingleUser extends React.Component { this.props.fetchUser(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); @@ -34,8 +44,12 @@ class SingleUser extends React.Component { return url; }; + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; + render() { - const { userEntry, match, deleteUser } = this.props; + const { userEntry } = this.props; if (!userEntry || userEntry.loading) { return ; @@ -52,7 +66,7 @@ class SingleUser extends React.Component { } const user = userEntry.entry; - const url = this.stripEndingSlash(match.url); + const url = this.matchedUrl(); // TODO i18n @@ -73,7 +87,7 @@ class SingleUser extends React.Component {
- +
@@ -102,8 +116,8 @@ const mapDispatchToProps = dispatch => { fetchUser: (name: string) => { dispatch(fetchUser(name)); }, - deleteUser: (user: User) => { - dispatch(deleteUser(user)); + deleteUser: (user: User, callback?: () => void) => { + dispatch(deleteUser(user, callback)); } }; }; diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index 850aaebf65..0792bfa616 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -1,9 +1,9 @@ // @flow -import { apiClient } from "../../apiclient"; -import type { User } from "../types/User"; -import type { UserEntry } from "../types/UserEntry"; -import { Dispatch, combineReducers } from "redux"; -import type { Action } from "../../types/Action"; +import {apiClient} from "../../apiclient"; +import type {User} from "../types/User"; +import type {UserEntry} from "../types/UserEntry"; +import {combineReducers, Dispatch} from "redux"; +import type {Action} from "../../types/Action"; export const FETCH_USERS_PENDING = "scm/users/FETCH_USERS_PENDING"; export const FETCH_USERS_SUCCESS = "scm/users/FETCH_USERS_SUCCESS"; @@ -173,13 +173,16 @@ export function createUserFailure(user: User, err: Error): Action { //modify user -export function modifyUser(user: User) { +export function modifyUser(user: User, callback?: () => void) { return function(dispatch: Dispatch) { dispatch(modifyUserPending(user)); return apiClient .putWithContentType(user._links.update.href, user, CONTENT_TYPE_USER) .then(() => { dispatch(modifyUserSuccess(user)); + if (callback) { + callback(); + } }) .catch(err => { dispatch(modifyUserFailure(user, err)); @@ -213,13 +216,16 @@ export function modifyUserFailure(user: User, error: Error): Action { //delete user -export function deleteUser(user: User) { +export function deleteUser(user: User, callback?: () => void) { return function(dispatch: any) { dispatch(deleteUserPending(user)); return apiClient .delete(user._links.delete.href) .then(() => { dispatch(deleteUserSuccess(user)); + if (callback) { + callback(); + } }) .catch(cause => { const error = new Error( diff --git a/scm-ui/src/users/modules/users.test.js b/scm-ui/src/users/modules/users.test.js index 65e03bf60d..0a13952293 100644 --- a/scm-ui/src/users/modules/users.test.js +++ b/scm-ui/src/users/modules/users.test.js @@ -3,46 +3,44 @@ import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; -import { - FETCH_USERS_PENDING, - FETCH_USERS_SUCCESS, - fetchUsers, - FETCH_USERS_FAILURE, - createUserPending, +import reducer, { + CREATE_USER_FAILURE, CREATE_USER_PENDING, CREATE_USER_SUCCESS, - CREATE_USER_FAILURE, - modifyUser, - MODIFY_USER_PENDING, - MODIFY_USER_FAILURE, - MODIFY_USER_SUCCESS, - deleteUserPending, - deleteUserFailure, + createUser, + createUserFailure, + createUserPending, + createUserSuccess, + DELETE_USER_FAILURE, DELETE_USER_PENDING, DELETE_USER_SUCCESS, - DELETE_USER_FAILURE, deleteUser, - fetchUsersFailure, - fetchUsersSuccess, - fetchUser, + deleteUserFailure, + deleteUserPending, + deleteUserSuccess, + FETCH_USER_FAILURE, FETCH_USER_PENDING, FETCH_USER_SUCCESS, - FETCH_USER_FAILURE, - createUser, - createUserSuccess, - createUserFailure, - modifyUserPending, - modifyUserSuccess, - modifyUserFailure, - fetchUserSuccess, - deleteUserSuccess, - fetchUsersPending, + FETCH_USERS_FAILURE, + FETCH_USERS_PENDING, + FETCH_USERS_SUCCESS, + fetchUser, + fetchUserFailure, fetchUserPending, - fetchUserFailure + fetchUsers, + fetchUsersFailure, + fetchUsersPending, + fetchUsersSuccess, + fetchUserSuccess, + MODIFY_USER_FAILURE, + MODIFY_USER_PENDING, + MODIFY_USER_SUCCESS, + modifyUser, + modifyUserFailure, + modifyUserPending, + modifyUserSuccess } from "./users"; -import reducer from "./users"; - const userZaphod = { active: true, admin: true, @@ -236,16 +234,32 @@ describe("users fetch()", () => { fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { status: 204 }); - // after update, the users are fetched again const store = mockStore({}); return store.dispatch(modifyUser(userZaphod)).then(() => { const actions = store.getActions(); + expect(actions.length).toBe(2); expect(actions[0].type).toEqual(MODIFY_USER_PENDING); expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS); }); }); + it("should call callback, after successful modified user", () => { + fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { + status: 204 + }); + + let called = false; + const callMe = () => { + called = true; + }; + + const store = mockStore({}); + return store.dispatch(modifyUser(userZaphod, callMe)).then(() => { + expect(called).toBeTruthy(); + }); + }); + it("should fail updating user on HTTP 500", () => { fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { status: 500 @@ -264,18 +278,33 @@ describe("users fetch()", () => { fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { status: 204 }); - // after update, the users are fetched again - fetchMock.getOnce(USERS_URL, response); const store = mockStore({}); return store.dispatch(deleteUser(userZaphod)).then(() => { const actions = store.getActions(); + expect(actions.length).toBe(2); expect(actions[0].type).toEqual(DELETE_USER_PENDING); expect(actions[0].payload).toBe(userZaphod); expect(actions[1].type).toEqual(DELETE_USER_SUCCESS); }); }); + it("should call the callback, after successful delete", () => { + fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { + status: 204 + }); + + let called = false; + const callMe = () => { + called = true; + }; + + const store = mockStore({}); + return store.dispatch(deleteUser(userZaphod, callMe)).then(() => { + expect(called).toBeTruthy(); + }); + }); + it("should fail to delete user zaphod", () => { fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { status: 500