diff options
Diffstat (limited to 'server/sonar-web/src/main/js')
69 files changed, 1076 insertions, 890 deletions
diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js index 644c05296ff..d3200c08918 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.js @@ -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 ( diff --git a/server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs b/server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs index 92d2c37a85c..00f5519d887 100644 --- a/server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs +++ b/server/sonar-web/src/main/js/app/components/nav/templates/nav-shortcuts-help.hbs @@ -26,8 +26,7 @@ <h3 class="shortcuts-section-title">{{t 'shortcuts.section.rules'}}</h3> <ul class="shortcuts-list"> - <li><span class="shortcut-button">↑</span> <span - class="shortcut-button">↓</span> {{t 'shortcuts.section.rules.navigate_between_rules'}}</li> + <li><span class="shortcut-button">↑</span> <span class="shortcut-button">↓</span> {{t 'shortcuts.section.rules.navigate_between_rules'}}</li> <li><span class="shortcut-button">→</span> {{t 'shortcuts.section.rules.open_details'}}</li> <li><span class="shortcut-button">←</span> {{t 'shortcuts.section.rules.return_to_list'}}</li> <li><span class="shortcut-button">a</span> {{t 'shortcuts.section.rules.activate'}}</li> @@ -38,12 +37,22 @@ <div class="column-half"> <h3 class="shortcuts-section-title">{{t 'shortcuts.section.issues'}}</h3> <ul class="shortcuts-list"> - <li><span class="shortcut-button">↑</span> <span - class="shortcut-button">↓</span> {{t 'shortcuts.section.issues.navigate_between_issues'}} + <li><span class="shortcut-button">↑</span> <span class="shortcut-button">↓</span> {{t 'shortcuts.section.issues.navigate_between_issues'}} </li> <li><span class="shortcut-button">→</span> {{t 'shortcuts.section.issues.open_details'}}</li> <li><span class="shortcut-button">←</span> {{t 'shortcuts.section.issues.return_to_list'}}</li> - <li><span class="shortcut-button">⎵</span> {{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> {{t 'shortcuts.section.issue.do_transition'}}</li> <li><span class="shortcut-button">a</span> {{t 'shortcuts.section.issue.assign'}}</li> <li><span class="shortcut-button">m</span> {{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 diff --git a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js index 384270c2919..b6ec74ca5a0 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js +++ b/server/sonar-web/src/main/js/apps/coding-rules/rule/rule-issues-view.js @@ -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 }) diff --git a/server/sonar-web/src/main/js/apps/issues/actions.js b/server/sonar-web/src/main/js/apps/issues/actions.js index 610628b15ff..2bde7c95332 100644 --- a/server/sonar-web/src/main/js/apps/issues/actions.js +++ b/server/sonar-web/src/main/js/apps/issues/actions.js @@ -20,12 +20,16 @@ // @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 }; + } +}; diff --git a/server/sonar-web/src/main/js/apps/issues/components/App.js b/server/sonar-web/src/main/js/apps/issues/components/App.js index 1b7f6a9f952..a6745fa2add 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/App.js +++ b/server/sonar-web/src/main/js/apps/issues/components/App.js @@ -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> ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js index 63b4680c2fe..057eb7e7cd4 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js +++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.js @@ -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) => ( diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js index e11340fc943..0fd09abce63 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.js @@ -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} />} diff --git a/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js index 9740f63e718..85868906179 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js +++ b/server/sonar-web/src/main/js/apps/issues/components/FiltersHeader.js @@ -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> diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js index 9c321fb007c..947be331377 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesSourceViewer.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js index 5c674e86355..7812c0dee9a 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js +++ b/server/sonar-web/src/main/js/apps/issues/components/MyIssuesFilter.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js index dcefa0a7d1b..d8591e0e6a3 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.js +++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.js @@ -19,13 +19,14 @@ */ // @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/conciseIssuesList/ReloadButton.js b/server/sonar-web/src/main/js/apps/issues/components/ReloadButton.js index 0034fad7d48..09b5a0d2069 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ReloadButton.js +++ b/server/sonar-web/src/main/js/apps/issues/components/ReloadButton.js @@ -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 ReloadButton(props: Props) { }; return ( - <a - className={classNames('concise-issues-list-header-button', props.className)} - href="#" - onClick={handleClick}> - {icon} - </a> + <Tooltip overlay={translate('reload')}> + <a + className={classNames('concise-issues-list-header-button', props.className)} + href="#" + onClick={handleClick}> + {icon} + </a> + </Tooltip> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js index f621e0a83ea..a4424b47942 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/BackButton.js @@ -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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js index bff17414951..922379d0a7f 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssueBox.js @@ -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"> diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js index 18e9018a6f5..fba7825a6b4 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesList.js @@ -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 }); } }; diff --git a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js index 2be4b44732e..e6a5f1c24b3 100644 --- a/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js +++ b/server/sonar-web/src/main/js/apps/issues/conciseIssuesList/ConciseIssuesListHeader.js @@ -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/sidebar/AssigneeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js index 05479435763..09609eabe9d 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.js @@ -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) { @@ -133,35 +137,49 @@ export default class AssigneeFacet extends React.PureComponent { ); 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js index d539229ff78..e0f85562ef9 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/AuthorFacet.js @@ -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) { @@ -71,28 +75,34 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js index d67b204f3cd..56e1328bc4f 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.js @@ -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()} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js index ddd82db1eab..c41bad19312 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/DirectoryFacet.js @@ -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) { @@ -92,28 +96,34 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js index 5d914f8380d..48b33e83586 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/FileFacet.js @@ -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) { @@ -88,28 +92,34 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js index 411c9b74d2e..c2c8592c5ca 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/LanguageFacet.js @@ -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) { @@ -84,30 +88,43 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js index 8711e017462..35e805a54bc 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ModuleFacet.js @@ -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) { @@ -85,28 +89,34 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js index 960a5bb1739..0fe8bc84ffb 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ProjectFacet.js @@ -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) { @@ -126,36 +130,50 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js index d83c56cd917..c905c98c2b8 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.js @@ -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 && diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js index ae9879d59f2..f28e7de47b0 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/RuleFacet.js @@ -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) { @@ -96,31 +100,43 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js index e95f44008d0..8b4edb49672 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.js @@ -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 && diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js index 613c3be57ed..5d9d8988c02 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.js @@ -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} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js index 40bfb251141..14f4a09fc63 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.js @@ -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 && diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js index 120eb8d890b..91fc77c5f54 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TagFacet.js @@ -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) { @@ -93,31 +97,44 @@ 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> ); } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js index c0eb0271058..f8b7630b834 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.js @@ -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 && diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js index 5dc2230f4e3..2ce4f76254c 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js index a55c903b90c..7c829b2b32b 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap index 80726a28b0b..d0de2e5b3cf 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.js.snap @@ -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} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap index 81d6ce875fc..03bc82ca440 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/Sidebar-test.js.snap @@ -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", ] diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js index ff6ef8387c9..19b720e9f7e 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/FacetHeader.js @@ -20,25 +20,34 @@ // @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> + ); } } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js index 5aa00c4a41e..ed3f143eedc 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/FacetHeader-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap index 3333ae8944d..e50a3519a42 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/components/__tests__/__snapshots__/FacetHeader-test.js.snap @@ -1,132 +1,171 @@ -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> `; diff --git a/server/sonar-web/src/main/js/apps/issues/styles.css b/server/sonar-web/src/main/js/apps/issues/styles.css index d035084daef..d40c2b9d90b 100644 --- a/server/sonar-web/src/main/js/apps/issues/styles.css +++ b/server/sonar-web/src/main/js/apps/issues/styles.css @@ -39,6 +39,12 @@ } } +.issues-main-header-spinner { + margin-left: 1px; + margin-right: 9px; + margin-top: -1px; +} + .concise-issues-list-header, .concise-issues-list-header-inner { } @@ -88,9 +94,11 @@ 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 } @@ -102,6 +110,8 @@ } .concise-issue-box-message { + overflow: hidden; + text-overflow: ellipsis; font-weight: bold; } @@ -129,4 +139,21 @@ 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 diff --git a/server/sonar-web/src/main/js/apps/issues/utils.js b/server/sonar-web/src/main/js/apps/issues/utils.js index b37344e37d5..1ad58e08816 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.js +++ b/server/sonar-web/src/main/js/apps/issues/utils.js @@ -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 = { diff --git a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js index a8fd9245089..add7d2bd892 100644 --- a/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js +++ b/server/sonar-web/src/main/js/apps/projects/components/AllProjects.js @@ -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> ); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js index c7cc3387c2a..8f29e6a59c7 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerBase.js @@ -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 }); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js index d97bc5a3b4a..d44df72f472 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.js @@ -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} diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js index 30ba67f06f3..01ce80da9e9 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/Line.js @@ -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> ); } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js index 73b0da7db15..f069427afe0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineCoverage.js @@ -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; } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js index 4edaca1c4c8..ee1373fdde5 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplicationBlock.js @@ -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; } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js index 5f0a2936859..85cc046b1c8 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/LineDuplications.js @@ -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; } } diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js index aacd16d7866..b657ec6e493 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineCoverage-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js index cd0baf595d0..9075badf989 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplicationBlock-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js index b2aa124a64a..52c22486bd0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineDuplications-test.js @@ -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(); }); diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap index ccf5c4d3c4f..d9fc3840499 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineCoverage-test.js.snap @@ -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> `; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap index b94d4b3bc09..ee28d4ae2fb 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplicationBlock-test.js.snap @@ -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`] = ` diff --git a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap index 7e977c88442..ebf65159849 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineDuplications-test.js.snap @@ -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 index 00000000000..2ad32dd05e7 --- /dev/null +++ b/server/sonar-web/src/main/js/components/common/EmptySearch.css @@ -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 diff --git a/server/sonar-web/src/main/js/components/common/EmptySearch.js b/server/sonar-web/src/main/js/components/common/EmptySearch.js index 904a6b2cbad..719a2239c90 100644 --- a/server/sonar-web/src/main/js/components/common/EmptySearch.js +++ b/server/sonar-web/src/main/js/components/common/EmptySearch.js @@ -19,18 +19,11 @@ */ // @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> diff --git a/server/sonar-web/src/main/js/components/common/SelectList.js b/server/sonar-web/src/main/js/components/common/SelectList.js index d5695f82a22..cbfadc9307a 100644 --- a/server/sonar-web/src/main/js/components/common/SelectList.js +++ b/server/sonar-web/src/main/js/components/common/SelectList.js @@ -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) => { diff --git a/server/sonar-web/src/main/js/components/controls/Checkbox.js b/server/sonar-web/src/main/js/components/controls/Checkbox.js index 5762cff7c6b..57a5d6969a5 100644 --- a/server/sonar-web/src/main/js/components/controls/Checkbox.js +++ b/server/sonar-web/src/main/js/components/controls/Checkbox.js @@ -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 }); diff --git a/server/sonar-web/src/main/js/components/issue/Issue.js b/server/sonar-web/src/main/js/components/issue/Issue.js index f45671c6f74..cd44f80e5b7 100644 --- a/server/sonar-web/src/main/js/components/issue/Issue.js +++ b/server/sonar-web/src/main/js/components/issue/Issue.js @@ -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 diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js index 90ddd3a73c6..c6ca72d5747 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTitleBar.js @@ -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> diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap index a0f131c9fc5..d2de34493cb 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueTitleBar-test.js.snap @@ -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", }, } } /> diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js index df38baef06a..933a43c818e 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetAssigneePopup.js @@ -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> diff --git a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js index 04b1a31ae28..ca155303323 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js +++ b/server/sonar-web/src/main/js/components/issue/popups/SetIssueTagsPopup.js @@ -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 index 4ae98a6918f..00000000000 --- a/server/sonar-web/src/main/js/components/layout/Page.js +++ /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 index d5f181ffc35..00000000000 --- a/server/sonar-web/src/main/js/components/layout/PageFilters.js +++ /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 index 85a63058139..00000000000 --- a/server/sonar-web/src/main/js/components/layout/PageMain.js +++ /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 index f4c07cd9c41..00000000000 --- a/server/sonar-web/src/main/js/components/layout/PageMainInner.js +++ /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 index a647d83c0c1..00000000000 --- a/server/sonar-web/src/main/js/components/layout/PageSide.js +++ /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> - ); -} diff --git a/server/sonar-web/src/main/js/helpers/scrolling.js b/server/sonar-web/src/main/js/helpers/scrolling.js index 60b89079ad5..8f23cf7f327 100644 --- a/server/sonar-web/src/main/js/helpers/scrolling.js +++ b/server/sonar-web/src/main/js/helpers/scrolling.js @@ -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); + } } }; diff --git a/server/sonar-web/src/main/js/helpers/urls.js b/server/sonar-web/src/main/js/helpers/urls.js index f8e127b89a0..ffa866797e7 100644 --- a/server/sonar-web/src/main/js/helpers/urls.js +++ b/server/sonar-web/src/main/js/helpers/urls.js @@ -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 } }; } /** |