diff --git a/scm-ui/src/users/containers/UserRow.js b/scm-ui/src/users/containers/UserRow.js index 2f2dba001b..40b0628031 100644 --- a/scm-ui/src/users/containers/UserRow.js +++ b/scm-ui/src/users/containers/UserRow.js @@ -4,13 +4,14 @@ import DeleteUserButton from "./DeleteUserButton"; import type { User } from "../types/User"; type Props = { - user: User, + entry: { loading: boolean, error: Error, user: User }, deleteUser: string => void }; export default class UserRow extends React.Component { render() { - const { user, deleteUser } = this.props; + const { deleteUser } = this.props; + const user = this.props.entry.entry; return ( {user.name} diff --git a/scm-ui/src/users/containers/UserTable.js b/scm-ui/src/users/containers/UserTable.js index 338e0663c9..9379945342 100644 --- a/scm-ui/src/users/containers/UserTable.js +++ b/scm-ui/src/users/containers/UserTable.js @@ -4,13 +4,14 @@ import UserRow from "./UserRow"; import type { User } from "../types/User"; type Props = { - users: Array, + entries: [{ loading: boolean, error: Error, user: User }], deleteUser: string => void }; class UserTable extends React.Component { render() { - const { users, deleteUser } = this.props; + const { deleteUser } = this.props; + const entries = this.props.entries; return ( @@ -22,8 +23,10 @@ class UserTable extends React.Component { - {users.map((user, index) => { - return ; + {entries.map((entry, index) => { + return ( + + ); })}
diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index e431c2a43f..04b983d750 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -2,15 +2,27 @@ import React from "react"; import { connect } from "react-redux"; -import { fetchUsers, addUser, editUser, deleteUser } from "../modules/users"; +import { + fetchUsers, + addUser, + editUser, + deleteUser, + getUsersFromState +} from "../modules/users"; import UserForm from "./UserForm"; import UserTable from "./UserTable"; import type { User } from "../types/User"; +type UserEntry = { + loading: boolean, + error: Error, + user: User +}; + type Props = { login: boolean, error: Error, - users: Array, + userEntries: Array, fetchUsers: () => void, deleteUser: string => void, addUser: User => void, @@ -31,7 +43,7 @@ class Users extends React.Component { }; render() { - const { users, deleteUser } = this.props; + const { userEntries, deleteUser } = this.props; const testUser: User = { name: "user", displayName: "user_display", @@ -40,13 +52,13 @@ class Users extends React.Component { active: true, admin: true }; - if (users) { + if (userEntries) { return (

SCM

Users

- + {/* */} {}} user={testUser} />
@@ -59,8 +71,12 @@ class Users extends React.Component { } const mapStateToProps = state => { + const userEntries = getUsersFromState(state); + if (!userEntries) { + return {}; + } return { - users: state.users.users + userEntries }; }; diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index cba960b484..96e251a36b 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -3,22 +3,22 @@ import { apiClient, PAGE_NOT_FOUND_ERROR } from "../../apiclient"; import type { User } from "../types/User"; import { ThunkDispatch } from "redux-thunk"; -const FETCH_USERS = "scm/users/FETCH"; -const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS"; -const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE"; -const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND"; +export const FETCH_USERS = "scm/users/FETCH"; +export const FETCH_USERS_SUCCESS = "scm/users/FETCH_SUCCESS"; +export const FETCH_USERS_FAILURE = "scm/users/FETCH_FAILURE"; +export const FETCH_USERS_NOTFOUND = "scm/users/FETCH_NOTFOUND"; -const ADD_USER = "scm/users/ADD"; -const ADD_USER_SUCCESS = "scm/users/ADD_SUCCESS"; -const ADD_USER_FAILURE = "scm/users/ADD_FAILURE"; +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"; -const EDIT_USER = "scm/users/EDIT"; -const EDIT_USER_SUCCESS = "scm/users/EDIT_SUCCESS"; -const EDIT_USER_FAILURE = "scm/users/EDIT_FAILURE"; +export const EDIT_USER = "scm/users/EDIT"; +export const EDIT_USER_SUCCESS = "scm/users/EDIT_SUCCESS"; +export const EDIT_USER_FAILURE = "scm/users/EDIT_FAILURE"; -const DELETE_USER = "scm/users/DELETE"; -const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS"; -const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE"; +export const DELETE_USER = "scm/users/DELETE"; +export const DELETE_USER_SUCCESS = "scm/users/DELETE_SUCCESS"; +export const DELETE_USER_FAILURE = "scm/users/DELETE_FAILURE"; const USERS_URL = "users"; @@ -45,7 +45,7 @@ function usersNotFound(url: string) { } export function fetchUsers() { - return function(dispatch: ThunkDispatch) { + return function(dispatch: any) { dispatch(requestUsers()); return apiClient .get(USERS_URL) @@ -179,21 +179,58 @@ export function deleteUser(link: string) { }; } +export function getUsersFromState(state) { + if (!state.users.users) { + return null; + } + const userNames = state.users.users.entries; + if (!userNames) { + return null; + } + var userEntries = new Array(); + + for (let userName of userNames) { + userEntries.push(state.users.usersByNames[userName]); + } + + return userEntries; +} + +function extractUsersByNames(users: Array, userNames: Array) { + var usersByNames = {}; + + for (let user of users) { + usersByNames[user.name] = { + entry: user + }; + } + return usersByNames; +} + export default function reducer(state: any = {}, action: any = {}) { switch (action.type) { case FETCH_USERS: + return { + loading: true, + error: null + }; case DELETE_USER: return { ...state, - users: null, - loading: true + users: null }; case FETCH_USERS_SUCCESS: + const users = action.payload._embedded.users; + const userNames = users.map(user => user.name); + const usersByNames = {...state.usersByNames, extractUsersByNames(users, userNames)}; return { ...state, - error: null, - users: action.payload._embedded.users, - loading: false + users: { + error: null, + entries: userNames, + loading: false + }, + usersByNames }; case FETCH_USERS_FAILURE: case DELETE_USER_FAILURE: diff --git a/scm-ui/src/users/modules/users.test.js b/scm-ui/src/users/modules/users.test.js new file mode 100644 index 0000000000..4f4073e7a9 --- /dev/null +++ b/scm-ui/src/users/modules/users.test.js @@ -0,0 +1,202 @@ +//@flow +import React from "react"; +import { configure, shallow } from "enzyme"; +import Adapter from "enzyme-adapter-react-16"; +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import fetchMock from "fetch-mock"; + +import { + FETCH_USERS, + FETCH_USERS_SUCCESS, + fetchUsers, + FETCH_USERS_FAILURE +} from "./users"; + +import reducer from "./users"; + +import "raf/polyfill"; + +configure({ adapter: new Adapter() }); + +const userZaphod = { + active: true, + admin: true, + creationDate: "2018-07-11T12:23:49.027Z", + displayName: "Z. Beeblebrox", + mail: "president@heartofgold.universe", + name: "zaphod", + password: "__dummypassword__", + type: "xml", + properties: {}, + _links: { + self: { + href: "http://localhost:8081/scm/api/rest/v2/users/zaphod" + }, + delete: { + href: "http://localhost:8081/scm/api/rest/v2/users/zaphod" + }, + update: { + href: "http://localhost:8081/scm/api/rest/v2/users/zaphod" + } + } +}; + +const userFord = { + active: true, + admin: false, + creationDate: "2018-07-06T13:21:18.459Z", + displayName: "F. Prefect", + mail: "ford@prefect.universe", + name: "ford", + password: "__dummypassword__", + type: "xml", + properties: {}, + _links: { + self: { + href: "http://localhost:8081/scm/api/rest/v2/users/ford" + }, + delete: { + href: "http://localhost:8081/scm/api/rest/v2/users/ford" + }, + update: { + href: "http://localhost:8081/scm/api/rest/v2/users/ford" + } + } +}; + +const responseBodyZaphod = { + page: 0, + pageTotal: 1, + _links: { + self: { + href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10" + }, + first: { + href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10" + }, + last: { + href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10" + }, + create: { + href: "http://localhost:3000/scm/api/rest/v2/users/" + } + }, + _embedded: { + users: [userZaphod] + } +}; + +const responseBody = { + page: 0, + pageTotal: 1, + _links: { + self: { + href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10" + }, + first: { + href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10" + }, + last: { + href: "http://localhost:3000/scm/api/rest/v2/users/?page=0&pageSize=10" + }, + create: { + href: "http://localhost:3000/scm/api/rest/v2/users/" + } + }, + _embedded: { + users: [userZaphod, userFord] + } +}; + +const response = { + headers: { "content-type": "application/json" }, + responseBody +}; + +describe("fetch tests", () => { + const mockStore = configureMockStore([thunk]); + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + test("successful users fetch", () => { + fetchMock.getOnce("/scm/api/rest/v2/users", response); + + const expectedActions = [ + { type: FETCH_USERS }, + { + type: FETCH_USERS_SUCCESS, + payload: response + } + ]; + + const store = mockStore({}); + + return store.dispatch(fetchUsers()).then(() => { + expect(store.getActions()).toEqual(expectedActions); + }); + }); + + test("me fetch failed", () => { + fetchMock.getOnce("/scm/api/rest/v2/users", { + status: 500 + }); + + const store = mockStore({}); + return store.dispatch(fetchUsers()).then(() => { + const actions = store.getActions(); + expect(actions[0].type).toEqual(FETCH_USERS); + expect(actions[1].type).toEqual(FETCH_USERS_FAILURE); + expect(actions[1].payload).toBeDefined(); + }); + }); +}); + +describe("reducer tests", () => { + test("users request", () => { + var newState = reducer({}, { type: FETCH_USERS }); + expect(newState.loading).toBeTruthy(); + expect(newState.error).toBeNull(); + }); + + test("fetch users successful", () => { + var newState = reducer( + {}, + { type: FETCH_USERS_SUCCESS, payload: responseBody } + ); + + expect(newState.users).toEqual({ + entries: ["zaphod", "ford"], + error: null, + loading: false + }); + + expect(newState.usersByNames).toEqual({ + zaphod: { + entry: userZaphod + }, + ford: { + entry: userFord + } + }); + }); + + test("reducer does not replace whole usersByNames map", () => { + const oldState = { + usersByNames: { + ford: { + entry: userFord + } + } + }; + + const newState = reducer(oldState, { + type: FETCH_USERS_SUCCESS, + payload: responseBodyZaphod + }); + expect(newState.usersByNames["zaphod"]).toBeDefined(); + expect(newState.usersByNames["ford"]).toBeDefined(); + }); +});