]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-13342 Fix faulty links
authorJeremy Davis <jeremy.davis@sonarsource.com>
Fri, 1 May 2020 08:50:58 +0000 (10:50 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 5 May 2020 20:03:43 +0000 (20:03 +0000)
23 files changed:
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap
server/sonar-web/src/main/js/app/components/search/Search.tsx
server/sonar-web/src/main/js/app/components/search/SearchResult.tsx
server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/components/App.tsx
server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
server/sonar-web/src/main/js/helpers/urls.ts
server/sonar-web/src/main/js/types/component.ts

index 4a8e7cdfdd6b73abaeded57b7a12dc37d9f2379d..cb82b50cc0b89a78b1ebc76e72b2ab120014263c 100644 (file)
@@ -32,13 +32,14 @@ import {
   isMainBranch,
   isPullRequest
 } from '../../helpers/branch-like';
-import { isSonarCloud } from '../../helpers/system';
+import { getPortfolioUrl } from '../../helpers/urls';
 import {
   fetchOrganization,
   registerBranchStatus,
   requireAuthorization
 } from '../../store/rootActions';
 import { BranchLike } from '../../types/branch-like';
+import { isPortfolioLike } from '../../types/component';
 import ComponentContainerNotFound from './ComponentContainerNotFound';
 import { ComponentContext } from './ComponentContext';
 import ComponentNav from './nav/component/ComponentNav';
@@ -46,7 +47,7 @@ import ComponentNav from './nav/component/ComponentNav';
 interface Props {
   children: React.ReactElement;
   fetchOrganization: (organization: string) => void;
-  location: Pick<Location, 'query'>;
+  location: Pick<Location, 'query' | 'pathname'>;
   registerBranchStatus: (branchLike: BranchLike, component: string, status: T.Status) => void;
   requireAuthorization: (router: Pick<Router, 'replace'>) => void;
   router: Pick<Router, 'replace'>;
@@ -116,9 +117,18 @@ export class ComponentContainer extends React.PureComponent<Props, State> {
       .then(([nav, { component }]) => {
         const componentWithQualifier = this.addQualifier({ ...nav, ...component });
 
-        if (isSonarCloud()) {
-          this.props.fetchOrganization(componentWithQualifier.organization);
+        /*
+         * There used to be a redirect from /dashboard to /portfolio which caused issues.
+         * Links should be fixed to not rely on this redirect, but:
+         * This is a fail-safe in case there are still some faulty links remaining.
+         */
+        if (
+          this.props.location.pathname.match('dashboard') &&
+          isPortfolioLike(componentWithQualifier.qualifier)
+        ) {
+          this.props.router.replace(getPortfolioUrl(component.key));
         }
+
         return componentWithQualifier;
       }, onError)
       .then(this.fetchBranches)
index fa61e9196401925616ae7a130f4dbf506eac5278..a69eb36c59b430a84d4180dbc92e115e7d6bb514 100644 (file)
@@ -26,8 +26,8 @@ import { getComponentData } from '../../../api/components';
 import { getComponentNavigation } from '../../../api/nav';
 import { STATUSES } from '../../../apps/background-tasks/constants';
 import { mockBranch, mockMainBranch, mockPullRequest } from '../../../helpers/mocks/branch-like';
-import { isSonarCloud } from '../../../helpers/system';
 import { mockComponent, mockLocation, mockRouter } from '../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../types/component';
 import { ComponentContainer } from '../ComponentContainer';
 
 jest.mock('../../../api/branches', () => {
@@ -63,10 +63,6 @@ jest.mock('../../../api/nav', () => ({
   })
 }));
 
-jest.mock('../../../helpers/system', () => ({
-  isSonarCloud: jest.fn().mockReturnValue(false)
-}));
-
 // mock this, because some of its children are using redux store
 jest.mock('../nav/component/ComponentNav', () => ({
   default: () => null
@@ -123,18 +119,6 @@ it('updates branches on change', async () => {
   expect(registerBranchStatus).toBeCalledTimes(2);
 });
 
-it('loads organization', async () => {
-  (isSonarCloud as jest.Mock).mockReturnValue(true);
-  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
-    component: { organization: 'org' }
-  });
-
-  const fetchOrganization = jest.fn();
-  shallowRender({ fetchOrganization });
-  await new Promise(setImmediate);
-  expect(fetchOrganization).toBeCalledWith('org');
-});
-
 it('fetches status', async () => {
   (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
     component: { organization: 'org' }
@@ -196,20 +180,36 @@ it('reload component after task progress finished', async () => {
 });
 
 it('should show component not found if it does not exist', async () => {
-  (getComponentNavigation as jest.Mock).mockRejectedValue({ status: 404 });
+  (getComponentNavigation as jest.Mock).mockRejectedValueOnce({ status: 404 });
   const wrapper = shallowRender();
   await waitAndUpdate(wrapper);
   expect(wrapper).toMatchSnapshot();
 });
 
 it('should redirect if the user has no access', async () => {
-  (getComponentNavigation as jest.Mock).mockRejectedValue({ status: 403 });
+  (getComponentNavigation as jest.Mock).mockRejectedValueOnce({ status: 403 });
   const requireAuthorization = jest.fn();
   const wrapper = shallowRender({ requireAuthorization });
   await waitAndUpdate(wrapper);
   expect(requireAuthorization).toBeCalled();
 });
 
+it('should redirect if the component is a portfolio', async () => {
+  const componentKey = 'comp-key';
+  (getComponentData as jest.Mock<any>).mockResolvedValueOnce({
+    component: { key: componentKey, breadcrumbs: [{ qualifier: ComponentQualifier.Portfolio }] }
+  });
+
+  const replace = jest.fn();
+
+  const wrapper = shallowRender({
+    location: mockLocation({ pathname: '/dashboard' }),
+    router: mockRouter({ replace })
+  });
+  await waitAndUpdate(wrapper);
+  expect(replace).toBeCalledWith({ pathname: '/portfolio', query: { id: componentKey } });
+});
+
 function shallowRender(props: Partial<ComponentContainer['props']> = {}) {
   return shallow<ComponentContainer>(
     <ComponentContainer
index b608d6484a63eac5fa5e5c739838d6b341e6a979..b7f97793e66584bcd774a78a2ad8e4a0be3dbef7 100644 (file)
@@ -22,7 +22,7 @@ import * as React from 'react';
 import { Link } from 'react-router';
 import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
 import { isMainBranch } from '../../../../helpers/branch-like';
-import { getProjectUrl } from '../../../../helpers/urls';
+import { getComponentOverviewUrl } from '../../../../helpers/urls';
 import { BranchLike } from '../../../../types/branch-like';
 
 interface Props {
@@ -53,7 +53,7 @@ export function Breadcrumb(props: Props) {
               <Link
                 className="link-no-underline text-ellipsis"
                 title={breadcrumbElement.name}
-                to={getProjectUrl(breadcrumbElement.key)}>
+                to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)}>
                 {breadcrumbElement.name}
               </Link>
             ) : (
index b217d44e23cf8daa2ea3d318fce21074bd9615cb..8fc0f56a6ddbc02b3de0913d216343d0b6d1c77e 100644 (file)
@@ -29,8 +29,9 @@ import { hasMessage, translate } from 'sonar-ui-common/helpers/l10n';
 import { withAppState } from '../../../../components/hoc/withAppState';
 import { getBranchLikeQuery, isMainBranch, isPullRequest } from '../../../../helpers/branch-like';
 import { isSonarCloud } from '../../../../helpers/system';
+import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls';
 import { BranchLike, BranchParameters } from '../../../../types/branch-like';
-import { ComponentQualifier } from '../../../../types/component';
+import { ComponentQualifier, isPortfolioLike } from '../../../../types/component';
 import './Menu.css';
 
 const SETTINGS_URLS = [
@@ -95,9 +96,7 @@ export class Menu extends React.PureComponent<Props> {
 
   isPortfolio = () => {
     const { qualifier } = this.props.component;
-    return (
-      qualifier === ComponentQualifier.Portfolio || qualifier === ComponentQualifier.SubPortfolio
-    );
+    return isPortfolioLike(qualifier);
   };
 
   isApplication = () => {
@@ -112,11 +111,12 @@ export class Menu extends React.PureComponent<Props> {
     return { id: this.props.component.key, ...getBranchLikeQuery(this.props.branchLike) };
   };
 
-  renderDashboardLink = (query: Query, isPortfolio: boolean) => {
-    const pathname = isPortfolio ? '/portfolio' : '/dashboard';
+  renderDashboardLink = ({ id, ...branchLike }: Query, isPortfolio: boolean) => {
     return (
       <li>
-        <Link activeClassName="active" to={{ pathname, query }}>
+        <Link
+          activeClassName="active"
+          to={isPortfolio ? getPortfolioUrl(id) : getProjectQueryUrl(id, branchLike)}>
           {translate('overview.page')}
         </Link>
       </li>
index 314c7e511a6fda659efd44997e26782673e7e1a0..161aff0449ded7f2dc0502841fa4856cee0acd56 100644 (file)
@@ -19,9 +19,8 @@ exports[`should render correctly 1`] = `
       title="parent-portfolio"
       to={
         Object {
-          "pathname": "/dashboard",
+          "pathname": "/portfolio",
           "query": Object {
-            "branch": undefined,
             "id": "parent-portfolio",
           },
         }
index 7db674fef924b4d5cf9fe8751794269e2e861325..ef00d7b8fad810005b73f64390fdb1600ffd65b9 100644 (file)
@@ -31,7 +31,8 @@ import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
 import { getSuggestions } from '../../../api/components';
-import { getCodeUrl, getProjectUrl } from '../../../helpers/urls';
+import { getCodeUrl, getComponentOverviewUrl } from '../../../helpers/urls';
+import { ComponentQualifier } from '../../../types/component';
 import RecentHistory from '../RecentHistory';
 import './Search.css';
 import { ComponentResult, More, Results, sortQualifiers } from './utils';
@@ -275,20 +276,30 @@ export class Search extends React.PureComponent<Props, State> {
   };
 
   openSelected = () => {
-    const { selected } = this.state;
+    const { results, selected } = this.state;
 
-    if (selected) {
-      if (selected.startsWith('qualifier###')) {
-        this.searchMore(selected.substr(12));
+    if (!selected) {
+      return;
+    }
+
+    if (selected.startsWith('qualifier###')) {
+      this.searchMore(selected.substr(12));
+    } else {
+      const file = this.findFile(selected);
+      if (file) {
+        this.props.router.push(getCodeUrl(file.project!, undefined, file.key));
       } else {
-        const file = this.findFile(selected);
-        if (file) {
-          this.props.router.push(getCodeUrl(file.project!, undefined, file.key));
-        } else {
-          this.props.router.push(getProjectUrl(selected));
+        let qualifier = ComponentQualifier.Project;
+
+        if ((results[ComponentQualifier.Portfolio] ?? []).find(r => r.key === selected)) {
+          qualifier = ComponentQualifier.Portfolio;
+        } else if ((results[ComponentQualifier.SubPortfolio] ?? []).find(r => r.key === selected)) {
+          qualifier = ComponentQualifier.SubPortfolio;
         }
-        this.closeSearch();
+
+        this.props.router.push(getComponentOverviewUrl(selected, qualifier));
       }
+      this.closeSearch();
     }
   };
 
index 1bf2f4bee515293fdd71053d0f06f73a973713d3..7f99d49e15de227d183f0f7ebbb248de5aeedf14 100644 (file)
@@ -23,7 +23,7 @@ import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
 import ClockIcon from 'sonar-ui-common/components/icons/ClockIcon';
 import FavoriteIcon from 'sonar-ui-common/components/icons/FavoriteIcon';
 import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
-import { getCodeUrl, getProjectUrl } from '../../../helpers/urls';
+import { getCodeUrl, getComponentOverviewUrl } from '../../../helpers/urls';
 import { ComponentResult } from './utils';
 
 interface Props {
@@ -114,7 +114,7 @@ export default class SearchResult extends React.PureComponent<Props, State> {
     const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';
     const to = isFile
       ? getCodeUrl(component.project!, undefined, component.key)
-      : getProjectUrl(component.key);
+      : getComponentOverviewUrl(component.key, component.qualifier);
 
     return (
       <li
index 1f9e458c2208e7b7b6d11a344764eb8190fa4c09..8786dcaaac59a4a5c035d660063143057f21376b 100644 (file)
@@ -20,6 +20,8 @@
 import { shallow, ShallowWrapper } from 'enzyme';
 import * as React from 'react';
 import { elementKeydown } from 'sonar-ui-common/helpers/testUtils';
+import { mockRouter } from '../../../../helpers/testMocks';
+import { ComponentQualifier } from '../../../../types/component';
 import { Search } from '../Search';
 
 it('selects results', () => {
@@ -29,7 +31,7 @@ it('selects results', () => {
     open: true,
     results: {
       TRK: [component('foo'), component('bar')],
-      BRC: [component('qwe', 'BRC')]
+      BRC: [component('qwe', ComponentQualifier.SubProject)]
     },
     selected: 'foo'
   });
@@ -44,17 +46,50 @@ it('selects results', () => {
   prev(form, 'foo');
 });
 
-it('opens selected on enter', () => {
-  const form = shallowRender();
+it('opens selected project on enter', () => {
+  const router = mockRouter();
+  const form = shallowRender({ router });
+  const selectedKey = 'project';
   form.setState({
     open: true,
-    results: { TRK: [component('foo')] },
-    selected: 'foo'
+    results: { [ComponentQualifier.Project]: [component(selectedKey)] },
+    selected: selectedKey
+  });
+
+  elementKeydown(form.find('SearchBox'), 13);
+  expect(router.push).toBeCalledWith({ pathname: '/dashboard', query: { id: selectedKey } });
+});
+
+it('opens selected portfolio on enter', () => {
+  const router = mockRouter();
+  const form = shallowRender({ router });
+  const selectedKey = 'portfolio';
+  form.setState({
+    open: true,
+    results: {
+      [ComponentQualifier.Portfolio]: [component(selectedKey, ComponentQualifier.Portfolio)]
+    },
+    selected: selectedKey
   });
-  const openSelected = jest.fn();
-  (form.instance() as Search).openSelected = openSelected;
+
+  elementKeydown(form.find('SearchBox'), 13);
+  expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } });
+});
+
+it('opens selected subportfolio on enter', () => {
+  const router = mockRouter();
+  const form = shallowRender({ router });
+  const selectedKey = 'sbprtfl';
+  form.setState({
+    open: true,
+    results: {
+      [ComponentQualifier.SubPortfolio]: [component(selectedKey, ComponentQualifier.SubPortfolio)]
+    },
+    selected: selectedKey
+  });
+
   elementKeydown(form.find('SearchBox'), 13);
-  expect(openSelected).toBeCalled();
+  expect(router.push).toBeCalledWith({ pathname: '/portfolio', query: { id: selectedKey } });
 });
 
 it('shows warning about short input', () => {
@@ -76,7 +111,7 @@ function shallowRender(props: Partial<Search['props']> = {}) {
   );
 }
 
-function component(key: string, qualifier = 'TRK') {
+function component(key: string, qualifier = ComponentQualifier.Project) {
   return { key, name: key, qualifier };
 }
 
index 21d0975f90598e6dda40f2ef09b4278e53e083de..db6ada7d8ae641fa547450c3508aedd85d37023c 100644 (file)
@@ -19,7 +19,6 @@ exports[`renders favorite 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -71,7 +70,6 @@ exports[`renders match 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -122,7 +120,6 @@ exports[`renders organizations 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -178,7 +175,6 @@ exports[`renders organizations 2`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -229,7 +225,6 @@ exports[`renders projects 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "qwe",
           },
         }
@@ -285,7 +280,6 @@ exports[`renders recently browsed 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -336,7 +330,6 @@ exports[`renders selected 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
@@ -385,7 +378,6 @@ exports[`renders selected 2`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "foo",
           },
         }
index 00bcb60c1aa4e382613762787524be63a2538473..e0b3c6e04e6e5f32dc16cf05f9e64dc0fcf458d4 100644 (file)
@@ -27,6 +27,7 @@ import Level from 'sonar-ui-common/components/ui/Level';
 import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
 import MetaLink from '../../../app/components/nav/component/projectInformation/meta/MetaLink';
 import { orderLinks } from '../../../helpers/projectLinks';
+import { getProjectUrl } from '../../../helpers/urls';
 
 interface Props {
   project: T.MyProject;
@@ -82,7 +83,7 @@ export default function ProjectCard({ project }: Props) {
       </aside>
 
       <h3 className="account-project-name">
-        <Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link>
+        <Link to={getProjectUrl(project.key)}>{project.name}</Link>
       </h3>
 
       {orderedLinks.length > 0 && (
index 9a7b9662ebbebdad0dfd20ef4385bff47334692c..18a7ffb43220ec51998182ea4fc2be8b7e6f0d61 100644 (file)
@@ -29,7 +29,7 @@ import {
   getProjectUrl,
   getPullRequestUrl
 } from '../../../helpers/urls';
-import { ComponentQualifier } from '../../../types/component';
+import { isPortfolioLike } from '../../../types/component';
 import TaskType from './TaskType';
 
 interface Props {
@@ -85,7 +85,7 @@ export default function TaskComponent({ task }: Props) {
 }
 
 function getTaskComponentUrl(componentKey: string, task: T.Task) {
-  if (task.componentQualifier === ComponentQualifier.Portfolio) {
+  if (isPortfolioLike(task.componentQualifier)) {
     return getPortfolioUrl(componentKey);
   } else if (task.branch) {
     return getBranchUrl(componentKey, task.branch);
index 90847a9a49ba2d819adffe8bd93064008e3b8389..7f7d7d32564de994f14663bed36d24c932228f27 100644 (file)
@@ -24,6 +24,7 @@ import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { colors } from '../../../app/theme';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { getProjectUrl } from '../../../helpers/urls';
 import { BranchLike } from '../../../types/branch-like';
 
 export function getTooltip(component: T.ComponentMeasure) {
@@ -82,11 +83,9 @@ export default function ComponentName({
   let inner = null;
 
   if (component.refKey && component.qualifier !== 'SVW') {
-    const branch = rootComponent.qualifier === 'APP' ? { branch: component.branch } : {};
+    const branch = rootComponent.qualifier === 'APP' ? component.branch : undefined;
     inner = (
-      <Link
-        className="link-with-icon"
-        to={{ pathname: '/dashboard', query: { id: component.refKey, ...branch } }}>
+      <Link className="link-with-icon" to={getProjectUrl(component.refKey, branch)}>
         <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
       </Link>
     );
index 4cc11714524b2781c1c96f80b3527daafb52371b..3978fbe2c62b4e4a9cbf429528813c1dd7151559 100644 (file)
@@ -187,6 +187,7 @@ foo"
       Object {
         "pathname": "/dashboard",
         "query": Object {
+          "branch": undefined,
           "id": "src/main/ts/app",
         },
       }
index 2fb35de7b063e6efd99e0ec83528cde31e99550b..93eb9ca5a2f04daa54e7102011e7e1bc19374ef6 100644 (file)
@@ -23,7 +23,7 @@ import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
 import { Router, withRouter } from '../../../components/hoc/withRouter';
 import { isPullRequest } from '../../../helpers/branch-like';
 import { BranchLike } from '../../../types/branch-like';
-import { ComponentQualifier } from '../../../types/component';
+import { isPortfolioLike } from '../../../types/component';
 import BranchOverview from '../branches/BranchOverview';
 
 const EmptyOverview = lazyLoadComponent(() => import('./EmptyOverview'));
@@ -40,9 +40,7 @@ interface Props {
 
 export class App extends React.PureComponent<Props> {
   isPortfolio = () => {
-    return ([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio] as string[]).includes(
-      this.props.component.qualifier
-    );
+    return isPortfolioLike(this.props.component.qualifier);
   };
 
   render() {
index 1808a844967f96ec0ed1a649ff5774bb56c3c729..0fa73bc841e539eb8dbfa395a732be8de0f68ff7 100644 (file)
@@ -25,7 +25,8 @@ import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n
 import { formatMeasure } from 'sonar-ui-common/helpers/measures';
 import { colors } from '../../../app/theme';
 import Measure from '../../../components/measure/Measure';
-import { getProjectUrl } from '../../../helpers/urls';
+import { getComponentOverviewUrl } from '../../../helpers/urls';
+import { ComponentQualifier } from '../../../types/component';
 import { SubComponent } from '../types';
 
 interface Props {
@@ -79,11 +80,14 @@ export default function WorstProjects({ component, subComponents, total }: Props
               <td>
                 <Link
                   className="link-with-icon"
-                  to={getProjectUrl(component.refKey || component.key)}>
+                  to={getComponentOverviewUrl(
+                    component.refKey || component.key,
+                    component.qualifier
+                  )}>
                   <QualifierIcon qualifier={component.qualifier} /> {component.name}
                 </Link>
               </td>
-              {component.qualifier === 'TRK'
+              {component.qualifier === ComponentQualifier.Project
                 ? renderCell(component.measures, 'alert_status', 'LEVEL')
                 : renderCell(component.measures, 'releasability_rating', 'RATING')}
               {renderCell(component.measures, 'reliability_rating', 'RATING')}
index 7fbb4c9db8b11dd8ec9e220de3c8241a7f4e6ec8..9ed6966558b5199b14fbddae9be49a9be7d3feeb 100644 (file)
@@ -56,9 +56,8 @@ exports[`renders 1`] = `
             style={Object {}}
             to={
               Object {
-                "pathname": "/dashboard",
+                "pathname": "/portfolio",
                 "query": Object {
-                  "branch": undefined,
                   "id": "foo",
                 },
               }
@@ -155,7 +154,6 @@ exports[`renders 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
-                  "branch": undefined,
                   "id": "barbar",
                 },
               }
@@ -252,7 +250,6 @@ exports[`renders 1`] = `
               Object {
                 "pathname": "/dashboard",
                 "query": Object {
-                  "branch": undefined,
                   "id": "bazbaz",
                 },
               }
index c04d80194dd514b7fc01e72c4b3758f59e776073..fe4a70a5f4a0e953a7c522037c5aea49d2c2c0f5 100644 (file)
@@ -25,8 +25,7 @@ import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
 import DateTooltipFormatter from 'sonar-ui-common/components/intl/DateTooltipFormatter';
 import { Project } from '../../api/components';
 import PrivacyBadgeContainer from '../../components/common/PrivacyBadgeContainer';
-import { getPortfolioUrl, getProjectUrl } from '../../helpers/urls';
-import { ComponentQualifier } from '../../types/component';
+import { getComponentOverviewUrl } from '../../helpers/urls';
 import './ProjectRow.css';
 import ProjectRowActions from './ProjectRowActions';
 
@@ -43,12 +42,6 @@ export default class ProjectRow extends React.PureComponent<Props> {
     this.props.onProjectCheck(this.props.project, checked);
   };
 
-  getComponentUrl(project: Project) {
-    return project.qualifier === ComponentQualifier.Portfolio
-      ? getPortfolioUrl(project.key)
-      : getProjectUrl(project.key);
-  }
-
   render() {
     const { organization, project, selected } = this.props;
 
@@ -59,7 +52,9 @@ export default class ProjectRow extends React.PureComponent<Props> {
         </td>
 
         <td className="nowrap hide-overflow project-row-text-cell">
-          <Link className="link-with-icon" to={this.getComponentUrl(project)}>
+          <Link
+            className="link-with-icon"
+            to={getComponentOverviewUrl(project.key, project.qualifier)}>
             <QualifierIcon className="little-spacer-right" qualifier={project.qualifier} />
 
             <Tooltip overlay={project.name} placement="left">
index 52589366e34a543e4b8b02aba5e20c0aca8a9608..c11442aa7ce85320b737584355a4b844167ff785 100644 (file)
@@ -24,7 +24,6 @@ exports[`renders 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "project",
           },
         }
@@ -217,7 +216,6 @@ exports[`renders: with lastAnalysisDate 1`] = `
         Object {
           "pathname": "/dashboard",
           "query": Object {
-            "branch": undefined,
             "id": "project",
           },
         }
index 06ae37a2e104951067e06b29fc902b1d4caffd7a..74a9a425c928b5c3c94665aa4b9a272f8ad3b608 100644 (file)
@@ -24,6 +24,7 @@ import ListFooter from 'sonar-ui-common/components/controls/ListFooter';
 import QualifierIcon from 'sonar-ui-common/components/icons/QualifierIcon';
 import { translate } from 'sonar-ui-common/helpers/l10n';
 import { getProfileProjects } from '../../../api/quality-profiles';
+import { getProjectUrl } from '../../../helpers/urls';
 import { Profile } from '../types';
 import ChangeProjectsForm from './ChangeProjectsForm';
 
@@ -141,9 +142,7 @@ export default class ProfileProjects extends React.PureComponent<Props, State> {
         <ul>
           {projects.map(project => (
             <li className="spacer-top js-profile-project" data-key={project.key} key={project.key}>
-              <Link
-                className="link-with-icon"
-                to={{ pathname: '/dashboard', query: { id: project.key } }}>
+              <Link className="link-with-icon" to={getProjectUrl(project.key)}>
                 <QualifierIcon qualifier="TRK" /> <span>{project.name}</span>
               </Link>
             </li>
index de37df033c24d21e90eb3dac48b5845d08696b14..32a907732308d71d711622d9c7cc3ff15e26df45 100644 (file)
@@ -69,6 +69,7 @@ exports[`should render correctly 2`] = `
             Object {
               "pathname": "/dashboard",
               "query": Object {
+                "branch": undefined,
                 "id": "org.sonarsource.xml:xml",
               },
             }
index 7e4ac320884b4c8f34565b3f99b64072edc25ff8..cd229908f9036a4a43f2f7a784065ecbfb7c80e1 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { ComponentQualifier } from '../../types/component';
 import {
   getComponentDrilldownUrl,
   getComponentIssuesUrl,
+  getComponentOverviewUrl,
   getComponentSecurityHotspotsUrl,
   getQualityGatesUrl,
   getQualityGateUrl
@@ -67,6 +69,33 @@ describe('getComponentSecurityHotspotsUrl', () => {
   });
 });
 
+describe('getComponentOverviewUrl', () => {
+  it('should return a portfolio url for a portfolio', () => {
+    expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Portfolio)).toEqual({
+      pathname: '/portfolio',
+      query: { id: SIMPLE_COMPONENT_KEY }
+    });
+  });
+  it('should return a portfolio url for a subportfolio', () => {
+    expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.SubPortfolio)).toEqual({
+      pathname: '/portfolio',
+      query: { id: SIMPLE_COMPONENT_KEY }
+    });
+  });
+  it('should return a dashboard url for a project', () => {
+    expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Project)).toEqual({
+      pathname: '/dashboard',
+      query: { id: SIMPLE_COMPONENT_KEY }
+    });
+  });
+  it('should return a dashboard url for an app', () => {
+    expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Application)).toEqual({
+      pathname: '/dashboard',
+      query: { id: SIMPLE_COMPONENT_KEY }
+    });
+  });
+});
+
 describe('#getComponentDrilldownUrl', () => {
   it('should return component drilldown url', () => {
     expect(
index 8727dd2dd964fcde1bfae16956787fb28a9269fa..ee7e4bf07ed5885de1bbe94512ad2543d0d2a48b 100644 (file)
  */
 import { getBaseUrl, Location } from 'sonar-ui-common/helpers/urls';
 import { getProfilePath } from '../apps/quality-profiles/utils';
-import { BranchLike } from '../types/branch-like';
+import { BranchLike, BranchParameters } from '../types/branch-like';
+import { ComponentQualifier, isPortfolioLike } from '../types/component';
 import { GraphType } from '../types/project-activity';
 import { getBranchLikeQuery, isBranch, isMainBranch, isPullRequest } from './branch-like';
 
 type Query = Location['query'];
 
+export function getComponentOverviewUrl(
+  componentKey: string,
+  componentQualifier: ComponentQualifier | string,
+  branchParameters?: BranchParameters
+) {
+  return isPortfolioLike(componentQualifier)
+    ? getPortfolioUrl(componentKey)
+    : getProjectQueryUrl(componentKey, branchParameters);
+}
+
 export function getProjectUrl(project: string, branch?: string): Location {
   return { pathname: '/dashboard', query: { id: project, branch } };
 }
 
+export function getProjectQueryUrl(project: string, branchParameters?: BranchParameters): Location {
+  return { pathname: '/dashboard', query: { id: project, ...branchParameters } };
+}
+
 export function getPortfolioUrl(key: string): Location {
   return { pathname: '/portfolio', query: { id: key } };
 }
index 6d059f21d543bc68a2a81e237f190df41c35a459..682fe0ecc8d1e1795c4a21e3d40697296f335a5c 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+
 export enum ComponentQualifier {
   Application = 'APP',
   Directory = 'DIR',
@@ -29,6 +30,16 @@ export enum ComponentQualifier {
   TestFile = 'UTS'
 }
 
+export function isPortfolioLike(componentQualifier?: string | ComponentQualifier) {
+  return Boolean(
+    componentQualifier &&
+      [
+        ComponentQualifier.Portfolio.toString(),
+        ComponentQualifier.SubPortfolio.toString()
+      ].includes(componentQualifier)
+  );
+}
+
 export enum Visibility {
   Public = 'public',
   Private = 'private'