Merged heads

This commit is contained in:
Philipp Czora
2018-07-26 09:52:21 +02:00
5 changed files with 130 additions and 71 deletions

View File

@@ -2,9 +2,8 @@
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 { InputField, Checkbox } from "../../components/forms"; import {Checkbox, InputField} from "../../components/forms";
import {SubmitButton} from "../../components/buttons"; import {SubmitButton} from "../../components/buttons";
import Loading from "../../components/Loading";
type Props = { type Props = {
submitForm: User => void, submitForm: User => void,

View File

@@ -1,27 +1,38 @@
//@flow //@flow
import React from "react"; 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 UserForm from "./../components/UserForm";
import type {User} from "../types/User"; import type {User} from "../types/User";
import {modifyUser} from "../modules/users"; import {modifyUser} from "../modules/users";
import type {History} from "history";
type Props = { type Props = {
user: User, user: User,
updateUser: User => void, modifyUser: (user: User, callback?: () => void) => void,
loading: boolean loading: boolean,
history: History
}; };
class EditUser extends React.Component<Props> { class EditUser extends React.Component<Props> {
userModified = (user: User) => () => {
this.props.history.push(`/user/${user.name}`);
};
modifyUser = (user: User) => {
this.props.modifyUser(user, this.userModified(user));
};
render() { render() {
const { user, updateUser } = this.props; const { user } = this.props;
return <UserForm submitForm={user => updateUser(user)} user={user} />; return <UserForm submitForm={user => this.modifyUser(user)} user={user} />;
} }
} }
const mapDispatchToProps = dispatch => { const mapDispatchToProps = dispatch => {
return { return {
updateUser: (user: User) => { modifyUser: (user: User, callback?: () => void) => {
dispatch(modifyUser(user)); dispatch(modifyUser(user, callback));
} }
}; };
}; };
@@ -33,4 +44,4 @@ const mapStateToProps = (state, ownProps) => {
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(EditUser); )(withRouter(EditUser));

View File

@@ -7,10 +7,11 @@ import { Details } from "./../components/table";
import EditUser from "./EditUser"; import EditUser from "./EditUser";
import type {User} from "../types/User"; import type {User} from "../types/User";
import type {UserEntry} from "../types/UserEntry"; import type {UserEntry} from "../types/UserEntry";
import { fetchUser, deleteUser } from "../modules/users"; import type {History} from "history";
import {deleteUser, fetchUser} from "../modules/users";
import Loading from "../../components/Loading"; import Loading from "../../components/Loading";
import { Navigation, Section, NavLink } from "../../components/navigation"; import {Navigation, NavLink, Section} from "../../components/navigation";
import {DeleteUserButton} from "./../components/buttons"; import {DeleteUserButton} from "./../components/buttons";
import ErrorPage from "../../components/ErrorPage"; import ErrorPage from "../../components/ErrorPage";
@@ -18,8 +19,9 @@ type Props = {
name: string, name: string,
userEntry?: UserEntry, userEntry?: UserEntry,
match: any, match: any,
deleteUser: (user: User) => void, deleteUser: (user: User, callback?: () => void) => void,
fetchUser: string => void fetchUser: string => void,
history: History
}; };
class SingleUser extends React.Component<Props> { class SingleUser extends React.Component<Props> {
@@ -27,6 +29,14 @@ class SingleUser extends React.Component<Props> {
this.props.fetchUser(this.props.name); this.props.fetchUser(this.props.name);
} }
userDeleted = () => {
this.props.history.push("/users");
};
deleteUser = (user: User) => {
this.props.deleteUser(user, this.userDeleted);
};
stripEndingSlash = (url: string) => { stripEndingSlash = (url: string) => {
if (url.endsWith("/")) { if (url.endsWith("/")) {
return url.substring(0, url.length - 2); return url.substring(0, url.length - 2);
@@ -34,8 +44,12 @@ class SingleUser extends React.Component<Props> {
return url; return url;
}; };
matchedUrl = () => {
return this.stripEndingSlash(this.props.match.url);
};
render() { render() {
const { userEntry, match, deleteUser } = this.props; const { userEntry } = this.props;
if (!userEntry || userEntry.loading) { if (!userEntry || userEntry.loading) {
return <Loading />; return <Loading />;
@@ -52,7 +66,7 @@ class SingleUser extends React.Component<Props> {
} }
const user = userEntry.entry; const user = userEntry.entry;
const url = this.stripEndingSlash(match.url); const url = this.matchedUrl();
// TODO i18n // TODO i18n
@@ -73,7 +87,7 @@ class SingleUser extends React.Component<Props> {
<NavLink to={`${url}/edit`} label="Edit" /> <NavLink to={`${url}/edit`} label="Edit" />
</Section> </Section>
<Section label="Actions"> <Section label="Actions">
<DeleteUserButton user={user} deleteUser={deleteUser} /> <DeleteUserButton user={user} deleteUser={this.deleteUser} />
<NavLink to="/users" label="Back" /> <NavLink to="/users" label="Back" />
</Section> </Section>
</Navigation> </Navigation>
@@ -102,8 +116,8 @@ const mapDispatchToProps = dispatch => {
fetchUser: (name: string) => { fetchUser: (name: string) => {
dispatch(fetchUser(name)); dispatch(fetchUser(name));
}, },
deleteUser: (user: User) => { deleteUser: (user: User, callback?: () => void) => {
dispatch(deleteUser(user)); dispatch(deleteUser(user, callback));
} }
}; };
}; };

View File

@@ -2,7 +2,7 @@
import {apiClient} from "../../apiclient"; import {apiClient} from "../../apiclient";
import type {User} from "../types/User"; import type {User} from "../types/User";
import type {UserEntry} from "../types/UserEntry"; import type {UserEntry} from "../types/UserEntry";
import { Dispatch, combineReducers } from "redux"; import {combineReducers, Dispatch} from "redux";
import type {Action} from "../../types/Action"; import type {Action} from "../../types/Action";
export const FETCH_USERS_PENDING = "scm/users/FETCH_USERS_PENDING"; export const FETCH_USERS_PENDING = "scm/users/FETCH_USERS_PENDING";
@@ -173,13 +173,16 @@ export function createUserFailure(user: User, err: Error): Action {
//modify user //modify user
export function modifyUser(user: User) { export function modifyUser(user: User, callback?: () => void) {
return function(dispatch: Dispatch) { return function(dispatch: Dispatch) {
dispatch(modifyUserPending(user)); dispatch(modifyUserPending(user));
return apiClient return apiClient
.putWithContentType(user._links.update.href, user, CONTENT_TYPE_USER) .putWithContentType(user._links.update.href, user, CONTENT_TYPE_USER)
.then(() => { .then(() => {
dispatch(modifyUserSuccess(user)); dispatch(modifyUserSuccess(user));
if (callback) {
callback();
}
}) })
.catch(err => { .catch(err => {
dispatch(modifyUserFailure(user, err)); dispatch(modifyUserFailure(user, err));
@@ -213,13 +216,16 @@ export function modifyUserFailure(user: User, error: Error): Action {
//delete user //delete user
export function deleteUser(user: User) { export function deleteUser(user: User, callback?: () => void) {
return function(dispatch: any) { return function(dispatch: any) {
dispatch(deleteUserPending(user)); dispatch(deleteUserPending(user));
return apiClient return apiClient
.delete(user._links.delete.href) .delete(user._links.delete.href)
.then(() => { .then(() => {
dispatch(deleteUserSuccess(user)); dispatch(deleteUserSuccess(user));
if (callback) {
callback();
}
}) })
.catch(cause => { .catch(cause => {
const error = new Error( const error = new Error(

View File

@@ -3,46 +3,44 @@ import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import fetchMock from "fetch-mock"; import fetchMock from "fetch-mock";
import { import reducer, {
FETCH_USERS_PENDING, CREATE_USER_FAILURE,
FETCH_USERS_SUCCESS,
fetchUsers,
FETCH_USERS_FAILURE,
createUserPending,
CREATE_USER_PENDING, CREATE_USER_PENDING,
CREATE_USER_SUCCESS, CREATE_USER_SUCCESS,
CREATE_USER_FAILURE, createUser,
modifyUser, createUserFailure,
MODIFY_USER_PENDING, createUserPending,
MODIFY_USER_FAILURE, createUserSuccess,
MODIFY_USER_SUCCESS, DELETE_USER_FAILURE,
deleteUserPending,
deleteUserFailure,
DELETE_USER_PENDING, DELETE_USER_PENDING,
DELETE_USER_SUCCESS, DELETE_USER_SUCCESS,
DELETE_USER_FAILURE,
deleteUser, deleteUser,
fetchUsersFailure, deleteUserFailure,
fetchUsersSuccess, deleteUserPending,
fetchUser, deleteUserSuccess,
FETCH_USER_FAILURE,
FETCH_USER_PENDING, FETCH_USER_PENDING,
FETCH_USER_SUCCESS, FETCH_USER_SUCCESS,
FETCH_USER_FAILURE, FETCH_USERS_FAILURE,
createUser, FETCH_USERS_PENDING,
createUserSuccess, FETCH_USERS_SUCCESS,
createUserFailure, fetchUser,
modifyUserPending, fetchUserFailure,
modifyUserSuccess,
modifyUserFailure,
fetchUserSuccess,
deleteUserSuccess,
fetchUsersPending,
fetchUserPending, fetchUserPending,
fetchUserFailure fetchUsers,
fetchUsersFailure,
fetchUsersPending,
fetchUsersSuccess,
fetchUserSuccess,
MODIFY_USER_FAILURE,
MODIFY_USER_PENDING,
MODIFY_USER_SUCCESS,
modifyUser,
modifyUserFailure,
modifyUserPending,
modifyUserSuccess
} from "./users"; } from "./users";
import reducer from "./users";
const userZaphod = { const userZaphod = {
active: true, active: true,
admin: true, admin: true,
@@ -236,16 +234,32 @@ describe("users fetch()", () => {
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
}); });
// after update, the users are fetched again
const store = mockStore({}); const store = mockStore({});
return store.dispatch(modifyUser(userZaphod)).then(() => { return store.dispatch(modifyUser(userZaphod)).then(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions.length).toBe(2);
expect(actions[0].type).toEqual(MODIFY_USER_PENDING); expect(actions[0].type).toEqual(MODIFY_USER_PENDING);
expect(actions[1].type).toEqual(MODIFY_USER_SUCCESS); 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", () => { it("should fail updating user on HTTP 500", () => {
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", {
status: 500 status: 500
@@ -264,18 +278,33 @@ 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(() => {
const actions = store.getActions(); const actions = store.getActions();
expect(actions.length).toBe(2);
expect(actions[0].type).toEqual(DELETE_USER_PENDING); expect(actions[0].type).toEqual(DELETE_USER_PENDING);
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);
}); });
}); });
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", () => { it("should fail to delete user zaphod", () => {
fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", { fetchMock.deleteOnce("http://localhost:8081/scm/api/rest/v2/users/zaphod", {
status: 500 status: 500