]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11082 open rule permalink on cmd+click
authorStas Vilchik <stas.vilchik@sonarsource.com>
Wed, 22 Aug 2018 09:29:50 +0000 (11:29 +0200)
committerSonarTech <sonartech@sonarsource.com>
Fri, 24 Aug 2018 18:21:20 +0000 (20:21 +0200)
server/sonar-web/src/main/js/apps/coding-rules/components/App.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleListItem.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/RuleListItem-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleListItem-test.tsx.snap [new file with mode: 0644]

index 320c759103e4fa2e3915d5f4189c737b70b73c63..8e17a963e7096913eef6ac32b4d264cd33e4d7fb 100644 (file)
@@ -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));
index 055fdff17aa68957da97a6e151b7f471a332bf9d..c2dc33806189d3c35d65aec7cc76a36baf6ef579 100644 (file)
@@ -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 (file)
index 0000000..6da0532
--- /dev/null
@@ -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 (file)
index 0000000..459a816
--- /dev/null
@@ -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>
+`;