]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20366 Migrate set admins modals
authorstanislavh <stanislav.honcharov@sonarsource.com>
Thu, 5 Oct 2023 16:34:57 +0000 (18:34 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 6 Oct 2023 20:02:52 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx
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/ProfilePermissionsGroup.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx
server/sonar-web/src/main/js/queries/quality-profiles.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 88bbd3f7bc95f5467a54fb77ecb2d65ed63ba8c6..763546dffbd23f86a176ca51a51be35419587bb9 100644 (file)
@@ -108,7 +108,9 @@ describe('Admin or user with permission', () => {
       });
       expect(ui.dialog.get()).toBeInTheDocument();
       await selectEvent.select(ui.selectUserOrGroup.get(), 'Buzz');
-      await user.click(ui.addButton.get());
+      await act(async () => {
+        await user.click(ui.addButton.get());
+      });
       expect(ui.permissionSection.byText('Buzz').get()).toBeInTheDocument();
 
       // Remove User
@@ -118,7 +120,9 @@ describe('Admin or user with permission', () => {
           .get(),
       );
       expect(ui.dialog.get()).toBeInTheDocument();
-      await user.click(ui.removeButton.get());
+      await act(async () => {
+        await user.click(ui.removeButton.get());
+      });
       expect(ui.permissionSection.byText('buzz').query()).not.toBeInTheDocument();
     });
 
@@ -135,7 +139,9 @@ describe('Admin or user with permission', () => {
       });
       expect(ui.dialog.get()).toBeInTheDocument();
       await selectEvent.select(ui.selectUserOrGroup.get(), 'ACDC');
-      await user.click(ui.addButton.get());
+      await act(async () => {
+        await user.click(ui.addButton.get());
+      });
       expect(ui.permissionSection.byText('ACDC').get()).toBeInTheDocument();
 
       // Remove group
@@ -145,7 +151,9 @@ describe('Admin or user with permission', () => {
           .get(),
       );
       expect(ui.dialog.get()).toBeInTheDocument();
-      await user.click(ui.removeButton.get());
+      await act(async () => {
+        await user.click(ui.removeButton.get());
+      });
       expect(ui.permissionSection.byText('ACDC').query()).not.toBeInTheDocument();
     });
 
index d6d85c4747dfbb6af7a8e09b1acac6ab2cd2a4a2..e3900a644b13152e781fff0bf38e22886e20c27e 100644 (file)
@@ -144,7 +144,7 @@ export default class ChangeProjectsForm extends React.PureComponent<Props, State
         isOverflowVisible
         onClose={this.props.onClose}
         body={
-          <div className="sw-mt-1">
+          <div className="sw-mt-1" id="profile-projects">
             <SelectList
               allowBulkSelection
               elements={this.state.projects.map((project) => project.key)}
index d858ec00debdec91d80592bbc67237dfe2e31067..99c7c269f33fcb2e50047cec52bce37399f57082 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, Modal } from 'design-system';
 import * as React from 'react';
-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';
+import { useAddGroupMutation, useAddUserMutation } from '../../../queries/quality-profiles';
 import { UserSelected } from '../../../types/types';
 import { Group } from './ProfilePermissions';
 import ProfilePermissionsFormSelect from './ProfilePermissionsFormSelect';
@@ -33,93 +32,68 @@ interface Props {
   profile: { language: string; name: string };
 }
 
