"numeral": "1.5.3",
"postcss-loader": "0.8.0",
"react": "0.14.2",
+ "react-addons-perf": "0.14.2",
"react-addons-test-utils": "0.14.2",
"react-dom": "0.14.2",
"react-redux": "4.0.1",
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';
};
}
+export function selectNext () {
+ return { type: SELECT_NEXT };
+}
+
+export function selectPrev () {
+ return { type: SELECT_PREV };
+}
+
export function startFetching () {
return { type: START_FETCHING };
}
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));
+ }
+ };
+}
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 });
}
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);
* 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';
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);
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 {
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;
</button>
<input
ref="input"
+ onKeyDown={this.handleKeyDown.bind(this)}
onChange={this.handleSearch.bind(this)}
value={query}
className="search-box-input"
*/
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) {
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,
sourceViewer: null,
searchResults: null,
searchQuery: '',
+ searchSelectedItem: null,
coverageMetric: null,
baseBreadcrumbs: [],
errorMessage: null
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:
background-color: #f5f5f5;
}
+table.data.zebra > tbody > tr.selected {
+ background-color: @lightBlue !important;
+}
+
.data thead tr.total {
background-color: #EFEFEF;
font-weight: normal;
browseAction,
searchAction,
updateQueryAction,
+ selectNext,
+ selectPrev,
startFetching,
stopFetching,
raiseError
const exampleComponent = { key: 'A' };
+const exampleComponent2 = { key: 'B' };
const exampleComponents = [
{ key: 'B' },
{ key: 'C' }
.to.equal('');
});
});
+ describe('searchSelectedItem', () => {
+ it('should be set to the first result', () => {
+ const results = [exampleComponent, exampleComponent2];
+ expect(current(initialState, searchAction(results)).searchSelectedItem)
+ .to.equal(exampleComponent);
+ });
+
+ it('should select next', () => {
+ const results = [exampleComponent, exampleComponent2];
+ const stateBefore = current(initialState, searchAction(results));
+ const stateAfter = current(stateBefore, selectNext());
+ expect(stateAfter.searchSelectedItem)
+ .to.equal(exampleComponent2);
+ });
+
+ it('should not select next', () => {
+ const results = [exampleComponent, exampleComponent2];
+ const stateBefore = Object.assign({}, current(initialState, searchAction(results)), {
+ searchSelectedItem: exampleComponent2
+ });
+ expect(current(stateBefore, selectNext()).searchSelectedItem)
+ .to.equal(exampleComponent2);
+ });
+
+ it('should select prev', () => {
+ const results = [exampleComponent, exampleComponent2];
+ const stateBefore = Object.assign({}, current(initialState, searchAction(results)), {
+ searchSelectedItem: exampleComponent2
+ });
+ expect(current(stateBefore, selectPrev()).searchSelectedItem)
+ .to.equal(exampleComponent);
+ });
+
+ it('should not select prev', () => {
+ const results = [exampleComponent, exampleComponent2];
+ const stateBefore = current(initialState, searchAction(results));
+ expect(current(stateBefore, selectPrev()).searchSelectedItem)
+ .to.equal(exampleComponent);
+ });
+
+ it('should ignore if no results', () => {
+ expect(current(initialState, selectNext()).searchSelectedItem)
+ .to.be.null;
+ expect(current(initialState, selectPrev()).searchSelectedItem)
+ .to.be.null;
+ });
+
+ it('should be reset on browse', () => {
+ const results = [exampleComponent, exampleComponent2];
+ const stateBefore = current(initialState, searchAction(results));
+ const stateAfter = current(stateBefore, browseAction(exampleComponent));
+ expect(stateAfter.searchSelectedItem)
+ .to.be.null;
+ });
+ });
describe('errorMessage', () => {
it('should be set', () => {
expect(current(initialState, raiseError('error!')).errorMessage)