* 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, isUser } from '../../../types/quality-gates';
import { QualityGate } from '../../../types/types';
import { UserBase } from '../../../types/users';
+import { QGPermissionOption } from '../utils';
import QualityGatePermissionsAddModalRenderer from './QualityGatePermissionsAddModalRenderer';
-type Option = UserBase | Group;
-export type OptionWithValue = Option & { value: string };
-
interface Props {
onClose: () => void;
onSubmit: (selection: UserBase | Group) => void;
}
interface State {
+ loading: boolean;
+ options: Array<QGPermissionOption>;
selection?: UserBase | Group;
}
const DEBOUNCE_DELAY = 250;
export default class QualityGatePermissionsAddModal extends React.Component<Props, State> {
- state: State = {};
+ state: State = {
+ loading: false,
+ options: [],
+ };
constructor(props: Props) {
super(props);
this.handleSearch = debounce(this.handleSearch, DEBOUNCE_DELAY);
}
- handleSearch = (
- q: string,
- resolve: (options: Options<LabelValueSelectOption<UserBase | Group>>) => void,
- ) => {
+ handleSearch = (q: string) => {
const { qualityGate } = this.props;
const queryParams: SearchPermissionsParameters = {
selected: 'deselected',
};
+ this.setState({ loading: true });
+
Promise.all([searchUsers(queryParams), searchGroups(queryParams)])
.then(([usersResponse, groupsResponse]) =>
- [...usersResponse.users, ...groupsResponse.groups].map((o) => ({
- value: o,
- label: isUser(o) ? `${o.name} ${o.login}` : o.name,
- })),
+ [...usersResponse.users, ...groupsResponse.groups].map(
+ (o) =>
+ ({
+ ...o,
+ value: isUser(o) ? o.login : o.name,
+ label: isUser(o) ? o.name ?? o.login : o.name,
+ }) as QGPermissionOption,
+ ),
)
- .then(resolve)
- .catch(() => resolve([]));
+ .then((options) => {
+ this.setState({ loading: false, options });
+ })
+ .catch(() => {
+ this.setState({ loading: false, options: [] });
+ });
};
- handleSelection = ({ value }: LabelValueSelectOption<UserBase | Group>) => {
- this.setState({ selection: value });
+ handleSelection = (selectionKey?: string) => {
+ this.setState(({ options }) => {
+ const selectedOption = selectionKey
+ ? options.find((o) => (isUser(o) ? o.login : o.name) === selectionKey)
+ : undefined;
+ return { selection: selectedOption };
+ });
};
handleSubmit = (event: React.SyntheticEvent<HTMLFormElement>) => {
render() {
const { submitting } = this.props;
- const { selection } = this.state;
+ const { loading, options, selection } = this.state;
return (
<QualityGatePermissionsAddModalRenderer
+ loading={loading}
onClose={this.props.onClose}
onSelection={this.handleSelection}
onSubmit={this.handleSubmit}
+ options={options}
handleSearch={this.handleSearch}
selection={selection}
submitting={submitting}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import {
- ButtonPrimary,
- FormField,
- GenericAvatar,
- LabelValueSelectOption,
- Modal,
- Note,
- SearchSelectDropdown,
- UserGroupIcon,
-} from 'design-system';
+import { IconPeople, SelectAsync } from '@sonarsource/echoes-react';
+import { ButtonPrimary, GenericAvatar, Modal, Note } from 'design-system';
import * as React from 'react';
-import { GroupBase, OptionProps, Options, SingleValue, components } from 'react-select';
import Avatar from '../../../components/ui/Avatar';
import { translate } from '../../../helpers/l10n';
import { Group as UserGroup, isUser } from '../../../types/quality-gates';
import { UserBase } from '../../../types/users';
+import { QGPermissionOption } from '../utils';
export interface QualityGatePermissionsAddModalRendererProps {
- handleSearch: (
- q: string,
- resolve: (options: Options<LabelValueSelectOption<UserBase | UserGroup>>) => void,
- ) => void;
+ handleSearch: (q: string) => void;
+ loading: boolean;
onClose: () => void;
- onSelection: (selection: SingleValue<LabelValueSelectOption<UserBase | UserGroup>>) => void;
+ onSelection: (selection: string) => void;
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
+ options: QGPermissionOption[];
selection?: UserBase | UserGroup;
submitting: boolean;
}
export default function QualityGatePermissionsAddModalRenderer(
props: Readonly<QualityGatePermissionsAddModalRendererProps>,
) {
- const { selection, submitting } = props;
+ const { loading, options, selection, submitting } = props;
- const renderedSelection = React.useMemo(() => {
- return <OptionRenderer option={selection} small />;
- }, [selection]);
+ const selectValue = selection && isUser(selection) ? selection.login : selection?.name;
return (
<Modal
headerTitle={translate('quality_gates.permissions.grant')}
body={
<form onSubmit={props.onSubmit} id={FORM_ID}>
- <FormField
+ <SelectAsync
+ ariaLabel={translate('quality_gates.permissions.search')}
+ className="sw-mb-4"
+ data={options}
+ id={USER_SELECT_INPUT_ID}
+ isLoading={loading}
label={translate('quality_gates.permissions.search')}
- htmlFor={USER_SELECT_INPUT_ID}
- >
- <SearchSelectDropdown
- className="sw-mb-2"
- controlAriaLabel={translate('quality_gates.permissions.search')}
- inputId={USER_SELECT_INPUT_ID}
- autoFocus
- defaultOptions
- noOptionsMessage={() => translate('no_results')}
- onChange={props.onSelection}
- loadOptions={props.handleSearch}
- getOptionValue={({ value }: LabelValueSelectOption<UserBase | UserGroup>) =>
- isUser(value) ? value.login : value.name
- }
- controlLabel={renderedSelection}
- components={{
- Option,
- }}
- />
- </FormField>
+ labelNotFound={translate('select.search.noMatches')}
+ onChange={props.onSelection}
+ onSearch={props.handleSearch}
+ optionComponent={OptionRenderer}
+ value={selectValue}
+ />
</form>
}
primaryButton={
);
}
-function OptionRenderer({
- option,
- small = false,
-}: Readonly<{
- option?: UserBase | UserGroup;
- small?: boolean;
-}>) {
+function OptionRenderer(option: Readonly<QGPermissionOption>) {
if (!option) {
return null;
}
return (
- <>
+ <div className="sw-flex sw-items-center sw-justify-start">
{isUser(option) ? (
<>
- <Avatar
- className={small ? 'sw-my-1' : ''}
- hash={option.avatar}
- name={option.name}
- size={small ? 'xs' : 'sm'}
- />
- <span className="sw-ml-2">
+ <Avatar hash={option.avatar} name={option.name} />
+ <div className="sw-ml-2">
<strong className="sw-body-sm-highlight sw-mr-1">{option.name}</strong>
+ <br />
<Note>{option.login}</Note>
- </span>
+ </div>
</>
) : (
<>
- <GenericAvatar
- className={small ? 'sw-my-1' : ''}
- Icon={UserGroupIcon}
- name={option.name}
- size={small ? 'xs' : 'sm'}
- />
+ <GenericAvatar Icon={IconPeople} name={option.name} />
<strong className="sw-body-sm-highlight sw-ml-2">{option.name}</strong>
</>
)}
- </>
- );
-}
-
-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;
-
- return (
- <components.Option {...props}>
- <div className="sw-flex sw-items-center">
- <OptionRenderer option={value} />
- </div>
- </components.Option>
+ </div>
);
}
});
await user.click(grantPermissionButton);
const popup = screen.getByRole('dialog');
- const searchUserInput = within(popup).getByRole('combobox', {
+ const searchUserInput = within(popup).getByRole('searchbox', {
name: 'quality_gates.permissions.search',
});
expect(searchUserInput).toBeInTheDocument();
});
expect(addUserButton).toBeDisabled();
await user.click(searchUserInput);
- await user.click(screen.getByText('userlogin'));
+ await user.click(screen.getByRole('option', { name: 'userlogin' }));
expect(addUserButton).toBeEnabled();
await user.click(addUserButton);
expect(screen.getByText('userlogin')).toBeInTheDocument();
});
await user.click(grantPermissionButton);
const popup = screen.getByRole('dialog');
- const searchUserInput = within(popup).getByRole('combobox', {
+ const searchUserInput = within(popup).getByRole('searchbox', {
name: 'quality_gates.permissions.search',
});
const addUserButton = screen.getByRole('button', {
name: 'add_verb',
});
await user.click(searchUserInput);
- await user.click(within(popup).getByLabelText('Foo'));
+ await user.click(within(popup).getByRole('option', { name: 'Foo Foo' }));
await user.click(addUserButton);
expect(screen.getByText('Foo')).toBeInTheDocument();
});
await user.click(grantPermissionButton);
const popup = screen.getByRole('dialog');
- const searchUserInput = within(popup).getByRole('combobox', {
+ const searchUserInput = within(popup).getByRole('searchbox', {
name: 'quality_gates.permissions.search',
});
await user.click(searchUserInput);
- expect(screen.getByText('no_results')).toBeInTheDocument();
+ expect(screen.getByText('select.search.noMatches')).toBeInTheDocument();
});
});
import { MetricKey } from '~sonar-aligned/types/metrics';
import { getLocalizedMetricName } from '../../helpers/l10n';
import { isDiffMetric } from '../../helpers/measures';
-import { CaycStatus, Condition, Dict, Metric, QualityGate } from '../../types/types';
+import { CaycStatus, Condition, Dict, Group, Metric, QualityGate } from '../../types/types';
+import { UserBase } from '../../types/users';
interface GroupedByMetricConditions {
caycConditions: Condition[];
type AllCaycMetricKeys = OptimizedCaycMetricKeys | UnoptimizedCaycMetricKeys;
+type UserOrGroup = UserBase | Group;
+export type QGPermissionOption = UserOrGroup & { label: string; value: string };
+
const COMMON_CONDITIONS: Record<
CommonCaycMetricKeys,
Condition & { shouldRenderOperator?: boolean }