aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps
diff options
context:
space:
mode:
authorGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-08 16:29:03 +0200
committerGrégoire Aubert <gregoire.aubert@sonarsource.com>2017-08-14 11:44:44 +0200
commitec01b0f2e37d0ac1aa9107c1abde9fde7ef7f9b0 (patch)
tree2f75421759d92e6feedc8477df60f90f2d69aa5a /server/sonar-web/src/main/js/apps
parent5bda516e7f849d1e5d32df47c689f8645955bae7 (diff)
downloadsonarqube-ec01b0f2e37d0ac1aa9107c1abde9fde7ef7f9b0.tar.gz
sonarqube-ec01b0f2e37d0ac1aa9107c1abde9fde7ef7f9b0.zip
SONAR-9614 Add keyboard shortcuts on project measures page
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/App.js4
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js17
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js20
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js1
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap1
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js10
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js8
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js117
-rw-r--r--server/sonar-web/src/main/js/apps/component-measures/style.css4
9 files changed, 151 insertions, 31 deletions
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/App.js b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
index 2a4e2ad6b62..a71b9e69b73 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/App.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/App.js
@@ -20,6 +20,7 @@
// @flow
import React from 'react';
import Helmet from 'react-helmet';
+import key from 'keymaster';
import MeasureContentContainer from './MeasureContentContainer';
import MeasureOverviewContainer from './MeasureOverviewContainer';
import Sidebar from '../sidebar/Sidebar';
@@ -71,7 +72,7 @@ export default class App extends React.PureComponent {
this.mounted = true;
this.props.fetchMetrics();
this.fetchMeasures(this.props);
-
+ key.setScope('measures-files');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.add('search-navigator-footer');
@@ -89,6 +90,7 @@ export default class App extends React.PureComponent {
componentWillUnmount() {
this.mounted = false;
+ key.deleteScope('measures-files');
const footer = document.getElementById('footer');
if (footer) {
footer.classList.remove('search-navigator-footer');
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
index a9b9c3ecbcf..6edb170b06e 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/Breadcrumbs.js
@@ -19,6 +19,7 @@
*/
// @flow
import React from 'react';
+import key from 'keymaster';
import Breadcrumb from './Breadcrumb';
import { getBreadcrumbs } from '../../../api/components';
import type { Component } from '../types';
@@ -44,6 +45,7 @@ export default class Breadcrumbs extends React.PureComponent {
componentDidMount() {
this.mounted = true;
this.fetchBreadcrumbs(this.props);
+ this.attachShortcuts();
}
componentWillReceiveProps(nextProps: Props) {
@@ -54,6 +56,21 @@ export default class Breadcrumbs extends React.PureComponent {
componentWillUnmount() {
this.mounted = false;
+ this.detachShortcuts();
+ }
+
+ attachShortcuts() {
+ key('left', 'measures-files', () => {
+ const { breadcrumbs } = this.state;
+ if (breadcrumbs.length > 1) {
+ this.props.handleSelect(breadcrumbs[breadcrumbs.length - 2].key);
+ }
+ return false;
+ });
+ }
+
+ detachShortcuts() {
+ key.unbind('left', 'measures-files');
}
fetchBreadcrumbs = ({ component, rootComponent }: Props) => {
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
index dbfd60547a0..66e9fe47756 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureContent.js
@@ -58,6 +58,7 @@ type State = {
components: Array<ComponentEnhanced>,
metric: ?Metric,
paging?: Paging,
+ selected: ?string,
view: ?string
};
@@ -68,6 +69,7 @@ export default class MeasureContent extends React.PureComponent {
components: [],
metric: null,
paging: null,
+ selected: null,
view: null
};
@@ -86,6 +88,13 @@ export default class MeasureContent extends React.PureComponent {
this.mounted = false;
}
+ getSelectedIndex = (): ?number => {
+ const index = this.state.components.findIndex(
+ component => component.key === this.state.selected
+ );
+ return index !== -1 ? index : null;
+ };
+
getComponentRequestParams = (view: string, metric: Metric, options: Object = {}) => {
const strategy = view === 'list' ? 'leaves' : 'children';
const metricKeys = [metric.key];
@@ -127,6 +136,7 @@ export default class MeasureContent extends React.PureComponent {
),
metric,
paging: r.paging,
+ selected: r.components.length > 0 ? r.components[0].key : null,
view
});
}
@@ -168,6 +178,8 @@ export default class MeasureContent extends React.PureComponent {
);
};
+ onSelectComponent = (component: string) => this.setState({ selected: component });
+
renderContent() {
const { component, leakPeriod } = this.props;
@@ -201,14 +213,18 @@ export default class MeasureContent extends React.PureComponent {
}
if (['list', 'tree'].includes(view)) {
+ const selectedIdx = this.getSelectedIndex();
return (
<FilesView
components={this.state.components}
fetchMore={this.fetchMoreComponents}
- handleSelect={this.props.updateSelected}
+ handleOpen={this.props.updateSelected}
+ handleSelect={this.onSelectComponent}
metric={metric}
metrics={this.props.metrics}
paging={this.state.paging}
+ selectedKey={selectedIdx != null ? this.state.selected : null}
+ selectedIdx={selectedIdx}
/>
);
}
@@ -253,7 +269,7 @@ export default class MeasureContent extends React.PureComponent {
view={view}
/>}
<PageActions
- current={this.state.components.length}
+ current={this.getSelectedIndex() + 1}
loading={this.props.loading}
isFile={isFile}
paging={this.state.paging}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js
index 42b22ac2213..ec33171709a 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/MeasureViewSelect.js
@@ -82,6 +82,7 @@ export default class MeasureViewSelect extends React.PureComponent {
render() {
return (
<Select
+ autoBlur={true}
className={this.props.className}
clearable={false}
searchable={false}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap
index 7d51b2dd127..7ab0276dde2 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap
+++ b/server/sonar-web/src/main/js/apps/component-measures/components/__tests__/__snapshots__/MeasureViewSelect-test.js.snap
@@ -4,6 +4,7 @@ exports[`should display correctly with treemap option 1`] = `
<Select
addLabelText="Add \\"{label}\\"?"
arrowRenderer={[Function]}
+ autoBlur={true}
autosize={true}
backspaceRemoves={true}
backspaceToRemoveMessage="Press backspace to remove {label}"
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
index 6b0850ec74e..bb6cadb2b74 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentCell.js
@@ -19,7 +19,6 @@
*/
// @flow
import React from 'react';
-import classNames from 'classnames';
import QualifierIcon from '../../../components/shared/QualifierIcon';
import { splitPath } from '../../../helpers/path';
import { getComponentUrl } from '../../../helpers/urls';
@@ -27,7 +26,6 @@ import type { Component } from '../types';
type Props = {
component: Component,
- isSelected: boolean,
onClick: string => void
};
@@ -69,24 +67,20 @@ export default class ComponentCell extends React.PureComponent {
render() {
const { component } = this.props;
- const linkClassName = classNames('link-no-underline', {
- selected: this.props.isSelected
- });
-
return (
<td className="measure-details-component-cell">
<div className="text-ellipsis">
{component.refId == null
? <a
id={'component-measures-component-link-' + component.key}
- className={linkClassName}
+ className="link-no-underline"
href={getComponentUrl(component.key)}
onClick={this.handleClick}>
{this.renderInner()}
</a>
: <a
id={'component-measures-component-link-' + component.key}
- className={linkClassName}
+ className="link-no-underline"
href={getComponentUrl(component.refKey || component.key)}>
<span className="big-spacer-right">
<i className="icon-detach" />
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js
index 29613eb6b1e..9b813b15ba3 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/ComponentsListRow.js
@@ -19,6 +19,7 @@
*/
// @flow
import React from 'react';
+import classNames from 'classnames';
import ComponentCell from './ComponentCell';
import MeasureCell from './MeasureCell';
import type { Component } from '../types';
@@ -38,9 +39,12 @@ export default function ComponentsListRow(props: Props) {
const measure = component.measures.find(measure => measure.metric === metric.key);
return { ...measure, metric };
});
+ const rowClass = classNames('measure-details-component-row', {
+ selected: props.isSelected
+ });
return (
- <tr>
- <ComponentCell component={component} isSelected={props.isSelected} onClick={props.onClick} />
+ <tr className={rowClass}>
+ <ComponentCell component={component} onClick={props.onClick} />
<MeasureCell component={component} metric={props.metric} />
diff --git a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js
index a37c37af1d6..c6d5c826fb5 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js
+++ b/server/sonar-web/src/main/js/apps/component-measures/drilldown/FilesView.js
@@ -19,8 +19,11 @@
*/
// @flow
import React from 'react';
+import key from 'keymaster';
+import { throttle } from 'lodash';
import ComponentsList from './ComponentsList';
import ListFooter from '../../../components/controls/ListFooter';
+import { scrollToElement } from '../../../helpers/scrolling';
import type { ComponentEnhanced, Paging } from '../types';
import type { Metric } from '../../../store/metrics/actions';
@@ -28,26 +31,104 @@ type Props = {|
components: Array<ComponentEnhanced>,
fetchMore: () => void,
handleSelect: string => void,
+ handleOpen: string => void,
metric: Metric,
metrics: { [string]: Metric },
- paging: ?Paging
+ paging: ?Paging,
+ selectedKey: ?string,
+ selectedIdx: ?number
|};
-export default function ListView(props: Props) {
- return (
- <div>
- <ComponentsList
- components={props.components}
- metrics={props.metrics}
- metric={props.metric}
- onClick={props.handleSelect}
- />
- {props.paging &&
- <ListFooter
- count={props.components.length}
- total={props.paging.total}
- loadMore={props.fetchMore}
- />}
- </div>
- );
+export default class ListView extends React.PureComponent {
+ listContainer: HTMLElement;
+ props: Props;
+
+ constructor(props: Props) {
+ super(props);
+ this.selectNext = throttle(this.selectNext, 100);
+ this.selectPrevious = throttle(this.selectPrevious, 100);
+ }
+
+ componentDidMount() {
+ this.attachShortcuts();
+ }
+
+ componentDidUpdate() {
+ if (this.listContainer && this.props.selectedIdx != null) {
+ const elem = this.listContainer.getElementsByClassName('selected')[0];
+ if (elem) {
+ scrollToElement(elem, { topOffset: 215, bottomOffset: 100 });
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this.detachShortcuts();
+ }
+
+ attachShortcuts() {
+ key('up', 'measures-files', () => {
+ this.selectPrevious();
+ return false;
+ });
+ key('down', 'measures-files', () => {
+ this.selectNext();
+ return false;
+ });
+ key('right', 'measures-files', () => {
+ this.openSelected();
+ return false;
+ });
+ }
+
+ detachShortcuts() {
+ ['up', 'down', 'right'].map(action => key.unbind(action, 'measures-files'));
+ }
+
+ openSelected = () => {
+ if (this.props.selectedKey != null) {
+ this.props.handleOpen(this.props.selectedKey);
+ }
+ };
+
+ selectPrevious = () => {
+ const { selectedIdx } = this.props;
+ if (selectedIdx != null && selectedIdx > 0) {
+ this.props.handleSelect(this.props.components[selectedIdx - 1].key);
+ } else {
+ this.props.handleSelect(this.props.components[this.props.components.length - 1].key);
+ }
+ };
+
+ selectNext = () => {
+ const { selectedIdx } = this.props;
+ if (selectedIdx != null && selectedIdx < this.props.components.length - 1) {
+ this.props.handleSelect(this.props.components[selectedIdx + 1].key);
+ } else {
+ this.props.handleSelect(this.props.components[0].key);
+ }
+ };
+
+ render() {
+ return (
+ <div
+ ref={elem => {
+ this.listContainer = elem;
+ }}>
+ <ComponentsList
+ components={this.props.components}
+ metrics={this.props.metrics}
+ metric={this.props.metric}
+ onClick={this.props.handleOpen}
+ selectedComponent={this.props.selectedKey}
+ />
+ {this.props.paging &&
+ <ListFooter
+ count={this.props.components.length}
+ total={this.props.paging.total}
+ loadMore={this.props.fetchMore}
+ />}
+ </div>
+ );
+ }
}
diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css
index 5f8e064860b..2dba101338e 100644
--- a/server/sonar-web/src/main/js/apps/component-measures/style.css
+++ b/server/sonar-web/src/main/js/apps/component-measures/style.css
@@ -52,6 +52,10 @@
margin-top: 4px;
}
+.measure-details-component-row.selected {
+ background-color: #cae3f2 !important;
+}
+
.measure-details-component-cell {
max-width: 0;
}