Add feature room-admin

Change-Id: I3bbe7bd7299b56e84ab918d2a8bb22facafecbf1
This commit is contained in:
Manuel Stahl 2019-03-11 17:02:07 +01:00
parent 8b339db56e
commit 3d9ec623e4
17 changed files with 360 additions and 0 deletions

View File

@ -3,6 +3,7 @@ import { routerReducer } from 'react-router-redux';
import homeReducer from '../features/home/redux/reducer'; import homeReducer from '../features/home/redux/reducer';
import commonReducer from '../features/common/redux/reducer'; import commonReducer from '../features/common/redux/reducer';
import userAdminReducer from '../features/user-admin/redux/reducer'; import userAdminReducer from '../features/user-admin/redux/reducer';
import roomAdminReducer from '../features/room-admin/redux/reducer';
// NOTE 1: DO NOT CHANGE the 'reducerMap' name and the declaration pattern. // NOTE 1: DO NOT CHANGE the 'reducerMap' name and the declaration pattern.
// This is used for Rekit cmds to register new features, remove features, etc. // This is used for Rekit cmds to register new features, remove features, etc.
@ -14,6 +15,7 @@ const reducerMap = {
home: homeReducer, home: homeReducer,
common: commonReducer, common: commonReducer,
userAdmin: userAdminReducer, userAdmin: userAdminReducer,
roomAdmin: roomAdminReducer,
}; };
export default combineReducers(reducerMap); export default combineReducers(reducerMap);

View File

