aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/code
diff options
context:
space:
mode:
authorStas Vilchik <vilchiks@gmail.com>2016-01-11 10:11:17 +0100
committerStas Vilchik <vilchiks@gmail.com>2016-01-11 12:40:33 +0100
commit0ef5d6d8e1f265622bb7671b67de8a661316105c (patch)
tree72d5030d96dbb1e285a4e827731bfc9bf833abb5 /server/sonar-web/src/main/js/apps/code
parentd27de6bf2ee1db24344362d11b78cd39b99a160c (diff)
downloadsonarqube-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')
-rw-r--r--server/sonar-web/src/main/js/apps/code/actions/index.js20
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Code.js14
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Component.js175
-rw-r--r--server/sonar-web/src/main/js/apps/code/components/Search.js23
-rw-r--r--server/sonar-web/src/main/js/apps/code/reducers/index.js35
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: