diff options
author | Stas Vilchik <stas.vilchik@sonarsource.com> | 2018-08-22 11:29:50 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-08-24 20:21:20 +0200 |
commit | fe5472dfe520e0681034fbd9996ccd030627b09b (patch) | |
tree | d3ccbb5900172b250298c0d0b7997be30e9c21e2 /server/sonar-web/src/main/js | |
parent | fede6d0a2378ce959e7cb1afdf221a92e22b8a6f (diff) | |
download | sonarqube-fe5472dfe520e0681034fbd9996ccd030627b09b.tar.gz sonarqube-fe5472dfe520e0681034fbd9996ccd030627b09b.zip |
SONAR-11082 open rule permalink on cmd+click
Diffstat (limited to 'server/sonar-web/src/main/js')
4 files changed, 192 insertions, 13 deletions
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<Props, State> { 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<Props, State> { 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<Props, State> { }; 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<Props, State> { openFacets: { ...state.openFacets, [facet]: false } })); + handleRuleOpen = (ruleKey: string) => { + this.props.router.push(this.getRulePath(ruleKey)); + }; + handleBack = (event: React.SyntheticEvent<HTMLAnchorElement>) => { event.preventDefault(); event.currentTarget.blur(); @@ -413,7 +416,7 @@ export class App extends React.PureComponent<Props, State> { }; handleFilterChange = (changes: Partial<Query>) => - 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<Props, State> { 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<Props, State> { 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<StateToProps, {}, OwnProps>(mapStateToProps)(App); +export default withRouter(connect<StateToProps, {}, OwnProps>(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<Query>) => 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<Props> { 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<Props> { <td> <div className="coding-rule-title"> - <Link className="link-no-underline" to={this.props.path}> + <Link + className="link-no-underline" + onClick={this.handleNameClick} + to={getRuleUrl(rule.key, this.props.organization)}> {rule.name} </Link> {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<Function>('onClick')({ ...mockEvent, button: 0 }); + expect(onOpen).toBeCalledWith('foo'); +}); + +function shallowRender(props?: Partial<RuleListItem['props']>) { + return shallow( + <RuleListItem + onActivate={jest.fn()} + onDeactivate={jest.fn()} + onFilterChange={jest.fn()} + onOpen={jest.fn()} + organization="org" + rule={rule} + selected={false} + {...props} + /> + ); +} 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`] = ` +<div + className="coding-rule" + data-rule="foo" +> + <table + className="coding-rule-table" + > + <tbody> + <tr> + <td> + <div + className="coding-rule-title" + > + <Link + className="link-no-underline" + onClick={[Function]} + onlyActiveOnIndex={false} + style={Object {}} + to={ + Object { + "pathname": "/organizations/org/rules", + "query": Object { + "open": "foo", + "rule_key": "foo", + }, + } + } + > + Use foo + </Link> + </div> + </td> + <td + className="coding-rule-table-meta-cell" + > + <div + className="display-flex-center coding-rule-meta" + > + <span + className="spacer-left note" + > + JavaScript + </span> + <Tooltip + overlay="coding_rules.type.tooltip.CODE_SMELL" + > + <span + className="display-inline-flex-center spacer-left note" + > + <IssueTypeIcon + className="little-spacer-right" + query="CODE_SMELL" + /> + issue.type.CODE_SMELL + </span> + </Tooltip> + <TagsList + allowUpdate={false} + className="note spacer-left" + tags={ + Array [ + "x", + "a", + "b", + ] + } + /> + <SimilarRulesFilter + onFilterChange={[MockFunction]} + rule={ + Object { + "key": "foo", + "lang": "js", + "langName": "JavaScript", + "name": "Use foo", + "severity": "MAJOR", + "status": "READY", + "sysTags": Array [ + "a", + "b", + ], + "tags": Array [ + "x", + ], + "type": "CODE_SMELL", + } + } + /> + </div> + </td> + </tr> + </tbody> + </table> +</div> +`; |