From: philippe-perrin-sonarsource Date: Thu, 20 Jun 2019 15:11:01 +0000 (+0200) Subject: SONAR-12210 Handle pagination in user & group list X-Git-Tag: 8.0~481 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c8aad7e3ccdb94828808d7b2780be2ad8ead3436;p=sonarqube.git SONAR-12210 Handle pagination in user & group list --- diff --git a/server/sonar-web/src/main/js/api/user_groups.ts b/server/sonar-web/src/main/js/api/user_groups.ts index a84be34b5ea..eae50459ee1 100644 --- a/server/sonar-web/src/main/js/api/user_groups.ts +++ b/server/sonar-web/src/main/js/api/user_groups.ts @@ -44,7 +44,7 @@ export function getUsersInGroup(data: { ps?: number; q?: string; selected?: string; -}): Promise<{ paging: T.Paging; users: GroupUser[] }> { +}): Promise { return getJSON('/api/user_groups/users', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/api/users.ts b/server/sonar-web/src/main/js/api/users.ts index 884a057da08..8daacb90b25 100644 --- a/server/sonar-web/src/main/js/api/users.ts +++ b/server/sonar-web/src/main/js/api/users.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { getJSON, post, postJSON, RequestData } from '../helpers/request'; +import { getJSON, post, postJSON } from '../helpers/request'; import throwGlobalError from '../app/utils/throwGlobalError'; export function getCurrentUser(): Promise { @@ -40,22 +40,14 @@ export interface UserGroup { selected: boolean; } -export function getUserGroups( - login: string, - organization?: string, - query?: string, - selected?: string -): Promise<{ paging: T.Paging; groups: UserGroup[] }> { - const data: RequestData = { login }; - if (organization) { - data.organization = organization; - } - if (query) { - data.q = query; - } - if (selected) { - data.selected = selected; - } +export function getUserGroups(data: { + login: string; + organization?: string; + p?: number; + ps?: number; + q?: string; + selected?: string; +}): Promise<{ paging: T.Paging; groups: UserGroup[] }> { return getJSON('/api/users/groups', data); } diff --git a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx index 14b4c813d14..9b475b87c6a 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx @@ -17,19 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as React from 'react'; import { find, without } from 'lodash'; -import Modal from '../../../components/controls/Modal'; -import SelectList, { Filter } from '../../../components/SelectList/SelectList'; -import { ResetButtonLink } from '../../../components/ui/buttons'; -import { translate } from '../../../helpers/l10n'; +import * as React from 'react'; import { - GroupUser, - removeUserFromGroup, addUserToGroup, - getUsersInGroup + getUsersInGroup, + GroupUser, + removeUserFromGroup } from '../../../api/user_groups'; import DeferredSpinner from '../../../components/common/DeferredSpinner'; +import Modal from '../../../components/controls/Modal'; +import SelectList, { Filter } from '../../../components/SelectList/SelectList'; +import { ResetButtonLink } from '../../../components/ui/buttons'; +import { translate } from '../../../helpers/l10n'; interface Props { group: T.Group; @@ -37,39 +37,83 @@ interface Props { organization: string | undefined; } +export interface SearchParams { + name: string; + organization?: string; + page: number; + pageSize: number; + query?: string; + selected: string; +} + interface State { + lastSearchParams: SearchParams; + listHasBeenTouched: boolean; loading: boolean; users: GroupUser[]; + usersTotalCount?: number; selectedUsers: string[]; } +const PAGE_SIZE = 100; + export default class EditMembers extends React.PureComponent { mounted = false; - state: State = { loading: true, users: [], selectedUsers: [] }; + + constructor(props: Props) { + super(props); + + this.state = { + lastSearchParams: { + name: props.group.name, + organization: props.organization, + page: 1, + pageSize: PAGE_SIZE, + query: '', + selected: Filter.Selected + }, + listHasBeenTouched: false, + loading: true, + users: [], + selectedUsers: [] + }; + } componentDidMount() { this.mounted = true; - this.handleSearch('', Filter.Selected); + this.fetchUsers(this.state.lastSearchParams); } componentWillUnmount() { this.mounted = false; } - handleSearch = (query: string, selected: Filter) => { - return getUsersInGroup({ - name: this.props.group.name, - organization: this.props.organization, - ps: 100, - q: query !== '' ? query : undefined, - selected + fetchUsers = (searchParams: SearchParams, more?: boolean) => + getUsersInGroup({ + ...searchParams, + p: searchParams.page, + ps: searchParams.pageSize, + q: searchParams.query !== '' ? searchParams.query : undefined }).then( data => { if (this.mounted) { - this.setState({ - loading: false, - users: data.users, - selectedUsers: data.users.filter(user => user.selected).map(user => user.login) + this.setState(prevState => { + const users = more ? [...prevState.users, ...data.users] : data.users; + const newSelectedUsers = data.users + .filter(user => user.selected) + .map(user => user.login); + const selectedUsers = more + ? [...prevState.selectedUsers, ...newSelectedUsers] + : newSelectedUsers; + + return { + lastSearchParams: searchParams, + listHasBeenTouched: false, + loading: false, + users, + usersTotalCount: data.total, + selectedUsers + }; }); } }, @@ -79,35 +123,57 @@ export default class EditMembers extends React.PureComponent { } } ); - }; - handleSelect = (login: string) => { - return addUserToGroup({ + handleLoadMore = () => + this.fetchUsers( + { + ...this.state.lastSearchParams, + page: this.state.lastSearchParams.page + 1 + }, + true + ); + + handleReload = () => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1 + }); + + handleSearch = (query: string, selected: Filter) => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1, + query, + selected + }); + + handleSelect = (login: string) => + addUserToGroup({ name: this.props.group.name, login, organization: this.props.organization }).then(() => { if (this.mounted) { this.setState((state: State) => ({ + listHasBeenTouched: true, selectedUsers: [...state.selectedUsers, login] })); } }); - }; - handleUnselect = (login: string) => { - return removeUserFromGroup({ + handleUnselect = (login: string) => + removeUserFromGroup({ name: this.props.group.name, login, organization: this.props.organization }).then(() => { if (this.mounted) { this.setState((state: State) => ({ + listHasBeenTouched: true, selectedUsers: without(state.selectedUsers, login) })); } }); - }; renderElement = (login: string): React.ReactNode => { const user = find(this.state.users, { login }); @@ -138,6 +204,12 @@ export default class EditMembers extends React.PureComponent { user.login)} + elementsTotalCount={this.state.usersTotalCount} + needReload={ + this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All + } + onLoadMore={this.handleLoadMore} + onReload={this.handleReload} onSearch={this.handleSearch} onSelect={this.handleSelect} onUnselect={this.handleUnselect} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx index 1331cff110c..82505385a2c 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembers-test.tsx @@ -36,6 +36,7 @@ it('should edit members', async () => { expect(wrapper).toMatchSnapshot(); await waitAndUpdate(wrapper); + click(wrapper.find('ResetButtonLink')); expect(onEdit).toBeCalled(); expect(wrapper).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx index 6ee23a8ecb9..88209b8e099 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/EditMembersModal-test.tsx @@ -17,15 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -/* eslint-disable import/first, import/order */ -import * as React from 'react'; import { shallow } from 'enzyme'; -import EditMembersModal from '../EditMembersModal'; +import * as React from 'react'; +import EditMembersModal, { SearchParams } from '../EditMembersModal'; +import SelectList, { Filter } from '../../../../components/SelectList/SelectList'; import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getUsersInGroup, addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups'; jest.mock('../../../../api/user_groups', () => ({ getUsersInGroup: jest.fn().mockResolvedValue({ - paging: { pageIndex: 0, pageSize: 10, total: 0 }, + paging: { pageIndex: 1, pageSize: 10, total: 1 }, users: [ { login: 'foo', @@ -33,20 +34,109 @@ jest.mock('../../../../api/user_groups', () => ({ selected: true } ] - }) + }), + addUserToGroup: jest.fn().mockResolvedValue({}), + removeUserFromGroup: jest.fn().mockResolvedValue({}) })); -const getUsersInGroup = require('../../../../api/user_groups').getUsersInGroup as jest.Mock; - -const group = { id: 1, name: 'foo', membersCount: 1 }; +beforeEach(() => { + jest.clearAllMocks(); +}); -it('should render modal', async () => { - getUsersInGroup.mockClear(); +it('should render modal properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); - const wrapper = shallow( {}} organization="bar" />); expect(wrapper).toMatchSnapshot(); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + + wrapper.setState({ listHasBeenTouched: true }); + expect(wrapper.find(SelectList).props().needReload).toBe(true); + + wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams }); + expect(wrapper.find(SelectList).props().needReload).toBe(false); +}); +it('should handle reload properly', async () => { + const wrapper = shallowRender(); await waitAndUpdate(wrapper); - expect(getUsersInGroup).toHaveBeenCalledTimes(1); - expect(wrapper).toMatchSnapshot(); + + wrapper.instance().handleReload(); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); }); + +it('should handle search reload properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSearch('foo', Filter.Selected); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1, + q: 'foo', + selected: Filter.Selected + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle load more properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleLoadMore(); + expect(getUsersInGroup).toHaveBeenCalledWith( + expect.objectContaining({ + p: 2 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle selection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSelect('toto'); + await waitAndUpdate(wrapper); + expect(addUserToGroup).toHaveBeenCalledWith( + expect.objectContaining({ + login: 'toto' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +it('should handle deselection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleUnselect('tata'); + await waitAndUpdate(wrapper); + expect(removeUserFromGroup).toHaveBeenCalledWith( + expect.objectContaining({ + login: 'tata' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap index 98241e10f0c..a2651397aff 100644 --- a/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/groups/components/__tests__/__snapshots__/EditMembers-test.tsx.snap @@ -311,6 +311,9 @@ exports[`should edit members 2`] = ` > -
-

- users.update -

-
-
- - - -
-
- - Done - -
- -`; - -exports[`should render modal 2`] = ` -
Done diff --git a/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx b/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx index fbc68ce2d8f..4355c2ab882 100644 --- a/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx +++ b/server/sonar-web/src/main/js/apps/organizationMembers/ManageMemberGroupsForm.tsx @@ -58,7 +58,10 @@ export default class ManageMemberGroupsForm extends React.PureComponent { this.setState({ loading: true }); - getUserGroups(this.props.member.login, this.props.organization.key).then( + getUserGroups({ + login: this.props.member.login, + organization: this.props.organization.key + }).then( response => { if (this.mounted) { this.setState({ loading: false, userGroups: keyBy(response.groups, 'name') }); diff --git a/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx index 3e4b4554f43..b1cbfbd4f69 100644 --- a/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx +++ b/server/sonar-web/src/main/js/apps/users/components/GroupsForm.tsx @@ -17,13 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as React from 'react'; import { find, without } from 'lodash'; +import * as React from 'react'; +import { getUserGroups, UserGroup } from '../../../api/users'; +import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups'; import Modal from '../../../components/controls/Modal'; import SelectList, { Filter } from '../../../components/SelectList/SelectList'; import { translate } from '../../../helpers/l10n'; -import { getUserGroups, UserGroup } from '../../../api/users'; -import { addUserToGroup, removeUserFromGroup } from '../../../api/user_groups'; interface Props { onClose: () => void; @@ -31,47 +31,130 @@ interface Props { user: T.User; } +export interface SearchParams { + login: string; + organization?: string; + page: number; + pageSize: number; + query?: string; + selected: string; +} + interface State { groups: UserGroup[]; + groupsTotalCount?: number; + lastSearchParams: SearchParams; + listHasBeenTouched: boolean; selectedGroups: string[]; } -export default class GroupsForm extends React.PureComponent { - container?: HTMLDivElement | null; - state: State = { groups: [], selectedGroups: [] }; +const PAGE_SIZE = 100; + +export default class GroupsForm extends React.PureComponent { + mounted = false; + + constructor(props: Props) { + super(props); + + this.state = { + groups: [], + lastSearchParams: { + login: props.user.login, + page: 1, + pageSize: PAGE_SIZE, + query: '', + selected: Filter.Selected + }, + listHasBeenTouched: false, + selectedGroups: [] + }; + } componentDidMount() { - this.handleSearch('', Filter.Selected); + this.mounted = true; + this.fetchUsers(this.state.lastSearchParams); } - handleSearch = (query: string, selected: Filter) => { - return getUserGroups(this.props.user.login, undefined, query, selected).then(data => { - this.setState({ - groups: data.groups, - selectedGroups: data.groups.filter(group => group.selected).map(group => group.name) - }); + componentWillUnmount() { + this.mounted = false; + } + + fetchUsers = (searchParams: SearchParams, more?: boolean) => + getUserGroups({ + login: searchParams.login, + organization: searchParams.organization !== '' ? searchParams.organization : undefined, + p: searchParams.page, + ps: searchParams.pageSize, + q: searchParams.query !== '' ? searchParams.query : undefined, + selected: searchParams.selected + }).then(data => { + if (this.mounted) { + this.setState(prevState => { + const groups = more ? [...prevState.groups, ...data.groups] : data.groups; + const newSeletedGroups = data.groups.filter(gp => gp.selected).map(gp => gp.name); + const selectedGroups = more + ? [...prevState.selectedGroups, ...newSeletedGroups] + : newSeletedGroups; + + return { + lastSearchParams: searchParams, + listHasBeenTouched: false, + groups, + groupsTotalCount: data.paging.total, + selectedGroups + }; + }); + } }); - }; - handleSelect = (name: string) => { - return addUserToGroup({ + handleLoadMore = () => + this.fetchUsers( + { + ...this.state.lastSearchParams, + page: this.state.lastSearchParams.page + 1 + }, + true + ); + + handleReload = () => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1 + }); + + handleSearch = (query: string, selected: Filter) => + this.fetchUsers({ + ...this.state.lastSearchParams, + page: 1, + query, + selected + }); + + handleSelect = (name: string) => + addUserToGroup({ name, login: this.props.user.login }).then(() => { - this.setState((state: State) => ({ selectedGroups: [...state.selectedGroups, name] })); + if (this.mounted) { + this.setState((state: State) => ({ + listHasBeenTouched: true, + selectedGroups: [...state.selectedGroups, name] + })); + } }); - }; - handleUnselect = (name: string) => { - return removeUserFromGroup({ + handleUnselect = (name: string) => + removeUserFromGroup({ name, login: this.props.user.login }).then(() => { - this.setState((state: State) => ({ - selectedGroups: without(state.selectedGroups, name) - })); + if (this.mounted) { + this.setState((state: State) => ({ + listHasBeenTouched: true, + selectedGroups: without(state.selectedGroups, name) + })); + } }); - }; handleCloseClick = (event: React.SyntheticEvent) => { event.preventDefault(); @@ -112,6 +195,12 @@ export default class GroupsForm extends React.PureComponent {
group.name)} + elementsTotalCount={this.state.groupsTotalCount} + needReload={ + this.state.listHasBeenTouched && this.state.lastSearchParams.selected !== Filter.All + } + onLoadMore={this.handleLoadMore} + onReload={this.handleReload} onSearch={this.handleSearch} onSelect={this.handleSelect} onUnselect={this.handleUnselect} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx b/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx new file mode 100644 index 00000000000..6c86567a743 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/GroupsForm-test.tsx @@ -0,0 +1,155 @@ +/* + * SonarQube + * Copyright (C) 2009-2019 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { shallow } from 'enzyme'; +import * as React from 'react'; +import GroupsForm, { SearchParams } from '../GroupsForm'; +import SelectList, { Filter } from '../../../../components/SelectList/SelectList'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; +import { getUserGroups } from '../../../../api/users'; +import { addUserToGroup, removeUserFromGroup } from '../../../../api/user_groups'; +import { mockUser } from '../../../../helpers/testMocks'; + +jest.mock('../../../../api/users', () => ({ + getUserGroups: jest.fn().mockResolvedValue({ + paging: { pageIndex: 1, pageSize: 10, total: 1 }, + groups: [ + { + id: 1001, + name: 'test1', + description: 'test1', + selected: true + }, + { + id: 1002, + name: 'test2', + description: 'test2', + selected: true + }, + { + id: 1003, + name: 'test3', + description: 'test3', + selected: false + } + ] + }) +})); + +jest.mock('../../../../api/user_groups', () => ({ + addUserToGroup: jest.fn().mockResolvedValue({}), + removeUserFromGroup: jest.fn().mockResolvedValue({}) +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +it('should render correctly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + expect(wrapper).toMatchSnapshot(); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + + wrapper.setState({ listHasBeenTouched: true }); + expect(wrapper.find(SelectList).props().needReload).toBe(true); + + wrapper.setState({ lastSearchParams: { selected: Filter.All } as SearchParams }); + expect(wrapper.find(SelectList).props().needReload).toBe(false); +}); + +it('should handle reload properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleReload(); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle search reload properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSearch('foo', Filter.Selected); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 1, + q: 'foo', + selected: Filter.Selected + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle load more properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleLoadMore(); + expect(getUserGroups).toHaveBeenCalledWith( + expect.objectContaining({ + p: 2 + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(false); +}); + +it('should handle selection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleSelect('toto'); + await waitAndUpdate(wrapper); + expect(addUserToGroup).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'toto' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +it('should handle deselection properly', async () => { + const wrapper = shallowRender(); + await waitAndUpdate(wrapper); + + wrapper.instance().handleUnselect('tata'); + await waitAndUpdate(wrapper); + expect(removeUserFromGroup).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'tata' + }) + ); + expect(wrapper.state().listHasBeenTouched).toBe(true); +}); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap new file mode 100644 index 00000000000..a544c913d55 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/users/components/__tests__/__snapshots__/GroupsForm-test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render correctly 1`] = ` + +
+

+ users.update_groups +

+
+
+ +
+ +
+`;