From fe5472dfe520e0681034fbd9996ccd030627b09b Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Wed, 22 Aug 2018 11:29:50 +0200 Subject: [PATCH] SONAR-11082 open rule permalink on cmd+click --- .../js/apps/coding-rules/components/App.tsx | 25 ++--- .../coding-rules/components/RuleListItem.tsx | 20 +++- .../__tests__/RuleListItem-test.tsx | 62 ++++++++++++ .../__snapshots__/RuleListItem-test.tsx.snap | 98 +++++++++++++++++++ 4 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx create mode 100644 server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx index 320c759103e..8e17a963e70 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; +import { withRouter, WithRouterProps } from 'react-router'; import * as PropTypes from 'prop-types'; import * as key from 'keymaster'; import { keyBy } from 'lodash'; @@ -67,8 +68,7 @@ interface StateToProps { userOrganizations: Organization[]; } -interface OwnProps { - location: { pathname: string; query: RawQuery }; +interface OwnProps extends WithRouterProps { organization: Organization | undefined; } @@ -95,8 +95,7 @@ export class App extends React.PureComponent { mounted = false; static contextTypes = { - organizationsEnabled: PropTypes.bool, - router: PropTypes.object.isRequired + organizationsEnabled: PropTypes.bool }; constructor(props: Props) { @@ -349,9 +348,9 @@ export class App extends React.PureComponent { openRule = (rule: string) => { const path = this.getRulePath(rule); if (this.state.openRule) { - this.context.router.replace(path); + this.props.router.replace(path); } else { - this.context.router.push(path); + this.props.router.push(path); } }; @@ -363,7 +362,7 @@ export class App extends React.PureComponent { }; closeRule = () => { - this.context.router.push({ + this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery(this.state.query), @@ -406,6 +405,10 @@ export class App extends React.PureComponent { openFacets: { ...state.openFacets, [facet]: false } })); + handleRuleOpen = (ruleKey: string) => { + this.props.router.push(this.getRulePath(ruleKey)); + }; + handleBack = (event: React.SyntheticEvent) => { event.preventDefault(); event.currentTarget.blur(); @@ -413,7 +416,7 @@ export class App extends React.PureComponent { }; handleFilterChange = (changes: Partial) => - this.context.router.push({ + this.props.router.push({ pathname: this.props.location.pathname, query: serializeQuery({ ...this.state.query, ...changes }) }); @@ -429,7 +432,7 @@ export class App extends React.PureComponent { handleReload = () => this.fetchFirstRules(); - handleReset = () => this.context.router.push({ pathname: this.props.location.pathname }); + handleReset = () => this.props.router.push({ pathname: this.props.location.pathname }); /** Tries to take rule by index, or takes the last one */ pickRuleAround = (rules: Rule[], selectedIndex: number | undefined) => { @@ -583,8 +586,8 @@ export class App extends React.PureComponent { onActivate={this.handleRuleActivate} onDeactivate={this.handleRuleDeactivate} onFilterChange={this.handleFilterChange} + onOpen={this.handleRuleOpen} organization={organization} - path={this.getRulePath(rule.key)} rule={rule} selected={rule.key === this.state.selected} selectedProfile={this.getSelectedProfile()} @@ -636,4 +639,4 @@ const mapStateToProps = (state: any) => ({ userOrganizations: getMyOrganizations(state) }); -export default connect(mapStateToProps)(App); +export default withRouter(connect(mapStateToProps)(App)); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx index 055fdff17aa..c2dc3380618 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx @@ -33,14 +33,15 @@ import SeverityIcon from '../../../components/icons-components/SeverityIcon'; import TagsList from '../../../components/tags/TagsList'; import Tooltip from '../../../components/controls/Tooltip'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { getRuleUrl } from '../../../helpers/urls'; interface Props { activation?: Activation; onActivate: (profile: string, rule: string, activation: Activation) => void; onDeactivate: (profile: string, rule: string) => void; onFilterChange: (changes: Partial) => void; + onOpen: (ruleKey: string) => void; organization: string | undefined; - path: { pathname: string; query: { [x: string]: any } }; rule: Rule; selected: boolean; selectedProfile?: Profile; @@ -68,6 +69,18 @@ export default class RuleListItem extends React.PureComponent { return Promise.resolve(); }; + handleNameClick = (event: React.MouseEvent) => { + // cmd(ctrl) + click should open a rule permalink in a new tab + const isLeftClickEvent = event.button === 0; + const isModifiedEvent = !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); + if (isModifiedEvent || !isLeftClickEvent) { + return; + } + + event.preventDefault(); + this.props.onOpen(this.props.rule.key); + }; + renderActivation = () => { const { activation, selectedProfile } = this.props; if (!activation) { @@ -178,7 +191,10 @@ export default class RuleListItem extends React.PureComponent {
- + {rule.name} {rule.isTemplate && ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx new file mode 100644 index 00000000000..6da05329f1b --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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 { shallow } from 'enzyme'; +import RuleListItem from '../RuleListItem'; +import { Rule } from '../../../../app/types'; +import { mockEvent } from '../../../../helpers/testUtils'; + +const rule: Rule = { + key: 'foo', + lang: 'js', + langName: 'JavaScript', + name: 'Use foo', + severity: 'MAJOR', + status: 'READY', + sysTags: ['a', 'b'], + tags: ['x'], + type: 'CODE_SMELL' +}; + +it('should render', () => { + expect(shallowRender()).toMatchSnapshot(); +}); + +it('should open rule', () => { + const onOpen = jest.fn(); + const wrapper = shallowRender({ onOpen }); + wrapper.find('Link').prop('onClick')({ ...mockEvent, button: 0 }); + expect(onOpen).toBeCalledWith('foo'); +}); + +function shallowRender(props?: Partial) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap new file mode 100644 index 00000000000..459a8165037 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap @@ -0,0 +1,98 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should render 1`] = ` +
+ + + + + + + +
+
+ + Use foo + +
+
+
+ + JavaScript + + + + + issue.type.CODE_SMELL + + + + +
+
+
+`; -- 2.39.5