Browse Source

SONAR-13772 Fix group deletion

SONAR-13773 Fix group pagination
tags/8.5.0.37579
Jeremy Davis 3 years ago
parent
commit
6885b465d9
20 changed files with 300 additions and 322 deletions
  1. 2
    2
      server/sonar-web/src/main/js/api/user_groups.ts
  2. 115
    51
      server/sonar-web/src/main/js/apps/groups/components/App.tsx
  3. 7
    7
      server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx
  4. 2
    6
      server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
  5. 0
    4
      server/sonar-web/src/main/js/apps/groups/components/Header.tsx
  6. 7
    7
      server/sonar-web/src/main/js/apps/groups/components/List.tsx
  7. 39
    121
      server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx
  8. 71
    9
      server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx
  9. 1
    1
      server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx
  10. 1
    5
      server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx
  11. 1
    1
      server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx
  12. 0
    1
      server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx
  13. 10
    48
      server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx
  14. 4
    6
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap
  15. 6
    1
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap
  16. 0
    8
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap
  17. 6
    7
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap
  18. 26
    36
      server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap
  19. 1
    1
      server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx
  20. 1
    0
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 2
- 2
server/sonar-web/src/main/js/api/user_groups.ts View File

@@ -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);
}

+ 115
- 51
server/sonar-web/src/main/js/apps/groups/components/App.tsx View File

@@ -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>
</>
);

+ 7
- 7
server/sonar-web/src/main/js/apps/groups/components/EditMembers.tsx View File

@@ -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} />
)}
</>
);

+ 2
- 6
server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx View File

@@ -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) => ({

+ 0
- 4
server/sonar-web/src/main/js/apps/groups/components/Header.tsx View File

@@ -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')}

+ 7
- 7
server/sonar-web/src/main/js/apps/groups/components/List.tsx View File

@@ -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>

+ 39
- 121
server/sonar-web/src/main/js/apps/groups/components/ListItem.tsx View File

@@ -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>
);
}

+ 71
- 9
server/sonar-web/src/main/js/apps/groups/components/__tests__/App-test.tsx View File

@@ -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} />);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx View File

@@ -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'));

+ 1
- 5
server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx View File

@@ -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} />
);
}

+ 1
- 1
server/sonar-web/src/main/js/apps/groups/components/__tests__/Header-test.tsx View File

@@ -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"]'));

+ 0
- 1
server/sonar-web/src/main/js/apps/groups/components/__tests__/List-test.tsx View File

@@ -45,7 +45,6 @@ function shallowRender(showAnyone = true) {
onDelete={jest.fn()}
onEdit={jest.fn()}
onEditMembers={jest.fn()}
organization="org"
showAnyone={showAnyone}
/>
);

+ 10
- 48
server/sonar-web/src/main/js/apps/groups/components/__tests__/ListItem-test.tsx View File

@@ -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();
});
}

+ 4
- 6
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -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>

+ 6
- 1
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap View File

@@ -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>

+ 0
- 8
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/Header-test.tsx.snap View File

@@ -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"
>

+ 6
- 7
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/List-test.tsx.snap View File

@@ -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>

+ 26
- 36
server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/ListItem-test.tsx.snap View File

@@ -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"
>

+ 1
- 1
server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx View File

@@ -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}
/>
))}

+ 1
- 0
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -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}"?


Loading…
Cancel
Save