update users module to use new pending and error state

This commit is contained in:
Sebastian Sdorra
2018-07-30 13:38:15 +02:00
parent 9e029c0c5c
commit 7be1366c1a
14 changed files with 274 additions and 443 deletions

View File

@@ -35,7 +35,10 @@ class Page extends React.Component<Props> {
} }
renderContent() { renderContent() {
const { loading, children } = this.props; const { loading, children, error } = this.props;
if (error) {
return null;
}
if (loading) { if (loading) {
return <Loading />; return <Loading />;
} }

View File

@@ -34,7 +34,11 @@ export default function reducer(state: Object = {}, action: Action): Object {
} else { } else {
const matches = RESET_PATTERN.exec(type); const matches = RESET_PATTERN.exec(type);
if (matches) { if (matches) {
return removeFromState(state, matches[1]); let identifier = matches[1];
if (action.itemId) {
identifier += "/" + action.itemId;
}
return removeFromState(state, identifier);
} }
} }
return state; return state;

View File

@@ -69,6 +69,16 @@ describe("pending reducer", () => {
expect(newState["FETCH_USER/21"]).toBe(true); expect(newState["FETCH_USER/21"]).toBe(true);
expect(newState["FETCH_USER/42"]).toBe(false); expect(newState["FETCH_USER/42"]).toBe(false);
}); });
it("should reset pending for a single item", () => {
const newState = reducer(
{
"FETCH_USER/42": true
},
{ type: "FETCH_USER_SUCCESS", itemId: 42 }
);
expect(newState["FETCH_USER/42"]).toBeFalsy();
});
}); });
describe("pending selectors", () => { describe("pending selectors", () => {

View File

@@ -0,0 +1,4 @@
export const PENDING_SUFFIX = "PENDING";
export const SUCCESS_SUFFIX = "SUCCESS";
export const FAILURE_SUFFIX = "FAILURE";
export const RESET_SUFFIX = "RESET";

View File

@@ -10,9 +10,3 @@ export type PagedCollection = Collection & {
page: number, page: number,
pageTotal: number pageTotal: number
}; };
export type PageCollectionStateSlice = {
entry?: PagedCollection,
error?: Error,
loading?: boolean
};

View File

@@ -1,12 +1,12 @@
//@flow //@flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import type { UserEntry } from "../../types/UserEntry"; import type { User } from "../../types/User";
import { NavLink } from "../../../components/navigation"; import { NavLink } from "../../../components/navigation";
type Props = { type Props = {
t: string => string, t: string => string,
user: UserEntry, user: User,
editUrl: String editUrl: String
}; };

View File

@@ -2,16 +2,16 @@
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 { UserEntry } from "../../types/UserEntry"; import type { User } from "../../types/User";
type Props = { type Props = {
t: string => string, t: string => string,
entries: Array<UserEntry> users: User[]
}; };
class UserTable extends React.Component<Props> { class UserTable extends React.Component<Props> {
render() { render() {
const { entries, t } = this.props; const { users, t } = this.props;
return ( return (
<table className="table is-hoverable is-fullwidth"> <table className="table is-hoverable is-fullwidth">
<thead> <thead>
@@ -23,8 +23,8 @@ class UserTable extends React.Component<Props> {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{entries.map((entry, index) => { {users.map((user, index) => {
return <UserRow key={index} user={entry.entry} />; return <UserRow key={index} user={user} />;
})} })}
</tbody> </tbody>
</table> </table>

View File

@@ -4,17 +4,26 @@ import { connect } from "react-redux";
import UserForm from "./../components/UserForm"; import UserForm from "./../components/UserForm";
import type { User } from "../types/User"; import type { User } from "../types/User";
import type { History } from "history"; import type { History } from "history";
import { createUser, createUserReset } from "../modules/users"; import {
createUser,
createUserReset,
isCreateUserPending,
getCreateUserFailure
} from "../modules/users";
import { Page } from "../../components/layout"; import { Page } from "../../components/layout";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
type Props = { type Props = {
t: string => string,
addUser: (user: User, callback?: () => void) => void,
loading?: boolean, loading?: boolean,
error?: Error, error?: Error,
history: History,
resetForm: () => void // dispatcher functions
addUser: (user: User, callback?: () => void) => void,
resetForm: () => void,
// context objects
t: string => string,
history: History
}; };
class AddUser extends React.Component<Props> { class AddUser extends React.Component<Props> {
@@ -61,10 +70,12 @@ const mapDispatchToProps = dispatch => {
}; };
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
if (state.users && state.users.create) { const loading = isCreateUserPending(state);
return state.users.create; const error = getCreateUserFailure(state);
} return {
return {}; loading,
error
};
}; };
export default connect( export default connect(

View File

@@ -1,16 +1,26 @@
//@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 { 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 {
import type {History} from "history"; modifyUser,
isModifyUserPending,
getModifyUserFailure
} from "../modules/users";
import type { History } from "history";
import ErrorNotification from "../../components/ErrorNotification";
type Props = { type Props = {
user: User,
modifyUser: (user: User, callback?: () => void) => void,
loading: boolean, loading: boolean,
error: Error,
// dispatch functions
modifyUser: (user: User, callback?: () => void) => void,
// context objects
user: User,
history: History history: History
}; };
@@ -24,8 +34,17 @@ class EditUser extends React.Component<Props> {
}; };
render() { render() {
const { user } = this.props; const { user, loading, error } = this.props;
return <UserForm submitForm={user => this.modifyUser(user)} user={user} />; return (
<div>
<ErrorNotification error={error} />
<UserForm
submitForm={user => this.modifyUser(user)}
user={user}
loading={loading}
/>
</div>
);
} }
} }
@@ -38,7 +57,12 @@ const mapDispatchToProps = dispatch => {
}; };
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
return {}; const loading = isModifyUserPending(state, ownProps.user.name);
const error = getModifyUserFailure(state, ownProps.user.name);
return {
loading,
error
};
}; };
export default connect( export default connect(

View File

@@ -6,9 +6,16 @@ import { Route } from "react-router";
import { Details } from "./../components/table"; 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 { History } from "history"; import type { History } from "history";
import { fetchUser, deleteUser } from "../modules/users"; import {
fetchUser,
deleteUser,
getUserByName,
isFetchUserPending,
getFetchUserFailure,
isDeleteUserPending,
getDeleteUserFailure
} from "../modules/users";
import Loading from "../../components/Loading"; import Loading from "../../components/Loading";
import { Navigation, Section, NavLink } from "../../components/navigation"; import { Navigation, Section, NavLink } from "../../components/navigation";
@@ -16,14 +23,19 @@ import { DeleteUserNavLink, EditUserNavLink } from "./../components/navLinks";
import ErrorPage from "../../components/ErrorPage"; import ErrorPage from "../../components/ErrorPage";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
type Props = { type Props = {
t: string => string,
name: string, name: string,
userEntry?: UserEntry, user: User,
match: any, loading: boolean,
error: Error,
// dispatcher functions
deleteUser: (user: User, callback?: () => void) => void, deleteUser: (user: User, callback?: () => void) => void,
fetchUser: string => void, fetchUser: string => void,
// context objects
t: string => string,
match: any,
history: History history: History
}; };
@@ -52,23 +64,22 @@ class SingleUser extends React.Component<Props> {
}; };
render() { render() {
const { t, userEntry } = this.props; const { t, loading, error, user } = this.props;
if (!userEntry || userEntry.loading) { if (error) {
return <Loading />;
}
if (userEntry.error) {
return ( return (
<ErrorPage <ErrorPage
title={t("single-user.error-title")} title={t("single-user.error-title")}
subtitle={t("single-user.error-subtitle")} subtitle={t("single-user.error-subtitle")}
error={userEntry.error} error={error}
/> />
); );
} }
const user = userEntry.entry; if (!user || loading) {
return <Loading />;
}
const url = this.matchedUrl(); const url = this.matchedUrl();
return ( return (
@@ -84,7 +95,10 @@ class SingleUser extends React.Component<Props> {
<div className="column"> <div className="column">
<Navigation> <Navigation>
<Section label={t("single-user.navigation-label")}> <Section label={t("single-user.navigation-label")}>
<NavLink to={`${url}`} label={t("single-user.information-label")} /> <NavLink
to={`${url}`}
label={t("single-user.information-label")}
/>
<EditUserNavLink user={user} editUrl={`${url}/edit`} /> <EditUserNavLink user={user} editUrl={`${url}/edit`} />
</Section> </Section>
<Section label={t("single-user.actions-label")}> <Section label={t("single-user.actions-label")}>
@@ -101,14 +115,17 @@ class SingleUser extends React.Component<Props> {
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const name = ownProps.match.params.name; const name = ownProps.match.params.name;
let userEntry; const user = getUserByName(state, name);
if (state.users && state.users.byNames) { const loading =
userEntry = state.users.byNames[name]; isFetchUserPending(state, name) || isDeleteUserPending(state, name);
} const error =
getFetchUserFailure(state, name) || getDeleteUserFailure(state, name);
return { return {
name, name,
userEntry user,
loading,
error
}; };
}; };

View File

@@ -9,20 +9,24 @@ import {
fetchUsersByLink, fetchUsersByLink,
getUsersFromState, getUsersFromState,
selectListAsCollection, selectListAsCollection,
isPermittedToCreateUsers isPermittedToCreateUsers,
isFetchUsersPending,
getFetchUsersFailure
} from "../modules/users"; } from "../modules/users";
import { Page } from "../../components/layout"; import { Page } from "../../components/layout";
import { UserTable } from "./../components/table"; import { UserTable } from "./../components/table";
import type { UserEntry } from "../types/UserEntry"; import type { User } from "../types/User";
import type { PageCollectionStateSlice } from "../../types/Collection"; import type { PagedCollection } from "../../types/Collection";
import Paginator from "../../components/Paginator"; import Paginator from "../../components/Paginator";
import CreateUserButton from "../components/buttons/CreateUserButton"; import CreateUserButton from "../components/buttons/CreateUserButton";
type Props = { type Props = {
userEntries: UserEntry[], users: User[],
loading: boolean,
error: Error,
canAddUsers: boolean, canAddUsers: boolean,
list: PageCollectionStateSlice, list: PagedCollection,
page: number, page: number,
// context objects // context objects
@@ -48,9 +52,9 @@ class Users extends React.Component<Props> {
*/ */
componentDidUpdate = (prevProps: Props) => { componentDidUpdate = (prevProps: Props) => {
const { page, list } = this.props; const { page, list } = this.props;
if (list.entry) { if (list.page) {
// backend starts paging by 0 // backend starts paging by 0
const statePage: number = list.entry.page + 1; const statePage: number = list.page + 1;
if (page !== statePage) { if (page !== statePage) {
this.props.history.push(`/users/${statePage}`); this.props.history.push(`/users/${statePage}`);
} }
@@ -58,15 +62,15 @@ class Users extends React.Component<Props> {
}; };
render() { render() {
const { userEntries, list, t } = this.props; const { users, loading, error, t } = this.props;
return ( return (
<Page <Page
title={t("users.title")} title={t("users.title")}
subtitle={t("users.subtitle")} subtitle={t("users.subtitle")}
loading={list.loading || !userEntries} loading={loading || !users}
error={list.error} error={error}
> >
<UserTable entries={userEntries} /> <UserTable users={users} />
{this.renderPaginator()} {this.renderPaginator()}
{this.renderCreateButton()} {this.renderCreateButton()}
</Page> </Page>
@@ -75,10 +79,8 @@ class Users extends React.Component<Props> {
renderPaginator() { renderPaginator() {
const { list } = this.props; const { list } = this.props;
if (list.entry) { if (list) {
return ( return <Paginator collection={list} onPageChange={this.onPageChange} />;
<Paginator collection={list.entry} onPageChange={this.onPageChange} />
);
} }
return null; return null;
} }
@@ -103,12 +105,18 @@ const getPageFromProps = props => {
}; };
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const users = getUsersFromState(state);
const loading = isFetchUsersPending(state);
const error = getFetchUsersFailure(state);
const page = getPageFromProps(ownProps); const page = getPageFromProps(ownProps);
const userEntries = getUsersFromState(state);
const canAddUsers = isPermittedToCreateUsers(state); const canAddUsers = isPermittedToCreateUsers(state);
const list = selectListAsCollection(state); const list = selectListAsCollection(state);
return { return {
userEntries, users,
loading,
error,
canAddUsers, canAddUsers,
list, list,
page page

View File

@@ -1,39 +1,46 @@
// @flow // @flow
import { apiClient } from "../../apiclient"; import { apiClient } from "../../apiclient";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import * as types from "../../modules/types";
import type { User } from "../types/User"; import type { User } from "../types/User";
import type { UserEntry } from "../types/UserEntry";
import { combineReducers, Dispatch } from "redux"; import { combineReducers, Dispatch } from "redux";
import type { Action } from "../../types/Action"; import type { Action } from "../../types/Action";
import type { PageCollectionStateSlice } from "../../types/Collection"; import type { PagedCollection } from "../../types/Collection";
export const FETCH_USERS_PENDING = "scm/users/FETCH_USERS_PENDING"; export const FETCH_USERS = "scm/users/FETCH_USERS";
export const FETCH_USERS_SUCCESS = "scm/users/FETCH_USERS_SUCCESS"; export const FETCH_USERS_PENDING = `${FETCH_USERS}_${types.PENDING_SUFFIX}`;
export const FETCH_USERS_FAILURE = "scm/users/FETCH_USERS_FAILURE"; export const FETCH_USERS_SUCCESS = `${FETCH_USERS}_${types.SUCCESS_SUFFIX}`;
export const FETCH_USERS_FAILURE = `${FETCH_USERS}_${types.FAILURE_SUFFIX}`;
export const FETCH_USER_PENDING = "scm/users/FETCH_USER_PENDING"; export const FETCH_USER = "scm/users/FETCH_USER";
export const FETCH_USER_SUCCESS = "scm/users/FETCH_USER_SUCCESS"; export const FETCH_USER_PENDING = `${FETCH_USER}_${types.PENDING_SUFFIX}`;
export const FETCH_USER_FAILURE = "scm/users/FETCH_USER_FAILURE"; export const FETCH_USER_SUCCESS = `${FETCH_USER}_${types.SUCCESS_SUFFIX}`;
export const FETCH_USER_FAILURE = `${FETCH_USER}_${types.FAILURE_SUFFIX}`;
export const CREATE_USER_PENDING = "scm/users/CREATE_USER_PENDING"; export const CREATE_USER = "scm/users/CREATE_USER";
export const CREATE_USER_SUCCESS = "scm/users/CREATE_USER_SUCCESS"; export const CREATE_USER_PENDING = `${CREATE_USER}_${types.PENDING_SUFFIX}`;
export const CREATE_USER_FAILURE = "scm/users/CREATE_USER_FAILURE"; export const CREATE_USER_SUCCESS = `${CREATE_USER}_${types.SUCCESS_SUFFIX}`;
export const CREATE_USER_RESET = "scm/users/CREATE_USER_RESET"; export const CREATE_USER_FAILURE = `${CREATE_USER}_${types.FAILURE_SUFFIX}`;
export const CREATE_USER_RESET = `${CREATE_USER}_${types.RESET_SUFFIX}`;
export const MODIFY_USER_PENDING = "scm/users/MODIFY_USER_PENDING"; export const MODIFY_USER = "scm/users/MODIFY_USER";
export const MODIFY_USER_SUCCESS = "scm/users/MODIFY_USER_SUCCESS"; export const MODIFY_USER_PENDING = `${MODIFY_USER}_${types.PENDING_SUFFIX}`;
export const MODIFY_USER_FAILURE = "scm/users/MODIFY_USER_FAILURE"; export const MODIFY_USER_SUCCESS = `${MODIFY_USER}_${types.SUCCESS_SUFFIX}`;
export const MODIFY_USER_FAILURE = `${MODIFY_USER}_${types.FAILURE_SUFFIX}`;
export const DELETE_USER_PENDING = "scm/users/DELETE_PENDING"; export const DELETE_USER = "scm/users/DELETE";
export const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS"; export const DELETE_USER_PENDING = `${DELETE_USER}_${types.PENDING_SUFFIX}`;
export const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE"; export const DELETE_USER_SUCCESS = `${DELETE_USER}_${types.SUCCESS_SUFFIX}`;
export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
const USERS_URL = "users"; const USERS_URL = "users";
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2"; const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
//TODO i18n // TODO i18n for error messages
//fetch users // fetch users
export function fetchUsers() { export function fetchUsers() {
return fetchUsersByLink(USERS_URL); return fetchUsersByLink(USERS_URL);
@@ -111,14 +118,16 @@ export function fetchUser(name: string) {
export function fetchUserPending(name: string): Action { export function fetchUserPending(name: string): Action {
return { return {
type: FETCH_USER_PENDING, type: FETCH_USER_PENDING,
payload: name payload: name,
itemId: name
}; };
} }
export function fetchUserSuccess(user: any): Action { export function fetchUserSuccess(user: any): Action {
return { return {
type: FETCH_USER_SUCCESS, type: FETCH_USER_SUCCESS,
payload: user payload: user,
itemId: user.name
}; };
} }
@@ -128,7 +137,8 @@ export function fetchUserFailure(name: string, error: Error): Action {
payload: { payload: {
name, name,
error error
} },
itemId: name
}; };
} }
@@ -203,14 +213,16 @@ export function modifyUser(user: User, callback?: () => void) {
export function modifyUserPending(user: User): Action { export function modifyUserPending(user: User): Action {
return { return {
type: MODIFY_USER_PENDING, type: MODIFY_USER_PENDING,
payload: user payload: user,
itemId: user.name
}; };
} }
export function modifyUserSuccess(user: User): Action { export function modifyUserSuccess(user: User): Action {
return { return {
type: MODIFY_USER_SUCCESS, type: MODIFY_USER_SUCCESS,
payload: user payload: user,
itemId: user.name
}; };
} }
@@ -220,7 +232,8 @@ export function modifyUserFailure(user: User, error: Error): Action {
payload: { payload: {
error, error,
user user
} },
itemId: user.name
}; };
} }
@@ -249,14 +262,16 @@ export function deleteUser(user: User, callback?: () => void) {
export function deleteUserPending(user: User): Action { export function deleteUserPending(user: User): Action {
return { return {
type: DELETE_USER_PENDING, type: DELETE_USER_PENDING,
payload: user payload: user,
itemId: user.name
}; };
} }
export function deleteUserSuccess(user: User): Action { export function deleteUserSuccess(user: User): Action {
return { return {
type: DELETE_USER_SUCCESS, type: DELETE_USER_SUCCESS,
payload: user payload: user,
itemId: user.name
}; };
} }
@@ -266,40 +281,20 @@ export function deleteUserFailure(user: User, error: Error): Action {
payload: { payload: {
error, error,
user user
} },
itemId: user.name
}; };
} }
//helper functions
export function getUsersFromState(state: any) {
if (!state.users.list) {
return null;
}
const userNames = state.users.list.entries;
if (!userNames) {
return null;
}
const userEntries: Array<UserEntry> = [];
for (let userName of userNames) {
userEntries.push(state.users.byNames[userName]);
}
return userEntries;
}
function extractUsersByNames( function extractUsersByNames(
users: Array<User>, users: User[],
userNames: Array<string>, userNames: string[],
oldUsersByNames: {} oldUsersByNames: Object
) { ) {
const usersByNames = {}; const usersByNames = {};
for (let user of users) { for (let user of users) {
usersByNames[user.name] = { usersByNames[user.name] = user;
entry: user
};
} }
for (let userName in oldUsersByNames) { for (let userName in oldUsersByNames) {
@@ -335,20 +330,12 @@ const reducerByName = (state: any, username: string, newUserState: any) => {
function listReducer(state: any = {}, action: any = {}) { function listReducer(state: any = {}, action: any = {}) {
switch (action.type) { switch (action.type) {
// Fetch all users actions
case FETCH_USERS_PENDING:
return {
...state,
loading: true
};
case FETCH_USERS_SUCCESS: case FETCH_USERS_SUCCESS:
const users = action.payload._embedded.users; const users = action.payload._embedded.users;
const userNames = users.map(user => user.name); const userNames = users.map(user => user.name);
return { return {
...state, ...state,
error: null,
entries: userNames, entries: userNames,
loading: false,
entry: { entry: {
userCreatePermission: action.payload._links.create ? true : false, userCreatePermission: action.payload._links.create ? true : false,
page: action.payload.page, page: action.payload.page,
@@ -356,12 +343,7 @@ function listReducer(state: any = {}, action: any = {}) {
_links: action.payload._links _links: action.payload._links
} }
}; };
case FETCH_USERS_FAILURE:
return {
...state,
loading: false,
error: action.payload.error
};
// Delete single user actions // Delete single user actions
case DELETE_USER_SUCCESS: case DELETE_USER_SUCCESS:
const newUserEntries = deleteUserInEntries( const newUserEntries = deleteUserInEntries(
@@ -389,44 +371,12 @@ function byNamesReducer(state: any = {}, action: any = {}) {
}; };
// Fetch single user actions // Fetch single user actions
case FETCH_USER_PENDING:
return reducerByName(state, action.payload, {
loading: true,
error: null
});
case FETCH_USER_SUCCESS: case FETCH_USER_SUCCESS:
return reducerByName(state, action.payload.name, { return reducerByName(state, action.payload.name, action.payload);
loading: false,
error: null,
entry: action.payload
});
case FETCH_USER_FAILURE:
return reducerByName(state, action.payload.name, {
loading: false,
error: action.payload.error
});
// Update single user actions
case MODIFY_USER_PENDING:
return reducerByName(state, action.payload.name, {
loading: true
});
case MODIFY_USER_SUCCESS: case MODIFY_USER_SUCCESS:
return reducerByName(state, action.payload.name, { return reducerByName(state, action.payload.name, action.payload);
entry: action.payload
});
case MODIFY_USER_FAILURE:
return reducerByName(state, action.payload.user.name, {
error: action.payload.error
});
// Delete single user actions
case DELETE_USER_PENDING:
return reducerByName(state, action.payload.name, {
loading: true,
error: null,
entry: action.payload
});
case DELETE_USER_SUCCESS: case DELETE_USER_SUCCESS:
const newUserByNames = deleteUserInUsersByNames( const newUserByNames = deleteUserInUsersByNames(
state, state,
@@ -434,17 +384,16 @@ function byNamesReducer(state: any = {}, action: any = {}) {
); );
return newUserByNames; return newUserByNames;
case DELETE_USER_FAILURE:
return reducerByName(state, action.payload.user.name, {
loading: false,
error: action.payload.error,
entry: action.payload.user
});
default: default:
return state; return state;
} }
} }
export default combineReducers({
list: listReducer,
byNames: byNamesReducer
});
// selectors // selectors
const selectList = (state: Object) => { const selectList = (state: Object) => {
@@ -454,7 +403,7 @@ const selectList = (state: Object) => {
return {}; return {};
}; };
const selectListEntry = (state: Object) => { const selectListEntry = (state: Object): Object => {
const list = selectList(state); const list = selectList(state);
if (list.entry) { if (list.entry) {
return list.entry; return list.entry;
@@ -462,10 +411,8 @@ const selectListEntry = (state: Object) => {
return {}; return {};
}; };
export const selectListAsCollection = ( export const selectListAsCollection = (state: Object): PagedCollection => {
state: Object return selectListEntry(state);
): PageCollectionStateSlice => {
return selectList(state);
}; };
export const isPermittedToCreateUsers = (state: Object): boolean => { export const isPermittedToCreateUsers = (state: Object): boolean => {
@@ -475,29 +422,63 @@ export const isPermittedToCreateUsers = (state: Object): boolean => {
} }
return false; return false;
}; };
function createReducer(state: any = {}, action: any = {}) {
switch (action.type) { export function getUsersFromState(state: Object) {
case CREATE_USER_PENDING: const userNames = selectList(state).entries;
return { if (!userNames) {
loading: true return null;
}; }
case CREATE_USER_SUCCESS: const userEntries: User[] = [];
case CREATE_USER_RESET:
return { for (let userName of userNames) {
loading: false userEntries.push(state.users.byNames[userName]);
}; }
case CREATE_USER_FAILURE:
return { return userEntries;
loading: false, }
error: action.payload
}; export function isFetchUsersPending(state: Object) {
default: return isPending(state, FETCH_USERS);
return state; }
export function getFetchUsersFailure(state: Object) {
return getFailure(state, FETCH_USERS);
}
export function isCreateUserPending(state: Object) {
return isPending(state, CREATE_USER);
}
export function getCreateUserFailure(state: Object) {
return getFailure(state, CREATE_USER);
}
export function getUserByName(state: Object, name: string) {
if (state.users && state.users.byNames) {
return state.users.byNames[name];
} }
} }
export default combineReducers({ export function isFetchUserPending(state: Object, name: string) {
list: listReducer, return isPending(state, FETCH_USER, name);
byNames: byNamesReducer, }
create: createReducer
}); export function getFetchUserFailure(state: Object, name: string) {
return getFailure(state, FETCH_USER, name);
}
export function isModifyUserPending(state: Object, name: string) {
return isPending(state, MODIFY_USER, name);
}
export function getModifyUserFailure(state: Object, name: string) {
return getFailure(state, MODIFY_USER, name);
}
export function isDeleteUserPending(state: Object, name: string) {
return isPending(state, DELETE_USER, name);
}
export function getDeleteUserFailure(state: Object, name: string) {
return getFailure(state, DELETE_USER, name);
}

View File

@@ -8,16 +8,10 @@ import reducer, {
CREATE_USER_PENDING, CREATE_USER_PENDING,
CREATE_USER_SUCCESS, CREATE_USER_SUCCESS,
createUser, createUser,
createUserFailure,
createUserPending,
createUserSuccess,
createUserReset,
DELETE_USER_FAILURE, DELETE_USER_FAILURE,
DELETE_USER_PENDING, DELETE_USER_PENDING,
DELETE_USER_SUCCESS, DELETE_USER_SUCCESS,
deleteUser, deleteUser,
deleteUserFailure,
deleteUserPending,
deleteUserSuccess, deleteUserSuccess,
FETCH_USER_FAILURE, FETCH_USER_FAILURE,
FETCH_USER_PENDING, FETCH_USER_PENDING,
@@ -26,21 +20,15 @@ import reducer, {
FETCH_USERS_PENDING, FETCH_USERS_PENDING,
FETCH_USERS_SUCCESS, FETCH_USERS_SUCCESS,
fetchUser, fetchUser,
fetchUserFailure,
fetchUserPending,
fetchUsers,
fetchUsersFailure,
fetchUsersPending,
fetchUsersSuccess,
fetchUserSuccess, fetchUserSuccess,
fetchUsers,
fetchUsersSuccess,
selectListAsCollection, selectListAsCollection,
isPermittedToCreateUsers, isPermittedToCreateUsers,
MODIFY_USER_FAILURE, MODIFY_USER_FAILURE,
MODIFY_USER_PENDING, MODIFY_USER_PENDING,
MODIFY_USER_SUCCESS, MODIFY_USER_SUCCESS,
modifyUser, modifyUser,
modifyUserFailure,
modifyUserPending,
modifyUserSuccess modifyUserSuccess
} from "./users"; } from "./users";
@@ -325,19 +313,11 @@ describe("users fetch()", () => {
}); });
describe("users reducer", () => { describe("users reducer", () => {
it("should update state correctly according to FETCH_USERS_PENDING action", () => {
const newState = reducer({}, fetchUsersPending());
expect(newState.list.loading).toBeTruthy();
expect(newState.list.error).toBeFalsy();
});
it("should update state correctly according to FETCH_USERS_SUCCESS action", () => { it("should update state correctly according to FETCH_USERS_SUCCESS action", () => {
const newState = reducer({}, fetchUsersSuccess(responseBody)); const newState = reducer({}, fetchUsersSuccess(responseBody));
expect(newState.list).toEqual({ expect(newState.list).toEqual({
entries: ["zaphod", "ford"], entries: ["zaphod", "ford"],
error: null,
loading: false,
entry: { entry: {
userCreatePermission: true, userCreatePermission: true,
page: 0, page: 0,
@@ -347,31 +327,13 @@ describe("users reducer", () => {
}); });
expect(newState.byNames).toEqual({ expect(newState.byNames).toEqual({
zaphod: { zaphod: userZaphod,
entry: userZaphod ford: userFord
},
ford: {
entry: userFord
}
}); });
expect(newState.list.entry.userCreatePermission).toBeTruthy(); expect(newState.list.entry.userCreatePermission).toBeTruthy();
}); });
it("should set error when fetching users failed", () => {
const oldState = {
list: {
loading: true
}
};
const error = new Error("kaputt");
const newState = reducer(oldState, fetchUsersFailure("url.com", error));
expect(newState.list.loading).toBeFalsy();
expect(newState.list.error).toEqual(error);
});
it("should set userCreatePermission to true if update link is present", () => { it("should set userCreatePermission to true if update link is present", () => {
const newState = reducer({}, fetchUsersSuccess(responseBody)); const newState = reducer({}, fetchUsersSuccess(responseBody));
@@ -381,9 +343,7 @@ describe("users reducer", () => {
it("should not replace whole byNames map when fetching users", () => { it("should not replace whole byNames map when fetching users", () => {
const oldState = { const oldState = {
byNames: { byNames: {
ford: { ford: userFord
entry: userFord
}
} }
}; };
@@ -392,95 +352,14 @@ describe("users reducer", () => {
expect(newState.byNames["ford"]).toBeDefined(); expect(newState.byNames["ford"]).toBeDefined();
}); });
it("should update state correctly according to DELETE_USER_PENDING action", () => {
const state = {
byNames: {
zaphod: {
loading: false,
error: null,
entry: userZaphod
}
}
};
const newState = reducer(state, deleteUserPending(userZaphod));
const zaphod = newState.byNames["zaphod"];
expect(zaphod.loading).toBeTruthy();
expect(zaphod.entry).toBe(userZaphod);
});
it("should not effect other users if one user will be deleted", () => {
const state = {
byNames: {
zaphod: {
loading: false,
error: null,
entry: userZaphod
},
ford: {
loading: false
}
}
};
const newState = reducer(state, deleteUserPending(userZaphod));
const ford = newState.byNames["ford"];
expect(ford.loading).toBeFalsy();
});
it("should set the error of user which could not be deleted", () => {
const state = {
byNames: {
zaphod: {
loading: true,
entry: userZaphod
}
}
};
const error = new Error("error");
const newState = reducer(state, deleteUserFailure(userZaphod, error));
const zaphod = newState.byNames["zaphod"];
expect(zaphod.loading).toBeFalsy();
expect(zaphod.error).toBe(error);
});
it("should not effect other users if one user could not be deleted", () => {
const state = {
byNames: {
zaphod: {
loading: false,
error: null,
entry: userZaphod
},
ford: {
loading: false
}
}
};
const error = new Error("error");
const newState = reducer(state, deleteUserFailure(userZaphod, error));
const ford = newState.byNames["ford"];
expect(ford.loading).toBeFalsy();
});
it("should remove user from state when delete succeeds", () => { it("should remove user from state when delete succeeds", () => {
const state = { const state = {
list: { list: {
entries: ["ford", "zaphod"] entries: ["ford", "zaphod"]
}, },
byNames: { byNames: {
zaphod: { zaphod: userZaphod,
loading: true, ford: userFord
error: null,
entry: userZaphod
},
ford: {
loading: true,
error: null,
entry: userFord
}
} }
}; };
@@ -499,68 +378,9 @@ describe("users reducer", () => {
expect(newState.byNames["zaphod"]).toBeTruthy(); expect(newState.byNames["zaphod"]).toBeTruthy();
}); });
it("should update state correctly according to CREATE_USER_PENDING action", () => {
const newState = reducer({}, createUserPending(userZaphod));
expect(newState.create.loading).toBeTruthy();
expect(newState.create.error).toBeFalsy();
});
it("should update state correctly according to CREATE_USER_SUCCESS action", () => {
const newState = reducer(
{ create: { loading: true } },
createUserSuccess()
);
expect(newState.create.loading).toBeFalsy();
expect(newState.create.error).toBeFalsy();
});
it("should set the loading to false and the error if user could not be created", () => {
const newState = reducer(
{ create: { loading: true, error: null } },
createUserFailure(new Error("kaputt kaputt"))
);
expect(newState.create.loading).toBeFalsy();
expect(newState.create.error).toEqual(new Error("kaputt kaputt"));
});
it("should reset the user create form", () => {
const newState = reducer(
{ create: { loading: true, error: new Error("kaputt kaputt") } },
createUserReset()
);
expect(newState.create.loading).toBeFalsy();
expect(newState.create.error).toBeFalsy();
});
it("should update state according to FETCH_USER_PENDING action", () => {
const newState = reducer({}, fetchUserPending("zaphod"));
expect(newState.byNames["zaphod"].loading).toBeTruthy();
});
it("should not affect list state", () => {
const newState = reducer(
{
list: {
entries: ["ford"]
}
},
fetchUserPending("zaphod")
);
expect(newState.byNames["zaphod"].loading).toBeTruthy();
expect(newState.list.entries).toEqual(["ford"]);
});
it("should update state according to FETCH_USER_FAILURE action", () => {
const error = new Error("kaputt!");
const newState = reducer({}, fetchUserFailure(userFord.name, error));
expect(newState.byNames["ford"].error).toBe(error);
expect(newState.byNames["ford"].loading).toBeFalsy();
});
it("should update state according to FETCH_USER_SUCCESS action", () => { it("should update state according to FETCH_USER_SUCCESS action", () => {
const newState = reducer({}, fetchUserSuccess(userFord)); const newState = reducer({}, fetchUserSuccess(userFord));
expect(newState.byNames["ford"].loading).toBeFalsy(); expect(newState.byNames["ford"]).toBe(userFord);
expect(newState.byNames["ford"].entry).toBe(userFord);
}); });
it("should affect users state nor the state of other users", () => { it("should affect users state nor the state of other users", () => {
@@ -572,62 +392,22 @@ describe("users reducer", () => {
}, },
fetchUserSuccess(userFord) fetchUserSuccess(userFord)
); );
expect(newState.byNames["ford"].loading).toBeFalsy(); expect(newState.byNames["ford"]).toBe(userFord);
expect(newState.byNames["ford"].entry).toBe(userFord);
expect(newState.list.entries).toEqual(["zaphod"]); expect(newState.list.entries).toEqual(["zaphod"]);
}); });
it("should update state according to MODIFY_USER_PENDING action", () => {
const newState = reducer(
{
byNames: {
ford: {
error: new Error("something"),
entry: {}
}
}
},
modifyUserPending(userFord)
);
expect(newState.byNames["ford"].loading).toBeTruthy();
expect(newState.byNames["ford"].error).toBeFalsy();
expect(newState.byNames["ford"].entry).toBeFalsy();
});
it("should update state according to MODIFY_USER_SUCCESS action", () => { it("should update state according to MODIFY_USER_SUCCESS action", () => {
const newState = reducer( const newState = reducer(
{ {
byNames: { byNames: {
ford: { ford: {
loading: true, name: "ford"
error: new Error("something"),
entry: {}
} }
} }
}, },
modifyUserSuccess(userFord) modifyUserSuccess(userFord)
); );
expect(newState.byNames["ford"].loading).toBeFalsy(); expect(newState.byNames["ford"]).toBe(userFord);
expect(newState.byNames["ford"].error).toBeFalsy();
expect(newState.byNames["ford"].entry).toBe(userFord);
});
it("should update state according to MODIFY_USER_SUCCESS action", () => {
const error = new Error("something went wrong");
const newState = reducer(
{
byNames: {
ford: {
loading: true,
entry: {}
}
}
},
modifyUserFailure(userFord, error)
);
expect(newState.byNames["ford"].loading).toBeFalsy();
expect(newState.byNames["ford"].error).toBe(error);
expect(newState.byNames["ford"].entry).toBeFalsy();
}); });
}); });

View File

@@ -1,5 +0,0 @@
export type UserEntry = {
loading: boolean,
error: Error,
entry: User
};