-interface State {
-  selected?: UserSelected | Group;
-  submitting: boolean;
-}
-
-export default class ProfilePermissionsForm extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { submitting: false };
+export default function ProfilePermissionForm(props: Readonly<Props>) {
+  const { profile } = props;
+  const [selected, setSelected] = React.useState<UserSelected | Group>();
 
-  componentDidMount() {
-    this.mounted = true;
-  }
+  const { mutate: addUser, isLoading: addingUser } = useAddUserMutation(() =>
+    props.onUserAdd(selected as UserSelected),
+  );
+  const { mutate: addGroup, isLoading: addingGroup } = useAddGroupMutation(() =>
+    props.onGroupAdd(selected as Group),
+  );
 
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  stopSubmitting = () => {
-    if (this.mounted) {
-      this.setState({ submitting: false });
-    }
-  };
-
-  handleUserAdd = (user: UserSelected) => {
-    const {
-      profile: { language, name },
-    } = this.props;
-    addUser({
-      language,
-      login: user.login,
-      qualityProfile: name,
-    }).then(() => this.props.onUserAdd(user), this.stopSubmitting);
-  };
+  const loading = addingUser || addingGroup;
 
-  handleGroupAdd = (group: Group) => {
-    const {
-      profile: { language, name },
-    } = this.props;
-    addGroup({
-      group: group.name,
-      language,
-      qualityProfile: name,
-    }).then(() => this.props.onGroupAdd(group), this.stopSubmitting);
-  };
+  const header = translate('quality_profiles.grant_permissions_to_user_or_group');
+  const submitDisabled = !selected || loading;
 
-  handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
+  const handleFormSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
     event.preventDefault();
-    const { selected } = this.state;
     if (selected) {
-      this.setState({ submitting: true });
-      if ((selected as UserSelected).login !== undefined) {
-        this.handleUserAdd(selected as UserSelected);
+      if (isSelectedUser(selected)) {
+        addUser({
+          language: profile.language,
+          login: selected.login,
+          qualityProfile: profile.name,
+        });
       } else {
-        this.handleGroupAdd(selected as Group);
+        addGroup({
+          language: profile.language,
+          group: selected.name,
+          qualityProfile: profile.name,
+        });
       }
     }
   };
 
-  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 (
-      <Modal contentLabel={header} onRequestClose={this.props.onClose}>
-        <header className="modal-head">
-          <h2>{header}</h2>
-        </header>
-        <form onSubmit={this.handleFormSubmit}>
-          <div className="modal-body">
-            <div className="modal-field">
-              <label htmlFor="change-profile-permission-input">
-                {translate('quality_profiles.search_description')}
-              </label>
-              <ProfilePermissionsFormSelect onChange={this.handleValueChange} profile={profile} />
-            </div>
-          </div>
-          <footer className="modal-foot">
-            {this.state.submitting && <i className="spinner spacer-right" />}
-            <SubmitButton disabled={submitDisabled}>{translate('add_verb')}</SubmitButton>
-            <ResetButtonLink onClick={this.props.onClose}>{translate('cancel')}</ResetButtonLink>
-          </footer>
+  return (
+    <Modal
+      isOverflowVisible
+      headerTitle={header}
+      onClose={props.onClose}
+      loading={loading}
+      primaryButton={
+        <ButtonPrimary type="submit" form="grant_permissions_form" disabled={submitDisabled}>
+          {translate('add_verb')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+      body={
+        <form onSubmit={handleFormSubmit} id="grant_permissions_form">
+          <FormField label={translate('quality_profiles.search_description')}>
+            <ProfilePermissionsFormSelect
+              onChange={(option) => setSelected(option)}
+              selected={selected}
+              profile={profile}
+            />
+          </FormField>
         </form>
-      </Modal>
-    );
-  }
+      }
+    />
+  );
+}
+
+function isSelectedUser(selected: UserSelected | Group): selected is UserSelected {
+  return (selected as UserSelected).login !== undefined;
 }
index 3d1f8ee2f2b185a4e1282374e1648e9acfbf2a58..c243187f0eeaf5bd4fd40e16e0832ca48c6c93c0 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, omit } from 'lodash';
+import {
+  Avatar,
+  GenericAvatar,
+  LabelValueSelectOption,
+  SearchSelectDropdown,
+  UserGroupIcon,
+} from 'design-system';
+import { omit } from 'lodash';
 import * as React from 'react';
-import { ControlProps, OptionProps, SingleValueProps, components } from 'react-select';
+import { useIntl } from 'react-intl';
 import {
   SearchUsersGroupsParameters,
   searchGroups,
   searchUsers,
 } from '../../../api/quality-profiles';
-import { SearchSelect } from '../../../components/controls/Select';
-import GroupIcon from '../../../components/icons/GroupIcon';
-import LegacyAvatar from '../../../components/ui/LegacyAvatar';
-import { translate } from '../../../helpers/l10n';
 import { UserSelected } from '../../../types/types';
 import { Group } from './ProfilePermissions';
 
 type Option = UserSelected | Group;
-type OptionWithValue = Option & { value: string };
 
 interface Props {
-  onChange: (option: OptionWithValue) => void;
+  onChange: (option: Option) => void;
   profile: { language: string; name: string };
+  selected?: Option;
 }
 
