]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16238 Fix QP permission Select behavior
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 5 Apr 2022 16:01:14 +0000 (18:01 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 6 Apr 2022 20:03:00 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsForm-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsFormSelect-test.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsForm-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfilePermissionsFormSelect-test.tsx.snap

index a8de3c4f43683a354b840f1b3cf1595c960ada2f..7cd6f079125f42c83e115b8b9a07b4b1cf81236d 100644 (file)
@@ -37,7 +37,7 @@ beforeAll(() => {
 
 afterEach(() => handler.reset());
 
-jest.setTimeout(10_000);
+jest.setTimeout(20_000);
 
 it('should list all rules', async () => {
   renderCodingRulesApp();
index 96035b0c7c4dcfa2cb539920832449b24103d5ca..420a235e5876f400908113e17a1349ae534a8663 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
-import {
-  addGroup,
-  addUser,
-  searchGroups,
-  searchUsers,
-  SearchUsersGroupsParameters
-} from '../../../api/quality-profiles';
+import { addGroup, addUser } from '../../../api/quality-profiles';
 import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import Modal from '../../../components/controls/Modal';
 import { translate } from '../../../helpers/l10n';
@@ -97,28 +91,12 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
     }
   };
 
-  handleSearch = (q: string) => {
-    const { profile } = this.props;
-    const parameters: SearchUsersGroupsParameters = {
-      language: profile.language,
-      q,
-      qualityProfile: profile.name,
-      selected: 'deselected'
-    };
-    return Promise.all([
-      searchUsers(parameters),
-      searchGroups(parameters)
-    ]).then(([usersResponse, groupsResponse]) => [
-      ...usersResponse.users,
-      ...groupsResponse.groups
-    ]);
-  };
-
   handleValueChange = (selected: UserSelected | Group) => {
     this.setState({ selected });
   };
 
   render() {
+    const { profile } = this.props;
     const header = translate('quality_profiles.grant_permissions_to_user_or_group');
     const submitDisabled = !this.state.selected || this.state.submitting;
     return (
@@ -132,11 +110,7 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
               <label htmlFor="change-profile-permission-input">
                 {translate('quality_profiles.search_description')}
               </label>
-              <ProfilePermissionsFormSelect
-                onChange={this.handleValueChange}
-                onSearch={this.handleSearch}
-                selected={this.state.selected}
-              />
+              <ProfilePermissionsFormSelect onChange={this.handleValueChange} profile={profile} />
             </div>
           </div>
           <footer className="modal-foot">
index 4ffb8d67b0b902074bacbb89671a9339af17f299..cb4d7abef7b390323f0a3bc560aafa25c5726ff7 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { debounce, identity, omit } from 'lodash';
+import { debounce, omit } from 'lodash';
 import * as React from 'react';
 import { components, ControlProps, OptionProps, SingleValueProps } from 'react-select';
-import Select from '../../../components/controls/Select';
+import {
+  searchGroups,
+  searchUsers,
+  SearchUsersGroupsParameters
+} from '../../../api/quality-profiles';
+import { SearchSelect } from '../../../components/controls/Select';
 import GroupIcon from '../../../components/icons/GroupIcon';
 import Avatar from '../../../components/ui/Avatar';
 import { translate } from '../../../helpers/l10n';
@@ -32,58 +37,17 @@ type OptionWithValue = Option & { value: string };
 
 interface Props {
   onChange: (option: OptionWithValue) => void;
-  onSearch: (query: string) => Promise<Option[]>;
-  selected?: Option;
+  profile: { language: string; name: string };
 }
 
-interface State {
-  loading: boolean;
-  query: string;
-  searchResults: Option[];
-}
-
-export default class ProfilePermissionsFormSelect extends React.PureComponent<Props, State> {
+export default class ProfilePermissionsFormSelect extends React.PureComponent<Props> {
   mounted = false;
 
   constructor(props: Props) {
     super(props);
     this.handleSearch = debounce(this.handleSearch, 250);
-    this.state = { loading: false, query: '', searchResults: [] };
-  }
-
-  componentDidMount() {
-    this.mounted = true;
-    this.handleSearch(this.state.query);
   }
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleSearch = (query: string) => {
-    this.setState({ loading: true });
-    this.props.onSearch(query).then(
-      searchResults => {
-        if (this.mounted) {
-          this.setState({ loading: false, searchResults });
-        }
-      },
-      () => {
-        if (this.mounted) {
-          this.setState({ loading: false });
-        }
-      }
-    );
-  };
-
-  handleInputChange = (newQuery: string) => {
-    const { query } = this.state;
-    if (query !== newQuery) {
-      this.setState({ query: newQuery });
-      this.handleSearch(newQuery);
-    }
-  };
-
   optionRenderer(props: OptionProps<OptionWithValue, false>) {
     const { data } = props;
     return (
@@ -105,44 +69,41 @@ export default class ProfilePermissionsFormSelect extends React.PureComponent<Pr
     </components.Control>
   );
 
+  handleSearch = (q: string, resolve: (options: OptionWithValue[]) => void) => {
+    const { profile } = this.props;
+    const parameters: SearchUsersGroupsParameters = {
+      language: profile.language,
+      q,
+      qualityProfile: profile.name,
+      selected: 'deselected'
+    };
+    Promise.all([searchUsers(parameters), searchGroups(parameters)])
+      .then(([usersResponse, groupsResponse]) => [...usersResponse.users, ...groupsResponse.groups])
+      .then((options: Option[]) => options.map(opt => ({ ...opt, value: getStringValue(opt) })))
+      .then(resolve)
+      .catch(() => resolve([]));
+  };
+
   render() {
     const noResultsText = translate('no_results');
-    const { selected } = this.props;
-    // create a uniq string both for users and groups
-    const options = this.state.searchResults.map(r => ({ ...r, value: getStringValue(r) }));
-
-    // when user input is empty the options shows only top 30 names
-    // the below code add the selected user so that it appears too
-    if (
-      selected !== undefined &&
-      options.find(o => o.value === getStringValue(selected)) === undefined
-    ) {
-      options.unshift({ ...selected, value: getStringValue(selected) });
-    }
 
     return (
-      <Select
+      <SearchSelect
         className="Select-big width-100"
         autoFocus={true}
         isClearable={false}
         id="change-profile-permission"
         inputId="change-profile-permission-input"
         onChange={this.props.onChange}
-        onInputChange={this.handleInputChange}
+        defaultOptions={true}
+        loadOptions={this.handleSearch}
         placeholder=""
         noOptionsMessage={() => noResultsText}
-        isLoading={this.state.loading}
-        options={options}
-        isSearchable={true}
-        filterOptions={identity}
         components={{
           Option: this.optionRenderer,
           SingleValue: this.singleValueRenderer,
           Control: this.controlRenderer
         }}
-        value={options.filter(
-          o => o.value === (this.props.selected && getStringValue(this.props.selected))
-        )}
       />
     );
   }
index 0ceac094fbfa02b9e3b34892a2c6b7b74a51d1ae..0a1943966cb9269a31d4dd44040e3b69118e0cc2 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
-import { addGroup, addUser, searchGroups, searchUsers } from '../../../../api/quality-profiles';
+import { addGroup, addUser } from '../../../../api/quality-profiles';
 import { mockGroup, mockUser } from '../../../../helpers/testMocks';
 import { submit, waitAndUpdate } from '../../../../helpers/testUtils';
 import { UserSelected } from '../../../../types/types';
 import ProfilePermissionsForm from '../ProfilePermissionsForm';
-import ProfilePermissionsFormSelect from '../ProfilePermissionsFormSelect';
 
 jest.mock('../../../../api/quality-profiles', () => ({
   addUser: jest.fn().mockResolvedValue(null),
@@ -46,7 +45,7 @@ it('correctly adds users', async () => {
 
   const user: UserSelected = { ...mockUser(), name: 'John doe', active: true, selected: true };
   wrapper.instance().handleValueChange(user);
-  expect(wrapper.find(ProfilePermissionsFormSelect).prop('selected')).toBe(user);
+  expect(wrapper.state().selected).toBe(user);
 
   submit(wrapper.find('form'));
   expect(wrapper).toMatchSnapshot();
@@ -68,7 +67,7 @@ it('correctly adds groups', async () => {
 
   const group = mockGroup();
   wrapper.instance().handleValueChange(group);
-  expect(wrapper.find(ProfilePermissionsFormSelect).prop('selected')).toBe(group);
+  expect(wrapper.state().selected).toBe(group);
 
   submit(wrapper.find('form'));
   expect(wrapper).toMatchSnapshot();
@@ -84,21 +83,6 @@ it('correctly adds groups', async () => {
   expect(onGroupAdd).toBeCalledWith(group);
 });
 
-it('correctly handles search', () => {
-  const wrapper = shallowRender();
-  wrapper.instance().handleSearch('foo');
-
-  const parameters = {
-    language: PROFILE.language,
-    q: 'foo',
-    qualityProfile: PROFILE.name,
-    selected: 'deselected'
-  };
-
-  expect(searchUsers).toBeCalledWith(parameters);
-  expect(searchGroups).toBeCalledWith(parameters);
-});
-
 function shallowRender(props: Partial<ProfilePermissionsForm['props']> = {}) {
   return shallow<ProfilePermissionsForm>(
     <ProfilePermissionsForm
index 1060121d1a261f17eb8cdd88985d655cd6c52296..53b417a26809ad3524e737e01f55a35440258271 100644 (file)
  */
 import { shallow } from 'enzyme';
 import * as React from 'react';
+import { searchGroups, searchUsers } from '../../../../api/quality-profiles';
 import {
   mockReactSelectControlProps,
   mockReactSelectOptionProps
 } from '../../../../helpers/mocks/react-select';
+import { mockUser } from '../../../../helpers/testMocks';
 import ProfilePermissionsFormSelect from '../ProfilePermissionsFormSelect';
 
 jest.mock('lodash', () => {
@@ -31,40 +33,28 @@ jest.mock('lodash', () => {
   return lodash;
 });
 
-it('renders', () => {
-  expect(
-    shallow(
-      <ProfilePermissionsFormSelect
-        onChange={jest.fn()}
-        onSearch={jest.fn(() => Promise.resolve([]))}
-        selected={{ name: 'lambda' }}
-      />
-    )
-  ).toMatchSnapshot();
-});
+jest.mock('../../../../api/quality-profiles', () => ({
+  searchGroups: jest.fn().mockResolvedValue([]),
+  searchUsers: jest.fn().mockResolvedValue([])
+}));
 
-it('searches', () => {
-  const onSearch = jest.fn(() => Promise.resolve([]));
-  const wrapper = shallow(
-    <ProfilePermissionsFormSelect
-      onChange={jest.fn()}
-      onSearch={onSearch}
-      selected={{ name: 'lambda' }}
-    />
-  );
-  expect(onSearch).toBeCalledWith('');
-  onSearch.mockClear();
-
-  wrapper.prop<Function>('onInputChange')('f');
-  expect(onSearch).toBeCalled();
+it('should render correctly', () => {
+  expect(shallowRender()).toMatchSnapshot();
+});
 
-  wrapper.prop<Function>('onInputChange')('foo');
-  expect(onSearch).toBeCalledWith('foo');
+it('should handle search', async () => {
+  (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [mockUser()] });
+  (searchGroups as jest.Mock).mockResolvedValueOnce({ groups: [{ name: 'group1' }] });
 
-  onSearch.mockClear();
+  const wrapper = shallowRender();
+  const query = 'Waldo';
+  const results = await new Promise(resolve => {
+    wrapper.instance().handleSearch(query, resolve);
+  });
+  expect(searchUsers).toBeCalledWith(expect.objectContaining({ q: query }));
+  expect(searchGroups).toBeCalledWith(expect.objectContaining({ q: query }));
 
-  wrapper.prop<Function>('onInputChange')('foo');
-  expect(onSearch).not.toBeCalled();
+  expect(results).toHaveLength(2);
 });
 
 it('should render option correctly', () => {
@@ -95,8 +85,7 @@ function shallowRender(overrides: Partial<ProfilePermissionsFormSelect['props']>
   return shallow<ProfilePermissionsFormSelect>(
     <ProfilePermissionsFormSelect
       onChange={jest.fn()}
-      onSearch={jest.fn(() => Promise.resolve([]))}
-      selected={{ name: 'lambda' }}
+      profile={{ language: 'Java', name: 'Sonar Way' }}
       {...overrides}
     />
   );
index f76052be05dfc22bf108179c3afc7b195980043b..75208b639c3250c5fa372bf973037fcb35fa739e 100644 (file)
@@ -28,12 +28,10 @@ exports[`correctly adds groups 1`] = `
         </label>
         <ProfilePermissionsFormSelect
           onChange={[Function]}
-          onSearch={[Function]}
-          selected={
+          profile={
             Object {
-              "id": 1,
-              "membersCount": 1,
-              "name": "Foo",
+              "language": "js",
+              "name": "Sonar way",
             }
           }
         />
@@ -88,14 +86,10 @@ exports[`correctly adds users 1`] = `
         </label>
         <ProfilePermissionsFormSelect
           onChange={[Function]}
-          onSearch={[Function]}
-          selected={
+          profile={
             Object {
-              "active": true,
-              "local": true,
-              "login": "john.doe",
-              "name": "John doe",
-              "selected": true,
+              "language": "js",
+              "name": "Sonar way",
             }
           }
         />
@@ -150,7 +144,12 @@ exports[`should render correctly: default 1`] = `
         </label>
         <ProfilePermissionsFormSelect
           onChange={[Function]}
-          onSearch={[Function]}
+          profile={
+            Object {
+              "language": "js",
+              "name": "Sonar way",
+            }
+          }
         />
       </div>
     </div>
@@ -200,7 +199,12 @@ exports[`should render correctly: submitting 1`] = `
         </label>
         <ProfilePermissionsFormSelect
           onChange={[Function]}
-          onSearch={[Function]}
+          profile={
+            Object {
+              "language": "js",
+              "name": "Sonar way",
+            }
+          }
         />
       </div>
     </div>
index 1d8007c3944105f17969e7ceeced0da5e6f744f8..35b8f64412ebc93b67fbe08a7bafa4429452de92 100644 (file)
@@ -1,7 +1,13 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`renders 1`] = `
-<Select
+exports[`should render control correctly: control renderer 1`] = `
+<Control
+  className="abs-height-100 Select-control"
+/>
+`;
+
+exports[`should render correctly 1`] = `
+<SearchSelect
   autoFocus={true}
   className="Select-big width-100"
   components={
@@ -11,38 +17,14 @@ exports[`renders 1`] = `
       "SingleValue": [Function],
     }
   }
-  filterOptions={[Function]}
+  defaultOptions={true}
   id="change-profile-permission"
   inputId="change-profile-permission-input"
   isClearable={false}
-  isLoading={true}
-  isSearchable={true}
+  loadOptions={[Function]}
   noOptionsMessage={[Function]}
   onChange={[MockFunction]}
-  onInputChange={[Function]}
-  options={
-    Array [
-      Object {
-        "name": "lambda",
-        "value": "group:lambda",
-      },
-    ]
-  }
   placeholder=""
-  value={
-    Array [
-      Object {
-        "name": "lambda",
-        "value": "group:lambda",
-      },
-    ]
-  }
-/>
-`;
-
-exports[`should render control correctly: control renderer 1`] = `
-<Control
-  className="abs-height-100 Select-control"
 />
 `;