Commit d494ebc7 by Peter Holmberg

flattened team state, tests for TeamMembers

parent 59b3bfd3
import React from 'react';
import { shallow } from 'enzyme';
import { TeamMembers, Props } from './TeamMembers';
import { TeamMember } from '../../types';
import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks';
const setup = (propOverrides?: object) => {
const props: Props = {
members: [] as TeamMember[],
searchMemberQuery: '',
setSearchMemberQuery: jest.fn(),
loadTeamMembers: jest.fn(),
addTeamMember: jest.fn(),
removeTeamMember: jest.fn(),
};
Object.assign(props, propOverrides);
const wrapper = shallow(<TeamMembers {...props} />);
const instance = wrapper.instance() as TeamMembers;
return {
wrapper,
instance,
};
};
describe('Render', () => {
it('should render component', () => {
const { wrapper } = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render team members', () => {
const { wrapper } = setup({
members: getMockTeamMembers(5),
});
expect(wrapper).toMatchSnapshot();
});
});
describe('Functions', () => {
describe('on search member query change', () => {
it('it should call setSearchMemberQuery', () => {
const { instance } = setup();
const mockEvent = { target: { value: 'member' } };
instance.onSearchQueryChange(mockEvent);
expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member');
});
});
describe('on remove member', () => {
const { instance } = setup();
const mockTeamMember = getMockTeamMember();
instance.onRemoveMember(mockTeamMember);
expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1);
});
describe('on add user to team', () => {
const { wrapper, instance } = setup();
wrapper.state().newTeamMember = {
id: 1,
label: '',
avatarUrl: '',
login: '',
};
instance.onAddUserToTeam();
expect(instance.props.addTeamMember).toHaveBeenCalledWith(1);
});
});
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import SlideDown from 'app/core/components/Animations/SlideDown';
import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton';
import { Team, TeamMember } from '../../types';
import { TeamMember } from '../../types';
import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
import { getSearchMemberQuery, getTeam } from './state/selectors';
import { getRouteParamsId } from '../../core/selectors/location';
import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
interface Props {
team: Team;
export interface Props {
members: TeamMember[];
searchMemberQuery: string;
loadTeamMembers: typeof loadTeamMembers;
addTeamMember: typeof addTeamMember;
......@@ -37,7 +35,7 @@ export class TeamMembers extends PureComponent<Props, State> {
this.props.setSearchMemberQuery(event.target.value);
};
removeMember(member: TeamMember) {
onRemoveMember(member: TeamMember) {
this.props.removeTeamMember(member.userId);
}
......@@ -63,7 +61,7 @@ export class TeamMembers extends PureComponent<Props, State> {
<td>{member.login}</td>
<td>{member.email}</td>
<td className="text-right">
<DeleteButton onConfirmDelete={() => this.removeMember(member)} />
<DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
</td>
</tr>
);
......@@ -71,7 +69,7 @@ export class TeamMembers extends PureComponent<Props, State> {
render() {
const { newTeamMember, isAdding } = this.state;
const { team, searchMemberQuery } = this.props;
const { searchMemberQuery, members } = this.props;
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
return (
......@@ -125,7 +123,7 @@ export class TeamMembers extends PureComponent<Props, State> {
<th style={{ width: '1%' }} />
</tr>
</thead>
<tbody>{team.members && team.members.map(member => this.renderMember(member))}</tbody>
<tbody>{members && members.map(member => this.renderMember(member))}</tbody>
</table>
</div>
</div>
......@@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent<Props, State> {
}
function mapStateToProps(state) {
const teamId = getRouteParamsId(state.location);
return {
team: getTeam(state.team, teamId),
members: getTeamMembers(state.team),
searchMemberQuery: getSearchMemberQuery(state.team),
};
}
......@@ -149,4 +145,4 @@ const mapDispatchToProps = {
setSearchMemberQuery,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers));
export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers);
......@@ -41,10 +41,10 @@ export class TeamPages extends PureComponent<Props, State> {
}
componentDidMount() {
this.loadTeam();
this.fetchTeam();
}
async loadTeam() {
async fetchTeam() {
const { loadTeam, teamId } = this.props;
await loadTeam(teamId);
......
import { Team } from '../../../types';
import { Team, TeamMember } from '../../../types';
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
let teams: Team[] = [];
......@@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
avatarUrl: 'some/url/',
email: `test-${i}@test.com`,
memberCount: i,
search: '',
members: [],
groups: [],
});
}
......@@ -25,13 +22,26 @@ export const getMockTeam = (): Team => {
avatarUrl: 'some/url/',
email: 'test@test.com',
memberCount: 1,
search: '',
members: [],
groups: [],
};
};
export const getMockTeamMember = () => {
export const getMockTeamMembers = (amount: number): TeamMember[] => {
let teamMembers: TeamMember[] = [];
for (let i = 1; i <= amount; i++) {
teamMembers.push({
userId: i,
teamId: 1,
avatarUrl: 'some/url/',
email: 'test@test.com',
login: `testUser-${i}`,
});
}
return teamMembers;
};
export const getMockTeamMember = (): TeamMember => {
return {
userId: 1,
teamId: 1,
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div>
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form--has-input-icon gf-form--grow"
>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Search members"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
</div>
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-success pull-right"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-plus"
/>
Add a member
</button>
</div>
<Component
in={false}
>
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
onClick={[Function]}
>
<i
className="fa fa-close"
/>
</button>
<h5>
Add Team Member
</h5>
<div
className="gf-form-inline"
>
<UserPicker
className="width-30"
onSelected={[Function]}
value={null}
/>
</div>
</div>
</Component>
<div
className="admin-list-table"
>
<table
className="filter-table filter-table--hover form-inline"
>
<thead>
<tr>
<th />
<th>
Name
</th>
<th>
Email
</th>
<th
style={
Object {
"width": "1%",
}
}
/>
</tr>
</thead>
<tbody />
</table>
</div>
</div>
`;
exports[`Render should render team members 1`] = `
<div>
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<label
className="gf-form--has-input-icon gf-form--grow"
>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Search members"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
</div>
<div
className="page-action-bar__spacer"
/>
<button
className="btn btn-success pull-right"
disabled={false}
onClick={[Function]}
>
<i
className="fa fa-plus"
/>
Add a member
</button>
</div>
<Component
in={false}
>
<div
className="cta-form"
>
<button
className="cta-form__close btn btn-transparent"
onClick={[Function]}
>
<i
className="fa fa-close"
/>
</button>
<h5>
Add Team Member
</h5>
<div
className="gf-form-inline"
>
<UserPicker
className="width-30"
onSelected={[Function]}
value={null}
/>
</div>
</div>
</Component>
<div
className="admin-list-table"
>
<table
className="filter-table filter-table--hover form-inline"
>
<thead>
<tr>
<th />
<th>
Name
</th>
<th>
Email
</th>
<th
style={
Object {
"width": "1%",
}
}
/>
</tr>
</thead>
<tbody>
<tr
key="1"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-1
</td>
<td>
test@test.com
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="2"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-2
</td>
<td>
test@test.com
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="3"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-3
</td>
<td>
test@test.com
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="4"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-4
</td>
<td>
test@test.com
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
<tr
key="5"
>
<td
className="width-4 text-center"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</td>
<td>
testUser-5
</td>
<td>
test@test.com
</td>
<td
className="text-right"
>
<DeleteButton
onConfirmDelete={[Function]}
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
`;
......@@ -21,12 +21,9 @@ exports[`Render should render group sync page 1`] = `
Object {
"avatarUrl": "some/url/",
"email": "test@test.com",
"groups": Array [],
"id": 1,
"memberCount": 1,
"members": Array [],
"name": "test",
"search": "",
}
}
/>
......@@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = `
<div
className="page-container page-body"
>
<TeamMembers
team={
Object {
"avatarUrl": "some/url/",
"email": "test@test.com",
"groups": Array [],
"id": 1,
"memberCount": 1,
"members": Array [],
"name": "test",
"search": "",
}
}
/>
<Connect(TeamMembers) />
</div>
</div>
`;
......@@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = `
Object {
"avatarUrl": "some/url/",
"email": "test@test.com",
"groups": Array [],
"id": 1,
"memberCount": 1,
"members": Array [],
"name": "test",
"search": "",
}
}
/>
......
......@@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult<void> {
}
export function loadTeamMembers(): ThunkResult<void> {
console.log('loading team members');
return async (dispatch, getStore) => {
const team = getStore().team.team;
......
......@@ -31,22 +31,42 @@ describe('teams reducer', () => {
});
describe('team reducer', () => {
it('should set team', () => {
const payload = getMockTeam();
const action: Action = {
type: ActionTypes.LoadTeam,
payload,
};
const result = teamReducer(initialTeamState, action);
expect(result.team).toEqual(payload);
});
it('should set team members', () => {
const mockTeamMember = getMockTeamMember();
const mockTeam = getMockTeam();
const state = {
...initialTeamState,
team: mockTeam,
};
const action: Action = {
type: ActionTypes.LoadTeamMembers,
payload: [mockTeamMember],
};
const result = teamReducer(state, action);
const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' };
const result = teamReducer(initialTeamState, action);
expect(result.members).toEqual([mockTeamMember]);
});
it('should set member search query', () => {
const payload = 'member';
const action: Action = {
type: ActionTypes.SetSearchMemberQuery,
payload,
};
const result = teamReducer(initialTeamState, action);
expect(result).toEqual(expectedState);
expect(result.searchMemberQuery).toEqual('member');
});
});
import { Team, TeamsState, TeamState } from '../../../types';
import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from '../../../types';
import { Action, ActionTypes } from './actions';
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' };
export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' };
export const initialTeamState: TeamState = {
team: {} as Team,
members: [] as TeamMember[],
groups: [] as TeamGroup[],
searchMemberQuery: '',
};
export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
switch (action.type) {
......@@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState
return { ...state, team: action.payload };
case ActionTypes.LoadTeamMembers:
return { ...state, team: { ...state.team, members: action.payload } };
return { ...state, members: action.payload };
case ActionTypes.SetSearchMemberQuery:
return { ...state, searchMemberQuery: action.payload };
......
import { getTeams } from './selectors';
import { getMultipleMockTeams } from '../__mocks__/teamMocks';
import { TeamsState } from '../../../types';
import { getTeam, getTeams } from './selectors';
import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks';
import { TeamsState, TeamState } from '../../../types';
describe('Team selectors', () => {
describe('Teams selectors', () => {
describe('Get teams', () => {
const mockTeams = getMultipleMockTeams(5);
......@@ -23,3 +23,17 @@ describe('Team selectors', () => {
});
});
});
describe('Team selectors', () => {
describe('Get team', () => {
const mockTeam = getMockTeam();
it('should return team if matching with location team', () => {
const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' };
const team = getTeam(mockState, '1');
expect(team).toEqual(mockTeam);
});
});
});
......@@ -2,8 +2,7 @@ export const getSearchQuery = state => state.searchQuery;
export const getSearchMemberQuery = state => state.searchMemberQuery;
export const getTeam = (state, currentTeamId) => {
if (state.team.id === currentTeamId) {
console.log('yes');
if (state.team.id === parseInt(currentTeamId)) {
return state.team;
}
};
......@@ -15,3 +14,11 @@ export const getTeams = state => {
return regex.test(team.name);
});
};
export const getTeamMembers = state => {
const regex = RegExp(state.searchMemberQuery, 'i');
return state.members.filter(member => {
return regex.test(member.login) || regex.test(member.email);
});
};
......@@ -63,9 +63,6 @@ export interface Team {
avatarUrl: string;
email: string;
memberCount: number;
search?: string;
members?: TeamMember[];
groups?: TeamGroup[];
}
export interface TeamMember {
......@@ -124,6 +121,8 @@ export interface TeamsState {
export interface TeamState {
team: Team;
members: TeamMember[];
groups: TeamGroup[];
searchMemberQuery: string;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment