]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22168 Align InputSelect
authorstanislavh <stanislav.honcharov@sonarsource.com>
Thu, 2 May 2024 15:29:48 +0000 (17:29 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 6 May 2024 20:02:40 +0000 (20:02 +0000)
28 files changed:
server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx
server/sonar-web/design-system/src/components/input/DiscreetSelect.tsx
server/sonar-web/design-system/src/components/input/InputSelect.tsx [deleted file]
server/sonar-web/design-system/src/components/input/SearchSelect.tsx
server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx
server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx
server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx [deleted file]
server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx
server/sonar-web/design-system/src/components/input/index.ts
server/sonar-web/design-system/src/sonar-aligned/components/index.ts
server/sonar-web/design-system/src/sonar-aligned/components/input/InputSelect.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/sonar-aligned/components/input/__tests__/InputSelect-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/sonar-aligned/components/input/index.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx
server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx
server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx

index ac651e16c34d114c1ec12f41f313bb564680702c..467fdab81a4bf60ee040dc3fba635a80ecbad3c1 100644 (file)
@@ -33,9 +33,9 @@ import {
   useDayPicker,
 } from 'react-day-picker';
 import { useIntl } from 'react-intl';
+import { InputSelect } from '../../sonar-aligned/components/input';
 import { InteractiveIcon } from '../InteractiveIcon';
 import { ChevronLeftIcon, ChevronRightIcon } from '../icons';
-import { InputSelect } from './InputSelect';
 
 const YEARS_TO_DISPLAY = 10;
 const MONTHS_IN_A_YEAR = 12;
index 13b6068c3fa08be11b2933abf6413b8925210f20..6981cefffc59d92c88e90beb05f238aa689077e9 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import styled from '@emotion/styled';
+import { GroupBase, OnChangeValue } from 'react-select';
 import tw from 'twin.macro';
 import { themeBorder, themeColor, themeContrast } from '../../helpers/theme';
-import { InputSizeKeys } from '../../types/theme';
-import { InputSelect, LabelValueSelectOption } from './InputSelect';
+import { InputSelect, SelectProps } from '../../sonar-aligned/components/input';
 
-interface Props<V> {
-  className?: string;
-  components?: Parameters<typeof InputSelect>[0]['components'];
+type DiscreetProps<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+> = SelectProps<Option, IsMulti, Group> & {
   customValue?: JSX.Element;
-  isDisabled?: boolean;
-  menuIsOpen?: boolean;
-  onMenuClose?: () => void;
-  onMenuOpen?: () => void;
-  options: Array<LabelValueSelectOption<V>>;
-  setValue: ({ value }: LabelValueSelectOption<V>) => void;
-  size?: InputSizeKeys;
-  value: V;
-}
+  setValue: (value: OnChangeValue<Option, IsMulti>) => void;
+};
 
-export function DiscreetSelect<V>({
-  className,
-  customValue,
-  onMenuOpen,
-  options,
-  size = 'small',
-  setValue,
-  value,
-  ...props
-}: Props<V>) {
+export function DiscreetSelect<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+>({ customValue, size = 'small', setValue, ...props }: DiscreetProps<Option, IsMulti, Group>) {
   return (
-    <StyledSelect
-      className={className}
+    <StyledSelect<Option, IsMulti, Group>
       onChange={setValue}
-      onMenuOpen={onMenuOpen}
-      options={options}
       placeholder={customValue}
       size={size}
-      value={options.find((item) => item.value === value)}
       {...props}
     />
   );
@@ -121,4 +107,4 @@ const StyledSelect = styled(InputSelect)`
   & .react-select__control--menu-is-open {
     ${tw`sw-border-none`};
   }
-`;
+` as typeof InputSelect;
diff --git a/server/sonar-web/design-system/src/components/input/InputSelect.tsx b/server/sonar-web/design-system/src/components/input/InputSelect.tsx
deleted file mode 100644 (file)
index 78be3d1..0000000
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 as themeInfo } from '@emotion/react';
-import classNames from 'classnames';
-import { omit } from 'lodash';
-import { ReactNode } from 'react';
-import { useIntl } from 'react-intl';
-import ReactSelect, {
-  ClearIndicatorProps,
-  GroupBase,
-  Props as NamedProps,
-  OptionProps,
-  StylesConfig,
-  components,
-} from 'react-select';
-import { INPUT_SIZES } from '../../helpers';
-import { themeBorder, themeColor, themeContrast } from '../../helpers/theme';
-import { InputSizeKeys } from '../../types/theme';
-import { InteractiveIcon } from '../InteractiveIcon';
-import { SearchHighlighter } from '../SearchHighlighter';
-
-import { ChevronDownIcon, CloseIcon } from '../icons';
-
-export interface LabelValueSelectOption<V> {
-  Icon?: ReactNode;
-  label: string;
-  value: V;
-}
-
-interface ExtensionProps {
-  clearLabel?: string;
-  size?: InputSizeKeys;
-}
-
-export type SelectProps<
-  V,
-  Option extends LabelValueSelectOption<V>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
-> = NamedProps<Option, IsMulti, Group> & ExtensionProps;
-
-export function IconOption<
-  V,
-  Option extends LabelValueSelectOption<V>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
->(props: OptionProps<Option, IsMulti, Group>) {
-  const {
-    data: { label, Icon },
-  } = props;
-
-  return (
-    <components.Option {...props}>
-      <div className="sw-flex sw-items-center sw-gap-1">
-        {Icon}
-        <SearchHighlighter>{label}</SearchHighlighter>
-      </div>
-    </components.Option>
-  );
-}
-
-function SingleValue<
-  V,
-  Option extends LabelValueSelectOption<V>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
->(props: OptionProps<Option, IsMulti, Group>) {
-  const {
-    data: { label, Icon },
-  } = props;
-
-  return (
-    <components.SingleValue {...props}>
-      <div className="sw-flex sw-items-center sw-gap-1">
-        {Icon}
-        {label}
-      </div>
-    </components.SingleValue>
-  );
-}
-
-function ClearIndicator<
-  V,
-  Option extends LabelValueSelectOption<V>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
->(
-  props: ClearIndicatorProps<Option, IsMulti, Group> & {
-    selectProps: SelectProps<V, Option, IsMulti, Group>;
-  },
-) {
-  const intl = useIntl();
-  const {
-    selectProps: { clearLabel },
-  } = props;
-
-  return (
-    <components.ClearIndicator {...props}>
-      <InteractiveIcon
-        Icon={CloseIcon}
-        aria-label={clearLabel ?? intl.formatMessage({ id: 'clear' })}
-        onClick={props.clearValue}
-        size="small"
-      />
-    </components.ClearIndicator>
-  );
-}
-
-function DropdownIndicator<
-  V,
-  Option extends LabelValueSelectOption<V>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
->(props: OptionProps<Option, IsMulti, Group>) {
-  return (
-    <components.DropdownIndicator {...props}>
-      <div className="sw-pr-2 sw-flex">
-        <ChevronDownIcon />
-      </div>
-    </components.DropdownIndicator>
-  );
-}
-
-export function InputSelect<
-  V,
-  Option extends LabelValueSelectOption<V>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
->({ size = 'medium', className, ...props }: SelectProps<V, Option, IsMulti, Group>) {
-  return (
-    <ReactSelect<Option, IsMulti, Group>
-      {...omit(props, 'className', 'large')}
-      className={classNames('react-select', className)}
-      classNamePrefix="react-select"
-      classNames={{
-        container: () => 'sw-relative sw-inline-block sw-align-middle',
-        placeholder: () => 'sw-truncate sw-leading-4',
-        menu: () => 'sw-z-dropdown-menu sw-ml-1/2 sw-mt-2',
-        menuList: () => 'sw-overflow-y-auto sw-py-2 sw-max-h-[12.25rem]',
-        clearIndicator: () => 'sw-p-0',
-        dropdownIndicator: () => classNames(props.isClearable && 'sw-p-0'),
-        control: ({ isDisabled }) =>
-          classNames(
-            'sw-box-border sw-rounded-2 sw-overflow-hidden',
-            isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed',
-          ),
-        option: ({ isDisabled }) =>
-          classNames(
-            'it__select-option sw-py-2 sw-px-3 sw-cursor-pointer',
-            isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed',
-          ),
-        ...props.classNames,
-      }}
-      components={{
-        ClearIndicator,
-        Option: IconOption,
-        SingleValue,
-        DropdownIndicator,
-        IndicatorSeparator: null,
-        ...props.components,
-      }}
-      isClearable={props.isClearable ?? false}
-      isSearchable={props.isSearchable ?? false}
-      onMenuOpen={props.onMenuOpen}
-      styles={selectStyle({ size })}
-    />
-  );
-}
-
-export function selectStyle<
-  V,
-  Option extends LabelValueSelectOption<V>,
-  IsMulti extends boolean = false,
-  Group extends GroupBase<Option> = GroupBase<Option>,
->({ size }: { size: InputSizeKeys }): StylesConfig<Option, IsMulti, Group> {
-  const theme = themeInfo();
-
-  return {
-    control: (base, { isFocused, menuIsOpen, isDisabled }) => ({
-      ...base,
-      color: themeContrast('inputBackground')({ theme }),
-      cursor: 'pointer',
-      background: themeColor('inputBackground')({ theme }),
-      transition: 'border 0.2s ease, outline 0.2s ease',
-      outline: isFocused && !menuIsOpen ? themeBorder('focus', 'inputFocus')({ theme }) : 'none',
-      ...(isDisabled && {
-        color: themeContrast('inputDisabled')({ theme }),
-        background: themeColor('inputDisabled')({ theme }),
-        border: themeBorder('default', 'inputDisabledBorder')({ theme }),
-        outline: 'none',
-      }),
-      ...(isFocused && {
-        border: themeBorder('default', 'inputBorder')({ theme }),
-      }),
-    }),
-    menu: (base) => ({
-      ...base,
-      width: INPUT_SIZES[size],
-    }),
-    option: (base, { isFocused, isSelected }) => ({
-      ...base,
-      ...((isSelected || isFocused) && {
-        background: themeColor('selectOptionSelected')({ theme }),
-        color: themeContrast('primaryLight')({ theme }),
-      }),
-    }),
-    singleValue: (base) => ({
-      ...base,
-      color: themeContrast('primaryLight')({ theme }),
-    }),
-    placeholder: (base) => ({
-      ...base,
-      color: themeContrast('inputPlaceholder')({ theme }),
-    }),
-  };
-}
index 673e8385ad6f77804c8156fd0fd4b6d5f56444e9..009dbb9d712a9e063c0d61d9cfaa11595a54e9e5 100644 (file)
  */
 import classNames from 'classnames';
 import { omit } from 'lodash';
-import React, { RefObject } from 'react';
+import React from 'react';
 import { GroupBase, InputProps } from 'react-select';
 import AsyncSelect, { AsyncProps } from 'react-select/async';
-import Select from 'react-select/dist/declarations/src/Select';
 import { INPUT_SIZES } from '../../helpers';
 import { Key } from '../../helpers/keyboard';
+import { SelectProps, selectStyle } from '../../sonar-aligned/components/input';
 import { InputSearch } from './InputSearch';
-import { LabelValueSelectOption, SelectProps, selectStyle } from './InputSelect';
 
 type SearchSelectProps<
-  V,
-  Option extends LabelValueSelectOption<V>,
+  Option,
   IsMulti extends boolean = false,
   Group extends GroupBase<Option> = GroupBase<Option>,
-> = SelectProps<V, Option, IsMulti, Group> & AsyncProps<Option, IsMulti, Group>;
+> = SelectProps<Option, IsMulti, Group> & AsyncProps<Option, IsMulti, Group>;
 
 export function SearchSelect<
-  V,
-  Option extends LabelValueSelectOption<V>,
+  Option,
   IsMulti extends boolean = false,
   Group extends GroupBase<Option> = GroupBase<Option>,
->({
-  size = 'full',
-  selectRef,
-  ...props
-}: SearchSelectProps<V, Option, IsMulti, Group> & {
-  selectRef?: RefObject<Select<Option, IsMulti, Group>>;
-}) {
-  const styles = selectStyle<V, Option, IsMulti, Group>({ size });
+>({ size = 'full', selectRef, ...props }: SearchSelectProps<Option, IsMulti, Group>) {
+  const styles = selectStyle<Option, IsMulti, Group>({ size });
   return (
     <AsyncSelect<Option, IsMulti, Group>
       {...omit(props, 'className', 'large')}
@@ -82,8 +73,7 @@ export function SearchSelect<
 }
 
 export function SearchSelectInput<
-  V,
-  Option extends LabelValueSelectOption<V>,
+  Option,
   IsMulti extends boolean = false,
   Group extends GroupBase<Option> = GroupBase<Option>,
 >(props: InputProps<Option, IsMulti, Group>) {
index 358197c0621e47cce591d2b8392e62d36c5da3f8..e103e3e4b5d5aa770fc8fbd73f51e216a65ef9e2 100644 (file)
@@ -30,10 +30,14 @@ import { AsyncProps } from 'react-select/async';
 import Select from 'react-select/dist/declarations/src/Select';
 import tw from 'twin.macro';
 import { PopupPlacement, PopupZLevel, themeBorder } from '../../helpers';
+import {
+  IconOption,
+  LabelValueSelectOption,
+  SelectProps,
+} from '../../sonar-aligned/components/input';
 import { InputSizeKeys } from '../../types/theme';
 import { DropdownToggler } from '../DropdownToggler';
 import { SearchHighlighterContext } from '../SearchHighlighter';
-import { IconOption, LabelValueSelectOption, SelectProps } from './InputSelect';
 import { SearchSelect } from './SearchSelect';
 import { SearchSelectDropdownControl } from './SearchSelectDropdownControl';
 
@@ -48,7 +52,7 @@ export interface SearchSelectDropdownProps<
   Option extends LabelValueSelectOption<V>,
   IsMulti extends boolean = false,
   Group extends GroupBase<Option> = GroupBase<Option>,
-> extends SelectProps<V, Option, IsMulti, Group>,
+> extends SelectProps<Option, IsMulti, Group>,
     AsyncProps<Option, IsMulti, Group> {
   className?: string;
   controlAriaLabel?: string;
index 36666870f477b3089de4412eb09ce7e1dedd15f1..4b8c057021d2102d9bfd8c75a6a0416ae6716414 100644 (file)
@@ -24,7 +24,7 @@ import { FCProps } from '../../../types/misc';
 import { DiscreetSelect } from '../DiscreetSelect';
 
 it('should render discreet select and invoke CB on value click', async () => {
-  const value = 'foo';
+  const value = options[0];
   const setValue = jest.fn();
 
   const user = userEvent.setup();
@@ -36,24 +36,21 @@ it('should render discreet select and invoke CB on value click', async () => {
   expect(setValue).toHaveBeenCalled();
 });
 
+const options = [
+  { label: 'foo-bar', value: 'foo', default: 1 },
+  {
+    label: 'bar-foo',
+    value: 'bar',
+    Icon: (
+      <span role="note" title="Icon">
+        Icon
+      </span>
+    ),
+  },
+];
+
 function setupWithProps(props: Partial<FCProps<typeof DiscreetSelect>>) {
   return render(
-    <DiscreetSelect
-      options={[
-        { label: 'foo-bar', value: 'foo' },
-        {
-          label: 'bar-foo',
-          value: 'bar',
-          Icon: (
-            <span role="note" title="Icon">
-              Icon
-            </span>
-          ),
-        },
-      ]}
-      setValue={jest.fn()}
-      value="foo"
-      {...props}
-    />,
+    <DiscreetSelect options={options} setValue={jest.fn()} value={options[0]} {...props} />,
   );
 }
diff --git a/server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx
deleted file mode 100644 (file)
index 7079cf5..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2024 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 userEvent from '@testing-library/user-event';
-import { renderWithContext } from '../../../helpers/testUtils';
-import { FCProps } from '../../../types/misc';
-import { InputSelect } from '../InputSelect';
-
-it('should render select input and be able to click and change', async () => {
-  const setValue = jest.fn();
-  const user = userEvent.setup();
-  setupWithProps({ placeholder: 'placeholder-foo', onChange: setValue });
-  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
-  await user.click(screen.getByRole('combobox'));
-  expect(screen.getByText(/option foo-bar focused/)).toBeInTheDocument();
-  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
-  await user.click(screen.getByText('bar-foo'));
-  expect(setValue).toHaveBeenCalled();
-  expect(screen.queryByText('placeholder-foo')).not.toBeInTheDocument();
-  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
-});
-
-it('should render select input with clearable', async () => {
-  const setValue = jest.fn();
-  const user = userEvent.setup();
-  setupWithProps({
-    placeholder: 'placeholder-foo',
-    onChange: setValue,
-    isClearable: true,
-    clearLabel: 'clear-label',
-  });
-  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
-  await user.click(screen.getByRole('combobox'));
-  expect(screen.getByText(/option foo-bar focused/)).toBeInTheDocument();
-  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
-  await user.click(screen.getByText('bar-foo'));
-  expect(setValue).toHaveBeenCalled();
-  expect(screen.queryByText('placeholder-foo')).not.toBeInTheDocument();
-  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
-
-  // Clear button container aria-hidden by default
-  expect(screen.getByRole('button', { name: 'clear-label', hidden: true })).toBeInTheDocument();
-  await user.click(screen.getByRole('button', { name: 'clear-label', hidden: true }));
-  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
-});
-
-it('should render select input with disabled prop', () => {
-  const setValue = jest.fn();
-  setupWithProps({ placeholder: 'placeholder-foo', onChange: setValue, isDisabled: true });
-  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
-  expect(screen.getByRole('combobox')).toBeDisabled();
-});
-
-function setupWithProps(props: Partial<FCProps<typeof InputSelect>>) {
-  return renderWithContext(
-    <InputSelect
-      {...props}
-      options={[
-        { label: 'foo-bar', value: 'foo' },
-        {
-          label: 'bar-foo',
-          value: 'bar',
-          Icon: (
-            <span role="note" title="Icon">
-              Icon
-            </span>
-          ),
-        },
-      ]}
-    />,
-  );
-}
index 0e0c83c33bc861a0c5be16baafb7de57ffef2d72..893f886f8c427ac0ab3d1078f0fc5b2b62499ab0 100644 (file)
@@ -20,8 +20,8 @@
 import { screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { renderWithContext } from '../../../helpers/testUtils';
+import { LabelValueSelectOption } from '../../../sonar-aligned';
 import { FCProps } from '../../../types/misc';
-import { LabelValueSelectOption } from '../InputSelect';
 import { SearchSelectDropdown } from '../SearchSelectDropdown';
 
 const defaultOptions = [
index 1545de7f2503e6fc7e5f5e8059b3575848528cf7..958e8a66e9687801c944cb067afd4ff618167bfe 100644 (file)
@@ -26,7 +26,6 @@ export * from './FormField';
 export * from './InputField';
 export * from './InputMultiSelect';
 export * from './InputSearch';
-export * from './InputSelect';
 export * from './MultiSelectMenu';
 export * from './RadioButton';
 export * from './SearchSelect';
index edbf22d1b666d5ee73e9dd2d08329a54acd7dcfe..9b86ae7d91a8ce4e31e0ee87cb269df6eec1a019 100644 (file)
@@ -24,4 +24,5 @@ export * from './MetricsRatingBadge';
 export * from './Table';
 export * from './ToggleButton';
 export * from './buttons';
+export * from './input';
 export * from './typography';
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/input/InputSelect.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/input/InputSelect.tsx
new file mode 100644 (file)
index 0000000..58d1d49
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 classNames from 'classnames';
+import { omit } from 'lodash';
+import { useMemo } from 'react';
+import ReactSelect, { GroupBase } from 'react-select';
+import {
+  ClearIndicator,
+  DropdownIndicator,
+  IconOption,
+  SelectProps,
+  SingleValue,
+  selectStyle,
+} from './SelectCommon';
+
+export function InputSelect<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+>({
+  size = 'medium',
+  className,
+  options,
+  getOptionLabel,
+  selectRef,
+  shouldSortOption = false,
+  ...props
+}: SelectProps<Option, IsMulti, Group>) {
+  const orderedOptions = useMemo(() => {
+    if (!options || options.length === 0) {
+      return options;
+    }
+
+    if (shouldSortOption) {
+      return (options as Option[]).sort((a, b) => {
+        const nameA = getOptionLabel?.(a).toUpperCase() ?? '';
+        const nameB = getOptionLabel?.(b).toUpperCase() ?? '';
+        if (nameA < nameB) {
+          return -1;
+        }
+        if (nameA > nameB) {
+          return 1;
+        }
+
+        return 0;
+      });
+    }
+
+    return options;
+  }, [shouldSortOption, getOptionLabel, options]);
+
+  return (
+    <ReactSelect<Option, IsMulti, Group>
+      {...omit(props, 'className', 'large')}
+      className={classNames('react-select', className)}
+      classNamePrefix="react-select"
+      classNames={{
+        container: () => 'sw-relative sw-inline-block sw-align-middle',
+        placeholder: () => 'sw-truncate sw-leading-4',
+        menu: () => 'sw-z-dropdown-menu sw-ml-1/2 sw-mt-2',
+        menuList: () => 'sw-overflow-y-auto sw-py-2 sw-max-h-[12.25rem]',
+        clearIndicator: () => 'sw-p-0',
+        dropdownIndicator: () => classNames(props.isClearable && 'sw-p-0'),
+        control: ({ isDisabled }) =>
+          classNames(
+            'sw-box-border sw-rounded-2 sw-overflow-hidden',
+            isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed',
+          ),
+        option: ({ isDisabled }) =>
+          classNames(
+            'it__select-option sw-py-2 sw-px-3 sw-cursor-pointer',
+            isDisabled && 'sw-pointer-events-none sw-cursor-not-allowed',
+          ),
+        ...props.classNames,
+      }}
+      components={{
+        ClearIndicator,
+        Option: IconOption,
+        SingleValue,
+        DropdownIndicator,
+        IndicatorSeparator: null,
+        ...props.components,
+      }}
+      getOptionLabel={getOptionLabel}
+      isClearable={props.isClearable ?? false}
+      isSearchable={props.isSearchable ?? false}
+      onMenuOpen={props.onMenuOpen}
+      options={orderedOptions}
+      ref={selectRef}
+      styles={selectStyle({ size })}
+    />
+  );
+}
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx
new file mode 100644 (file)
index 0000000..8414e6f
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 as themeInfo } from '@emotion/react';
+import { RefObject } from 'react';
+import { useIntl } from 'react-intl';
+import {
+  ClearIndicatorProps,
+  GroupBase,
+  Props as NamedProps,
+  OptionProps,
+  StylesConfig,
+  components,
+} from 'react-select';
+import Select from 'react-select/dist/declarations/src/Select';
+import { InteractiveIcon } from '../../../components/InteractiveIcon';
+import { SearchHighlighter } from '../../../components/SearchHighlighter';
+import { ChevronDownIcon, CloseIcon } from '../../../components/icons';
+import { INPUT_SIZES } from '../../../helpers';
+import { themeBorder, themeColor, themeContrast } from '../../../helpers/theme';
+import { InputSizeKeys } from '../../../types/theme';
+
+export interface ExtensionProps<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+> {
+  clearLabel?: string;
+  selectRef?: RefObject<Select<Option, IsMulti, Group>>;
+  shouldSortOption?: boolean;
+  size?: InputSizeKeys;
+}
+
+export type SelectProps<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+> = NamedProps<Option, IsMulti, Group> & ExtensionProps<Option, IsMulti, Group>;
+
+export function IconOption<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+>(props: OptionProps<Option, IsMulti, Group>) {
+  const { label, isSelected } = props;
+  const { Icon } = props.data as { Icon: JSX.Element };
+
+  return (
+    <components.Option {...props}>
+      <div aria-selected={isSelected} className="sw-flex sw-items-center sw-gap-1" role="option">
+        {Icon}
+        <SearchHighlighter>{label}</SearchHighlighter>
+      </div>
+    </components.Option>
+  );
+}
+
+export function SingleValue<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+>(props: OptionProps<Option, IsMulti, Group>) {
+  const label = props.selectProps.getOptionLabel(props.data);
+  const { Icon } = props.data as { Icon: JSX.Element };
+
+  return (
+    <components.SingleValue {...props}>
+      <div className="sw-flex sw-items-center sw-gap-1">
+        {Icon}
+        {label}
+      </div>
+    </components.SingleValue>
+  );
+}
+
+export function ClearIndicator<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+>(
+  props: ClearIndicatorProps<Option, IsMulti, Group> & {
+    selectProps: SelectProps<Option, IsMulti, Group>;
+  },
+) {
+  const intl = useIntl();
+  const {
+    selectProps: { clearLabel },
+  } = props;
+
+  return (
+    <components.ClearIndicator {...props}>
+      <InteractiveIcon
+        Icon={CloseIcon}
+        aria-label={clearLabel ?? intl.formatMessage({ id: 'clear' })}
+        onClick={props.clearValue}
+        size="small"
+      />
+    </components.ClearIndicator>
+  );
+}
+
+export function DropdownIndicator<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+>(props: OptionProps<Option, IsMulti, Group>) {
+  return (
+    <components.DropdownIndicator {...props}>
+      <div className="sw-pr-2 sw-flex">
+        <ChevronDownIcon />
+      </div>
+    </components.DropdownIndicator>
+  );
+}
+
+export function selectStyle<
+  Option,
+  IsMulti extends boolean = false,
+  Group extends GroupBase<Option> = GroupBase<Option>,
+>({ size }: { size: InputSizeKeys }): StylesConfig<Option, IsMulti, Group> {
+  const theme = themeInfo();
+
+  return {
+    control: (base, { isFocused, menuIsOpen, isDisabled }) => ({
+      ...base,
+      color: themeContrast('inputBackground')({ theme }),
+      cursor: 'pointer',
+      background: themeColor('inputBackground')({ theme }),
+      transition: 'border 0.2s ease, outline 0.2s ease',
+      outline: isFocused && !menuIsOpen ? themeBorder('focus', 'inputFocus')({ theme }) : 'none',
+      ...(isDisabled && {
+        color: themeContrast('inputDisabled')({ theme }),
+        background: themeColor('inputDisabled')({ theme }),
+        border: themeBorder('default', 'inputDisabledBorder')({ theme }),
+        outline: 'none',
+      }),
+      ...(isFocused && {
+        border: themeBorder('default', 'inputBorder')({ theme }),
+      }),
+    }),
+    menu: (base) => ({
+      ...base,
+      width: INPUT_SIZES[size],
+    }),
+    option: (base, { isFocused, isSelected }) => ({
+      ...base,
+      ...((isSelected || isFocused) && {
+        background: themeColor('selectOptionSelected')({ theme }),
+        color: themeContrast('primaryLight')({ theme }),
+      }),
+    }),
+    singleValue: (base) => ({
+      ...base,
+      color: themeContrast('primaryLight')({ theme }),
+    }),
+    placeholder: (base) => ({
+      ...base,
+      color: themeContrast('inputPlaceholder')({ theme }),
+    }),
+  };
+}
+
+export interface LabelValueSelectOption<V = string> {
+  Icon?: React.ReactNode;
+  label: string;
+  value: V;
+}
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/input/__tests__/InputSelect-test.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/input/__tests__/InputSelect-test.tsx
new file mode 100644 (file)
index 0000000..f94686b
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 userEvent from '@testing-library/user-event';
+import { renderWithContext } from '../../../../helpers/testUtils';
+import { FCProps } from '../../../../types/misc';
+import { InputSelect } from '../InputSelect';
+
+it('should render select input and be able to click and change', async () => {
+  const setValue = jest.fn();
+  const user = userEvent.setup();
+  setupWithProps({ placeholder: 'placeholder-foo', onChange: setValue });
+  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
+  await user.click(screen.getByRole('combobox'));
+  expect(screen.getByText(/option foo-bar focused/)).toBeInTheDocument();
+  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
+  await user.click(screen.getByText('bar-foo'));
+  expect(setValue).toHaveBeenCalled();
+  expect(screen.queryByText('placeholder-foo')).not.toBeInTheDocument();
+  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
+});
+
+it('should render select input with clearable', async () => {
+  const setValue = jest.fn();
+  const user = userEvent.setup();
+  setupWithProps({
+    placeholder: 'placeholder-foo',
+    onChange: setValue,
+    isClearable: true,
+    clearLabel: 'clear-label',
+  });
+  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
+  await user.click(screen.getByRole('combobox'));
+  expect(screen.getByText(/option foo-bar focused/)).toBeInTheDocument();
+  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
+  await user.click(screen.getByText('bar-foo'));
+  expect(setValue).toHaveBeenCalled();
+  expect(screen.queryByText('placeholder-foo')).not.toBeInTheDocument();
+  expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument();
+
+  // Clear button container aria-hidden by default
+  expect(screen.getByRole('button', { name: 'clear-label', hidden: true })).toBeInTheDocument();
+  await user.click(screen.getByRole('button', { name: 'clear-label', hidden: true }));
+  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
+});
+
+it('should render select input with disabled prop', () => {
+  const setValue = jest.fn();
+  setupWithProps({ placeholder: 'placeholder-foo', onChange: setValue, isDisabled: true });
+  expect(screen.getByText('placeholder-foo')).toBeInTheDocument();
+  expect(screen.getByRole('combobox')).toBeDisabled();
+});
+
+it('should render the select options with sorting when shouldSortOption is true and getOptionLabel passed', async () => {
+  const { user } = setupWithProps({
+    shouldSortOption: true,
+    getOptionLabel: (o: { label: string }) => o.label,
+  });
+  await user.click(screen.getByRole('combobox'));
+  const options = screen.getAllByRole('option');
+  expect(options).toHaveLength(2);
+  expect(options[0]).toHaveTextContent('bar-foo');
+  expect(options[1]).toHaveTextContent('foo-bar');
+});
+
+function setupWithProps(props: Partial<FCProps<typeof InputSelect>>) {
+  return renderWithContext(
+    <InputSelect
+      {...props}
+      options={[
+        { label: 'foo-bar', value: 'foo' },
+        {
+          label: 'bar-foo',
+          value: 'bar',
+          Icon: (
+            <span role="note" title="Icon">
+              Icon
+            </span>
+          ),
+        },
+      ]}
+    />,
+  );
+}
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/input/index.ts b/server/sonar-web/design-system/src/sonar-aligned/components/input/index.ts
new file mode 100644 (file)
index 0000000..b8fd68f
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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 * from './InputSelect';
+export * from './SelectCommon';
index fc29f14295f8c8c17fc1bd6bcbaa7e1282290cdb..22fb8307f43319663cfcdd9feb1eba87fd3dc4e5 100644 (file)
@@ -32,7 +32,7 @@ interface StatusFilterProps {
 export default function StatusFilter(props: Readonly<StatusFilterProps>) {
   const { id, value, onChange } = props;
 
-  const options: LabelValueSelectOption<string>[] = [
+  const options: LabelValueSelectOption[] = [
     { value: STATUSES.ALL, label: translate('background_task.status.ALL') },
     {
       value: STATUSES.ALL_EXCEPT_PENDING,
@@ -46,7 +46,7 @@ export default function StatusFilter(props: Readonly<StatusFilterProps>) {
   ];
 
   const handleChange = React.useCallback(
-    ({ value }: LabelValueSelectOption<string>) => {
+    ({ value }: LabelValueSelectOption) => {
       onChange(value);
     },
     [onChange],
index d2d21891ae8ccbb98a576dba58d20a6d7323b3aa..9a7a620c706dd71a151ddfa149061581e0fcc511 100644 (file)
@@ -30,7 +30,7 @@ interface Props {
 }
 
 export default class TypesFilter extends React.PureComponent<Props> {
-  handleChange = ({ value }: LabelValueSelectOption<string>) => {
+  handleChange = ({ value }: LabelValueSelectOption) => {
     this.props.onChange(value);
   };
 
@@ -43,7 +43,7 @@ export default class TypesFilter extends React.PureComponent<Props> {
       };
     });
 
-    const allOptions: LabelValueSelectOption<string>[] = [
+    const allOptions: LabelValueSelectOption[] = [
       { value: ALL_TYPES, label: translate('background_task.type.ALL') },
       ...options,
     ];
index fce45c211b2960db5574fa4e7efeae8b4f5b0bd1..4f7142ab3a9d0a5d5e508d56d8d926dd70fb4383 100644 (file)
@@ -224,7 +224,7 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
   );
 
   const StatusField = React.useMemo(() => {
-    const statusesOptions = RULE_STATUSES.map((status) => ({
+    const statusesOptions = RULE_STATUSES.map((status: Status) => ({
       label: translate('rules.status', status),
       value: status,
     }));
index 7ef68d6dea5aac14ad0127ca97f81bd3c94908f8..7a0fb275869d8285356d7a75b7189fea48546218 100644 (file)
@@ -304,7 +304,7 @@ export default function AzureProjectCreate({
 function transformToOptions(
   projects: AzureProject[],
   repositories?: Dict<AzureRepository[]>,
-): Array<GroupBase<LabelValueSelectOption<string>>> {
+): Array<GroupBase<LabelValueSelectOption>> {
   return projects.map(({ name: projectName }) => ({
     label: projectName,
     options:
@@ -314,6 +314,6 @@ function transformToOptions(
   }));
 }
 
-function transformToOption({ name }: AzureRepository): LabelValueSelectOption<string> {
+function transformToOption({ name }: AzureRepository): LabelValueSelectOption {
   return { value: name, label: name };
 }
index 1c206512972e9ba577de904855441c34bf324b5d..a9081c898f5ca5a618292b77588b6de2d8b05e1b 100644 (file)
@@ -212,9 +212,6 @@ export default function BitbucketCloudProjectCreate(props: Readonly<Props>) {
   );
 }
 
-function transformToOption({
-  name,
-  slug,
-}: BitbucketCloudRepository): LabelValueSelectOption<string> {
+function transformToOption({ name, slug }: BitbucketCloudRepository): LabelValueSelectOption {
   return { value: slug, label: name };
 }
index 9eab22d95f5ccbd74e59fbbfaf188bac55fa1d9b..e22904aedf153c1d592403f5299d647f66a22b06 100644 (file)
@@ -256,6 +256,6 @@ export default function GitHubProjectCreate(props: Readonly<Props>) {
 function transformToOption({
   key,
   name,
-}: GithubOrganization | GithubRepository): LabelValueSelectOption<string> {
+}: GithubOrganization | GithubRepository): LabelValueSelectOption {
   return { value: key, label: name };
 }
index 6b72f0db23420a81e5b36a0f1d88c90b7ae946fd..a5ff527ff4ecfea53ee91c81820765c78cfd8652 100644 (file)
@@ -200,6 +200,6 @@ export default function GitlabProjectCreate(props: Readonly<Props>) {
   );
 }
 
-function transformToOption({ id, name }: GitlabProject): LabelValueSelectOption<string> {
+function transformToOption({ id, name }: GitlabProject): LabelValueSelectOption {
   return { value: id, label: name };
 }
index 3acbfa32de7052a3addfd65091298b96cef93403..781e9aa77b6ce0adcbb99306e14bf1536f5a8e5c 100644 (file)
@@ -33,10 +33,10 @@ export const MIN_QUERY_LENGTH = 2;
 const UNASSIGNED = { value: '', label: translate('unassigned') };
 
 export interface AssigneeSelectProps {
-  assignee?: SingleValue<LabelValueSelectOption<string>>;
+  assignee?: SingleValue<LabelValueSelectOption>;
   className?: string;
   issues: Issue[];
-  onAssigneeSelect: (assignee: SingleValue<LabelValueSelectOption<string>>) => void;
+  onAssigneeSelect: (assignee: SingleValue<LabelValueSelectOption>) => void;
   inputId: string;
 }
 
index 3721dfe716cba441b707d5f7fc92ef40977d02b9..439af6367832ee2c7488d3122be286bf951ad4de 100644 (file)
@@ -54,7 +54,7 @@ interface Props {
 
 interface FormFields {
   addTags?: Array<string>;
-  assignee?: SingleValue<LabelValueSelectOption<string>>;
+  assignee?: SingleValue<LabelValueSelectOption>;
   comment?: string;
   notifications?: boolean;
   removeTags?: Array<string>;
@@ -126,7 +126,7 @@ export class BulkChangeModal extends React.PureComponent<Props, State> {
     return this.props.fetchIssues({ additionalFields: 'actions,transitions', ps: MAX_PAGE_SIZE });
   };
 
-  handleAssigneeSelect = (assignee: SingleValue<LabelValueSelectOption<string>>) => {
+  handleAssigneeSelect = (assignee: SingleValue<LabelValueSelectOption>) => {
     this.setState({ assignee });
   };
 
index 78b18822c402b2ff74510b90210131e8d5cb13d3..2fe4a93fb8b4c7aaac3e7afeba6a26c3b1d240fd 100644 (file)
@@ -89,7 +89,7 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
     }
   };
 
-  handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => {
+  handlePermissionTemplateChange = ({ value }: LabelValueSelectOption) => {
     this.setState({ permissionTemplate: value });
   };
 
index 578135e7c6ecc766876261d20d99ae4b756fcbf7..7b76d0800d8be4f3727ea8e1aa5a39388896a457 100644 (file)
@@ -45,7 +45,7 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil
   const eventTypes = isApp
     ? Object.values(ApplicationAnalysisEventCategory)
     : Object.values(ProjectAnalysisEventCategory);
-  const options: LabelValueSelectOption<string>[] = eventTypes.map((category) => ({
+  const options: LabelValueSelectOption[] = eventTypes.map((category) => ({
     label: translate('event.category', category),
     value: category,
   }));
@@ -64,7 +64,7 @@ export default function ProjectActivityPageFilters(props: ProjectActivityPageFil
           aria-label={translate('project_activity.filter_events')}
           className="sw-mr-8 sw-body-sm sw-w-abs-200"
           isClearable
-          onChange={(data: LabelValueSelectOption<string>) => handleCategoryChange(data)}
+          onChange={(data: LabelValueSelectOption) => handleCategoryChange(data)}
           options={options}
           placeholder={translate('project_activity.filter_events')}
           size="full"
index 49b50a6b71ba1b31196de85b6a01a92d8edce802..2d4a69f12f3b09d42007962659da5d3b9eec46a2 100644 (file)
@@ -57,7 +57,7 @@ export default class PerspectiveSelect extends React.PureComponent<Props> {
         <InputSelect
           aria-labelledby="aria-projects-perspective"
           className="sw-mr-4 sw-body-sm"
-          onChange={(data: LabelValueSelectOption<string>) => this.handleChange(data)}
+          onChange={(data: LabelValueSelectOption) => this.handleChange(data)}
           options={options}
           placeholder={translate('project_activity.filter_events')}
           size="small"
index cc84a71c1274e16cc2f337e014d7d2a81ea8f6e6..347f3ae37de56f804906d8ecd28bf7397e144c70 100644 (file)
@@ -130,7 +130,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
     }
   };
 
-  handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => {
+  handlePermissionTemplateChange = ({ value }: LabelValueSelectOption) => {
     this.setState({ permissionTemplate: value });
   };
 
index 963f7f8302d7e1b50c480cf19ce780be1804c4b2..363abc3a087febcc67dd73de3874bbe4cd3d6e13 100644 (file)
@@ -77,7 +77,9 @@ export default function QualityGatePermissionsAddModalRenderer(
               noOptionsMessage={() => translate('no_results')}
               onChange={props.onSelection}
               loadOptions={props.handleSearch}
-              getOptionValue={({ value }) => (isUser(value) ? value.login : value.name)}
+              getOptionValue={({ value }: LabelValueSelectOption<UserBase | UserGroup>) =>
+                isUser(value) ? value.login : value.name
+              }
               controlLabel={renderedSelection}
               components={{
                 Option,