@ -4,6 +4,7 @@ import _ from 'lodash';
import commonRoute from '../features/common/route'; import commonRoute from '../features/common/route';
import homeRoute from '../features/home/route'; import homeRoute from '../features/home/route';
import userAdminRoute from '../features/user-admin/route'; import userAdminRoute from '../features/user-admin/route';
import roomAdminRoute from '../features/room-admin/route';
// NOTE: DO NOT CHANGE the 'childRoutes' name and the declaration pattern. // NOTE: DO NOT CHANGE the 'childRoutes' name and the declaration pattern.
// This is used for Rekit cmds to register routes config for new features, and remove config when remove features, etc. // This is used for Rekit cmds to register routes config for new features, and remove config when remove features, etc.
@ -11,6 +12,7 @@ const childRoutes = [
homeRoute, homeRoute,
commonRoute, commonRoute,
userAdminRoute, userAdminRoute,
roomAdminRoute,
]; ];
const routes = [{ const routes = [{

View File

@ -80,6 +80,10 @@ class Layout extends Component {
component={Link} to="/user-admin"> component={Link} to="/user-admin">
<ListItemText primary="Users" /> <ListItemText primary="Users" />
</ListItem> </ListItem>
<ListItem button key="Rooms" selected={pathname === "/room-admin"}
component={Link} to="/room-admin">
<ListItemText primary="Rooms" />
</ListItem>
</List> </List>
</div> </div>
); );

View File

@ -0,0 +1,61 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'
import { Table, TableBody, TableCell, TableHead, TableRow } from '@material-ui/core';
import * as actions from './redux/actions';
export class List extends Component {
static propTypes = {
roomAdmin: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
};
componentDidMount() {
const { fetchPublicRooms } = this.props.actions;
fetchPublicRooms();
}
render() {
const { mtx } = this.props;
const { roomList, fetchRoomsPending } = this.props.roomAdmin;
return (
<div className="room-admin-list">
{(!mtx || !mtx.clientRunning) && <Redirect to="/" />}
{fetchRoomsPending && <img alt="logging in" src="data:image/gif;base64,R0lGODlhEAAQAPIAAP///wAAAMLCwkJCQgAAAGJiYoKCgpKSkiH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAADMwi63P4wyklrE2MIOggZnAdOmGYJRbExwroUmcG2LmDEwnHQLVsYOd2mBzkYDAdKa+dIAAAh+QQJCgAAACwAAAAAEAAQAAADNAi63P5OjCEgG4QMu7DmikRxQlFUYDEZIGBMRVsaqHwctXXf7WEYB4Ag1xjihkMZsiUkKhIAIfkECQoAAAAsAAAAABAAEAAAAzYIujIjK8pByJDMlFYvBoVjHA70GU7xSUJhmKtwHPAKzLO9HMaoKwJZ7Rf8AYPDDzKpZBqfvwQAIfkECQoAAAAsAAAAABAAEAAAAzMIumIlK8oyhpHsnFZfhYumCYUhDAQxRIdhHBGqRoKw0R8DYlJd8z0fMDgsGo/IpHI5TAAAIfkECQoAAAAsAAAAABAAEAAAAzIIunInK0rnZBTwGPNMgQwmdsNgXGJUlIWEuR5oWUIpz8pAEAMe6TwfwyYsGo/IpFKSAAAh+QQJCgAAACwAAAAAEAAQAAADMwi6IMKQORfjdOe82p4wGccc4CEuQradylesojEMBgsUc2G7sDX3lQGBMLAJibufbSlKAAAh+QQJCgAAACwAAAAAEAAQAAADMgi63P7wCRHZnFVdmgHu2nFwlWCI3WGc3TSWhUFGxTAUkGCbtgENBMJAEJsxgMLWzpEAACH5BAkKAAAALAAAAAAQABAAAAMyCLrc/jDKSatlQtScKdceCAjDII7HcQ4EMTCpyrCuUBjCYRgHVtqlAiB1YhiCnlsRkAAAOwAAAAAAAAAAAA==" />}
<Table className="room-list">
<TableHead>
<TableRow>
<TableCell>Name</TableCell><TableCell>Room-ID</TableCell><TableCell>Members</TableCell>
</TableRow>
</TableHead>
<TableBody>
{roomList.map(item => (<TableRow key={item.room_id}><TableCell>{item.name}</TableCell><TableCell>{item.room_id}</TableCell><TableCell>{item.num_joined_members}</TableCell></TableRow>))}
</TableBody>
</Table>
</div>
);
}
}
/* istanbul ignore next */
function mapStateToProps(state) {
return {
mtx: state.common.mtx,
roomAdmin: state.roomAdmin,
};
}
/* istanbul ignore next */
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({ ...actions }, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(List);

View File

@ -0,0 +1,9 @@
@import '../../styles/mixins';
.room-admin-list {
th {
background-color: #EEE;
font-size: 1rem;
font-weight: bold;
}
}

View File

@ -0,0 +1 @@
export { default as List } from './List';

View File

@ -0,0 +1 @@
export { fetchPublicRooms, dismissFetchPublicRoomsError } from './fetchPublicRooms';

View File

@ -0,0 +1,4 @@
export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN';
export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS';
export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE';
export const ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR = 'ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR';

View File

@ -0,0 +1,88 @@
import {
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN,
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS,
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE,
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
} from './constants';
// Rekit uses redux-thunk for async actions by default: https://github.com/gaearon/redux-thunk
// If you prefer redux-saga, you can use rekit-plugin-redux-saga: https://github.com/supnate/rekit-plugin-redux-saga
export function fetchPublicRooms(args = {}) {
return (dispatch, getState) => {
dispatch({
type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN,
});
const promise = new Promise((resolve, reject) => {
const mtx = getState().common.mtx;
const doRequest = mtx.publicRooms();
doRequest.then(
(res) => {
dispatch({
type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS,
data: res,
});
resolve(res);
},
// Use rejectHandler as the second argument so that render errors won't be caught.
(err) => {
dispatch({
type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE,
data: { error: err },
});
reject(err);
},
);
});
return promise;
};
}
// Async action saves request error by default, this method is used to dismiss the error info.
// If you don't want errors to be saved in Redux store, just ignore this method.
export function dismissFetchPublicRoomsError() {
return {
type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
};
}
export function reducer(state, action) {
switch (action.type) {
case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN:
// Just after a request is sent
return {
...state,
fetchPublicRoomsPending: true,
fetchPublicRoomsError: null,
};
case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS:
// The request is success
return {
...state,
roomList: action.data.chunk,
fetchPublicRoomsPending: false,
fetchPublicRoomsError: null,
//roomList: action.data.chunk,
};
case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE:
// The request is failed
return {
...state,
fetchPublicRoomsPending: false,
fetchPublicRoomsError: action.data.error,
};
case ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR:
// Dismiss the request failure error
return {
...state,
fetchPublicRoomsError: null,
};
default:
return state;
}
}

View File

@ -0,0 +1,14 @@
// Initial state is the place you define all initial values for the Redux store of the feature.
// In the 'standard' way, initialState is defined in reducers: http://redux.js.org/docs/basics/Reducers.html
// But when application grows, there will be multiple reducers files, it's not intuitive what data is managed by the whole store.
// So Rekit extracts the initial state definition into a separate module so that you can have
// a quick view about what data is used for the feature, at any time.
// NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions.
const initialState = {
fetchPublicRoomsPending: false,
fetchPublicRoomsError: null,
roomList: [],
};
export default initialState;

View File

@ -0,0 +1,25 @@
// This is the root reducer of the feature. It is used for:
// 1. Load reducers from each action in the feature and process them one by one.
// Note that this part of code is mainly maintained by Rekit, you usually don't need to edit them.
// 2. Write cross-topic reducers. If a reducer is not bound to some specific action.
// Then it could be written here.
// Learn more from the introduction of this approach:
// https://medium.com/@nate_wang/a-new-approach-for-managing-redux-actions-91c26ce8b5da.
import initialState from './initialState';
import { reducer as fetchPublicRoomsReducer } from './fetchPublicRooms';
const reducers = [
fetchPublicRoomsReducer,
];
export default function reducer(state = initialState, action) {
let newState;
switch (action.type) {
// Handle cross-topic actions here
default:
newState = state;
break;
}
return reducers.reduce((s, r) => r(s, action), newState);
}

View File

@ -0,0 +1,16 @@
// This is the JSON way to define React Router rules in a Rekit app.
// Learn more from: http://rekit.js.org/docs/routing.html
import {
List,
} from './';
import { Layout } from '../common';
export default {
path: 'room-admin',
name: 'Room admin',
component: Layout,
childRoutes: [
{ path: 'list', name: 'List', component: List, isIndex: true },
],
};

View File

@ -0,0 +1,2 @@
@import '../../styles/mixins';
@import './List';

View File

@ -3,3 +3,4 @@
@import '../features/home/style'; @import '../features/home/style';
@import '../features/common/style'; @import '../features/common/style';
@import '../features/user-admin/style'; @import '../features/user-admin/style';
@import '../features/room-admin/style';

View File

@ -0,0 +1,19 @@
import React from 'react';
import { shallow } from 'enzyme';
import { List } from '../../../src/features/room-admin/List';
describe('room-admin/List', () => {
it('renders node with correct class name', () => {
const props = {
roomAdmin: {},
actions: {},
};
const renderedComponent = shallow(
<List {...props} />
);
expect(
renderedComponent.find('.room-admin-list').length
).toBe(1);
});
});

View File

@ -0,0 +1,97 @@
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import nock from 'nock';
import {
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN,
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS,
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE,
ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
} from '../../../../src/features/room-admin/redux/constants';
import {
fetchPublicRooms,
dismissFetchPublicRoomsError,
reducer,
} from '../../../../src/features/room-admin/redux/fetchPublicRooms';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('room-admin/redux/fetchPublicRooms', () => {
afterEach(() => {
nock.cleanAll();
});
it('dispatches success action when fetchPublicRooms succeeds', () => {
const store = mockStore({});
return store.dispatch(fetchPublicRooms())
.then(() => {
const actions = store.getActions();
expect(actions[0]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN);
expect(actions[1]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS);
});
});
it('dispatches failure action when fetchPublicRooms fails', () => {
const store = mockStore({});
return store.dispatch(fetchPublicRooms({ error: true }))
.catch(() => {
const actions = store.getActions();
expect(actions[0]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN);
expect(actions[1]).toHaveProperty('type', ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE);
expect(actions[1]).toHaveProperty('data.error', expect.anything());
});
});
it('returns correct action by dismissFetchPublicRoomsError', () => {
const expectedAction = {
type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR,
};
expect(dismissFetchPublicRoomsError()).toEqual(expectedAction);
});
it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN correctly', () => {
const prevState = { fetchPublicRoomsPending: false };
const state = reducer(
prevState,
{ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_BEGIN }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.fetchPublicRoomsPending).toBe(true);
});
it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS correctly', () => {
const prevState = { fetchPublicRoomsPending: true };
const state = reducer(
prevState,
{ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_SUCCESS, data: {} }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.fetchPublicRoomsPending).toBe(false);
});
it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE correctly', () => {
const prevState = { fetchPublicRoomsPending: true };
const state = reducer(
prevState,
{ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_FAILURE, data: { error: new Error('some error') } }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.fetchPublicRoomsPending).toBe(false);
expect(state.fetchPublicRoomsError).toEqual(expect.anything());
});
it('handles action type ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR correctly', () => {
const prevState = { fetchPublicRoomsError: new Error('some error') };
const state = reducer(
prevState,
{ type: ROOM_ADMIN_FETCH_PUBLIC_ROOMS_DISMISS_ERROR }
);
expect(state).not.toBe(prevState); // should be immutable
expect(state.fetchPublicRoomsError).toBe(null);
});
});

View File

@ -0,0 +1,14 @@
import reducer from '../../../../src/features/room-admin/redux/reducer';
describe('room-admin/redux/reducer', () => {
it('does nothing if no matched action', () => {
const prevState = {};
const state = reducer(
prevState,
{ type: '__UNKNOWN_ACTION_TYPE__' }
);
expect(state).toBe(prevState);
});
// TODO: add global reducer test if needed.
});