@@ -62,7 +62,7 @@ export function removeUserFromGroup(data: { | |||
export function createGroup(data: { | |||
description?: string; | |||
organization: string | undefined; | |||
organization?: string; | |||
name: string; | |||
}): Promise<T.Group> { | |||
return postJSON('/api/user_groups/create', data).then(r => r.group, throwGlobalError); | |||
@@ -72,6 +72,6 @@ export function updateGroup(data: { description?: string; id: number; name?: str | |||
return post('/api/user_groups/update', data).catch(throwGlobalError); | |||
} | |||
export function deleteGroup(data: { name: string; organization: string | undefined }) { | |||
export function deleteGroup(data: { name: string; organization?: string }) { | |||
return post('/api/user_groups/delete', data).catch(throwGlobalError); | |||
} |
@@ -22,23 +22,24 @@ import { Helmet } from 'react-helmet-async'; | |||
import ListFooter from 'sonar-ui-common/components/controls/ListFooter'; | |||
import SearchBox from 'sonar-ui-common/components/controls/SearchBox'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { omitNil } from 'sonar-ui-common/helpers/request'; | |||
import { createGroup, deleteGroup, searchUsersGroups, updateGroup } from '../../../api/user_groups'; | |||
import Suggestions from '../../../app/components/embed-docs-modal/Suggestions'; | |||
import DeleteForm from './DeleteForm'; | |||
import Form from './Form'; | |||
import Header from './Header'; | |||
import List from './List'; | |||
interface Props { | |||
organization?: Pick<T.Organization, 'key'>; | |||
} | |||
interface State { | |||
groups?: T.Group[]; | |||
editedGroup?: T.Group; | |||
groupToBeDeleted?: T.Group; | |||
loading: boolean; | |||
paging?: T.Paging; | |||
query: string; | |||
} | |||
export default class App extends React.PureComponent<Props, State> { | |||
export default class App extends React.PureComponent<{}, State> { | |||
mounted = false; | |||
state: State = { loading: true, query: '' }; | |||
@@ -51,14 +52,9 @@ export default class App extends React.PureComponent<Props, State> { | |||
this.mounted = false; | |||
} | |||
get organization() { | |||
return this.props.organization && this.props.organization.key; | |||
} | |||
makeFetchGroupsRequest = (data?: { p?: number; q?: string }) => { | |||
this.setState({ loading: true }); | |||
return searchUsersGroups({ | |||
organization: this.organization, | |||
q: this.state.query, | |||
...data | |||
}); | |||
@@ -70,18 +66,24 @@ export default class App extends React.PureComponent<Props, State> { | |||
} | |||
}; | |||
fetchGroups = (data?: { p?: number; q?: string }) => { | |||
this.makeFetchGroupsRequest(data).then(({ groups, paging }) => { | |||
fetchGroups = async (data?: { p?: number; q?: string }) => { | |||
try { | |||
const { groups, paging } = await this.makeFetchGroupsRequest(data); | |||
if (this.mounted) { | |||
this.setState({ groups, loading: false, paging }); | |||
} | |||
}, this.stopLoading); | |||
} catch { | |||
this.stopLoading(); | |||
} | |||
}; | |||
fetchMoreGroups = () => { | |||
const { paging } = this.state; | |||
if (paging && paging.total > paging.pageIndex * paging.pageSize) { | |||
this.makeFetchGroupsRequest({ p: paging.pageIndex + 1 }).then(({ groups, paging }) => { | |||
fetchMoreGroups = async () => { | |||
const { paging: currentPaging } = this.state; | |||
if (currentPaging && currentPaging.total > currentPaging.pageIndex * currentPaging.pageSize) { | |||
try { | |||
const { groups, paging } = await this.makeFetchGroupsRequest({ | |||
p: currentPaging.pageIndex + 1 | |||
}); | |||
if (this.mounted) { | |||
this.setState(({ groups: existingGroups = [] }) => ({ | |||
groups: [...existingGroups, ...groups], | |||
@@ -89,7 +91,9 @@ export default class App extends React.PureComponent<Props, State> { | |||
paging | |||
})); | |||
} | |||
}, this.stopLoading); | |||
} catch { | |||
this.stopLoading(); | |||
} | |||
} | |||
}; | |||
@@ -98,52 +102,94 @@ export default class App extends React.PureComponent<Props, State> { | |||
this.setState({ query }); | |||
}; | |||
refresh = () => { | |||
this.fetchGroups({ q: this.state.query }); | |||
}; | |||
refresh = async () => { | |||
const { paging, query } = this.state; | |||
handleCreate = (data: { description: string; name: string }) => { | |||
return createGroup({ ...data, organization: this.organization }).then(group => { | |||
if (this.mounted) { | |||
this.setState(({ groups = [] }: State) => ({ | |||
groups: [...groups, group] | |||
})); | |||
await this.fetchGroups({ q: query }); | |||
// reload all pages in order | |||
if (paging && paging.pageIndex > 1) { | |||
for (let p = 1; p < paging.pageIndex; p++) { | |||
await this.fetchMoreGroups(); | |||
} | |||
}); | |||
} | |||
}; | |||
handleDelete = (name: string) => { | |||
return deleteGroup({ name, organization: this.organization }).then(() => { | |||
if (this.mounted) { | |||
this.setState(({ groups = [] }: State) => ({ | |||
groups: groups.filter(group => group.name !== name) | |||
})); | |||
} | |||
}); | |||
closeDeleteForm = () => { | |||
this.setState({ groupToBeDeleted: undefined }); | |||
}; | |||
handleEdit = (data: { description?: string; id: number; name?: string }) => { | |||
return updateGroup(data).then(() => { | |||
if (this.mounted) { | |||
this.setState(({ groups = [] }: State) => ({ | |||
groups: groups.map(group => (group.id === data.id ? { ...group, ...data } : group)) | |||
})); | |||
} | |||
}); | |||
closeEditForm = () => { | |||
this.setState({ editedGroup: undefined }); | |||
}; | |||
openDeleteForm = (group: T.Group) => { | |||
this.setState({ groupToBeDeleted: group }); | |||
}; | |||
openEditForm = (group: T.Group) => { | |||
this.setState({ editedGroup: group }); | |||
}; | |||
handleCreate = async (data: { description: string; name: string }) => { | |||
await createGroup({ ...data }); | |||
await this.refresh(); | |||
}; | |||
handleDelete = async () => { | |||
const { groupToBeDeleted } = this.state; | |||
if (!groupToBeDeleted) { | |||
return; | |||
} | |||
await deleteGroup({ name: groupToBeDeleted.name }); | |||
await this.refresh(); | |||
if (this.mounted) { | |||
this.setState({ groupToBeDeleted: undefined }); | |||
} | |||
}; | |||
handleEdit = async ({ name, description }: { name?: string; description: string }) => { | |||
const { editedGroup } = this.state; | |||
if (!editedGroup) { | |||
return; | |||
} | |||
const data = { | |||
description, | |||
id: editedGroup.id, | |||
// pass `name` only if it has changed, otherwise the WS fails | |||
...omitNil({ name: name !== editedGroup.name ? name : undefined }) | |||
}; | |||
await updateGroup(data); | |||
if (this.mounted) { | |||
this.setState(({ groups = [] }: State) => ({ | |||
editedGroup: undefined, | |||
groups: groups.map(group => | |||
group.name === editedGroup.name ? { ...group, ...data } : group | |||
) | |||
})); | |||
} | |||
}; | |||
render() { | |||
const { groups, loading, paging, query } = this.state; | |||
const { editedGroup, groupToBeDeleted, groups, loading, paging, query } = this.state; | |||
const showAnyone = | |||
this.props.organization === undefined && 'anyone'.includes(query.toLowerCase()); | |||
const showAnyone = 'anyone'.includes(query.toLowerCase()); | |||
return ( | |||
<> | |||
<Suggestions suggestions="user_groups" /> | |||
<Helmet defer={false} title={translate('user_groups.page')} /> | |||
<div className="page page-limited" id="groups-page"> | |||
<Header loading={loading} onCreate={this.handleCreate} /> | |||
<Header onCreate={this.handleCreate} /> | |||
<SearchBox | |||
className="big-spacer-bottom" | |||
@@ -157,10 +203,9 @@ export default class App extends React.PureComponent<Props, State> { | |||
{groups !== undefined && ( | |||
<List | |||
groups={groups} | |||
onDelete={this.handleDelete} | |||
onEdit={this.handleEdit} | |||
onDelete={this.openDeleteForm} | |||
onEdit={this.openEditForm} | |||
onEditMembers={this.refresh} | |||
organization={this.organization} | |||
showAnyone={showAnyone} | |||
/> | |||
)} | |||
@@ -169,12 +214,31 @@ export default class App extends React.PureComponent<Props, State> { | |||
<div id="groups-list-footer"> | |||
<ListFooter | |||
count={showAnyone ? groups.length + 1 : groups.length} | |||
loading={loading} | |||
loadMore={this.fetchMoreGroups} | |||
ready={!loading} | |||
total={showAnyone ? paging.total + 1 : paging.total} | |||
/> | |||
</div> | |||
)} | |||
{groupToBeDeleted && ( | |||
<DeleteForm | |||
group={groupToBeDeleted} | |||
onClose={this.closeDeleteForm} | |||
onSubmit={this.handleDelete} | |||
/> | |||
)} | |||
{editedGroup && ( | |||
<Form | |||
confirmButtonText={translate('update_verb')} | |||
group={editedGroup} | |||
header={translate('groups.update_group')} | |||
onClose={this.closeEditForm} | |||
onSubmit={this.handleEdit} | |||
/> | |||
)} | |||
</div> | |||
</> | |||
); |
@@ -20,12 +20,12 @@ | |||
import * as React from 'react'; | |||
import { ButtonIcon } from 'sonar-ui-common/components/controls/buttons'; | |||
import BulletListIcon from 'sonar-ui-common/components/icons/BulletListIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import EditMembersModal from './EditMembersModal'; | |||
interface Props { | |||
group: T.Group; | |||
onEdit: () => void; | |||
organization: string | undefined; | |||
} | |||
interface State { | |||
@@ -59,15 +59,15 @@ export default class EditMembers extends React.PureComponent<Props, State> { | |||
render() { | |||
return ( | |||
<> | |||
<ButtonIcon className="button-small" onClick={this.handleMembersClick}> | |||
<ButtonIcon | |||
aria-label={translate('groups.users.edit')} | |||
className="button-small" | |||
onClick={this.handleMembersClick} | |||
title={translate('groups.users.edit')}> | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
{this.state.modal && ( | |||
<EditMembersModal | |||
group={this.props.group} | |||
onClose={this.handleModalClose} | |||
organization={this.props.organization} | |||
/> | |||
<EditMembersModal group={this.props.group} onClose={this.handleModalClose} /> | |||
)} | |||
</> | |||
); |
@@ -31,7 +31,6 @@ import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../a | |||
interface Props { | |||
group: T.Group; | |||
onClose: () => void; | |||
organization: string | undefined; | |||
} | |||
interface State { | |||
@@ -66,7 +65,6 @@ export default class EditMembersModal extends React.PureComponent<Props, State> | |||
fetchUsers = (searchParams: SelectListSearchParams) => | |||
getUsersInGroup({ | |||
name: this.props.group.name, | |||
organization: this.props.organization, | |||
p: searchParams.page, | |||
ps: searchParams.pageSize, | |||
q: searchParams.query !== '' ? searchParams.query : undefined, | |||
@@ -97,8 +95,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State> | |||
handleSelect = (login: string) => | |||
addUserToGroup({ | |||
name: this.props.group.name, | |||
login, | |||
organization: this.props.organization | |||
login | |||
}).then(() => { | |||
if (this.mounted) { | |||
this.setState((state: State) => ({ | |||
@@ -111,8 +108,7 @@ export default class EditMembersModal extends React.PureComponent<Props, State> | |||
handleUnselect = (login: string) => | |||
removeUserFromGroup({ | |||
name: this.props.group.name, | |||
login, | |||
organization: this.props.organization | |||
login | |||
}).then(() => { | |||
if (this.mounted) { | |||
this.setState((state: State) => ({ |
@@ -19,12 +19,10 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { Button } from 'sonar-ui-common/components/controls/buttons'; | |||
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import Form from './Form'; | |||
interface Props { | |||
loading: boolean; | |||
onCreate: (data: { description: string; name: string }) => Promise<void>; | |||
} | |||
@@ -64,8 +62,6 @@ export default class Header extends React.PureComponent<Props, State> { | |||
<header className="page-header" id="groups-header"> | |||
<h1 className="page-title">{translate('user_groups.page')}</h1> | |||
<DeferredSpinner loading={this.props.loading} /> | |||
<div className="page-actions"> | |||
<Button id="groups-create" onClick={this.handleCreateClick}> | |||
{translate('groups.create_group')} |
@@ -24,10 +24,9 @@ import ListItem from './ListItem'; | |||
interface Props { | |||
groups: T.Group[]; | |||
onDelete: (name: string) => Promise<void>; | |||
onEdit: (data: { description?: string; id: number; name?: string }) => Promise<void>; | |||
onDelete: (group: T.Group) => void; | |||
onEdit: (group: T.Group) => void; | |||
onEditMembers: () => void; | |||
organization: string | undefined; | |||
showAnyone: boolean; | |||
} | |||
@@ -38,7 +37,9 @@ export default function List(props: Props) { | |||
<thead> | |||
<tr> | |||
<th /> | |||
<th className="nowrap">{translate('members')}</th> | |||
<th className="nowrap width-10" colSpan={2}> | |||
{translate('members')} | |||
</th> | |||
<th className="nowrap">{translate('description')}</th> | |||
<th /> | |||
</tr> | |||
@@ -49,7 +50,7 @@ export default function List(props: Props) { | |||
<td className="width-20"> | |||
<strong className="js-group-name">{translate('groups.anyone')}</strong> | |||
</td> | |||
<td className="width-10" /> | |||
<td className="width-10" colSpan={2} /> | |||
<td className="width-40" colSpan={2}> | |||
<span className="js-group-description"> | |||
{translate('user_groups.anyone.description')} | |||
@@ -61,11 +62,10 @@ export default function List(props: Props) { | |||
{sortBy(props.groups, group => group.name.toLowerCase()).map(group => ( | |||
<ListItem | |||
group={group} | |||
key={group.id} | |||
key={group.name} | |||
onDelete={props.onDelete} | |||
onEdit={props.onEdit} | |||
onEditMembers={props.onEditMembers} | |||
organization={props.organization} | |||
/> | |||
))} | |||
</tbody> |
@@ -23,132 +23,50 @@ import ActionsDropdown, { | |||
ActionsDropdownItem | |||
} from 'sonar-ui-common/components/controls/ActionsDropdown'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { omitNil } from 'sonar-ui-common/helpers/request'; | |||
import DeleteForm from './DeleteForm'; | |||
import EditMembers from './EditMembers'; | |||
import Form from './Form'; | |||
interface Props { | |||
export interface ListItemProps { | |||
group: T.Group; | |||
onDelete: (name: string) => Promise<void>; | |||
onEdit: (data: { description?: string; id: number; name?: string }) => Promise<void>; | |||
onDelete: (group: T.Group) => void; | |||
onEdit: (group: T.Group) => void; | |||
onEditMembers: () => void; | |||
organization: string | undefined; | |||
} | |||
interface State { | |||
deleteForm: boolean; | |||
editForm: boolean; | |||
} | |||
export default class ListItem extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { deleteForm: false, editForm: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
handleDeleteClick = () => { | |||
this.setState({ deleteForm: true }); | |||
}; | |||
handleEditClick = () => { | |||
this.setState({ editForm: true }); | |||
}; | |||
closeDeleteForm = () => { | |||
if (this.mounted) { | |||
this.setState({ deleteForm: false }); | |||
} | |||
}; | |||
closeEditForm = () => { | |||
if (this.mounted) { | |||
this.setState({ editForm: false }); | |||
} | |||
}; | |||
handleDeleteFormSubmit = () => { | |||
return this.props.onDelete(this.props.group.name); | |||
}; | |||
handleEditFormSubmit = ({ name, description }: { name: string; description: string }) => { | |||
const { group } = this.props; | |||
return this.props.onEdit({ | |||
description, | |||
id: group.id, | |||
// pass `name` only if it has changed, otherwise the WS fails | |||
...omitNil({ name: name !== group.name ? name : undefined }) | |||
}); | |||
}; | |||
render() { | |||
const { group } = this.props; | |||
return ( | |||
<tr data-id={group.id}> | |||
<td className=" width-20"> | |||
<strong className="js-group-name">{group.name}</strong> | |||
{group.default && <span className="little-spacer-left">({translate('default')})</span>} | |||
</td> | |||
<td className="width-10"> | |||
<div className="display-flex-center"> | |||
<span className="spacer-right">{group.membersCount}</span> | |||
{!group.default && ( | |||
<EditMembers | |||
group={group} | |||
onEdit={this.props.onEditMembers} | |||
organization={this.props.organization} | |||
/> | |||
)} | |||
</div> | |||
</td> | |||
<td className="width-40"> | |||
<span className="js-group-description">{group.description}</span> | |||
</td> | |||
<td className="thin nowrap text-right"> | |||
{!group.default && ( | |||
<ActionsDropdown> | |||
<ActionsDropdownItem className="js-group-update" onClick={this.handleEditClick}> | |||
{translate('update_details')} | |||
</ActionsDropdownItem> | |||
<ActionsDropdownDivider /> | |||
<ActionsDropdownItem | |||
className="js-group-delete" | |||
destructive={true} | |||
onClick={this.handleDeleteClick}> | |||
{translate('delete')} | |||
</ActionsDropdownItem> | |||
</ActionsDropdown> | |||
)} | |||
</td> | |||
{this.state.deleteForm && ( | |||
<DeleteForm | |||
group={group} | |||
onClose={this.closeDeleteForm} | |||
onSubmit={this.handleDeleteFormSubmit} | |||
/> | |||
)} | |||
{this.state.editForm && ( | |||
<Form | |||
confirmButtonText={translate('update_verb')} | |||
group={group} | |||
header={translate('groups.update_group')} | |||
onClose={this.closeEditForm} | |||
onSubmit={this.handleEditFormSubmit} | |||
/> | |||
export default function ListItem(props: ListItemProps) { | |||
const { group } = props; | |||
return ( | |||
<tr data-id={group.name}> | |||
<td className="width-20"> | |||
<strong className="js-group-name">{group.name}</strong> | |||
{group.default && <span className="little-spacer-left">({translate('default')})</span>} | |||
</td> | |||
<td className="thin text-middle text-right little-padded-right">{group.membersCount}</td> | |||
<td className="little-padded-left"> | |||
{!group.default && <EditMembers group={group} onEdit={props.onEditMembers} />} | |||
</td> | |||
<td className="width-40"> | |||
<span className="js-group-description">{group.description}</span> | |||
</td> | |||
<td className="thin nowrap text-right"> | |||
{!group.default && ( | |||
<ActionsDropdown> | |||
<ActionsDropdownItem className="js-group-update" onClick={() => props.onEdit(group)}> | |||
{translate('update_details')} | |||
</ActionsDropdownItem> | |||
<ActionsDropdownDivider /> | |||
<ActionsDropdownItem | |||
className="js-group-delete" | |||
destructive={true} | |||
onClick={() => props.onDelete(group)}> | |||
{translate('delete')} | |||
</ActionsDropdownItem> | |||
</ActionsDropdown> | |||
)} | |||
</tr> | |||
); | |||
} | |||
</td> | |||
</tr> | |||
); | |||
} |
@@ -26,7 +26,7 @@ import { | |||
searchUsersGroups, | |||
updateGroup | |||
} from '../../../../api/user_groups'; | |||
import { mockOrganization } from '../../../../helpers/testMocks'; | |||
import { mockGroup } from '../../../../helpers/testMocks'; | |||
import App from '../App'; | |||
jest.mock('../../../../api/user_groups', () => ({ | |||
@@ -68,7 +68,7 @@ it('should render correctly', async () => { | |||
const wrapper = shallowRender(); | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchUsersGroups).toHaveBeenCalledWith({ organization: 'foo', q: '' }); | |||
expect(searchUsersGroups).toHaveBeenCalledWith({ q: '' }); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
@@ -79,23 +79,32 @@ it('should correctly handle creation', async () => { | |||
wrapper.instance().handleCreate({ description: 'Desc foo', name: 'foo' }); | |||
await waitAndUpdate(wrapper); | |||
expect(createGroup).toHaveBeenCalled(); | |||
expect(wrapper.state('groups')).toHaveLength(3); | |||
}); | |||
it('should correctly handle deletion', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state('groups')).toHaveLength(2); | |||
wrapper.instance().handleDelete('Members'); | |||
wrapper.setState({ groupToBeDeleted: mockGroup({ name: 'Members' }) }); | |||
wrapper.instance().handleDelete(); | |||
await waitAndUpdate(wrapper); | |||
expect(deleteGroup).toHaveBeenCalled(); | |||
expect(wrapper.state('groups')).toHaveLength(1); | |||
expect(wrapper.state().groupToBeDeleted).toBeUndefined(); | |||
}); | |||
it('should ignore deletion', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ groupToBeDeleted: undefined }); | |||
wrapper.instance().handleDelete(); | |||
expect(deleteGroup).not.toHaveBeenCalled(); | |||
}); | |||
it('should correctly handle edition', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.instance().handleEdit({ id: 1, description: 'foo', name: 'bar' }); | |||
wrapper.setState({ editedGroup: mockGroup({ id: 1, name: 'Owners' }) }); | |||
wrapper.instance().handleEdit({ description: 'foo', name: 'bar' }); | |||
await waitAndUpdate(wrapper); | |||
expect(updateGroup).toHaveBeenCalled(); | |||
expect(wrapper.state('groups')).toContainEqual({ | |||
@@ -107,12 +116,20 @@ it('should correctly handle edition', async () => { | |||
}); | |||
}); | |||
it('should ignore edition', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.setState({ editedGroup: undefined }); | |||
wrapper.instance().handleEdit({ description: 'nope', name: 'nuhuh' }); | |||
expect(updateGroup).not.toHaveBeenCalled(); | |||
}); | |||
it('should fetch more groups', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('ListFooter').prop<Function>('loadMore')(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchUsersGroups).toHaveBeenCalledWith({ organization: 'foo', p: 2, q: '' }); | |||
expect(searchUsersGroups).toHaveBeenCalledWith({ p: 2, q: '' }); | |||
expect(wrapper.state('groups')).toHaveLength(4); | |||
}); | |||
@@ -120,10 +137,55 @@ it('should search for groups', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
wrapper.find('SearchBox').prop<Function>('onChange')('foo'); | |||
expect(searchUsersGroups).toBeCalledWith({ organization: 'foo', q: 'foo' }); | |||
expect(searchUsersGroups).toBeCalledWith({ q: 'foo' }); | |||
expect(wrapper.state('query')).toBe('foo'); | |||
}); | |||
it('should handle edit modal', async () => { | |||
const editedGroup = mockGroup(); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().editedGroup).toBeUndefined(); | |||
wrapper.instance().openEditForm(editedGroup); | |||
expect(wrapper.state().editedGroup).toEqual(editedGroup); | |||
wrapper.instance().closeEditForm(); | |||
expect(wrapper.state().editedGroup).toBeUndefined(); | |||
}); | |||
it('should handle delete modal', async () => { | |||
const groupToBeDeleted = mockGroup(); | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
expect(wrapper.state().groupToBeDeleted).toBeUndefined(); | |||
wrapper.instance().openDeleteForm(groupToBeDeleted); | |||
expect(wrapper.state().groupToBeDeleted).toEqual(groupToBeDeleted); | |||
wrapper.instance().closeDeleteForm(); | |||
expect(wrapper.state().groupToBeDeleted).toBeUndefined(); | |||
}); | |||
it('should refresh correctly', async () => { | |||
const wrapper = shallowRender(); | |||
await waitAndUpdate(wrapper); | |||
const query = 'preserve me'; | |||
wrapper.setState({ paging: { pageIndex: 2, pageSize: 2, total: 5 }, query }); | |||
(searchUsersGroups as jest.Mock).mockClear(); | |||
wrapper.instance().refresh(); | |||
await waitAndUpdate(wrapper); | |||
expect(searchUsersGroups).toHaveBeenNthCalledWith(1, { organization: undefined, q: query }); | |||
expect(searchUsersGroups).toHaveBeenNthCalledWith(2, { organization: undefined, q: query, p: 2 }); | |||
}); | |||
function shallowRender(props: Partial<App['props']> = {}) { | |||
return shallow<App>(<App organization={mockOrganization()} {...props} />); | |||
return shallow<App>(<App {...props} />); | |||
} |
@@ -26,7 +26,7 @@ it('should edit members', () => { | |||
const group = { id: 3, name: 'Foo', membersCount: 5 }; | |||
const onEdit = jest.fn(); | |||
const wrapper = shallow(<EditMembers group={group} onEdit={onEdit} organization="org" />); | |||
const wrapper = shallow(<EditMembers group={group} onEdit={onEdit} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('ButtonIcon')); |
@@ -24,7 +24,6 @@ import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; | |||
import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../../api/user_groups'; | |||
import EditMembersModal from '../EditMembersModal'; | |||
const organization = 'orga'; | |||
const group = { id: 1, name: 'foo', membersCount: 1 }; | |||
jest.mock('../../../../api/user_groups', () => ({ | |||
@@ -68,7 +67,6 @@ it('should render modal properly', async () => { | |||
expect(getUsersInGroup).toHaveBeenCalledWith( | |||
expect.objectContaining({ | |||
name: group.name, | |||
organization, | |||
p: 1, | |||
ps: 100, | |||
q: undefined, | |||
@@ -88,7 +86,6 @@ it('should handle selection properly', async () => { | |||
expect(addUserToGroup).toHaveBeenCalledWith( | |||
expect.objectContaining({ | |||
name: group.name, | |||
organization, | |||
login: 'toto' | |||
}) | |||
); | |||
@@ -103,7 +100,6 @@ it('should handle deselection properly', async () => { | |||
expect(removeUserFromGroup).toHaveBeenCalledWith( | |||
expect.objectContaining({ | |||
name: group.name, | |||
organization, | |||
login: 'tata' | |||
}) | |||
); | |||
@@ -112,6 +108,6 @@ it('should handle deselection properly', async () => { | |||
function shallowRender(props: Partial<EditMembersModal['props']> = {}) { | |||
return shallow<EditMembersModal>( | |||
<EditMembersModal group={group} onClose={jest.fn()} organization={organization} {...props} /> | |||
<EditMembersModal group={group} onClose={jest.fn()} {...props} /> | |||
); | |||
} |
@@ -24,7 +24,7 @@ import Header from '../Header'; | |||
it('should create new group', () => { | |||
const onCreate = jest.fn(() => Promise.resolve()); | |||
const wrapper = shallow(<Header loading={false} onCreate={onCreate} />); | |||
const wrapper = shallow(<Header onCreate={onCreate} />); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('[id="groups-create"]')); |
@@ -45,7 +45,6 @@ function shallowRender(showAnyone = true) { | |||
onDelete={jest.fn()} | |||
onEdit={jest.fn()} | |||
onEditMembers={jest.fn()} | |||
organization="org" | |||
showAnyone={showAnyone} | |||
/> | |||
); |
@@ -19,60 +19,22 @@ | |||
*/ | |||
import { shallow } from 'enzyme'; | |||
import * as React from 'react'; | |||
import { click } from 'sonar-ui-common/helpers/testUtils'; | |||
import ListItem from '../ListItem'; | |||
import { mockGroup } from '../../../../helpers/testMocks'; | |||
import ListItem, { ListItemProps } from '../ListItem'; | |||
it('should edit group', () => { | |||
const group = { id: 3, name: 'Foo', membersCount: 5 }; | |||
const onEdit = jest.fn(); | |||
const wrapper = shallow( | |||
<ListItem | |||
group={group} | |||
onDelete={jest.fn()} | |||
onEdit={onEdit} | |||
onEditMembers={jest.fn()} | |||
organization="org" | |||
/> | |||
); | |||
click(wrapper.find('.js-group-update')); | |||
wrapper.update(); | |||
wrapper.find('Form').prop<Function>('onSubmit')({ name: 'Bar', description: 'bla bla' }); | |||
expect(onEdit).lastCalledWith({ description: 'bla bla', id: 3, name: 'Bar' }); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ group: mockGroup({ default: true }) })).toMatchSnapshot('default group'); | |||
}); | |||
it('should delete group', () => { | |||
const group = { id: 3, name: 'Foo', membersCount: 5 }; | |||
const onDelete = jest.fn(); | |||
const wrapper = shallow( | |||
function shallowRender(overrides: Partial<ListItemProps> = {}) { | |||
return shallow( | |||
<ListItem | |||
group={group} | |||
onDelete={onDelete} | |||
onEdit={jest.fn()} | |||
onEditMembers={jest.fn()} | |||
organization="org" | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
click(wrapper.find('.js-group-delete')); | |||
wrapper.update(); | |||
wrapper.find('DeleteForm').prop<Function>('onSubmit')(); | |||
expect(onDelete).toBeCalledWith('Foo'); | |||
}); | |||
it('should render default group', () => { | |||
const group = { default: true, id: 3, name: 'Foo', membersCount: 5 }; | |||
const wrapper = shallow( | |||
<ListItem | |||
group={group} | |||
group={mockGroup()} | |||
onDelete={jest.fn()} | |||
onEdit={jest.fn()} | |||
onEditMembers={jest.fn()} | |||
organization="org" | |||
{...overrides} | |||
/> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
} |
@@ -15,7 +15,6 @@ exports[`should render correctly 1`] = ` | |||
id="groups-page" | |||
> | |||
<Header | |||
loading={true} | |||
onCreate={[Function]} | |||
/> | |||
<SearchBox | |||
@@ -45,7 +44,6 @@ exports[`should render correctly 2`] = ` | |||
id="groups-page" | |||
> | |||
<Header | |||
loading={false} | |||
onCreate={[Function]} | |||
/> | |||
<SearchBox | |||
@@ -78,17 +76,17 @@ exports[`should render correctly 2`] = ` | |||
onDelete={[Function]} | |||
onEdit={[Function]} | |||
onEditMembers={[Function]} | |||
organization="foo" | |||
showAnyone={false} | |||
showAnyone={true} | |||
/> | |||
<div | |||
id="groups-list-footer" | |||
> | |||
<ListFooter | |||
count={2} | |||
count={3} | |||
loadMore={[Function]} | |||
loading={false} | |||
ready={true} | |||
total={4} | |||
total={5} | |||
/> | |||
</div> | |||
</div> |
@@ -3,8 +3,10 @@ | |||
exports[`should edit members 1`] = ` | |||
<Fragment> | |||
<ButtonIcon | |||
aria-label="groups.users.edit" | |||
className="button-small" | |||
onClick={[Function]} | |||
title="groups.users.edit" | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
@@ -14,8 +16,10 @@ exports[`should edit members 1`] = ` | |||
exports[`should edit members 2`] = ` | |||
<Fragment> | |||
<ButtonIcon | |||
aria-label="groups.users.edit" | |||
className="button-small" | |||
onClick={[Function]} | |||
title="groups.users.edit" | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> | |||
@@ -28,7 +32,6 @@ exports[`should edit members 2`] = ` | |||
} | |||
} | |||
onClose={[Function]} | |||
organization="org" | |||
/> | |||
</Fragment> | |||
`; | |||
@@ -36,8 +39,10 @@ exports[`should edit members 2`] = ` | |||
exports[`should edit members 3`] = ` | |||
<Fragment> | |||
<ButtonIcon | |||
aria-label="groups.users.edit" | |||
className="button-small" | |||
onClick={[Function]} | |||
title="groups.users.edit" | |||
> | |||
<BulletListIcon /> | |||
</ButtonIcon> |
@@ -11,10 +11,6 @@ exports[`should create new group 1`] = ` | |||
> | |||
user_groups.page | |||
</h1> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<div | |||
className="page-actions" | |||
> | |||
@@ -45,10 +41,6 @@ exports[`should create new group 2`] = ` | |||
> | |||
user_groups.page | |||
</h1> | |||
<DeferredSpinner | |||
loading={false} | |||
timeout={100} | |||
/> | |||
<div | |||
className="page-actions" | |||
> |
@@ -12,7 +12,8 @@ exports[`should render 1`] = ` | |||
<tr> | |||
<th /> | |||
<th | |||
className="nowrap" | |||
className="nowrap width-10" | |||
colSpan={2} | |||
> | |||
members | |||
</th> | |||
@@ -40,6 +41,7 @@ exports[`should render 1`] = ` | |||
</td> | |||
<td | |||
className="width-10" | |||
colSpan={2} | |||
/> | |||
<td | |||
className="width-40" | |||
@@ -62,11 +64,10 @@ exports[`should render 1`] = ` | |||
"name": "bar", | |||
} | |||
} | |||
key="3" | |||
key="bar" | |||
onDelete={[MockFunction]} | |||
onEdit={[MockFunction]} | |||
onEditMembers={[MockFunction]} | |||
organization="org" | |||
/> | |||
<ListItem | |||
group={ | |||
@@ -78,11 +79,10 @@ exports[`should render 1`] = ` | |||
"name": "foo", | |||
} | |||
} | |||
key="2" | |||
key="foo" | |||
onDelete={[MockFunction]} | |||
onEdit={[MockFunction]} | |||
onEditMembers={[MockFunction]} | |||
organization="org" | |||
/> | |||
<ListItem | |||
group={ | |||
@@ -94,11 +94,10 @@ exports[`should render 1`] = ` | |||
"name": "sonar-users", | |||
} | |||
} | |||
key="1" | |||
key="sonar-users" | |||
onDelete={[MockFunction]} | |||
onEdit={[MockFunction]} | |||
onEditMembers={[MockFunction]} | |||
organization="org" | |||
/> | |||
</tbody> | |||
</table> |
@@ -1,11 +1,11 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should delete group 1`] = ` | |||
exports[`should render correctly 1`] = ` | |||
<tr | |||
data-id={3} | |||
data-id="Foo" | |||
> | |||
<td | |||
className=" width-20" | |||
className="width-20" | |||
> | |||
<strong | |||
className="js-group-name" | |||
@@ -14,28 +14,23 @@ exports[`should delete group 1`] = ` | |||
</strong> | |||
</td> | |||
<td | |||
className="width-10" | |||
className="thin text-middle text-right little-padded-right" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
5 | |||
</span> | |||
<EditMembers | |||
group={ | |||
Object { | |||
"id": 3, | |||
"membersCount": 5, | |||
"name": "Foo", | |||
} | |||
1 | |||
</td> | |||
<td | |||
className="little-padded-left" | |||
> | |||
<EditMembers | |||
group={ | |||
Object { | |||
"id": 1, | |||
"membersCount": 1, | |||
"name": "Foo", | |||
} | |||
onEdit={[MockFunction]} | |||
organization="org" | |||
/> | |||
</div> | |||
} | |||
onEdit={[MockFunction]} | |||
/> | |||
</td> | |||
<td | |||
className="width-40" | |||
@@ -67,12 +62,12 @@ exports[`should delete group 1`] = ` | |||
</tr> | |||
`; | |||
exports[`should render default group 1`] = ` | |||
exports[`should render correctly: default group 1`] = ` | |||
<tr | |||
data-id={3} | |||
data-id="Foo" | |||
> | |||
<td | |||
className=" width-20" | |||
className="width-20" | |||
> | |||
<strong | |||
className="js-group-name" | |||
@@ -88,18 +83,13 @@ exports[`should render default group 1`] = ` | |||
</span> | |||
</td> | |||
<td | |||
className="width-10" | |||
className="thin text-middle text-right little-padded-right" | |||
> | |||
<div | |||
className="display-flex-center" | |||
> | |||
<span | |||
className="spacer-right" | |||
> | |||
5 | |||
</span> | |||
</div> | |||
1 | |||
</td> | |||
<td | |||
className="little-padded-left" | |||
/> | |||
<td | |||
className="width-40" | |||
> |
@@ -139,7 +139,7 @@ export default class ManageMemberGroupsForm extends React.PureComponent<Props, S | |||
<OrganizationGroupCheckbox | |||
checked={this.isGroupSelected(group.name)} | |||
group={group} | |||
key={group.id} | |||
key={group.name} | |||
onCheck={this.onCheck} | |||
/> | |||
))} |
@@ -1748,6 +1748,7 @@ users.search_description=Search users by login or name | |||
users.update=Update users | |||
users.update_details=Update details | |||
groups.users.edit=Change group members | |||
groups.remove=Remove group | |||
groups.remove.confirmation=Are you sure you want to remove group "{user}"? | |||