]> source.dussan.org Git - sonarqube.git/commitdiff
Fix SONAR-10639 (#201)
authorPascal Mugnier <pascal.mugnier@sonarsource.com>
Mon, 28 May 2018 07:57:06 +0000 (09:57 +0200)
committerSonarTech <sonartech@sonarsource.com>
Mon, 28 May 2018 18:20:43 +0000 (20:20 +0200)
23 files changed:
server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/Sidebar.js
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx
server/sonar-web/src/main/js/components/facet/FacetItem.tsx
server/sonar-web/src/main/js/components/facet/__tests__/FacetItem-test.tsx
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/FacetItem-test.tsx.snap
server/sonar-web/src/main/js/components/facet/utils.ts [new file with mode: 0644]
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 7e8442789700defc981812ed7672eb3f3761b098..f2d93003e5e606a4a8bef2c2d83be8f5bf64ce2b 100644 (file)
@@ -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}
       />
     );
index 184b47b8f5b216930898f7d68a759c990e4bfb39..4b8edc255c383dd3eafc3f25c7c25812f630bffd 100644 (file)
@@ -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}
index 77cae8a2d9c74bcc63c202dd8b8024fe3810bb76..b4896bafea6fb9bffd7fd09be0c8b7dd3b924fb8 100644 (file)
@@ -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}
           />
         ))}
index a09a7e62a4299f9233aa22f0550d66be6dde2f60..01e9bc56a9ef5d78cdfda935618d249bb16005d9 100644 (file)
@@ -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}
           />
         ))}
index 32f8fd2bdf566ce9072403240824dffb25ce5aa9..e26b93431949e0edcdfe3b79054a2c6d8c248749 100644 (file)
@@ -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}
           />
         ))}
index db57e1698e47c638385277df68a5367d3da2cc3e..a7556e900e32e4a13d23275239042ceba94ea4d6 100644 (file)
@@ -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}
           />
         ))}
index 3c9d5f0bfab0f2024aa4b555d4ac631daa5484a0..0ab488213280b64258950ea72f26428c5c8f5962 100644 (file)
@@ -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}
           />
         ))}
index 9626862ec695f2b615decff46aa2700df0c0f424..5fccc6f21c5892e40ff1bd72e78383bde0934b0d 100644 (file)
@@ -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}
           />
         ))}
index a326a2f65e24d597726eb8b713a759b7033fd4d6..69d9fa87f6897d5f8d24bc865fd0eb3d73f2b550 100644 (file)
@@ -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}
           />
         ))}
index 911555a65731cf179b3a25b33acb1c1550ace98b..5533b3734c059ec5a96499e05baed712c12e7acd 100644 (file)
@@ -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}
       />
     );
index 920e20028367d7c0f16daeb1c2461d800f9b417a..f1d8b43fbe1e2c362556f8a235158cbb63925348 100644 (file)
@@ -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}
           />
         ))}
index 9b27208e74c16a1190113407dbaaf73decd116bb..e68f6d8bc910ffb718cf110521fff57daf69fc6e 100644 (file)
@@ -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}
       />
     );
index ad563640a4fc3225086c14b5e1af69e54bb2be92..f34c2695d084431b39f7c3fa594a65b46a015727 100644 (file)
@@ -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}
       />
     );
index 7a2fbe66e1201bc7fd9238fdd364f5ffac2f660f..dab0d43d2982876da338935173eecad74acd2afc 100644 (file)
@@ -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}
           />
         ))}
index 146a6dbe6343e7f49a6f6b0cc1f3a41bdc4f9bc6..889115e7111a9dff701de161984824c9bc5b2bbe 100644 (file)
@@ -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}
       />
     );
index 13d70175782711ce39a42b99b316323fbf49b22e..7378d918cc1047463c4ca37a922571311dee8cce 100644 (file)
@@ -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', () => {
index 1d183049d3d8b2e1ffe4b10624166b022d6cebde..e28401150f44a351d3b6c061e35ee995d7ea7efb 100644 (file)
@@ -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]}
index 2c316a8cce925717506db22430be575b723d58de..50a9fa718e09a25d927e8bb8a0ebcde5c62419b3 100644 (file)
@@ -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 });
index 17f9a3c4b166fe3b718b084fe6762c2b51336832..ccd195276467716476b418b785f27109023c101b 100644 (file)
@@ -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>
     );
   }
 }
index 95c822c8eb39c4367a399a92df4324ada21a8f73..e6633a9ff6de3dbcabbca74dc28e5d200802fd1b 100644 (file)
@@ -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();
 });
 
index 3044d7ddbbdbf99c06802df572017f72e6f60a95..959d23931079f84ec31b5bde77951c7aa2810d83 100644 (file)
 // 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 (file)
index 0000000..8c55de0
--- /dev/null
@@ -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;
+}
index 92c53af4b182b74e931083584408e6357ee7030a..b3d6d238765df321e25f2ada7502fd50d250099a 100644 (file)
@@ -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