]> source.dussan.org Git - sonarqube.git/commitdiff
apply feedback for issues page (#1980)
authorStas Vilchik <stas-vilchik@users.noreply.github.com>
Wed, 26 Apr 2017 19:09:55 +0000 (21:09 +0200)
committerGitHub <noreply@github.com>
Wed, 26 Apr 2017 19:09:55 +0000 (21:09 +0200)
79 files changed:
server/sonar-web/package.json
server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js
server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs
server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js
server/sonar-web/src/main/js/apps/issues/actions.js
server/sonar-web/src/main/js/apps/issues/components/App.js
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js
server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js
server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js
server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js
server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js
server/sonar-web/src/main/js/apps/issues/components/PageActions.js
server/sonar-web/src/main/js/apps/issues/components/ReloadButton.js [new file with mode: 0644]
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js
server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js [deleted file]
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap
server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js
server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap
server/sonar-web/src/main/js/apps/issues/styles.css
server/sonar-web/src/main/js/apps/issues/utils.js
server/sonar-web/src/main/js/apps/projects/components/AllProjects.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js
server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js
server/sonar-web/src/main/js/components/SourceViewer/components/Line.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js
server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap
server/sonar-web/src/main/js/components/common/EmptySearch.css [new file with mode: 0644]
server/sonar-web/src/main/js/components/common/EmptySearch.js
server/sonar-web/src/main/js/components/common/SelectList.js
server/sonar-web/src/main/js/components/controls/Checkbox.js
server/sonar-web/src/main/js/components/issue/Issue.js
server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js
server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap
server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js
server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js
server/sonar-web/src/main/js/components/layout/Page.js [deleted file]
server/sonar-web/src/main/js/components/layout/PageFilters.js [deleted file]
server/sonar-web/src/main/js/components/layout/PageMain.js [deleted file]
server/sonar-web/src/main/js/components/layout/PageMainInner.js [deleted file]
server/sonar-web/src/main/js/components/layout/PageSide.js [deleted file]
server/sonar-web/src/main/js/helpers/scrolling.js
server/sonar-web/src/main/js/helpers/urls.js
server/sonar-web/src/main/less/components/badges.less
server/sonar-web/src/main/less/components/issues.less
server/sonar-web/src/main/less/components/modals.less
server/sonar-web/src/main/less/components/page.less
server/sonar-web/src/main/less/components/search-navigator.less
server/sonar-web/src/main/less/pages/issues.less
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5c02baf44440fe88df53e55781841fe060a7477c..592a9f901d03c1bcb4f409b06bfd8a7ae8caa975 100644 (file)
@@ -17,7 +17,6 @@
     "d3-selection": "1.0.5",
     "d3-shape": "1.0.6",
     "escape-html": "1.0.3",
-    "glamor": "2.20.24",
     "handlebars": "2.0.0",
     "history": "2.0.0",
     "jquery": "2.2.0",
index 644c05296ff055ac9cf15093c9eeb484bf560498..d3200c089182a363b0f81cf7b28871e2033bea99 100644 (file)
@@ -62,7 +62,7 @@ export default class GlobalNavMenu extends React.PureComponent {
 
   renderIssuesLink() {
     const query = this.props.currentUser.isLoggedIn
-      ? { myIssues: 'true', resolved: 'false' }
+      ? { createdInLast: '1w', myIssues: 'true', resolved: 'false' }
       : { resolved: 'false' };
     const active = this.props.location.pathname === 'issues';
     return (
index 92d2c37a85c8db8b36c9deaf0046d9f0692ec212..00f5519d887ccb6e614f8ad2f0230ade809ffd37 100644 (file)
@@ -26,8 +26,7 @@
 
       <h3 class="shortcuts-section-title">{{t 'shortcuts.section.rules'}}</h3>
       <ul class="shortcuts-list">
-        <li><span class="shortcut-button">&uarr;</span> <span
-            class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.navigate_between_rules'}}</li>
+        <li><span class="shortcut-button">&uarr;</span> <span class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.navigate_between_rules'}}</li>
         <li><span class="shortcut-button">&rarr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.open_details'}}</li>
         <li><span class="shortcut-button">&larr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.return_to_list'}}</li>
         <li><span class="shortcut-button">a</span> &nbsp;&nbsp; {{t 'shortcuts.section.rules.activate'}}</li>
     <div class="column-half">
       <h3 class="shortcuts-section-title">{{t 'shortcuts.section.issues'}}</h3>
       <ul class="shortcuts-list">
-        <li><span class="shortcut-button">&uarr;</span> <span
-            class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.navigate_between_issues'}}
+        <li><span class="shortcut-button">&uarr;</span> <span class="shortcut-button">&darr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.navigate_between_issues'}}
         </li>
         <li><span class="shortcut-button">&rarr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.open_details'}}</li>
         <li><span class="shortcut-button">&larr;</span> &nbsp;&nbsp; {{t 'shortcuts.section.issues.return_to_list'}}</li>
-        <li><span class="shortcut-button">⎵</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.select'}}</li>
+        <li>
+          <span class="shortcut-button">alt</span>
+          <span class=>+</span>
+          <span class="shortcut-button">↑</span>
+          <span class="shortcut-button">↓</span> {{t 'issues.to_navigate_issue_locations'}}
+        </li>
+        <li>
+          <span class="shortcut-button">alt</span>
+          <span class=>+</span>
+          <span class="shortcut-button">←</span>
+          <span class="shortcut-button">→</span> {{t 'issues.to_switch_flows'}}
+        </li>
         <li><span class="shortcut-button">f</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.do_transition'}}</li>
         <li><span class="shortcut-button">a</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.assign'}}</li>
         <li><span class="shortcut-button">m</span> &nbsp;&nbsp; {{t 'shortcuts.section.issue.assign_to_me'}}</li>
@@ -58,4 +67,4 @@
 
 <div class="modal-foot">
   <a class="js-modal-close" href="#">{{t 'close'}}</a>
-</div>
+</div>
\ No newline at end of file
index 384270c29193f59036a6f2ad4906baf5c230ceff..b6ec74ca5a069d50fa87ff6397f92e74f2180b48 100644 (file)
@@ -20,7 +20,7 @@
 import $ from 'jquery';
 import Marionette from 'backbone.marionette';
 import Template from '../templates/rule/coding-rules-rule-issues.hbs';
-import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { getComponentIssuesUrlAsString } from '../../../helpers/urls';
 
 export default Marionette.ItemView.extend({
   template: Template,
@@ -55,7 +55,7 @@ export default Marionette.ItemView.extend({
           ...project,
           name: projectBase != null ? projectBase.longName : '',
           issuesUrl: projectBase != null &&
-            getComponentIssuesUrl(projectBase.key, {
+            getComponentIssuesUrlAsString(projectBase.key, {
               resolved: 'false',
               rules: this.model.id
             })
index 610628b15ff6fa9de157aafcbacd035ae91d047e..2bde7c95332d6b70106d391e039e1f635b7619f0 100644 (file)
 // @flow
 import type { State } from './components/App';
 
-export const enableLocationsNavigator = (state: State) => ({
-  locationsNavigator: true,
-  selectedFlowIndex: state.selectedFlowIndex ||
-    (state.openIssue && state.openIssue.flows.length > 0 ? 0 : null),
-  selectedLocationIndex: state.selectedLocationIndex || 0
-});
+export const enableLocationsNavigator = (state: State) => {
+  const { openIssue } = state;
+  if (openIssue && (openIssue.secondaryLocations.length > 0 || openIssue.flows.length > 0)) {
+    return {
+      locationsNavigator: true,
+      selectedFlowIndex: state.selectedFlowIndex || (openIssue.flows.length > 0 ? 0 : null),
+      selectedLocationIndex: state.selectedLocationIndex || 0
+    };
+  }
+};
 
 export const disableLocationsNavigator = () => ({
   locationsNavigator: false
@@ -70,3 +74,17 @@ export const selectPreviousLocation = (state: State) => {
 export const selectFlow = (nextIndex: ?number) => () => {
   return { selectedFlowIndex: nextIndex, selectedLocationIndex: 0 };
 };
+
+export const selectNextFlow = (state: State) => {
+  const { openIssue, selectedFlowIndex } = state;
+  if (openIssue && selectedFlowIndex != null && openIssue.flows.length > selectedFlowIndex + 1) {
+    return { selectedFlowIndex: selectedFlowIndex + 1, selectedLocationIndex: 0 };
+  }
+};
+
+export const selectPreviousFlow = (state: State) => {
+  const { openIssue, selectedFlowIndex } = state;
+  if (openIssue && selectedFlowIndex != null && selectedFlowIndex > 0) {
+    return { selectedFlowIndex: selectedFlowIndex - 1, selectedLocationIndex: 0 };
+  }
+};
index 1b7f6a9f9522d2778598971de69879b8286c6721..a6745fa2adda670b2f68a2d5f942808bb66939b3 100644 (file)
@@ -39,7 +39,8 @@ import {
   areQueriesEqual,
   getOpen,
   serializeQuery,
-  parseFacets
+  parseFacets,
+  mapFacet
 } from '../utils';
 import type {
   Query,
@@ -53,11 +54,6 @@ import type {
 } from '../utils';
 import ListFooter from '../../../components/controls/ListFooter';
 import EmptySearch from '../../../components/common/EmptySearch';
-import Page from '../../../components/layout/Page';
-import PageMain from '../../../components/layout/PageMain';
-import PageMainInner from '../../../components/layout/PageMainInner';
-import PageSide from '../../../components/layout/PageSide';
-import PageFilters from '../../../components/layout/PageFilters';
 import { translate, translateWithParameters } from '../../../helpers/l10n';
 import { scrollToElement } from '../../../helpers/scrolling';
 import type { Issue } from '../../../components/issue/types';
@@ -227,6 +223,14 @@ export default class App extends React.PureComponent {
       // alt + down
       event.preventDefault();
       this.selectPreviousLocation();
+    } else if (event.keyCode === 37 && event.altKey) {
+      // alt + left
+      event.preventDefault();
+      this.selectPreviousFlow();
+    } else if (event.keyCode === 39 && event.altKey) {
+      // alt + right
+      event.preventDefault();
+      this.selectNextFlow();
     }
   };
 
@@ -311,6 +315,7 @@ export default class App extends React.PureComponent {
           open: undefined
         }
       });
+      this.scrollToSelectedIssue(false);
     }
   };
 
@@ -321,43 +326,30 @@ export default class App extends React.PureComponent {
     }
   };
 
-  scrollToSelectedIssue = () => {
+  scrollToSelectedIssue = (smooth: boolean = true) => {
     const { selected } = this.state;
     if (selected) {
       const element = document.querySelector(`[data-issue="${selected}"]`);
       if (element) {
-        scrollToElement(element, 150, 100);
+        scrollToElement(element, { topOffset: 150, bottomOffset: 100, smooth });
       }
     }
   };
 
   fetchIssues = (additional?: {}, requestFacets?: boolean = false): Promise<*> => {
     const { component } = this.props;
-    const { myIssues, query } = this.state;
+    const { myIssues, openFacets, query } = this.state;
+
+    const facets = requestFacets
+      ? Object.keys(openFacets).filter(facet => openFacets[facet]).map(mapFacet).join(',')
+      : undefined;
 
     const parameters = {
       componentKeys: component && component.key,
       ...serializeQuery(query),
       s: 'FILE_LINE',
-      ps: 25,
-      facets: requestFacets
-        ? [
-            'assignees',
-            'authors',
-            'createdAt',
-            'directories',
-            'fileUuids',
-            'languages',
-            'moduleUuids',
-            'projectUuids',
-            'resolutions',
-            'rules',
-            'severities',
-            'statuses',
-            'tags',
-            'types'
-          ].join()
-        : undefined,
+      ps: 100,
+      facets,
       ...additional
     };
 
@@ -464,6 +456,32 @@ export default class App extends React.PureComponent {
     });
   };
 
+  fetchFacet = (facet: string) => {
+    return this.fetchIssues({ ps: 1, facets: mapFacet(facet) }).then(({ facets, ...other }) => {
+      if (this.mounted) {
+        this.setState(state => ({
+          facets: { ...state.facets, ...parseFacets(facets) },
+          referencedComponents: {
+            ...state.referencedComponents,
+            ...keyBy(other.components, 'uuid')
+          },
+          referencedLanguages: {
+            ...state.referencedLanguages,
+            ...keyBy(other.languages, 'key')
+          },
+          referencedRules: {
+            ...state.referencedRules,
+            ...keyBy(other.rules, 'key')
+          },
+          referencedUsers: {
+            ...state.referencedUsers,
+            ...keyBy(other.users, 'login')
+          }
+        }));
+      }
+    });
+  };
+
   isFiltered = () => {
     const serialized = serializeQuery(this.state.query);
     return !areQueriesEqual(serialized, DEFAULT_QUERY);
@@ -510,6 +528,9 @@ export default class App extends React.PureComponent {
     this.setState(state => ({
       openFacets: { ...state.openFacets, [property]: !state.openFacets[property] }
     }));
+    if (!this.state.facets[property]) {
+      this.fetchFacet(property);
+    }
   };
 
   handleReset = () => {
@@ -564,6 +585,10 @@ export default class App extends React.PureComponent {
     this.closeBulkChange();
   };
 
+  handleReload = () => {
+    this.fetchFirstIssues();
+  };
+
   handleReloadAndOpenFirst = () => {
     this.fetchFirstIssues().then(issues => {
       if (issues.length > 0) {
@@ -576,6 +601,8 @@ export default class App extends React.PureComponent {
   selectNextLocation = () => this.setState(actions.selectNextLocation);
   selectPreviousLocation = () => this.setState(actions.selectPreviousLocation);
   selectFlow = (index: ?number) => this.setState(actions.selectFlow(index));
+  selectNextFlow = () => this.setState(actions.selectNextFlow);
+  selectPreviousFlow = () => this.setState(actions.selectPreviousFlow);
 
   renderBulkChange(openIssue: ?Issue) {
     const { component, currentUser } = this.props;
@@ -627,7 +654,7 @@ export default class App extends React.PureComponent {
     const { query } = this.state;
 
     return (
-      <PageFilters>
+      <div className="layout-page-filters">
         {currentUser.isLoggedIn &&
           <MyIssuesFilter
             myIssues={this.state.myIssues}
@@ -647,7 +674,7 @@ export default class App extends React.PureComponent {
           referencedRules={this.state.referencedRules}
           referencedUsers={this.state.referencedUsers}
         />
-      </PageFilters>
+      </div>
     );
   }
 
@@ -655,7 +682,7 @@ export default class App extends React.PureComponent {
     const { issues, paging } = this.state;
 
     return (
-      <PageFilters>
+      <div className="layout-page-filters">
         <ConciseIssuesListHeader
           loading={this.state.loading}
           onBackClick={this.closeIssue}
@@ -675,7 +702,7 @@ export default class App extends React.PureComponent {
         {paging != null &&
           paging.total > 0 &&
           <ListFooter total={paging.total} count={issues.length} loadMore={this.fetchMoreIssues} />}
-      </PageFilters>
+      </div>
     );
   }
 
@@ -683,24 +710,28 @@ export default class App extends React.PureComponent {
     const top = this.props.component ? 95 : 30;
 
     return (
-      <PageSide top={top}>
-        {openIssue == null ? this.renderFacets() : this.renderConciseIssuesList()}
-      </PageSide>
+      <div className="layout-page-side-outer">
+        <div className="layout-page-side" style={{ top }}>
+          <div className="layout-page-side-inner">
+            {openIssue == null ? this.renderFacets() : this.renderConciseIssuesList()}
+          </div>
+        </div>
+      </div>
     );
   }
 
-  renderList(openIssue: ?Issue) {
+  renderList() {
     const { component, currentUser } = this.props;
-    const { issues, paging } = this.state;
+    const { issues, openIssue, paging } = this.state;
     const selectedIndex = this.getSelectedIndex();
     const selectedIssue = selectedIndex != null ? issues[selectedIndex] : null;
 
-    if (paging == null) {
+    if (paging == null || openIssue != null) {
       return null;
     }
 
     return (
-      <div className={openIssue != null ? 'hidden' : undefined}>
+      <div>
         {paging.total > 0 &&
           <IssuesList
             checked={this.state.checked}
@@ -722,12 +753,22 @@ export default class App extends React.PureComponent {
   }
 
   renderShortcutsForLocations() {
+    const { openIssue } = this.state;
+    if (openIssue == null || (!openIssue.secondaryLocations.length && !openIssue.flows.length)) {
+      return null;
+    }
+    const hasSeveralFlows = openIssue.flows.length > 1;
     return (
       <div className="pull-right note">
         <span className="shortcut-button little-spacer-right">alt</span>
         <span className="little-spacer-right">{'+'}</span>
         <span className="shortcut-button little-spacer-right">↑</span>
         <span className="shortcut-button little-spacer-right">↓</span>
+        {hasSeveralFlows &&
+          <span>
+            <span className="shortcut-button little-spacer-right">←</span>
+            <span className="shortcut-button little-spacer-right">→</span>
+          </span>}
         {translate('issues.to_navigate_issue_locations')}
       </div>
     );
@@ -740,50 +781,50 @@ export default class App extends React.PureComponent {
     const selectedIndex = this.getSelectedIndex();
 
     return (
-      <Page className="issues" id="issues-page">
+      <div className="layout-page issues" id="issues-page">
         <Helmet title={translate('issues.page')} titleTemplate="%s - SonarQube" />
 
         {this.renderSide(openIssue)}
 
-        <PageMain>
+        <div className="layout-page-main">
           <div className="issues-header-panel issues-main-header">
             <div className="issues-header-panel-inner issues-main-header-inner">
-              <PageMainInner>
+              <div className="layout-page-main-inner">
                 {this.renderBulkChange(openIssue)}
                 {openIssue != null
-                  ? <div className="pull-left">
+                  ? <div className="pull-left width-60">
                       <ComponentBreadcrumbs component={component} issue={openIssue} />
                     </div>
                   : <PageActions
                       loading={this.state.loading}
+                      onReload={this.handleReload}
                       paging={paging}
                       selectedIndex={selectedIndex}
                     />}
-                {openIssue != null && this.renderShortcutsForLocations()}
-              </PageMainInner>
+                {this.renderShortcutsForLocations()}
+              </div>
             </div>
           </div>
 
-          <PageMainInner>
+          <div className="layout-page-main-inner">
             <div>
-              {openIssue != null &&
-                <IssuesSourceViewer
-                  openIssue={openIssue}
-                  loadIssues={this.fetchIssuesForComponent}
-                  onIssueChange={this.handleIssueChange}
-                  onIssueSelect={this.openIssue}
-                  onLocationSelect={this.selectLocation}
-                  selectedFlowIndex={this.state.selectedFlowIndex}
-                  selectedLocationIndex={
-                    this.state.locationsNavigator ? this.state.selectedLocationIndex : null
-                  }
-                />}
-
-              {this.renderList(openIssue)}
+              {openIssue
+                ? <IssuesSourceViewer
+                    openIssue={openIssue}
+                    loadIssues={this.fetchIssuesForComponent}
+                    onIssueChange={this.handleIssueChange}
+                    onIssueSelect={this.openIssue}
+                    onLocationSelect={this.selectLocation}
+                    selectedFlowIndex={this.state.selectedFlowIndex}
+                    selectedLocationIndex={
+                      this.state.locationsNavigator ? this.state.selectedLocationIndex : null
+                    }
+                  />
+                : this.renderList()}
             </div>
-          </PageMainInner>
-        </PageMain>
-      </Page>
+          </div>
+        </div>
+      </div>
     );
   }
 }
index 63b4680c2fe27bc719c62fdea22bdcae19c1c737..057eb7e7cd44ecee1aa13b5aef34aa8e98085f54 100644 (file)
@@ -21,7 +21,6 @@
 import React from 'react';
 import Modal from 'react-modal';
 import Select from 'react-select';
-import { css } from 'glamor';
 import { pickBy, sortBy } from 'lodash';
 import SearchSelect from './SearchSelect';
 import Checkbox from '../../../components/controls/Checkbox';
@@ -228,11 +227,7 @@ export default class BulkChangeModal extends React.PureComponent {
   );
 
   renderCheckbox = (field: string) => (
-    <Checkbox
-      className={css({ paddingTop: 6, paddingRight: 8 })}
-      checked={this.state[field] != null}
-      onCheck={this.handleFieldCheck(field)}
-    />
+    <Checkbox checked={this.state[field] != null} onCheck={this.handleFieldCheck(field)} />
   );
 
   renderAffected = (affected: number) => (
index e11340fc943c4501a6a49d22b4801d6824387152..0fd09abce63c9de27f1e11178aea4706253c89a9 100644 (file)
@@ -42,7 +42,7 @@ export default class ComponentBreadcrumbs extends React.PureComponent {
     const displaySubProject = component == null || !['BRC', 'DIR'].includes(component.qualifier);
 
     return (
-      <div className="component-name">
+      <div className="component-name text-ellipsis">
         {displayOrganization &&
           <Organization linkClassName="link-no-underline" organizationKey={issue.organization} />}
 
index 9740f63e7183d9c0691307da81fde779b0bf1456..8586890617917dc3c58f37e03ffce7b3d53720c8 100644 (file)
@@ -19,7 +19,6 @@
  */
 // @flow
 import React from 'react';
-import { css } from 'glamor';
 import { translate } from '../../../helpers/l10n';
 
 type Props = {
@@ -27,8 +26,6 @@ type Props = {
   onReset: () => void
 };
 
-const styles = css({ marginBottom: 12, paddingBottom: 11, borderBottom: '1px solid #e6e6e6' });
-
 export default class FiltersHeader extends React.PureComponent {
   props: Props;
 
@@ -40,9 +37,9 @@ export default class FiltersHeader extends React.PureComponent {
 
   render() {
     return (
-      <div className={styles}>
+      <div className="issues-filters-header">
         {this.props.displayReset &&
-          <div className={css({ float: 'right' })}>
+          <div className="pull-right">
             <button className="button-red" onClick={this.handleResetClick}>
               {translate('clear_all_filters')}
             </button>
index 9c321fb007c4f2e44c47ff5cdbf9996b0fd8a66e..947be331377702deb59c97da0f1c04b3f2dbe212 100644 (file)
@@ -46,16 +46,20 @@ export default class IssuesSourceViewer extends React.PureComponent {
     }
   }
 
-  scrollToIssue = () => {
+  scrollToIssue = (smooth: boolean = true) => {
     const element = this.node.querySelector(`[data-issue="${this.props.openIssue.key}"]`);
     if (element) {
-      this.handleScroll(element);
+      this.handleScroll(element, smooth);
     }
   };
 
-  handleScroll = (element: HTMLElement) => {
+  handleScroll = (element: HTMLElement, smooth: boolean = true) => {
     const offset = window.innerHeight / 2;
-    scrollToElement(element, offset - 100, offset);
+    scrollToElement(element, { topOffset: offset - 100, bottomOffset: offset, smooth });
+  };
+
+  handleLoaded = () => {
+    this.scrollToIssue(false);
   };
 
   render() {
@@ -80,7 +84,7 @@ export default class IssuesSourceViewer extends React.PureComponent {
           highlightedLocations={locations}
           highlightedLocationMessage={locationMessage}
           loadIssues={this.props.loadIssues}
-          onLoaded={this.scrollToIssue}
+          onLoaded={this.handleLoaded}
           onLocationSelect={this.props.onLocationSelect}
           onIssueChange={this.props.onIssueChange}
           onIssueSelect={this.props.onIssueSelect}
index 5c674e8635527b8707f686b1d46c4dd054fc6ee0..7812c0dee9aeee99f13d9068c20d814edb9caf20 100644 (file)
@@ -19,7 +19,6 @@
  */
 // @flow
 import React from 'react';
-import { css } from 'glamor';
 import { translate } from '../../../helpers/l10n';
 
 type Props = {|
@@ -40,7 +39,7 @@ export default class MyIssuesFilter extends React.PureComponent {
     const { myIssues } = this.props;
 
     return (
-      <div className={css({ marginBottom: 24, textAlign: 'center' })}>
+      <div className="issues-my-issues-filter">
         <div className="button-group">
           <button
             className={myIssues ? 'button-active' : undefined}
index dcefa0a7d1bb0638ae6de05e0fe1b9955908fef9..d8591e0e6a31bad91f3ac4ae3ca77022c2b90145 100644 (file)
  */
 // @flow
 import React from 'react';
-import { css } from 'glamor';
 import IssuesCounter from './IssuesCounter';
+import ReloadButton from './ReloadButton';
 import type { Paging } from '../utils';
 import { translate } from '../../../helpers/l10n';
 
 type Props = {|
   loading: boolean,
+  onReload: () => void,
   paging: ?Paging,
   selectedIndex: ?number
 |};
@@ -55,11 +56,13 @@ export default class PageActions extends React.PureComponent {
     const { paging, selectedIndex } = this.props;
 
     return (
-      <div className={css({ float: 'right' })}>
+      <div className="pull-right">
         {this.renderShortcuts()}
 
-        <div className={css({ display: 'inline-block', minWidth: 80, textAlign: 'right' })}>
-          {this.props.loading && <i className="spinner spacer-right" />}
+        <div className="issues-page-actions">
+          {this.props.loading
+            ? <i className="issues-main-header-spinner spinner" />
+            : <ReloadButton className="spacer-right" onClick={this.props.onReload} />}
           {paging != null && <IssuesCounter current={selectedIndex} total={paging.total} />}
         </div>
       </div>
diff --git a/server/sonar-web/src/main/js/apps/issues/components/ReloadButton.js b/server/sonar-web/src/main/js/apps/issues/components/ReloadButton.js
new file mode 100644 (file)
index 0000000..09b5a0d
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+// @flow
+import React from 'react';
+import classNames from 'classnames';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
+
+type Props = {|
+  className?: string,
+  onClick: () => void
+|};
+
+/* eslint-disable max-len */
+const icon = (
+  <svg width="18" height="24" viewBox="0 0 18 24">
+    <path d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z" />
+  </svg>
+);
+/* eslint-enable max-len */
+
+export default function ReloadButton(props: Props) {
+  const handleClick = (event: Event) => {
+    event.preventDefault();
+    props.onClick();
+  };
+
+  return (
+    <Tooltip overlay={translate('reload')}>
+      <a
+        className={classNames('concise-issues-list-header-button', props.className)}
+        href="#"
+        onClick={handleClick}>
+        {icon}
+      </a>
+    </Tooltip>
+  );
+}
index f621e0a83eaf00ca25680516c737ae5989ff4ff6..a4424b479421da9e4f5a6966923e6db746acad41 100644 (file)
@@ -20,6 +20,8 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
 
 type Props = {|
   className?: string,
@@ -41,11 +43,13 @@ export default function BackButton(props: Props) {
   };
 
   return (
-    <a
-      className={classNames('concise-issues-list-header-button', props.className)}
-      href="#"
-      onClick={handleClick}>
-      {icon}
-    </a>
+    <Tooltip overlay={translate('issues.return_to_list')}>
+      <a
+        className={classNames('concise-issues-list-header-button', props.className)}
+        href="#"
+        onClick={handleClick}>
+        {icon}
+      </a>
+    </Tooltip>
   );
 }
index bff17414951f3f12aed90ac4bae9c3940a14e550..922379d0a7fa7c0760671187ba20efe6b584f0b6 100644 (file)
@@ -31,30 +31,47 @@ type Props = {|
   onClick: string => void,
   onFlowSelect: number => void,
   onLocationSelect: number => void,
-  scroll: HTMLElement => void,
+  scroll: (element: HTMLElement, bottomOffset: ?number) => void,
   selected: boolean,
   selectedFlowIndex: ?number,
   selectedLocationIndex: ?number
 |};
 
 export default class ConciseIssueBox extends React.PureComponent {
-  node: HTMLElement;
+  messageElement: HTMLElement;
+  rootElement: HTMLElement;
   props: Props;
 
   componentDidMount() {
-    // scroll to the message element and not to the root element,
-    // because the root element can be huge and exceed the window height
     if (this.props.selected) {
-      this.props.scroll(this.node);
+      this.handleScroll();
     }
   }
 
   componentDidUpdate(prevProps: Props) {
     if (this.props.selected && prevProps.selected !== this.props.selected) {
-      this.props.scroll(this.node);
+      this.handleScroll();
     }
   }
 
+  handleScroll = () => {
+    const { selectedFlowIndex } = this.props;
+    const { flows, secondaryLocations } = this.props.issue;
+
+    const locations = selectedFlowIndex != null
+      ? flows[selectedFlowIndex]
+      : flows.length > 0 ? flows[0] : secondaryLocations;
+
+    if (locations == null || locations.length < 15) {
+      // if there are no locations, or there are just few
+      // then ensuse that the whole box is visible
+      this.props.scroll(this.rootElement);
+    } else {
+      // otherwise scroll until the the message element is located on top
+      this.props.scroll(this.messageElement, window.innerHeight - 200);
+    }
+  };
+
   handleClick = (event: Event) => {
     event.preventDefault();
     this.props.onClick(this.props.issue.key);
@@ -70,8 +87,9 @@ export default class ConciseIssueBox extends React.PureComponent {
     return (
       <div
         className={classNames('concise-issue-box', 'clearfix', { selected })}
+        ref={node => (this.rootElement = node)}
         {...clickAttributes}>
-        <div className="concise-issue-box-message" ref={node => (this.node = node)}>
+        <div className="concise-issue-box-message" ref={node => (this.messageElement = node)}>
           {issue.message}
         </div>
         <div className="concise-issue-box-attributes">
index 18e9018a6f5a344d178f89277b73c272bf8af966..fba7825a6b44ce9165d0ac9b6200ff262d5d316c 100644 (file)
@@ -36,10 +36,10 @@ type Props = {|
 export default class ConciseIssuesList extends React.PureComponent {
   props: Props;
 
-  handleScroll = (element: HTMLElement) => {
+  handleScroll = (element: HTMLElement, bottomOffset: number = 100) => {
     const scrollableElement = document.querySelector('.layout-page-side');
     if (element && scrollableElement) {
-      scrollToElement(element, 150, 100, scrollableElement);
+      scrollToElement(element, { topOffset: 150, bottomOffset, parent: scrollableElement });
     }
   };
 
index 2be4b44732eb3a37d94ea7963f91ea31e5f99239..e6a5f1c24b3d4264793673126f8fad11961a3c5c 100644 (file)
@@ -20,7 +20,7 @@
 // @flow
 import React from 'react';
 import BackButton from './BackButton';
-import ReloadButton from './ReloadButton';
+import ReloadButton from '../components/ReloadButton';
 import IssuesCounter from '../components/IssuesCounter';
 import type { Paging } from '../utils';
 
diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js
deleted file mode 100644 (file)
index 0034fad..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import classNames from 'classnames';
-
-type Props = {|
-  className?: string,
-  onClick: () => void
-|};
-
-/* eslint-disable max-len */
-const icon = (
-  <svg width="18" height="24" viewBox="0 0 18 24">
-    <path d="M16.6454 8.1084c-.3-.5-.9-.7-1.4-.4-.5.3-.7.9-.4 1.4.9 1.6 1.1 3.4.6 5.1-.5 1.7-1.7 3.2-3.2 4-3.3 1.8-7.4.6-9.1-2.7-1.8-3.1-.8-6.9 2.1-8.8v3.3h2v-7h-7v2h3.9c-3.7 2.5-5 7.5-2.8 11.4 1.6 3 4.6 4.6 7.7 4.6 1.4 0 2.8-.3 4.2-1.1 2-1.1 3.5-3 4.2-5.2.6-2.2.3-4.6-.8-6.6z" />
-  </svg>
-);
-/* eslint-enable max-len */
-
-export default function ReloadButton(props: Props) {
-  const handleClick = (event: Event) => {
-    event.preventDefault();
-    props.onClick();
-  };
-
-  return (
-    <a
-      className={classNames('concise-issues-list-header-button', props.className)}
-      href="#"
-      onClick={handleClick}>
-      {icon}
-    </a>
-  );
-}
index 0547943576386480ba65c5c9e39901085befb357..09609eabe9d3e2534098dcaaa1d76a782f11990f 100644 (file)
@@ -69,6 +69,10 @@ export default class AssigneeFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ assigned: true, assignees: [] });
+  };
+
   handleSearch = (query: string) => searchAssignees(query, this.props.component);
 
   handleSelect = (assignee: string) => {
@@ -117,7 +121,7 @@ export default class AssigneeFacet extends React.PureComponent {
     );
   };
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -132,36 +136,50 @@ export default class AssigneeFacet extends React.PureComponent {
       key => -stats[key]
     );
 
+    return (
+      <FacetItemsList>
+        {assignees.map(assignee => (
+          <FacetItem
+            active={this.isAssigneeActive(assignee)}
+            facetMode={this.props.facetMode}
+            key={assignee}
+            name={this.getAssigneeName(assignee)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(assignee)}
+            value={assignee}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  renderFooter() {
+    if (!this.props.stats) {
+      return null;
+    }
+
+    return (
+      <FacetFooter
+        onSearch={this.handleSearch}
+        onSelect={this.handleSelect}
+        renderOption={this.renderOption}
+      />
+    );
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={!this.props.assigned || this.props.assignees.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.assignees.length + (this.props.assigned ? 0 : 1)}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {assignees.map(assignee => (
-              <FacetItem
-                active={this.isAssigneeActive(assignee)}
-                facetMode={this.props.facetMode}
-                key={assignee}
-                name={this.getAssigneeName(assignee)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(assignee)}
-                value={assignee}
-              />
-            ))}
-          </FacetItemsList>}
-
-        {this.props.open &&
-          <FacetFooter
-            onSearch={this.handleSearch}
-            onSelect={this.handleSelect}
-            renderOption={this.renderOption}
-          />}
+        {this.props.open && this.renderList()}
+        {this.props.open && this.renderFooter()}
       </FacetBox>
     );
   }
index d539229ff788f097b39982f669e2a8239a05d667..e0f85562ef97b622faecec1daa62f1c9231b247b 100644 (file)
@@ -56,12 +56,16 @@ export default class AuthorFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getStat(author: string): ?number {
     const { stats } = this.props;
     return stats ? stats[author] : null;
   }
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -70,29 +74,35 @@ export default class AuthorFacet extends React.PureComponent {
 
     const authors = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {authors.map(author => (
+          <FacetItem
+            active={this.props.authors.includes(author)}
+            facetMode={this.props.facetMode}
+            key={author}
+            name={author}
+            onClick={this.handleItemClick}
+            stat={this.getStat(author)}
+            value={author}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.authors.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.authors.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {authors.map(author => (
-              <FacetItem
-                active={this.props.authors.includes(author)}
-                facetMode={this.props.facetMode}
-                key={author}
-                name={author}
-                onClick={this.handleItemClick}
-                stat={this.getStat(author)}
-                value={author}
-              />
-            ))}
-          </FacetItemsList>}
+        {this.props.open && this.renderList()}
       </FacetBox>
     );
   }
index d67b204f3cd9cee0baa10dfaea8a310e0d2f46c8..56e1328bc4f7f499de27733c0f4a8265bdee8d25 100644 (file)
@@ -59,6 +59,10 @@ export default class CreationDateFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.resetTo({});
+  };
+
   resetTo = (changes: {}) => {
     this.props.onChange({
       createdAfter: undefined,
@@ -252,19 +256,14 @@ export default class CreationDateFacet extends React.PureComponent {
       this.props.createdInLast.length > 0 ||
       this.props.sinceLeakPeriod;
 
-    const { stats } = this.props;
-
-    if (!stats) {
-      return null;
-    }
-
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={hasValue}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={hasValue ? 1 : 0}
         />
 
         {this.props.open && this.renderInner()}
index ddd82db1eab6e23952da0615ac64181e2db73422..c41bad1931230223a8ea41ef51fffe048434127c 100644 (file)
@@ -61,6 +61,10 @@ export default class DirectoryFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getStat(directory: string): ?number {
     const { stats } = this.props;
     return stats ? stats[directory] : null;
@@ -82,7 +86,7 @@ export default class DirectoryFacet extends React.PureComponent {
     );
   }
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -91,29 +95,35 @@ export default class DirectoryFacet extends React.PureComponent {
 
     const directories = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {directories.map(directory => (
+          <FacetItem
+            active={this.props.directories.includes(directory)}
+            facetMode={this.props.facetMode}
+            key={directory}
+            name={this.renderName(directory)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(directory)}
+            value={directory}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.directories.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.directories.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {directories.map(directory => (
-              <FacetItem
-                active={this.props.directories.includes(directory)}
-                facetMode={this.props.facetMode}
-                key={directory}
-                name={this.renderName(directory)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(directory)}
-                value={directory}
-              />
-            ))}
-          </FacetItemsList>}
+        {this.props.open && this.renderList()}
       </FacetBox>
     );
   }
index 5d914f8380dac2e65079b902a59538380fa786d3..48b33e835868ac84acc183c9c01715bb85bab12a 100644 (file)
@@ -60,6 +60,10 @@ export default class FileFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getStat(file: string): ?number {
     const { stats } = this.props;
     return stats ? stats[file] : null;
@@ -78,7 +82,7 @@ export default class FileFacet extends React.PureComponent {
     );
   }
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -87,29 +91,35 @@ export default class FileFacet extends React.PureComponent {
 
     const files = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {files.map(file => (
+          <FacetItem
+            active={this.props.files.includes(file)}
+            facetMode={this.props.facetMode}
+            key={file}
+            name={this.renderName(file)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(file)}
+            value={file}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.files.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.files.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {files.map(file => (
-              <FacetItem
-                active={this.props.files.includes(file)}
-                facetMode={this.props.facetMode}
-                key={file}
-                name={this.renderName(file)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(file)}
-                value={file}
-              />
-            ))}
-          </FacetItemsList>}
+        {this.props.open && this.renderList()}
       </FacetBox>
     );
   }
index 411c9b74d2e66a349a95c94191924d45c8641f7a..c2c8592c5cab3555a7b0b258f7cccd8f924d693d 100644 (file)
@@ -59,6 +59,10 @@ export default class LanguageFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getLanguageName(language: string): string {
     const { referencedLanguages } = this.props;
     return referencedLanguages[language] ? referencedLanguages[language].name : language;
@@ -74,7 +78,7 @@ export default class LanguageFacet extends React.PureComponent {
     this.props.onChange({ [this.property]: uniq([...languages, language]) });
   };
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -83,31 +87,44 @@ export default class LanguageFacet extends React.PureComponent {
 
     const languages = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {languages.map(language => (
+          <FacetItem
+            active={this.props.languages.includes(language)}
+            facetMode={this.props.facetMode}
+            key={language}
+            name={this.getLanguageName(language)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(language)}
+            value={language}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  renderFooter() {
+    if (!this.props.stats) {
+      return null;
+    }
+
+    return <LanguageFacetFooter onSelect={this.handleSelect} />;
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.languages.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.languages.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {languages.map(language => (
-              <FacetItem
-                active={this.props.languages.includes(language)}
-                facetMode={this.props.facetMode}
-                key={language}
-                name={this.getLanguageName(language)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(language)}
-                value={language}
-              />
-            ))}
-          </FacetItemsList>}
-
-        {this.props.open && <LanguageFacetFooter onSelect={this.handleSelect} />}
+        {this.props.open && this.renderList()}
+        {this.props.open && this.renderFooter()}
       </FacetBox>
     );
   }
index 8711e0174628e618227c9b197f59c9f1a0dcc9c9..35e805a54bc545514fae464a1a2c4a8c765ce16b 100644 (file)
@@ -59,6 +59,10 @@ export default class ModuleFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getStat(module: string): ?number {
     const { stats } = this.props;
     return stats ? stats[module] : null;
@@ -75,7 +79,7 @@ export default class ModuleFacet extends React.PureComponent {
     );
   }
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -84,29 +88,35 @@ export default class ModuleFacet extends React.PureComponent {
 
     const modules = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {modules.map(module => (
+          <FacetItem
+            active={this.props.modules.includes(module)}
+            facetMode={this.props.facetMode}
+            key={module}
+            name={this.renderName(module)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(module)}
+            value={module}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.modules.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.modules.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {modules.map(module => (
-              <FacetItem
-                active={this.props.modules.includes(module)}
-                facetMode={this.props.facetMode}
-                key={module}
-                name={this.renderName(module)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(module)}
-                value={module}
-              />
-            ))}
-          </FacetItemsList>}
+        {this.props.open && this.renderList()}
       </FacetBox>
     );
   }
index 960a5bb1739742dc963919c51404f6a1a4874008..0fe8bc84ffbd35a366d0ef8675744c2c802d9974 100644 (file)
@@ -63,6 +63,10 @@ export default class ProjectFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   handleSearch = (query: string) => {
     const { component } = this.props;
 
@@ -116,7 +120,7 @@ export default class ProjectFacet extends React.PureComponent {
     );
   };
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -125,37 +129,51 @@ export default class ProjectFacet extends React.PureComponent {
 
     const projects = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {projects.map(project => (
+          <FacetItem
+            active={this.props.projects.includes(project)}
+            facetMode={this.props.facetMode}
+            key={project}
+            name={this.renderName(project)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(project)}
+            value={project}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  renderFooter() {
+    if (!this.props.stats) {
+      return null;
+    }
+
+    return (
+      <FacetFooter
+        minimumQueryLength={3}
+        onSearch={this.handleSearch}
+        onSelect={this.handleSelect}
+        renderOption={this.renderOption}
+      />
+    );
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.projects.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.projects.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {projects.map(project => (
-              <FacetItem
-                active={this.props.projects.includes(project)}
-                facetMode={this.props.facetMode}
-                key={project}
-                name={this.renderName(project)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(project)}
-                value={project}
-              />
-            ))}
-          </FacetItemsList>}
-
-        {this.props.open &&
-          <FacetFooter
-            minimumQueryLength={3}
-            onSearch={this.handleSearch}
-            onSelect={this.handleSelect}
-            renderOption={this.renderOption}
-          />}
+        {this.props.open && this.renderList()}
+        {this.props.open && this.renderFooter()}
       </FacetBox>
     );
   }
index d83c56cd9172e6e6cf7c16c22fdac502855b9b7f..c905c98c2b8a87b40d551accd9af848b40727c21 100644 (file)
@@ -65,6 +65,10 @@ export default class ResolutionFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ resolved: false, resolutions: [] });
+  };
+
   isFacetItemActive(resolution: string) {
     return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution);
   }
@@ -103,10 +107,11 @@ export default class ResolutionFacet extends React.PureComponent {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={!this.props.resolved || this.props.resolutions.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.resolutions.length}
         />
 
         {this.props.open &&
index ae9879d59f24ef8ea6ef15c8432da3c5abc8347b..f28e7de47b019d0b9ff5f2edba969f85f8dc795c 100644 (file)
@@ -60,6 +60,10 @@ export default class RuleFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   handleSearch = (query: string) => {
     const { languages } = this.props;
     return searchRules({
@@ -86,7 +90,7 @@ export default class RuleFacet extends React.PureComponent {
     return stats ? stats[rule] : null;
   }
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -95,32 +99,44 @@ export default class RuleFacet extends React.PureComponent {
 
     const rules = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {rules.map(rule => (
+          <FacetItem
+            active={this.props.rules.includes(rule)}
+            facetMode={this.props.facetMode}
+            key={rule}
+            name={this.getRuleName(rule)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(rule)}
+            value={rule}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  renderFooter() {
+    if (!this.props.stats) {
+      return null;
+    }
+
+    return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />;
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.rules.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.rules.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {rules.map(rule => (
-              <FacetItem
-                active={this.props.rules.includes(rule)}
-                facetMode={this.props.facetMode}
-                key={rule}
-                name={this.getRuleName(rule)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(rule)}
-                value={rule}
-              />
-            ))}
-          </FacetItemsList>}
-
-        {this.props.open &&
-          <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />}
+        {this.props.open && this.renderList()}
+        {this.props.open && this.renderFooter()}
       </FacetBox>
     );
   }
index e95f44008d01a312f6b6fee8c29d5fa358565fcb..8b4edb49672b46d27b8ad24bf873400024270f01 100644 (file)
@@ -57,6 +57,10 @@ export default class SeverityFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getStat(severity: string): ?number {
     const { stats } = this.props;
     return stats ? stats[severity] : null;
@@ -87,10 +91,11 @@ export default class SeverityFacet extends React.PureComponent {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.severities.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.severities.length}
         />
 
         {this.props.open &&
index 613c3be57edb915ded392a0881fa60b96d3dd7b4..5d9d8988c026e0a9fd4d35a4243947a20c5f8365 100644 (file)
@@ -65,8 +65,9 @@ export default class Sidebar extends React.PureComponent {
 
     const displayProjectsFacet: boolean =
       component == null || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
-    const displayModulesFacet = component == null || component.qualifier !== 'DIR';
-    const displayDirectoriesFacet = component == null || component.qualifier !== 'DIR';
+    const displayModulesFacet = component != null && component.qualifier !== 'DIR';
+    const displayDirectoriesFacet = component != null && component.qualifier !== 'DIR';
+    const displayFilesFacet = component != null;
     const displayAuthorFacet = component == null || component.qualifier !== 'DEV';
 
     return (
@@ -167,15 +168,16 @@ export default class Sidebar extends React.PureComponent {
             referencedComponents={this.props.referencedComponents}
             stats={facets.directories}
           />}
-        <FileFacet
-          facetMode={query.facetMode}
-          onChange={this.props.onFilterChange}
-          onToggle={this.props.onFacetToggle}
-          open={!!openFacets.files}
-          files={query.files}
-          referencedComponents={this.props.referencedComponents}
-          stats={facets.files}
-        />
+        {displayFilesFacet &&
+          <FileFacet
+            facetMode={query.facetMode}
+            onChange={this.props.onFilterChange}
+            onToggle={this.props.onFacetToggle}
+            open={!!openFacets.files}
+            files={query.files}
+            referencedComponents={this.props.referencedComponents}
+            stats={facets.files}
+          />}
         {!this.props.myIssues &&
           <AssigneeFacet
             component={component}
index 40bfb2511419585cfc4e6ba37aebe72524c1cb38..14f4a09fc637836f3612b57c5ef3ed1f5b01c816 100644 (file)
@@ -56,6 +56,10 @@ export default class StatusFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getStat(status: string): ?number {
     const { stats } = this.props;
     return stats ? stats[status] : null;
@@ -96,10 +100,11 @@ export default class StatusFacet extends React.PureComponent {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.statuses.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.statuses.length}
         />
 
         {this.props.open &&
index 120eb8d890b59be04551d9cedb0641f5c9006c0e..91fc77c5f546f6e6f84bf9442d1255134acb47e5 100644 (file)
@@ -58,6 +58,10 @@ export default class TagFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   handleSearch = (query: string) => {
     return searchIssueTags({ ps: 50, q: query }).then(tags =>
       tags.map(tag => ({ label: tag, value: tag }))
@@ -83,7 +87,7 @@ export default class TagFacet extends React.PureComponent {
     );
   }
 
-  render() {
+  renderList() {
     const { stats } = this.props;
 
     if (!stats) {
@@ -92,32 +96,45 @@ export default class TagFacet extends React.PureComponent {
 
     const tags = sortBy(Object.keys(stats), key => -stats[key]);
 
+    return (
+      <FacetItemsList>
+        {tags.map(tag => (
+          <FacetItem
+            active={this.props.tags.includes(tag)}
+            facetMode={this.props.facetMode}
+            key={tag}
+            name={this.renderTag(tag)}
+            onClick={this.handleItemClick}
+            stat={this.getStat(tag)}
+            value={tag}
+          />
+        ))}
+      </FacetItemsList>
+    );
+  }
+
+  renderFooter() {
+    if (!this.props.stats) {
+      return null;
+    }
+
+    return <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />;
+  }
+
+  render() {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.tags.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.tags.length}
         />
 
-        {this.props.open &&
-          <FacetItemsList>
-            {tags.map(tag => (
-              <FacetItem
-                active={this.props.tags.includes(tag)}
-                facetMode={this.props.facetMode}
-                key={tag}
-                name={this.renderTag(tag)}
-                onClick={this.handleItemClick}
-                stat={this.getStat(tag)}
-                value={tag}
-              />
-            ))}
-          </FacetItemsList>}
-
-        {this.props.open &&
-          <FacetFooter onSearch={this.handleSearch} onSelect={this.handleSelect} />}
+        {this.props.open && this.renderList()}
+
+        {this.props.open && this.renderFooter()}
       </FacetBox>
     );
   }
index c0eb02710582c48291f42c1ef9af5ac3009afa77..f8b7630b834da2b3d3777fd2109da9faff79b7a0 100644 (file)
@@ -57,6 +57,10 @@ export default class TypeFacet extends React.PureComponent {
     this.props.onToggle(this.property);
   };
 
+  handleClear = () => {
+    this.props.onChange({ [this.property]: [] });
+  };
+
   getStat(type: string): ?number {
     const { stats } = this.props;
     return stats ? stats[type] : null;
@@ -86,10 +90,11 @@ export default class TypeFacet extends React.PureComponent {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
-          hasValue={this.props.types.length > 0}
           name={translate('issues.facet', this.property)}
+          onClear={this.handleClear}
           onClick={this.handleHeaderClick}
           open={this.props.open}
+          values={this.props.types.length}
         />
 
         {this.props.open &&
index 5dc2230f4e347ecd3c8c7d7cd12e7ee9704d2888..2ce4f76254c2460ce1e833ab50a3cf13ea6b3fab 100644 (file)
@@ -42,7 +42,7 @@ it('should render', () => {
   expect(renderAssigneeFacet()).toMatchSnapshot();
 });
 
-it('should not render without stats', () => {
+it('should render without stats', () => {
   expect(renderAssigneeFacet({ stats: null })).toMatchSnapshot();
 });
 
index a55c903b90cbc92f90e462ba4ba2ac996fb924c5..7c829b2b32b8082da8261c6798f6ae92ac95641e 100644 (file)
@@ -28,7 +28,7 @@ const renderSidebar = props =>
     .children()
     .map(node => node.name());
 
-it('should render all facets', () => {
+it('should render facets for global page', () => {
   expect(renderSidebar()).toMatchSnapshot();
 });
 
index 80726a28b0bf08408c147e90745651be97ceb021..d0de2e5b3cf01501b129e036835e94c562edd247 100644 (file)
@@ -1,13 +1,12 @@
-exports[`test should not render without stats 1`] = `null`;
-
 exports[`test should render 1`] = `
 <FacetBox
   property="assignees">
   <FacetHeader
-    hasValue={false}
     name="issues.facet.assignees"
+    onClear={[Function]}
     onClick={[Function]}
-    open={true} />
+    open={true}
+    values={0} />
   <FacetItemsList>
     <FacetItem
       active={false}
@@ -62,14 +61,27 @@ exports[`test should render footer select option 1`] = `
 </span>
 `;
 
+exports[`test should render without stats 1`] = `
+<FacetBox
+  property="assignees">
+  <FacetHeader
+    name="issues.facet.assignees"
+    onClear={[Function]}
+    onClick={[Function]}
+    open={true}
+    values={0} />
+</FacetBox>
+`;
+
 exports[`test should select unassigned 1`] = `
 <FacetBox
   property="assignees">
   <FacetHeader
-    hasValue={true}
     name="issues.facet.assignees"
+    onClear={[Function]}
     onClick={[Function]}
-    open={true} />
+    open={true}
+    values={1} />
   <FacetItemsList>
     <FacetItem
       active={true}
@@ -118,10 +130,11 @@ exports[`test should select user 1`] = `
 <FacetBox
   property="assignees">
   <FacetHeader
-    hasValue={true}
     name="issues.facet.assignees"
+    onClear={[Function]}
     onClick={[Function]}
-    open={true} />
+    open={true}
+    values={1} />
   <FacetItemsList>
     <FacetItem
       active={false}
index 81d6ce875fc293c8884d48b34e26968eb9996439..03bc82ca4405f88cd5878eeb993d764f87811e80 100644 (file)
@@ -1,4 +1,4 @@
-exports[`test should render all facets 1`] = `
+exports[`test should render facets for developer 1`] = `
 Array [
   "FacetMode",
   "TypeFacet",
@@ -13,12 +13,11 @@ Array [
   "DirectoryFacet",
   "FileFacet",
   "AssigneeFacet",
-  "AuthorFacet",
   "LanguageFacet",
 ]
 `;
 
-exports[`test should render facets for developer 1`] = `
+exports[`test should render facets for directory 1`] = `
 Array [
   "FacetMode",
   "TypeFacet",
@@ -28,16 +27,14 @@ Array [
   "CreationDateFacet",
   "RuleFacet",
   "TagFacet",
-  "ProjectFacet",
-  "ModuleFacet",
-  "DirectoryFacet",
   "FileFacet",
   "AssigneeFacet",
+  "AuthorFacet",
   "LanguageFacet",
 ]
 `;
 
-exports[`test should render facets for directory 1`] = `
+exports[`test should render facets for global page 1`] = `
 Array [
   "FacetMode",
   "TypeFacet",
@@ -47,7 +44,7 @@ Array [
   "CreationDateFacet",
   "RuleFacet",
   "TagFacet",
-  "FileFacet",
+  "ProjectFacet",
   "AssigneeFacet",
   "AuthorFacet",
   "LanguageFacet",
@@ -103,9 +100,6 @@ Array [
   "RuleFacet",
   "TagFacet",
   "ProjectFacet",
-  "ModuleFacet",
-  "DirectoryFacet",
-  "FileFacet",
   "AuthorFacet",
   "LanguageFacet",
 ]
index ff6ef8387c977b85e93e766bf22e1b17d97c8cd5..19b720e9f7eca970250bb608d76a7d3a87a98e49 100644 (file)
 // @flow
 /* eslint-disable max-len */
 import React from 'react';
+import { translate } from '../../../../helpers/l10n';
 
-type Props = {
-  hasValue: boolean,
+type Props = {|
   name: string,
+  onClear?: () => void,
   onClick?: () => void,
-  open: boolean
-};
+  open: boolean,
+  values?: number
+|};
 
 export default class FacetHeader extends React.PureComponent {
   props: Props;
 
   static defaultProps = {
-    hasValue: false,
     open: true
   };
 
-  handleClick = (e: Event & { currentTarget: HTMLElement }) => {
-    e.preventDefault();
-    e.currentTarget.blur();
+  handleClearClick = (event: Event & { currentTarget: HTMLElement }) => {
+    event.preventDefault();
+    event.currentTarget.blur();
+    if (this.props.onClear) {
+      this.props.onClear();
+    }
+  };
+
+  handleClick = (event: Event & { currentTarget: HTMLElement }) => {
+    event.preventDefault();
+    event.currentTarget.blur();
     if (this.props.onClick) {
       this.props.onClick();
     }
@@ -61,23 +70,32 @@ export default class FacetHeader extends React.PureComponent {
   }
 
   renderValueIndicator() {
-    return this.props.hasValue && !this.props.open
-      ? <svg viewBox="0 0 1792 1792" width="8" height="8" style={{ paddingTop: 5, paddingLeft: 8 }}>
-          <path
-            d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"
-            fill="#4b9fd5"
-          />
-        </svg>
-      : null;
+    if (this.props.open || !this.props.values) {
+      return null;
+    }
+    return <span className="spacer-left badge is-rounded">{this.props.values}</span>;
   }
 
   render() {
-    return this.props.onClick
-      ? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}>
-          {this.renderCheckbox()}{' '}{this.props.name}{' '}{this.renderValueIndicator()}
-        </a>
-      : <span className="search-navigator-facet-header">
-          {this.props.name}
-        </span>;
+    const showClearButton: boolean = !!this.props.values && this.props.onClear != null;
+
+    return (
+      <div>
+        {showClearButton &&
+          <button
+            className="search-navigator-facet-header-button button-small button-red"
+            onClick={this.handleClearClick}>
+            {translate('clear')}
+          </button>}
+
+        {this.props.onClick
+          ? <a className="search-navigator-facet-header" href="#" onClick={this.handleClick}>
+              {this.renderCheckbox()}{' '}{this.props.name}{' '}{this.renderValueIndicator()}
+            </a>
+          : <span className="search-navigator-facet-header">
+              {this.props.name}
+            </span>}
+      </div>
+    );
   }
 }
index 5aa00c4a41e9d76a5781269b702070a531d7c61f..ed3f143eedcaceb4f6e8c3fc7ec5408962d0e006 100644 (file)
@@ -25,37 +25,41 @@ import FacetHeader from '../FacetHeader';
 
 it('should render open facet with value', () => {
   expect(
-    shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={true} />)
+    shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} values={1} />)
   ).toMatchSnapshot();
 });
 
 it('should render open facet without value', () => {
-  expect(
-    shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={true} />)
-  ).toMatchSnapshot();
+  expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={true} />)).toMatchSnapshot();
 });
 
 it('should render closed facet with value', () => {
   expect(
-    shallow(<FacetHeader hasValue={true} name="foo" onClick={jest.fn()} open={false} />)
+    shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} values={1} />)
   ).toMatchSnapshot();
 });
 
 it('should render closed facet without value', () => {
-  expect(
-    shallow(<FacetHeader hasValue={false} name="foo" onClick={jest.fn()} open={false} />)
-  ).toMatchSnapshot();
+  expect(shallow(<FacetHeader name="foo" onClick={jest.fn()} open={false} />)).toMatchSnapshot();
 });
 
 it('should render without link', () => {
-  expect(shallow(<FacetHeader hasValue={false} name="foo" open={false} />)).toMatchSnapshot();
+  expect(shallow(<FacetHeader name="foo" open={false} />)).toMatchSnapshot();
 });
 
 it('should call onClick', () => {
   const onClick = jest.fn();
+  const wrapper = shallow(<FacetHeader name="foo" onClick={onClick} open={false} />);
+  click(wrapper.find('a'));
+  expect(onClick).toHaveBeenCalled();
+});
+
+it('should clear', () => {
+  const onClear = jest.fn();
   const wrapper = shallow(
-    <FacetHeader hasValue={false} name="foo" onClick={onClick} open={false} />
+    <FacetHeader name="foo" onClear={onClear} onClick={jest.fn()} open={false} values={3} />
   );
-  click(wrapper);
-  expect(onClick).toHaveBeenCalled();
+  expect(wrapper).toMatchSnapshot();
+  click(wrapper.find('.button-red'));
+  expect(onClear).toHaveBeenCalled();
 });
index 3333ae8944d5cdaa3e1f2265f80c94d76580f2de..e50a3519a4252952f89fa9a883187e282476cc47 100644 (file)
-exports[`test should render closed facet with value 1`] = `
-<a
-  className="search-navigator-facet-header"
-  href="#"
-  onClick={[Function]}>
-  <svg
-    height="10"
-    style={
-      Object {
-        "paddingTop": 3,
+exports[`test should clear 1`] = `
+<div>
+  <button
+    className="search-navigator-facet-header-button button-small button-red"
+    onClick={[Function]}>
+    clear
+  </button>
+  <a
+    className="search-navigator-facet-header"
+    href="#"
+    onClick={[Function]}>
+    <svg
+      height="10"
+      style={
+        Object {
+          "paddingTop": 3,
+        }
       }
-    }
-    viewBox="0 0 1792 1792"
-    width="10">
-    <path
-      d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+      viewBox="0 0 1792 1792"
+      width="10">
+      <path
+        d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+        style={
+          Object {
+            "fill": "currentColor ",
+          }
+        } />
+    </svg>
+     
+    foo
+     
+    <span
+      className="spacer-left badge is-rounded">
+      3
+    </span>
+  </a>
+</div>
+`;
+
+exports[`test should render closed facet with value 1`] = `
+<div>
+  <a
+    className="search-navigator-facet-header"
+    href="#"
+    onClick={[Function]}>
+    <svg
+      height="10"
       style={
         Object {
-          "fill": "currentColor ",
+          "paddingTop": 3,
         }
-      } />
-  </svg>
-   
-  foo
-   
-  <svg
-    height="8"
-    style={
-      Object {
-        "paddingLeft": 8,
-        "paddingTop": 5,
       }
-    }
-    viewBox="0 0 1792 1792"
-    width="8">
-    <path
-      d="M1664 896q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"
-      fill="#4b9fd5" />
-  </svg>
-</a>
+      viewBox="0 0 1792 1792"
+      width="10">
+      <path
+        d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+        style={
+          Object {
+            "fill": "currentColor ",
+          }
+        } />
+    </svg>
+     
+    foo
+     
+    <span
+      className="spacer-left badge is-rounded">
+      1
+    </span>
+  </a>
+</div>
 `;
 
 exports[`test should render closed facet without value 1`] = `
-<a
-  className="search-navigator-facet-header"
-  href="#"
-  onClick={[Function]}>
-  <svg
-    height="10"
-    style={
-      Object {
-        "paddingTop": 3,
-      }
-    }
-    viewBox="0 0 1792 1792"
-    width="10">
-    <path
-      d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+<div>
+  <a
+    className="search-navigator-facet-header"
+    href="#"
+    onClick={[Function]}>
+    <svg
+      height="10"
       style={
         Object {
-          "fill": "currentColor ",
+          "paddingTop": 3,
         }
-      } />
-  </svg>
-   
-  foo
-   
-</a>
+      }
+      viewBox="0 0 1792 1792"
+      width="10">
+      <path
+        d="M1363 877l-742 742q-19 19-45 19t-45-19l-166-166q-19-19-19-45t19-45l531-531-531-531q-19-19-19-45t19-45l166-166q19-19 45-19t45 19l742 742q19 19 19 45t-19 45z"
+        style={
+          Object {
+            "fill": "currentColor ",
+          }
+        } />
+    </svg>
+     
+    foo
+     
+  </a>
+</div>
 `;
 
 exports[`test should render open facet with value 1`] = `
-<a
-  className="search-navigator-facet-header"
-  href="#"
-  onClick={[Function]}>
-  <svg
-    height="10"
-    style={
-      Object {
-        "paddingTop": 3,
-      }
-    }
-    viewBox="0 0 1792 1792"
-    width="10">
-    <path
-      d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+<div>
+  <a
+    className="search-navigator-facet-header"
+    href="#"
+    onClick={[Function]}>
+    <svg
+      height="10"
       style={
         Object {
-          "fill": "currentColor ",
+          "paddingTop": 3,
         }
-      } />
-  </svg>
-   
-  foo
-   
-</a>
+      }
+      viewBox="0 0 1792 1792"
+      width="10">
+      <path
+        d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+        style={
+          Object {
+            "fill": "currentColor ",
+          }
+        } />
+    </svg>
+     
+    foo
+     
+  </a>
+</div>
 `;
 
 exports[`test should render open facet without value 1`] = `
-<a
-  className="search-navigator-facet-header"
-  href="#"
-  onClick={[Function]}>
-  <svg
-    height="10"
-    style={
-      Object {
-        "paddingTop": 3,
-      }
-    }
-    viewBox="0 0 1792 1792"
-    width="10">
-    <path
-      d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+<div>
+  <a
+    className="search-navigator-facet-header"
+    href="#"
+    onClick={[Function]}>
+    <svg
+      height="10"
       style={
         Object {
-          "fill": "currentColor ",
+          "paddingTop": 3,
         }
-      } />
-  </svg>
-   
-  foo
-   
-</a>
+      }
+      viewBox="0 0 1792 1792"
+      width="10">
+      <path
+        d="M1683 808l-742 741q-19 19-45 19t-45-19l-742-741q-19-19-19-45.5t19-45.5l166-165q19-19 45-19t45 19l531 531 531-531q19-19 45-19t45 19l166 165q19 19 19 45.5t-19 45.5z"
+        style={
+          Object {
+            "fill": "currentColor ",
+          }
+        } />
+    </svg>
+     
+    foo
+     
+  </a>
+</div>
 `;
 
 exports[`test should render without link 1`] = `
-<span
-  className="search-navigator-facet-header">
-  foo
-</span>
+<div>
+  <span
+    className="search-navigator-facet-header">
+    foo
+  </span>
+</div>
 `;
index d035084daef3b6c3390d2fc5687fa3b2dbc5c69f..d40c2b9d90be8bf5344e081daa70a726af85b123 100644 (file)
   }
 }
 
+.issues-main-header-spinner {
+  margin-left: 1px;
+  margin-right: 9px;
+  margin-top: -1px;
+}
+
 .concise-issues-list-header,
 .concise-issues-list-header-inner {
 }
   transition: background-color 0.3s ease, border-color 0.3s ease;
 }
 
-.concise-issue-box:hover,
-.concise-issue-box:focus {
+.concise-issue-box:hover {
   background-color: #ffeaea;
+}
+
+.concise-issue-box:focus {
   outline: none
 }
 
 }
 
 .concise-issue-box-message {
+  overflow: hidden;
+  text-overflow: ellipsis;
   font-weight: bold;
 }
 
   display: flex;
   align-items: flex-start;
   border: none;
+}
+
+.issues-filters-header {
+  margin-bottom: 12px;
+  padding-bottom: 11px;
+  border-bottom: 1px solid #e6e6e6;
+}
+
+.issues-my-issues-filter {
+  margin-bottom: 24px;
+  text-align: center;
+}
+
+.issues-page-actions {
+  display: inline-block;
+  min-width: 80px;
+  text-align: right;
 }
\ No newline at end of file
index b37344e37d508be97fcebef7a0a2af5b745190d4..1ad58e08816c6f24d335015f18fd784ef3948bbc 100644 (file)
@@ -164,6 +164,15 @@ type RawFacet = {
 
 export type Facet = { [string]: number };
 
+export const mapFacet = (facet: string): string => {
+  const propertyMapping = {
+    files: 'fileUuids',
+    modules: 'moduleUuids',
+    projects: 'projectUuids'
+  };
+  return propertyMapping[facet] || facet;
+};
+
 export const parseFacets = (facets: Array<RawFacet>): { [string]: Facet } => {
   // for readability purpose
   const propertyMapping = {
index a8fd92450896f5fd5f3c18b7fb17eec06c18ccb1..add7d2bd892d1dec229269ae1098d98fb2893913 100644 (file)
@@ -24,11 +24,6 @@ import ProjectsListFooterContainer from './ProjectsListFooterContainer';
 import PageSidebar from './PageSidebar';
 import VisualizationsContainer from '../visualizations/VisualizationsContainer';
 import { parseUrlQuery } from '../store/utils';
-import Page from '../../../components/layout/Page';
-import PageMain from '../../../components/layout/PageMain';
-import PageMainInner from '../../../components/layout/PageMainInner';
-import PageSide from '../../../components/layout/PageSide';
-import PageFilters from '../../../components/layout/PageFilters';
 import '../styles.css';
 
 export default class AllProjects extends React.PureComponent {
@@ -100,19 +95,23 @@ export default class AllProjects extends React.PureComponent {
     const top = this.props.organization ? 95 : 30;
 
     return (
-      <Page className="projects-page">
-        <PageSide top={top}>
-          <PageFilters>
-            <PageSidebar
-              query={query}
-              isFavorite={this.props.isFavorite}
-              organization={this.props.organization}
-            />
-          </PageFilters>
-        </PageSide>
+      <div className="layout-page projects-page">
+        <div className="layout-page-side-outer">
+          <div className="layout-page-side" style={{ top }}>
+            <div className="layout-page-side-inner">
+              <div className="layout-page-filters">
+                <PageSidebar
+                  query={query}
+                  isFavorite={this.props.isFavorite}
+                  organization={this.props.organization}
+                />
+              </div>
+            </div>
+          </div>
+        </div>
 
-        <PageMain>
-          <PageMainInner>
+        <div className="layout-page-main">
+          <div className="layout-page-main-inner">
             <PageHeaderContainer onViewChange={this.handleViewChange} view={view} />
             {view === 'list' &&
               <ProjectsListContainer
@@ -132,9 +131,9 @@ export default class AllProjects extends React.PureComponent {
                 sort={query.sort}
                 visualization={visualization}
               />}
-          </PageMainInner>
-        </PageMain>
-      </Page>
+          </div>
+        </div>
+      </div>
     );
   }
 }
index c7cc3387c2a494ea947d96d9dd10a7040e4f5685..8f29e6a59c79ebfe422d6239b5fc5aaf883c9c96 100644 (file)
@@ -180,7 +180,7 @@ export default class SourceViewerBase extends React.PureComponent {
       `.source-line-code[data-line-number="${line}"] .source-line-issue-locations`
     );
     if (lineElement) {
-      scrollToElement(lineElement, 125, 75);
+      scrollToElement(lineElement, { topOffset: 125, bottomOffset: 75 });
     }
   }
 
index d97bc5a3b4afbac7ff453a13bc58998f5d9c72bc..d44df72f472a6dc9ab08d3099f487a5611e5fb72 100644 (file)
@@ -22,7 +22,7 @@ import React from 'react';
 import { Link } from 'react-router';
 import QualifierIcon from '../shared/QualifierIcon';
 import FavoriteContainer from '../controls/FavoriteContainer';
-import { getProjectUrl, getIssuesUrl } from '../../helpers/urls';
+import { getProjectUrl, getComponentIssuesUrl } from '../../helpers/urls';
 import { collapsedDirFromPath, fileFromPath } from '../../helpers/path';
 import { translate } from '../../helpers/l10n';
 import { formatMeasure } from '../../helpers/measures';
@@ -171,7 +171,7 @@ export default class SourceViewerHeader extends React.PureComponent {
           <div className="source-viewer-header-measure">
             <span className="source-viewer-header-measure-value">
               <Link
-                to={getIssuesUrl({ resolved: 'false', fileUuids: uuid })}
+                to={getComponentIssuesUrl(project, { resolved: 'false', fileUuids: uuid })}
                 className="source-viewer-header-external-link"
                 target="_blank">
                 {measures.issues != null ? formatMeasure(measures.issues, 'SHORT_INT') : 0}
index 30ba67f06f3bdd4e767f3c6bf905e4315f4880f2..01ce80da9e91d0e3fa3bde15a3c16bf549b1c1e8 100644 (file)
@@ -28,7 +28,6 @@ import LineDuplications from './LineDuplications';
 import LineDuplicationBlock from './LineDuplicationBlock';
 import LineIssuesIndicator from './LineIssuesIndicator';
 import LineCode from './LineCode';
-import { TooltipsContainer } from '../../mixins/tooltips-mixin';
 import type { SourceLine } from '../types';
 import type { LinearIssueLocation } from '../helpers/indexing';
 import type { Issue } from '../../issue/types';
@@ -99,62 +98,60 @@ export default class Line extends React.PureComponent {
     });
 
     return (
-      <TooltipsContainer>
-        <tr className={className} data-line-number={line.line}>
-          <LineNumber line={line} onClick={this.props.onClick} />
+      <tr className={className} data-line-number={line.line}>
+        <LineNumber line={line} onClick={this.props.onClick} />
 
-          <LineSCM
-            line={line}
-            onClick={this.props.onSCMClick}
-            previousLine={this.props.previousLine}
-          />
-
-          {this.props.displayCoverage &&
-            <LineCoverage line={line} onClick={this.props.onCoverageClick} />}
-
-          {this.props.displayDuplications &&
-            <LineDuplications line={line} onClick={this.props.loadDuplications} />}
+        <LineSCM
+          line={line}
+          onClick={this.props.onSCMClick}
+          previousLine={this.props.previousLine}
+        />
 
-          {times(duplicationsCount).map(index => (
-            <LineDuplicationBlock
-              duplicated={duplications.includes(index)}
-              index={index}
-              key={index}
-              line={this.props.line}
-              onClick={this.props.onDuplicationClick}
-            />
-          ))}
+        {this.props.displayCoverage &&
+          <LineCoverage line={line} onClick={this.props.onCoverageClick} />}
 
-          {this.props.displayIssues &&
-            !this.props.displayAllIssues &&
-            <LineIssuesIndicator
-              issues={this.props.issues}
-              line={line}
-              onClick={this.handleIssuesIndicatorClick}
-            />}
+        {this.props.displayDuplications &&
+          <LineDuplications line={line} onClick={this.props.loadDuplications} />}
 
-          {this.props.displayFiltered &&
-            <td className="source-meta source-line-filtered-container" data-line-number={line.line}>
-              <div className="source-line-bar" />
-            </td>}
+        {times(duplicationsCount).map(index => (
+          <LineDuplicationBlock
+            duplicated={duplications.includes(index)}
+            index={index}
+            key={index}
+            line={this.props.line}
+            onClick={this.props.onDuplicationClick}
+          />
+        ))}
 
-          <LineCode
-            highlightedLocationMessage={this.props.highlightedLocationMessage}
-            highlightedSymbols={this.props.highlightedSymbols}
+        {this.props.displayIssues &&
+          !this.props.displayAllIssues &&
+          <LineIssuesIndicator
             issues={this.props.issues}
-            issueLocations={this.props.issueLocations}
             line={line}
-            onIssueChange={this.props.onIssueChange}
-            onIssueSelect={this.props.onIssueSelect}
-            onLocationSelect={this.props.onLocationSelect}
-            onSymbolClick={this.props.onSymbolClick}
-            scroll={this.props.scroll}
-            secondaryIssueLocations={this.props.secondaryIssueLocations}
-            selectedIssue={this.props.selectedIssue}
-            showIssues={this.props.openIssues || this.props.displayAllIssues}
-          />
-        </tr>
-      </TooltipsContainer>
+            onClick={this.handleIssuesIndicatorClick}
+          />}
+
+        {this.props.displayFiltered &&
+          <td className="source-meta source-line-filtered-container" data-line-number={line.line}>
+            <div className="source-line-bar" />
+          </td>}
+
+        <LineCode
+          highlightedLocationMessage={this.props.highlightedLocationMessage}
+          highlightedSymbols={this.props.highlightedSymbols}
+          issues={this.props.issues}
+          issueLocations={this.props.issueLocations}
+          line={line}
+          onIssueChange={this.props.onIssueChange}
+          onIssueSelect={this.props.onIssueSelect}
+          onLocationSelect={this.props.onLocationSelect}
+          onSymbolClick={this.props.onSymbolClick}
+          scroll={this.props.scroll}
+          secondaryIssueLocations={this.props.secondaryIssueLocations}
+          selectedIssue={this.props.selectedIssue}
+          showIssues={this.props.openIssues || this.props.displayAllIssues}
+        />
+      </tr>
     );
   }
 }
index 73b0da7db159b03b9184d00f2e3af0d744d80752..f069427afe002b4625e64757f2ee6fc46e724836 100644 (file)
@@ -19,6 +19,7 @@
  */
 // @flow
 import React from 'react';
+import Tooltip from '../../controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
 import type { SourceLine } from '../types';
 
@@ -40,21 +41,23 @@ export default class LineCoverage extends React.PureComponent {
     const className =
       'source-meta source-line-coverage' +
       (line.coverageStatus != null ? ` source-line-${line.coverageStatus}` : '');
-    const title = line.coverageStatus != null
-      ? translate('source_viewer.tooltip', line.coverageStatus)
-      : undefined;
-    return (
+    const cell = (
       <td
         className={className}
         data-line-number={line.line}
-        title={title}
-        data-placement={line.coverageStatus != null ? 'right' : undefined}
-        data-toggle={line.coverageStatus != null ? 'tooltip' : undefined}
         role={line.coverageStatus != null ? 'button' : undefined}
         tabIndex={line.coverageStatus != null ? 0 : undefined}
         onClick={line.coverageStatus != null ? this.handleClick : undefined}>
         <div className="source-line-bar" />
       </td>
     );
+
+    return line.coverageStatus != null
+      ? <Tooltip
+          overlay={translate('source_viewer.tooltip', line.coverageStatus)}
+          placement="right">
+          {cell}
+        </Tooltip>
+      : cell;
   }
 }
index 4edaca1c4c8930cee8f88b64da59e28eda8c20e3..ee1373fdde5478b479b24333ce83f1163a604e6a 100644 (file)
@@ -20,6 +20,7 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
+import Tooltip from '../../controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
 import type { SourceLine } from '../types';
 
@@ -44,20 +45,23 @@ export default class LineDuplicationBlock extends React.PureComponent {
       'source-line-duplicated': duplicated
     });
 
-    return (
+    const cell = (
       <td
         key={index}
         className={className}
         data-line-number={line.line}
         data-index={index}
-        title={duplicated ? translate('source_viewer.tooltip.duplicated_block') : undefined}
-        data-placement={duplicated ? 'right' : undefined}
-        data-toggle={duplicated ? 'tooltip' : undefined}
         role={duplicated ? 'button' : undefined}
         tabIndex={duplicated ? '0' : undefined}
         onClick={duplicated ? this.handleClick : undefined}>
         <div className="source-line-bar" />
       </td>
     );
+
+    return duplicated
+      ? <Tooltip overlay={translate('source_viewer.tooltip.duplicated_block')} placement="right">
+          {cell}
+        </Tooltip>
+      : cell;
   }
 }
index 5f0a2936859a78e1f5e3d49bd6f7802519b4bde4..85cc046b1c843b2b05886501e2e02018601f94c8 100644 (file)
@@ -20,6 +20,7 @@
 // @flow
 import React from 'react';
 import classNames from 'classnames';
+import Tooltip from '../../controls/Tooltip';
 import { translate } from '../../../helpers/l10n';
 import type { SourceLine } from '../types';
 
@@ -41,19 +42,21 @@ export default class LineDuplications extends React.PureComponent {
     const className = classNames('source-meta', 'source-line-duplications', {
       'source-line-duplicated': line.duplicated
     });
-    const title = line.duplicated ? translate('source_viewer.tooltip.duplicated_line') : undefined;
 
-    return (
+    const cell = (
       <td
         className={className}
-        title={title}
-        data-placement={line.duplicated ? 'right' : undefined}
-        data-toggle={line.duplicated ? 'tooltip' : undefined}
         role={line.duplicated ? 'button' : undefined}
         tabIndex={line.duplicated ? 0 : undefined}
         onClick={line.duplicated ? this.handleClick : undefined}>
         <div className="source-line-bar" />
       </td>
     );
+
+    return line.duplicated
+      ? <Tooltip overlay={translate('source_viewer.tooltip.duplicated_line')} placement="right">
+          {cell}
+        </Tooltip>
+      : cell;
   }
 }
index aacd16d7866ce50472b6c531538374efcb4fbcc7..b657ec6e4931ff4a2369b83314091ced8e3084ca 100644 (file)
@@ -27,7 +27,7 @@ it('render covered line', () => {
   const onClick = jest.fn();
   const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
   expect(wrapper).toMatchSnapshot();
-  click(wrapper);
+  click(wrapper.find('[tabIndex]'));
   expect(onClick).toHaveBeenCalled();
 });
 
@@ -36,7 +36,7 @@ it('render uncovered line', () => {
   const onClick = jest.fn();
   const wrapper = shallow(<LineCoverage line={line} onClick={onClick} />);
   expect(wrapper).toMatchSnapshot();
-  click(wrapper);
+  click(wrapper.find('[tabIndex]'));
   expect(onClick).toHaveBeenCalled();
 });
 
index cd0baf595d0f2f78439e0b4f2381b409df854623..9075badf9894724d61127700fbba66dc3601f256 100644 (file)
@@ -29,7 +29,7 @@ it('render duplicated line', () => {
     <LineDuplicationBlock index={1} duplicated={true} line={line} onClick={onClick} />
   );
   expect(wrapper).toMatchSnapshot();
-  click(wrapper);
+  click(wrapper.find('[tabIndex]'));
   expect(onClick).toHaveBeenCalled();
 });
 
index b2aa124a64a0304aa51b8781ec852cb8465b8295..52c22486bd05af721cb01ce9e3a7cd30c97c2e8d 100644 (file)
@@ -27,7 +27,7 @@ it('render duplicated line', () => {
   const onClick = jest.fn();
   const wrapper = shallow(<LineDuplications line={line} onClick={onClick} />);
   expect(wrapper).toMatchSnapshot();
-  click(wrapper);
+  click(wrapper.find('[tabIndex]'));
   expect(onClick).toHaveBeenCalled();
 });
 
index ccf5c4d3c4fc4b22ddc5d971b9e7731fd7cde997..d9fc384049908092500f3207d50427bd00574349 100644 (file)
@@ -1,16 +1,17 @@
 exports[`test render covered line 1`] = `
-<td
-  className="source-meta source-line-coverage source-line-covered"
-  data-line-number={3}
-  data-placement="right"
-  data-toggle="tooltip"
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
-  title="source_viewer.tooltip.covered">
-  <div
-    className="source-line-bar" />
-</td>
+<Tooltip
+  overlay="source_viewer.tooltip.covered"
+  placement="right">
+  <td
+    className="source-meta source-line-coverage source-line-covered"
+    data-line-number={3}
+    onClick={[Function]}
+    role="button"
+    tabIndex={0}>
+    <div
+      className="source-line-bar" />
+  </td>
+</Tooltip>
 `;
 
 exports[`test render line with unknown coverage 1`] = `
@@ -23,16 +24,17 @@ exports[`test render line with unknown coverage 1`] = `
 `;
 
 exports[`test render uncovered line 1`] = `
-<td
-  className="source-meta source-line-coverage source-line-uncovered"
-  data-line-number={3}
-  data-placement="right"
-  data-toggle="tooltip"
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
-  title="source_viewer.tooltip.uncovered">
-  <div
-    className="source-line-bar" />
-</td>
+<Tooltip
+  overlay="source_viewer.tooltip.uncovered"
+  placement="right">
+  <td
+    className="source-meta source-line-coverage source-line-uncovered"
+    data-line-number={3}
+    onClick={[Function]}
+    role="button"
+    tabIndex={0}>
+    <div
+      className="source-line-bar" />
+  </td>
+</Tooltip>
 `;
index b94d4b3bc09a854797c52cdd0424b007380617c9..ee28d4ae2fb7eaff2a6f3b7816b7469f0847f970 100644 (file)
@@ -1,17 +1,18 @@
 exports[`test render duplicated line 1`] = `
-<td
-  className="source-meta source-line-duplications-extra source-line-duplicated"
-  data-index={1}
-  data-line-number={3}
-  data-placement="right"
-  data-toggle="tooltip"
-  onClick={[Function]}
-  role="button"
-  tabIndex="0"
-  title="source_viewer.tooltip.duplicated_block">
-  <div
-    className="source-line-bar" />
-</td>
+<Tooltip
+  overlay="source_viewer.tooltip.duplicated_block"
+  placement="right">
+  <td
+    className="source-meta source-line-duplications-extra source-line-duplicated"
+    data-index={1}
+    data-line-number={3}
+    onClick={[Function]}
+    role="button"
+    tabIndex="0">
+    <div
+      className="source-line-bar" />
+  </td>
+</Tooltip>
 `;
 
 exports[`test render not duplicated line 1`] = `
index 7e977c88442868354bcb06d6922be06c4ee6d90c..ebf651598498335df66558d49dd4807a288e0468 100644 (file)
@@ -1,15 +1,16 @@
 exports[`test render duplicated line 1`] = `
-<td
-  className="source-meta source-line-duplications source-line-duplicated"
-  data-placement="right"
-  data-toggle="tooltip"
-  onClick={[Function]}
-  role="button"
-  tabIndex={0}
-  title="source_viewer.tooltip.duplicated_line">
-  <div
-    className="source-line-bar" />
-</td>
+<Tooltip
+  overlay="source_viewer.tooltip.duplicated_line"
+  placement="right">
+  <td
+    className="source-meta source-line-duplications source-line-duplicated"
+    onClick={[Function]}
+    role="button"
+    tabIndex={0}>
+    <div
+      className="source-line-bar" />
+  </td>
+</Tooltip>
 `;
 
 exports[`test render not duplicated line 1`] = `
diff --git a/server/sonar-web/src/main/js/components/common/EmptySearch.css b/server/sonar-web/src/main/js/components/common/EmptySearch.css
new file mode 100644 (file)
index 0000000..2ad32dd
--- /dev/null
@@ -0,0 +1,7 @@
+.empty-search {
+  padding: 60px 0;
+  border: 1px solid #e6e6e6;
+  border-radius: 2px;
+  color: #777;
+  text-align: center;
+}
\ No newline at end of file
index 904a6b2cbad8dd7e69d5c9be49dc79d98194e3e4..719a2239c90838475cee262b8f6c40293fae41c5 100644 (file)
  */
 // @flow
 import React from 'react';
-import { css } from 'glamor';
 import { translate } from '../../helpers/l10n';
+import './EmptySearch.css';
 
 const EmptySearch = () => (
-  <div
-    className={css({
-      padding: '60px 0',
-      border: '1px solid #e6e6e6',
-      borderRadius: 2,
-      textAlign: 'center',
-      color: '#777'
-    })}>
+  <div className="empty-search">
     <h3>{translate('no_results_search')}</h3>
     <p className="big-spacer-top">{translate('no_results_search.2')}</p>
   </div>
index d5695f82a225d56dd7510236f0d74336cfdd81a1..cbfadc9307a9375e557189522456abb3968d1ca8 100644 (file)
@@ -36,6 +36,7 @@ type State = {
 
 export default class SelectList extends React.PureComponent {
   currentKeyScope: string;
+  previousFilter: Function;
   previousKeyScope: string;
   props: Props;
   state: State;
@@ -66,9 +67,18 @@ export default class SelectList extends React.PureComponent {
 
   attachShortcuts = () => {
     this.previousKeyScope = key.getScope();
+    this.previousFilter = key.filter;
     this.currentKeyScope = uniqueId('key-scope');
     key.setScope(this.currentKeyScope);
 
+    // sometimes there is a *focused* search field next to the SelectList component
+    // we need to allow shortcuts in this case, but only for the used keys
+    key.filter = (event: KeyboardEvent & { target: HTMLElement }) => {
+      const tagName = (event.target || event.srcElement).tagName;
+      const isInput = tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
+      return [13, 38, 40].includes(event.keyCode) || !isInput;
+    };
+
     key('down', this.currentKeyScope, () => {
       this.setState(this.selectNextElement);
       return false;
@@ -80,7 +90,7 @@ export default class SelectList extends React.PureComponent {
     });
 
     key('return', this.currentKeyScope, () => {
-      if (this.state.active) {
+      if (this.state.active != null) {
         this.handleSelect(this.state.active);
       }
       return false;
@@ -90,6 +100,7 @@ export default class SelectList extends React.PureComponent {
   detachShortcuts = () => {
     key.setScope(this.previousKeyScope);
     key.deleteScope(this.currentKeyScope);
+    key.filter = this.previousFilter;
   };
 
   handleSelect = (item: string) => {
index 5762cff7c6b4a5232943a115db7908e41488ed8c..57a5d6969a5e446cd4bcbf26f5730f9ce2184e02 100644 (file)
@@ -44,9 +44,7 @@ export default class Checkbox extends React.PureComponent {
   }
 
   render() {
-    const className = classNames('icon-checkbox', {
-      // trick to work with glamor
-      [this.props.className]: true,
+    const className = classNames('icon-checkbox', this.props.className, {
       'icon-checkbox-checked': this.props.checked,
       'icon-checkbox-single': this.props.thirdState
     });
index f45671c6f74fb76a07fb00092c26b0090bcac460..cd44f80e5b7d241c701a53c49fff7b738fe455fd 100644 (file)
@@ -19,6 +19,7 @@
  */
 // @flow
 import React from 'react';
+import key from 'keymaster';
 import IssueView from './IssueView';
 import { updateIssue } from './actions';
 import { setIssueAssignee } from '../../api/issues';
@@ -86,11 +87,39 @@ export default class BaseIssue extends React.PureComponent {
   }
 
   bindShortcuts() {
-    document.addEventListener('keypress', this.handleKeyPress);
+    key('f', 'issues', () => {
+      this.togglePopup('transition');
+      return false;
+    });
+    key('a', 'issues', () => {
+      this.togglePopup('assign');
+      return false;
+    });
+    key('m', 'issues', () => {
+      this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me');
+      return false;
+    });
+    key('i', 'issues', () => {
+      this.togglePopup('set-severity');
+      return false;
+    });
+    key('c', 'issues', () => {
+      this.togglePopup('comment');
+      return false;
+    });
+    key('t', 'issues', () => {
+      this.togglePopup('edit-tags');
+      return false;
+    });
   }
 
   unbindShortcuts() {
-    document.removeEventListener('keypress', this.handleKeyPress);
+    key.unbind('f', 'issues');
+    key.unbind('a', 'issues');
+    key.unbind('m', 'issues');
+    key.unbind('i', 'issues');
+    key.unbind('c', 'issues');
+    key.unbind('t', 'issues');
   }
 
   togglePopup = (popupName: string, open?: boolean) => {
@@ -118,30 +147,6 @@ export default class BaseIssue extends React.PureComponent {
     onFail(this.context.store.dispatch)(error);
   };
 
-  handleKeyPress = (e: Object) => {
-    const tagName = e.target.tagName.toUpperCase();
-    const shouldHandle = tagName !== 'INPUT' && tagName !== 'TEXTAREA' && tagName !== 'BUTTON';
-
-    if (shouldHandle) {
-      switch (e.key) {
-        case 'f':
-          return this.togglePopup('transition');
-        case 'a':
-          return this.togglePopup('assign');
-        case 'm':
-          return this.props.issue.actions.includes('assign_to_me') && this.handleAssignement('_me');
-        case 'p':
-          return this.togglePopup('plan');
-        case 'i':
-          return this.togglePopup('set-severity');
-        case 'c':
-          return this.togglePopup('comment');
-        case 't':
-          return this.togglePopup('edit-tags');
-      }
-    }
-  };
-
   render() {
     return (
       <IssueView
index 90ddd3a73c6f08249294964f82621eb6e8838d42..c6ca72d5747cde6b13db5b78eed894bafd24e4dc 100644 (file)
@@ -94,7 +94,10 @@ export default function IssueTitleBar(props: Props) {
                 <li className="issue-meta">
                   {onIssuesPage
                     ? locationsBadge
-                    : <Link onClick={stopPropagation} to={getSingleIssueUrl(issue.key)}>
+                    : <Link
+                        onClick={stopPropagation}
+                        target="_blank"
+                        to={getSingleIssueUrl(issue.key)}>
                         {locationsBadge}
                       </Link>}
                 </li>}
@@ -102,6 +105,7 @@ export default function IssueTitleBar(props: Props) {
                 <Link
                   className="js-issue-permalink icon-link"
                   onClick={stopPropagation}
+                  target="_blank"
                   to={getSingleIssueUrl(issue.key)}
                 />
               </li>
index a0f131c9fc5e61dd54562fe7aacb312273eeb252..d2de34493cbc12ffbade3548101712062a0a1f16 100644 (file)
@@ -49,11 +49,13 @@ exports[`test should render the titlebar correctly 1`] = `
               onClick={[Function]}
               onlyActiveOnIndex={false}
               style={Object {}}
+              target="_blank"
               to={
                 Object {
                   "pathname": "/issues",
                   "query": Object {
                     "issues": "AVsae-CQS-9G3txfbFN2",
+                    "open": "AVsae-CQS-9G3txfbFN2",
                   },
                 }
               } />
@@ -116,11 +118,13 @@ exports[`test should render the titlebar with the filter 1`] = `
               onClick={[Function]}
               onlyActiveOnIndex={false}
               style={Object {}}
+              target="_blank"
               to={
                 Object {
                   "pathname": "/issues",
                   "query": Object {
                     "issues": "AVsae-CQS-9G3txfbFN2",
+                    "open": "AVsae-CQS-9G3txfbFN2",
                   },
                 }
               } />
index df38baef06a7d0ec10cadd9798d423c97ee9ddd2..933a43c818e3cfd48f59e7e3f864b3e53668228c 100644 (file)
@@ -19,8 +19,6 @@
  */
 // @flow
 import React from 'react';
-import classNames from 'classnames';
-import { css } from 'glamor';
 import { debounce, map } from 'lodash';
 import Avatar from '../../../components/ui/Avatar';
 import BubblePopup from '../../../components/common/BubblePopup';
@@ -54,7 +52,6 @@ type State = {
 };
 
 const LIST_SIZE = 10;
-const USER_MARGIN = css({ marginLeft: '24px' });
 
 export default class SetAssigneePopup extends React.PureComponent {
   defaultUsersArray: Array<User>;
@@ -152,9 +149,8 @@ export default class SetAssigneePopup extends React.PureComponent {
                     size={16}
                   />}
                 <span
-                  className={classNames('vertical-middle', {
-                    [USER_MARGIN]: !(user.avatar || user.email)
-                  })}>
+                  className="vertical-middle"
+                  style={{ marginLeft: !user.avatar && !user.email ? 24 : undefined }}>
                   {user.name}
                 </span>
               </SelectListItem>
index 04b1a31ae288f5406758ea6a9d16a2a70e534655..ca155303323c37edcd92fd3558b50e59e2ddff70 100644 (file)
@@ -37,6 +37,7 @@ type State = {
 const LIST_SIZE = 10;
 
 export default class SetIssueTagsPopup extends React.PureComponent {
+  mounted: boolean;
   props: Props;
   state: State;
 
@@ -47,15 +48,22 @@ export default class SetIssueTagsPopup extends React.PureComponent {
   }
 
   componentDidMount() {
+    this.mounted = true;
     this.onSearch('');
   }
 
+  componentWillUnmount() {
+    this.mounted = false;
+  }
+
   onSearch = (query: string) => {
     searchIssueTags({
       q: query || '',
       ps: Math.min(this.props.selectedTags.length - 1 + LIST_SIZE, 100)
     }).then((tags: Array<string>) => {
-      this.setState({ searchResult: tags });
+      if (this.mounted) {
+        this.setState({ searchResult: tags });
+      }
     }, this.props.onFail);
   };
 
diff --git a/server/sonar-web/src/main/js/components/layout/Page.js b/server/sonar-web/src/main/js/components/layout/Page.js
deleted file mode 100644 (file)
index 4ae98a6..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { css } from 'glamor';
-
-type Props = {
-  className?: string,
-  children?: React.Element<*>
-};
-
-const styles = css({
-  display: 'flex',
-  alignItems: 'stretch',
-  width: '100%',
-  flexGrow: 1
-});
-
-export default function Page({ className, children, ...other }: Props) {
-  return (
-    <div className={styles + (className ? ` ${className}` : '')} {...other}>
-      {children}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/layout/PageFilters.js b/server/sonar-web/src/main/js/components/layout/PageFilters.js
deleted file mode 100644 (file)
index d5f181f..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { css } from 'glamor';
-
-type Props = {
-  children?: React.Element<*>
-};
-
-export default function PageSide(props: Props) {
-  return (
-    <div className={css({ width: 260, padding: 20 })}>
-      {props.children}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/layout/PageMain.js b/server/sonar-web/src/main/js/components/layout/PageMain.js
deleted file mode 100644 (file)
index 85a6305..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { css } from 'glamor';
-
-type Props = {
-  children?: React.Element<*>
-};
-
-export default function PageMain(props: Props) {
-  return (
-    <div className={css({ flexGrow: 1, minWidth: 740, padding: 20 })}>
-      {props.children}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/layout/PageMainInner.js b/server/sonar-web/src/main/js/components/layout/PageMainInner.js
deleted file mode 100644 (file)
index f4c07cd..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { css } from 'glamor';
-
-type Props = {
-  children?: React.Element<*>
-};
-
-export default function PageMainInner(props: Props) {
-  return (
-    <div className={css({ minWidth: 740, maxWidth: 980 })}>
-      {props.children}
-    </div>
-  );
-}
diff --git a/server/sonar-web/src/main/js/components/layout/PageSide.js b/server/sonar-web/src/main/js/components/layout/PageSide.js
deleted file mode 100644 (file)
index a647d83..0000000
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-// @flow
-import React from 'react';
-import { css, media } from 'glamor';
-
-type Props = {
-  children?: React.Element<*>,
-  top?: number
-};
-
-const width = css(
-  {
-    width: 'calc(50vw - 360px)'
-  },
-  media('(max-width: 1320px)', { width: 300 })
-);
-
-const sideStyles = css(width, {
-  flexGrow: 0,
-  flexShrink: 0,
-  backgroundColor: '#f3f3f3'
-});
-
-const sideStickyStyles = css(width, {
-  position: 'fixed',
-  zIndex: 40,
-  top: 0,
-  bottom: 0,
-  left: 0,
-  borderRight: '1px solid #e6e6e6',
-  overflowY: 'auto',
-  overflowX: 'hidden',
-  backgroundColor: '#f3f3f3'
-});
-
-const sideInnerStyles = css(
-  {
-    width: 300,
-    marginLeft: 'calc(50vw - 660px)',
-    backgroundColor: '#f3f3f3'
-  },
-  media('(max-width: 1320px)', { marginLeft: 0 })
-);
-
-export default function PageSide(props: Props) {
-  return (
-    <div className={sideStyles}>
-      <div className={`layout-page-side ${sideStickyStyles}`} style={{ top: props.top || 30 }}>
-        <div className={sideInnerStyles}>
-          {props.children}
-        </div>
-      </div>
-    </div>
-  );
-}
index 60b89079ad577d09c75c55ee3dc690d21a66948d..8f23cf7f327afc50b7fb89ed6657112b36f600e2 100644 (file)
@@ -37,13 +37,12 @@ const scrollElement = (element: HTMLElement, position: number) => {
 };
 
 let smoothScrollTop = (y: number, parent) => {
-  const scrollTop = getScrollPosition(parent);
+  let scrollTop = getScrollPosition(parent);
   const scrollingDown = y > scrollTop;
   const step = Math.ceil(Math.abs(y - scrollTop) / SCROLLING_STEPS);
   let stepsDone = 0;
 
   const interval = setInterval(() => {
-    const scrollTop = getScrollPosition(parent);
     if (scrollTop === y || SCROLLING_STEPS === stepsDone) {
       clearInterval(interval);
     } else {
@@ -54,6 +53,7 @@ let smoothScrollTop = (y: number, parent) => {
         goal = Math.max(y, scrollTop - step);
       }
       stepsDone++;
+      scrollTop = goal;
       scrollElement(parent, goal);
     }
   }, SCROLLING_INTERVAL);
@@ -63,23 +63,41 @@ smoothScrollTop = debounce(smoothScrollTop, SCROLLING_DURATION, { leading: true
 
 export const scrollToElement = (
   element: HTMLElement,
-  topOffset: number = 0,
-  bottomOffset: number = 0,
-  parent: HTMLElement = window
+  options: {
+    topOffset?: number,
+    bottomOffset?: number,
+    parent?: HTMLElement,
+    smooth?: boolean
+  }
 ) => {
+  const opts = { topOffset: 0, bottomOffset: 0, parent: window, smooth: true, ...options };
+  const { parent } = opts;
+
   const { top, bottom } = element.getBoundingClientRect();
+
   const scrollTop = getScrollPosition(parent);
+
   const height: number = parent === window
     ? window.innerHeight
     : parent.getBoundingClientRect().height;
 
   const parentTop = parent === window ? 0 : parent.getBoundingClientRect().top;
 
-  if (top - parentTop < topOffset) {
-    smoothScrollTop(scrollTop - topOffset + top - parentTop, parent);
+  if (top - parentTop < opts.topOffset) {
+    const goal = scrollTop - opts.topOffset + top - parentTop;
+    if (opts.smooth) {
+      smoothScrollTop(goal, parent);
+    } else {
+      scrollElement(parent, goal);
+    }
   }
 
-  if (bottom - parentTop > height - bottomOffset) {
-    smoothScrollTop(scrollTop + bottom - parentTop - height + bottomOffset, parent);
+  if (bottom - parentTop > height - opts.bottomOffset) {
+    const goal = scrollTop + bottom - parentTop - height + opts.bottomOffset;
+    if (opts.smooth) {
+      smoothScrollTop(goal, parent);
+    } else {
+      scrollElement(parent, goal);
+    }
   }
 };
index f8e127b89a0b76293508c40338964d99bab85687..ffa866797e79506edb83c9cc7defbf1e50dedc0a 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { stringify } from 'querystring';
 import { getProfilePath } from '../apps/quality-profiles/utils';
 
 /**
@@ -49,11 +50,16 @@ export function getComponentIssuesUrl(componentKey, query) {
   return { pathname: '/project/issues', query: { ...query, id: componentKey } };
 }
 
+export function getComponentIssuesUrlAsString(componentKey, query) {
+  const path = getComponentIssuesUrl(componentKey, query);
+  return `${window.baseUrl}${path.pathname}?${stringify(path.query)}`;
+}
+
 /**
  * Generate URL for a single issue
  */
 export function getSingleIssueUrl(issues) {
-  return { pathname: '/issues', query: { issues } };
+  return { pathname: '/issues', query: { issues, open: issues } };
 }
 
 /**
index b70943136737c02f07d2b96eba883ae95b9789a8..55cdcf4825c4d8ab2a0eeb345a42450c41736ae0 100644 (file)
@@ -26,7 +26,7 @@
   min-width: 10px;
   padding: 2px 7px;
   font-size: 11px;
-  font-weight: 300;
+  font-weight: normal;
   letter-spacing: 0.03em;
   color: @white;
   line-height: 12px;
 
   a& { .link-no-underline; }
 
+  &.is-rounded {
+    padding-left: 5px;
+    padding-right: 5px;
+    border-radius: 50px;
+  }
+
   .list-group-item > &,
   .list-group-item-heading > & {
     float: right;
index 7d8eae94131ef3f417ce83f78e81296416af8c38..2bc33802fc314e4518769fc64a34fc727715bdef 100644 (file)
@@ -39,6 +39,8 @@
   padding-bottom: @bottomPadding;
   border: 1px solid transparent;
   background-color: @issueBackgroundColor;
+  outline: none;
+  transition: border-color 0.3s ease;
 }
 
 .issue-list,
index 291d61c7fc861054234300c95bfe1bb8a24d98fb..9ec14a30229e6cc0ef87790159cfe90656b39e67 100644 (file)
@@ -174,6 +174,11 @@ ul.modal-head-metadata li {
     margin-top: 5px;
     margin-bottom: 4px;
   }
+
+  & > .icon-checkbox {
+    padding-top: 6px;
+    padding-right: 8px;
+  }
 }
 
 .modal-field {
index cd284d7a54650c0c9b375d23eb0e7b927d6105ef..6ab19d4d724abd81d3587cdc1d75ee23c2263754 100644 (file)
     }
   }
 }
+
+.layout-page {
+  display: flex;
+  align-items: stretch;
+  width: 100%;
+  flex-grow: 1;
+}
+
+.layout-page-filters {
+  width: 260px;
+  padding: 20px;
+}
+
+.layout-page-main {
+  flex-grow: 1;
+  min-width: 740px;
+  padding: 20px;
+}
+
+.layout-page-main-inner {
+  min-width: 740px;
+  max-width: 980px;
+}
+
+.layout-page-side-outer {
+  width: ~"calc(50vw - 360px)";
+  flex-grow: 0;
+  flex-shrink: 0;
+  background-color: #f3f3f3;
+}
+
+.layout-page-side {
+  position: fixed;
+  z-index: 40;
+  top: 30px;
+  bottom: 0;
+  left: 0;
+  width: ~"calc(50vw - 360px)";
+  border-right: 1px solid #e6e6e6;
+  overflow-y: auto;
+  overflow-x: hidden;
+  background-color: #f3f3f3;
+}
+
+.layout-page-side-inner {
+  width: 300px;
+  margin-left: ~"calc(50vw - 660px)";
+  background-color: #f3f3f3;
+}
+
+@media (max-width: 1320px) {
+  .layout-page-side-outer {
+    width: 300px;
+  }
+
+  .layout-page-side {
+    width: 300px;
+  }
+
+  .layout-page-side-inner {
+    margin-left: 0;
+  }
+}
\ No newline at end of file
index 8b0203bfd2a88c58818e312cbde01135e1b017f6..28b1bc89efc26f5e2c62784ccf28b1f64661b207 100644 (file)
   white-space: normal;
   overflow: hidden;
   font-size: 0;
+  cursor: not-allowed;
   transition: none;
 
   a& {
   font-weight: 600;
 }
 
+.search-navigator-facet-header-button {
+  float: right;
+  margin-top: 6px;
+}
+
 .search-navigator-facet-list {
   margin: 0 0 0 0;
   padding: 0 10px 10px;
index c6b9336153e95245ebe132e65c330b259c90741b..c5cd49130637d5985a6ba51889fdb76f40e9ae9c 100644 (file)
@@ -25,7 +25,7 @@
 
 
 .issues {
-
+  
   &.sticky {
 
     .issues-workspace-list,
   .search-navigator-facet-footer {
     padding: 0 0 10px 0;
   }
+
+  .issue-list {
+    /* no math, just a good guess */
+    min-width: 640px;
+    width: 800px;
+
+    @media (max-width: 1320px) {
+      & {
+        width: ~"calc(60vw - 40px)";
+      }
+    }
+  }
+
+  .issue {
+    cursor: pointer;
+
+    &:hover {
+      border-color: @issueBorderColor;
+    } 
+  }
 }
 
 .issues-workspace-list-component {
index 8b8ada2672c63f0ea29a20625ad586f89ecaa35d..30429ece861f6b4224eabb5a849c6ce30d957283 100644 (file)
@@ -814,16 +814,16 @@ babel-register@^6.22.0:
     mkdirp "^0.5.1"
     source-map-support "^0.4.2"
 
-babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.9.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
+babel-runtime@6.x, babel-runtime@^6.0.0, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.23.0, babel-runtime@^6.9.0:
+  version "6.23.0"
+  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
   dependencies:
     core-js "^2.4.0"
     regenerator-runtime "^0.10.0"
 
-babel-runtime@^6.23.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
+babel-runtime@^6.11.6, babel-runtime@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.22.0.tgz#1cf8b4ac67c77a4ddb0db2ae1f74de52ac4ca611"
   dependencies:
     core-js "^2.4.0"
     regenerator-runtime "^0.10.0"
@@ -2381,7 +2381,7 @@ fbjs@0.1.0-alpha.10:
     promise "^7.0.3"
     whatwg-fetch "^0.9.0"
 
-fbjs@^0.8.1, fbjs@^0.8.4, fbjs@^0.8.8:
+fbjs@^0.8.1, fbjs@^0.8.4:
   version "0.8.8"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6"
   dependencies:
@@ -2631,14 +2631,6 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
-glamor@2.20.24:
-  version "2.20.24"
-  resolved "https://repox.sonarsource.com/api/npm/npm/glamor/-/glamor-2.20.24.tgz#a299af2eec687322634ba38e4a0854d8743d2041"
-  dependencies:
-    babel-runtime "^6.18.0"
-    fbjs "^0.8.8"
-    object-assign "^4.1.0"
-
 glob-base@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
index 2ceec54e51987a1d0d12ad8922c7b838dbbab184..8a4e37c9af299c443814667a2c1d5b272ee9a6aa 100644 (file)
@@ -227,6 +227,7 @@ bulk_change=Bulk Change
 bulleted_point=Bulleted point
 check_project=Check project
 coding_rules=Rules
+clear=Clear
 clear_all_filters=Clear All Filters
 click_to_add_to_favorites=Click to add to favorites
 click_to_remove_from_favorites=Click to remove from favorites
@@ -675,7 +676,7 @@ issue.effort=Effort:
 issue.x_effort={0} effort
 issue.creation_date=Created
 issue.filter_similar_issues=Filter Similar Issues
-issue.this_issue_involves_x_code_locations=This issue involved {0} code locations
+issue.this_issue_involves_x_code_locations=This issue involves {0} code location(s)
 issues.return_to_list=Return to List
 issues.issues_limit_reached=For usability reasons, only the {0} issues are displayed.
 issues.bulk_change=All Issues ({0})
@@ -695,6 +696,7 @@ issues.issues=issues
 issues.to_select_issues=to select issues
 issues.to_navigate=to navigate
 issues.to_navigate_issue_locations=to navigate issue locations
+issues.to_switch_flows=to switch flows
 issues.leak_period=Leak Period
 issues.my_issues=My Issues