restructure ui for users

This commit is contained in:
Sebastian Sdorra
2018-07-25 13:21:49 +02:00
parent 978565609a
commit fe0b7ea986
19 changed files with 426 additions and 201 deletions

View File

@@ -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() {

View 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;

View 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;

View 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;

View 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;

View 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";

View File

@@ -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>

View File

@@ -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));
} }
}; };
}; };

View File

@@ -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}
/>
);
} }
} }

View File

@@ -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");
}); });

View 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);

View File

@@ -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(

View 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);

View File

@@ -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 />;

View File

@@ -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>
); );
} }

View File

@@ -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>

View File

@@ -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));
} }
}; };
}; };

View File

@@ -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 {

View File

@@ -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", () => {