Added tests, fixed edit/add users

This commit is contained in:
Philipp Czora
2018-07-19 12:05:50 +02:00
parent 3c0ea782aa
commit f4c1403f71
11 changed files with 168 additions and 109 deletions

View File

@@ -0,0 +1,11 @@
//@flow
import React from "react";
import Button, { type ButtonProps } from "./Button";
class AddButton extends React.Component<ButtonProps> {
render() {
return <Button type="default" {...this.props} />;
}
}
export default AddButton;

View File

@@ -1,12 +1,15 @@
//@flow
import React from "react";
import classNames from "classnames";
import { Link } from "react-router-dom";
export type ButtonProps = {
label: string,
loading?: boolean,
disabled?: boolean,
action: () => void
action?: () => void,
link?: string,
fullWidth?: boolean
};
type Props = ButtonProps & {
@@ -14,18 +17,33 @@ type Props = ButtonProps & {
};
class Button extends React.Component<Props> {
render() {
const { label, loading, disabled, type, action } = this.props;
renderButton = () => {
const { label, loading, disabled, type, action, fullWidth } = this.props;
const loadingClass = loading ? "is-loading" : "";
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
return (
<button
disabled={disabled}
onClick={action}
className={classNames("button", "is-" + type, loadingClass)}
onClick={action ? action : () => {}}
className={classNames(
"button",
"is-" + type,
loadingClass,
fullWidthClass
)}
>
{label}
</button>
);
};
render() {
const { link } = this.props;
if (link) {
return <Link to={link}>{this.renderButton()}</Link>;
} else {
return this.renderButton();
}
}
}

View File

@@ -1,41 +1,10 @@
//@flow
import React from "react";
import classNames from "classnames";
import Button, { type ButtonProps } from "./Button";
type Props = {
value: string,
disabled?: boolean,
isLoading?: boolean,
large?: boolean,
fullWidth?: boolean
};
class SubmitButton extends React.Component<Props> {
class SubmitButton extends React.Component<ButtonProps> {
render() {
const { value, large, fullWidth, isLoading, disabled } = this.props;
const largeClass = large ? "is-large" : "";
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
const loadingClass = isLoading ? "is-loading" : "";
return (
<div className="field">
<div className="control">
<button
disabled={disabled}
className={classNames(
"button",
"is-link",
largeClass,
fullWidthClass,
loadingClass
)}
>
{value}
</button>
</div>
</div>
);
return <Button type="primary" {...this.props} />;
}
}

View File

@@ -117,10 +117,10 @@ class Login extends React.Component<Props, State> {
onChange={this.handlePasswordChange}
/>
<SubmitButton
value="Login"
label="Login"
disabled={this.isInValid()}
fullWidth={true}
isLoading={loading}
loading={loading}
/>
</form>
</div>

View File

@@ -5,10 +5,10 @@ import UserForm from "./UserForm";
import type { User } from "../types/User";
import { addUser } from "../modules/users";
import { Route, Link } from "react-router-dom";
type Props = {
addUser: User => void
addUser: User => void,
loading?: boolean
};
class AddUser extends React.Component<Props> {
@@ -17,7 +17,10 @@ class AddUser extends React.Component<Props> {
return (
<div>
<UserForm submitForm={user => addUser(user)} />
<UserForm
submitForm={user => addUser(user)}
loading={this.props.loading}
/>
</div>
);
}
@@ -32,6 +35,11 @@ const mapDispatchToProps = dispatch => {
};
const mapStateToProps = (state, ownProps) => {
if (state.users && state.users.users) {
return {
loading: state.users.users.loading
};
}
return {};
};

View File

@@ -3,21 +3,16 @@ import React from "react";
import { connect } from "react-redux";
import UserForm from "./UserForm";
import type { User } from "../types/User";
import Loading from "../../components/Loading";
import {
updateUser,
deleteUser,
editUser,
fetchUser,
getUsersFromState
} from "../modules/users";
import { Route, Link } from "react-router-dom";
import { updateUser, fetchUser } from "../modules/users";
type Props = {
name: string,
fetchUser: string => void,
usersByNames: Map<string, any>,
updateUser: User => void
userEntry?: UserEntry,
updateUser: User => void,
loading: boolean
};
class EditUser extends React.Component<Props> {
@@ -28,15 +23,18 @@ class EditUser extends React.Component<Props> {
render() {
const submitUser = this.props.updateUser;
const { usersByNames, name } = this.props;
const { userEntry } = this.props;
if (!usersByNames || usersByNames[name].loading) {
return <div>Loading...</div>;
if (!userEntry || userEntry.loading) {
return <Loading />;
} else {
const user = usersByNames[name].entry;
return (
<div>
<UserForm submitForm={user => submitUser(user)} user={user} />
<UserForm
submitForm={user => submitUser(user)}
user={userEntry.entry}
loading={userEntry.loading}
/>
</div>
);
}
@@ -55,9 +53,15 @@ const mapDispatchToProps = dispatch => {
};
const mapStateToProps = (state, ownProps) => {
const name = ownProps.match.params.name;
let userEntry;
if (state.users && state.users.usersByNames) {
userEntry = state.users.usersByNames[name];
}
return {
usersByNames: state.users.usersByNames,
name: ownProps.match.params.name
name,
userEntry
};
};

View File

@@ -1,7 +1,6 @@
//@flow
import React from "react";
import EditButton from "../../components/EditButton";
import type { User } from "../types/User";
import type { UserEntry } from "../types/UserEntry";
import { Link } from "react-router-dom";
@@ -17,11 +16,7 @@ class EditUserButton extends React.Component<Props> {
if (!this.isEditable()) {
return "";
}
return (
<Link to={link}>
<EditButton label="Edit" action={() => {}} loading={entry.loading} />
</Link>
);
return <EditButton label="Edit" link={link} loading={entry.loading} />;
}
isEditable = () => {

View File

@@ -4,10 +4,12 @@ import type { User } from "../types/User";
import InputField from "../../components/InputField";
import Checkbox from "../../components/Checkbox";
import SubmitButton from "../../components/SubmitButton";
import { connect } from "react-redux";
type Props = {
submitForm: User => void,
user?: User
user?: User,
loading?: boolean
};
class UserForm extends React.Component<Props, User> {
@@ -69,7 +71,7 @@ class UserForm extends React.Component<Props, User> {
onChange={this.handleActiveChange}
checked={user ? user.active : false}
/>
<SubmitButton value="Submit" />
<SubmitButton label="Submit" loading={this.props.loading} />
</form>
</div>
);

View File

@@ -7,6 +7,7 @@ import Loading from "../../components/Loading";
import ErrorNotification from "../../components/ErrorNotification";
import UserTable from "./UserTable";
import type { User } from "../types/User";
import AddButton from "../../components/AddButton";
import type { UserEntry } from "../types/UserEntry";
type Props = {
@@ -14,7 +15,8 @@ type Props = {
error: Error,
userEntries: Array<UserEntry>,
fetchUsers: () => void,
deleteUser: User => void
deleteUser: User => void,
canAddUsers: boolean
};
class Users extends React.Component<Props, User> {
@@ -29,6 +31,7 @@ class Users extends React.Component<Props, User> {
<h1 className="title">SCM</h1>
<h2 className="subtitle">Users</h2>
{this.renderContent()}
{this.renderAddButton()}
</div>
</section>
);
@@ -47,13 +50,26 @@ class Users extends React.Component<Props, User> {
return <Loading />;
}
}
renderAddButton() {
if (this.props.canAddUsers) {
return (
<div>
<AddButton label="Add User" link="/users/add" />
</div>
);
} else {
return;
}
}
}
const mapStateToProps = state => {
const userEntries = getUsersFromState(state);
return {
userEntries,
error: state.users.error
error: state.users.error,
canAddUsers: state.users.userCreatePermission
};
};

View File

@@ -16,8 +16,6 @@ export const ADD_USER = "scm/users/ADD";
export const ADD_USER_SUCCESS = "scm/users/ADD_SUCCESS";
export const ADD_USER_FAILURE = "scm/users/ADD_FAILURE";
export const EDIT_USER = "scm/users/EDIT";
export const UPDATE_USER = "scm/users/UPDATE";
export const UPDATE_USER_SUCCESS = "scm/users/UPDATE_SUCCESS";
export const UPDATE_USER_FAILURE = "scm/users/UPDATE_FAILURE";
@@ -31,13 +29,13 @@ const USER_URL = "users/";
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
function requestUsers() {
export function requestUsers() {
return {
type: FETCH_USERS
};
}
function failedToFetchUsers(url: string, err: Error) {
export function failedToFetchUsers(url: string, err: Error) {
return {
type: FETCH_USERS_FAILURE,
payload: err,
@@ -78,14 +76,14 @@ export function fetchUsers() {
};
}
function fetchUsersSuccess(users: any) {
export function fetchUsersSuccess(users: any) {
return {
type: FETCH_USERS_SUCCESS,
payload: users
};
}
function requestUser(name: string) {
export function requestUser(name: string) {
return {
type: FETCH_USER,
payload: { name }
@@ -126,7 +124,7 @@ function fetchUserSuccess(user: User) {
};
}
function requestAddUser(user: User) {
export function requestAddUser(user: User) {
return {
type: ADD_USER,
user
@@ -142,17 +140,24 @@ export function addUser(user: User) {
dispatch(addUserSuccess());
dispatch(fetchUsers());
})
.catch(err => dispatch(addUserFailure(user, err)));
.catch(err =>
dispatch(
addUserFailure(
user,
new Error(`failed to add user ${user.name}: ${err.message}`)
)
)
);
};
}
function addUserSuccess() {
export function addUserSuccess() {
return {
type: ADD_USER_SUCCESS
};
}
function addUserFailure(user: User, err: Error) {
export function addUserFailure(user: User, err: Error) {
return {
type: ADD_USER_FAILURE,
payload: err,
@@ -267,13 +272,6 @@ function extractUsersByNames(
return usersByNames;
}
export function editUser(user: User) {
return {
type: EDIT_USER,
user
};
}
const reduceUsersByNames = (
state: any,
username: string,
@@ -320,6 +318,7 @@ export default function reducer(state: any = {}, action: any = {}) {
return {
...state,
users: {
userCreatePermission: action.payload._links.create ? true : false,
error: null,
entries: userNames,
loading: false
@@ -349,10 +348,33 @@ export default function reducer(state: any = {}, action: any = {}) {
error: action.payload.error,
entry: action.payload.user
});
case EDIT_USER:
case ADD_USER:
return {
...state,
editUser: action.user
users: {
...state.users,
loading: true,
error: null
}
};
case ADD_USER_SUCCESS:
return {
...state,
users: {
...state.users,
loading: false,
error: null
}
};
case ADD_USER_FAILURE:
return {
...state,
users: {
...state.users,
loading: false,
error: action.payload
}
};
default:
return state;

View File

@@ -16,13 +16,17 @@ import {
UPDATE_USER,
UPDATE_USER_FAILURE,
UPDATE_USER_SUCCESS,
EDIT_USER,
requestDeleteUser,
deleteUserFailure,
DELETE_USER,
DELETE_USER_SUCCESS,
DELETE_USER_FAILURE,
deleteUser
deleteUser,
requestUsers,
fetchUsersSuccess,
requestAddUser,
addUserSuccess,
addUserFailure
} from "./users";
import reducer from "./users";
@@ -258,16 +262,13 @@ describe("users fetch()", () => {
describe("users reducer", () => {
test("should update state correctly according to FETCH_USERS action", () => {
const newState = reducer({}, { type: FETCH_USERS });
const newState = reducer({}, requestUsers());
expect(newState.loading).toBeTruthy();
expect(newState.error).toBeNull();
});
it("should update state correctly according to FETCH_USERS_SUCCESS action", () => {
const newState = reducer(
{},
{ type: FETCH_USERS_SUCCESS, payload: responseBody }
);
const newState = reducer({}, fetchUsersSuccess(responseBody));
expect(newState.users).toEqual({
entries: ["zaphod", "ford"],
@@ -285,7 +286,7 @@ describe("users reducer", () => {
});
});
test("should update state correctly according to DELETE_USER action", () => {
it("should update state correctly according to DELETE_USER action", () => {
const state = {
usersByNames: {
zaphod: {
@@ -367,22 +368,35 @@ describe("users reducer", () => {
}
};
const newState = reducer(oldState, {
type: FETCH_USERS_SUCCESS,
payload: responseBodyZaphod
});
const newState = reducer(oldState, fetchUsersSuccess(responseBody));
expect(newState.usersByNames["zaphod"]).toBeDefined();
expect(newState.usersByNames["ford"]).toBeDefined();
});
it("should update state correctly according to EDIT_USER action", () => {
it("should set userCreatePermission to true if update link is present", () => {
const newState = reducer({}, fetchUsersSuccess(responseBody));
expect(newState.users.userCreatePermission).toBeTruthy();
});
it("should update state correctly according to ADD_USER action", () => {
const newState = reducer({}, requestAddUser(userZaphod));
expect(newState.users.loading).toBeTruthy();
expect(newState.users.error).toBeNull();
});
it("should update state correctly according to ADD_USER_SUCCESS action", () => {
const newState = reducer({ loading: true }, addUserSuccess());
expect(newState.users.loading).toBeFalsy();
expect(newState.users.error).toBeNull();
});
it("should set the loading to false and the error if user could not be added", () => {
const newState = reducer(
{},
{
type: EDIT_USER,
user: userZaphod
}
{ loading: true, error: null },
addUserFailure(userFord, new Error("kaputt kaputt"))
);
expect(newState.editUser).toEqual(userZaphod);
expect(newState.users.loading).toBeFalsy();
expect(newState.users.error).toEqual(new Error("kaputt kaputt"));
});
});