-const DEBOUNCE_DELAY = 250;
+export default function ProfilePermissionsFormSelect(props: Readonly<Props>) {
+  const { profile, selected } = props;
+  const [defaultOptions, setDefaultOptions] = React.useState<LabelValueSelectOption<string>[]>([]);
+  const intl = useIntl();
 
-export default class ProfilePermissionsFormSelect extends React.PureComponent<Props> {
-  constructor(props: Props) {
-    super(props);
-    this.handleSearch = debounce(this.handleSearch, DEBOUNCE_DELAY);
-  }
+  const value = selected ? getOption(selected) : null;
+  const controlLabel = selected ? (
+    <>
+      {isUser(selected) ? (
+        <Avatar hash={selected.avatar} name={selected.name} size="xs" />
+      ) : (
+        <GenericAvatar Icon={UserGroupIcon} name={selected.name} size="xs" />
+      )}{' '}
+      {selected.name}
+    </>
+  ) : undefined;
 
-  optionRenderer(props: OptionProps<OptionWithValue, false>) {
-    const { data } = props;
-    return <components.Option {...props}>{customOptions(data)}</components.Option>;
-  }
+  const loadOptions = React.useCallback(
+    async (q = '') => {
+      const parameters: SearchUsersGroupsParameters = {
+        language: profile.language,
+        q,
+        qualityProfile: profile.name,
+        selected: 'deselected',
+      };
+      const [{ users }, { groups }] = await Promise.all([
+        searchUsers(parameters),
+        searchGroups(parameters),
+      ]);
 
-  singleValueRenderer = (props: SingleValueProps<OptionWithValue, false>) => (
-    <components.SingleValue {...props}>{customOptions(props.data)}</components.SingleValue>
+      return { users, groups };
+    },
+    [profile.language, profile.name],
   );
 
-  controlRenderer = (props: ControlProps<OptionWithValue, false>) => (
-    <components.Control {...omit(props, ['children'])} className="abs-height-100">
-      {props.children}
-    </components.Control>
-  );
+  const loadInitial = React.useCallback(async () => {
+    try {
+      const { users, groups } = await loadOptions();
+      setDefaultOptions([...users, ...groups].map(getOption));
+    } catch {
+      // noop
+    }
+  }, [loadOptions]);
 
-  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([]));
+  const handleSearch = (q: string, cb: (options: LabelValueSelectOption<string>[]) => void) => {
+    loadOptions(q)
+      .then(({ users, groups }) => [...users, ...groups].map(getOption))
+      // eslint-disable-next-line promise/no-callback-in-promise
+      .then(cb)
+      // eslint-disable-next-line promise/no-callback-in-promise
+      .catch(() => cb([]));
   };
 
-  render() {
-    const noResultsText = translate('no_results');
+  const handleChange = (option: LabelValueSelectOption<string>) => {
+    props.onChange(omit(option, ['Icon', 'label', 'value']) as Option);
+  };
 
-    return (
-      <SearchSelect
-        className="width-100"
-        autoFocus
-        isClearable={false}
-        id="change-profile-permission"
-        inputId="change-profile-permission-input"
-        onChange={this.props.onChange}
-        defaultOptions
-        loadOptions={this.handleSearch}
-        placeholder=""
-        noOptionsMessage={() => noResultsText}
-        large
-        components={{
-          Option: this.optionRenderer,
-          SingleValue: this.singleValueRenderer,
-          Control: this.controlRenderer,
-        }}
-      />
-    );
-  }
+  React.useEffect(() => {
+    loadInitial();
+  }, [loadInitial]);
+
+  return (
+    <SearchSelectDropdown
+      id="change-profile-permission"
+      inputId="change-profile-permission-input"
+      controlAriaLabel={intl.formatMessage({ id: 'quality_profiles.search_description' })}
+      size="full"
+      controlLabel={controlLabel}
+      onChange={handleChange}
+      defaultOptions={defaultOptions}
+      loadOptions={handleSearch}
+      value={value}
+    />
+  );
 }
 
+const getOption = (option: Option): LabelValueSelectOption<string> => {
+  return {
+    ...option,
+    value: getStringValue(option),
+    label: option.name,
+    Icon: isUser(option) ? (
+      <Avatar hash={option.avatar} name={option.name} size="xs" />
+    ) : (
+      <GenericAvatar Icon={UserGroupIcon} name={option.name} size="xs" />
+    ),
+  };
+};
+
 function isUser(option: Option): option is UserSelected {
   return (option as UserSelected).login !== undefined;
 }
@@ -111,18 +139,3 @@ function isUser(option: Option): option is UserSelected {
 function getStringValue(option: Option) {
   return isUser(option) ? `user:${option.login}` : `group:${option.name}`;
 }
-
-function customOptions(option: OptionWithValue) {
-  return isUser(option) ? (
-    <span className="display-flex-center">
-      <LegacyAvatar hash={option.avatar} name={option.name} size={16} />
-      <strong className="spacer-left">{option.name}</strong>
-      <span className="note little-spacer-left">{option.login}</span>
-    </span>
-  ) : (
-    <span className="display-flex-center">
-      <GroupIcon size={16} />
-      <strong className="spacer-left">{option.name}</strong>
-    </span>
-  );
-}
index 85dd7fbf67a7f991428892efc36ad52d076ae85d..f0b80064bc244e22a3142512782e2bcaa2123d75 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 { DestructiveIcon, GenericAvatar, TrashIcon, UserGroupIcon } from 'design-system';
+import {
+  DangerButtonPrimary,
+  DestructiveIcon,
+  GenericAvatar,
+  Modal,
+  TrashIcon,
+  UserGroupIcon,
+} from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { removeGroup } from '../../../api/quality-profiles';
-import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal';
-import { Button, ResetButtonLink } from '../../../components/controls/buttons';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { Group } from './ProfilePermissions';
 
@@ -32,104 +37,62 @@ interface Props {
   profile: { language: string; name: string };
 }
 
-interface State {
-  deleteModal: boolean;
-}
-
-export default class ProfilePermissionsGroup extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { deleteModal: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleDeleteClick = () => {
-    this.setState({ deleteModal: true });
-  };
-
-  handleDeleteModalClose = () => {
-    if (this.mounted) {
-      this.setState({ deleteModal: false });
-    }
-  };
-
-  handleDelete = () => {
-    const { group, profile } = this.props;
+export default function ProfilePermissionsGroup(props: Readonly<Props>) {
+  const { group, profile } = props;
+  const [deleteDialogOpened, setDeleteDialogOpened] = React.useState(false);
 
+  const handleDelete = () => {
     return removeGroup({
       group: group.name,
       language: profile.language,
       qualityProfile: profile.name,
     }).then(() => {
-      this.handleDeleteModalClose();
-      this.props.onDelete(group);
+      setDeleteDialogOpened(false);
+      props.onDelete(group);
     });
   };
 
-  renderDeleteModal = (props: ChildrenProps) => (
-    <div>
-      <header className="modal-head">
-        <h2>{translate('quality_profiles.permissions.remove.group')}</h2>
-      </header>
-
-      <div className="modal-body">
-        <FormattedMessage
-          defaultMessage={translate('quality_profiles.permissions.remove.group.confirmation')}
-          id="quality_profiles.permissions.remove.group.confirmation"
-          values={{
-            user: <strong>{this.props.group.name}</strong>,
-          }}
+  return (
+    <div className="sw-flex sw-items-center sw-justify-between">
+      <div className="sw-flex sw-truncate">
+        <GenericAvatar
+          Icon={UserGroupIcon}
+          className="sw-mt-1/2 sw-mr-3 sw-grow-0 sw-shrink-0"
+          name={group.name}
+          size="xs"
         />
+        <strong className="sw-body-sm-highlight sw-truncate fs-mask">{group.name}</strong>
       </div>
+      <DestructiveIcon
+        Icon={TrashIcon}
+        aria-label={translateWithParameters(
+          'quality_profiles.permissions.remove.group_x',
+          group.name,
+        )}
+        onClick={() => setDeleteDialogOpened(true)}
+      />
 
-      <footer className="modal-foot">
-        {props.submitting && <i className="spinner spacer-right" />}
-        <Button className="button-red" disabled={props.submitting} onClick={props.onSubmitClick}>
-          {translate('remove')}
-        </Button>
-        <ResetButtonLink onClick={props.onCloseClick}>{translate('cancel')}</ResetButtonLink>
-      </footer>
+      {deleteDialogOpened && (
+        <Modal
+          headerTitle={translate('quality_profiles.permissions.remove.group')}
+          onClose={() => setDeleteDialogOpened(false)}
+          body={
+            <FormattedMessage
+              defaultMessage={translate('quality_profiles.permissions.remove.group.confirmation')}
+              id="quality_profiles.permissions.remove.group.confirmation"
+              values={{
+                user: <strong>{group.name}</strong>,
+              }}
+            />
+          }
+          primaryButton={
+            <DangerButtonPrimary autoFocus onClick={handleDelete}>
+              {translate('remove')}
+            </DangerButtonPrimary>
+          }
+          secondaryButtonLabel={translate('cancel')}
+        />
+      )}
     </div>
   );
-
-  render() {
-    const { group } = this.props;
-
-    return (
-      <div className="sw-flex sw-items-center sw-justify-between">
-        <div className="sw-flex sw-truncate">
-          <GenericAvatar
-            Icon={UserGroupIcon}
-            className="sw-mt-1/2 sw-mr-3 sw-grow-0 sw-shrink-0"
-            name={group.name}
-            size="xs"
-          />
-          <strong className="sw-body-sm-highlight sw-truncate fs-mask">{group.name}</strong>
-        </div>
-        <DestructiveIcon
-          Icon={TrashIcon}
-          aria-label={translateWithParameters(
-            'quality_profiles.permissions.remove.group_x',
-            group.name,
-          )}
-          onClick={this.handleDeleteClick}
-        />
-
-        {this.state.deleteModal && (
-          <SimpleModal
-            header={translate('quality_profiles.permissions.remove.group')}
-            onClose={this.handleDeleteModalClose}
-            onSubmit={this.handleDelete}
-          >
-            {this.renderDeleteModal}
-          </SimpleModal>
-        )}
-      </div>
-    );
-  }
 }
index 23f67fb5366abd65d7acde6a91cc6fe5b3558502..6e3aa372dcdd05aba279de2cc8f8da26c3af5d78 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 { Avatar, DestructiveIcon, Note, TrashIcon } from 'design-system';
+import {
+  Avatar,
+  DangerButtonPrimary,
+  DestructiveIcon,
+  Modal,
+  Note,
+  TrashIcon,
+} from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
 import { removeUser } from '../../../api/quality-profiles';
-import SimpleModal, { ChildrenProps } from '../../../components/controls/SimpleModal';
-import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { UserSelected } from '../../../types/types';
 
@@ -32,111 +37,65 @@ interface Props {
   user: UserSelected;
 }
 
-interface State {
-  deleteModal: boolean;
-}
-
-export default class ProfilePermissionsUser extends React.PureComponent<Props, State> {
-  mounted = false;
-  state: State = { deleteModal: false };
-
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleDeleteClick = () => {
-    this.setState({ deleteModal: true });
-  };
-
-  handleDeleteModalClose = () => {
-    if (this.mounted) {
-      this.setState({ deleteModal: false });
-    }
-  };
-
-  handleDelete = () => {
-    const { profile, user } = this.props;
+export default function ProfilePermissionsGroup(props: Readonly<Props>) {
+  const { user, profile } = props;
+  const [deleteDialogOpened, setDeleteDialogOpened] = React.useState(false);
 
+  const handleDelete = () => {
     return removeUser({
       language: profile.language,
       login: user.login,
       qualityProfile: profile.name,
     }).then(() => {
-      this.handleDeleteModalClose();
-      this.props.onDelete(user);
+      setDeleteDialogOpened(false);
+      props.onDelete(user);
     });
   };
 
-  renderDeleteModal = (props: ChildrenProps) => (
-    <div>
-      <header className="modal-head">
-        <h2>{translate('quality_profiles.permissions.remove.user')}</h2>
-      </header>
-
-      <div className="modal-body">
-        <FormattedMessage
-          defaultMessage={translate('quality_profiles.permissions.remove.user.confirmation')}
-          id="quality_profiles.permissions.remove.user.confirmation"
-          values={{
-            user: <strong>{this.props.user.name}</strong>,
-          }}
+  return (
+    <div className="sw-flex sw-items-center sw-justify-between">
+      <div className="sw-flex sw-truncate">
+        <Avatar
+          className="sw-mt-1/2 sw-mr-3 sw-grow-0 sw-shrink-0"
+          hash={user.avatar}
+          name={user.name}
+          size="xs"
         />
+        <div className="sw-truncate fs-mask">
+          <strong className="sw-body-sm-highlight">{user.name}</strong>
+          <Note className="sw-block">{user.login}</Note>
+        </div>
       </div>
+      <DestructiveIcon
+        Icon={TrashIcon}
+        aria-label={translateWithParameters(
+          'quality_profiles.permissions.remove.user_x',
+          user.name,
+        )}
+        onClick={() => setDeleteDialogOpened(true)}
+      />
 
-      <footer className="modal-foot">
-        {props.submitting && <i className="spinner spacer-right" />}
-        <SubmitButton
-          className="button-red"
-          disabled={props.submitting}
-          onClick={props.onSubmitClick}
-        >
-          {translate('remove')}
-        </SubmitButton>
-        <ResetButtonLink onClick={props.onCloseClick}>{translate('cancel')}</ResetButtonLink>
-      </footer>
+      {deleteDialogOpened && (
+        <Modal
+          headerTitle={translate('quality_profiles.permissions.remove.user')}
+          onClose={() => setDeleteDialogOpened(false)}
+          body={
+            <FormattedMessage
+              defaultMessage={translate('quality_profiles.permissions.remove.user.confirmation')}
+              id="quality_profiles.permissions.remove.user.confirmation"
+              values={{
+                user: <strong>{user.name}</strong>,
+              }}
+            />
+          }
+          primaryButton={
+            <DangerButtonPrimary autoFocus onClick={handleDelete}>
+              {translate('remove')}
+            </DangerButtonPrimary>
+          }
+          secondaryButtonLabel={translate('cancel')}
+        />
+      )}
     </div>
   );
-
-  render() {
-    const { user } = this.props;
-
-    return (
-      <div className="sw-flex sw-items-center sw-justify-between">
-        <div className="sw-flex sw-truncate">
-          <Avatar
-            className="sw-mt-1/2 sw-mr-3 sw-grow-0 sw-shrink-0"
-            hash={user.avatar}
-            name={user.name}
-            size="xs"
-          />
-          <div className="sw-truncate fs-mask">
-            <strong className="sw-body-sm-highlight">{user.name}</strong>
-            <Note className="sw-block">{user.login}</Note>
-          </div>
-        </div>
-        <DestructiveIcon
-          Icon={TrashIcon}
-          aria-label={translateWithParameters(
-            'quality_profiles.permissions.remove.user_x',
-            user.name,
-          )}
-          onClick={this.handleDeleteClick}
-        />
-
-        {this.state.deleteModal && (
-          <SimpleModal
-            header={translate('quality_profiles.permissions.remove.user')}
-            onClose={this.handleDeleteModalClose}
-            onSubmit={this.handleDelete}
-          >
-            {this.renderDeleteModal}
-          </SimpleModal>
-        )}
-      </div>
-    );
-  }
 }
index 3783e073b119490ce721a0263cd5aa9d7cf8e209..0396f4bfe67279e03110ed9a9187e31fcab2c883 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 { UseQueryResult, useQuery } from '@tanstack/react-query';
-import { Profile, getProfileInheritance } from '../api/quality-profiles';
+import { UseQueryResult, useMutation, useQuery } from '@tanstack/react-query';
+import {
+  AddRemoveGroupParameters,
+  AddRemoveUserParameters,
+  Profile,
+  addGroup,
+  addUser,
+  getProfileInheritance,
+} from '../api/quality-profiles';
 import { ProfileInheritanceDetails } from '../types/types';
 
 export function useProfileInheritanceQuery(
@@ -41,3 +48,17 @@ export function useProfileInheritanceQuery(
     },
   });
 }
+
+export function useAddUserMutation(onSuccess: () => unknown) {
+  return useMutation({
+    mutationFn: (data: AddRemoveUserParameters) => addUser(data),
+    onSuccess,
+  });
+}
+
+export function useAddGroupMutation(onSuccess: () => unknown) {
+  return useMutation({
+    mutationFn: (data: AddRemoveGroupParameters) => addGroup(data),
+    onSuccess,
+  });
+}
index 4941aab5cd117a88932ed8daa9303bec8227d2dd..3d5c5531f2bb1e4a1a5112c533bd49a27e69cdfe 100644 (file)
@@ -2072,7 +2072,7 @@ quality_profiles.default_permissions=Users with the global "Administer Quality P
 quality_profiles.grant_permissions_to_more_users=Grant permissions to more users
 quality_profiles.grant_permissions_to_user_or_group=Grant permissions to a user or a group
 quality_profiles.additional_user_groups=Additional users / groups:
-quality_profiles.search_description=Search users by login or name, and groups by name: 
+quality_profiles.search_description=Search users by login or name, and groups by name:
 quality_profiles.permissions.remove.user=Remove permission from user
 quality_profiles.permissions.remove.user_x=Remove permission from user {0}
 quality_profiles.permissions.remove.user.confirmation=Are you sure you want to remove permission on this quality profile from user {user}?