From: Jeremy Davis Date: Fri, 1 May 2020 08:50:58 +0000 (+0200) Subject: SONAR-13342 Fix faulty links X-Git-Tag: 8.4.0.35506~227 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=d2dc8046525bb4538ca4d7ba4c5bc91346e31596;p=sonarqube.git SONAR-13342 Fix faulty links --- diff --git a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx index 4a8e7cdfdd6..cb82b50cc0b 100644 --- a/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/ComponentContainer.tsx @@ -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: Pick; registerBranchStatus: (branchLike: BranchLike, component: string, status: T.Status) => void; requireAuthorization: (router: Pick) => void; router: Pick; @@ -116,9 +117,18 @@ export class ComponentContainer extends React.PureComponent { .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) diff --git a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx index fa61e919640..a69eb36c59b 100644 --- a/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx +++ b/server/sonar-web/src/main/js/app/components/__tests__/ComponentContainer-test.tsx @@ -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).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).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).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 = {}) { return shallow( + to={getComponentOverviewUrl(breadcrumbElement.key, breadcrumbElement.qualifier)}> {breadcrumbElement.name} ) : ( diff --git a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx index b217d44e23c..8fc0f56a6dd 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/component/Menu.tsx @@ -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 { 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 { 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 (
  • - + {translate('overview.page')}
  • diff --git a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap index 314c7e511a6..161aff0449d 100644 --- a/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/nav/component/__tests__/__snapshots__/Breadcrumb-test.tsx.snap @@ -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", }, } diff --git a/server/sonar-web/src/main/js/app/components/search/Search.tsx b/server/sonar-web/src/main/js/app/components/search/Search.tsx index 7db674fef92..ef00d7b8fad 100644 --- a/server/sonar-web/src/main/js/app/components/search/Search.tsx +++ b/server/sonar-web/src/main/js/app/components/search/Search.tsx @@ -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 { }; 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(); } }; diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx index 1bf2f4bee51..7f99d49e15d 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx +++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx @@ -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 { 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 (
  • { @@ -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 = {}) { ); } -function component(key: string, qualifier = 'TRK') { +function component(key: string, qualifier = ComponentQualifier.Project) { return { key, name: key, qualifier }; } diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap index 21d0975f905..db6ada7d8ae 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap @@ -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", }, } diff --git a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx index 00bcb60c1aa..e0b3c6e04e6 100644 --- a/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx +++ b/server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx @@ -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) {

    - {project.name} + {project.name}

    {orderedLinks.length > 0 && ( diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx index 9a7b9662ebb..18a7ffb4322 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/TaskComponent.tsx @@ -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); diff --git a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx index 90847a9a49b..7f7d7d32564 100644 --- a/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx +++ b/server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx @@ -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 = ( - + {name} ); diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap index 4cc11714524..3978fbe2c62 100644 --- a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap @@ -187,6 +187,7 @@ foo" Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "src/main/ts/app", }, } diff --git a/server/sonar-web/src/main/js/apps/overview/components/App.tsx b/server/sonar-web/src/main/js/apps/overview/components/App.tsx index 2fb35de7b06..93eb9ca5a2f 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/App.tsx @@ -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 { isPortfolio = () => { - return ([ComponentQualifier.Portfolio, ComponentQualifier.SubPortfolio] as string[]).includes( - this.props.component.qualifier - ); + return isPortfolioLike(this.props.component.qualifier); }; render() { diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx index 1808a844967..0fa73bc841e 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx +++ b/server/sonar-web/src/main/js/apps/portfolio/components/WorstProjects.tsx @@ -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 + to={getComponentOverviewUrl( + component.refKey || component.key, + component.qualifier + )}> {component.name} - {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')} diff --git a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap index 7fbb4c9db8b..9ed6966558b 100644 --- a/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/portfolio/components/__tests__/__snapshots__/WorstProjects-test.tsx.snap @@ -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", }, } diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx index c04d80194dd..fe4a70a5f4a 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx +++ b/server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx @@ -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 { 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 { - + diff --git a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap index 52589366e34..c11442aa7ce 100644 --- a/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectsManagement/__tests__/__snapshots__/ProjectRow-test.tsx.snap @@ -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", }, } diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx index 06ae37a2e10..74a9a425c92 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/ProfileProjects.tsx @@ -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 {
      {projects.map(project => (
    • - + {project.name}
    • diff --git a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap index de37df033c2..32a90773230 100644 --- a/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/__snapshots__/ProfileProjects-test.tsx.snap @@ -69,6 +69,7 @@ exports[`should render correctly 2`] = ` Object { "pathname": "/dashboard", "query": Object { + "branch": undefined, "id": "org.sonarsource.xml:xml", }, } diff --git a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts index 7e4ac320884..cd229908f90 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts +++ b/server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts @@ -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( diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 8727dd2dd96..ee7e4bf07ed 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -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 } }; } diff --git a/server/sonar-web/src/main/js/types/component.ts b/server/sonar-web/src/main/js/types/component.ts index 6d059f21d54..682fe0ecc8d 100644 --- a/server/sonar-web/src/main/js/types/component.ts +++ b/server/sonar-web/src/main/js/types/component.ts @@ -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'