aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx24
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx17
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap45
-rw-r--r--server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx7
-rw-r--r--server/sonar-web/src/main/js/components/facet/FacetItem.tsx53
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap146
-rw-r--r--server/sonar-web/src/main/js/components/facet/utils.ts23
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties2
23 files changed, 370 insertions, 155 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
index 7e844278970..f2d93003e5e 100644
--- a/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
+++ b/server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
@@ -51,16 +51,18 @@ interface Props extends BasicProps {
}
export default class Facet extends React.PureComponent<Props> {
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { values } = this.props;
let newValue;
if (this.props.singleSelection) {
const value = values.length ? values[0] : undefined;
newValue = itemValue === value ? undefined : itemValue;
- } else {
+ } else if (multiple) {
newValue = orderBy(
values.includes(itemValue) ? without(values, itemValue) : [...values, itemValue]
);
+ } else {
+ newValue = values.includes(itemValue) && values.length < 2 ? [] : [itemValue];
}
this.props.onChange({ [this.props.property]: newValue });
};
@@ -85,6 +87,7 @@ export default class Facet extends React.PureComponent<Props> {
name={renderName(value)}
onClick={this.handleItemClick}
stat={stat && formatMeasure(stat, 'SHORT_INT')}
+ tooltip={this.props.values.length === 1 && !active}
value={value}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js
index 184b47b8f5b..4b8edc255c3 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js
@@ -85,8 +85,8 @@ export default class Sidebar extends React.PureComponent {
/>
{groupByDomains(this.props.measures).map(domain => (
<DomainFacet
- key={domain.name}
domain={domain}
+ key={domain.name}
onChange={this.changeMetric}
onToggle={this.toggleFacet}
open={this.state.openFacets[domain.name] === true}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
index 77cae8a2d9c..b4896bafea6 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
@@ -50,17 +50,21 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
+ const { assignees } = this.props;
if (itemValue === '') {
// unassigned
this.props.onChange({ assigned: !this.props.assigned, assignees: [] });
- } else {
- // defined assignee
- const { assignees } = this.props;
+ } else if (multiple) {
const newValue = sortBy(
assignees.includes(itemValue) ? without(assignees, itemValue) : [...assignees, itemValue]
);
- this.props.onChange({ assigned: true, assignees: newValue });
+ this.props.onChange({ assigned: true, [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ assigned: true,
+ [this.property]: assignees.includes(itemValue) && assignees.length < 2 ? [] : [itemValue]
+ });
}
};
@@ -169,6 +173,7 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
name={this.getAssigneeName(assignee)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(assignee), this.props.facetMode)}
+ tooltip={this.props.assignees.length === 1 && !this.isAssigneeActive(assignee)}
value={assignee}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
index a09a7e62a42..01e9bc56a9e 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
@@ -43,12 +43,18 @@ export default class AuthorFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { authors } = this.props;
- const newValue = sortBy(
- authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = sortBy(
+ authors.includes(itemValue) ? without(authors, itemValue) : [...authors, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: authors.includes(itemValue) && authors.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -83,6 +89,7 @@ export default class AuthorFacet extends React.PureComponent<Props> {
name={author}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(author), this.props.facetMode)}
+ tooltip={this.props.authors.length === 1 && !this.props.authors.includes(author)}
value={author}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx
index 32f8fd2bdf5..e26b9343194 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx
@@ -46,14 +46,21 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { directories } = this.props;
- const newValue = sortBy(
- directories.includes(itemValue)
- ? without(directories, itemValue)
- : [...directories, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = sortBy(
+ directories.includes(itemValue)
+ ? without(directories, itemValue)
+ : [...directories, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]:
+ directories.includes(itemValue) && directories.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -97,6 +104,9 @@ export default class DirectoryFacet extends React.PureComponent<Props> {
name={this.renderName(directory)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(directory), this.props.facetMode)}
+ tooltip={
+ this.props.directories.length === 1 && !this.props.directories.includes(directory)
+ }
value={directory}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx
index db57e1698e4..a7556e900e3 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx
@@ -46,12 +46,18 @@ export default class FileFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { files } = this.props;
- const newValue = sortBy(
- files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = sortBy(
+ files.includes(itemValue) ? without(files, itemValue) : [...files, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: files.includes(itemValue) && files.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -101,6 +107,7 @@ export default class FileFacet extends React.PureComponent<Props> {
name={this.renderName(file)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(file), this.props.facetMode)}
+ tooltip={this.props.files.length === 1 && !this.props.files.includes(file)}
value={file}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
index 3c9d5f0bfab..0ab48821328 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
@@ -45,12 +45,18 @@ export default class LanguageFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { languages } = this.props;
- const newValue = sortBy(
- languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = sortBy(
+ languages.includes(itemValue) ? without(languages, itemValue) : [...languages, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: languages.includes(itemValue) && languages.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -95,6 +101,7 @@ export default class LanguageFacet extends React.PureComponent<Props> {
name={this.getLanguageName(language)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(language), this.props.facetMode)}
+ tooltip={this.props.languages.length === 1 && !this.props.languages.includes(language)}
value={language}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx
index 9626862ec69..5fccc6f21c5 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx
@@ -45,12 +45,18 @@ export default class ModuleFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { modules } = this.props;
- const newValue = sortBy(
- modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = sortBy(
+ modules.includes(itemValue) ? without(modules, itemValue) : [...modules, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: modules.includes(itemValue) && modules.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -99,6 +105,7 @@ export default class ModuleFacet extends React.PureComponent<Props> {
name={this.renderName(module)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(module), this.props.facetMode)}
+ tooltip={this.props.modules.length === 1 && !this.props.modules.includes(module)}
value={module}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
index a326a2f65e2..69d9fa87f68 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
@@ -51,12 +51,18 @@ export default class ProjectFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { projects } = this.props;
- const newValue = sortBy(
- projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = sortBy(
+ projects.includes(itemValue) ? without(projects, itemValue) : [...projects, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: projects.includes(itemValue) && projects.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -153,6 +159,7 @@ export default class ProjectFacet extends React.PureComponent<Props> {
name={this.renderName(project)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(project), this.props.facetMode)}
+ tooltip={this.props.projects.length === 1 && !this.props.projects.includes(project)}
value={project}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
index 911555a6573..5533b3734c0 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
@@ -44,19 +44,24 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
+ const { resolutions } = this.props;
if (itemValue === '') {
// unresolved
this.props.onChange({ resolved: !this.props.resolved, resolutions: [] });
- } else {
- // defined resolution
- const { resolutions } = this.props;
+ } else if (multiple) {
const newValue = orderBy(
resolutions.includes(itemValue)
? without(resolutions, itemValue)
: [...resolutions, itemValue]
);
- this.props.onChange({ resolved: true, resolutions: newValue });
+ this.props.onChange({ resolved: true, [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ resolved: true,
+ [this.property]:
+ resolutions.includes(itemValue) && resolutions.length < 2 ? [] : [itemValue]
+ });
}
};
@@ -95,6 +100,11 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
name={this.getFacetItemName(resolution)}
onClick={this.handleItemClick}
stat={formatFacetStat(stat, this.props.facetMode)}
+ tooltip={
+ this.props.resolutions.length === 1 &&
+ resolution !== '' &&
+ !this.props.resolutions.includes(resolution)
+ }
value={resolution}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
index 920e2002836..f1d8b43fbe1 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
@@ -48,12 +48,18 @@ export default class RuleFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { rules } = this.props;
- const newValue = sortBy(
- rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = sortBy(
+ rules.includes(itemValue) ? without(rules, itemValue) : [...rules, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: rules.includes(itemValue) && rules.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -111,6 +117,7 @@ export default class RuleFacet extends React.PureComponent<Props> {
name={this.getRuleName(rule)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(rule), this.props.facetMode)}
+ tooltip={this.props.rules.length === 1 && !this.props.rules.includes(rule)}
value={rule}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
index 9b27208e74c..e68f6d8bc91 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
@@ -44,12 +44,18 @@ export default class SeverityFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { severities } = this.props;
- const newValue = orderBy(
- severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = orderBy(
+ severities.includes(itemValue) ? without(severities, itemValue) : [...severities, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: severities.includes(itemValue) && severities.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -79,6 +85,7 @@ export default class SeverityFacet extends React.PureComponent<Props> {
name={<SeverityHelper severity={severity} />}
onClick={this.handleItemClick}
stat={formatFacetStat(stat, this.props.facetMode)}
+ tooltip={this.props.severities.length === 1 && !this.props.severities.includes(severity)}
value={severity}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
index ad563640a4f..f34c2695d08 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
@@ -44,12 +44,18 @@ export default class StatusFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { statuses } = this.props;
- const newValue = orderBy(
- statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = orderBy(
+ statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: statuses.includes(itemValue) && statuses.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -79,6 +85,7 @@ export default class StatusFacet extends React.PureComponent<Props> {
name={<StatusHelper resolution={undefined} status={status} />}
onClick={this.handleItemClick}
stat={formatFacetStat(stat, this.props.facetMode)}
+ tooltip={this.props.statuses.length === 1 && !this.props.statuses.includes(status)}
value={status}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
index 7a2fbe66e12..dab0d43d298 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
@@ -50,12 +50,19 @@ export default class TagFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { tags } = this.props;
- const newValue = sortBy(
- tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const { tags } = this.props;
+ const newValue = sortBy(
+ tags.includes(itemValue) ? without(tags, itemValue) : [...tags, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: tags.includes(itemValue) && tags.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -114,6 +121,7 @@ export default class TagFacet extends React.PureComponent<Props> {
name={this.renderTag(tag)}
onClick={this.handleItemClick}
stat={formatFacetStat(this.getStat(tag), this.props.facetMode)}
+ tooltip={this.props.tags.length === 1 && !this.props.tags.includes(tag)}
value={tag}
/>
))}
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
index 146a6dbe634..889115e7111 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
@@ -44,12 +44,18 @@ export default class TypeFacet extends React.PureComponent<Props> {
open: true
};
- handleItemClick = (itemValue: string) => {
+ handleItemClick = (itemValue: string, multiple: boolean) => {
const { types } = this.props;
- const newValue = orderBy(
- types.includes(itemValue) ? without(types, itemValue) : [...types, itemValue]
- );
- this.props.onChange({ [this.property]: newValue });
+ if (multiple) {
+ const newValue = orderBy(
+ types.includes(itemValue) ? without(types, itemValue) : [...types, itemValue]
+ );
+ this.props.onChange({ [this.property]: newValue });
+ } else {
+ this.props.onChange({
+ [this.property]: types.includes(itemValue) && types.length < 2 ? [] : [itemValue]
+ });
+ }
};
handleHeaderClick = () => {
@@ -82,6 +88,7 @@ export default class TypeFacet extends React.PureComponent<Props> {
}
onClick={this.handleItemClick}
stat={formatFacetStat(stat, this.props.facetMode)}
+ tooltip={this.props.types.length === 1 && !this.props.types.includes(type)}
value={type}
/>
);
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
index 13d70175782..7378d918cc1 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
@@ -35,7 +35,7 @@ const renderAssigneeFacet = (props?: Partial<Props>) =>
open={true}
organization={undefined}
referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }}
- stats={{ '': 5, foo: 13, bar: 7 }}
+ stats={{ '': 5, foo: 13, bar: 7, baz: 6 }}
{...props}
/>
);
@@ -75,10 +75,10 @@ it('should call onChange', () => {
expect(onChange).lastCalledWith({ assigned: false, assignees: [] });
itemOnClick('bar');
- expect(onChange).lastCalledWith({ assigned: true, assignees: ['bar', 'foo'] });
+ expect(onChange).lastCalledWith({ assigned: true, assignees: ['bar'] });
- itemOnClick('foo');
- expect(onChange).lastCalledWith({ assigned: true, assignees: [] });
+ itemOnClick('baz', true);
+ expect(onChange).lastCalledWith({ assigned: true, assignees: ['baz', 'foo'] });
});
it('should call onToggle', () => {
diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
index 1d183049d3d..e28401150f4 100644
--- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
+++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
@@ -20,6 +20,7 @@ exports[`should render 1`] = `
name="unassigned"
onClick={[Function]}
stat="5"
+ tooltip={false}
value=""
/>
<FacetItem
@@ -41,6 +42,7 @@ exports[`should render 1`] = `
}
onClick={[Function]}
stat="13"
+ tooltip={false}
value="foo"
/>
<FacetItem
@@ -52,8 +54,21 @@ exports[`should render 1`] = `
name="bar"
onClick={[Function]}
stat="7"
+ tooltip={false}
value="bar"
/>
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ key="baz"
+ loading={false}
+ name="baz"
+ onClick={[Function]}
+ stat="6"
+ tooltip={false}
+ value="baz"
+ />
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
@@ -113,6 +128,7 @@ exports[`should select unassigned 1`] = `
name="unassigned"
onClick={[Function]}
stat="5"
+ tooltip={false}
value=""
/>
<FacetItem
@@ -134,6 +150,7 @@ exports[`should select unassigned 1`] = `
}
onClick={[Function]}
stat="13"
+ tooltip={false}
value="foo"
/>
<FacetItem
@@ -145,8 +162,21 @@ exports[`should select unassigned 1`] = `
name="bar"
onClick={[Function]}
stat="7"
+ tooltip={false}
value="bar"
/>
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ key="baz"
+ loading={false}
+ name="baz"
+ onClick={[Function]}
+ stat="6"
+ tooltip={false}
+ value="baz"
+ />
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
@@ -180,6 +210,7 @@ exports[`should select user 1`] = `
name="unassigned"
onClick={[Function]}
stat="5"
+ tooltip={true}
value=""
/>
<FacetItem
@@ -201,6 +232,7 @@ exports[`should select user 1`] = `
}
onClick={[Function]}
stat="13"
+ tooltip={false}
value="foo"
/>
<FacetItem
@@ -212,8 +244,21 @@ exports[`should select user 1`] = `
name="bar"
onClick={[Function]}
stat="7"
+ tooltip={true}
value="bar"
/>
+ <FacetItem
+ active={false}
+ disabled={false}
+ halfWidth={false}
+ key="baz"
+ loading={false}
+ name="baz"
+ onClick={[Function]}
+ stat="6"
+ tooltip={true}
+ value="baz"
+ />
</FacetItemsList>
<FacetFooter
onSearch={[Function]}
diff --git a/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx
index 2c316a8cce9..50a9fa718e0 100644
--- a/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx
+++ b/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx
@@ -65,7 +65,7 @@ export default class Filter extends React.PureComponent<Props> {
);
}
- handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
@@ -74,14 +74,15 @@ export default class Filter extends React.PureComponent<Props> {
let urlOption;
if (option) {
- if (Array.isArray(value)) {
+ if (Array.isArray(value) && event.ctrlKey) {
if (this.isSelected(option)) {
urlOption = value.length > 1 ? value.filter(val => val !== option).join(',') : null;
} else {
urlOption = value.concat(option).join(',');
}
} else {
- urlOption = this.isSelected(option) ? null : option;
+ urlOption =
+ this.isSelected(option) && (!Array.isArray(value) || value.length < 2) ? null : option;
}
this.props.onQueryChange({ [property]: urlOption });
diff --git a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
index 17f9a3c4b16..ccd19527646 100644
--- a/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
+++ b/server/sonar-web/src/main/js/components/facet/FacetItem.tsx
@@ -19,6 +19,9 @@
*/
import * as React from 'react';
import * as classNames from 'classnames';
+import { isOnMac } from './utils';
+import Tooltip from '../controls/Tooltip';
+import { translate } from '../../helpers/l10n';
export interface Props {
active?: boolean;
@@ -27,8 +30,9 @@ export interface Props {
halfWidth?: boolean;
loading?: boolean;
name: React.ReactNode;
- onClick: (x: string) => void;
+ onClick: (x: string, multiple?: boolean) => void;
stat?: React.ReactNode;
+ tooltip?: boolean;
value: string;
}
@@ -39,10 +43,10 @@ export default class FacetItem extends React.PureComponent<Props> {
loading: false
};
- handleClick = (event: React.SyntheticEvent<HTMLAnchorElement>) => {
+ handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.currentTarget.blur();
- this.props.onClick(this.props.value);
+ this.props.onClick(this.props.value, event.ctrlKey || event.metaKey);
};
render() {
@@ -51,20 +55,37 @@ export default class FacetItem extends React.PureComponent<Props> {
'search-navigator-facet-half': this.props.halfWidth
});
- return this.props.disabled ? (
- <span className={className} data-facet={this.props.value}>
- <span className="facet-name">{this.props.name}</span>
- {this.props.stat != null && (
- <span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
- )}
- </span>
- ) : (
- <a className={className} data-facet={this.props.value} href="#" onClick={this.handleClick}>
- <span className="facet-name">{this.props.name}</span>
- {this.props.stat != null && (
- <span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
+ const overlay =
+ this.props.tooltip && !this.props.disabled
+ ? translate(
+ isOnMac()
+ ? 'shortcuts.section.global.facets.multiselection.mac'
+ : 'shortcuts.section.global.facets.multiselection'
+ )
+ : undefined;
+
+ return (
+ <Tooltip overlay={overlay} placement="right">
+ {this.props.disabled ? (
+ <span className={className} data-facet={this.props.value}>
+ <span className="facet-name">{this.props.name}</span>
+ {this.props.stat != null && (
+ <span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
+ )}
+ </span>
+ ) : (
+ <a
+ className={className}
+ data-facet={this.props.value}
+ href="#"
+ onClick={this.handleClick}>
+ <span className="facet-name">{this.props.name}</span>
+ {this.props.stat != null && (
+ <span className="facet-stat">{this.props.loading ? '' : this.props.stat}</span>
+ )}
+ </a>
)}
- </a>
+ </Tooltip>
);
}
}
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx
index 95c822c8eb3..e6633a9ff6d 100644
--- a/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx
@@ -49,7 +49,7 @@ it('should render half width', () => {
it('should call onClick', () => {
const onClick = jest.fn();
const wrapper = renderFacetItem({ onClick });
- click(wrapper, { currentTarget: { blur() {}, dataset: { value: 'bar' } } });
+ click(wrapper.find('a'), { currentTarget: { blur() {}, dataset: { value: 'bar' } } });
expect(onClick).toHaveBeenCalled();
});
diff --git a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap
index 3044d7ddbbd..959d2393107 100644
--- a/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap
+++ b/server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap
@@ -1,94 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should loading stat 1`] = `
-<a
- className="facet search-navigator-facet"
- data-facet="bar"
- href="#"
- onClick={[Function]}
+<Tooltip
+ placement="right"
>
- <span
- className="facet-name"
+ <a
+ className="facet search-navigator-facet"
+ data-facet="bar"
+ href="#"
+ onClick={[Function]}
>
- foo
- </span>
-</a>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+ </a>
+</Tooltip>
`;
exports[`should render active 1`] = `
-<a
- className="facet search-navigator-facet active"
- data-facet="bar"
- href="#"
- onClick={[Function]}
+<Tooltip
+ placement="right"
>
- <span
- className="facet-name"
+ <a
+ className="facet search-navigator-facet active"
+ data-facet="bar"
+ href="#"
+ onClick={[Function]}
>
- foo
- </span>
-</a>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+ </a>
+</Tooltip>
`;
exports[`should render disabled 1`] = `
-<span
- className="facet search-navigator-facet"
- data-facet="bar"
+<Tooltip
+ placement="right"
>
<span
- className="facet-name"
+ className="facet search-navigator-facet"
+ data-facet="bar"
>
- foo
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
</span>
-</span>
+</Tooltip>
`;
exports[`should render half width 1`] = `
-<a
- className="facet search-navigator-facet search-navigator-facet-half"
- data-facet="bar"
- href="#"
- onClick={[Function]}
+<Tooltip
+ placement="right"
>
- <span
- className="facet-name"
+ <a
+ className="facet search-navigator-facet search-navigator-facet-half"
+ data-facet="bar"
+ href="#"
+ onClick={[Function]}
>
- foo
- </span>
-</a>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+ </a>
+</Tooltip>
`;
exports[`should render inactive 1`] = `
-<a
- className="facet search-navigator-facet"
- data-facet="bar"
- href="#"
- onClick={[Function]}
+<Tooltip
+ placement="right"
>
- <span
- className="facet-name"
+ <a
+ className="facet search-navigator-facet"
+ data-facet="bar"
+ href="#"
+ onClick={[Function]}
>
- foo
- </span>
-</a>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+ </a>
+</Tooltip>
`;
exports[`should render stat 1`] = `
-<a
- className="facet search-navigator-facet"
- data-facet="bar"
- href="#"
- onClick={[Function]}
+<Tooltip
+ placement="right"
>
- <span
- className="facet-name"
- >
- foo
- </span>
- <span
- className="facet-stat"
+ <a
+ className="facet search-navigator-facet"
+ data-facet="bar"
+ href="#"
+ onClick={[Function]}
>
- 13
- </span>
-</a>
+ <span
+ className="facet-name"
+ >
+ foo
+ </span>
+ <span
+ className="facet-stat"
+ >
+ 13
+ </span>
+ </a>
+</Tooltip>
`;
diff --git a/server/sonar-web/src/main/js/components/facet/utils.ts b/server/sonar-web/src/main/js/components/facet/utils.ts
new file mode 100644
index 00000000000..8c55de0abdd
--- /dev/null
+++ b/server/sonar-web/src/main/js/components/facet/utils.ts
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+export function isOnMac() {
+ return navigator.userAgent.indexOf('Mac OS X') !== -1;
+}
diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
index 92c53af4b18..b3d6d238765 100644
--- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties
+++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties
@@ -893,6 +893,8 @@ help.section.tutorials=Tutorials
shortcuts.section.global=Global
shortcuts.section.global.search=quickly open search bar
shortcuts.section.global.shortcuts=open this window
+shortcuts.section.global.facets.multiselection=Ctrl + click to add to selection
+shortcuts.section.global.facets.multiselection.mac=Cmd + click to add to selection
shortcuts.section.issues=Issues Page
shortcuts.section.issues.navigate_between_issues=navigate between issues