aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2024-05-02 17:29:48 +0200
committersonartech <sonartech@sonarsource.com>2024-05-06 20:02:40 +0000
commit422081485bf8e9646921f48d7b1f8d54b9854bd7 (patch)
tree4f05d3e9ff70bc1b3788bdd5aa48ffca03999c44
parent6e3bc57e5e081c223b9d527f4de45c6b90e132f5 (diff)
downloadsonarqube-422081485bf8e9646921f48d7b1f8d54b9854bd7.tar.gz
sonarqube-422081485bf8e9646921f48d7b1f8d54b9854bd7.zip
SONAR-22168 Align InputSelect
-rw-r--r--server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx2
-rw-r--r--server/sonar-web/design-system/src/components/input/DiscreetSelect.tsx46
-rw-r--r--server/sonar-web/design-system/src/components/input/SearchSelect.tsx26
-rw-r--r--server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx8
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx33
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx2
-rw-r--r--server/sonar-web/design-system/src/components/input/index.ts1
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/index.ts1
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/input/InputSelect.tsx110
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx (renamed from server/sonar-web/design-system/src/components/input/InputSelect.tsx)126
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/input/__tests__/InputSelect-test.tsx (renamed from server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx)16
-rw-r--r--server/sonar-web/design-system/src/sonar-aligned/components/input/index.ts22
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx4
-rw-r--r--server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx2
-rw-r--r--server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx4
26 files changed, 254 insertions, 184 deletions
diff --git a/server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx b/server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx
index ac651e16c34..467fdab81a4 100644
--- a/server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx
+++ b/server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx
@@ -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;
diff --git a/server/sonar-web/design-system/src/components/input/DiscreetSelect.tsx b/server/sonar-web/design-system/src/components/input/DiscreetSelect.tsx
index 13b6068c3fa..6981cefffc5 100644
--- a/server/sonar-web/design-system/src/components/input/DiscreetSelect.tsx
+++ b/server/sonar-web/design-system/src/components/input/DiscreetSelect.tsx
@@ -18,44 +18,30 @@
* 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/SearchSelect.tsx b/server/sonar-web/design-system/src/components/input/SearchSelect.tsx
index 673e8385ad6..009dbb9d712 100644
--- a/server/sonar-web/design-system/src/components/input/SearchSelect.tsx
+++ b/server/sonar-web/design-system/src/components/input/SearchSelect.tsx
@@ -19,35 +19,26 @@
*/
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>) {
diff --git a/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx b/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx
index 358197c0621..e103e3e4b5d 100644
--- a/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx
+++ b/server/sonar-web/design-system/src/components/input/SearchSelectDropdown.tsx
@@ -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;
diff --git a/server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx
index 36666870f47..4b8c057021d 100644
--- a/server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx
+++ b/server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx
@@ -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__/SearchSelectDropdown-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx
index 0e0c83c33bc..893f886f8c4 100644
--- a/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx
+++ b/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx
@@ -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 = [
diff --git a/server/sonar-web/design-system/src/components/input/index.ts b/server/sonar-web/design-system/src/components/input/index.ts
index 1545de7f250..958e8a66e96 100644
--- a/server/sonar-web/design-system/src/components/input/index.ts
+++ b/server/sonar-web/design-system/src/components/input/index.ts
@@ -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';
diff --git a/server/sonar-web/design-system/src/sonar-aligned/components/index.ts b/server/sonar-web/design-system/src/sonar-aligned/components/index.ts
index edbf22d1b66..9b86ae7d91a 100644
--- a/server/sonar-web/design-system/src/sonar-aligned/components/index.ts
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/index.ts
@@ -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
index 00000000000..58d1d499ccf
--- /dev/null
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/input/InputSelect.tsx
@@ -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/components/input/InputSelect.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx
index 78be3d1c4af..8414e6f3b79 100644
--- a/server/sonar-web/design-system/src/components/input/InputSelect.tsx
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/input/SelectCommon.tsx
@@ -18,11 +18,9 @@
* 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 { RefObject } from 'react';
import { useIntl } from 'react-intl';
-import ReactSelect, {
+import {
ClearIndicatorProps,
GroupBase,
Props as NamedProps,
@@ -30,45 +28,42 @@ import ReactSelect, {
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 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';
-import { ChevronDownIcon, CloseIcon } from '../icons';
-
-export interface LabelValueSelectOption<V> {
- Icon?: ReactNode;
- label: string;
- value: V;
-}
-
-interface ExtensionProps {
+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<
- V,
- Option extends LabelValueSelectOption<V>,
+ Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>,
-> = NamedProps<Option, IsMulti, Group> & ExtensionProps;
+> = NamedProps<Option, IsMulti, Group> & ExtensionProps<Option, IsMulti, Group>;
export function IconOption<
- V,
- Option extends LabelValueSelectOption<V>,
+ Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>,
>(props: OptionProps<Option, IsMulti, Group>) {
- const {
- data: { label, Icon },
- } = props;
+ const { label, isSelected } = props;
+ const { Icon } = props.data as { Icon: JSX.Element };
return (
<components.Option {...props}>
- <div className="sw-flex sw-items-center sw-gap-1">
+ <div aria-selected={isSelected} className="sw-flex sw-items-center sw-gap-1" role="option">
{Icon}
<SearchHighlighter>{label}</SearchHighlighter>
</div>
@@ -76,15 +71,13 @@ export function IconOption<
);
}
-function SingleValue<
- V,
- Option extends LabelValueSelectOption<V>,
+export function SingleValue<
+ Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>,
>(props: OptionProps<Option, IsMulti, Group>) {
- const {
- data: { label, Icon },
- } = props;
+ const label = props.selectProps.getOptionLabel(props.data);
+ const { Icon } = props.data as { Icon: JSX.Element };
return (
<components.SingleValue {...props}>
@@ -96,14 +89,13 @@ function SingleValue<
);
}
-function ClearIndicator<
- V,
- Option extends LabelValueSelectOption<V>,
+export function ClearIndicator<
+ Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>,
>(
props: ClearIndicatorProps<Option, IsMulti, Group> & {
- selectProps: SelectProps<V, Option, IsMulti, Group>;
+ selectProps: SelectProps<Option, IsMulti, Group>;
},
) {
const intl = useIntl();
@@ -123,9 +115,8 @@ function ClearIndicator<
);
}
-function DropdownIndicator<
- V,
- Option extends LabelValueSelectOption<V>,
+export function DropdownIndicator<
+ Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>,
>(props: OptionProps<Option, IsMulti, Group>) {
@@ -138,55 +129,8 @@ function 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>,
+ Option,
IsMulti extends boolean = false,
Group extends GroupBase<Option> = GroupBase<Option>,
>({ size }: { size: InputSizeKeys }): StylesConfig<Option, IsMulti, Group> {
@@ -231,3 +175,9 @@ export function selectStyle<
}),
};
}
+
+export interface LabelValueSelectOption<V = string> {
+ Icon?: React.ReactNode;
+ label: string;
+ value: V;
+}
diff --git a/server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx b/server/sonar-web/design-system/src/sonar-aligned/components/input/__tests__/InputSelect-test.tsx
index 7079cf5e6f8..f94686b1c53 100644
--- a/server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/input/__tests__/InputSelect-test.tsx
@@ -19,8 +19,8 @@
*/
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { renderWithContext } from '../../../helpers/testUtils';
-import { FCProps } from '../../../types/misc';
+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 () => {
@@ -68,6 +68,18 @@ it('should render select input with disabled prop', () => {
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
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
index 00000000000..b8fd68f2a75
--- /dev/null
+++ b/server/sonar-web/design-system/src/sonar-aligned/components/input/index.ts
@@ -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';
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx
index fc29f14295f..22fb8307f43 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/StatusFilter.tsx
@@ -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],
diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx
index d2d21891ae8..9a7a620c706 100644
--- a/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx
+++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TypesFilter.tsx
@@ -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,
];
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
index fce45c211b2..4f7142ab3a9 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
@@ -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,
}));
diff --git a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx
index 7ef68d6dea5..7a0fb275869 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreate.tsx
@@ -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 };
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
index 1c206512972..a9081c898f5 100644
--- a/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreate.tsx
@@ -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 };
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
index 9eab22d95f5..e22904aedf1 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreate.tsx
@@ -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 };
}
diff --git a/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx b/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
index 6b72f0db234..a5ff527ff4e 100644
--- a/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
+++ b/server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreate.tsx
@@ -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 };
}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx b/server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx
index 3acbfa32de7..781e9aa77b6 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/AssigneeSelect.tsx
@@ -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;
}
diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
index 3721dfe716c..439af636783 100644
--- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
@@ -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 });
};
diff --git a/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx b/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
index 78b18822c40..2fe4a93fb8b 100644
--- a/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
+++ b/server/sonar-web/src/main/js/apps/permissions/project/components/ApplyTemplate.tsx
@@ -89,7 +89,7 @@ export default class ApplyTemplate extends React.PureComponent<Props, State> {
}
};
- handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => {
+ handlePermissionTemplateChange = ({ value }: LabelValueSelectOption) => {
this.setState({ permissionTemplate: value });
};
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx
index 578135e7c6e..7b76d0800d8 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityPageFilters.tsx
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx
index 49b50a6b71b..2d4a69f12f3 100644
--- a/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/components/PerspectiveSelect.tsx
@@ -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"
diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
index cc84a71c127..347f3ae37de 100644
--- a/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
+++ b/server/sonar-web/src/main/js/apps/projectsManagement/BulkApplyTemplateModal.tsx
@@ -130,7 +130,7 @@ export default class BulkApplyTemplateModal extends React.PureComponent<Props, S
}
};
- handlePermissionTemplateChange = ({ value }: LabelValueSelectOption<string>) => {
+ handlePermissionTemplateChange = ({ value }: LabelValueSelectOption) => {
this.setState({ permissionTemplate: value });
};
diff --git a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx
index 963f7f8302d..363abc3a087 100644
--- a/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx
+++ b/server/sonar-web/src/main/js/apps/quality-gates/components/QualityGatePermissionsAddModalRenderer.tsx
@@ -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,