@@ -42,7 +42,6 @@ url: /user-guide/keyboard-shortcuts/ | |||
| `↑` `↓` | select files | | |||
| `→` | open file | | |||
| `←` | return back to the list | | |||
| `j` `k` | switch between files | | |||
## Rules Page | |||
@@ -25,7 +25,7 @@ import MeasureHeader from './MeasureHeader'; | |||
import MeasureViewSelect from './MeasureViewSelect'; | |||
import PageActions from '../../../components/ui/PageActions'; | |||
import { complementary } from '../config/complementary'; | |||
import CodeView from '../drilldown/CodeView'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
import FilesView from '../drilldown/FilesView'; | |||
import TreeMapView from '../drilldown/TreeMapView'; | |||
import { Query, View, isFileType, enhanceComponent, isViewType } from '../utils'; | |||
@@ -263,21 +263,6 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
return index !== -1 ? index : undefined; | |||
}; | |||
renderCode() { | |||
return ( | |||
<div className="measure-details-viewer"> | |||
<CodeView | |||
branchLike={this.props.branchLike} | |||
component={this.state.baseComponent!} | |||
components={this.state.components} | |||
leakPeriod={this.props.leakPeriod} | |||
selectedIdx={this.getSelectedIndex()} | |||
updateSelected={this.updateSelected} | |||
/> | |||
</div> | |||
); | |||
} | |||
renderMeasure() { | |||
const { view } = this.props; | |||
const { metric } = this.state; | |||
@@ -318,7 +303,7 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
render() { | |||
const { branchLike, rootComponent, view } = this.props; | |||
const { baseComponent, measure, metric, secondaryMeasure } = this.state; | |||
const { baseComponent, measure, metric, paging, secondaryMeasure } = this.state; | |||
if (!baseComponent || !metric) { | |||
return null; | |||
@@ -349,24 +334,25 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
<div className="display-flex-center"> | |||
{!isFile && | |||
metric && ( | |||
<MeasureViewSelect | |||
className="measure-view-select big-spacer-right" | |||
handleViewChange={this.updateView} | |||
metric={metric} | |||
view={view} | |||
/> | |||
<> | |||
<MeasureViewSelect | |||
className="measure-view-select big-spacer-right" | |||
handleViewChange={this.updateView} | |||
metric={metric} | |||
view={view} | |||
/> | |||
<PageActions | |||
current={ | |||
selectedIdx !== undefined && view !== 'treemap' | |||
? selectedIdx + 1 | |||
: undefined | |||
} | |||
showShortcuts={['list', 'tree'].includes(view)} | |||
total={paging && paging.total} | |||
/> | |||
</> | |||
)} | |||
<PageActions | |||
current={ | |||
selectedIdx !== undefined && view !== 'treemap' | |||
? selectedIdx + 1 | |||
: undefined | |||
} | |||
isFile={isFile} | |||
paging={this.state.paging} | |||
showShortcuts={['list', 'tree'].includes(view)} | |||
totalLoadedComponents={this.state.components.length} | |||
/> | |||
</div> | |||
} | |||
/> | |||
@@ -383,7 +369,13 @@ export default class MeasureContent extends React.PureComponent<Props, State> { | |||
metric={metric} | |||
secondaryMeasure={secondaryMeasure} | |||
/> | |||
{isFile ? this.renderCode() : this.renderMeasure()} | |||
{isFile ? ( | |||
<div className="measure-details-viewer"> | |||
<SourceViewer branchLike={branchLike} component={baseComponent.key} /> | |||
</div> | |||
) : ( | |||
this.renderMeasure() | |||
)} | |||
</div> | |||
</div> | |||
); |
@@ -134,7 +134,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> { | |||
render() { | |||
const { branchLike, component, leakPeriod, rootComponent } = this.props; | |||
const isFile = isFileType(component); | |||
const { paging } = this.state; | |||
return ( | |||
<div className={this.props.className}> | |||
<div className="layout-page-header-panel layout-page-main-header"> | |||
@@ -154,8 +154,7 @@ export default class MeasureOverview extends React.PureComponent<Props, State> { | |||
right={ | |||
<PageActions | |||
current={this.state.components.length} | |||
isFile={isFile} | |||
paging={this.state.paging} | |||
total={paging && paging.total} | |||
/> | |||
} | |||
/> |
@@ -1,81 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import * as key from 'keymaster'; | |||
import SourceViewer from '../../../components/SourceViewer/SourceViewer'; | |||
interface Props { | |||
branchLike?: T.BranchLike; | |||
component: T.ComponentMeasure; | |||
components: T.ComponentMeasureEnhanced[]; | |||
leakPeriod?: T.Period; | |||
selectedIdx?: number; | |||
updateSelected: (component: string) => void; | |||
} | |||
export default class CodeView extends React.PureComponent<Props> { | |||
componentDidMount() { | |||
this.attachShortcuts(); | |||
} | |||
componentWillUnmount() { | |||
this.detachShortcuts(); | |||
} | |||
attachShortcuts() { | |||
key('j', 'measures-files', () => { | |||
this.selectNext(); | |||
return false; | |||
}); | |||
key('k', 'measures-files', () => { | |||
this.selectPrevious(); | |||
return false; | |||
}); | |||
} | |||
detachShortcuts() { | |||
['j', 'k'].forEach(action => key.unbind(action, 'measures-files')); | |||
} | |||
selectPrevious = () => { | |||
const { selectedIdx } = this.props; | |||
if (selectedIdx != null && selectedIdx > 0) { | |||
const prevComponent = this.props.components[selectedIdx - 1]; | |||
if (prevComponent) { | |||
this.props.updateSelected(prevComponent.key); | |||
} | |||
} | |||
}; | |||
selectNext = () => { | |||
const { components, selectedIdx } = this.props; | |||
if (selectedIdx != null && selectedIdx < components.length - 1) { | |||
const nextComponent = components[selectedIdx + 1]; | |||
if (nextComponent) { | |||
this.props.updateSelected(nextComponent.key); | |||
} | |||
} | |||
}; | |||
render() { | |||
const { branchLike, component } = this.props; | |||
return <SourceViewer branchLike={branchLike} component={component.key} />; | |||
} | |||
} |
@@ -64,15 +64,6 @@ | |||
white-space: nowrap; | |||
} | |||
.measure-details-page-actions { | |||
display: inline-block; | |||
text-align: right; | |||
} | |||
.measure-details-page-actions .spinner { | |||
vertical-align: text-bottom; | |||
} | |||
.measure-details-metric { | |||
display: flex; | |||
align-items: center; |
@@ -130,7 +130,7 @@ it('should support not cycling through elements, and triggering a callback on re | |||
expect(onHighlight).toBeCalledWith(COMPONENTS[0]); | |||
}); | |||
it('should correctly bind key events for sibling navigation', () => { | |||
it('should correctly bind key events for codeview navigation', () => { | |||
const onGoToParent = jest.fn(); | |||
const onHighlight = jest.fn(); | |||
const onSelect = jest.fn(); | |||
@@ -149,26 +149,16 @@ it('should correctly bind key events for sibling navigation', () => { | |||
keydown('down'); | |||
expect(onHighlight).not.toBeCalled(); | |||
expect(onSelect).not.toBeCalled(); | |||
keydown('up'); | |||
expect(onHighlight).not.toBeCalled(); | |||
expect(onSelect).not.toBeCalled(); | |||
keydown('right'); | |||
expect(onHighlight).not.toBeCalled(); | |||
expect(onSelect).not.toBeCalled(); | |||
keydown('enter'); | |||
expect(onHighlight).not.toBeCalled(); | |||
expect(onSelect).not.toBeCalled(); | |||
keydown('j'); | |||
expect(onSelect).toBeCalledWith(COMPONENTS[2]); | |||
keydown('k'); | |||
expect(onSelect).toBeCalledWith(COMPONENTS[0]); | |||
keydown('left'); | |||
expect(onGoToParent).toBeCalled(); | |||
}); |
@@ -64,12 +64,6 @@ export default function withKeyboardNavigation<P>( | |||
this.handleSelectParent(); | |||
return false; // always hijack left | |||
}); | |||
key('k', KEY_SCOPE, () => { | |||
return this.skipIfNotFile(this.handleSelectPrevious); | |||
}); | |||
key('j', KEY_SCOPE, () => { | |||
return this.skipIfNotFile(this.handleSelectNext); | |||
}); | |||
}; | |||
detachShortcuts = () => { | |||
@@ -90,15 +84,6 @@ export default function withKeyboardNavigation<P>( | |||
} | |||
}; | |||
skipIfNotFile = (handler: () => void) => { | |||
if (this.props.isFile) { | |||
handler(); | |||
return false; | |||
} else { | |||
return true; | |||
} | |||
}; | |||
handleHighlightNext = () => { | |||
if (this.props.onHighlight === undefined) { | |||
return; | |||
@@ -172,18 +157,9 @@ export default function withKeyboardNavigation<P>( | |||
}; | |||
render() { | |||
const { components = [], isFile } = this.props; | |||
const index = this.getCurrentIndex(); | |||
return ( | |||
<> | |||
<PageActions | |||
current={index > -1 ? index + 1 : undefined} | |||
isFile={isFile} | |||
showPaging={isFile && index > -1} | |||
showShortcuts={true} | |||
totalLoadedComponents={components.length} | |||
/> | |||
<PageActions showShortcuts={!this.props.isFile} /> | |||
<WrappedComponent {...this.props} /> | |||
</> |
@@ -21,64 +21,37 @@ import * as React from 'react'; | |||
import FilesCounter from './FilesCounter'; | |||
import { translate } from '../../helpers/l10n'; | |||
interface Props { | |||
export interface Props { | |||
current?: number; | |||
isFile?: boolean; | |||
paging?: T.Paging; | |||
showPaging?: boolean; | |||
showShortcuts?: boolean; | |||
totalLoadedComponents?: number; | |||
total?: number; | |||
} | |||
export default function PageActions(props: Props) { | |||
const { isFile, paging, showPaging, showShortcuts, totalLoadedComponents } = props; | |||
let total = 0; | |||
if (showPaging && totalLoadedComponents) { | |||
total = totalLoadedComponents; | |||
} else if (paging !== undefined) { | |||
total = isFile && totalLoadedComponents ? totalLoadedComponents : paging.total; | |||
} | |||
const { current, showShortcuts, total = 0 } = props; | |||
return ( | |||
<div className="page-actions display-flex-center"> | |||
{!isFile && showShortcuts && renderShortcuts()} | |||
{isFile && (paging || showPaging) && renderFileShortcuts()} | |||
{showShortcuts && ( | |||
<span className="note nowrap"> | |||
<span className="big-spacer-right"> | |||
<span className="shortcut-button little-spacer-right">↑</span> | |||
<span className="shortcut-button little-spacer-right">↓</span> | |||
{translate('component_measures.to_select_files')} | |||
</span> | |||
<span> | |||
<span className="shortcut-button little-spacer-right">←</span> | |||
<span className="shortcut-button little-spacer-right">→</span> | |||
{translate('component_measures.to_navigate')} | |||
</span> | |||
</span> | |||
)} | |||
{total > 0 && ( | |||
<div className="measure-details-page-actions nowrap"> | |||
<FilesCounter className="big-spacer-left" current={props.current} total={total} /> | |||
<div className="nowrap"> | |||
<FilesCounter className="big-spacer-left" current={current} total={total} /> | |||
</div> | |||
)} | |||
</div> | |||
); | |||
} | |||
function renderShortcuts() { | |||
return ( | |||
<span className="note nowrap"> | |||
<span className="big-spacer-right"> | |||
<span className="shortcut-button little-spacer-right">↑</span> | |||
<span className="shortcut-button little-spacer-right">↓</span> | |||
{translate('component_measures.to_select_files')} | |||
</span> | |||
<span> | |||
<span className="shortcut-button little-spacer-right">←</span> | |||
<span className="shortcut-button little-spacer-right">→</span> | |||
{translate('component_measures.to_navigate')} | |||
</span> | |||
</span> | |||
); | |||
} | |||
function renderFileShortcuts() { | |||
return ( | |||
<span className="note nowrap"> | |||
<span> | |||
<span className="shortcut-button little-spacer-right">j</span> | |||
<span className="shortcut-button little-spacer-right">k</span> | |||
{translate('component_measures.to_navigate_files')} | |||
</span> | |||
</span> | |||
); | |||
} |
@@ -19,56 +19,14 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import PageActions from '../PageActions'; | |||
import PageActions, { Props } from '../PageActions'; | |||
const PAGING = { | |||
pageIndex: 1, | |||
pageSize: 100, | |||
total: 120 | |||
}; | |||
it('should display correctly for a project', () => { | |||
expect( | |||
shallow(<PageActions isFile={false} showShortcuts={true} totalLoadedComponents={20} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should display correctly for a file', () => { | |||
const wrapper = shallow( | |||
<PageActions isFile={true} showShortcuts={true} totalLoadedComponents={10} /> | |||
); | |||
expect(wrapper).toMatchSnapshot(); | |||
wrapper.setProps({ paging: { total: 100 } }); | |||
expect(wrapper).toMatchSnapshot(); | |||
it('should render correctly', () => { | |||
expect(shallowRender()).toMatchSnapshot(); | |||
expect(shallowRender({ total: 10 })).toMatchSnapshot(); | |||
expect(shallowRender({ current: 12, showShortcuts: false, total: 120 })).toMatchSnapshot(); | |||
}); | |||
it('should not display shortcuts for treemap', () => { | |||
expect( | |||
shallow(<PageActions isFile={false} showShortcuts={false} totalLoadedComponents={20} />) | |||
).toMatchSnapshot(); | |||
}); | |||
it('should display the total of files', () => { | |||
expect( | |||
shallow( | |||
<PageActions | |||
current={12} | |||
isFile={false} | |||
paging={PAGING} | |||
showShortcuts={false} | |||
totalLoadedComponents={20} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
expect( | |||
shallow( | |||
<PageActions | |||
current={12} | |||
isFile={true} | |||
paging={PAGING} | |||
showShortcuts={true} | |||
totalLoadedComponents={20} | |||
/> | |||
) | |||
).toMatchSnapshot(); | |||
}); | |||
function shallowRender(props: Partial<Props> = {}) { | |||
return shallow(<PageActions showShortcuts={true} {...props} />); | |||
} |
@@ -1,44 +1,45 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display correctly for a file 1`] = ` | |||
<div | |||
className="page-actions display-flex-center" | |||
/> | |||
`; | |||
exports[`should display correctly for a file 2`] = ` | |||
exports[`should render correctly 1`] = ` | |||
<div | |||
className="page-actions display-flex-center" | |||
> | |||
<span | |||
className="note nowrap" | |||
> | |||
<span | |||
className="big-spacer-right" | |||
> | |||
<span | |||
className="shortcut-button little-spacer-right" | |||
> | |||
↑ | |||
</span> | |||
<span | |||
className="shortcut-button little-spacer-right" | |||
> | |||
↓ | |||
</span> | |||
component_measures.to_select_files | |||
</span> | |||
<span> | |||
<span | |||
className="shortcut-button little-spacer-right" | |||
> | |||
j | |||
← | |||
</span> | |||
<span | |||
className="shortcut-button little-spacer-right" | |||
> | |||
k | |||
→ | |||
</span> | |||
component_measures.to_navigate_files | |||
component_measures.to_navigate | |||
</span> | |||
</span> | |||
<div | |||
className="measure-details-page-actions nowrap" | |||
> | |||
<FilesCounter | |||
className="big-spacer-left" | |||
total={10} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should display correctly for a project 1`] = ` | |||
exports[`should render correctly 2`] = ` | |||
<div | |||
className="page-actions display-flex-center" | |||
> | |||
@@ -74,60 +75,29 @@ exports[`should display correctly for a project 1`] = ` | |||
component_measures.to_navigate | |||
</span> | |||
</span> | |||
</div> | |||
`; | |||
exports[`should display the total of files 1`] = ` | |||
<div | |||
className="page-actions display-flex-center" | |||
> | |||
<div | |||
className="measure-details-page-actions nowrap" | |||
className="nowrap" | |||
> | |||
<FilesCounter | |||
className="big-spacer-left" | |||
current={12} | |||
total={120} | |||
total={10} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should display the total of files 2`] = ` | |||
exports[`should render correctly 3`] = ` | |||
<div | |||
className="page-actions display-flex-center" | |||
> | |||
<span | |||
className="note nowrap" | |||
> | |||
<span> | |||
<span | |||
className="shortcut-button little-spacer-right" | |||
> | |||
j | |||
</span> | |||
<span | |||
className="shortcut-button little-spacer-right" | |||
> | |||
k | |||
</span> | |||
component_measures.to_navigate_files | |||
</span> | |||
</span> | |||
<div | |||
className="measure-details-page-actions nowrap" | |||
className="nowrap" | |||
> | |||
<FilesCounter | |||
className="big-spacer-left" | |||
current={12} | |||
total={20} | |||
total={120} | |||
/> | |||
</div> | |||
</div> | |||
`; | |||
exports[`should not display shortcuts for treemap 1`] = ` | |||
<div | |||
className="page-actions display-flex-center" | |||
/> | |||
`; |
@@ -73,9 +73,7 @@ export const KEYCODE_MAP: { [keycode: number]: string } = { | |||
37: 'left', | |||
38: 'up', | |||
39: 'right', | |||
40: 'down', | |||
74: 'j', | |||
75: 'k' | |||
40: 'down' | |||
}; | |||
export function keydown(key: number | string): void { |