]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22418 Migrate QualityGatePermissionsAddModal
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 26 Jun 2024 15:24:41 +0000 (17:24 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 10 Jul 2024 20:02:38 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/apps/quality-gates/utils.ts

index 865e0dee61d74b8da689220e07aa8d4a284b19a2..3998b6d2507b4c88307145cbe1e0fcb49b85bb03 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 { LabelValueSelectOption } from 'design-system';
 import { debounce } from 'lodash';
 import * as React from 'react';
-import { Options } from 'react-select';
 import { searchGroups, searchUsers } from '../../../api/quality-gates';
 import { Group, SearchPermissionsParameters, isUser } from '../../../types/quality-gates';
 import { QualityGate } from '../../../types/types';
 import { UserBase } from '../../../types/users';
+import { QGPermissionOption } from '../utils';
 import QualityGatePermissionsAddModalRenderer from './QualityGatePermissionsAddModalRenderer';
 
-type Option = UserBase | Group;
-export type OptionWithValue = Option & { value: string };
-
 interface Props {
   onClose: () => void;
   onSubmit: (selection: UserBase | Group) => void;
@@ -38,23 +34,25 @@ interface Props {
 }
 
 interface State {
+  loading: boolean;
+  options: Array<QGPermissionOption>;
   selection?: UserBase | Group;
 }
 
 const DEBOUNCE_DELAY = 250;
 
 export default class QualityGatePermissionsAddModal extends React.Component<Props, State> {
-  state: State = {};
+  state: State = {
+    loading: false,
+    options: [],
+  };
 
   constructor(props: Props) {
     super(props);
     this.handleSearch = debounce(this.handleSearch, DEBOUNCE_DELAY);
   }
 
-  handleSearch = (
-    q: string,
-    resolve: (options: Options<LabelValueSelectOption<UserBase | Group>>) => void,
-  ) => {
+  handleSearch = (q: string) => {
     const { qualityGate } = this.props;
 
     const queryParams: SearchPermissionsParameters = {
@@ -63,19 +61,34 @@ export default class QualityGatePermissionsAddModal extends React.Component<Prop
       selected: 'deselected',
     };
 
+    this.setState({ loading: true });
+
     Promise.all([searchUsers(queryParams), searchGroups(queryParams)])
       .then(([usersResponse, groupsResponse]) =>
-        [...usersResponse.users, ...groupsResponse.groups].map((o) => ({
-          value: o,
-          label: isUser(o) ? `${o.name} ${o.login}` : o.name,
-        })),
+        [...usersResponse.users, ...groupsResponse.groups].map(
+          (o) =>
+            ({
+              ...o,
+              value: isUser(o) ? o.login : o.name,
+              label: isUser(o) ? o.name ?? o.login : o.name,
+            }) as QGPermissionOption,
+        ),
       )
-      .then(resolve)
-      .catch(() => resolve([]));
+      .then((options) => {
+        this.setState({ loading: false, options });
+      })
+      .catch(() => {
+        this.setState({ loading: false, options: [] });
+      });
   };
 
-  handleSelection = ({ value }: LabelValueSelectOption<UserBase | Group>) => {
-    this.setState({ selection: value });
+  handleSelection = (selectionKey?: string) => {
+    this.setState(({ options }) => {
+      const selectedOption = selectionKey
+        ? options.find((o) => (isUser(o) ? o.login : o.name) === selectionKey)
+        : undefined;
+      return { selection: selectedOption };
+    });
   };
 
   handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
@@ -88,13 +101,15 @@ export default class QualityGatePermissionsAddModal extends React.Component<Prop
 
   render() {
     const { submitting } = this.props;
-    const { selection } = this.state;
+    const { loading, options, selection } = this.state;
 
     return (
       <QualityGatePermissionsAddModalRenderer
+        loading={loading}
         onClose={this.props.onClose}
         onSelection={this.handleSelection}
         onSubmit={this.handleSubmit}
+        options={options}
         handleSearch={this.handleSearch}
         selection={selection}
         submitting={submitting}
index edd4db2b41dcdb3a6f03f8d49964078c8a8a42d8..5d5f021c213ff6096ffd2fc5930789ec95660897 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 {
-  ButtonPrimary,
-  FormField,
-  GenericAvatar,
-  LabelValueSelectOption,
-  Modal,
-  Note,
-  SearchSelectDropdown,
-  UserGroupIcon,
-} from 'design-system';
+import { IconPeople, SelectAsync } from '@sonarsource/echoes-react';
+import { ButtonPrimary, GenericAvatar, Modal, Note } from 'design-system';
 import * as React from 'react';
-import { GroupBase, OptionProps, Options, SingleValue, components } from 'react-select';
 import Avatar from '../../../components/ui/Avatar';
 import { translate } from '../../../helpers/l10n';
 import { Group as UserGroup, isUser } from '../../../types/quality-gates';
 import { UserBase } from '../../../types/users';
+import { QGPermissionOption } from '../utils';
 
 export interface QualityGatePermissionsAddModalRendererProps {
-  handleSearch: (
-    q: string,
-    resolve: (options: Options<LabelValueSelectOption<UserBase | UserGroup>>) => void,
-  ) => void;
+  handleSearch: (q: string) => void;
+  loading: boolean;
   onClose: () => void;
-  onSelection: (selection: SingleValue<LabelValueSelectOption<UserBase | UserGroup>>) => void;
+  onSelection: (selection: string) => void;
   onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
+  options: QGPermissionOption[];
   selection?: UserBase | UserGroup;
   submitting: boolean;
 }
@@ -52,11 +43,9 @@ const USER_SELECT_INPUT_ID = 'quality-gate-permissions-add-modal-select-input';
 export default function QualityGatePermissionsAddModalRenderer(
   props: Readonly<QualityGatePermissionsAddModalRendererProps>,
 ) {
-  const { selection, submitting } = props;
+  const { loading, options, selection, submitting } = props;
 
-  const renderedSelection = React.useMemo(() => {
-    return <OptionRenderer option={selection} small />;
-  }, [selection]);
+  const selectValue = selection && isUser(selection) ? selection.login : selection?.name;
 
   return (
     <Modal
@@ -64,28 +53,19 @@ export default function QualityGatePermissionsAddModalRenderer(
       headerTitle={translate('quality_gates.permissions.grant')}
       body={
         <form onSubmit={props.onSubmit} id={FORM_ID}>
-          <FormField
+          <SelectAsync
+            ariaLabel={translate('quality_gates.permissions.search')}
+            className="sw-mb-4"
+            data={options}
+            id={USER_SELECT_INPUT_ID}
+            isLoading={loading}
             label={translate('quality_gates.permissions.search')}
-            htmlFor={USER_SELECT_INPUT_ID}
-          >
-            <SearchSelectDropdown
-              className="sw-mb-2"
-              controlAriaLabel={translate('quality_gates.permissions.search')}
-              inputId={USER_SELECT_INPUT_ID}
-              autoFocus
-              defaultOptions
-              noOptionsMessage={() => translate('no_results')}
-              onChange={props.onSelection}
-              loadOptions={props.handleSearch}
-              getOptionValue={({ value }: LabelValueSelectOption<UserBase | UserGroup>) =>
-                isUser(value) ? value.login : value.name
-              }
-              controlLabel={renderedSelection}
-              components={{
-                Option,
-              }}
-            />
-          </FormField>
+            labelNotFound={translate('select.search.noMatches')}
+            onChange={props.onSelection}
+            onSearch={props.handleSearch}
+            optionComponent={OptionRenderer}
+            value={selectValue}
+          />
         </form>
       }
       primaryButton={
@@ -98,60 +78,27 @@ export default function QualityGatePermissionsAddModalRenderer(
   );
 }
 
-function OptionRenderer({
-  option,
-  small = false,
-}: Readonly<{
-  option?: UserBase | UserGroup;
-  small?: boolean;
-}>) {
+function OptionRenderer(option: Readonly<QGPermissionOption>) {
   if (!option) {
     return null;
   }
   return (
-    <>
+    <div className="sw-flex sw-items-center sw-justify-start">
       {isUser(option) ? (
         <>
-          <Avatar
-            className={small ? 'sw-my-1' : ''}
-            hash={option.avatar}
-            name={option.name}
-            size={small ? 'xs' : 'sm'}
-          />
-          <span className="sw-ml-2">
+          <Avatar hash={option.avatar} name={option.name} />
+          <div className="sw-ml-2">
             <strong className="sw-body-sm-highlight sw-mr-1">{option.name}</strong>
+            <br />
             <Note>{option.login}</Note>
-          </span>
+          </div>
         </>
       ) : (
         <>
-          <GenericAvatar
-            className={small ? 'sw-my-1' : ''}
-            Icon={UserGroupIcon}
-            name={option.name}
-            size={small ? 'xs' : 'sm'}
-          />
+          <GenericAvatar Icon={IconPeople} name={option.name} />
           <strong className="sw-body-sm-highlight sw-ml-2">{option.name}</strong>
         </>
       )}
-    </>
-  );
-}
-
-function Option<
-  Option extends LabelValueSelectOption<UserBase | UserGroup>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
->(props: OptionProps<Option, IsMulti, Group>) {
-  const {
-    data: { value },
-  } = props;
-
-  return (
-    <components.Option {...props}>
-      <div className="sw-flex sw-items-center">
-        <OptionRenderer option={value} />
-      </div>
-    </components.Option>
+    </div>
   );
 }
index f7552d13c32047d267e43117556432fad146d3ca..f37c00c96a1e6eec70aeefc10c8fca1905bb0ccb 100644 (file)
@@ -735,7 +735,7 @@ describe('The Permissions section', () => {
     });
     await user.click(grantPermissionButton);
     const popup = screen.getByRole('dialog');
-    const searchUserInput = within(popup).getByRole('combobox', {
+    const searchUserInput = within(popup).getByRole('searchbox', {
       name: 'quality_gates.permissions.search',
     });
     expect(searchUserInput).toBeInTheDocument();
@@ -744,7 +744,7 @@ describe('The Permissions section', () => {
     });
     expect(addUserButton).toBeDisabled();
     await user.click(searchUserInput);
-    await user.click(screen.getByText('userlogin'));
+    await user.click(screen.getByRole('option', { name: 'userlogin' }));
     expect(addUserButton).toBeEnabled();
     await user.click(addUserButton);
     expect(screen.getByText('userlogin')).toBeInTheDocument();
@@ -784,14 +784,14 @@ describe('The Permissions section', () => {
     });
     await user.click(grantPermissionButton);
     const popup = screen.getByRole('dialog');
-    const searchUserInput = within(popup).getByRole('combobox', {
+    const searchUserInput = within(popup).getByRole('searchbox', {
       name: 'quality_gates.permissions.search',
     });
     const addUserButton = screen.getByRole('button', {
       name: 'add_verb',
     });
     await user.click(searchUserInput);
-    await user.click(within(popup).getByLabelText('Foo'));
+    await user.click(within(popup).getByRole('option', { name: 'Foo Foo' }));
     await user.click(addUserButton);
     expect(screen.getByText('Foo')).toBeInTheDocument();
 
@@ -817,12 +817,12 @@ describe('The Permissions section', () => {
     });
     await user.click(grantPermissionButton);
     const popup = screen.getByRole('dialog');
-    const searchUserInput = within(popup).getByRole('combobox', {
+    const searchUserInput = within(popup).getByRole('searchbox', {
       name: 'quality_gates.permissions.search',
     });
     await user.click(searchUserInput);
 
-    expect(screen.getByText('no_results')).toBeInTheDocument();
+    expect(screen.getByText('select.search.noMatches')).toBeInTheDocument();
   });
 });
 
index fe1cff639065354f25a5bc8ea7d767593f8b841b..c971c53b3969bcb2a984f03e0fc36b681ceea1fc 100644 (file)
@@ -21,7 +21,8 @@ import { sortBy } from 'lodash';
 import { MetricKey } from '~sonar-aligned/types/metrics';
 import { getLocalizedMetricName } from '../../helpers/l10n';
 import { isDiffMetric } from '../../helpers/measures';
-import { CaycStatus, Condition, Dict, Metric, QualityGate } from '../../types/types';
+import { CaycStatus, Condition, Dict, Group, Metric, QualityGate } from '../../types/types';
+import { UserBase } from '../../types/users';
 
 interface GroupedByMetricConditions {
   caycConditions: Condition[];
@@ -44,6 +45,9 @@ type UnoptimizedCaycMetricKeys =
 
 type AllCaycMetricKeys = OptimizedCaycMetricKeys | UnoptimizedCaycMetricKeys;
 
+type UserOrGroup = UserBase | Group;
+export type QGPermissionOption = UserOrGroup & { label: string; value: string };
+
 const COMMON_CONDITIONS: Record<
   CommonCaycMetricKeys,
   Condition & { shouldRenderOperator?: boolean }