aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorStas Vilchik <stas.vilchik@sonarsource.com>2018-08-22 11:29:50 +0200
committerSonarTech <sonartech@sonarsource.com>2018-08-24 20:21:20 +0200
commitfe5472dfe520e0681034fbd9996ccd030627b09b (patch)
treed3ccbb5900172b250298c0d0b7997be30e9c21e2 /server/sonar-web/src/main/js
parentfede6d0a2378ce959e7cb1afdf221a92e22b8a6f (diff)
downloadsonarqube-fe5472dfe520e0681034fbd9996ccd030627b09b.tar.gz
sonarqube-fe5472dfe520e0681034fbd9996ccd030627b09b.zip
SONAR-11082 open rule permalink on cmd+click
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx25
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx20
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx62
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap98
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>
+`;