Browse Source

SONAR-22049 Align queryToSearch method

pull/3361/head
stanislavh 1 month ago
parent
commit
639bde5900
33 changed files with 183 additions and 173 deletions
  1. 2
    2
      server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx
  2. 5
    2
      server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx
  3. 2
    2
      server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx
  4. 2
    2
      server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
  5. 2
    2
      server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectAccordion.tsx
  6. 2
    2
      server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreateRenderer.tsx
  7. 5
    2
      server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectsList.tsx
  8. 2
    2
      server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx
  9. 5
    2
      server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudSearchForm.tsx
  10. 2
    2
      server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketImportRepositoryForm.tsx
  11. 2
    2
      server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectAccordion.tsx
  12. 2
    2
      server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx
  13. 2
    2
      server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreateRenderer.tsx
  14. 2
    2
      server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectSelectionForm.tsx
  15. 2
    2
      server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx
  16. 3
    3
      server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx
  17. 5
    2
      server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx
  18. 1
    1
      server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx
  19. 1
    1
      server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx
  20. 5
    2
      server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx
  21. 2
    2
      server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.tsx
  22. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/EmptyFavoriteSearch.tsx
  23. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx
  24. 2
    2
      server/sonar-web/src/main/js/apps/projects/components/__tests__/CreateApplication-test.tsx
  25. 5
    5
      server/sonar-web/src/main/js/apps/quality-profiles/utils.ts
  26. 2
    2
      server/sonar-web/src/main/js/apps/web-api/components/Action.tsx
  27. 2
    2
      server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx
  28. 3
    3
      server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx
  29. 23
    44
      server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
  30. 27
    23
      server/sonar-web/src/main/js/helpers/urls.ts
  31. 3
    3
      server/sonar-web/src/main/js/sonar-aligned/components/hoc/withRouter.tsx
  32. 40
    14
      server/sonar-web/src/main/js/sonar-aligned/helpers/__tests__/urls-test.ts
  33. 16
    30
      server/sonar-web/src/main/js/sonar-aligned/helpers/urls.ts

+ 2
- 2
server/sonar-web/src/main/js/app/components/indexation/IndexationNotificationRenderer.tsx View File

} from 'design-system'; } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import DocumentationLink from '../../../components/common/DocumentationLink'; import DocumentationLink from '../../../components/common/DocumentationLink';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import { IndexationNotificationType } from '../../../types/indexation'; import { IndexationNotificationType } from '../../../types/indexation';
<Link <Link
to={{ to={{
pathname: '/admin/background_tasks', pathname: '/admin/background_tasks',
search: queryToSearch({
search: queryToSearchString({
taskType: TaskTypes.IssueSync, taskType: TaskTypes.IssueSync,
status: hasError ? TaskStatuses.Failed : undefined, status: hasError ? TaskStatuses.Failed : undefined,
}), }),

+ 5
- 2
server/sonar-web/src/main/js/app/components/nav/component/branch-like/Menu.tsx View File

import * as React from 'react'; import * as React from 'react';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter'; import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { isBranch } from '~sonar-aligned/helpers/branch-like'; import { isBranch } from '~sonar-aligned/helpers/branch-like';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import { Router } from '~sonar-aligned/types/router'; import { Router } from '~sonar-aligned/types/router';
import { import {
onClick={() => { onClick={() => {
onClose(); onClose();
}} }}
to={{ pathname: '/project/branches', search: queryToSearch({ id: component.key }) }}
to={{
pathname: '/project/branches',
search: queryToSearchString({ id: component.key }),
}}
> >
{translate('branch_like_navigation.manage')} {translate('branch_like_navigation.manage')}
</Link> </Link>

+ 2
- 2
server/sonar-web/src/main/js/apps/audit-logs/components/AuditAppRenderer.tsx View File

import * as React from 'react'; import * as React from 'react';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import { now } from '../../../helpers/dates'; import { now } from '../../../helpers/dates';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
<Link <Link
to={{ to={{
pathname: '/admin/settings', pathname: '/admin/settings',
search: queryToSearch({ category: 'housekeeping' }),
search: queryToSearchString({ category: 'housekeeping' }),
hash: '#auditLogs', hash: '#auditLogs',
}} }}
> >

+ 2
- 2
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx View File

import { Badge, BranchIcon, LightLabel, Note, QualifierIcon } from 'design-system'; import { Badge, BranchIcon, LightLabel, Note, QualifierIcon } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like'; import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { isDefined } from '../../../helpers/types'; import { isDefined } from '../../../helpers/types';
<LinkStandalone <LinkStandalone
highlight={LinkHighlight.CurrentColor} highlight={LinkHighlight.CurrentColor}
iconLeft={showIcon && <QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />} iconLeft={showIcon && <QualifierIcon className="sw-mr-2" qualifier={component.qualifier} />}
to={{ pathname: '/code', search: queryToSearch(query) }}
to={{ pathname: '/code', search: queryToSearchString(query) }}
> >
{name} {name}
</LinkStandalone> </LinkStandalone>

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectAccordion.tsx View File

import { Accordion, FlagMessage, Link, SearchHighlighter, Spinner } from 'design-system'; import { Accordion, FlagMessage, Link, SearchHighlighter, Spinner } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import ListFooter from '../../../../components/controls/ListFooter'; import ListFooter from '../../../../components/controls/ListFooter';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/system'; import { getBaseUrl } from '../../../../helpers/system';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({
search: queryToSearchString({
mode: CreateProjectModes.AzureDevOps, mode: CreateProjectModes.AzureDevOps,
resetPat: 1, resetPat: 1,
}), }),

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectCreateRenderer.tsx View File

} from 'design-system'; } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { useAppState } from '../../../../app/components/app-state/withAppStateContext'; import { useAppState } from '../../../../app/components/app-state/withAppStateContext';
import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({
search: queryToSearchString({
mode: CreateProjectModes.AzureDevOps, mode: CreateProjectModes.AzureDevOps,
mono: true, mono: true,
}), }),

+ 5
- 2
server/sonar-web/src/main/js/apps/create/project/Azure/AzureProjectsList.tsx View File

import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import ListFooter from '../../../../components/controls/ListFooter'; import ListFooter from '../../../../components/controls/ListFooter';
import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { AzureProject, AzureRepository } from '../../../../types/alm-integration'; import { AzureProject, AzureRepository } from '../../../../types/alm-integration';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({ mode: CreateProjectModes.AzureDevOps, resetPat: 1 }),
search: queryToSearchString({
mode: CreateProjectModes.AzureDevOps,
resetPat: 1,
}),
}} }}
> >
{translate('onboarding.create_project.update_your_token')} {translate('onboarding.create_project.update_your_token')}

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudProjectCreateRender.tsx View File

