From 0b3de542c8211c7963c549184b887d63408fcde2 Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 5 Sep 2023 15:07:01 +0200 Subject: [PATCH] SONAR-20337 migrate Permissions section to new UI --- .../src/components/{ => avatar}/Avatar.tsx | 14 +- .../components/{ => avatar}/GenericAvatar.tsx | 25 ++- .../{ => avatar}/__tests__/Avatar-test.tsx | 4 +- .../__tests__/GenericAvatar-test.tsx | 12 +- .../src/components/avatar/utils.ts | 34 ++++ .../src/components/icons/Icon.tsx | 4 +- .../design-system/src/components/index.ts | 4 +- .../input/SearchSelectDropdownControl.tsx | 11 +- .../components/PermissionItem.tsx | 56 +++++-- .../components/QualityGatePermissions.tsx | 4 +- .../QualityGatePermissionsAddModal.tsx | 29 ++-- ...QualityGatePermissionsAddModalRenderer.tsx | 156 +++++++++++------- .../QualityGatePermissionsRenderer.tsx | 78 +++++---- .../components/__tests__/QualityGate-it.tsx | 8 +- 14 files changed, 276 insertions(+), 163 deletions(-) rename server/sonar-web/design-system/src/components/{ => avatar}/Avatar.tsx (93%) rename server/sonar-web/design-system/src/components/{ => avatar}/GenericAvatar.tsx (75%) rename server/sonar-web/design-system/src/components/{ => avatar}/__tests__/Avatar-test.tsx (96%) rename server/sonar-web/design-system/src/components/{ => avatar}/__tests__/GenericAvatar-test.tsx (83%) create mode 100644 server/sonar-web/design-system/src/components/avatar/utils.ts diff --git a/server/sonar-web/design-system/src/components/Avatar.tsx b/server/sonar-web/design-system/src/components/avatar/Avatar.tsx similarity index 93% rename from server/sonar-web/design-system/src/components/Avatar.tsx rename to server/sonar-web/design-system/src/components/avatar/Avatar.tsx index bef11b9e2d0..e8e9ede6f53 100644 --- a/server/sonar-web/design-system/src/components/Avatar.tsx +++ b/server/sonar-web/design-system/src/components/avatar/Avatar.tsx @@ -20,17 +20,9 @@ import styled from '@emotion/styled'; import { ReactEventHandler, useState } from 'react'; import tw from 'twin.macro'; -import { themeBorder, themeColor } from '../helpers/theme'; +import { themeBorder, themeColor } from '../../helpers/theme'; import { GenericAvatar } from './GenericAvatar'; - -type Size = 'xs' | 'sm' | 'md' | 'lg'; - -const sizeMap: Record = { - xs: 16, - sm: 24, - md: 40, - lg: 64, -}; +import { Size, sizeMap } from './utils'; interface AvatarProps { border?: boolean; @@ -107,7 +99,7 @@ export function Avatar({ return ; } - return ; + return ; } const StyledAvatar = styled.img<{ border?: boolean }>` diff --git a/server/sonar-web/design-system/src/components/GenericAvatar.tsx b/server/sonar-web/design-system/src/components/avatar/GenericAvatar.tsx similarity index 75% rename from server/sonar-web/design-system/src/components/GenericAvatar.tsx rename to server/sonar-web/design-system/src/components/avatar/GenericAvatar.tsx index 4d8fa6901be..38f4fbef6d9 100644 --- a/server/sonar-web/design-system/src/components/GenericAvatar.tsx +++ b/server/sonar-web/design-system/src/components/avatar/GenericAvatar.tsx @@ -21,23 +21,36 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import React from 'react'; import tw from 'twin.macro'; -import { themeAvatarColor } from '../helpers/theme'; -import { IconProps } from './icons/Icon'; +import { themeAvatarColor } from '../../helpers/theme'; +import { IconProps } from '../icons/Icon'; +import { Size, iconSizeMap, sizeMap } from './utils'; export interface GenericAvatarProps { Icon?: React.ComponentType; className?: string; name: string; - size?: number; + size?: Size; } -export function GenericAvatar({ className, Icon, name, size = 24 }: GenericAvatarProps) { +export function GenericAvatar({ className, Icon, name, size = 'sm' }: GenericAvatarProps) { const theme = useTheme(); const text = name.length > 0 ? name[0].toUpperCase() : ''; + const iconSize = iconSizeMap[size]; + return ( - - {Icon ? : text} + + {Icon ? ( + + ) : ( + text + )} ); } diff --git a/server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx b/server/sonar-web/design-system/src/components/avatar/__tests__/Avatar-test.tsx similarity index 96% rename from server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx rename to server/sonar-web/design-system/src/components/avatar/__tests__/Avatar-test.tsx index e18fa033ded..2d845c0e752 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx +++ b/server/sonar-web/design-system/src/components/avatar/__tests__/Avatar-test.tsx @@ -21,8 +21,8 @@ /* eslint-disable import/no-extraneous-dependencies */ import { fireEvent, screen } from '@testing-library/react'; -import { render } from '../../helpers/testUtils'; -import { FCProps } from '../../types/misc'; +import { render } from '../../../helpers/testUtils'; +import { FCProps } from '../../../types/misc'; import { Avatar } from '../Avatar'; const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}'; diff --git a/server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx b/server/sonar-web/design-system/src/components/avatar/__tests__/GenericAvatar-test.tsx similarity index 83% rename from server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx rename to server/sonar-web/design-system/src/components/avatar/__tests__/GenericAvatar-test.tsx index 83b7fdf6bc3..15405a7eee8 100644 --- a/server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx +++ b/server/sonar-web/design-system/src/components/avatar/__tests__/GenericAvatar-test.tsx @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { screen } from '@testing-library/react'; -import { render } from '../../helpers/testUtils'; +import { render } from '../../../helpers/testUtils'; +import { CustomIcon, IconProps } from '../../icons/Icon'; import { GenericAvatar } from '../GenericAvatar'; -import { CustomIcon, IconProps } from '../icons/Icon'; function TestIcon(props: IconProps) { return ( @@ -31,9 +31,9 @@ function TestIcon(props: IconProps) { } it('should render single word and size', () => { - render(); + render(); const image = screen.getByRole('img'); - expect(image).toHaveAttribute('size', '15'); + expect(image).toHaveAttribute('size', '16'); expect(screen.getByText('F')).toBeInTheDocument(); }); @@ -45,7 +45,7 @@ it('should render multiple word with default size', () => { }); it('should render without name', () => { - render(); + render(); const image = screen.getByRole('img'); - expect(image).toHaveAttribute('size', '32'); + expect(image).toHaveAttribute('size', '40'); }); diff --git a/server/sonar-web/design-system/src/components/avatar/utils.ts b/server/sonar-web/design-system/src/components/avatar/utils.ts new file mode 100644 index 00000000000..bf1857142eb --- /dev/null +++ b/server/sonar-web/design-system/src/components/avatar/utils.ts @@ -0,0 +1,34 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 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. + */ +export type Size = 'xs' | 'sm' | 'md' | 'lg'; + +export const sizeMap: Record = { + xs: 16, + sm: 24, + md: 40, + lg: 64, +}; + +export const iconSizeMap: Record = { + xs: 12, + sm: 18, + md: 24, + lg: 24, +}; diff --git a/server/sonar-web/design-system/src/components/icons/Icon.tsx b/server/sonar-web/design-system/src/components/icons/Icon.tsx index f8cb3f39ca6..58b03f1aa65 100644 --- a/server/sonar-web/design-system/src/components/icons/Icon.tsx +++ b/server/sonar-web/design-system/src/components/icons/Icon.tsx @@ -93,10 +93,12 @@ export function OcticonHoc( function IconWrapper({ fill, ...props }: IconProps) { const theme = useTheme(); + const size = props.width ?? props.height ?? 'small'; + return ( diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index b284e595baa..34f72bf173b 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -19,7 +19,6 @@ */ export * from './Accordion'; -export * from './Avatar'; export { Badge } from './Badge'; export * from './Banner'; export { BarChart } from './BarChart'; @@ -42,7 +41,6 @@ export { FailedQGConditionLink } from './FailedQGConditionLink'; export * from './FavoriteButton'; export { FlagMessage } from './FlagMessage'; export * from './FlowStep'; -export * from './GenericAvatar'; export * from './HighlightedSection'; export { Histogram } from './Histogram'; export { HotspotRating } from './HotspotRating'; @@ -83,6 +81,8 @@ export * from './TreeMap'; export * from './TreeMapRect'; export * from './TutorialStep'; export * from './TutorialStepList'; +export * from './avatar/Avatar'; +export * from './avatar/GenericAvatar'; export * from './buttons'; export { ClipboardIconButton } from './clipboard'; export * from './code-line/LineCoverage'; diff --git a/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx b/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx index 7090d3c4c91..8f6bbf78e0e 100644 --- a/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx +++ b/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx @@ -57,10 +57,13 @@ export function SearchSelectDropdownControl(props: SearchSelectDropdownControlPr tabIndex={disabled ? -1 : 0} > {label} diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx index ec30c1f8039..ac9ed080937 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx @@ -17,10 +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 { + ContentCell, + DestructiveIcon, + GenericAvatar, + Note, + TrashIcon, + UserGroupIcon, +} from 'design-system'; import * as React from 'react'; -import { DeleteButton } from '../../../components/controls/buttons'; -import GroupIcon from '../../../components/icons/GroupIcon'; -import LegacyAvatar from '../../../components/ui/LegacyAvatar'; +import { useIntl } from 'react-intl'; +import Avatar from '../../../components/ui/Avatar'; import { Group, isUser } from '../../../types/quality-gates'; import { UserBase } from '../../../types/users'; @@ -31,24 +38,37 @@ export interface PermissionItemProps { export default function PermissionItem(props: PermissionItemProps) { const { item } = props; + const { formatMessage } = useIntl(); return ( -
- {isUser(item) ? ( - - ) : ( - - )} + <> + + {isUser(item) ? ( + + ) : ( + + )} + -
- {item.name} - {isUser(item) &&
{item.login}
} -
+ +
+ {item.name} + {isUser(item) && {item.login}} +
+
- props.onClickDelete(item)} - data-testid="permission-delete-button" - /> -
+ + props.onClickDelete(item)} + data-testid="permission-delete-button" + /> + + ); } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx index c617b35f7a6..2a96ec24206 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx @@ -27,7 +27,7 @@ import { searchGroups, searchUsers, } from '../../../api/quality-gates'; -import { Group, isUser, SearchPermissionsParameters } from '../../../types/quality-gates'; +import { Group, SearchPermissionsParameters, isUser } from '../../../types/quality-gates'; import { QualityGate } from '../../../types/types'; import { UserBase } from '../../../types/users'; import QualityGatePermissionsRenderer from './QualityGatePermissionsRenderer'; @@ -159,10 +159,12 @@ export default class QualityGatePermissions extends React.Component ({ users: users.filter((u) => u.login !== item.login), + permissionToDelete: undefined, })); } else { this.setState(({ groups }) => ({ groups: groups.filter((g) => g.name !== item.name), + permissionToDelete: undefined, })); } } diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx index 86846046e10..f4e36d6a02b 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModal.tsx @@ -17,10 +17,12 @@ * 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 } from '../../../types/quality-gates'; +import { Group, SearchPermissionsParameters, isUser } from '../../../types/quality-gates'; import { QualityGate } from '../../../types/types'; import { UserBase } from '../../../types/users'; import QualityGatePermissionsAddModalRenderer from './QualityGatePermissionsAddModalRenderer'; @@ -42,7 +44,6 @@ interface State { const DEBOUNCE_DELAY = 250; export default class QualityGatePermissionsAddModal extends React.Component { - mounted = false; state: State = {}; constructor(props: Props) { @@ -50,15 +51,10 @@ export default class QualityGatePermissionsAddModal extends React.Component void) => { + handleSearch = ( + q: string, + resolve: (options: Options>) => void + ) => { const { qualityGate } = this.props; const queryParams: SearchPermissionsParameters = { @@ -68,13 +64,18 @@ export default class QualityGatePermissionsAddModal extends React.Component [...usersResponse.users, ...groupsResponse.groups]) + .then(([usersResponse, groupsResponse]) => + [...usersResponse.users, ...groupsResponse.groups].map((o) => ({ + value: o, + label: isUser(o) ? `${o.name} ${o.login}` : o.name, + })) + ) .then(resolve) .catch(() => resolve([])); }; - handleSelection = (selection: UserBase | Group) => { - this.setState({ selection }); + handleSelection = ({ value }: LabelValueSelectOption) => { + this.setState({ selection: value }); }; handleSubmit = (event: React.SyntheticEvent) => { diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx index c4cbfa9688a..37d49034fa9 100644 --- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx @@ -17,103 +17,139 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { omit } from 'lodash'; +import { + ButtonPrimary, + FormField, + GenericAvatar, + LabelValueSelectOption, + Modal, + SearchSelectDropdown, + UserGroupIcon, +} from 'design-system'; import * as React from 'react'; -import { components, ControlProps, OptionProps, SingleValueProps } from 'react-select'; -import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; -import Modal from '../../../components/controls/Modal'; -import { SearchSelect } from '../../../components/controls/Select'; -import GroupIcon from '../../../components/icons/GroupIcon'; -import LegacyAvatar from '../../../components/ui/LegacyAvatar'; +import { GroupBase, OptionProps, Options, SingleValue, components } from 'react-select'; +import Avatar from '../../../components/ui/Avatar'; import { translate } from '../../../helpers/l10n'; -import { Group, isUser } from '../../../types/quality-gates'; +import { Group as UserGroup, isUser } from '../../../types/quality-gates'; import { UserBase } from '../../../types/users'; -import { OptionWithValue } from './QualityGatePermissionsAddModal'; export interface QualityGatePermissionsAddModalRendererProps { onClose: () => void; - handleSearch: (q: string, resolve: (options: OptionWithValue[]) => void) => void; - onSelection: (selection: OptionWithValue) => void; - selection?: UserBase | Group; + handleSearch: ( + q: string, + resolve: (options: Options>) => void + ) => void; + onSelection: (selection: SingleValue>) => void; + selection?: UserBase | UserGroup; onSubmit: (event: React.SyntheticEvent) => void; submitting: boolean; } +const FORM_ID = 'quality-gate-permissions-add-modal'; +const USER_SELECT_INPUT_ID = 'quality-gate-permissions-add-modal-select-input'; + export default function QualityGatePermissionsAddModalRenderer( props: QualityGatePermissionsAddModalRendererProps, ) { const { selection, submitting } = props; - const header = translate('quality_gates.permissions.grant'); - - const noResultsText = translate('no_results'); + const renderedSelection = React.useMemo(() => { + return ; + }, [selection]); return ( - -
-

{header}

-
-
-
-
- - + + noResultsText} + noOptionsMessage={() => translate('no_results')} onChange={props.onSelection} loadOptions={props.handleSearch} - getOptionValue={(opt) => (isUser(opt) ? opt.login : opt.name)} - large + getOptionValue={({ value }) => (isUser(value) ? value.login : value.name)} + controlLabel={renderedSelection} components={{ - Option: optionRenderer, - SingleValue: singleValueRenderer, - Control: controlRenderer, + Option, }} /> -
-
-
- {submitting && } - {translate('add_verb')} - {translate('cancel')} -
- -
+ + + } + primaryButton={ + + {translate('add_verb')} + + } + secondaryButtonLabel={translate('cancel')} + /> ); } -export function customOptions(option: OptionWithValue) { +function OptionRenderer({ + option, + small = false, +}: { + option?: UserBase | UserGroup; + small?: boolean; +}) { + if (!option) { + return null; + } return ( - + <> {isUser(option) ? ( - + <> + + + {option.name} + {option.login} + + ) : ( - + <> + + {option.name} + )} - {option.name} - {isUser(option) && {option.login}} - + ); } -function optionRenderer(props: OptionProps) { - return {customOptions(props.data)}; -} - -function singleValueRenderer(props: SingleValueProps) { - return {customOptions(props.data)}; -} +function Option< + Option extends LabelValueSelectOption, + IsMulti extends boolean = false, + Group extends GroupBase