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 React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import SlideDown from 'app/core/components/Animations/SlideDown'; import SlideDown from 'app/core/components/Animations/SlideDown';
import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; 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 { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
import { getSearchMemberQuery, getTeam } from './state/selectors'; import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
import { getRouteParamsId } from '../../core/selectors/location';
interface Props { export interface Props {
team: Team; members: TeamMember[];
searchMemberQuery: string; searchMemberQuery: string;
loadTeamMembers: typeof loadTeamMembers; loadTeamMembers: typeof loadTeamMembers;
addTeamMember: typeof addTeamMember; addTeamMember: typeof addTeamMember;
...@@ -37,7 +35,7 @@ export class TeamMembers extends PureComponent<Props, State> { ...@@ -37,7 +35,7 @@ export class TeamMembers extends PureComponent<Props, State> {
this.props.setSearchMemberQuery(event.target.value); this.props.setSearchMemberQuery(event.target.value);
}; };
removeMember(member: TeamMember) { onRemoveMember(member: TeamMember) {
this.props.removeTeamMember(member.userId); this.props.removeTeamMember(member.userId);
} }
...@@ -63,7 +61,7 @@ export class TeamMembers extends PureComponent<Props, State> { ...@@ -63,7 +61,7 @@ export class TeamMembers extends PureComponent<Props, State> {
<td>{member.login}</td> <td>{member.login}</td>
<td>{member.email}</td> <td>{member.email}</td>
<td className="text-right"> <td className="text-right">
<DeleteButton onConfirmDelete={() => this.removeMember(member)} /> <DeleteButton onConfirmDelete={() => this.onRemoveMember(member)} />
</td> </td>
</tr> </tr>
); );
...@@ -71,7 +69,7 @@ export class TeamMembers extends PureComponent<Props, State> { ...@@ -71,7 +69,7 @@ export class TeamMembers extends PureComponent<Props, State> {
render() { render() {
const { newTeamMember, isAdding } = this.state; const { newTeamMember, isAdding } = this.state;
const { team, searchMemberQuery } = this.props; const { searchMemberQuery, members } = this.props;
const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
return ( return (
...@@ -125,7 +123,7 @@ export class TeamMembers extends PureComponent<Props, State> { ...@@ -125,7 +123,7 @@ export class TeamMembers extends PureComponent<Props, State> {
<th style={{ width: '1%' }} /> <th style={{ width: '1%' }} />
</tr> </tr>
</thead> </thead>
<tbody>{team.members && team.members.map(member => this.renderMember(member))}</tbody> <tbody>{members && members.map(member => this.renderMember(member))}</tbody>
</table> </table>
</div> </div>
</div> </div>
...@@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent<Props, State> { ...@@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent<Props, State> {
} }
function mapStateToProps(state) { function mapStateToProps(state) {
const teamId = getRouteParamsId(state.location);
return { return {
team: getTeam(state.team, teamId), members: getTeamMembers(state.team),
searchMemberQuery: getSearchMemberQuery(state.team), searchMemberQuery: getSearchMemberQuery(state.team),
}; };
} }
...@@ -149,4 +145,4 @@ const mapDispatchToProps = { ...@@ -149,4 +145,4 @@ const mapDispatchToProps = {
setSearchMemberQuery, 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> { ...@@ -41,10 +41,10 @@ export class TeamPages extends PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
this.loadTeam(); this.fetchTeam();
} }
async loadTeam() { async fetchTeam() {
const { loadTeam, teamId } = this.props; const { loadTeam, teamId } = this.props;
await loadTeam(teamId); await loadTeam(teamId);
......
import { Team } from '../../../types'; import { Team, TeamMember } from '../../../types';
export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
let teams: Team[] = []; let teams: Team[] = [];
...@@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { ...@@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => {
avatarUrl: 'some/url/', avatarUrl: 'some/url/',
email: `test-${i}@test.com`, email: `test-${i}@test.com`,
memberCount: i, memberCount: i,
search: '',
members: [],
groups: [],
}); });
} }
...@@ -25,13 +22,26 @@ export const getMockTeam = (): Team => { ...@@ -25,13 +22,26 @@ export const getMockTeam = (): Team => {
avatarUrl: 'some/url/', avatarUrl: 'some/url/',
email: 'test@test.com', email: 'test@test.com',
memberCount: 1, 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 { return {
userId: 1, userId: 1,
teamId: 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`] = ` ...@@ -21,12 +21,9 @@ exports[`Render should render group sync page 1`] = `
Object { Object {
"avatarUrl": "some/url/", "avatarUrl": "some/url/",
"email": "test@test.com", "email": "test@test.com",
"groups": Array [],
"id": 1, "id": 1,
"memberCount": 1, "memberCount": 1,
"members": Array [],
"name": "test", "name": "test",
"search": "",
} }
} }
/> />
...@@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = ` ...@@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = `
<div <div
className="page-container page-body" className="page-container page-body"
> >
<TeamMembers <Connect(TeamMembers) />
team={
Object {
"avatarUrl": "some/url/",
"email": "test@test.com",
"groups": Array [],
"id": 1,
"memberCount": 1,
"members": Array [],
"name": "test",
"search": "",
}
}
/>
</div> </div>
</div> </div>
`; `;
...@@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = ` ...@@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = `
Object { Object {
"avatarUrl": "some/url/", "avatarUrl": "some/url/",
"email": "test@test.com", "email": "test@test.com",
"groups": Array [],
"id": 1, "id": 1,
"memberCount": 1, "memberCount": 1,
"members": Array [],
"name": "test", "name": "test",
"search": "",
} }
} }
/> />
......
...@@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult<void> { ...@@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult<void> {
} }
export function loadTeamMembers(): ThunkResult<void> { export function loadTeamMembers(): ThunkResult<void> {
console.log('loading team members');
return async (dispatch, getStore) => { return async (dispatch, getStore) => {
const team = getStore().team.team; const team = getStore().team.team;
......
...@@ -31,22 +31,42 @@ describe('teams reducer', () => { ...@@ -31,22 +31,42 @@ describe('teams reducer', () => {
}); });
describe('team 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', () => { it('should set team members', () => {
const mockTeamMember = getMockTeamMember(); const mockTeamMember = getMockTeamMember();
const mockTeam = getMockTeam();
const state = {
...initialTeamState,
team: mockTeam,
};
const action: Action = { const action: Action = {
type: ActionTypes.LoadTeamMembers, type: ActionTypes.LoadTeamMembers,
payload: [mockTeamMember], payload: [mockTeamMember],
}; };
const result = teamReducer(state, action); const result = teamReducer(initialTeamState, action);
const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' };
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'; import { Action, ActionTypes } from './actions';
export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; 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 => { export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => {
switch (action.type) { switch (action.type) {
...@@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState ...@@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState
return { ...state, team: action.payload }; return { ...state, team: action.payload };
case ActionTypes.LoadTeamMembers: case ActionTypes.LoadTeamMembers:
return { ...state, team: { ...state.team, members: action.payload } }; return { ...state, members: action.payload };
case ActionTypes.SetSearchMemberQuery: case ActionTypes.SetSearchMemberQuery:
return { ...state, searchMemberQuery: action.payload }; return { ...state, searchMemberQuery: action.payload };
......
import { getTeams } from './selectors'; import { getTeam, getTeams } from './selectors';
import { getMultipleMockTeams } from '../__mocks__/teamMocks'; import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks';
import { TeamsState } from '../../../types'; import { TeamsState, TeamState } from '../../../types';
describe('Team selectors', () => { describe('Teams selectors', () => {
describe('Get teams', () => { describe('Get teams', () => {
const mockTeams = getMultipleMockTeams(5); const mockTeams = getMultipleMockTeams(5);
...@@ -23,3 +23,17 @@ describe('Team selectors', () => { ...@@ -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; ...@@ -2,8 +2,7 @@ export const getSearchQuery = state => state.searchQuery;
export const getSearchMemberQuery = state => state.searchMemberQuery; export const getSearchMemberQuery = state => state.searchMemberQuery;
export const getTeam = (state, currentTeamId) => { export const getTeam = (state, currentTeamId) => {
if (state.team.id === currentTeamId) { if (state.team.id === parseInt(currentTeamId)) {
console.log('yes');
return state.team; return state.team;
} }
}; };
...@@ -15,3 +14,11 @@ export const getTeams = state => { ...@@ -15,3 +14,11 @@ export const getTeams = state => {
return regex.test(team.name); 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 { ...@@ -63,9 +63,6 @@ export interface Team {
avatarUrl: string; avatarUrl: string;
email: string; email: string;
memberCount: number; memberCount: number;
search?: string;
members?: TeamMember[];
groups?: TeamGroup[];
} }
export interface TeamMember { export interface TeamMember {
...@@ -124,6 +121,8 @@ export interface TeamsState { ...@@ -124,6 +121,8 @@ export interface TeamsState {
export interface TeamState { export interface TeamState {
team: Team; team: Team;
members: TeamMember[];
groups: TeamGroup[];
searchMemberQuery: string; 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