import { LightPrimary, Title } from 'design-system'; import { LightPrimary, Title } from 'design-system';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { BitbucketCloudRepository } from '../../../../types/alm-integration'; import { BitbucketCloudRepository } from '../../../../types/alm-integration';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({
search: queryToSearchString({
mode: CreateProjectModes.BitbucketCloud, mode: CreateProjectModes.BitbucketCloud,
mono: true, mono: true,
}), }),

+ 5
- 2
server/sonar-web/src/main/js/apps/create/project/BitbucketCloud/BitbucketCloudSearchForm.tsx View File

import { FlagMessage, InputSearch, LightPrimary, Link } from 'design-system'; import { FlagMessage, InputSearch, LightPrimary, Link } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import ListFooter from '../../../../components/controls/ListFooter'; import ListFooter from '../../../../components/controls/ListFooter';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/system'; import { getBaseUrl } from '../../../../helpers/system';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({ mode: CreateProjectModes.BitbucketCloud, resetPat: 1 }),
search: queryToSearchString({
mode: CreateProjectModes.BitbucketCloud,
resetPat: 1,
}),
}} }}
> >
{translate('onboarding.create_project.update_your_token')} {translate('onboarding.create_project.update_your_token')}

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketImportRepositoryForm.tsx View File

import { FlagMessage, InputSearch, Link } from 'design-system'; import { FlagMessage, InputSearch, Link } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { import {
BitbucketProject, BitbucketProject,
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({
search: queryToSearchString({
mode: CreateProjectModes.BitbucketServer, mode: CreateProjectModes.BitbucketServer,
resetPat: 1, resetPat: 1,
}), }),

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/BitbucketServer/BitbucketProjectAccordion.tsx View File

import { Accordion, FlagMessage, Link } from 'design-system'; import { Accordion, FlagMessage, Link } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../../helpers/l10n';
import { getBaseUrl } from '../../../../helpers/system'; import { getBaseUrl } from '../../../../helpers/system';
import { BitbucketProject, BitbucketRepository } from '../../../../types/alm-integration'; import { BitbucketProject, BitbucketRepository } from '../../../../types/alm-integration';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({
search: queryToSearchString({
mode: CreateProjectModes.BitbucketServer, mode: CreateProjectModes.BitbucketServer,
resetPat: 1, resetPat: 1,
}), }),

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/Github/GitHubProjectCreateRenderer.tsx View File

import { DarkLabel, FlagMessage, InputSelect, LightPrimary, Title } from 'design-system'; import { DarkLabel, FlagMessage, InputSelect, LightPrimary, Title } from 'design-system';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { useAppState } from '../../../../app/components/app-state/withAppStateContext'; import { useAppState } from '../../../../app/components/app-state/withAppStateContext';
import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({
search: queryToSearchString({
mode: CreateProjectModes.GitHub, mode: CreateProjectModes.GitHub,
mono: true, mono: true,
}), }),

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectCreateRenderer.tsx View File

import { LightPrimary, Title } from 'design-system'; import { LightPrimary, Title } from 'design-system';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext'; import { AvailableFeaturesContext } from '../../../../app/components/available-features/AvailableFeaturesContext';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
import { GitlabProject } from '../../../../types/alm-integration'; import { GitlabProject } from '../../../../types/alm-integration';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({
search: queryToSearchString({
mode: CreateProjectModes.GitLab, mode: CreateProjectModes.GitLab,
mono: true, mono: true,
}), }),

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/Gitlab/GitlabProjectSelectionForm.tsx View File

import { FlagMessage, InputSearch, LightPrimary } from 'design-system'; import { FlagMessage, InputSearch, LightPrimary } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import ListFooter from '../../../../components/controls/ListFooter'; import ListFooter from '../../../../components/controls/ListFooter';
import Tooltip from '../../../../components/controls/Tooltip'; import Tooltip from '../../../../components/controls/Tooltip';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
<Link <Link
to={{ to={{
pathname: '/projects/create', pathname: '/projects/create',
search: queryToSearch({ mode: CreateProjectModes.GitLab, resetPat: 1 }),
search: queryToSearchString({ mode: CreateProjectModes.GitLab, resetPat: 1 }),
}} }}
> >
{translate('onboarding.create_project.update_your_token')} {translate('onboarding.create_project.update_your_token')}

+ 2
- 2
server/sonar-web/src/main/js/apps/create/project/components/NewCodeDefinitionSelection.tsx View File

import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { useNavigate, unstable_usePrompt as usePrompt } from 'react-router-dom'; import { useNavigate, unstable_usePrompt as usePrompt } from 'react-router-dom';
import { useLocation } from '~sonar-aligned/components/hoc/withRouter'; import { useLocation } from '~sonar-aligned/components/hoc/withRouter';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector'; import NewCodeDefinitionSelector from '../../../../components/new-code-definition/NewCodeDefinitionSelector';
import { useDocUrl } from '../../../../helpers/docs'; import { useDocUrl } from '../../../../helpers/docs';
import { translate } from '../../../../helpers/l10n'; import { translate } from '../../../../helpers/l10n';
} else { } else {
navigate({ navigate({
pathname: '/projects', pathname: '/projects',
search: queryToSearch({ sort: '-creation_date' }),
search: queryToSearchString({ sort: '-creation_date' }),
}); });
} }
}; };

