Add feature room-admin
Change-Id: I3bbe7bd7299b56e84ab918d2a8bb22facafecbf1
This commit is contained in:
parent
8b339db56e
commit
3d9ec623e4
@ -3,6 +3,7 @@ import { routerReducer } from 'react-router-redux';
|
||||
import homeReducer from '../features/home/redux/reducer';
|
||||
import commonReducer from '../features/common/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.
|
||||
// This is used for Rekit cmds to register new features, remove features, etc.
|
||||
@ -14,6 +15,7 @@ const reducerMap = {
|
||||
home: homeReducer,
|
||||
common: commonReducer,
|
||||
userAdmin: userAdminReducer,
|
||||
roomAdmin: roomAdminReducer,
|
||||
};
|
||||
|
||||
export default combineReducers(reducerMap);
|
||||
|
@ -4,6 +4,7 @@ import _ from 'lodash';
|
||||
import commonRoute from '../features/common/route';
|
||||
import homeRoute from '../features/home/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.
|
||||
// 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,
|
||||
commonRoute,
|
||||
userAdminRoute,
|
||||
roomAdminRoute,
|
||||
];
|
||||
|
||||
const routes = [{
|
||||
|
@ -80,6 +80,10 @@ class Layout extends Component {
|
||||
component={Link} to="/user-admin">
|
||||
<ListItemText primary="Users" />
|
||||
</ListItem>
|
||||
<ListItem button key="Rooms" selected={pathname === "/room-admin"}
|
||||
component={Link} to="/room-admin">
|
||||
<ListItemText primary="Rooms" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
|
61
src/features/room-admin/List.js
Normal file
61
src/features/room-admin/List.js
Normal 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);
|
9
src/features/room-admin/List.scss
Normal file
9
src/features/room-admin/List.scss
Normal file
@ -0,0 +1,9 @@
|
||||
@import '../../styles/mixins';
|
||||
|
||||
.room-admin-list {
|
||||
th {
|
||||
background-color: #EEE;
|
||||
font-size: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
1
src/features/room-admin/index.js
Normal file
1
src/features/room-admin/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default as List } from './List';
|
1
src/features/room-admin/redux/actions.js
Normal file
1
src/features/room-admin/redux/actions.js
Normal file
@ -0,0 +1 @@
|
||||
export { fetchPublicRooms, dismissFetchPublicRoomsError } from './fetchPublicRooms';
|
4
src/features/room-admin/redux/constants.js
Normal file
4
src/features/room-admin/redux/constants.js
Normal 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';
|
88
src/features/room-admin/redux/fetchPublicRooms.js
Normal file
88
src/features/room-admin/redux/fetchPublicRooms.js
Normal 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;
|
||||
}
|
||||
}
|
14
src/features/room-admin/redux/initialState.js
Normal file
14
src/features/room-admin/redux/initialState.js
Normal 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;
|
25
src/features/room-admin/redux/reducer.js
Normal file
25
src/features/room-admin/redux/reducer.js
Normal 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);
|
||||
}
|
16
src/features/room-admin/route.js
Normal file
16
src/features/room-admin/route.js
Normal 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 },
|
||||
],
|
||||
};
|
2
src/features/room-admin/style.scss
Normal file
2
src/features/room-admin/style.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@import '../../styles/mixins';
|
||||
@import './List';
|
@ -3,3 +3,4 @@
|
||||
@import '../features/home/style';
|
||||
@import '../features/common/style';
|
||||
@import '../features/user-admin/style';
|
||||
@import '../features/room-admin/style';
|
||||
|
19
tests/features/room-admin/List.test.js
Normal file
19
tests/features/room-admin/List.test.js
Normal 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);
|
||||
});
|
||||
});
|
97
tests/features/room-admin/redux/fetchPublicRooms.test.js
Normal file
97
tests/features/room-admin/redux/fetchPublicRooms.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
|
14
tests/features/room-admin/redux/reducer.test.js
Normal file
14
tests/features/room-admin/redux/reducer.test.js
Normal 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.
|
||||
});
|
Loading…
Reference in New Issue
Block a user