diff options
8 files changed, 287 insertions, 349 deletions
diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx index 88bbd3f7bc9..763546dffbd 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/__tests__/QualityProfileApp-it.tsx @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx index d6d85c4747d..e3900a644b1 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ChangeProjectsForm.tsx @@ -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)} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx index d858ec00deb..99c7c269f33 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx @@ -17,11 +17,10 @@ * 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; } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx index 3d1f8ee2f2b..c243187f0ee 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx @@ -17,93 +17,121 @@ * 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> - ); -} diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx index 85dd7fbf67a..f0b80064bc2 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsGroup.tsx @@ -17,12 +17,17 @@ * 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> - ); - } } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx index 23f67fb5366..6e3aa372dcd 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx @@ -17,12 +17,17 @@ * 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> - ); - } } diff --git a/server/sonar-web/src/main/js/queries/quality-profiles.ts b/server/sonar-web/src/main/js/queries/quality-profiles.ts index 3783e073b11..0396f4bfe67 100644 --- a/server/sonar-web/src/main/js/queries/quality-profiles.ts +++ b/server/sonar-web/src/main/js/queries/quality-profiles.ts @@ -17,8 +17,15 @@ * 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, + }); +} diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 4941aab5cd1..3d5c5531f2b 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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}? |