aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src
diff options
context:
space:
mode:
authorPascal Mugnier <pascal.mugnier@sonarsource.com>2018-03-20 11:56:00 +0100
committerSonarTech <sonartech@sonarsource.com>2018-03-22 12:37:49 +0100
commit82a1cb0b4a6ea692624b290b15080fdf7d1b475c (patch)
treef0014002f03c233da8b4628440a28784fcd136f3 /server/sonar-web/src
parent169ffd14e1e2510b5fe6845577678b00f56ff9eb (diff)
downloadsonarqube-82a1cb0b4a6ea692624b290b15080fdf7d1b475c.tar.gz
sonarqube-82a1cb0b4a6ea692624b290b15080fdf7d1b475c.zip
SONAR-9642 Reduce steps for selecting a custom chart (#6)
Diffstat (limited to 'server/sonar-web/src')
-rw-r--r--server/sonar-web/src/main/js/app/styles/components/menu.css15
-rw-r--r--server/sonar-web/src/main/js/app/styles/init/misc.css4
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx3
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx5
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js1
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js6
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js215
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx79
-rw-r--r--server/sonar-web/src/main/js/apps/projectActivity/utils.js2
-rw-r--r--server/sonar-web/src/main/js/components/common/MultiSelect.tsx67
-rw-r--r--server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx33
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx17
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap121
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueTags.js9
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx5
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap1
-rw-r--r--server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx1
-rw-r--r--server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap4
23 files changed, 455 insertions, 182 deletions
diff --git a/server/sonar-web/src/main/js/app/styles/components/menu.css b/server/sonar-web/src/main/js/app/styles/components/menu.css
index 5e479d6fc20..11e247b18a0 100644
--- a/server/sonar-web/src/main/js/app/styles/components/menu.css
+++ b/server/sonar-web/src/main/js/app/styles/components/menu.css
@@ -56,6 +56,10 @@
background-color: var(--barBorderColor);
}
+.menu > li > a.disabled {
+ color: #bbb !important;
+}
+
.menu > li > a:hover,
.menu > li > a:focus {
text-decoration: none;
@@ -75,11 +79,20 @@
background-color: var(--barBackgroundColor);
}
-.menu .menu-vertically-limited {
+.menu .menu-vertically-limited,
+.menu.menu-vertically-limited {
max-height: 300px;
overflow-y: auto;
}
+.menu-vertically-limited.with-top-separator {
+ border-top: 1px solid #e6e6e6;
+}
+
+.menu-vertically-limited.with-bottom-separator {
+ border-bottom: 1px solid #e6e6e6;
+}
+
.menu .menu-footer > a > span {
border-bottom: 1px solid var(--gray80);
color: var(--secondFontColor);
diff --git a/server/sonar-web/src/main/js/app/styles/init/misc.css b/server/sonar-web/src/main/js/app/styles/init/misc.css
index 72cadc3c21b..f87994be76e 100644
--- a/server/sonar-web/src/main/js/app/styles/init/misc.css
+++ b/server/sonar-web/src/main/js/app/styles/init/misc.css
@@ -29,6 +29,10 @@
vertical-align: middle;
}
+.vertical-text-top {
+ vertical-align: text-top;
+}
+
.nowrap {
white-space: nowrap;
}
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
index 97b99759784..87359ccaf3f 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsMeta.tsx
@@ -101,6 +101,7 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> {
const { canWrite, ruleDetails } = this.props;
const { sysTags = [], tags = [] } = ruleDetails;
const allTags = [...sysTags, ...tags];
+
return (
<li className="coding-rules-detail-property" data-meta="tags">
{this.props.canWrite ? (
@@ -111,7 +112,7 @@ export default class RuleDetailsMeta extends React.PureComponent<Props, State> {
organization={this.props.organization}
setTags={this.props.onTagsChange}
sysTags={sysTags}
- tags={tags}
+ tags={allTags}
/>
}
position="bottomleft"
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
index ddd9a5d54ed..38d3520623c 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsTagsPopup.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { without, uniq } from 'lodash';
+import { without, uniq, difference } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { getRuleTags } from '../../../api/rules';
import { BubblePopupPosition } from '../../../components/common/BubblePopup';
@@ -74,6 +74,7 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
};
render() {
+ const availableTags = difference(this.state.searchResult, this.props.tags);
return (
<TagsSelector
listSize={LIST_SIZE}
@@ -82,7 +83,7 @@ export default class RuleDetailsTagsPopup extends React.PureComponent<Props, Sta
onUnselect={this.onUnselect}
position={this.props.popupPosition || {}}
selectedTags={this.props.tags}
- tags={this.state.searchResult}
+ tags={availableTags}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
index 4dde591feb7..bdc74e4d941 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTags.tsx
@@ -85,11 +85,11 @@ export default class MetaTags extends React.PureComponent<Props, State> {
right: containerPos.width - eltPos.width
});
- handleSetProjectTags = (tags: string[]) => {
- setProjectTags({ project: this.props.component.key, tags: tags.join(',') }).then(
- () => this.props.onComponentChange({ tags }),
- () => {}
- );
+ handleSetProjectTags = (values: string[]) => {
+ setProjectTags({
+ project: this.props.component.key,
+ tags: values.join(',')
+ }).then(() => this.props.onComponentChange({ tags: values }), () => {});
};
render() {
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
index 2fdd9fe1f6e..783688ebf7c 100644
--- a/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
+++ b/server/sonar-web/src/main/js/apps/overview/meta/MetaTagsSelector.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { without } from 'lodash';
+import { without, difference } from 'lodash';
import TagsSelector from '../../../components/tags/TagsSelector';
import { BubblePopupPosition } from '../../../components/common/BubblePopup';
import { searchProjectTags } from '../../../api/components';
@@ -71,6 +71,7 @@ export default class MetaTagsSelector extends React.PureComponent<Props, State>
};
render() {
+ const availableTags = difference(this.state.searchResult, this.props.selectedTags);
return (
<TagsSelector
listSize={LIST_SIZE}
@@ -79,7 +80,7 @@ export default class MetaTagsSelector extends React.PureComponent<Props, State>
onUnselect={this.onUnselect}
position={this.props.position}
selectedTags={this.props.selectedTags}
- tags={this.state.searchResult}
+ tags={availableTags}
/>
);
}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
index 908b2b09241..43057590919 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.js
@@ -202,6 +202,7 @@ export default class ProjectActivityGraphs extends React.PureComponent {
graph={query.graph}
metrics={metrics}
metricsTypeFilter={this.getMetricsTypeFilter()}
+ removeCustomMetric={this.removeCustomMetric}
selectedMetrics={this.props.query.customMetrics}
updateGraph={this.updateGraph}
/>
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js
index 7b0a4a2a4bb..c9d1c0ec807 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphsHeader.js
@@ -28,6 +28,7 @@ import { translate } from '../../../helpers/l10n';
/*::
type Props = {
addCustomMetric: string => void,
+ removeCustomMetric: string => void,
graph: string,
metrics: Array<Metric>,
metricsTypeFilter: ?Array<string>,
@@ -56,10 +57,10 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent {
<Select
className="pull-left input-medium"
clearable={false}
+ onChange={this.handleGraphChange}
+ options={selectOptions}
searchable={false}
value={this.props.graph}
- options={selectOptions}
- onChange={this.handleGraphChange}
/>
{isCustomGraph(this.props.graph) && (
<AddGraphMetric
@@ -67,6 +68,7 @@ export default class ProjectActivityGraphsHeader extends React.PureComponent {
className="pull-left spacer-left"
metrics={this.props.metrics}
metricsTypeFilter={this.props.metricsTypeFilter}
+ removeMetric={this.props.removeCustomMetric}
selectedMetrics={this.props.selectedMetrics}
/>
)}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap
index fbfde630913..839975cbd4d 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/__snapshots__/ProjectActivityGraphs-test.js.snap
@@ -17,6 +17,7 @@ exports[`should render correctly the graph and legends 1`] = `
]
}
metricsTypeFilter={null}
+ removeCustomMetric={[Function]}
updateGraph={[Function]}
/>
<GraphsHistory
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
index 73d9c3e4af2..78b47814b1b 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetric.js
@@ -19,16 +19,12 @@
*/
// @flow
import React from 'react';
-import classNames from 'classnames';
-import Modal from '../../../../components/controls/Modal';
-import Select from '../../../../components/controls/Select';
-import Tooltip from '../../../../components/controls/Tooltip';
+import { find, sortBy } from 'lodash';
+import AddGraphMetricPopup from './AddGraphMetricPopup';
+import DropdownIcon from '../../../../components/icons-components/DropdownIcon';
+import BubblePopupHelper from '../../../../components/common/BubblePopupHelper';
import { isDiffMetric } from '../../../../helpers/measures';
-import {
- getLocalizedMetricName,
- translate,
- translateWithParameters
-} from '../../../../helpers/l10n';
+import { getLocalizedMetricName, translate } from '../../../../helpers/l10n';
/*:: import type { Metric } from '../../types'; */
/*::
@@ -37,6 +33,7 @@ type Props = {
className?: string,
metrics: Array<Metric>,
metricsTypeFilter: ?Array<string>,
+ removeMetric: (metric: string) => void,
selectedMetrics: Array<string>
};
*/
@@ -44,24 +41,46 @@ type Props = {
/*::
type State = {
open: boolean,
- selectedMetric?: string
+ query: string,
};
*/
export default class AddGraphMetric extends React.PureComponent {
/*:: props: Props; */
state /*: State */ = {
- open: false
+ open: false,
+ metrics: [],
+ query: '',
+ selectedMetrics: []
};
- getMetricsOptions = (metricsTypeFilter /*: ?Array<string> */) => {
- return this.props.metrics
+ filterSelected = (query /*: string*/, selectedElements /*: string[]*/) => {
+ return selectedElements.filter(element =>
+ this.getLocalizedMetricNameFromKey(element)
+ .toLowerCase()
+ .includes(query.toLowerCase())
+ );
+ };
+
+ getPopupPos = (containerPos /*: ClientRect*/) => ({
+ top: containerPos.height,
+ right: containerPos.width - 240
+ });
+
+ filterMetricsElements = (
+ { metricsTypeFilter, metrics, selectedMetrics } /*: Props */,
+ query /*: string*/
+ ) => {
+ return metrics
.filter(metric => {
if (
metric.hidden ||
isDiffMetric(metric.key) ||
['DATA', 'DISTRIB'].includes(metric.type) ||
- this.props.selectedMetrics.includes(metric.key)
+ selectedMetrics.includes(metric.key) ||
+ !getLocalizedMetricName(metric)
+ .toLowerCase()
+ .includes(query.toLowerCase())
) {
return false;
}
@@ -70,112 +89,96 @@ export default class AddGraphMetric extends React.PureComponent {
}
return true;
})
- .map((metric /*: Metric */) => ({
- value: metric.key,
- label: getLocalizedMetricName(metric)
- }));
+ .map(metric => metric.key);
+ };
+
+ getSelectedMetricsElements = (
+ metrics /*: Array<Metric> */,
+ selectedMetrics /*: Array<string> | null */,
+ query /*: string */
+ ) => {
+ const selected /*: Array<string> */ =
+ selectedMetrics === null ? this.props.selectedMetrics : selectedMetrics;
+ return metrics.filter(metric => selected.includes(metric.key)).map(metric => metric.key);
};
- openForm = () => {
- this.setState({
- open: true
+ getLocalizedMetricNameFromKey = (key /*: string*/) => {
+ const metric = find(this.props.metrics, { key });
+ return metric === undefined ? key : getLocalizedMetricName(metric);
+ };
+
+ toggleForm = () => {
+ this.setState(state => {
+ return { open: !state.open };
});
};
- closeForm = () => {
- this.setState({
- open: false,
- selectedMetric: undefined
+ onSearch = (query /*: string */) => {
+ this.setState({ query });
+ return Promise.resolve();
+ };
+
+ onSelect = (metric /*: string */) => {
+ this.props.addMetric(metric);
+ this.setState(state => {
+ return {
+ selectedMetrics: sortBy([...state.selectedMetrics, metric]),
+ metrics: this.filterMetricsElements(this.props, state.query)
+ };
});
};
- handleChange = (option /*: { value: string, label: string } */) =>
- this.setState({ selectedMetric: option.value });
+ onUnselect = (metric /*: string */) => {
+ this.props.removeMetric(metric);
+ this.setState(state => {
+ return {
+ metrics: sortBy([...state.metrics, metric]),
+ selectedMetrics: state.selectedMetrics.filter(selected => selected !== metric)
+ };
+ });
+ };
- handleSubmit = (e /*: Object */) => {
- e.preventDefault();
- if (this.state.selectedMetric) {
- this.props.addMetric(this.state.selectedMetric);
- this.closeForm();
- }
+ togglePopup = (open /*: boolean*/) => {
+ this.setState({ open });
};
- renderModal() {
- const { metricsTypeFilter } = this.props;
- const header = translate('project_activity.graphs.custom.add_metric');
+ render() {
+ const { query } = this.state;
+ const filteredMetrics = this.filterMetricsElements(this.props, query);
+ const selectedMetrics = this.getSelectedMetricsElements(
+ this.props.metrics,
+ this.props.selectedMetrics,
+ query
+ );
return (
- <Modal key="add-metric-modal" contentLabel={header} onRequestClose={this.closeForm}>
- <header className="modal-head">
- <h2>{header}</h2>
- </header>
- <form onSubmit={this.handleSubmit}>
- <div className="modal-body">
- <div className="modal-large-field">
- <label>{translate('project_activity.graphs.custom.search')}</label>
- <Select
- autofocus={true}
- className="Select-big"
- clearable={false}
- noResultsText={translate('no_results')}
- onChange={this.handleChange}
- options={this.getMetricsOptions(metricsTypeFilter)}
- placeholder=""
- searchable={true}
- value={this.state.selectedMetric}
- />
- <span className="alert alert-info">
- {metricsTypeFilter != null && metricsTypeFilter.length > 0
- ? translateWithParameters(
- 'project_activity.graphs.custom.type_x_message',
- metricsTypeFilter
- .map(type => translate('metric.type', type))
- .sort()
- .join(', ')
- )
- : translate('project_activity.graphs.custom.add_metric_info')}
- </span>
- </div>
- </div>
- <footer className="modal-foot">
- <div>
- <button type="submit" disabled={!this.state.selectedMetric}>
+ <div className="display-inline-block">
+ <BubblePopupHelper
+ isOpen={this.state.open}
+ offset={{ horizontal: 16, vertical: 0 }}
+ popup={
+ <AddGraphMetricPopup
+ elements={filteredMetrics}
+ filterSelected={this.filterSelected}
+ metricsTypeFilter={this.props.metricsTypeFilter}
+ onSearch={this.onSearch}
+ onSelect={this.onSelect}
+ onUnselect={this.onUnselect}
+ renderLabel={element => this.getLocalizedMetricNameFromKey(element)}
+ selectedElements={selectedMetrics}
+ />
+ }
+ position="bottomright"
+ togglePopup={this.togglePopup}>
+ <button className="spacer-left" onClick={this.toggleForm} type="button">
+ <span>
+ <span className="text-ellipsis spacer-right">
{translate('project_activity.graphs.custom.add')}
- </button>
- <button type="reset" className="button-link" onClick={this.closeForm}>
- {translate('cancel')}
- </button>
- </div>
- </footer>
- </form>
- </Modal>
- );
- }
-
- render() {
- if (this.props.selectedMetrics.length >= 6) {
- // Use the class .disabled instead of the property to prevent a bug from
- // rc-tooltip : https://github.com/react-component/tooltip/issues/18
- return (
- <Tooltip
- placement="right"
- overlay={translate('project_activity.graphs.custom.add_metric_info')}>
- <button className={classNames('disabled', this.props.className)}>
- {translate('project_activity.graphs.custom.add')}
+ </span>
+ <DropdownIcon className="vertical-text-top" />
+ </span>
</button>
- </Tooltip>
- );
- }
-
- const buttonComponent = (
- <button key="add-metric-button" className={this.props.className} onClick={this.openForm}>
- {translate('project_activity.graphs.custom.add')}
- </button>
+ </BubblePopupHelper>
+ </div>
);
-
- if (this.state.open) {
- return [buttonComponent, this.renderModal()];
- }
-
- return buttonComponent;
}
}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx
new file mode 100644
index 00000000000..81864bf8920
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/projectActivity/components/forms/AddGraphMetricPopup.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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 BubblePopup from '../../../../components/common/BubblePopup';
+import MultiSelect from '../../../../components/common/MultiSelect';
+import { translate, translateWithParameters } from '../../../../helpers/l10n';
+
+interface Props {
+ elements: string[];
+ filterSelected: (query: string, selectedElements: string[]) => string[];
+ metricsTypeFilter: string[];
+ onSearch: (query: string) => Promise<void>;
+ onSelect: (item: string) => void;
+ onUnselect: (item: string) => void;
+ popupPosition?: any;
+ renderLabel: (element: string) => React.ReactNode;
+ selectedElements: string[];
+}
+
+export default function AddGraphMetricPopup({ elements, metricsTypeFilter, ...props }: Props) {
+ let footerNode: React.ReactNode = '';
+
+ if (props.selectedElements.length >= 6) {
+ footerNode = (
+ <span className="alert alert-info spacer-left spacer-right spacer-top">
+ {translate('project_activity.graphs.custom.add_metric_info')}
+ </span>
+ );
+ } else if (metricsTypeFilter != null && metricsTypeFilter.length > 0) {
+ footerNode = (
+ <span className="alert alert-info spacer-left spacer-right spacer-top">
+ {translateWithParameters(
+ 'project_activity.graphs.custom.type_x_message',
+ metricsTypeFilter
+ .map((type: string) => translate('metric.type', type))
+ .sort()
+ .join(', ')
+ )}
+ </span>
+ );
+ }
+
+ return (
+ <BubblePopup
+ customClass="bubble-popup-bottom-right bubble-popup-menu abs-width-300"
+ position={props.popupPosition}>
+ <MultiSelect
+ allowNewElements={false}
+ allowSelection={props.selectedElements.length < 6}
+ elements={elements}
+ filterSelected={props.filterSelected}
+ footerNode={footerNode}
+ onSearch={props.onSearch}
+ onSelect={props.onSelect}
+ onUnselect={props.onUnselect}
+ placeholder={translate('search.search_for_tags')}
+ renderLabel={props.renderLabel}
+ selectedElements={props.selectedElements}
+ />
+ </BubblePopup>
+ );
+}
diff --git a/server/sonar-web/src/main/js/apps/projectActivity/utils.js b/server/sonar-web/src/main/js/apps/projectActivity/utils.js
index 5a8ae9aa5a1..ad2e53d848a 100644
--- a/server/sonar-web/src/main/js/apps/projectActivity/utils.js
+++ b/server/sonar-web/src/main/js/apps/projectActivity/utils.js
@@ -111,7 +111,7 @@ export function generateSeries(
metrics /*: Array<Metric> | { [string]: Metric } */,
displayedMetrics /*: Array<string> */
) /*: Array<Serie> */ {
- if (displayedMetrics.length <= 0) {
+ if (displayedMetrics.length <= 0 || typeof measuresHistory === 'undefined') {
return [];
}
return sortBy(
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx
index 91a68f25fe8..892cab66813 100644
--- a/server/sonar-web/src/main/js/components/common/MultiSelect.tsx
+++ b/server/sonar-web/src/main/js/components/common/MultiSelect.tsx
@@ -18,17 +18,24 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
+import * as classNames from 'classnames';
import { difference } from 'lodash';
import MultiSelectOption from './MultiSelectOption';
import SearchBox from '../controls/SearchBox';
+import { translateWithParameters } from '../../helpers/l10n';
interface Props {
+ allowNewElements?: boolean;
+ allowSelection?: boolean;
elements: string[];
+ filterSelected?: (query: string, selectedElements: string[]) => string[];
+ footerNode?: React.ReactNode;
listSize?: number;
onSearch: (query: string) => Promise<void>;
onSelect: (item: string) => void;
onUnselect: (item: string) => void;
placeholder: string;
+ renderLabel?: (element: string) => React.ReactNode;
selectedElements: string[];
validateSearchInput?: (value: string) => string;
}
@@ -42,7 +49,9 @@ interface State {
}
interface DefaultProps {
+ filterSelected: (query: string, selectedElements: string[]) => string[];
listSize: number;
+ renderLabel: (element: string) => React.ReactNode;
validateSearchInput: (value: string) => string;
}
@@ -54,7 +63,10 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
mounted = false;
static defaultProps: DefaultProps = {
- listSize: 10,
+ filterSelected: (query: string, selectedElements: string[]) =>
+ selectedElements.filter(elem => elem.includes(query)),
+ listSize: 0,
+ renderLabel: (element: string) => element,
validateSearchInput: (value: string) => value
};
@@ -72,7 +84,7 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
componentDidMount() {
this.mounted = true;
this.onSearchQuery('');
- this.updateSelectedElements(this.props);
+ this.updateSelectedElements(this.props as PropsWithDefault);
this.updateUnselectedElements(this.props as PropsWithDefault);
if (this.container) {
this.container.addEventListener('keydown', this.handleKeyboard, true);
@@ -164,13 +176,13 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
onUnselectItem = (item: string) => this.props.onUnselect(item);
isNewElement = (elem: string, { selectedElements, elements }: Props) =>
- elem && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1;
+ elem.length > 0 && selectedElements.indexOf(elem) === -1 && elements.indexOf(elem) === -1;
- updateSelectedElements = (props: Props) => {
+ updateSelectedElements = (props: PropsWithDefault) => {
this.setState((state: State) => {
if (state.query) {
return {
- selectedElements: [...props.selectedElements.filter(elem => elem.includes(state.query))]
+ selectedElements: props.filterSelected(state.query, props.selectedElements)
};
} else {
return { selectedElements: [...props.selectedElements] };
@@ -180,7 +192,9 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
updateUnselectedElements = (props: PropsWithDefault) => {
this.setState((state: State) => {
- if (props.listSize < state.selectedElements.length) {
+ if (props.listSize === 0) {
+ return { unselectedElements: difference(props.elements, props.selectedElements) };
+ } else if (props.listSize < state.selectedElements.length) {
return { unselectedElements: [] };
} else {
return {
@@ -239,8 +253,17 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
};
render() {
+ const { allowSelection = true, allowNewElements = true, footerNode = '' } = this.props;
+ const { renderLabel } = this.props as PropsWithDefault;
const { query, activeIdx, selectedElements, unselectedElements } = this.state;
const activeElement = this.getAllElements(this.props, this.state)[activeIdx];
+ const infiniteList = this.props.listSize === 0;
+ const listClasses = classNames('menu', {
+ 'menu-vertically-limited': infiniteList,
+ 'spacer-top': infiniteList,
+ 'with-top-separator': infiniteList,
+ 'with-bottom-separator': Boolean(footerNode)
+ });
return (
<div className="multi-select" ref={div => (this.container = div)}>
@@ -254,7 +277,11 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
value={query}
/>
</div>
- <ul className="menu">
+ <ul className={listClasses}>
+ {selectedElements.length < 1 &&
+ unselectedElements.length < 1 && (
+ <li className="spacer-left">{translateWithParameters('no_results_for_x', query)}</li>
+ )}
{selectedElements.length > 0 &&
selectedElements.map(element => (
<MultiSelectOption
@@ -263,6 +290,7 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
key={element}
onHover={this.handleElementHover}
onSelectChange={this.handleSelectChange}
+ renderLabel={renderLabel}
selected={true}
/>
))}
@@ -270,23 +298,28 @@ export default class MultiSelect extends React.PureComponent<Props, State> {
unselectedElements.map(element => (
<MultiSelectOption
active={activeElement === element}
+ disabled={!allowSelection}
element={element}
key={element}
onHover={this.handleElementHover}
onSelectChange={this.handleSelectChange}
+ renderLabel={renderLabel}
/>
))}
- {this.isNewElement(query, this.props) && (
- <MultiSelectOption
- active={activeElement === query}
- custom={true}
- element={query}
- key={query}
- onHover={this.handleElementHover}
- onSelectChange={this.handleSelectChange}
- />
- )}
+ {allowNewElements &&
+ this.isNewElement(query, this.props) && (
+ <MultiSelectOption
+ active={activeElement === query}
+ custom={true}
+ element={query}
+ key={query}
+ onHover={this.handleElementHover}
+ onSelectChange={this.handleSelectChange}
+ renderLabel={renderLabel}
+ />
+ )}
</ul>
+ {footerNode}
</div>
);
}
diff --git a/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx b/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx
index 89429426d7e..879bdada43f 100644
--- a/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx
+++ b/server/sonar-web/src/main/js/components/common/MultiSelectOption.tsx
@@ -21,12 +21,14 @@ import * as React from 'react';
import * as classNames from 'classnames';
interface Props {
- element: string;
- selected?: boolean;
- custom?: boolean;
active?: boolean;
- onSelectChange: (elem: string, selected: boolean) => void;
+ custom?: boolean;
+ disabled?: boolean;
+ element: string;
onHover: (elem: string) => void;
+ onSelectChange: (elem: string, selected: boolean) => void;
+ renderLabel: (element: string) => React.ReactNode;
+ selected?: boolean;
}
export default class MultiSelectOption extends React.PureComponent<Props> {
@@ -34,27 +36,36 @@ export default class MultiSelectOption extends React.PureComponent<Props> {
evt.stopPropagation();
evt.preventDefault();
evt.currentTarget.blur();
- this.props.onSelectChange(this.props.element, !this.props.selected);
+
+ if (!this.props.disabled) {
+ this.props.onSelectChange(this.props.element, !this.props.selected);
+ }
};
handleHover = () => this.props.onHover(this.props.element);
render() {
+ const { selected, disabled } = this.props;
const className = classNames('icon-checkbox', {
- 'icon-checkbox-checked': this.props.selected
+ 'icon-checkbox-checked': selected,
+ 'icon-checkbox-invisible': disabled
+ });
+ const activeClass = classNames({
+ active: this.props.active,
+ disabled,
+ 'cursor-not-allowed': disabled
});
- const activeClass = classNames({ active: this.props.active });
return (
<li>
<a
- href="#"
className={activeClass}
+ href="#"
onClick={this.handleSelect}
- onMouseOver={this.handleHover}
- onFocus={this.handleHover}>
+ onFocus={this.handleHover}
+ onMouseOver={this.handleHover}>
<i className={className} /> {this.props.custom && '+ '}
- {this.props.element}
+ {this.props.renderLabel(this.props.element)}
</a>
</li>
);
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx
index 097fd543c36..ab5971a2899 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx
+++ b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelect-test.tsx
@@ -27,10 +27,15 @@ const props = {
onSearch: () => Promise.resolve(),
onSelect: () => {},
onUnselect: () => {},
+ renderLabel: (element: string) => element,
placeholder: ''
};
-const elements = ['foo', 'bar', 'baz'];
+const elements = [
+ { key: 'foo', label: 'foo' },
+ { key: 'bar', label: 'bar' },
+ { key: 'baz', label: 'baz' }
+];
it('should render multiselect with selected elements', () => {
const multiselect = shallow(<MultiSelect {...props} />);
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx
index 5b72d18b21e..88776be4045 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx
+++ b/server/sonar-web/src/main/js/components/common/__tests__/MultiSelectOption-test.tsx
@@ -24,21 +24,26 @@ import MultiSelectOption from '../MultiSelectOption';
const props = {
element: 'mytag',
onSelectChange: () => {},
- onHover: () => {}
+ onHover: () => {},
+ renderLabel: (element: string) => element
};
-it('should render standard tag', () => {
+it('should render standard element', () => {
expect(shallow(<MultiSelectOption {...props} />)).toMatchSnapshot();
});
-it('should render selected tag', () => {
+it('should render selected element', () => {
expect(shallow(<MultiSelectOption {...props} selected={true} />)).toMatchSnapshot();
});
-it('should render custom tag', () => {
+it('should render custom element', () => {
expect(shallow(<MultiSelectOption {...props} custom={true} />)).toMatchSnapshot();
});
-it('should render active tag', () => {
- expect(shallow(<MultiSelectOption {...props} selected={true} active={true} />)).toMatchSnapshot();
+it('should render active element', () => {
+ expect(shallow(<MultiSelectOption {...props} active={true} selected={true} />)).toMatchSnapshot();
+});
+
+it('should render disabled element', () => {
+ expect(shallow(<MultiSelectOption {...props} disabled={true} />)).toMatchSnapshot();
});
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap
index 69b2384f396..87a1bfd052c 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelect-test.tsx.snap
@@ -17,7 +17,7 @@ exports[`should render multiselect with selected elements 1`] = `
/>
</div>
<ul
- className="menu"
+ className="menu menu-vertically-limited spacer-top with-top-separator"
>
<MultiSelectOption
active={true}
@@ -25,6 +25,7 @@ exports[`should render multiselect with selected elements 1`] = `
key="bar"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
selected={true}
/>
</ul>
@@ -48,7 +49,7 @@ exports[`should render multiselect with selected elements 2`] = `
/>
</div>
<ul
- className="menu"
+ className="menu menu-vertically-limited spacer-top with-top-separator"
>
<MultiSelectOption
active={true}
@@ -56,21 +57,50 @@ exports[`should render multiselect with selected elements 2`] = `
key="bar"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
selected={true}
/>
<MultiSelectOption
active={false}
- element="foo"
- key="foo"
+ disabled={false}
+ element={
+ Object {
+ "key": "foo",
+ "label": "foo",
+ }
+ }
+ key="[object Object]"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
/>
<MultiSelectOption
active={false}
- element="baz"
- key="baz"
+ disabled={false}
+ element={
+ Object {
+ "key": "bar",
+ "label": "bar",
+ }
+ }
+ key="[object Object]"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
+ />
+ <MultiSelectOption
+ active={false}
+ disabled={false}
+ element={
+ Object {
+ "key": "baz",
+ "label": "baz",
+ }
+ }
+ key="[object Object]"
+ onHover={[Function]}
+ onSelectChange={[Function]}
+ renderLabel={[Function]}
/>
</ul>
</div>
@@ -93,7 +123,7 @@ exports[`should render multiselect with selected elements 3`] = `
/>
</div>
<ul
- className="menu"
+ className="menu menu-vertically-limited spacer-top with-top-separator"
>
<MultiSelectOption
active={false}
@@ -101,21 +131,50 @@ exports[`should render multiselect with selected elements 3`] = `
key="bar"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
selected={true}
/>
<MultiSelectOption
active={false}
- element="foo"
- key="foo"
+ disabled={false}
+ element={
+ Object {
+ "key": "foo",
+ "label": "foo",
+ }
+ }
+ key="[object Object]"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
/>
<MultiSelectOption
active={true}
- element="baz"
- key="baz"
+ disabled={false}
+ element={
+ Object {
+ "key": "bar",
+ "label": "bar",
+ }
+ }
+ key="[object Object]"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
+ />
+ <MultiSelectOption
+ active={false}
+ disabled={false}
+ element={
+ Object {
+ "key": "baz",
+ "label": "baz",
+ }
+ }
+ key="[object Object]"
+ onHover={[Function]}
+ onSelectChange={[Function]}
+ renderLabel={[Function]}
/>
</ul>
</div>
@@ -138,7 +197,7 @@ exports[`should render multiselect with selected elements 4`] = `
/>
</div>
<ul
- className="menu"
+ className="menu menu-vertically-limited spacer-top with-top-separator"
>
<MultiSelectOption
active={false}
@@ -146,21 +205,50 @@ exports[`should render multiselect with selected elements 4`] = `
key="bar"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
selected={true}
/>
<MultiSelectOption
active={false}
- element="foo"
- key="foo"
+ disabled={false}
+ element={
+ Object {
+ "key": "foo",
+ "label": "foo",
+ }
+ }
+ key="[object Object]"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
/>
<MultiSelectOption
active={true}
- element="baz"
- key="baz"
+ disabled={false}
+ element={
+ Object {
+ "key": "bar",
+ "label": "bar",
+ }
+ }
+ key="[object Object]"
+ onHover={[Function]}
+ onSelectChange={[Function]}
+ renderLabel={[Function]}
+ />
+ <MultiSelectOption
+ active={false}
+ disabled={false}
+ element={
+ Object {
+ "key": "baz",
+ "label": "baz",
+ }
+ }
+ key="[object Object]"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
/>
<MultiSelectOption
active={false}
@@ -169,6 +257,7 @@ exports[`should render multiselect with selected elements 4`] = `
key="test"
onHover={[Function]}
onSelectChange={[Function]}
+ renderLabel={[Function]}
/>
</ul>
</div>
diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap
index b7b9f2a61c0..3f9a5cc7ef5 100644
--- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/MultiSelectOption-test.tsx.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`should render active tag 1`] = `
+exports[`should render active element 1`] = `
<li>
<a
className="active"
@@ -18,7 +18,7 @@ exports[`should render active tag 1`] = `
</li>
`;
-exports[`should render custom tag 1`] = `
+exports[`should render custom element 1`] = `
<li>
<a
className=""
@@ -37,7 +37,25 @@ exports[`should render custom tag 1`] = `
</li>
`;
-exports[`should render selected tag 1`] = `
+exports[`should render disabled element 1`] = `
+<li>
+ <a
+ className="disabled cursor-not-allowed"
+ href="#"
+ onClick={[Function]}
+ onFocus={[Function]}
+ onMouseOver={[Function]}
+ >
+ <i
+ className="icon-checkbox icon-checkbox-invisible"
+ />
+
+ mytag
+ </a>
+</li>
+`;
+
+exports[`should render selected element 1`] = `
<li>
<a
className=""
@@ -55,7 +73,7 @@ exports[`should render selected tag 1`] = `
</li>
`;
-exports[`should render standard tag 1`] = `
+exports[`should render standard element 1`] = `
<li>
<a
className=""
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
index e40a85b71af..fb183a7f8b7 100644
--- a/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
+++ b/server/sonar-web/src/main/js/components/issue/components/IssueTags.js
@@ -59,20 +59,21 @@ export default class IssueTags extends React.PureComponent {
render() {
const { issue } = this.props;
+ const { tags = [] } = issue;
if (this.props.canSetTags) {
return (
<BubblePopupHelper
isOpen={this.props.isOpen}
- position="bottomright"
- togglePopup={this.toggleSetTags}
popup={
<SetIssueTagsPopup
organization={issue.projectOrganization}
- selectedTags={issue.tags}
+ selectedTags={tags}
setTags={this.setTags}
/>
- }>
+ }
+ position="bottomright"
+ togglePopup={this.toggleSetTags}>
<button
className={'js-issue-edit-tags button-link issue-action issue-action-with-options'}
onClick={this.toggleSetTags}>
diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
index 7aa3bebba62..90b8af5f628 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
+++ b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.tsx
@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
-import { without } from 'lodash';
+import { difference, without } from 'lodash';
import { BubblePopupPosition } from '../../../components/common/BubblePopup';
import TagsSelector from '../../../components/tags/TagsSelector';
import { searchIssueTags } from '../../../api/issues';
@@ -72,6 +72,7 @@ export default class SetIssueTagsPopup extends React.PureComponent<Props, State>
};
render() {
+ const availableTags = difference(this.state.searchResult, this.props.selectedTags);
return (
<TagsSelector
listSize={LIST_SIZE}
@@ -80,7 +81,7 @@ export default class SetIssueTagsPopup extends React.PureComponent<Props, State>
onUnselect={this.onUnselect}
position={this.props.popupPosition}
selectedTags={this.props.selectedTags}
- tags={this.state.searchResult}
+ tags={availableTags}
/>
);
}
diff --git a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
index 36d73b1a4cf..57990df2131 100644
--- a/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/issue/popups/__tests__/__snapshots__/SetIssueTagsPopup-test.tsx.snap
@@ -14,7 +14,6 @@ exports[`should render tags popup correctly 1`] = `
}
tags={
Array [
- "mytag",
"test",
"second",
]
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx b/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx
index f2dd87eda02..a3e895bef65 100644
--- a/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx
+++ b/server/sonar-web/src/main/js/components/tags/__tests__/TagsSelector-test.tsx
@@ -26,6 +26,7 @@ const props = {
onSearch: () => Promise.resolve(),
onSelect: () => {},
onUnselect: () => {},
+ renderLabel: (element: string) => element,
position: { right: 0, top: 0 },
selectedTags: ['bar'],
tags: ['foo', 'bar', 'baz']
diff --git a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
index c2d4623b8c8..b6d33603ede 100644
--- a/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/tags/__tests__/__snapshots__/TagsSelector-test.tsx.snap
@@ -18,11 +18,13 @@ exports[`should render with selected tags 1`] = `
"baz",
]
}
+ filterSelected={[Function]}
listSize={10}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
placeholder="search.search_for_tags"
+ renderLabel={[Function]}
selectedElements={
Array [
"bar",
@@ -45,11 +47,13 @@ exports[`should render without tags at all 1`] = `
>
<MultiSelect
elements={Array []}
+ filterSelected={[Function]}
listSize={10}
onSearch={[Function]}
onSelect={[Function]}
onUnselect={[Function]}
placeholder="search.search_for_tags"
+ renderLabel={[Function]}
selectedElements={Array []}
validateSearchInput={[Function]}
/>