diff options
author | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-08 16:29:03 +0200 |
---|---|---|
committer | Grégoire Aubert <gregoire.aubert@sonarsource.com> | 2017-08-14 11:44:44 +0200 |
commit | ec01b0f2e37d0ac1aa9107c1abde9fde7ef7f9b0 (patch) | |
tree | 2f75421759d92e6feedc8477df60f90f2d69aa5a /server/sonar-web/src/main/js/apps | |
parent | 5bda516e7f849d1e5d32df47c689f8645955bae7 (diff) | |
download | sonarqube-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')
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; } |