+ 3
- 3
server/sonar-web/src/main/js/apps/overview/branches/FirstAnalysisNextStepsNotif.tsx View File

import { Link } from 'design-system'; import { Link } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext';
import DismissableAlert from '../../../components/ui/DismissableAlert'; import DismissableAlert from '../../../components/ui/DismissableAlert';
<Link <Link
to={{ to={{
pathname: '/tutorials', pathname: '/tutorials',
search: queryToSearch({ id: component.key }),
search: queryToSearchString({ id: component.key }),
}} }}
> >
{translate('overview.project.next_steps.links.set_up_ci')} {translate('overview.project.next_steps.links.set_up_ci')}
<Link <Link
to={{ to={{
pathname: '/project/settings', pathname: '/project/settings',
search: queryToSearch({
search: queryToSearchString({
id: component.key, id: component.key,
category: PULL_REQUEST_DECORATION_BINDING_CATEGORY, category: PULL_REQUEST_DECORATION_BINDING_CATEGORY,
}), }),

+ 5
- 2
server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanelNoNewCode.tsx View File

import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like'; import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import DocumentationLink from '../../../components/common/DocumentationLink'; import DocumentationLink from '../../../components/common/DocumentationLink';
import { Image } from '../../../components/common/Image'; import { Image } from '../../../components/common/Image';
<Link <Link
to={{ to={{
pathname: '/project/baseline', pathname: '/project/baseline',
search: queryToSearch({ id: component.key, ...getBranchLikeQuery(branch) }),
search: queryToSearchString({
id: component.key,
...getBranchLikeQuery(branch),
}),
}} }}
> >
{translate('settings.new_code_period.category')} {translate('settings.new_code_period.category')}

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/branches/QualityGateCondition.tsx View File



const metricKey = condition.measure.metric.key; const metricKey = condition.measure.metric.key;


const METRICS_TO_URL_MAPPING: Dict<() => Path> = {
const METRICS_TO_URL_MAPPING: Dict<() => Partial<Path>> = {
[MetricKey.reliability_rating]: () => [MetricKey.reliability_rating]: () =>
this.getUrlForBugsOrVulnerabilities(IssueType.Bug, false), this.getUrlForBugsOrVulnerabilities(IssueType.Bug, false),
[MetricKey.new_reliability_rating]: () => [MetricKey.new_reliability_rating]: () =>

+ 1
- 1
server/sonar-web/src/main/js/apps/overview/components/IssueMeasuresCardInner.tsx View File

metric: MetricKey; metric: MetricKey;
value?: string; value?: string;
header: React.ReactNode; header: React.ReactNode;
url: Path;
url: Partial<Path>;
failed?: boolean; failed?: boolean;
icon?: React.ReactNode; icon?: React.ReactNode;
disabled?: boolean; disabled?: boolean;

+ 5
- 2
server/sonar-web/src/main/js/apps/permission-templates/components/ActionsCell.tsx View File

import { difference } from 'lodash'; import { difference } from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { withRouter } from '~sonar-aligned/components/hoc/withRouter'; import { withRouter } from '~sonar-aligned/components/hoc/withRouter';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { Router } from '~sonar-aligned/types/router'; import { Router } from '~sonar-aligned/types/router';
import { import {
deletePermissionTemplate, deletePermissionTemplate,


{!this.props.fromDetails && ( {!this.props.fromDetails && (
<ItemLink <ItemLink
to={{ pathname: PERMISSION_TEMPLATES_PATH, search: queryToSearch({ id: t.id }) }}
to={{
pathname: PERMISSION_TEMPLATES_PATH,
search: queryToSearchString({ id: t.id }),
}}
> >
{translate('edit_permissions')} {translate('edit_permissions')}
</ItemLink> </ItemLink>

+ 2
- 2
server/sonar-web/src/main/js/apps/permission-templates/components/NameCell.tsx View File

*/ */
import { CodeSnippet, ContentCell, Link } from 'design-system'; import { CodeSnippet, ContentCell, Link } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { PermissionTemplate } from '../../../types/types'; import { PermissionTemplate } from '../../../types/types';
import { PERMISSION_TEMPLATES_PATH } from '../utils'; import { PERMISSION_TEMPLATES_PATH } from '../utils';
import Defaults from './Defaults'; import Defaults from './Defaults';
<ContentCell> <ContentCell>
<div className="sw-flex sw-flex-col"> <div className="sw-flex sw-flex-col">
<span> <span>
<Link to={{ pathname, search: queryToSearch({ id: template.id }) }}>
<Link to={{ pathname, search: queryToSearchString({ id: template.id }) }}>
<span className="js-name">{template.name}</span> <span className="js-name">{template.name}</span>
</Link> </Link>
</span> </span>

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/EmptyFavoriteSearch.tsx View File

import { FishVisual, Highlight, StandoutLink } from 'design-system'; import { FishVisual, Highlight, StandoutLink } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { Dict } from '../../../types/types'; import { Dict } from '../../../types/types';
import { Query } from '../query'; import { Query } from '../query';
<StandoutLink <StandoutLink
to={{ to={{
pathname: '/projects', pathname: '/projects',
search: queryToSearch(query as Dict<string | undefined | number>),
search: queryToSearchString(query as Dict<string | undefined | number>),
}} }}
> >
{translate('all')} {translate('all')}

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/ProjectCreationMenuItem.tsx View File



import { ItemLink } from 'design-system'; import { ItemLink } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { Image } from '../../../components/common/Image'; import { Image } from '../../../components/common/Image';
import { translate } from '../../../helpers/l10n'; import { translate } from '../../../helpers/l10n';
import { AlmKeys } from '../../../types/alm-settings'; import { AlmKeys } from '../../../types/alm-settings';
return ( return (
<ItemLink <ItemLink
className="sw-flex sw-items-center" className="sw-flex sw-items-center"
to={{ pathname: '/projects/create', search: queryToSearch({ mode: alm }) }}
to={{ pathname: '/projects/create', search: queryToSearchString({ mode: alm }) }}
> >
{alm !== 'manual' && ( {alm !== 'manual' && (
<Image alt={alm} className="sw-mr-2" width={16} src={`/images/alm/${almIcon}.svg`} /> <Image alt={alm} className="sw-mr-2" width={16} src={`/images/alm/${almIcon}.svg`} />

+ 2
- 2
server/sonar-web/src/main/js/apps/projects/components/__tests__/CreateApplication-test.tsx View File

*/ */
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import * as React from 'react'; import * as React from 'react';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import { createApplication } from '../../../../api/application'; import { createApplication } from '../../../../api/application';
import { getComponentNavigation } from '../../../../api/navigation'; import { getComponentNavigation } from '../../../../api/navigation';
); );
expect(routerPush).toHaveBeenCalledWith({ expect(routerPush).toHaveBeenCalledWith({
pathname: '/project/admin/extension/developer-server/application-console', pathname: '/project/admin/extension/developer-server/application-console',
search: queryToSearch({
search: queryToSearchString({
id: 'app', id: 'app',
}), }),
}); });

+ 5
- 5
server/sonar-web/src/main/js/apps/quality-profiles/utils.ts View File

*/ */
import { differenceInYears } from 'date-fns'; import { differenceInYears } from 'date-fns';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { Profile as BaseProfile } from '../../api/quality-profiles'; import { Profile as BaseProfile } from '../../api/quality-profiles';
import { isValidDate, parseDate } from '../../helpers/dates'; import { isValidDate, parseDate } from '../../helpers/dates';
import { PROFILE_COMPARE_PATH, PROFILE_PATH } from './constants'; import { PROFILE_COMPARE_PATH, PROFILE_PATH } from './constants';


export const getProfilesForLanguagePath = (language: string) => ({ export const getProfilesForLanguagePath = (language: string) => ({
pathname: PROFILE_PATH, pathname: PROFILE_PATH,
search: queryToSearch({ language }),
search: queryToSearchString({ language }),
}); });


export const getProfilePath = (name: string, language: string) => ({ export const getProfilePath = (name: string, language: string) => ({
pathname: `${PROFILE_PATH}/show`, pathname: `${PROFILE_PATH}/show`,
search: queryToSearch({ name, language }),
search: queryToSearchString({ name, language }),
}); });


export const getProfileComparePath = (name: string, language: string, withKey?: string) => { export const getProfileComparePath = (name: string, language: string, withKey?: string) => {
} }
return { return {
pathname: PROFILE_COMPARE_PATH, pathname: PROFILE_COMPARE_PATH,
search: queryToSearch(query),
search: queryToSearchString(query),
}; };
}; };


} }
return { return {
pathname: `${PROFILE_PATH}/changelog`, pathname: `${PROFILE_PATH}/changelog`,
search: queryToSearch(query),
search: queryToSearchString(query),
}; };
}; };



+ 2
- 2
server/sonar-web/src/main/js/apps/web-api/components/Action.tsx View File

*/ */
import { Badge, Card, LinkBox, LinkIcon, SubHeading, Tabs } from 'design-system'; import { Badge, Card, LinkBox, LinkIcon, SubHeading, Tabs } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { translate, translateWithParameters } from '../../../helpers/l10n'; import { translate, translateWithParameters } from '../../../helpers/l10n';
import { WebApi } from '../../../types/types'; import { WebApi } from '../../../types/types';
import { getActionKey, serializeQuery } from '../utils'; import { getActionKey, serializeQuery } from '../utils';
<LinkBox <LinkBox
to={{ to={{
pathname: '/web_api/' + actionKey, pathname: '/web_api/' + actionKey,
search: queryToSearch(
search: queryToSearchString(
serializeQuery({ serializeQuery({
deprecated: Boolean(action.deprecatedSince), deprecated: Boolean(action.deprecatedSince),
internal: Boolean(action.internal), internal: Boolean(action.internal),

+ 2
- 2
server/sonar-web/src/main/js/apps/web-api/components/Menu.tsx View File

import { SubnavigationGroup, SubnavigationItem } from 'design-system'; import { SubnavigationGroup, SubnavigationItem } from 'design-system';
import * as React from 'react'; import * as React from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { WebApi } from '../../../types/types'; import { WebApi } from '../../../types/types';
import { Query, actionsFilter, isDomainPathActive, serializeQuery } from '../utils'; import { Query, actionsFilter, isDomainPathActive, serializeQuery } from '../utils';
import DeprecatedBadge from './DeprecatedBadge'; import DeprecatedBadge from './DeprecatedBadge';
(domainPath: string) => { (domainPath: string) => {
navigateTo({ navigateTo({
pathname: '/web_api/' + domainPath, pathname: '/web_api/' + domainPath,
search: queryToSearch(serializeQuery(query)),
search: queryToSearchString(serializeQuery(query)),
}); });
}, },
[query, navigateTo], [query, navigateTo],

+ 3
- 3
server/sonar-web/src/main/js/components/new-code-definition/NCDAutoUpdateMessage.tsx View File

import { Banner, Link } from 'design-system'; import { Banner, Link } from 'design-system';
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages'; import { MessageTypes, checkMessageDismissed, setMessageDismissed } from '../../api/messages';
import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext'; import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
isGlobalBanner isGlobalBanner
? { ? {
pathname: '/admin/settings', pathname: '/admin/settings',
search: queryToSearch({
search: queryToSearchString({
category: NEW_CODE_PERIOD_CATEGORY, category: NEW_CODE_PERIOD_CATEGORY,
}), }),
} }
: { : {
pathname: '/project/baseline', pathname: '/project/baseline',
search: queryToSearch({
search: queryToSearchString({
id: component.key, id: component.key,
}), }),
}, },

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

* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import { searchParamsToQuery } from '~sonar-aligned/helpers/router'; import { searchParamsToQuery } from '~sonar-aligned/helpers/router';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import { AlmKeys } from '../../types/alm-settings'; import { AlmKeys } from '../../types/alm-settings';
import { IssueType } from '../../types/issues'; import { IssueType } from '../../types/issues';
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Portfolio)).toEqual( expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Portfolio)).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/portfolio', pathname: '/portfolio',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY }),
}), }),
); );
}); });
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.SubPortfolio)).toEqual( expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.SubPortfolio)).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/portfolio', pathname: '/portfolio',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY }),
}), }),
); );
}); });
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Project)).toEqual( expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Project)).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/dashboard', pathname: '/dashboard',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY }),
}), }),
); );
}); });
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/dashboard', pathname: '/dashboard',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, code_scope: 'new' }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY, code_scope: 'new' }),
}), }),
); );
}); });
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/dashboard', pathname: '/dashboard',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, code_scope: 'overall' }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY, code_scope: 'overall' }),
}), }),
); );
}); });
expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Application)).toEqual( expect(getComponentOverviewUrl(SIMPLE_COMPONENT_KEY, ComponentQualifier.Application)).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/dashboard', pathname: '/dashboard',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY }),
}), }),
); );
}); });
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, metric: METRIC }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY, metric: METRIC }),
}), }),
); );
}); });
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({ id: COMPLEX_COMPONENT_KEY, metric: METRIC }),
search: queryToSearchString({ id: COMPLEX_COMPONENT_KEY, metric: METRIC }),
}), }),
); );
}); });
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY, metric: METRIC }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY, metric: METRIC }),
}), }),
); );


).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({
search: queryToSearchString({
id: SIMPLE_COMPONENT_KEY, id: SIMPLE_COMPONENT_KEY,
metric: METRIC, metric: METRIC,
view: 'list', view: 'list',
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({
search: queryToSearchString({
id: SIMPLE_COMPONENT_KEY, id: SIMPLE_COMPONENT_KEY,
metric: METRIC, metric: METRIC,
selected: COMPLEX_COMPONENT_KEY, selected: COMPLEX_COMPONENT_KEY,
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({
search: queryToSearchString({
id: SIMPLE_COMPONENT_KEY, id: SIMPLE_COMPONENT_KEY,
metric: METRIC, metric: METRIC,
branch: 'foo', branch: 'foo',
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({
search: queryToSearchString({
id: SIMPLE_COMPONENT_KEY, id: SIMPLE_COMPONENT_KEY,
metric: METRIC, metric: METRIC,
view: MeasurePageView.list, view: MeasurePageView.list,
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({
search: queryToSearchString({
id: SIMPLE_COMPONENT_KEY, id: SIMPLE_COMPONENT_KEY,
metric: METRIC, metric: METRIC,
view: MeasurePageView.treemap, view: MeasurePageView.treemap,
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/component_measures', pathname: '/component_measures',
search: queryToSearch({
search: queryToSearchString({
id: SIMPLE_COMPONENT_KEY, id: SIMPLE_COMPONENT_KEY,
metric: METRIC, metric: METRIC,
pullRequest: '1', pullRequest: '1',
const type = IssueType.Bug; const type = IssueType.Bug;
expect(getIssuesUrl({ type })).toEqual({ expect(getIssuesUrl({ type })).toEqual({
pathname: '/issues', pathname: '/issues',
search: queryToSearch({ type }),
search: queryToSearchString({ type }),
}); });
}); });
}); });
it('should work as expected', () => { it('should work as expected', () => {
expect(getGlobalSettingsUrl('foo')).toEqual({ expect(getGlobalSettingsUrl('foo')).toEqual({
pathname: '/admin/settings', pathname: '/admin/settings',
search: queryToSearch({ category: 'foo' }),
search: queryToSearchString({ category: 'foo' }),
}); });
expect(getGlobalSettingsUrl('foo', { alm: AlmKeys.GitHub })).toEqual({ expect(getGlobalSettingsUrl('foo', { alm: AlmKeys.GitHub })).toEqual({
pathname: '/admin/settings', pathname: '/admin/settings',
search: queryToSearch({ category: 'foo', alm: AlmKeys.GitHub }),
search: queryToSearchString({ category: 'foo', alm: AlmKeys.GitHub }),
}); });
}); });
}); });
it('should work as expected', () => { it('should work as expected', () => {
expect(getProjectSettingsUrl('foo')).toEqual({ expect(getProjectSettingsUrl('foo')).toEqual({
pathname: '/project/settings', pathname: '/project/settings',
search: queryToSearch({ id: 'foo' }),
search: queryToSearchString({ id: 'foo' }),
}); });
expect(getProjectSettingsUrl('foo', 'bar')).toEqual({ expect(getProjectSettingsUrl('foo', 'bar')).toEqual({
pathname: '/project/settings', pathname: '/project/settings',
search: queryToSearch({ id: 'foo', category: 'bar' }),
search: queryToSearchString({ id: 'foo', category: 'bar' }),
}); });
}); });
}); });
expect( expect(
getPathUrlAsString({ getPathUrlAsString({
pathname: '/dashboard', pathname: '/dashboard',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY }),
}), }),
).toBe('/dashboard?id=' + SIMPLE_COMPONENT_KEY); ).toBe('/dashboard?id=' + SIMPLE_COMPONENT_KEY);
}); });
expect( expect(
getPathUrlAsString({ getPathUrlAsString({
pathname: '/dashboard', pathname: '/dashboard',
search: queryToSearch({ id: COMPLEX_COMPONENT_KEY }),
search: queryToSearchString({ id: COMPLEX_COMPONENT_KEY }),
}), }),
).toBe('/dashboard?id=' + COMPLEX_COMPONENT_KEY_ENCODED); ).toBe('/dashboard?id=' + COMPLEX_COMPONENT_KEY_ENCODED);
}); });
}); });
}); });


