]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20337 migrate Permissions section to new UI
authorJeremy Davis <jeremy.davis@sonarsource.com>
Tue, 5 Sep 2023 13:07:01 +0000 (15:07 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 19 Sep 2023 20:02:46 +0000 (20:02 +0000)
18 files changed:
server/sonar-web/design-system/src/components/Avatar.tsx [deleted file]
server/sonar-web/design-system/src/components/GenericAvatar.tsx [deleted file]
server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx [deleted file]
server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx [deleted file]
server/sonar-web/design-system/src/components/avatar/Avatar.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/avatar/GenericAvatar.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/avatar/__tests__/Avatar-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/avatar/__tests__/GenericAvatar-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/avatar/utils.ts [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/Icon.tsx
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/PermissionItem.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissions.tsx
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/QualityGatePermissionsRenderer.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx

diff --git a/server/sonar-web/design-system/src/components/Avatar.tsx b/server/sonar-web/design-system/src/components/Avatar.tsx
deleted file mode 100644 (file)
index bef11b9..0000000
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * 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.
- */
-import styled from '@emotion/styled';
-import { ReactEventHandler, useState } from 'react';
-import tw from 'twin.macro';
-import { themeBorder, themeColor } from '../helpers/theme';
-import { GenericAvatar } from './GenericAvatar';
-
-type Size = 'xs' | 'sm' | 'md' | 'lg';
-
-const sizeMap: Record<Size, number> = {
-  xs: 16,
-  sm: 24,
-  md: 40,
-  lg: 64,
-};
-
-interface AvatarProps {
-  border?: boolean;
-  className?: string;
-  enableGravatar?: boolean;
-  gravatarServerUrl?: string;
-  hash?: string;
-  name?: string;
-  organizationAvatar?: string;
-  organizationName?: string;
-  size?: Size;
-}
-
-/**
- * (!) Do not use directly. it requires the gravatar settings to properly fetch the avatars.
- * This is injected by the `Avatar` component in `components/ui` in sonar-web
- */
-export function Avatar({
-  className,
-  enableGravatar,
-  gravatarServerUrl,
-  hash,
-  name,
-  organizationAvatar,
-  organizationName,
-  size = 'sm',
-  border,
-}: AvatarProps) {
-  const [imgError, setImgError] = useState(false);
-  const numberSize = sizeMap[size];
-  const resolvedName = organizationName ?? name;
-
-  const handleImgError: ReactEventHandler<HTMLImageElement> = () => {
-    setImgError(true);
-  };
-
-  if (!imgError) {
-    if (enableGravatar && gravatarServerUrl && hash) {
-      const url = gravatarServerUrl
-        .replace('{EMAIL_MD5}', hash)
-        .replace('{SIZE}', String(numberSize * 2));
-
-      return (
-        <StyledAvatar
-          alt={resolvedName}
-          border={border}
-          className={className}
-          height={numberSize}
-          onError={handleImgError}
-          role="img"
-          src={url}
-          width={numberSize}
-        />
-      );
-    }
-
-    if (resolvedName && organizationAvatar) {
-      return (
-        <StyledAvatar
-          alt={resolvedName}
-          border={border}
-          className={className}
-          height={numberSize}
-          onError={handleImgError}
-          role="img"
-          src={organizationAvatar}
-          width={numberSize}
-        />
-      );
-    }
-  }
-
-  if (!resolvedName) {
-    return <input className="sw-appearance-none" />;
-  }
-
-  return <GenericAvatar className={className} name={resolvedName} size={numberSize} />;
-}
-
-const StyledAvatar = styled.img<{ border?: boolean }>`
-  ${tw`sw-inline-flex`};
-  ${tw`sw-items-center`};
-  ${tw`sw-justify-center`};
-  ${tw`sw-align-top`};
-  ${tw`sw-rounded-1`};
-  border: ${({ border }) => (border ? themeBorder('default', 'avatarBorder') : '')};
-  background: ${themeColor('avatarBackground')};
-`;
diff --git a/server/sonar-web/design-system/src/components/GenericAvatar.tsx b/server/sonar-web/design-system/src/components/GenericAvatar.tsx
deleted file mode 100644 (file)
index 4d8fa69..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.
- */
-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';
-
-export interface GenericAvatarProps {
-  Icon?: React.ComponentType<IconProps>;
-  className?: string;
-  name: string;
-  size?: number;
-}
-
-export function GenericAvatar({ className, Icon, name, size = 24 }: GenericAvatarProps) {
-  const theme = useTheme();
-  const text = name.length > 0 ? name[0].toUpperCase() : '';
-
-  return (
-    <StyledGenericAvatar aria-label={name} className={className} name={name} role="img" size={size}>
-      {Icon ? <Icon fill={themeAvatarColor(name, true)({ theme })} /> : text}
-    </StyledGenericAvatar>
-  );
-}
-
-export const StyledGenericAvatar = styled.div<{ name: string; size: number }>`
-  ${tw`sw-text-center`};
-  ${tw`sw-align-top`};
-  ${tw`sw-select-none`};
-  ${tw`sw-font-regular`};
-  ${tw`sw-rounded-1`};
-  ${tw`sw-inline-flex`};
-  ${tw`sw-items-center`};
-  ${tw`sw-justify-center`};
-  height: ${({ size }) => size}px;
-  width: ${({ size }) => size}px;
-  background-color: ${({ name, theme }) => themeAvatarColor(name)({ theme })};
-  color: ${({ name, theme }) => themeAvatarColor(name, true)({ theme })};
-  font-size: ${({ size }) => Math.max(Math.floor(size / 2), 8)}px;
-  line-height: ${({ size }) => size}px;
-`;
diff --git a/server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Avatar-test.tsx
deleted file mode 100644 (file)
index e18fa03..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.
- */
-
-/* eslint-disable import/no-extraneous-dependencies */
-
-import { fireEvent, screen } from '@testing-library/react';
-import { render } from '../../helpers/testUtils';
-import { FCProps } from '../../types/misc';
-import { Avatar } from '../Avatar';
-
-const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}';
-
-it('should render avatar with border', () => {
-  setupWithProps({ border: true, hash: '7daf6c79d4802916d83f6266e24850af' });
-  expect(screen.getByRole('img')).toHaveStyle('border: 1px solid rgb(225,230,243)');
-});
-
-it('should be able to render with hash only', () => {
-  setupWithProps({ hash: '7daf6c79d4802916d83f6266e24850af' });
-  expect(screen.getByRole('img')).toHaveAttribute(
-    'src',
-    'http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=48'
-  );
-});
-
-it('should fall back to generated on error', () => {
-  setupWithProps({ hash: '7daf6c79d4802916d83f6266e24850af' });
-  fireEvent(screen.getByRole('img'), new Event('error'));
-  expect(screen.getByRole('img')).not.toHaveAttribute('src');
-});
-
-it('should fall back to dummy avatar', () => {
-  setupWithProps({ enableGravatar: false });
-  expect(screen.getByRole('img')).not.toHaveAttribute('src');
-});
-
-it('should return null if no name is set', () => {
-  setupWithProps({ name: undefined });
-  expect(screen.queryByRole('img')).not.toBeInTheDocument();
-});
-
-it('should display organization avatar correctly', () => {
-  const avatar = 'http://example.com/avatar.png';
-  setupWithProps({ organizationAvatar: avatar, organizationName: 'my-org' });
-  expect(screen.getByRole('img')).toHaveAttribute('src', avatar);
-});
-
-function setupWithProps(props: Partial<FCProps<typeof Avatar>> = {}) {
-  return render(
-    <Avatar enableGravatar gravatarServerUrl={gravatarServerUrl} name="foo" {...props} />
-  );
-}
diff --git a/server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx b/server/sonar-web/design-system/src/components/__tests__/GenericAvatar-test.tsx
deleted file mode 100644 (file)
index 83b7fdf..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * 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.
- */
-import { screen } from '@testing-library/react';
-import { render } from '../../helpers/testUtils';
-import { GenericAvatar } from '../GenericAvatar';
-import { CustomIcon, IconProps } from '../icons/Icon';
-
-function TestIcon(props: IconProps) {
-  return (
-    <CustomIcon {...props}>
-      <path d="l10 10" />
-    </CustomIcon>
-  );
-}
-
-it('should render single word and size', () => {
-  render(<GenericAvatar name="foo" size={15} />);
-  const image = screen.getByRole('img');
-  expect(image).toHaveAttribute('size', '15');
-  expect(screen.getByText('F')).toBeInTheDocument();
-});
-
-it('should render multiple word with default size', () => {
-  render(<GenericAvatar name="foo bar" />);
-  const image = screen.getByRole('img');
-  expect(image).toHaveAttribute('size', '24');
-  expect(screen.getByText('F')).toBeInTheDocument();
-});
-
-it('should render without name', () => {
-  render(<GenericAvatar Icon={TestIcon} name="" size={32} />);
-  const image = screen.getByRole('img');
-  expect(image).toHaveAttribute('size', '32');
-});
diff --git a/server/sonar-web/design-system/src/components/avatar/Avatar.tsx b/server/sonar-web/design-system/src/components/avatar/Avatar.tsx
new file mode 100644 (file)
index 0000000..e8e9ede
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+import styled from '@emotion/styled';
+import { ReactEventHandler, useState } from 'react';
+import tw from 'twin.macro';
+import { themeBorder, themeColor } from '../../helpers/theme';
+import { GenericAvatar } from './GenericAvatar';
+import { Size, sizeMap } from './utils';
+
+interface AvatarProps {
+  border?: boolean;
+  className?: string;
+  enableGravatar?: boolean;
+  gravatarServerUrl?: string;
+  hash?: string;
+  name?: string;
+  organizationAvatar?: string;
+  organizationName?: string;
+  size?: Size;
+}
+
+/**
+ * (!) Do not use directly. it requires the gravatar settings to properly fetch the avatars.
+ * This is injected by the `Avatar` component in `components/ui` in sonar-web
+ */
+export function Avatar({
+  className,
+  enableGravatar,
+  gravatarServerUrl,
+  hash,
+  name,
+  organizationAvatar,
+  organizationName,
+  size = 'sm',
+  border,
+}: AvatarProps) {
+  const [imgError, setImgError] = useState(false);
+  const numberSize = sizeMap[size];
+  const resolvedName = organizationName ?? name;
+
+  const handleImgError: ReactEventHandler<HTMLImageElement> = () => {
+    setImgError(true);
+  };
+
+  if (!imgError) {
+    if (enableGravatar && gravatarServerUrl && hash) {
+      const url = gravatarServerUrl
+        .replace('{EMAIL_MD5}', hash)
+        .replace('{SIZE}', String(numberSize * 2));
+
+      return (
+        <StyledAvatar
+          alt={resolvedName}
+          border={border}
+          className={className}
+          height={numberSize}
+          onError={handleImgError}
+          role="img"
+          src={url}
+          width={numberSize}
+        />
+      );
+    }
+
+    if (resolvedName && organizationAvatar) {
+      return (
+        <StyledAvatar
+          alt={resolvedName}
+          border={border}
+          className={className}
+          height={numberSize}
+          onError={handleImgError}
+          role="img"
+          src={organizationAvatar}
+          width={numberSize}
+        />
+      );
+    }
+  }
+
+  if (!resolvedName) {
+    return <input className="sw-appearance-none" />;
+  }
+
+  return <GenericAvatar className={className} name={resolvedName} size={size} />;
+}
+
+const StyledAvatar = styled.img<{ border?: boolean }>`
+  ${tw`sw-inline-flex`};
+  ${tw`sw-items-center`};
+  ${tw`sw-justify-center`};
+  ${tw`sw-align-top`};
+  ${tw`sw-rounded-1`};
+  border: ${({ border }) => (border ? themeBorder('default', 'avatarBorder') : '')};
+  background: ${themeColor('avatarBackground')};
+`;
diff --git a/server/sonar-web/design-system/src/components/avatar/GenericAvatar.tsx b/server/sonar-web/design-system/src/components/avatar/GenericAvatar.tsx
new file mode 100644 (file)
index 0000000..38f4fbe
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+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 { Size, iconSizeMap, sizeMap } from './utils';
+
+export interface GenericAvatarProps {
+  Icon?: React.ComponentType<IconProps>;
+  className?: string;
+  name: string;
+  size?: Size;
+}
+
+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 (
+    <StyledGenericAvatar
+      aria-label={name}
+      className={className}
+      name={name}
+      role="img"
+      size={sizeMap[size]}
+    >
+      {Icon ? (
+        <Icon fill={themeAvatarColor(name, true)({ theme })} height={iconSize} width={iconSize} />
+      ) : (
+        text
+      )}
+    </StyledGenericAvatar>
+  );
+}
+
+export const StyledGenericAvatar = styled.div<{ name: string; size: number }>`
+  ${tw`sw-text-center`};
+  ${tw`sw-align-top`};
+  ${tw`sw-select-none`};
+  ${tw`sw-font-regular`};
+  ${tw`sw-rounded-1`};
+  ${tw`sw-inline-flex`};
+  ${tw`sw-items-center`};
+  ${tw`sw-justify-center`};
+  height: ${({ size }) => size}px;
+  width: ${({ size }) => size}px;
+  background-color: ${({ name, theme }) => themeAvatarColor(name)({ theme })};
+  color: ${({ name, theme }) => themeAvatarColor(name, true)({ theme })};
+  font-size: ${({ size }) => Math.max(Math.floor(size / 2), 8)}px;
+  line-height: ${({ size }) => size}px;
+`;
diff --git a/server/sonar-web/design-system/src/components/avatar/__tests__/Avatar-test.tsx b/server/sonar-web/design-system/src/components/avatar/__tests__/Avatar-test.tsx
new file mode 100644 (file)
index 0000000..2d845c0
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable import/no-extraneous-dependencies */
+
+import { fireEvent, screen } from '@testing-library/react';
+import { render } from '../../../helpers/testUtils';
+import { FCProps } from '../../../types/misc';
+import { Avatar } from '../Avatar';
+
+const gravatarServerUrl = 'http://example.com/{EMAIL_MD5}.jpg?s={SIZE}';
+
+it('should render avatar with border', () => {
+  setupWithProps({ border: true, hash: '7daf6c79d4802916d83f6266e24850af' });
+  expect(screen.getByRole('img')).toHaveStyle('border: 1px solid rgb(225,230,243)');
+});
+
+it('should be able to render with hash only', () => {
+  setupWithProps({ hash: '7daf6c79d4802916d83f6266e24850af' });
+  expect(screen.getByRole('img')).toHaveAttribute(
+    'src',
+    'http://example.com/7daf6c79d4802916d83f6266e24850af.jpg?s=48'
+  );
+});
+
+it('should fall back to generated on error', () => {
+  setupWithProps({ hash: '7daf6c79d4802916d83f6266e24850af' });
+  fireEvent(screen.getByRole('img'), new Event('error'));
+  expect(screen.getByRole('img')).not.toHaveAttribute('src');
+});
+
+it('should fall back to dummy avatar', () => {
+  setupWithProps({ enableGravatar: false });
+  expect(screen.getByRole('img')).not.toHaveAttribute('src');
+});
+
+it('should return null if no name is set', () => {
+  setupWithProps({ name: undefined });
+  expect(screen.queryByRole('img')).not.toBeInTheDocument();
+});
+
+it('should display organization avatar correctly', () => {
+  const avatar = 'http://example.com/avatar.png';
+  setupWithProps({ organizationAvatar: avatar, organizationName: 'my-org' });
+  expect(screen.getByRole('img')).toHaveAttribute('src', avatar);
+});
+
+function setupWithProps(props: Partial<FCProps<typeof Avatar>> = {}) {
+  return render(
+    <Avatar enableGravatar gravatarServerUrl={gravatarServerUrl} name="foo" {...props} />
+  );
+}
diff --git a/server/sonar-web/design-system/src/components/avatar/__tests__/GenericAvatar-test.tsx b/server/sonar-web/design-system/src/components/avatar/__tests__/GenericAvatar-test.tsx
new file mode 100644 (file)
index 0000000..15405a7
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+import { screen } from '@testing-library/react';
+import { render } from '../../../helpers/testUtils';
+import { CustomIcon, IconProps } from '../../icons/Icon';
+import { GenericAvatar } from '../GenericAvatar';
+
+function TestIcon(props: IconProps) {
+  return (
+    <CustomIcon {...props}>
+      <path d="l10 10" />
+    </CustomIcon>
+  );
+}
+
+it('should render single word and size', () => {
+  render(<GenericAvatar name="foo" size="xs" />);
+  const image = screen.getByRole('img');
+  expect(image).toHaveAttribute('size', '16');
+  expect(screen.getByText('F')).toBeInTheDocument();
+});
+
+it('should render multiple word with default size', () => {
+  render(<GenericAvatar name="foo bar" />);
+  const image = screen.getByRole('img');
+  expect(image).toHaveAttribute('size', '24');
+  expect(screen.getByText('F')).toBeInTheDocument();
+});
+
+it('should render without name', () => {
+  render(<GenericAvatar Icon={TestIcon} name="" size="md" />);
+  const image = screen.getByRole('img');
+  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 (file)
index 0000000..bf18571
--- /dev/null
@@ -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<Size, number> = {
+  xs: 16,
+  sm: 24,
+  md: 40,
+  lg: 64,
+};
+
+export const iconSizeMap: Record<Size, number> = {
+  xs: 12,
+  sm: 18,
+  md: 24,
+  lg: 24,
+};
index f8cb3f39ca68b03d58cb66157b47673e0f6c53d7..58b03f1aa65fc29636645a979f3fad4e258e75ac 100644 (file)
@@ -93,10 +93,12 @@ export function OcticonHoc(
   function IconWrapper({ fill, ...props }: IconProps) {
     const theme = useTheme();
 
+    const size = props.width ?? props.height ?? 'small';
+
     return (
       <WrappedOcticon
         fill={fill && themeColor(fill)({ theme })}
-        size="small"
+        size={size}
         verticalAlign="middle"
         {...props}
       />
index b284e595baa18cee2c347d682e24044d0eba2721..34f72bf173b155a7aac8c45b1b5d6c2da9df1092 100644 (file)
@@ -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';
index 7090d3c4c91018d7d6fd242d3a9e90c68ec9be62..8f6bbf78e0e5e0746233d888834e4e9db840cdcc 100644 (file)
@@ -57,10 +57,13 @@ export function SearchSelectDropdownControl(props: SearchSelectDropdownControlPr
       tabIndex={disabled ? -1 : 0}
     >
       <InputValue
-        className={classNames('it__js-search-input-value sw-flex sw-justify-between', {
-          'is-disabled': disabled,
-          'is-placeholder': !label,
-        })}
+        className={classNames(
+          'it__js-search-input-value sw-flex sw-justify-between sw-items-center',
+          {
+            'is-disabled': disabled,
+            'is-placeholder': !label,
+          }
+        )}
       >
         <span className="sw-truncate">{label}</span>
         <ChevronDownIcon className="sw-ml-1" />
index ec30c1f8039e113ea961f90175f3e018f7dd2b5e..ac9ed080937e1def6fb1035ad885757b2d91c203 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 {
+  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 (
-    <div className="display-flex-center permission-list-item padded">
-      {isUser(item) ? (
-        <LegacyAvatar className="spacer-right" hash={item.avatar} name={item.name} size={32} />
-      ) : (
-        <GroupIcon className="pull-left spacer-right" size={32} />
-      )}
+    <>
+      <ContentCell width={0}>
+        {isUser(item) ? (
+          <Avatar hash={item.avatar} name={item.name} size="md" />
+        ) : (
+          <GenericAvatar Icon={UserGroupIcon} name={item.name} size="md" />
+        )}
+      </ContentCell>
 
-      <div className="overflow-hidden flex-1">
-        <strong>{item.name}</strong>
-        {isUser(item) && <div className="note">{item.login}</div>}
-      </div>
+      <ContentCell>
+        <div className="sw-flex sw-flex-col">
+          <strong className="sw-body-sm-highlight">{item.name}</strong>
+          {isUser(item) && <Note>{item.login}</Note>}
+        </div>
+      </ContentCell>
 
-      <DeleteButton
-        onClick={() => props.onClickDelete(item)}
-        data-testid="permission-delete-button"
-      />
-    </div>
+      <ContentCell>
+        <DestructiveIcon
+          aria-label={formatMessage({
+            id: isUser(item)
+              ? 'quality_gates.permissions.remove.user'
+              : 'quality_gates.permissions.remove.group',
+          })}
+          Icon={TrashIcon}
+          onClick={() => props.onClickDelete(item)}
+          data-testid="permission-delete-button"
+        />
+      </ContentCell>
+    </>
   );
 }
index c617b35f7a62411e7998e9bb1e74fa6b0504188c..2a96ec24206f8b52bfc23eaf377deec017e770a3 100644 (file)
@@ -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<Props, State
       if (isUser(item)) {
         this.setState(({ users }) => ({
           users: users.filter((u) => u.login !== item.login),
+          permissionToDelete: undefined,
         }));
       } else {
         this.setState(({ groups }) => ({
           groups: groups.filter((g) => g.name !== item.name),
+          permissionToDelete: undefined,
         }));
       }
     }
index 86846046e10f6a2befa4d34cdf0d0871b756e114..f4e36d6a02ba84eab96bdec2f014dc6490917979 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 } 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<Props, State> {
-  mounted = false;
   state: State = {};
 
   constructor(props: Props) {
@@ -50,15 +51,10 @@ export default class QualityGatePermissionsAddModal extends React.Component<Prop
     this.handleSearch = debounce(this.handleSearch, DEBOUNCE_DELAY);
   }
 
-  componentDidMount() {
-    this.mounted = true;
-  }
-
-  componentWillUnmount() {
-    this.mounted = false;
-  }
-
-  handleSearch = (q: string, resolve: (options: OptionWithValue[]) => void) => {
+  handleSearch = (
+    q: string,
+    resolve: (options: Options<LabelValueSelectOption<UserBase | Group>>) => void
+  ) => {
     const { qualityGate } = this.props;
 
     const queryParams: SearchPermissionsParameters = {
@@ -68,13 +64,18 @@ export default class QualityGatePermissionsAddModal extends React.Component<Prop
     };
 
     Promise.all([searchUsers(queryParams), searchGroups(queryParams)])
-      .then(([usersResponse, groupsResponse]) => [...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<UserBase | Group>) => {
+    this.setState({ selection: value });
   };
 
   handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
index c4cbfa9688ab3f408019e8a8fcdc24608c17fc68..37d49034fa9d43a070f538ff16759f449166ce76 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 { 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<LabelValueSelectOption<UserBase | UserGroup>>) => void
+  ) => void;
+  onSelection: (selection: SingleValue<LabelValueSelectOption<UserBase | UserGroup>>) => void;
+  selection?: UserBase | UserGroup;
   onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => 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 <OptionRenderer option={selection} small />;
+  }, [selection]);
 
   return (
-    <Modal contentLabel={header} onRequestClose={props.onClose}>
-      <header className="modal-head">
-        <h2>{header}</h2>
-      </header>
-      <form onSubmit={props.onSubmit}>
-        <div className="modal-body">
-          <div className="modal-field">
-            <label htmlFor="quality-gate-permissions-add-modal-select-input">
-              {translate('quality_gates.permissions.search')}
-            </label>
-            <SearchSelect
-              inputId="quality-gate-permissions-add-modal-select-input"
+    <Modal
+      onClose={props.onClose}
+      headerTitle={translate('quality_gates.permissions.grant')}
+      body={
+        <form onSubmit={props.onSubmit} id={FORM_ID}>
+          <FormField
+            label={translate('quality_gates.permissions.search')}
+            htmlFor={USER_SELECT_INPUT_ID}
+          >
+            <SearchSelectDropdown
+              controlAriaLabel={translate('quality_gates.permissions.search')}
+              inputId={USER_SELECT_INPUT_ID}
               autoFocus
               isClearable={false}
               placeholder=""
               defaultOptions
-              noOptionsMessage={() => 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,
               }}
             />
-          </div>
-        </div>
-        <footer className="modal-foot">
-          {submitting && <i className="spinner spacer-right" />}
-          <SubmitButton disabled={!selection || submitting}>{translate('add_verb')}</SubmitButton>
-          <ResetButtonLink onClick={props.onClose}>{translate('cancel')}</ResetButtonLink>
-        </footer>
-      </form>
-    </Modal>
+          </FormField>
+        </form>
+      }
+      primaryButton={
+        <ButtonPrimary disabled={!selection || submitting} type="submit" form={FORM_ID}>
+          {translate('add_verb')}
+        </ButtonPrimary>
+      }
+      secondaryButtonLabel={translate('cancel')}
+    />
   );
 }
 
-export function customOptions(option: OptionWithValue) {
+function OptionRenderer({
+  option,
+  small = false,
+}: {
+  option?: UserBase | UserGroup;
+  small?: boolean;
+}) {
+  if (!option) {
+    return null;
+  }
   return (
-    <span className="display-flex-center" data-testid="qg-add-permission-option">
+    <>
       {isUser(option) ? (
-        <LegacyAvatar hash={option.avatar} name={option.name} size={16} />
+        <>
+          <Avatar
+            className={small ? 'sw-my-1/2' : ''}
+            hash={option.avatar}
+            name={option.name}
+            size={small ? 'xs' : 'sm'}
+          />
+          <span className="sw-ml-2">
+            <strong className="sw-body-sm-highlight sw-mr-1">{option.name}</strong>
+            {option.login}
+          </span>
+        </>
       ) : (
-        <GroupIcon size={16} />
+        <>
+          <GenericAvatar
+            className={small ? 'sw-my-1/2' : ''}
+            Icon={UserGroupIcon}
+            name={option.name}
+            size={small ? 'xs' : 'sm'}
+          />
+          <strong className="sw-body-sm-highlight sw-ml-2">{option.name}</strong>
+        </>
       )}
-      <strong className="spacer-left">{option.name}</strong>
-      {isUser(option) && <span className="note little-spacer-left">{option.login}</span>}
-    </span>
+    </>
   );
 }
 
-function optionRenderer(props: OptionProps<OptionWithValue, false>) {
-  return <components.Option {...props}>{customOptions(props.data)}</components.Option>;
-}
-
-function singleValueRenderer(props: SingleValueProps<OptionWithValue, false>) {
-  return <components.SingleValue {...props}>{customOptions(props.data)}</components.SingleValue>;
-}
+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;
 
-function controlRenderer(props: ControlProps<OptionWithValue, false>) {
   return (
-    <components.Control {...omit(props, ['children'])} className="abs-height-100">
-      {props.children}
-    </components.Control>
+    <components.Option {...props}>
+      <div className="sw-flex sw-items-center">
+        <OptionRenderer option={value} />
+      </div>
+    </components.Option>
   );
 }
index 97dd4a14deecb9dd372e93d2266ff53ce654dc53..5434dd3736cb2d7d7ebe4251cc9152254c3d7431 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 {
+  ButtonSecondary,
+  DangerButtonPrimary,
+  Modal,
+  Spinner,
+  SubTitle,
+  Table,
+  TableRowInteractive,
+} from 'design-system';
 import * as React from 'react';
 import { FormattedMessage } from 'react-intl';
-import ConfirmModal from '../../../components/controls/ConfirmModal';
-import { Button } from '../../../components/controls/buttons';
-import Spinner from '../../../components/ui/Spinner';
 import { translate } from '../../../helpers/l10n';
 import { Group, isUser } from '../../../types/quality-gates';
 import { QualityGate } from '../../../types/types';
@@ -50,29 +56,31 @@ export default function QualityGatePermissionsRenderer(props: QualityGatePermiss
     props;
 
   return (
-    <div className="quality-gate-permissions" data-testid="quality-gate-permissions">
-      <h3 className="spacer-bottom">{translate('quality_gates.permissions')}</h3>
-      <p className="spacer-bottom">{translate('quality_gates.permissions.help')}</p>
+    <div data-testid="quality-gate-permissions">
+      <SubTitle as="h3" className="sw-body-md-highlight">
+        {translate('quality_gates.permissions')}
+      </SubTitle>
+      <p className="sw-body-sm sw-mb-2">{translate('quality_gates.permissions.help')}</p>
       <div>
         <Spinner loading={loading}>
-          <ul>
+          <Table columnCount={3} columnWidths={['40px', 'auto', '1%']} width="100%">
             {users.map((user) => (
-              <li key={user.login}>
+              <TableRowInteractive key={user.login}>
                 <PermissionItem onClickDelete={props.onClickDeletePermission} item={user} />
-              </li>
+              </TableRowInteractive>
             ))}
             {groups.map((group) => (
-              <li key={group.name}>
+              <TableRowInteractive key={group.name}>
                 <PermissionItem onClickDelete={props.onClickDeletePermission} item={group} />
-              </li>
+              </TableRowInteractive>
             ))}
-          </ul>
+          </Table>
         </Spinner>
       </div>
 
-      <Button className="big-spacer-top" onClick={props.onClickAddPermission}>
+      <ButtonSecondary className="sw-mt-4" onClick={props.onClickAddPermission}>
         {translate('quality_gates.permissions.grant')}
-      </Button>
+      </ButtonSecondary>
 
       {showAddModal && (
         <QualityGatePermissionsAddModal
@@ -84,30 +92,34 @@ export default function QualityGatePermissionsRenderer(props: QualityGatePermiss
       )}
 
       {permissionToDelete && (
-        <ConfirmModal
-          header={
+        <Modal
+          headerTitle={
             isUser(permissionToDelete)
               ? translate('quality_gates.permissions.remove.user')
               : translate('quality_gates.permissions.remove.group')
           }
-          confirmButtonText={translate('remove')}
-          isDestructive
-          confirmData={permissionToDelete}
+          body={
+            <FormattedMessage
+              defaultMessage={
+                isUser(permissionToDelete)
+                  ? translate('quality_gates.permissions.remove.user.confirmation')
+                  : translate('quality_gates.permissions.remove.group.confirmation')
+              }
+              id="remove.confirmation"
+              values={{
+                user: <strong>{permissionToDelete.name}</strong>,
+              }}
+            />
+          }
+          primaryButton={
+            <DangerButtonPrimary
+              onClick={() => props.onConfirmDeletePermission(permissionToDelete)}
+            >
+              {translate('remove')}
+            </DangerButtonPrimary>
+          }
           onClose={props.onCloseDeletePermission}
-          onConfirm={props.onConfirmDeletePermission}
-        >
-          <FormattedMessage
-            defaultMessage={
-              isUser(permissionToDelete)
-                ? translate('quality_gates.permissions.remove.user.confirmation')
-                : translate('quality_gates.permissions.remove.group.confirmation')
-            }
-            id="remove.confirmation"
-            values={{
-              user: <strong>{permissionToDelete.name}</strong>,
-            }}
-          />
-        </ConfirmModal>
+        />
       )}
     </div>
   );
index bf92f73326a7161f3132c163573c1ceaad5e69ed..3dbcf3e3ddc4ab6027157be4bc4ef6ffffad943a 100644 (file)
@@ -571,7 +571,6 @@ describe('The Permissions section', () => {
     });
     expect(addUserButton).toBeDisabled();
     await user.click(searchUserInput);
-    expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
     await user.click(screen.getByText('userlogin'));
     expect(addUserButton).toBeEnabled();
     await user.click(addUserButton);
@@ -588,7 +587,7 @@ describe('The Permissions section', () => {
     await user.click(cancelButton);
 
     const permissionList = within(await screen.findByTestId('quality-gate-permissions'));
-    expect(permissionList.getByRole('listitem')).toBeInTheDocument();
+    expect(permissionList.getByRole('row')).toBeInTheDocument();
 
     // Delete the user permission
     const deleteButton = screen.getByTestId('permission-delete-button');
@@ -596,7 +595,7 @@ describe('The Permissions section', () => {
     const deletePopup = screen.getByRole('dialog');
     const dialogDeleteButton = within(deletePopup).getByRole('button', { name: 'remove' });
     await user.click(dialogDeleteButton);
-    expect(permissionList.queryByRole('listitem')).not.toBeInTheDocument();
+    expect(permissionList.queryByRole('row')).not.toBeInTheDocument();
   });
 
   it('should assign permission to a group and delete it later', async () => {
@@ -619,8 +618,7 @@ describe('The Permissions section', () => {
       name: 'add_verb',
     });
     await user.click(searchUserInput);
-    expect(screen.getAllByTestId('qg-add-permission-option')).toHaveLength(2);
-    await user.click(screen.getAllByTestId('qg-add-permission-option')[1]);
+    await user.click(within(popup).getByLabelText('Foo'));
     await user.click(addUserButton);
     expect(screen.getByText('Foo')).toBeInTheDocument();