diff options
author | Stas Vilchik <vilchiks@gmail.com> | 2016-01-11 10:11:17 +0100 |
---|---|---|
committer | Stas Vilchik <vilchiks@gmail.com> | 2016-01-11 12:40:33 +0100 |
commit | 0ef5d6d8e1f265622bb7671b67de8a661316105c (patch) | |
tree | 72d5030d96dbb1e285a4e827731bfc9bf833abb5 /server/sonar-web/src/main/js/apps/code | |
parent | d27de6bf2ee1db24344362d11b78cd39b99a160c (diff) | |
download | sonarqube-0ef5d6d8e1f265622bb7671b67de8a661316105c.tar.gz sonarqube-0ef5d6d8e1f265622bb7671b67de8a661316105c.zip |
SONAR-7178 Provide keyboard shortcuts to navigate between search results
Diffstat (limited to 'server/sonar-web/src/main/js/apps/code')
5 files changed, 198 insertions, 69 deletions
diff --git a/server/sonar-web/src/main/js/apps/code/actions/index.js b/server/sonar-web/src/main/js/apps/code/actions/index.js index bc3515f64cb..3fa51d365aa 100644 --- a/server/sonar-web/src/main/js/apps/code/actions/index.js +++ b/server/sonar-web/src/main/js/apps/code/actions/index.js @@ -43,6 +43,8 @@ const METRICS_WITH_COVERAGE = [ export const INIT = 'INIT'; export const BROWSE = 'BROWSE'; export const SEARCH = 'SEARCH'; +export const SELECT_NEXT = 'SELECT_NEXT'; +export const SELECT_PREV = 'SELECT_PREV'; export const UPDATE_QUERY = 'UPDATE_QUERY'; export const START_FETCHING = 'START_FETCHING'; export const STOP_FETCHING = 'STOP_FETCHING'; @@ -80,6 +82,14 @@ export function updateQueryAction (query) { }; } +export function selectNext () { + return { type: SELECT_NEXT }; +} + +export function selectPrev () { + return { type: SELECT_PREV }; +} + export function startFetching () { return { type: START_FETCHING }; } @@ -210,3 +220,13 @@ export function search (query, baseComponent) { debouncedSearch(query, baseComponent, dispatch); }; } + +export function selectCurrent () { + return (dispatch, getState) => { + const { searchResults } = getState().current; + if (searchResults) { + const componentKey = getState().current.searchSelectedItem.key; + dispatch(browse(componentKey)); + } + }; +} diff --git a/server/sonar-web/src/main/js/apps/code/components/Code.js b/server/sonar-web/src/main/js/apps/code/components/Code.js index f2c61b51ec7..0a78c6e6d63 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Code.js +++ b/server/sonar-web/src/main/js/apps/code/components/Code.js @@ -65,7 +65,7 @@ class Code extends Component { const shouldShowSearchResults = !!searchResults; const shouldShowSourceViewer = !!sourceViewer; const shouldShowComponents = !shouldShowSearchResults && !shouldShowSourceViewer && components; - const shouldShowBreadcrumbs = !shouldShowSearchResults && Array.isArray(breadcrumbs) && breadcrumbs.length > 1; + const shouldShowBreadcrumbs = !shouldShowSearchResults && Array.isArray(breadcrumbs) && breadcrumbs.length > 1; const componentsClassName = classNames('spacer-top', { 'new-loading': fetching }); @@ -124,5 +124,15 @@ class Code extends Component { } export default connect(state => { - return Object.assign({ routing: state.routing }, state.current); + return { + routing: state.routing, + fetching: state.current.fetching, + baseComponent: state.current.baseComponent, + components: state.current.components, + breadcrumbs: state.current.breadcrumbs, + sourceViewer: state.current.sourceViewer, + coverageMetric: state.current.coverageMetric, + searchResults: state.current.searchResults, + errorMessage: state.current.errorMessage + }; })(Code); diff --git a/server/sonar-web/src/main/js/apps/code/components/Component.js b/server/sonar-web/src/main/js/apps/code/components/Component.js index 205af9cef65..4636da3028b 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Component.js +++ b/server/sonar-web/src/main/js/apps/code/components/Component.js @@ -17,7 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import classNames from 'classnames'; import React from 'react'; +import ReactDOM from 'react-dom'; +import { connect } from 'react-redux'; import ComponentName from './ComponentName'; import ComponentMeasure from './ComponentMeasure'; @@ -25,74 +28,116 @@ import ComponentDetach from './ComponentDetach'; import ComponentPin from './ComponentPin'; -const Component = ({ component, previous, coverageMetric, onBrowse }) => { - let componentAction = null; +const TOP_OFFSET = 200; +const BOTTOM_OFFSET = 10; - switch (component.qualifier) { - case 'FIL': - case 'UTS': - componentAction = <ComponentPin component={component}/>; - break; - default: - componentAction = <ComponentDetach component={component}/>; + +class Component extends React.Component { + componentDidMount () { + this.handleUpdate(); } - return ( - <tr> - <td className="thin nowrap"> - <span className="spacer-right"> - {componentAction} - </span> - </td> - <td className="code-name-cell"> - <ComponentName - component={component} - previous={previous} - onBrowse={onBrowse}/> - </td> - <td className="thin nowrap text-right"> - <div className="code-components-cell"> - <ComponentMeasure - component={component} - metricKey="ncloc" - metricType="SHORT_INT"/> - </div> - </td> - <td className="thin nowrap text-right"> - <div className="code-components-cell"> - <ComponentMeasure - component={component} - metricKey="sqale_index" - metricType="SHORT_WORK_DUR"/> - </div> - </td> - <td className="thin nowrap text-right"> - <div className="code-components-cell"> - <ComponentMeasure - component={component} - metricKey="violations" - metricType="SHORT_INT"/> - </div> - </td> - <td className="thin nowrap text-right"> - <div className="code-components-cell"> - <ComponentMeasure - component={component} - metricKey={coverageMetric} - metricType="PERCENT"/> - </div> - </td> - <td className="thin nowrap text-right"> - <div className="code-components-cell"> - <ComponentMeasure + componentDidUpdate () { + this.handleUpdate(); + } + + handleUpdate () { + const { selected } = this.props; + if (selected) { + setTimeout(() => { + this.handleScroll(); + }, 0); + } + } + + handleScroll () { + const node = ReactDOM.findDOMNode(this); + const position = node.getBoundingClientRect(); + const { top, bottom } = position; + if (bottom > window.innerHeight - BOTTOM_OFFSET) { + window.scrollTo(0, bottom - window.innerHeight + window.scrollY + BOTTOM_OFFSET); + } else if (top < TOP_OFFSET) { + window.scrollTo(0, top + window.scrollY - TOP_OFFSET); + } + } + + render () { + const { component, selected, previous, coverageMetric, onBrowse } = this.props; + + let componentAction = null; + + switch (component.qualifier) { + case 'FIL': + case 'UTS': + componentAction = <ComponentPin component={component}/>; + break; + default: + componentAction = <ComponentDetach component={component}/>; + } + + return ( + <tr className={classNames({ 'selected': selected })}> + <td className="thin nowrap"> + <span className="spacer-right"> + {componentAction} + </span> + </td> + <td className="code-name-cell"> + <ComponentName component={component} - metricKey="duplicated_lines_density" - metricType="PERCENT"/> - </div> - </td> - </tr> - ); -}; + previous={previous} + onBrowse={onBrowse}/> + </td> + <td className="thin nowrap text-right"> + <div className="code-components-cell"> + <ComponentMeasure + component={component} + metricKey="ncloc" + metricType="SHORT_INT"/> + </div> + </td> + <td className="thin nowrap text-right"> + <div className="code-components-cell"> + <ComponentMeasure + component={component} + metricKey="sqale_index" + metricType="SHORT_WORK_DUR"/> + </div> + </td> + <td className="thin nowrap text-right"> + <div className="code-components-cell"> + <ComponentMeasure + component={component} + metricKey="violations" + metricType="SHORT_INT"/> + </div> + </td> + <td className="thin nowrap text-right"> + <div className="code-components-cell"> + <ComponentMeasure + component={component} + metricKey={coverageMetric} + metricType="PERCENT"/> + </div> + </td> + <td className="thin nowrap text-right"> + <div className="code-components-cell"> + <ComponentMeasure + component={component} + metricKey="duplicated_lines_density" + metricType="PERCENT"/> + </div> + </td> + </tr> + ); + } +} + +function mapStateToProps (state, ownProps) { + return { + selected: state.current.searchSelectedItem === ownProps.component + }; +} -export default Component; +export default connect(mapStateToProps)(Component); diff --git a/server/sonar-web/src/main/js/apps/code/components/Search.js b/server/sonar-web/src/main/js/apps/code/components/Search.js index 2a6c602926f..e29efa62600 100644 --- a/server/sonar-web/src/main/js/apps/code/components/Search.js +++ b/server/sonar-web/src/main/js/apps/code/components/Search.js @@ -20,7 +20,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; -import { search } from '../actions'; +import { search, selectCurrent, selectNext, selectPrev } from '../actions'; class Search extends Component { @@ -28,6 +28,26 @@ class Search extends Component { this.refs.input.focus(); } + handleKeyDown (e) { + const { dispatch } = this.props; + switch (e.keyCode) { + case 13: + e.preventDefault(); + dispatch(selectCurrent()); + break; + case 38: + e.preventDefault(); + dispatch(selectPrev()); + break; + case 40: + e.preventDefault(); + dispatch(selectNext()); + break; + default: + // do nothing + } + } + handleSearch (e) { e.preventDefault(); const { dispatch, component } = this.props; @@ -47,6 +67,7 @@ class Search extends Component { </button> <input ref="input" + onKeyDown={this.handleKeyDown.bind(this)} onChange={this.handleSearch.bind(this)} value={query} className="search-box-input" diff --git a/server/sonar-web/src/main/js/apps/code/reducers/index.js b/server/sonar-web/src/main/js/apps/code/reducers/index.js index 2c8b31a1ebb..0e697778d32 100644 --- a/server/sonar-web/src/main/js/apps/code/reducers/index.js +++ b/server/sonar-web/src/main/js/apps/code/reducers/index.js @@ -19,7 +19,8 @@ */ import _ from 'underscore'; -import { INIT, BROWSE, SEARCH, UPDATE_QUERY, START_FETCHING, STOP_FETCHING, RAISE_ERROR } from '../actions'; +import { INIT, BROWSE, SEARCH, UPDATE_QUERY, SELECT_NEXT, SELECT_PREV, START_FETCHING, STOP_FETCHING, + RAISE_ERROR } from '../actions'; function hasSourceCode (component) { @@ -68,6 +69,25 @@ function sortChildren (children) { return temp; } +function getNext (element, list) { + if (list) { + const length = list.length; + const index = list.indexOf(element); + return index < length - 1 ? list[index + 1] : element; + } else { + return element; + } +} + +function getPrev (element, list) { + if (list) { + const index = list.indexOf(element); + return index > 0 ? list[index - 1] : element; + } else { + return element; + } +} + export const initialState = { fetching: false, @@ -77,6 +97,7 @@ export const initialState = { sourceViewer: null, searchResults: null, searchQuery: '', + searchSelectedItem: null, coverageMetric: null, baseBreadcrumbs: [], errorMessage: null @@ -105,17 +126,29 @@ export function current (state = initialState, action) { sourceViewer, searchResults: null, searchQuery: '', + searchSelectedItem: null, errorMessage: null }; case SEARCH: return { ...state, searchResults: action.components, + searchSelectedItem: _.first(action.components), sourceViewer: null, errorMessage: null }; case UPDATE_QUERY: return { ...state, searchQuery: action.query }; + case SELECT_NEXT: + return { + ...state, + searchSelectedItem: getNext(state.searchSelectedItem, state.searchResults) + }; + case SELECT_PREV: + return { + ...state, + searchSelectedItem: getPrev(state.searchSelectedItem, state.searchResults) + }; case START_FETCHING: return { ...state, fetching: true }; case STOP_FETCHING: |