Browse Source

SONAR-13342 Fix faulty links

tags/8.4.0.35506
Jeremy Davis 4 years ago
parent
commit
d2dc804652
23 changed files with 192 additions and 97 deletions
  1. 14
    4
      server/sonar-web/src/main/js/app/components/ComponentContainer.tsx
  2. 19
    19
      server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx
  3. 2
    2
      server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx
  4. 7
    7
      server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx
  5. 1
    2
      server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap
  6. 22
    11
      server/sonar-web/src/main/js/app/components/search/Search.tsx
  7. 2
    2
      server/sonar-web/src/main/js/app/components/search/SearchResult.tsx
  8. 44
    9
      server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx
  9. 0
    8
      server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap
  10. 2
    1
      server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
  11. 2
    2
      server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx
  12. 3
    4
      server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
  13. 1
    0
      server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap
  14. 2
    4
      server/sonar-web/src/main/js/apps/overview/components/App.tsx
  15. 7
    3
      server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx
  16. 1
    4
      server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap
  17. 4
    9
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
  18. 0
    2
      server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap
  19. 2
    3
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx
  20. 1
    0
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap
  21. 29
    0
      server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
  22. 16
    1
      server/sonar-web/src/main/js/helpers/urls.ts
  23. 11
    0
      server/sonar-web/src/main/js/types/component.ts

+ 14
- 4
server/sonar-web/src/main/js/app/components/ComponentContainer.tsx View 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)

+ 19
- 19
server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx View 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

+ 2
- 2
server/sonar-web/src/main/js/app/components/nav/component/Breadcrumb.tsx View 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>
) : (

+ 7
- 7
server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx View 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>

+ 1
- 2
server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap View 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",
},
}

+ 22
- 11
server/sonar-web/src/main/js/app/components/search/Search.tsx View 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();
}
};


+ 2
- 2
server/sonar-web/src/main/js/app/components/search/SearchResult.tsx View 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

+ 44
- 9
server/sonar-web/src/main/js/app/components/search/__tests__/Search-test.tsx View 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 };
}


+ 0
- 8
server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap View 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",
},
}

+ 2
- 1
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx View 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 && (

+ 2
- 2
server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx View 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);

+ 3
- 4
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx View 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>
);

+ 1
- 0
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap View File

@@ -187,6 +187,7 @@ foo"
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "src/main/ts/app",
},
}

+ 2
- 4
server/sonar-web/src/main/js/apps/overview/components/App.tsx View 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() {

+ 7
- 3
server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx View 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')}

+ 1
- 4
server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap View 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",
},
}

+ 4
- 9
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx View 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">

+ 0
- 2
server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap View 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",
},
}

+ 2
- 3
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx View 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>

+ 1
- 0
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap View File

@@ -69,6 +69,7 @@ exports[`should render correctly 2`] = `
Object {
"pathname": "/dashboard",
"query": Object {
"branch": undefined,
"id": "org.sonarsource.xml:xml",
},
}

+ 29
- 0
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts View File

@@ -17,9 +17,11 @@
* 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(

+ 16
- 1
server/sonar-web/src/main/js/helpers/urls.ts View File

@@ -19,16 +19,31 @@
*/
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 } };
}

+ 11
- 0
server/sonar-web/src/main/js/types/component.ts View 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'

Loading…
Cancel
Save