describe('queryToSearch', () => {
it('should handle all types', () => {
const query = {
author: ['GRRM', 'JKR', 'Stross'],
b1: true,
b2: false,
emptyArray: [],
normalString: 'hello',
undef: undefined,
};

expect(queryToSearch(query)).toBe(
'?b1=true&b2=false&normalString=hello&author=GRRM&author=JKR&author=Stross',
);
});

it('should handle an missing query', () => {
expect(queryToSearch()).toBe('?');
});
});

describe('convertToTo', () => { describe('convertToTo', () => {
it('should handle locations with a query', () => { it('should handle locations with a query', () => {
expect(convertToTo(mockLocation({ pathname: '/account', query: { id: 1 } }))).toEqual({ expect(convertToTo(mockLocation({ pathname: '/account', query: { id: 1 } }))).toEqual({

+ 27
- 23
server/sonar-web/src/main/js/helpers/urls.ts View File

*/ */
import { Path, To } from 'react-router-dom'; import { Path, To } from 'react-router-dom';
import { getBranchLikeQuery, isBranch, isMainBranch } from '~sonar-aligned/helpers/branch-like'; import { getBranchLikeQuery, isBranch, isMainBranch } from '~sonar-aligned/helpers/branch-like';
import { queryToSearch } from '~sonar-aligned/helpers/urls';
import { queryToSearchString } from '~sonar-aligned/helpers/urls';
import { BranchParameters } from '~sonar-aligned/types/branch-like'; import { BranchParameters } from '~sonar-aligned/types/branch-like';
import { ComponentQualifier } from '~sonar-aligned/types/component'; import { ComponentQualifier } from '~sonar-aligned/types/component';
import { getProfilePath } from '../apps/quality-profiles/utils'; import { getProfilePath } from '../apps/quality-profiles/utils';
): Partial<Path> { ): Partial<Path> {
return { return {
pathname: PROJECT_BASE_URL, pathname: PROJECT_BASE_URL,
search: queryToSearch({ id: project, branch, ...(codeScope && { code_scope: codeScope }) }),
search: queryToSearchString({
id: project,
branch,
...(codeScope && { code_scope: codeScope }),
}),
}; };
} }


export function getProjectSecurityHotspots(project: string): To { export function getProjectSecurityHotspots(project: string): To {
return { return {
pathname: '/security_hotspots', pathname: '/security_hotspots',
search: queryToSearch({ id: project }),
search: queryToSearchString({ id: project }),
}; };
} }


): To { ): To {
return { return {
pathname: PROJECT_BASE_URL, pathname: PROJECT_BASE_URL,
search: queryToSearch({
search: queryToSearchString({
id: project, id: project,
...branchParameters, ...branchParameters,
...(codeScope && { code_scope: codeScope }), ...(codeScope && { code_scope: codeScope }),
} }


export function getPortfolioUrl(key: string): To { export function getPortfolioUrl(key: string): To {
return { pathname: '/portfolio', search: queryToSearch({ id: key }) };
return { pathname: '/portfolio', search: queryToSearchString({ id: key }) };
} }


export function getPortfolioAdminUrl(key: string): To { export function getPortfolioAdminUrl(key: string): To {
return { return {
pathname: '/project/admin/extension/governance/console', pathname: '/project/admin/extension/governance/console',
search: queryToSearch({ id: key, qualifier: ComponentQualifier.Portfolio }),
search: queryToSearchString({ id: key, qualifier: ComponentQualifier.Portfolio }),
}; };
} }


export function getApplicationAdminUrl(key: string): To { export function getApplicationAdminUrl(key: string): To {
return { return {
pathname: '/project/admin/extension/developer-server/application-console', pathname: '/project/admin/extension/developer-server/application-console',
search: queryToSearch({ id: key }),
search: queryToSearchString({ id: key }),
}; };
} }


componentKey: string, componentKey: string,
status?: string, status?: string,
taskType?: string, taskType?: string,
): Path {
): Partial<Path> {
return { return {
pathname: '/project/background_tasks', pathname: '/project/background_tasks',
search: queryToSearch({ id: componentKey, status, taskType }),
search: queryToSearchString({ id: componentKey, status, taskType }),
hash: '', hash: '',
}; };
} }
} }


export function getBranchUrl(project: string, branch: string): Partial<Path> { export function getBranchUrl(project: string, branch: string): Partial<Path> {
return { pathname: PROJECT_BASE_URL, search: queryToSearch({ branch, id: project }) };
return { pathname: PROJECT_BASE_URL, search: queryToSearchString({ branch, id: project }) };
} }


export function getPullRequestUrl(project: string, pullRequest: string): Partial<Path> { export function getPullRequestUrl(project: string, pullRequest: string): Partial<Path> {
return { pathname: PROJECT_BASE_URL, search: queryToSearch({ id: project, pullRequest }) };
return { pathname: PROJECT_BASE_URL, search: queryToSearchString({ id: project, pullRequest }) };
} }


/** /**
*/ */
export function getIssuesUrl(query: Query): To { export function getIssuesUrl(query: Query): To {
const pathname = '/issues'; const pathname = '/issues';
return { pathname, search: queryToSearch(query) };
return { pathname, search: queryToSearchString(query) };
} }


/** /**
if (selectionKey) { if (selectionKey) {
query.selected = selectionKey; query.selected = selectionKey;
} }
return { pathname: '/component_measures', search: queryToSearch(query) };
return { pathname: '/component_measures', search: queryToSearchString(query) };
} }


export function getComponentDrilldownUrlWithSelection( export function getComponentDrilldownUrlWithSelection(
export function getActivityUrl(component: string, branchLike?: BranchLike, graph?: GraphType) { export function getActivityUrl(component: string, branchLike?: BranchLike, graph?: GraphType) {
return { return {
pathname: '/project/activity', pathname: '/project/activity',
search: queryToSearch({ id: component, graph, ...getBranchLikeQuery(branchLike) }),
search: queryToSearchString({ id: component, graph, ...getBranchLikeQuery(branchLike) }),
}; };
} }


export function getMeasureHistoryUrl(component: string, metric: string, branchLike?: BranchLike) { export function getMeasureHistoryUrl(component: string, metric: string, branchLike?: BranchLike) {
return { return {
pathname: '/project/activity', pathname: '/project/activity',
search: queryToSearch({
search: queryToSearchString({
id: component, id: component,
graph: 'custom', graph: 'custom',
custom_metrics: metric, custom_metrics: metric,
* Generate URL for a component's permissions page * Generate URL for a component's permissions page
*/ */
export function getComponentPermissionsUrl(componentKey: string): To { export function getComponentPermissionsUrl(componentKey: string): To {
return { pathname: '/project_roles', search: queryToSearch({ id: componentKey }) };
return { pathname: '/project_roles', search: queryToSearchString({ id: componentKey }) };
} }


/** /**
): Partial<Path> { ): Partial<Path> {
return { return {
pathname: '/tutorials', pathname: '/tutorials',
search: queryToSearch({ id: project, selectedTutorial }),
search: queryToSearchString({ id: project, selectedTutorial }),
}; };
} }


*/ */
export function getCreateProjectModeLocation(mode?: string): Partial<Path> { export function getCreateProjectModeLocation(mode?: string): Partial<Path> {
return { return {
search: queryToSearch({ mode }),
search: queryToSearchString({ mode }),
}; };
} }


): Partial<Path> { ): Partial<Path> {
return { return {
pathname: '/admin/settings', pathname: '/admin/settings',
search: queryToSearch({ category, ...query }),
search: queryToSearchString({ category, ...query }),
}; };
} }


export function getProjectSettingsUrl(id: string, category?: string): Partial<Path> { export function getProjectSettingsUrl(id: string, category?: string): Partial<Path> {
return { return {
pathname: '/project/settings', pathname: '/project/settings',
search: queryToSearch({ id, category }),
search: queryToSearchString({ id, category }),
}; };
} }


* Generate URL for the rules page * Generate URL for the rules page
*/ */
export function getRulesUrl(query: Query): Partial<Path> { export function getRulesUrl(query: Query): Partial<Path> {
return { pathname: '/coding_rules', search: queryToSearch(query) };
return { pathname: '/coding_rules', search: queryToSearchString(query) };
} }


/** /**
): Partial<Path> { ): Partial<Path> {
return { return {
pathname: '/code', pathname: '/code',
search: queryToSearch({
search: queryToSearchString({
id: project, id: project,
...getBranchLikeQuery(branchLike), ...getBranchLikeQuery(branchLike),
selected, selected,


export function convertToTo(link: string | Location) { export function convertToTo(link: string | Location) {
if (linkIsLocation(link)) { if (linkIsLocation(link)) {
return { pathname: link.pathname, search: queryToSearch(link.query) } as Partial<Path>;
return { pathname: link.pathname, search: queryToSearchString(link.query) } as Partial<Path>;
} }
return link; return link;
} }

+ 3
- 3
server/sonar-web/src/main/js/sonar-aligned/components/hoc/withRouter.tsx View File

useSearchParams, useSearchParams,
} from 'react-router-dom'; } from 'react-router-dom';
import { searchParamsToQuery } from '../../helpers/router'; import { searchParamsToQuery } from '../../helpers/router';
import { queryToSearch } from '../../helpers/urls';
import { queryToSearchString } from '../../helpers/urls';
import { Location, Router } from '../../types/router'; import { Location, Router } from '../../types/router';
import { getWrappedDisplayName } from './utils'; import { getWrappedDisplayName } from './utils';


() => ({ () => ({
replace: (path: string | Partial<Location>) => { replace: (path: string | Partial<Location>) => {
if ((path as Location).query) { if ((path as Location).query) {
path.search = queryToSearch((path as Location).query);
path.search = queryToSearchString((path as Location).query);
} }
navigate(path, { replace: true }); navigate(path, { replace: true });
}, },
push: (path: string | Partial<Location>) => { push: (path: string | Partial<Location>) => {
if ((path as Location).query) { if ((path as Location).query) {
path.search = queryToSearch((path as Location).query);
path.search = queryToSearchString((path as Location).query);
} }
navigate(path); navigate(path);
}, },

+ 40
- 14
server/sonar-web/src/main/js/sonar-aligned/helpers/__tests__/urls-test.ts View File

*/ */
import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils';
import { SecurityStandard } from '../../../types/security'; import { SecurityStandard } from '../../../types/security';
import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl, queryToSearch } from '../urls';
import {
getComponentIssuesUrl,
getComponentSecurityHotspotsUrl,
queryToSearchString,
} from '../urls';


const SIMPLE_COMPONENT_KEY = 'sonarqube'; const SIMPLE_COMPONENT_KEY = 'sonarqube';


describe('queryToSearch', () => {
it('should return query by default', () => {
expect(queryToSearch()).toBe('?');
describe('#queryToSearchString', () => {
it('should handle query as array', () => {
expect(
queryToSearchString([
['key1', 'value1'],
['key1', 'value2'],
['key2', 'value1'],
]),
).toBe('?key1=value1&key1=value2&key2=value1');
});

it('should handle query as string', () => {
expect(queryToSearchString('a=1')).toBe('?a=1');
}); });


it('should return query with string values', () => {
expect(queryToSearch({ key: 'value' })).toBe('?key=value');
it('should handle query as URLSearchParams', () => {
expect(queryToSearchString(new URLSearchParams({ a: '1', b: '2' }))).toBe('?a=1&b=2');
}); });


it('should remove empty values', () => {
expect(queryToSearch({ key: 'value', anotherKey: '' })).toBe('?key=value');
it('should handle all types', () => {
const query = {
author: ['GRRM', 'JKR', 'Stross'],
b1: true,
b2: false,
number: 0,
emptyArray: [],
normalString: 'hello',
undef: undefined,
};

expect(queryToSearchString(query)).toBe(
'?author=GRRM&author=JKR&author=Stross&b1=true&b2=false&number=0&normalString=hello',
);
}); });


it('should return query with array values', () => {
expect(queryToSearch({ key: ['value1', 'value2'] })).toBe('?key=value1&key=value2');
it('should handle an missing query', () => {
expect(queryToSearchString()).toBeUndefined();
}); });
}); });


expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY)).toEqual( expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY)).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/project/issues', pathname: '/project/issues',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY }),
}), }),
); );
}); });
expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, DEFAULT_ISSUES_QUERY)).toEqual( expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, DEFAULT_ISSUES_QUERY)).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/project/issues', pathname: '/project/issues',
search: queryToSearch({ ...DEFAULT_ISSUES_QUERY, id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ ...DEFAULT_ISSUES_QUERY, id: SIMPLE_COMPONENT_KEY }),
}), }),
); );
}); });
expect(getComponentSecurityHotspotsUrl(SIMPLE_COMPONENT_KEY)).toEqual( expect(getComponentSecurityHotspotsUrl(SIMPLE_COMPONENT_KEY)).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/security_hotspots', pathname: '/security_hotspots',
search: queryToSearch({ id: SIMPLE_COMPONENT_KEY }),
search: queryToSearchString({ id: SIMPLE_COMPONENT_KEY }),
}), }),
); );
}); });
).toEqual( ).toEqual(
expect.objectContaining({ expect.objectContaining({
pathname: '/security_hotspots', pathname: '/security_hotspots',
search: queryToSearch({
search: queryToSearchString({
id: SIMPLE_COMPONENT_KEY, id: SIMPLE_COMPONENT_KEY,
inNewCodePeriod: 'true', inNewCodePeriod: 'true',
[SecurityStandard.OWASP_TOP10_2021]: 'a1', [SecurityStandard.OWASP_TOP10_2021]: 'a1',

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

* along with this program; if not, write to the Free Software Foundation, * along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/ */
import { isArray, mapValues, omitBy, pick } from 'lodash';
import { Path } from 'react-router-dom';
import { mapValues, omitBy, pick } from 'lodash';
import { Path, URLSearchParamsInit, createSearchParams } from 'react-router-dom';
import { cleanQuery } from '../../helpers/query';
import { Query } from '../../helpers/urls'; import { Query } from '../../helpers/urls';
import { BranchLike } from '../../types/branch-like'; import { BranchLike } from '../../types/branch-like';
import { SecurityStandard } from '../../types/security'; import { SecurityStandard } from '../../types/security';
import { getBranchLikeQuery } from '../helpers/branch-like'; import { getBranchLikeQuery } from '../helpers/branch-like';
import { RawQuery } from '../types/router'; import { RawQuery } from '../types/router';


export function queryToSearch(query: RawQuery = {}) {
const arrayParams: Array<{ key: string; values: string[] }> = [];
export function queryToSearchString(query: RawQuery | URLSearchParamsInit = {}) {
let filteredQuery = query;


const stringParams = mapValues(query, (value, key) => {
// array values are added afterwards
if (isArray(value)) {
arrayParams.push({ key, values: value });
return '';
}
if (typeof query !== 'string' && !Array.isArray(query) && !(query instanceof URLSearchParams)) {
filteredQuery = cleanQuery(query);
mapValues(filteredQuery, (value) => (value as string).toString());
filteredQuery = omitBy(filteredQuery, (value) => value.length === 0);
}


return value != null ? `${value}` : '';
});
const filteredParams = omitBy(stringParams, (v: string) => v.length === 0);
const searchParams = new URLSearchParams(filteredParams);
const queryString = createSearchParams(filteredQuery as URLSearchParamsInit).toString();


/*
* Add each value separately
* e.g. author: ['a', 'b'] should be serialized as
* author=a&author=b
*/
arrayParams.forEach(({ key, values }) => {
values.forEach((value) => {
searchParams.append(key, value);
});
});

return `?${searchParams.toString()}`;
return queryString ? `?${queryString}` : undefined;
} }


/** /**
* Generate URL for a component's issues page * Generate URL for a component's issues page
*/ */
export function getComponentIssuesUrl(componentKey: string, query?: Query): Path {
export function getComponentIssuesUrl(componentKey: string, query?: Query): Partial<Path> {
return { return {
pathname: '/project/issues', pathname: '/project/issues',
search: queryToSearch({ ...(query || {}), id: componentKey }),
search: queryToSearchString({ ...(query || {}), id: componentKey }),
hash: '', hash: '',
}; };
} }
componentKey: string, componentKey: string,
branchLike?: BranchLike, branchLike?: BranchLike,
query: Query = {}, query: Query = {},
): Path {
): Partial<Path> {
const { inNewCodePeriod, hotspots, assignedToMe, files } = query; const { inNewCodePeriod, hotspots, assignedToMe, files } = query;
return { return {
pathname: '/security_hotspots', pathname: '/security_hotspots',
search: queryToSearch({
search: queryToSearchString({
id: componentKey, id: componentKey,
inNewCodePeriod, inNewCodePeriod,
hotspots, hotspots,

Loading…
Cancel
Save