import { FacetValue } from '../app/types';
import { getJSON, post, postJSON, RequestData } from '../helpers/request';
import { RawIssue } from '../helpers/issues';
+import throwGlobalError from '../app/utils/throwGlobalError';
export interface IssueResponse {
components?: Array<{}>;
export function searchIssueTags(
data: { organization?: string; ps?: number; q?: string } = { ps: 100 }
): Promise<string[]> {
- return getJSON('/api/issues/tags', data).then(r => r.tags);
+ return getJSON('/api/issues/tags', data)
+ .then(r => r.tags)
+ .catch(throwGlobalError);
}
export function getIssueChangelog(issue: string): Promise<any> {
}
interface State {
- searchResult: any[];
+ searchResult: string[];
}
const LIST_SIZE = 10;
componentDidMount() {
this.mounted = true;
- this.onSearch('');
}
componentWillUnmount() {
}
onSearch = (query: string) => {
- getRuleTags({
+ return getRuleTags({
q: query,
ps: Math.min(this.props.tags.length + LIST_SIZE, 100),
organization: this.props.organization
render() {
return (
<TagsSelector
- position={this.props.popupPosition || {}}
- tags={this.state.searchResult}
- selectedTags={this.props.tags}
listSize={LIST_SIZE}
onSearch={this.onSearch}
onSelect={this.onSelect}
onUnselect={this.onUnselect}
+ position={this.props.popupPosition || {}}
+ selectedTags={this.props.tags}
+ tags={this.state.searchResult}
/>
);
}
const LIST_SIZE = 10;
export default class MetaTagsSelector extends React.PureComponent<Props, State> {
+ mounted = false;
state: State = { searchResult: [] };
componentDidMount() {
- this.onSearch('');
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
}
onSearch = (query: string) => {
- searchProjectTags({
+ return searchProjectTags({
q: query,
ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
- }).then(result => this.setState({ searchResult: result.tags }), () => {});
+ }).then(
+ ({ tags }) => {
+ if (this.mounted) {
+ this.setState({ searchResult: tags });
+ }
+ },
+ () => {}
+ );
};
onSelect = (tag: string) => {
render() {
return (
<TagsSelector
- position={this.props.position}
- tags={this.state.searchResult}
- selectedTags={this.props.selectedTags}
listSize={LIST_SIZE}
onSearch={this.onSearch}
onSelect={this.onSelect}
onUnselect={this.onUnselect}
+ position={this.props.position}
+ selectedTags={this.props.selectedTags}
+ tags={this.state.searchResult}
/>
);
}
import SearchBox from '../controls/SearchBox';
interface Props {
- selectedElements: Array<string>;
- elements: Array<string>;
+ elements: string[];
listSize?: number;
- onSearch: (query: string) => void;
+ onSearch: (query: string) => Promise<void>;
onSelect: (item: string) => void;
onUnselect: (item: string) => void;
- validateSearchInput?: (value: string) => string;
placeholder: string;
+ selectedElements: string[];
+ validateSearchInput?: (value: string) => string;
}
interface State {
- query: string;
- selectedElements: Array<string>;
- unselectedElements: Array<string>;
activeIdx: number;
+ loading: boolean;
+ query: string;
+ selectedElements: string[];
+ unselectedElements: string[];
}
interface DefaultProps {
export default class MultiSelect extends React.PureComponent<Props, State> {
container?: HTMLDivElement | null;
searchInput?: HTMLInputElement | null;
+ mounted = false;
static defaultProps: DefaultProps = {
listSize: 10,
constructor(props: Props) {
super(props);
this.state = {
+ activeIdx: 0,
+ loading: true,
query: '',
selectedElements: [],
- unselectedElements: [],
- activeIdx: 0
+ unselectedElements: []
};
}
componentDidMount() {
+ this.mounted = true;
+ this.onSearchQuery('');
this.updateSelectedElements(this.props);
this.updateUnselectedElements(this.props as PropsWithDefault);
if (this.container) {
}
componentWillUnmount() {
+ this.mounted = false;
if (this.container) {
this.container.removeEventListener('keydown', this.handleKeyboard);
}
handleKeyboard = (evt: KeyboardEvent) => {
switch (evt.keyCode) {
case 40: // down
- this.setState(this.selectNextElement);
evt.stopPropagation();
evt.preventDefault();
+ this.setState(this.selectNextElement);
break;
case 38: // up
- this.setState(this.selectPreviousElement);
evt.stopPropagation();
evt.preventDefault();
+ this.setState(this.selectPreviousElement);
break;
case 37: // left
case 39: // right
};
onSearchQuery = (query: string) => {
- this.setState({ query, activeIdx: 0 });
- this.props.onSearch(query);
+ this.setState({ activeIdx: 0, loading: true, query });
+ this.props.onSearch(query).then(this.stopLoading, this.stopLoading);
};
onSelectItem = (item: string) => {
}
};
+ stopLoading = () => {
+ if (this.mounted) {
+ this.setState({ loading: false });
+ }
+ };
+
toggleSelect = (item: string) => {
if (this.props.selectedElements.indexOf(item) === -1) {
this.onSelectItem(item);
<SearchBox
autoFocus={true}
className="little-spacer-top"
+ loading={this.state.loading}
onChange={this.handleSearchChange}
placeholder={this.props.placeholder}
value={query}
const props = {
selectedElements: ['bar'],
elements: [],
- onSearch: () => {},
+ onSearch: () => Promise.resolve(),
onSelect: () => {},
onUnselect: () => {},
placeholder: ''
<SearchBox
autoFocus={true}
className="little-spacer-top"
+ loading={true}
onChange={[Function]}
placeholder=""
value=""
<SearchBox
autoFocus={true}
className="little-spacer-top"
+ loading={true}
onChange={[Function]}
placeholder=""
value=""
<SearchBox
autoFocus={true}
className="little-spacer-top"
+ loading={true}
onChange={[Function]}
placeholder=""
value=""
<SearchBox
autoFocus={true}
className="little-spacer-top"
+ loading={true}
onChange={[Function]}
placeholder=""
value="test"
transition: color 0.3s ease;
}
+.search-box > .spinner {
+ position: absolute;
+ top: 4px;
+ left: 5px;
+}
+
.search-box-clear {
position: absolute;
top: 4px;
import * as React from 'react';
import * as classNames from 'classnames';
import { debounce, Cancelable } from 'lodash';
-import SearchIcon from '../icons-components/SearchIcon';
import ClearIcon from '../icons-components/ClearIcon';
+import SearchIcon from '../icons-components/SearchIcon';
+import DeferredSpinner from '../common/DeferredSpinner';
import { ButtonIcon } from '../ui/buttons';
import * as theme from '../../app/theme';
import { translateWithParameters } from '../../helpers/l10n';
className?: string;
innerRef?: (node: HTMLInputElement | null) => void;
id?: string;
+ loading?: boolean;
minLength?: number;
onChange: (value: string) => void;
onClick?: React.MouseEventHandler<HTMLInputElement>;
};
render() {
- const { minLength } = this.props;
+ const { loading, minLength } = this.props;
const { value } = this.state;
const inputClassName = classNames('search-box-input', {
value={value}
/>
- <SearchIcon className="search-box-magnifier" />
+ <DeferredSpinner loading={loading !== undefined ? loading : false}>
+ <SearchIcon className="search-box-magnifier" />
+ </DeferredSpinner>
{value && (
<ButtonIcon
type="search"
value="foo"
/>
- <SearchIcon
- className="search-box-magnifier"
- />
+ <DeferredSpinner
+ loading={false}
+ timeout={100}
+ >
+ <SearchIcon
+ className="search-box-magnifier"
+ />
+ </DeferredSpinner>
<ButtonIcon
className="button-tiny search-box-clear"
color="#999"
togglePopup={this.toggleSetTags}
popup={
<SetIssueTagsPopup
- onFail={this.props.onFail}
organization={issue.projectOrganization}
selectedTags={issue.tags}
setTags={this.setTags}
isOpen={true}
popup={
<SetIssueTagsPopup
- onFail={[MockFunction]}
organization="foo"
selectedTags={
Array [
isOpen={false}
popup={
<SetIssueTagsPopup
- onFail={[MockFunction]}
organization="foo"
selectedTags={
Array [
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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.
- */
-//@flow
-import React from 'react';
-import { without } from 'lodash';
-import TagsSelector from '../../../components/tags/TagsSelector';
-import { searchIssueTags } from '../../../api/issues';
-
-/*::
-type Props = {
- popupPosition?: {},
- onFail: Error => void,
- organization: string,
- selectedTags: Array<string>,
- setTags: (Array<string>) => void
-};
-*/
-
-/*::
-type State = {
- searchResult: Array<string>
-};
-*/
-
-const LIST_SIZE = 10;
-
-export default class SetIssueTagsPopup extends React.PureComponent {
- /*:: mounted: boolean; */
- /*:: props: Props; */
- state /*: State */ = { searchResult: [] };
-
- componentDidMount() {
- this.mounted = true;
- this.onSearch('');
- }
-
- componentWillUnmount() {
- this.mounted = false;
- }
-
- onSearch = (query /*: string */) => {
- searchIssueTags({
- q: query,
- ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
- organization: this.props.organization
- }).then((tags /*: Array<string> */) => {
- if (this.mounted) {
- this.setState({ searchResult: tags });
- }
- }, this.props.onFail);
- };
-
- onSelect = (tag /*: string */) => {
- this.props.setTags([...this.props.selectedTags, tag]);
- };
-
- onUnselect = (tag /*: string */) => {
- this.props.setTags(without(this.props.selectedTags, tag));
- };
-
- render() {
- return (
- <TagsSelector
- position={this.props.popupPosition}
- tags={this.state.searchResult}
- selectedTags={this.props.selectedTags}
- listSize={LIST_SIZE}
- onSearch={this.onSearch}
- onSelect={this.onSelect}
- onUnselect={this.onUnselect}
- />
- );
- }
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { without } from 'lodash';
+import { BubblePopupPosition } from '../../../components/common/BubblePopup';
+import TagsSelector from '../../../components/tags/TagsSelector';
+import { searchIssueTags } from '../../../api/issues';
+
+interface Props {
+ popupPosition: BubblePopupPosition;
+ organization: string;
+ selectedTags: string[];
+ setTags: (tags: string[]) => void;
+}
+
+interface State {
+ searchResult: string[];
+}
+
+const LIST_SIZE = 10;
+
+export default class SetIssueTagsPopup extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State = { searchResult: [] };
+
+ componentDidMount() {
+ this.mounted = true;
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ onSearch = (query: string) => {
+ return searchIssueTags({
+ q: query,
+ ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100),
+ organization: this.props.organization
+ }).then(
+ (tags: string[]) => {
+ if (this.mounted) {
+ this.setState({ searchResult: tags });
+ }
+ },
+ () => {}
+ );
+ };
+
+ onSelect = (tag: string) => {
+ this.props.setTags([...this.props.selectedTags, tag]);
+ };
+
+ onUnselect = (tag: string) => {
+ this.props.setTags(without(this.props.selectedTags, tag));
+ };
+
+ render() {
+ return (
+ <TagsSelector
+ listSize={LIST_SIZE}
+ onSearch={this.onSearch}
+ onSelect={this.onSelect}
+ onUnselect={this.onUnselect}
+ position={this.props.popupPosition}
+ selectedTags={this.props.selectedTags}
+ tags={this.state.searchResult}
+ />
+ );
+ }
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2018 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 { shallow } from 'enzyme';
-import React from 'react';
-import SetIssueTagsPopup from '../SetIssueTagsPopup';
-
-it('should render tags popup correctly', () => {
- const element = shallow(
- <SetIssueTagsPopup
- onFail={jest.fn()}
- organization="foo"
- selectedTags="mytag"
- setTags={jest.fn()}
- />
- );
- element.setState({ searchResult: ['mytag', 'test', 'second'] });
- expect(element).toMatchSnapshot();
-});
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import SetIssueTagsPopup from '../SetIssueTagsPopup';
+
+it('should render tags popup correctly', () => {
+ const element = shallow(
+ <SetIssueTagsPopup
+ organization="foo"
+ popupPosition={{}}
+ selectedTags={['mytag']}
+ setTags={jest.fn()}
+ />
+ );
+ element.setState({ searchResult: ['mytag', 'test', 'second'] });
+ expect(element).toMatchSnapshot();
+});
+++ /dev/null
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`should render tags popup correctly 1`] = `
-<TagsSelector
- listSize={10}
- onSearch={[Function]}
- onSelect={[Function]}
- onUnselect={[Function]}
- selectedTags="mytag"
- tags={
- Array [
- "mytag",
- "test",
- "second",
- ]
- }
-/>
-`;
--- /dev/null
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`should render tags popup correctly 1`] = `
+<TagsSelector
+ listSize={10}
+ onSearch={[Function]}
+ onSelect={[Function]}
+ onUnselect={[Function]}
+ position={Object {}}
+ selectedTags={
+ Array [
+ "mytag",
+ ]
+ }
+ tags={
+ Array [
+ "mytag",
+ "test",
+ "second",
+ ]
+ }
+/>
+`;
import './TagsList.css';
interface Props {
- position: BubblePopupPosition;
- tags: string[];
- selectedTags: string[];
listSize: number;
- onSearch: (query: string) => void;
+ onSearch: (query: string) => Promise<void>;
onSelect: (item: string) => void;
onUnselect: (item: string) => void;
+ position: BubblePopupPosition;
+ selectedTags: string[];
+ tags: string[];
}
export default function TagsSelector(props: Props) {
return (
<BubblePopup
- position={props.position}
- customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300">
+ customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
+ position={props.position}>
<MultiSelect
elements={props.tags}
- selectedElements={props.selectedTags}
listSize={props.listSize}
onSearch={props.onSearch}
onSelect={props.onSelect}
onUnselect={props.onUnselect}
- validateSearchInput={validateTag}
placeholder={translate('search.search_for_tags')}
+ selectedElements={props.selectedTags}
+ validateSearchInput={validateTag}
/>
</BubblePopup>
);
import TagsSelector, { validateTag } from '../TagsSelector';
const props = {
- position: { right: 0, top: 0 },
listSize: 10,
- tags: ['foo', 'bar', 'baz'],
- selectedTags: ['bar'],
- onSearch: () => {},
+ onSearch: () => Promise.resolve(),
onSelect: () => {},
- onUnselect: () => {}
+ onUnselect: () => {},
+ position: { right: 0, top: 0 },
+ selectedTags: ['bar'],
+ tags: ['foo', 'bar', 'baz']
};
it('should render with selected tags', () => {
});
it('should render without tags at all', () => {
- expect(shallow(<TagsSelector {...props} tags={[]} selectedTags={[]} />)).toMatchSnapshot();
+ expect(shallow(<TagsSelector {...props} selectedTags={[]} tags={[]} />)).toMatchSnapshot();
});
it('should validate tags correctly', () => {