import { UserBase } from '../../../types/users';
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;
- query: string;
- searchResults: Array<UserBase | Group>;
selection?: UserBase | Group;
}
export default class QualityGatePermissionsAddModal extends React.Component<Props, State> {
mounted = false;
- state: State = {
- loading: false,
- query: '',
- searchResults: []
- };
+ state: State = {};
constructor(props: Props) {
super(props);
}
componentDidMount() {
- const { query } = this.state;
this.mounted = true;
- this.handleSearch(query);
}
componentWillUnmount() {
this.mounted = false;
}
- handleSearch = async (query: string) => {
+ handleSearch = (q: string, resolve: (options: OptionWithValue[]) => void) => {
const { qualityGate } = this.props;
- this.setState({ loading: true });
const queryParams: SearchPermissionsParameters = {
gateName: qualityGate.name,
- q: query,
+ q,
selected: 'deselected'
};
- try {
- const [{ users }, { groups }] = await Promise.all([
- searchUsers(queryParams),
- searchGroups(queryParams)
- ]);
- if (this.mounted) {
- this.setState({ loading: false, searchResults: [...users, ...groups] });
- }
- } catch {
- if (this.mounted) {
- this.setState({ loading: false });
- }
- }
- };
-
- handleInputChange = (newQuery: string) => {
- const { query } = this.state;
- if (query !== newQuery) {
- this.setState({ query: newQuery });
- this.handleSearch(newQuery);
- }
+ Promise.all([searchUsers(queryParams), searchGroups(queryParams)])
+ .then(([usersResponse, groupsResponse]) => [...usersResponse.users, ...groupsResponse.groups])
+ .then(resolve)
+ .catch(() => resolve([]));
};
handleSelection = (selection: UserBase | Group) => {
render() {
const { submitting } = this.props;
- const { loading, searchResults, selection } = this.state;
+ const { selection } = this.state;
return (
<QualityGatePermissionsAddModalRenderer
- loading={loading}
onClose={this.props.onClose}
- onInputChange={this.handleInputChange}
onSelection={this.handleSelection}
onSubmit={this.handleSubmit}
- searchResults={searchResults}
+ 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 { identity, omit } from 'lodash';
+import { omit } from 'lodash';
import * as React from 'react';
import { components, ControlProps, OptionProps, SingleValueProps } from 'react-select';
import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons';
import Modal from '../../../components/controls/Modal';
-import Select from '../../../components/controls/Select';
+import { SearchSelect } from '../../../components/controls/Select';
import GroupIcon from '../../../components/icons/GroupIcon';
import Avatar from '../../../components/ui/Avatar';
import { translate } from '../../../helpers/l10n';
import { Group, isUser } from '../../../types/quality-gates';
import { UserBase } from '../../../types/users';
+import { OptionWithValue } from './QualityGatePermissionsAddModal';
export interface QualityGatePermissionsAddModalRendererProps {
onClose: () => void;
- onInputChange: (query: string) => void;
+ handleSearch: (q: string, resolve: (options: OptionWithValue[]) => void) => void;
+ onSelection: (selection: OptionWithValue) => void;
+ selection?: UserBase | Group;
onSubmit: (event: React.SyntheticEvent<HTMLFormElement>) => void;
- onSelection: (selection: Option) => void;
submitting: boolean;
- loading: boolean;
- searchResults: Array<UserBase | Group>;
- selection?: UserBase | Group;
}
-export type Option = (UserBase | Group) & { value: string };
-
export default function QualityGatePermissionsAddModalRenderer(
props: QualityGatePermissionsAddModalRendererProps
) {
- const { loading, searchResults, selection, submitting } = props;
+ const { selection, submitting } = props;
const header = translate('quality_gates.permissions.grant');
const noResultsText = translate('no_results');
- const options = searchResults.map(r => ({ ...r, value: getValue(r) }));
-
return (
<Modal contentLabel={header} onRequestClose={props.onClose}>
<header className="modal-head">
<div className="modal-body">
<div className="modal-field">
<label>{translate('quality_gates.permissions.search')}</label>
- <Select
+ <SearchSelect
className="Select-big"
autoFocus={true}
isClearable={false}
- isSearchable={true}
placeholder=""
- isLoading={loading}
- filterOptions={identity}
+ defaultOptions={true}
noOptionsMessage={() => noResultsText}
onChange={props.onSelection}
- onInputChange={props.onInputChange}
+ loadOptions={props.handleSearch}
+ getOptionValue={opt => (isUser(opt) ? opt.login : opt.name)}
components={{
Option: optionRenderer,
SingleValue: singleValueRenderer,
Control: controlRenderer
}}
- options={options}
- value={options.find(o => o.value === (selection && getValue(selection)))}
/>
</div>
</div>
);
}
-function getValue(option: UserBase | Group) {
- return isUser(option) ? option.login : option.name;
-}
-
-export function customOptions(option: Option) {
+export function customOptions(option: OptionWithValue) {
return (
<>
{isUser(option) ? (
);
}
-function optionRenderer(props: OptionProps<Option, false>) {
+function optionRenderer(props: OptionProps<OptionWithValue, false>) {
return (
<components.Option {...props} className="Select-option">
{customOptions(props.data)}
);
}
-function singleValueRenderer(props: SingleValueProps<Option>) {
+function singleValueRenderer(props: SingleValueProps<OptionWithValue>) {
return (
<components.SingleValue {...props} className="Select-value-label">
{customOptions(props.data)}
);
}
-function controlRenderer(props: ControlProps<Option, false>) {
+function controlRenderer(props: ControlProps<OptionWithValue, false>) {
return (
<components.Control {...omit(props, ['children'])} className="abs-height-100 Select-control">
{props.children}
import { searchGroups, searchUsers } from '../../../../api/quality-gates';
import { mockQualityGate } from '../../../../helpers/mocks/quality-gates';
import { mockUserBase } from '../../../../helpers/mocks/users';
-import { mockEvent, waitAndUpdate } from '../../../../helpers/testUtils';
+import { mockEvent } from '../../../../helpers/testUtils';
import QualityGatePermissionsAddModal from '../QualityGatePermissionsAddModal';
+import QualityGatePermissionsAddModalRenderer from '../QualityGatePermissionsAddModalRenderer';
jest.mock('../../../../api/quality-gates', () => ({
searchUsers: jest.fn().mockResolvedValue({ users: [] }),
const wrapper = shallowRender();
- expect(wrapper.state().loading).toBe(true);
-
- await waitAndUpdate(wrapper);
-
- expect(wrapper.state().loading).toBe(false);
- expect(searchUsers).toBeCalledWith({ gateName: 'qualitygate', q: '', selected: 'deselected' });
- expect(searchGroups).toBeCalledWith({
- gateName: 'qualitygate',
- q: '',
- selected: 'deselected'
+ const query = 'Waldo';
+ const results = await new Promise(resolve => {
+ wrapper.instance().handleSearch(query, resolve);
});
- expect(wrapper.state().searchResults).toHaveLength(2);
-});
-
-it('should fetch users and groups', async () => {
- (searchUsers as jest.Mock).mockResolvedValueOnce({ users: [mockUserBase()] });
- (searchGroups as jest.Mock).mockResolvedValueOnce({ groups: [{ name: 'group' }] });
-
- const wrapper = shallowRender();
- const query = 'query';
- wrapper.instance().handleSearch(query);
+ expect(searchUsers).toBeCalledWith(expect.objectContaining({ q: query }));
+ expect(searchGroups).toBeCalledWith(expect.objectContaining({ q: query }));
- expect(wrapper.state().loading).toBe(true);
- expect(searchUsers).toBeCalledWith({ gateName: 'qualitygate', q: query, selected: 'deselected' });
- expect(searchGroups).toBeCalledWith({
- gateName: 'qualitygate',
- q: query,
- selected: 'deselected'
- });
-
- await waitAndUpdate(wrapper);
-
- expect(wrapper.state().loading).toBe(false);
- expect(wrapper.state().searchResults).toHaveLength(2);
-});
-
-it('should handle input change', () => {
- const wrapper = shallowRender();
-
- wrapper.instance().handleSearch = jest.fn();
- const { handleSearch } = wrapper.instance();
-
- wrapper.instance().handleInputChange('a');
-
- expect(wrapper.state().query).toBe('a');
- expect(handleSearch).toBeCalled();
-
- const query = 'query';
- wrapper.instance().handleInputChange(query);
-
- expect(wrapper.state().query).toBe(query);
- expect(handleSearch).toBeCalledWith(query);
-
- jest.clearAllMocks();
- wrapper.instance().handleInputChange(query); // input change with same parameter
-
- expect(wrapper.state().query).toBe(query);
- expect(handleSearch).not.toBeCalled();
+ expect(results).toHaveLength(2);
});
it('should handleSelection', () => {
const wrapper = shallowRender();
- const selection = mockUserBase();
- wrapper.instance().handleSelection(selection);
+ const selection = { ...mockUserBase(), value: 'value' };
+ wrapper
+ .find(QualityGatePermissionsAddModalRenderer)
+ .props()
+ .onSelection(selection);
expect(wrapper.state().selection).toBe(selection);
});
it('should render correctly', () => {
expect(shallowRender()).toMatchSnapshot('default');
- expect(shallowRender({ selection: mockUserBase() })).toMatchSnapshot('selection');
expect(shallowRender({ selection: mockUserBase(), submitting: true })).toMatchSnapshot(
- 'submitting'
+ 'with selection and submitting'
);
- expect(
- shallowRender({ searchResults: [mockUserBase(), { name: 'group name' }] })
- ).toMatchSnapshot('query and results');
});
it('should render options correctly', () => {
function shallowRender(overrides: Partial<QualityGatePermissionsAddModalRendererProps> = {}) {
return shallow(
<QualityGatePermissionsAddModalRenderer
- loading={false}
onClose={jest.fn()}
- onInputChange={jest.fn()}
onSelection={jest.fn()}
onSubmit={jest.fn()}
- searchResults={[]}
+ handleSearch={jest.fn()}
submitting={false}
{...overrides}
/>
exports[`should render correctly 1`] = `
<QualityGatePermissionsAddModalRenderer
- loading={true}
+ handleSearch={[Function]}
onClose={[MockFunction]}
- onInputChange={[Function]}
onSelection={[Function]}
onSubmit={[Function]}
- searchResults={Array []}
submitting={false}
/>
`;
<label>
quality_gates.permissions.search
</label>
- <Select
+ <SearchSelect
autoFocus={true}
className="Select-big"
components={
"SingleValue": [Function],
}
}
- filterOptions={[Function]}
+ defaultOptions={true}
+ getOptionValue={[Function]}
isClearable={false}
- isLoading={false}
- isSearchable={true}
+ loadOptions={[MockFunction]}
noOptionsMessage={[Function]}
onChange={[MockFunction]}
- onInputChange={[MockFunction]}
- options={Array []}
placeholder=""
/>
</div>
</Modal>
`;
-exports[`should render correctly: query and results 1`] = `
+exports[`should render correctly: with selection and submitting 1`] = `
<Modal
contentLabel="quality_gates.permissions.grant"
onRequestClose={[MockFunction]}
<label>
quality_gates.permissions.search
</label>
- <Select
+ <SearchSelect
autoFocus={true}
className="Select-big"
components={
"SingleValue": [Function],
}
}
- filterOptions={[Function]}
+ defaultOptions={true}
+ getOptionValue={[Function]}
isClearable={false}
- isLoading={false}
- isSearchable={true}
+ loadOptions={[MockFunction]}
noOptionsMessage={[Function]}
onChange={[MockFunction]}
- onInputChange={[MockFunction]}
- options={
- Array [
- Object {
- "login": "userlogin",
- "value": "userlogin",
- },
- Object {
- "name": "group name",
- "value": "group name",
- },
- ]
- }
- placeholder=""
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <SubmitButton
- disabled={true}
- >
- add_verb
- </SubmitButton>
- <ResetButtonLink
- onClick={[MockFunction]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
-
-exports[`should render correctly: selection 1`] = `
-<Modal
- contentLabel="quality_gates.permissions.grant"
- onRequestClose={[MockFunction]}
->
- <header
- className="modal-head"
- >
- <h2>
- quality_gates.permissions.grant
- </h2>
- </header>
- <form
- onSubmit={[MockFunction]}
- >
- <div
- className="modal-body"
- >
- <div
- className="modal-field"
- >
- <label>
- quality_gates.permissions.search
- </label>
- <Select
- autoFocus={true}
- className="Select-big"
- components={
- Object {
- "Control": [Function],
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
- filterOptions={[Function]}
- isClearable={false}
- isLoading={false}
- isSearchable={true}
- noOptionsMessage={[Function]}
- onChange={[MockFunction]}
- onInputChange={[MockFunction]}
- options={Array []}
- placeholder=""
- />
- </div>
- </div>
- <footer
- className="modal-foot"
- >
- <SubmitButton
- disabled={false}
- >
- add_verb
- </SubmitButton>
- <ResetButtonLink
- onClick={[MockFunction]}
- >
- cancel
- </ResetButtonLink>
- </footer>
- </form>
-</Modal>
-`;
-
-exports[`should render correctly: submitting 1`] = `
-<Modal
- contentLabel="quality_gates.permissions.grant"
- onRequestClose={[MockFunction]}
->
- <header
- className="modal-head"
- >
- <h2>
- quality_gates.permissions.grant
- </h2>
- </header>
- <form
- onSubmit={[MockFunction]}
- >
- <div
- className="modal-body"
- >
- <div
- className="modal-field"
- >
- <label>
- quality_gates.permissions.search
- </label>
- <Select
- autoFocus={true}
- className="Select-big"
- components={
- Object {
- "Control": [Function],
- "Option": [Function],
- "SingleValue": [Function],
- }
- }
- filterOptions={[Function]}
- isClearable={false}
- isLoading={false}
- isSearchable={true}
- noOptionsMessage={[Function]}
- onChange={[MockFunction]}
- onInputChange={[MockFunction]}
- options={Array []}
placeholder=""
/>
</div>
profile: { language: string; name: string };
}
-export default class ProfilePermissionsFormSelect extends React.PureComponent<Props> {
- mounted = false;
+const DEBOUNCE_DELAY = 250;
+export default class ProfilePermissionsFormSelect extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
- this.handleSearch = debounce(this.handleSearch, 250);
+ this.handleSearch = debounce(this.handleSearch, DEBOUNCE_DELAY);
}
optionRenderer(props: OptionProps<OptionWithValue, false>) {