diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2023-10-27 15:51:27 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-11-08 20:02:52 +0000 |
commit | a1be2cd1286ff3a24fc27d9c9a387069f5eafb91 (patch) | |
tree | 72f1828893ff5829da5a2dcb86128105b0911c49 | |
parent | 01a084c37da6150434a250334d933ea0443c06a2 (diff) | |
download | sonarqube-a1be2cd1286ff3a24fc27d9c9a387069f5eafb91.tar.gz sonarqube-a1be2cd1286ff3a24fc27d9c9a387069f5eafb91.zip |
SONAR-20871 Add new status facet in issues list
36 files changed, 381 insertions, 528 deletions
diff --git a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts index 581b44578d2..48027575795 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -20,13 +20,7 @@ import { cloneDeep, uniqueId } from 'lodash'; import { RuleDescriptionSections } from '../../apps/coding-rules/rule'; -import { - ISSUE_TYPES, - RESOLUTIONS, - SEVERITIES, - SOURCE_SCOPES, - STATUSES, -} from '../../helpers/constants'; +import { ISSUE_TYPES, SEVERITIES, SIMPLE_STATUSES, SOURCE_SCOPES } from '../../helpers/constants'; import { mockIssueAuthors, mockIssueChangelog } from '../../helpers/mocks/issues'; import { RequestData } from '../../helpers/request'; import { getStandards } from '../../helpers/security-standard'; @@ -336,9 +330,8 @@ export default class IssuesServiceMock { property: name, values: ( { - resolutions: RESOLUTIONS, severities: SEVERITIES, - statuses: STATUSES, + simpleStatuses: SIMPLE_STATUSES, types: ISSUE_TYPES, scopes: SOURCE_SCOPES.map(({ scope }) => scope), projects: ['org.project1', 'org.project2'], @@ -389,6 +382,11 @@ export default class IssuesServiceMock { // Filter list (only supports assignee, type and severity) const filteredList = this.list + .filter( + (item) => + !query.simpleStatuses || + query.simpleStatuses.split(',').includes(item.issue.simpleStatus), + ) .filter((item) => { if (!query.cleanCodeAttributeCategories) { return true; @@ -448,15 +446,10 @@ export default class IssuesServiceMock { (item) => !query.severities || query.severities.split(',').includes(item.issue.severity), ) .filter((item) => !query.scopes || query.scopes.split(',').includes(item.issue.scope)) - .filter((item) => !query.statuses || query.statuses.split(',').includes(item.issue.status)) .filter((item) => !query.projects || query.projects.split(',').includes(item.issue.project)) .filter((item) => !query.rules || query.rules.split(',').includes(item.issue.rule)) .filter( (item) => - !query.resolutions || query.resolutions.split(',').includes(item.issue.resolution), - ) - .filter( - (item) => !query.inNewCodePeriod || new Date(item.issue.creationDate) > new Date('2023-01-10'), ) .filter((item) => { diff --git a/server/sonar-web/src/main/js/api/mocks/data/issues.ts b/server/sonar-web/src/main/js/api/mocks/data/issues.ts index f3d42d750bf..116e77c6771 100644 --- a/server/sonar-web/src/main/js/api/mocks/data/issues.ts +++ b/server/sonar-web/src/main/js/api/mocks/data/issues.ts @@ -317,10 +317,10 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa impacts: [ { softwareQuality: SoftwareQuality.Security, severity: SoftwareImpactSeverity.High }, ], - ruleDescriptionContextKey: 'spring', resolution: IssueResolution.Unresolved, status: IssueStatus.Open, simpleStatus: IssueSimpleStatus.Open, + ruleDescriptionContextKey: 'spring', }), snippets: keyBy( [ @@ -347,6 +347,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa }, resolution: IssueResolution.Fixed, status: IssueStatus.Confirmed, + simpleStatus: IssueSimpleStatus.Confirmed, }), snippets: keyBy( [ 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 e37b2475bc6..c9c1f352fc2 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 @@ -87,7 +87,7 @@ it('should render the component nav correctly for portfolio', async () => { expect(await ui.portfolioTitle.find()).toHaveAttribute('href', '/portfolio?id=portfolioKey'); expect(ui.issuesPageLink.get()).toHaveAttribute( 'href', - '/project/issues?id=portfolioKey&resolved=false', + '/project/issues?id=portfolioKey&simpleStatuses=OPEN%2CCONFIRMED', ); expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=portfolioKey'); expect(ui.activityPageLink.get()).toHaveAttribute('href', '/project/activity?id=portfolioKey'); @@ -119,7 +119,7 @@ it('should render the component nav correctly for projects', async () => { expect(ui.overviewPageLink.get()).toHaveAttribute('href', '/dashboard?id=project-key'); expect(ui.issuesPageLink.get()).toHaveAttribute( 'href', - '/project/issues?id=project-key&resolved=false', + '/project/issues?id=project-key&simpleStatuses=OPEN%2CCONFIRMED', ); expect(ui.hotspotsPageLink.get()).toHaveAttribute('href', '/security_hotspots?id=project-key'); expect(ui.measuresPageLink.get()).toHaveAttribute('href', '/component_measures?id=project-key'); 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 03d341ff571..0ebe49b816c 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 @@ -27,6 +27,7 @@ import { } from 'design-system'; import * as React from 'react'; import Tooltip from '../../../../components/controls/Tooltip'; +import { DEFAULT_ISSUES_QUERY } from '../../../../components/shared/utils'; import { getBranchLikeQuery, isPullRequest } from '../../../../helpers/branch-like'; import { hasMessage, translate, translateWithParameters } from '../../../../helpers/l10n'; import { getPortfolioUrl, getProjectQueryUrl } from '../../../../helpers/urls'; @@ -183,7 +184,7 @@ export function Menu(props: Props) { return renderMenuLink({ label: translate('issues.page'), pathname: '/project/issues', - additionalQueryParams: { resolved: 'false' }, + additionalQueryParams: DEFAULT_ISSUES_QUERY, }); }; diff --git a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx index 6c81cdc7325..69d0ba9648b 100644 --- a/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx +++ b/server/sonar-web/src/main/js/app/components/nav/global/GlobalNavMenu.tsx @@ -23,6 +23,7 @@ import * as React from 'react'; import { NavLink } from 'react-router-dom'; import { isMySet } from '../../../../apps/issues/utils'; import Link from '../../../../components/common/Link'; +import { DEFAULT_ISSUES_QUERY } from '../../../../components/shared/utils'; import { translate } from '../../../../helpers/l10n'; import { getQualityGatesUrl } from '../../../../helpers/urls'; import { AppState } from '../../../../types/appstate'; @@ -71,8 +72,8 @@ class GlobalNavMenu extends React.PureComponent<Props> { renderIssuesLink() { const search = ( this.props.currentUser.isLoggedIn && isMySet() - ? new URLSearchParams({ resolved: 'false', myIssues: 'true' }) - : new URLSearchParams({ resolved: 'false' }) + ? new URLSearchParams({ myIssues: 'true', ...DEFAULT_ISSUES_QUERY }) + : new URLSearchParams(DEFAULT_ISSUES_QUERY) ).toString(); return ( diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx index f6f1431dd5f..9e66325c971 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsIssues.tsx @@ -27,6 +27,7 @@ import withAvailableFeatures, { WithAvailableFeaturesProps, } from '../../../app/components/available-features/withAvailableFeatures'; import Tooltip from '../../../components/controls/Tooltip'; +import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; import { translate } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; import { getIssuesUrl } from '../../../helpers/urls'; @@ -81,7 +82,7 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> { this.setState({ loading: true }); getFacet( { - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, rules: key, }, FacetName.Projects, @@ -139,7 +140,7 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> { if (total === undefined) { return null; } - const path = getIssuesUrl({ resolved: 'false', rules: key }); + const path = getIssuesUrl({ ...DEFAULT_ISSUES_QUERY, rules: key }); const totalItem = ( <span className="little-spacer-left"> @@ -163,7 +164,7 @@ export class RuleDetailsIssues extends React.PureComponent<Props, State> { ruleDetails: { key }, } = this.props; - const path = getIssuesUrl({ resolved: 'false', rules: key, projects: project.key }); + const path = getIssuesUrl({ ...DEFAULT_ISSUES_QUERY, rules: key, projects: project.key }); return ( <TableRow key={project.key}> <ContentCell>{project.name}</ContentCell> diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx index 527b152307d..ce0e9931252 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-Filtering-it.tsx @@ -86,17 +86,11 @@ describe('issues app filtering', () => { await user.click(ui.mainScopeFilter.get()); expect(ui.issueItem4.query()).not.toBeInTheDocument(); - // Resolution - await user.click(ui.resolutionFacet.get()); - await user.click(ui.fixedResolutionFilter.get()); - expect(ui.issueItem2.query()).not.toBeInTheDocument(); - // Check that filters were applied as expected expect(ui.issueItem6.get()).toBeInTheDocument(); // Status - await user.click(ui.statusFacet.get()); - + await user.click(ui.simpleStatusFacet.get()); await user.click(ui.openStatusFilter.get()); expect(ui.issueItem6.query()).not.toBeInTheDocument(); // Issue 6 should vanish @@ -106,9 +100,6 @@ describe('issues app filtering', () => { await user.keyboard('{/Control}'); expect(ui.issueItem6.get()).toBeInTheDocument(); // Issue 6 should come back - // Clear resolution filter - await user.click(ui.clearResolutionFacet.get()); - // Rule await user.click(ui.ruleFacet.get()); await user.click(screen.getByRole('checkbox', { name: 'other' })); @@ -154,7 +145,6 @@ describe('issues app filtering', () => { await user.click(ui.clearIssueTypeFacet.get()); await user.click(ui.clearSeverityFacet.get()); await user.click(ui.clearScopeFacet.get()); - await user.click(ui.clearStatusFacet.get()); await user.click(ui.clearRuleFacet.get()); await user.click(ui.clearTagFacet.get()); await user.click(ui.clearProjectFacet.get()); @@ -360,7 +350,7 @@ describe('issues app when reindexing', () => { expect(ui.resolutionFacet.query()).not.toBeInTheDocument(); expect(ui.ruleFacet.query()).not.toBeInTheDocument(); expect(ui.scopeFacet.query()).not.toBeInTheDocument(); - expect(ui.statusFacet.query()).not.toBeInTheDocument(); + expect(ui.simpleStatusFacet.query()).not.toBeInTheDocument(); expect(ui.tagFacet.query()).not.toBeInTheDocument(); // Indexation message diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts index 398f7b9b791..7f55fb9c1e2 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/utils-test.ts @@ -22,6 +22,7 @@ import { SoftwareImpactSeverity, SoftwareQuality, } from '../../../types/clean-code-taxonomy'; +import { IssueSimpleStatus } from '../../../types/issues'; import { SecurityStandard } from '../../../types/security'; import { parseQuery, @@ -62,15 +63,13 @@ describe('serialize/deserialize', () => { 'owaspAsvs-4.0': ['2'], owaspAsvsLevel: '2', projects: ['a', 'b'], - resolutions: ['a', 'b'], - resolved: true, rules: ['a', 'b'], sort: 'rules', scopes: ['a', 'b'], severities: ['a', 'b'], inNewCodePeriod: true, sonarsourceSecurity: ['a', 'b'], - statuses: ['a', 'b'], + simpleStatuses: [IssueSimpleStatus.Accepted, IssueSimpleStatus.Confirmed], tags: ['a', 'b'], types: ['a', 'b'], }), @@ -97,14 +96,13 @@ describe('serialize/deserialize', () => { 'owaspAsvs-4.0': '2', owaspAsvsLevel: '2', projects: 'a,b', - resolutions: 'a,b', rules: 'a,b', s: 'rules', scopes: 'a,b', inNewCodePeriod: 'true', severities: 'a,b', sonarsourceSecurity: 'a,b', - statuses: 'a,b', + simpleStatuses: 'ACCEPTED,CONFIRMED', tags: 'a,b', types: 'a,b', }); @@ -146,18 +144,79 @@ describe('serialize/deserialize', () => { 'pciDss-3.2': [], 'pciDss-4.0': [], projects: [], - resolutions: [], - resolved: true, rules: [], scopes: [], severities: ['CRITICAL', 'MAJOR'], sonarsourceSecurity: [], sort: '', - statuses: [], + simpleStatuses: [], tags: [], types: [], }); }); + + it('should map deprecated status and resolution query to new simple statuses', () => { + expect(parseQuery({ statuses: 'OPEN' }).simpleStatuses).toEqual([IssueSimpleStatus.Open]); + expect(parseQuery({ statuses: 'REOPENED' }).simpleStatuses).toEqual([IssueSimpleStatus.Open]); + expect(parseQuery({ statuses: 'CONFIRMED' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Confirmed, + ]); + expect(parseQuery({ statuses: 'RESOLVED' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Fixed, + IssueSimpleStatus.Accepted, + IssueSimpleStatus.FalsePositive, + ]); + expect(parseQuery({ statuses: 'OPEN,REOPENED' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Open, + ]); + expect(parseQuery({ statuses: 'OPEN,CONFIRMED' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Open, + IssueSimpleStatus.Confirmed, + ]); + + // Resolutions + expect(parseQuery({ resolutions: 'FALSE-POSITIVE' }).simpleStatuses).toEqual([ + IssueSimpleStatus.FalsePositive, + ]); + expect(parseQuery({ resolutions: 'WONTFIX' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Accepted, + ]); + expect(parseQuery({ resolutions: 'REMOVED' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Fixed, + ]); + expect(parseQuery({ resolutions: 'REMOVED,WONTFIX,FALSE-POSITIVE' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Fixed, + IssueSimpleStatus.Accepted, + IssueSimpleStatus.FalsePositive, + ]); + + // Both statuses and resolutions + expect( + parseQuery({ resolutions: 'FALSE-POSITIVE', statuses: 'RESOLVED' }).simpleStatuses, + ).toEqual([IssueSimpleStatus.FalsePositive]); + expect(parseQuery({ resolutions: 'WONTFIX', statuses: 'RESOLVED' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Accepted, + ]); + + // With resolved=false + expect( + parseQuery({ resolutions: 'WONTFIX', statuses: 'RESOLVED', resolved: 'false' }) + .simpleStatuses, + ).toEqual([IssueSimpleStatus.Accepted, IssueSimpleStatus.Open, IssueSimpleStatus.Confirmed]); + expect(parseQuery({ statuses: 'OPEN', resolved: 'false' }).simpleStatuses).toEqual([ + IssueSimpleStatus.Open, + ]); + + // With simple status + expect( + parseQuery({ + resolutions: 'WONTFIX', + statuses: 'RESOLVED', + resolved: 'false', + simpleStatuses: 'FIXED', + }).simpleStatuses, + ).toEqual([IssueSimpleStatus.Fixed]); + }); }); describe('shouldOpenStandardsFacet', () => { diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index e2ed868e887..fffb416f365 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -51,6 +51,7 @@ import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import IssueTabViewer from '../../../components/rules/IssueTabViewer'; import '../../../components/search-navigator.css'; +import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; import Spinner from '../../../components/ui/Spinner'; import { fillBranchLike, getBranchLikeQuery, isSameBranchLike } from '../../../helpers/branch-like'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; @@ -148,7 +149,6 @@ export interface State { selectedLocationIndex?: number; } -const DEFAULT_QUERY = { resolved: 'false' }; const MAX_INITAL_FETCH = 1000; const VARIANTS_FACET = 'codeVariants'; const ISSUES_PAGE_SIZE = 100; @@ -700,7 +700,7 @@ export class App extends React.PureComponent<Props, State> { isFiltered = () => { const serialized = serializeQuery(this.state.query); - return !areQueriesEqual(serialized, DEFAULT_QUERY); + return !areQueriesEqual(serialized, DEFAULT_ISSUES_QUERY); }; getCheckedIssues = () => { @@ -828,7 +828,7 @@ export class App extends React.PureComponent<Props, State> { this.props.router.push({ pathname: this.props.location.pathname, query: { - ...DEFAULT_QUERY, + ...DEFAULT_ISSUES_QUERY, ...getBranchLikeQuery(this.props.branchLike), id: this.props.component?.key, myIssues: this.state.myIssues ? 'true' : undefined, @@ -1156,7 +1156,6 @@ export class App extends React.PureComponent<Props, State> { checked={this.state.checked} component={component} issues={issues} - onFilterChange={this.handleFilterChange} onIssueChange={this.handleIssueChange} onIssueCheck={currentUser.isLoggedIn ? this.handleIssueCheck : undefined} onIssueSelect={this.selectIssue} @@ -1234,7 +1233,6 @@ export class App extends React.PureComponent<Props, State> { {this.renderHeader({ openIssue, paging })} <Spinner loading={loadingRule}> - {/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */} {openIssue && openRuleDetails ? ( <IssueTabViewer activityTabContent={ diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx index ffc1c603931..2c526415fa6 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesList.tsx @@ -19,18 +19,17 @@ */ import { groupBy } from 'lodash'; import * as React from 'react'; +import IssueItem from '../../../components/issue/Issue'; import { BranchLike } from '../../../types/branch-like'; import { Component, Issue } from '../../../types/types'; -import { Query } from '../utils'; + import ComponentBreadcrumbs from './ComponentBreadcrumbs'; -import ListItem from './ListItem'; interface Props { branchLike: BranchLike | undefined; checked: string[]; component: Component | undefined; issues: Issue[]; - onFilterChange: (changes: Partial<Query>) => void; onIssueChange: (issue: Issue) => void; onIssueCheck: ((issueKey: string) => void) | undefined; onIssueSelect: (issueKey: string) => void; @@ -69,7 +68,7 @@ export default class IssuesList extends React.PureComponent<Props, State> { </li> <ul> {issues.map((issue) => ( - <ListItem + <IssueItem branchLike={branchLike} checked={checked.includes(issue.key)} issue={issue} @@ -77,7 +76,6 @@ export default class IssuesList extends React.PureComponent<Props, State> { onChange={this.props.onIssueChange} onCheck={this.props.onIssueCheck} onSelect={this.props.onIssueSelect} - onFilterChange={this.props.onFilterChange} onPopupToggle={this.props.onPopupToggle} openPopup={openPopup && openPopup.issue === issue.key ? openPopup.name : undefined} selected={selectedIssue != null && selectedIssue.key === issue.key} diff --git a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx b/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx deleted file mode 100644 index 0dfaca0f545..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/components/ListItem.tsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import * as React from 'react'; -import Issue from '../../../components/issue/Issue'; -import { BranchLike } from '../../../types/branch-like'; -import { Issue as TypeIssue } from '../../../types/types'; -import { Query } from '../utils'; - -interface Props { - branchLike: BranchLike | undefined; - checked: boolean; - issue: TypeIssue; - onChange: (issue: TypeIssue) => void; - onCheck: ((issueKey: string) => void) | undefined; - onSelect: (issueKey: string) => void; - onFilterChange: (changes: Partial<Query>) => void; - onPopupToggle: (issue: string, popupName: string, open?: boolean) => void; - openPopup: string | undefined; - selected: boolean; -} - -export default class ListItem extends React.PureComponent<Props> { - handleFilter = (property: string, issue: TypeIssue) => { - const { onFilterChange } = this.props; - - const issuesReset = { issues: [] }; - - if (property.startsWith('tag###')) { - const tag = property.substring('tag###'.length); - onFilterChange({ ...issuesReset, tags: [tag] }); - } else { - switch (property) { - case 'type': - onFilterChange({ ...issuesReset, types: [issue.type] }); - break; - case 'severity': - onFilterChange({ ...issuesReset, severities: [issue.severity] }); - break; - case 'status': - onFilterChange({ ...issuesReset, statuses: [issue.status] }); - break; - case 'resolution': - if (issue.resolution) { - onFilterChange({ ...issuesReset, resolved: true, resolutions: [issue.resolution] }); - } else { - onFilterChange({ ...issuesReset, resolved: false, resolutions: [] }); - } - break; - case 'assignee': - if (issue.assignee) { - onFilterChange({ ...issuesReset, assigned: true, assignees: [issue.assignee] }); - } else { - onFilterChange({ ...issuesReset, assigned: false, assignees: [] }); - } - break; - case 'rule': - onFilterChange({ ...issuesReset, rules: [issue.rule] }); - break; - case 'project': - onFilterChange({ ...issuesReset, projects: [issue.projectKey] }); - break; - case 'file': - onFilterChange({ ...issuesReset, files: [issue.componentUuid] }); - } - } - }; - - render() { - const { branchLike, issue } = this.props; - - return ( - <Issue - branchLike={branchLike} - checked={this.props.checked} - issue={issue} - onChange={this.props.onChange} - onCheck={this.props.onCheck} - onSelect={this.props.onSelect} - onPopupToggle={this.props.onPopupToggle} - openPopup={this.props.openPopup} - selected={this.props.selected} - /> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx index e97d9794092..65364ec12cf 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx @@ -37,6 +37,7 @@ import { ComponentContext } from '../../../app/components/componentContext/Compo import { useCurrentUser } from '../../../app/components/current-user/CurrentUserContext'; import Tooltip from '../../../components/controls/Tooltip'; import { ClipboardBase } from '../../../components/controls/clipboard'; +import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; import { getBranchLikeQuery, isBranch, isPullRequest } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; @@ -181,7 +182,7 @@ export function IssueSourceViewerHeader(props: Readonly<Props>) { to={getComponentIssuesUrl(project, { ...getBranchLikeQuery(branchLike), files: path, - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, })} > {translate('source_viewer.view_all_issues')} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx deleted file mode 100644 index b2995f774de..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import { FacetBox, FacetItem } from 'design-system'; -import { orderBy, without } from 'lodash'; -import * as React from 'react'; -import { RESOLUTIONS } from '../../../helpers/constants'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { Dict } from '../../../types/types'; -import { Query, formatFacetStat } from '../utils'; -import { FacetItemsColumns } from './FacetItemsColumns'; -import { MultipleSelectionHint } from './MultipleSelectionHint'; - -interface Props { - fetching: boolean; - onChange: (changes: Partial<Query>) => void; - onToggle: (property: string) => void; - open: boolean; - resolved: boolean; - resolutions: string[]; - stats: Dict<number> | undefined; -} - -export class ResolutionFacet extends React.PureComponent<Props> { - property = 'resolutions'; - - static defaultProps = { - open: true, - }; - - handleItemClick = (itemValue: string, multiple: boolean) => { - const { resolutions } = this.props; - - if (itemValue === '') { - // unresolved - this.props.onChange({ resolved: !this.props.resolved, resolutions: [] }); - } else if (multiple) { - const newValue = orderBy( - resolutions.includes(itemValue) - ? without(resolutions, itemValue) - : [...resolutions, itemValue], - ); - - this.props.onChange({ resolved: true, [this.property]: newValue }); - } else { - this.props.onChange({ - resolved: true, - [this.property]: - resolutions.includes(itemValue) && resolutions.length < 2 ? [] : [itemValue], - }); - } - }; - - handleHeaderClick = () => { - this.props.onToggle(this.property); - }; - - handleClear = () => { - this.props.onChange({ resolved: false, resolutions: [] }); - }; - - isFacetItemActive(resolution: string) { - return resolution === '' ? !this.props.resolved : this.props.resolutions.includes(resolution); - } - - getFacetItemName(resolution: string) { - return resolution === '' ? translate('unresolved') : translate('issue.resolution', resolution); - } - - getStat(resolution: string) { - const { stats } = this.props; - - return stats ? stats[resolution] : undefined; - } - - renderItem = (resolution: string) => { - const active = this.isFacetItemActive(resolution); - const stat = this.getStat(resolution); - - return ( - <FacetItem - active={active} - className="it__search-navigator-facet" - key={resolution} - name={this.getFacetItemName(resolution)} - onClick={this.handleItemClick} - stat={formatFacetStat(stat) ?? 0} - tooltip={this.getFacetItemName(resolution)} - value={resolution} - /> - ); - }; - - render() { - const { fetching, open, resolutions } = this.props; - - // below: -1 because "Unresolved" is mutually exclusive with the rest - const nbSelectableItems = RESOLUTIONS.filter(this.getStat.bind(this)).length - 1; - - const nbSelectedItems = resolutions.length; - const headerId = `facet_${this.property}`; - - return ( - <FacetBox - className="it__search-navigator-facet-box it__search-navigator-facet-header" - clearIconLabel={translate('clear')} - count={nbSelectedItems} - countLabel={translateWithParameters('x_selected', nbSelectedItems)} - data-property={this.property} - id={headerId} - loading={fetching} - name={translate('issues.facet', this.property)} - onClear={this.handleClear} - onClick={this.handleHeaderClick} - open={open} - > - <FacetItemsColumns>{RESOLUTIONS.map(this.renderItem)}</FacetItemsColumns> - - <MultipleSelectionHint - nbSelectableItems={nbSelectableItems} - nbSelectedItems={nbSelectedItems} - /> - </FacetBox> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx index 2b86e04154c..141cb21fe1b 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx @@ -52,13 +52,12 @@ import { FileFacet } from './FileFacet'; import { LanguageFacet } from './LanguageFacet'; import { PeriodFilter } from './PeriodFilter'; import { ProjectFacet } from './ProjectFacet'; -import { ResolutionFacet } from './ResolutionFacet'; import { RuleFacet } from './RuleFacet'; import { ScopeFacet } from './ScopeFacet'; import { SeverityFacet } from './SeverityFacet'; +import { SimpleStatusFacet } from './SimpleStatusFacet'; import { SoftwareQualityFacet } from './SoftwareQualityFacet'; import { StandardFacet } from './StandardFacet'; -import { StatusFacet } from './StatusFacet'; import { TagFacet } from './TagFacet'; import { TypeFacet } from './TypeFacet'; import { VariantFacet } from './VariantFacet'; @@ -246,25 +245,13 @@ export class SidebarClass extends React.PureComponent<Props> { <BasicSeparator className="sw-my-4" /> - <ResolutionFacet - fetching={this.props.loadingFacets.resolutions === true} + <SimpleStatusFacet + fetching={this.props.loadingFacets.simpleStatuses === true} onChange={this.props.onFilterChange} onToggle={this.props.onFacetToggle} - open={!!openFacets.resolutions} - resolutions={query.resolutions} - resolved={query.resolved} - stats={facets.resolutions} - /> - - <BasicSeparator className="sw-my-4" /> - - <StatusFacet - fetching={this.props.loadingFacets.statuses === true} - onChange={this.props.onFilterChange} - onToggle={this.props.onFacetToggle} - open={!!openFacets.statuses} - stats={facets.statuses} - statuses={query.statuses} + open={!!openFacets.simpleStatuses} + simpleStatuses={query.simpleStatuses} + stats={facets.simpleStatuses} /> <BasicSeparator className="sw-my-4" /> diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/SimpleStatusFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/SimpleStatusFacet.tsx new file mode 100644 index 00000000000..5889d9fc2c2 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/SimpleStatusFacet.tsx @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { FacetBox, FacetItem } from 'design-system'; +import { FacetItemsList } from './FacetItemsList'; + +import { isEqual, sortBy, without } from 'lodash'; +import * as React from 'react'; +import { useIntl } from 'react-intl'; +import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; +import { SIMPLE_STATUSES } from '../../../helpers/constants'; +import { IssueSimpleStatus } from '../../../types/issues'; +import { formatFacetStat } from '../utils'; +import { MultipleSelectionHint } from './MultipleSelectionHint'; +import { CommonProps } from './SimpleListStyleFacet'; + +interface Props extends CommonProps { + simpleStatuses: Array<IssueSimpleStatus>; +} + +const property = 'simpleStatuses'; +const headerId = `facet_${property}`; + +const defaultStatuses = DEFAULT_ISSUES_QUERY.simpleStatuses.split(',') as IssueSimpleStatus[]; + +export function SimpleStatusFacet(props: Readonly<Props>) { + const { simpleStatuses = [], stats = {}, fetching, open, help, needIssueSync } = props; + const intl = useIntl(); + + const nbSelectableItems = SIMPLE_STATUSES.filter( + (item) => !defaultStatuses.includes(item) && stats[item], + ).length; + const hasDefaultSelection = isEqual(sortBy(simpleStatuses), sortBy(defaultStatuses)); + const nbSelectedItems = hasDefaultSelection ? 0 : simpleStatuses.length; + + return ( + <FacetBox + className="it__search-navigator-facet-box it__search-navigator-facet-header" + clearIconLabel={intl.formatMessage({ id: 'clear' })} + count={nbSelectedItems} + countLabel={intl.formatMessage({ id: 'x_selected' }, { '0': nbSelectedItems })} + data-property={property} + id={headerId} + loading={fetching} + name={intl.formatMessage({ id: `issues.facet.${property}` })} + onClear={() => + props.onChange({ + [property]: defaultStatuses, + }) + } + onClick={() => props.onToggle(property)} + open={open} + help={help} + > + <FacetItemsList labelledby={headerId}> + {SIMPLE_STATUSES.map((item) => { + const active = simpleStatuses.includes(item); + const stat = stats[item]; + + return ( + <FacetItem + active={active} + className="it__search-navigator-facet" + key={item} + name={intl.formatMessage({ id: `issue.simple_status.${item}` })} + onClick={(itemValue: IssueSimpleStatus, multiple) => { + if (multiple) { + props.onChange({ + [property]: active + ? without(simpleStatuses, itemValue) + : [...simpleStatuses, itemValue], + }); + } else { + props.onChange({ + [property]: active && simpleStatuses.length === 1 ? [] : [itemValue], + }); + } + }} + stat={(!needIssueSync && formatFacetStat(stat)) ?? 0} + value={item} + /> + ); + })} + </FacetItemsList> + + <MultipleSelectionHint + nbSelectableItems={nbSelectableItems} + nbSelectedItems={simpleStatuses.length} + /> + </FacetBox> + ); +} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx deleted file mode 100644 index f87bb220321..00000000000 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import { - FacetBox, - FacetItem, - StatusConfirmedIcon, - StatusOpenIcon, - StatusReopenedIcon, - StatusResolvedIcon, -} from 'design-system'; -import { orderBy, without } from 'lodash'; -import * as React from 'react'; -import { STATUSES } from '../../../helpers/constants'; -import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { Dict } from '../../../types/types'; -import { Query, formatFacetStat } from '../utils'; -import { FacetItemsColumns } from './FacetItemsColumns'; -import { MultipleSelectionHint } from './MultipleSelectionHint'; - -interface Props { - fetching: boolean; - onChange: (changes: Partial<Query>) => void; - onToggle: (property: string) => void; - open: boolean; - stats: Dict<number> | undefined; - statuses: string[]; -} - -export class StatusFacet extends React.PureComponent<Props> { - property = 'statuses'; - - static defaultProps = { open: true }; - - handleItemClick = (itemValue: string, multiple: boolean) => { - const { statuses } = this.props; - - if (multiple) { - const newValue = orderBy( - statuses.includes(itemValue) ? without(statuses, itemValue) : [...statuses, itemValue], - ); - - this.props.onChange({ [this.property]: newValue }); - } else { - this.props.onChange({ - [this.property]: statuses.includes(itemValue) && statuses.length < 2 ? [] : [itemValue], - }); - } - }; - - handleHeaderClick = () => { - this.props.onToggle(this.property); - }; - - handleClear = () => { - this.props.onChange({ [this.property]: [] }); - }; - - getStat(status: string) { - const { stats } = this.props; - - return stats ? stats[status] : undefined; - } - - renderItem = (status: string) => { - const active = this.props.statuses.includes(status); - const stat = this.getStat(status); - - return ( - <FacetItem - active={active} - className="it__search-navigator-facet" - icon={ - { - CLOSED: <StatusResolvedIcon />, - CONFIRMED: <StatusConfirmedIcon />, - OPEN: <StatusOpenIcon />, - REOPENED: <StatusReopenedIcon />, - RESOLVED: <StatusResolvedIcon />, - }[status] - } - key={status} - name={translate('issue.status', status)} - onClick={this.handleItemClick} - stat={formatFacetStat(stat) ?? 0} - tooltip={translate('issue.status', status)} - value={status} - /> - ); - }; - - render() { - const { fetching, open, statuses } = this.props; - - const nbSelectableItems = STATUSES.filter(this.getStat.bind(this)).length; - const nbSelectedItems = statuses.length; - const headerId = `facet_${this.property}`; - - return ( - <FacetBox - className="it__search-navigator-facet-box it__search-navigator-facet-header" - clearIconLabel={translate('clear')} - count={nbSelectedItems} - countLabel={translateWithParameters('x_selected', nbSelectedItems)} - data-property={this.property} - id={headerId} - loading={fetching} - name={translate('issues.facet', this.property)} - onClear={this.handleClear} - onClick={this.handleHeaderClick} - open={open} - > - <FacetItemsColumns>{STATUSES.map(this.renderItem)}</FacetItemsColumns> - - <MultipleSelectionHint - nbSelectableItems={nbSelectableItems} - nbSelectedItems={nbSelectedItems} - /> - </FacetBox> - ); - } -} diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx index dd743080664..cd19d2908bb 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/Sidebar-it.tsx @@ -51,8 +51,7 @@ it('should render correct facets for Application', () => { 'issues.facet.impactSeverities', 'issues.facet.types', 'issues.facet.scopes', - 'issues.facet.resolutions', - 'issues.facet.statuses', + 'issues.facet.simpleStatuses', 'issues.facet.standards', 'issues.facet.createdAt', 'issues.facet.languages', @@ -74,8 +73,7 @@ it('should render correct facets for Portfolio', () => { 'issues.facet.impactSeverities', 'issues.facet.types', 'issues.facet.scopes', - 'issues.facet.resolutions', - 'issues.facet.statuses', + 'issues.facet.simpleStatuses', 'issues.facet.standards', 'issues.facet.createdAt', 'issues.facet.languages', @@ -97,8 +95,7 @@ it('should render correct facets for SubPortfolio', () => { 'issues.facet.impactSeverities', 'issues.facet.types', 'issues.facet.scopes', - 'issues.facet.resolutions', - 'issues.facet.statuses', + 'issues.facet.simpleStatuses', 'issues.facet.standards', 'issues.facet.createdAt', 'issues.facet.languages', diff --git a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx index 1830e38c66d..5644efe5ca4 100644 --- a/server/sonar-web/src/main/js/apps/issues/test-utils.tsx +++ b/server/sonar-web/src/main/js/apps/issues/test-utils.tsx @@ -74,7 +74,7 @@ export const ui = { resolutionFacet: byRole('button', { name: 'issues.facet.resolutions' }), ruleFacet: byRole('button', { name: 'issues.facet.rules' }), scopeFacet: byRole('button', { name: 'issues.facet.scopes' }), - statusFacet: byRole('button', { name: 'issues.facet.statuses' }), + simpleStatusFacet: byRole('button', { name: 'issues.facet.simpleStatuses' }), tagFacet: byRole('button', { name: 'issues.facet.tags' }), typeFacet: byRole('button', { name: 'issues.facet.types' }), cleanCodeAttributeCategoryFacet: byRole('button', { @@ -97,7 +97,7 @@ export const ui = { clearRuleFacet: byTestId('clear-issues.facet.rules'), clearScopeFacet: byTestId('clear-issues.facet.scopes'), clearSeverityFacet: byTestId('clear-issues.facet.impactSeverities'), - clearStatusFacet: byTestId('clear-issues.facet.statuses'), + clearSimpleStatusFacet: byTestId('clear-issues.facet.simpleStatuses'), clearTagFacet: byTestId('clear-issues.facet.tags'), responsibleCategoryFilter: byRole('checkbox', { @@ -110,11 +110,11 @@ export const ui = { name: `software_quality.${SoftwareQuality.Maintainability}`, }), codeSmellIssueTypeFilter: byRole('checkbox', { name: 'issue.type.CODE_SMELL' }), - confirmedStatusFilter: byRole('checkbox', { name: 'issue.status.CONFIRMED' }), + confirmedStatusFilter: byRole('checkbox', { name: 'issue.simple_status.CONFIRMED' }), fixedResolutionFilter: byRole('checkbox', { name: 'issue.resolution.FIXED' }), mainScopeFilter: byRole('checkbox', { name: 'issue.scope.MAIN' }), mediumSeverityFilter: byRole('checkbox', { name: `severity.${SoftwareImpactSeverity.Medium}` }), - openStatusFilter: byRole('checkbox', { name: 'issue.status.OPEN' }), + openStatusFilter: byRole('checkbox', { name: 'issue.simple_status.OPEN' }), vulnerabilityIssueTypeFilter: byRole('checkbox', { name: 'issue.type.VULNERABILITY' }), clearAllFilters: byRole('button', { name: 'clear_all_filters' }), diff --git a/server/sonar-web/src/main/js/apps/issues/utils.ts b/server/sonar-web/src/main/js/apps/issues/utils.ts index f043d71ee6a..85ebc2f8d99 100644 --- a/server/sonar-web/src/main/js/apps/issues/utils.ts +++ b/server/sonar-web/src/main/js/apps/issues/utils.ts @@ -17,8 +17,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { isArray } from 'lodash'; +import { intersection, isArray, uniq } from 'lodash'; import { getUsers } from '../../api/users'; +import { DEFAULT_ISSUES_QUERY } from '../../components/shared/utils'; import { formatMeasure } from '../../helpers/measures'; import { cleanQuery, @@ -38,7 +39,13 @@ import { SoftwareImpactSeverity, SoftwareQuality, } from '../../types/clean-code-taxonomy'; -import { Facet, RawFacet } from '../../types/issues'; +import { + Facet, + IssueResolution, + IssueSimpleStatus, + IssueStatus, + RawFacet, +} from '../../types/issues'; import { MetricType } from '../../types/metrics'; import { SecurityStandard } from '../../types/security'; import { Dict, Issue, Paging, RawQuery } from '../../types/types'; @@ -70,15 +77,13 @@ export interface Query { [OWASP_ASVS_4_0]: string[]; owaspAsvsLevel: string; projects: string[]; - resolutions: string[]; - resolved: boolean; rules: string[]; scopes: string[]; severities: string[]; inNewCodePeriod: boolean; sonarsourceSecurity: string[]; sort: string; - statuses: string[]; + simpleStatuses: IssueSimpleStatus[]; tags: string[]; types: string[]; } @@ -120,20 +125,78 @@ export function parseQuery(query: RawQuery): Query { [OWASP_ASVS_4_0]: parseAsArray(query[OWASP_ASVS_4_0], parseAsString), owaspAsvsLevel: parseAsString(query['owaspAsvsLevel']), projects: parseAsArray(query.projects, parseAsString), - resolutions: parseAsArray(query.resolutions, parseAsString), - resolved: parseAsBoolean(query.resolved), rules: parseAsArray(query.rules, parseAsString), scopes: parseAsArray(query.scopes, parseAsString), severities: parseAsArray(query.severities, parseAsString), sonarsourceSecurity: parseAsArray(query.sonarsourceSecurity, parseAsString), sort: parseAsSort(query.s), - statuses: parseAsArray(query.statuses, parseAsString), + simpleStatuses: parseSimpleStatuses(query), tags: parseAsArray(query.tags, parseAsString), types: parseAsArray(query.types, parseAsString), codeVariants: parseAsArray(query.codeVariants, parseAsString), }; } +function parseSimpleStatuses(query: RawQuery) { + let result: Array<IssueSimpleStatus> = []; + + if (query.simpleStatuses) { + return parseAsArray<IssueSimpleStatus>(query.simpleStatuses, parseAsString); + } + + const deprecatedStatusesMap = { + [IssueStatus.Open]: [IssueSimpleStatus.Open], + [IssueStatus.Confirmed]: [IssueSimpleStatus.Confirmed], + [IssueStatus.Reopened]: [IssueSimpleStatus.Open], + [IssueStatus.Resolved]: [ + IssueSimpleStatus.Fixed, + IssueSimpleStatus.Accepted, + IssueSimpleStatus.FalsePositive, + ], + [IssueStatus.Closed]: [IssueSimpleStatus.Fixed], + }; + const deprecatedResolutionsMap = { + [IssueResolution.FalsePositive]: [IssueSimpleStatus.FalsePositive], + [IssueResolution.WontFix]: [IssueSimpleStatus.Accepted], + [IssueResolution.Fixed]: [IssueSimpleStatus.Fixed], + [IssueResolution.Removed]: [IssueSimpleStatus.Fixed], + [IssueResolution.Unresolved]: [IssueSimpleStatus.Open, IssueSimpleStatus.Confirmed], + }; + + const simpleStatusesFromStatuses = parseAsArray<IssueStatus>(query.statuses, parseAsString) + .map((status) => deprecatedStatusesMap[status]) + .filter(Boolean) + .flat(); + const simpleStatusesFromResolutions = parseAsArray<IssueResolution>( + query.resolutions, + parseAsString, + ) + .map((status) => deprecatedResolutionsMap[status]) + .filter(Boolean) + .flat(); + + const intesectedSimpleStatuses = intersection( + simpleStatusesFromStatuses, + simpleStatusesFromResolutions, + ); + result = intesectedSimpleStatuses.length + ? intesectedSimpleStatuses + : simpleStatusesFromResolutions.concat(simpleStatusesFromStatuses); + + if ( + query.resolved === 'false' && + [IssueSimpleStatus.Open, IssueSimpleStatus.Confirmed].every( + (status) => !result.includes(status), + ) + ) { + result = result.concat( + parseAsArray<IssueSimpleStatus>(DEFAULT_ISSUES_QUERY.simpleStatuses, parseAsString), + ); + } + + return uniq(result); +} + export function getOpen(query: RawQuery): string | undefined { return query.open; } @@ -167,8 +230,6 @@ export function serializeQuery(query: Query): RawQuery { [OWASP_ASVS_4_0]: serializeStringArray(query[OWASP_ASVS_4_0]), owaspAsvsLevel: serializeString(query['owaspAsvsLevel']), projects: serializeStringArray(query.projects), - resolutions: serializeStringArray(query.resolutions), - resolved: query.resolved ? undefined : 'false', rules: serializeStringArray(query.rules), s: serializeString(query.sort), scopes: serializeStringArray(query.scopes), @@ -177,7 +238,7 @@ export function serializeQuery(query: Query): RawQuery { impactSoftwareQualities: serializeStringArray(query.impactSoftwareQualities), inNewCodePeriod: query.inNewCodePeriod ? 'true' : undefined, sonarsourceSecurity: serializeStringArray(query.sonarsourceSecurity), - statuses: serializeStringArray(query.statuses), + simpleStatuses: serializeStringArray(query.simpleStatuses), tags: serializeStringArray(query.tags), types: serializeStringArray(query.types), codeVariants: serializeStringArray(query.codeVariants), diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx index cf919d5561f..a909f2d2ed2 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx @@ -21,6 +21,7 @@ import classNames from 'classnames'; import * as React from 'react'; import { useIntl } from 'react-intl'; import { getLeakValue } from '../../../components/measure/utils'; +import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { findMeasure } from '../../../helpers/measures'; import { @@ -62,7 +63,7 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) label={newViolations === '1' ? 'issue' : 'issues'} url={getComponentIssuesUrl(component.key, { ...getBranchLikeQuery(branchLike), - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, })} value={newViolations} failedConditions={failedConditions} @@ -104,7 +105,6 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>) } url={getComponentSecurityHotspotsUrl(component.key, { ...getBranchLikeQuery(branchLike), - resolved: 'false', })} value={newSecurityHotspots} failedConditions={failedConditions} diff --git a/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx index 0477b195282..22b53b8a9ee 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/BranchQualityGateConditions.tsx @@ -21,7 +21,11 @@ import { ChevronRightIcon, DangerButtonSecondary } from 'design-system'; import React from 'react'; import { useIntl } from 'react-intl'; -import { isIssueMeasure, propsToIssueParams } from '../../../components/shared/utils'; +import { + DEFAULT_ISSUES_QUERY, + isIssueMeasure, + propsToIssueParams, +} from '../../../components/shared/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { getLocalizedMetricName } from '../../../helpers/l10n'; import { formatMeasure, getShortType, isDiffMetric } from '../../../helpers/measures'; @@ -181,7 +185,7 @@ function getQGConditionUrl( }); } return getComponentIssuesUrl(componentKey, { - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, types: ratingIssueType, ...getBranchLikeQuery(branchLike), ...(sinceLeakPeriod ? { sinceLeakPeriod: 'true' } : {}), diff --git a/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx b/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx index a2a1c55f1db..7209e447cde 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/IssueLabel.tsx @@ -23,6 +23,7 @@ import * as React from 'react'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import Tooltip from '../../../components/controls/Tooltip'; import { getLeakValue } from '../../../components/measure/utils'; +import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures'; @@ -58,7 +59,7 @@ export function IssueLabel(props: IssueLabelProps) { const params = { ...getBranchLikeQuery(branchLike), inNewCodePeriod: useDiffMetric ? 'true' : 'false', - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, types: type, }; diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx index 2104460da8a..cd154bb377b 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/QualityGateCondition.tsx @@ -22,7 +22,11 @@ import * as React from 'react'; import { Path } from 'react-router-dom'; import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; import MeasureIndicator from '../../../components/measure/MeasureIndicator'; -import { isIssueMeasure, propsToIssueParams } from '../../../components/shared/utils'; +import { + DEFAULT_ISSUES_QUERY, + isIssueMeasure, + propsToIssueParams, +} from '../../../components/shared/utils'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures'; @@ -47,7 +51,7 @@ interface Props { export default class QualityGateCondition extends React.PureComponent<Props> { getIssuesUrl = (inNewCodePeriod: boolean, customQuery: Dict<string>) => { const query: Dict<string | undefined> = { - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, ...getBranchLikeQuery(this.props.branchLike), ...customQuery, }; diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx index 48a2dd79305..a38af0f8da2 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/__tests__/BranchQualityGate-it.tsx @@ -39,7 +39,7 @@ it('renders failed QG', () => { expect(maintainabilityRatingLink).toBeInTheDocument(); expect(maintainabilityRatingLink).toHaveAttribute( 'href', - '/project/issues?resolved=false&types=CODE_SMELL&pullRequest=1001&sinceLeakPeriod=true&id=my-project', + '/project/issues?simpleStatuses=OPEN%2CCONFIRMED&types=CODE_SMELL&pullRequest=1001&sinceLeakPeriod=true&id=my-project', ); // Security Hotspots rating condition @@ -59,7 +59,7 @@ it('renders failed QG', () => { expect(codeSmellsLink).toBeInTheDocument(); expect(codeSmellsLink).toHaveAttribute( 'href', - '/project/issues?resolved=false&types=CODE_SMELL&pullRequest=1001&id=my-project', + '/project/issues?simpleStatuses=OPEN%2CCONFIRMED&types=CODE_SMELL&pullRequest=1001&id=my-project', ); // Conditions to cover diff --git a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx index 0685021f205..20a1ca08152 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/components/SourceViewer/SourceViewerHeader.tsx @@ -37,7 +37,6 @@ import { themeColor, } from 'design-system'; import * as React from 'react'; - import { getBranchLikeQuery } from '../../helpers/branch-like'; import { ISSUE_TYPES } from '../../helpers/constants'; import { ISSUETYPE_METRIC_KEYS_MAP } from '../../helpers/issues'; @@ -52,6 +51,7 @@ import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl, } from '../../helpers/urls'; +import { DEFAULT_ISSUES_QUERY } from '../shared/utils'; import { ComponentQualifier } from '../../types/component'; import { IssueType } from '../../types/issues'; @@ -89,7 +89,7 @@ export default class SourceViewerHeader extends React.PureComponent<Props> { const params = { ...getBranchLikeQuery(branchLike), files: sourceViewerFile.path, - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, types: type, }; diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts index cc9656f1ef1..a29d83f34c0 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/loadIssues.ts @@ -23,6 +23,7 @@ import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { parseIssueFromResponse } from '../../../helpers/issues'; import { BranchLike } from '../../../types/branch-like'; import { Issue, RawQuery } from '../../../types/types'; +import { DEFAULT_ISSUES_QUERY } from '../../shared/utils'; // maximum possible value const PAGE_SIZE = 500; @@ -32,16 +33,16 @@ const PAGE_MAX = 20; function buildListQuery(component: string, branchLike: BranchLike | undefined) { return { component, - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, ...getBranchLikeQuery(branchLike), }; } function buildSearchQuery(component: string, branchLike: BranchLike | undefined) { return { + ...DEFAULT_ISSUES_QUERY, additionalFields: '_all', componentKeys: component, - resolved: 'false', s: 'FILE_LINE', ...getBranchLikeQuery(branchLike), }; diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx index a1bae5c76ef..75a2bef6d3c 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueMessage.tsx @@ -27,6 +27,7 @@ import { getComponentIssuesUrl, getIssuesUrl } from '../../../helpers/urls'; import { BranchLike } from '../../../types/branch-like'; import { Issue } from '../../../types/types'; import { useLocation } from '../../hoc/withRouter'; +import { DEFAULT_ISSUES_QUERY } from '../../shared/utils'; export interface IssueMessageProps { issue: Issue; @@ -48,7 +49,7 @@ export default function IssueMessage(props: IssueMessageProps) { ...getBranchLikeQuery(branchLike), files: issue.componentLongName, open: issue.key, - resolved: 'false', + ...DEFAULT_ISSUES_QUERY, why: '1', }); diff --git a/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts b/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts index 495248657e8..f83c2dfa817 100644 --- a/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts +++ b/server/sonar-web/src/main/js/components/shared/__tests__/utils-test.ts @@ -23,13 +23,13 @@ import { propsToIssueParams } from '../utils'; describe('propsToIssueParams', () => { it('should render correct default parameters', () => { - expect(propsToIssueParams('other')).toEqual({ resolved: 'false' }); + expect(propsToIssueParams('other')).toEqual({ simpleStatuses: 'OPEN,CONFIRMED' }); }); it(`should render correct params`, () => { expect(propsToIssueParams(MetricKey.false_positive_issues, true)).toEqual({ - resolutions: 'FALSE-POSITIVE', inNewCodePeriod: true, + simpleStatuses: 'FALSE_POSITIVE', }); }); }); diff --git a/server/sonar-web/src/main/js/components/shared/utils.ts b/server/sonar-web/src/main/js/components/shared/utils.ts index a073929ddf3..12358551c9e 100644 --- a/server/sonar-web/src/main/js/components/shared/utils.ts +++ b/server/sonar-web/src/main/js/components/shared/utils.ts @@ -17,7 +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. */ - +import { IssueSimpleStatus } from '../../types/issues'; import { MetricKey } from '../../types/metrics'; import { Dict } from '../../types/types'; @@ -46,27 +46,31 @@ const ISSUE_MEASURES = [ MetricKey.new_vulnerabilities, ]; +export const DEFAULT_ISSUES_QUERY = { + simpleStatuses: `${IssueSimpleStatus.Open},${IssueSimpleStatus.Confirmed}`, +}; + const issueParamsPerMetric: Dict<Dict<string>> = { - [MetricKey.blocker_violations]: { resolved: 'false', severities: 'BLOCKER' }, - [MetricKey.new_blocker_violations]: { resolved: 'false', severities: 'BLOCKER' }, - [MetricKey.critical_violations]: { resolved: 'false', severities: 'CRITICAL' }, - [MetricKey.new_critical_violations]: { resolved: 'false', severities: 'CRITICAL' }, - [MetricKey.major_violations]: { resolved: 'false', severities: 'MAJOR' }, - [MetricKey.new_major_violations]: { resolved: 'false', severities: 'MAJOR' }, - [MetricKey.minor_violations]: { resolved: 'false', severities: 'MINOR' }, - [MetricKey.new_minor_violations]: { resolved: 'false', severities: 'MINOR' }, - [MetricKey.info_violations]: { resolved: 'false', severities: 'INFO' }, - [MetricKey.new_info_violations]: { resolved: 'false', severities: 'INFO' }, - [MetricKey.open_issues]: { resolved: 'false', statuses: 'OPEN' }, - [MetricKey.reopened_issues]: { resolved: 'false', statuses: 'REOPENED' }, - [MetricKey.confirmed_issues]: { resolved: 'false', statuses: 'CONFIRMED' }, - [MetricKey.false_positive_issues]: { resolutions: 'FALSE-POSITIVE' }, - [MetricKey.code_smells]: { resolved: 'false', types: 'CODE_SMELL' }, - [MetricKey.new_code_smells]: { resolved: 'false', types: 'CODE_SMELL' }, - [MetricKey.bugs]: { resolved: 'false', types: 'BUG' }, - [MetricKey.new_bugs]: { resolved: 'false', types: 'BUG' }, - [MetricKey.vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' }, - [MetricKey.new_vulnerabilities]: { resolved: 'false', types: 'VULNERABILITY' }, + [MetricKey.blocker_violations]: { severities: 'BLOCKER' }, + [MetricKey.new_blocker_violations]: { severities: 'BLOCKER' }, + [MetricKey.critical_violations]: { severities: 'CRITICAL' }, + [MetricKey.new_critical_violations]: { severities: 'CRITICAL' }, + [MetricKey.major_violations]: { severities: 'MAJOR' }, + [MetricKey.new_major_violations]: { severities: 'MAJOR' }, + [MetricKey.minor_violations]: { severities: 'MINOR' }, + [MetricKey.new_minor_violations]: { severities: 'MINOR' }, + [MetricKey.info_violations]: { severities: 'INFO' }, + [MetricKey.new_info_violations]: { severities: 'INFO' }, + [MetricKey.open_issues]: { simpleStatuses: IssueSimpleStatus.Open }, + [MetricKey.reopened_issues]: { simpleStatuses: IssueSimpleStatus.Open }, + [MetricKey.confirmed_issues]: { simpleStatuses: IssueSimpleStatus.Confirmed }, + [MetricKey.false_positive_issues]: { simpleStatuses: IssueSimpleStatus.FalsePositive }, + [MetricKey.code_smells]: { types: 'CODE_SMELL' }, + [MetricKey.new_code_smells]: { types: 'CODE_SMELL' }, + [MetricKey.bugs]: { types: 'BUG' }, + [MetricKey.new_bugs]: { types: 'BUG' }, + [MetricKey.vulnerabilities]: { types: 'VULNERABILITY' }, + [MetricKey.new_vulnerabilities]: { types: 'VULNERABILITY' }, }; export function isIssueMeasure(metric: string) { @@ -75,7 +79,8 @@ export function isIssueMeasure(metric: string) { export function propsToIssueParams(metric: string, inNewCodePeriod = false) { const params: Dict<string | boolean> = { - ...(issueParamsPerMetric[metric] || { resolved: 'false' }), + ...DEFAULT_ISSUES_QUERY, + ...issueParamsPerMetric[metric], }; if (inNewCodePeriod) { 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 6e19414632b..57948fc3a0b 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,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. */ +import { DEFAULT_ISSUES_QUERY } from '../../components/shared/utils'; import { AlmKeys } from '../../types/alm-settings'; import { ComponentQualifier } from '../../types/component'; import { IssueType } from '../../types/issues'; @@ -102,10 +103,10 @@ describe('#getComponentIssuesUrl', () => { }); it('should work with parameters', () => { - expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, { resolved: 'false' })).toEqual( + expect(getComponentIssuesUrl(SIMPLE_COMPONENT_KEY, DEFAULT_ISSUES_QUERY)).toEqual( expect.objectContaining({ pathname: '/project/issues', - search: queryToSearch({ resolved: 'false', id: SIMPLE_COMPONENT_KEY }), + search: queryToSearch({ ...DEFAULT_ISSUES_QUERY, id: SIMPLE_COMPONENT_KEY }), }), ); }); diff --git a/server/sonar-web/src/main/js/helpers/constants.ts b/server/sonar-web/src/main/js/helpers/constants.ts index b1d98632e55..20d4cab5b02 100644 --- a/server/sonar-web/src/main/js/helpers/constants.ts +++ b/server/sonar-web/src/main/js/helpers/constants.ts @@ -25,7 +25,13 @@ import { SoftwareQuality, } from '../types/clean-code-taxonomy'; import { ComponentQualifier } from '../types/component'; -import { IssueResolution, IssueScope, IssueSeverity, IssueType } from '../types/issues'; +import { + IssueResolution, + IssueScope, + IssueSeverity, + IssueSimpleStatus, + IssueType, +} from '../types/issues'; import { RuleType } from '../types/types'; export const SEVERITIES = Object.values(IssueSeverity); @@ -38,6 +44,14 @@ export const SOFTWARE_QUALITIES = Object.values(SoftwareQuality); export const STATUSES = ['OPEN', 'CONFIRMED', 'REOPENED', 'RESOLVED', 'CLOSED']; +export const SIMPLE_STATUSES = [ + IssueSimpleStatus.Open, + IssueSimpleStatus.Accepted, + IssueSimpleStatus.FalsePositive, + IssueSimpleStatus.Confirmed, + IssueSimpleStatus.Fixed, +]; + export const ISSUE_TYPES: IssueType[] = [ IssueType.Bug, IssueType.Vulnerability, diff --git a/server/sonar-web/src/main/js/helpers/mocks/issues.ts b/server/sonar-web/src/main/js/helpers/mocks/issues.ts index 96d005d574c..dedc2f1ff1d 100644 --- a/server/sonar-web/src/main/js/helpers/mocks/issues.ts +++ b/server/sonar-web/src/main/js/helpers/mocks/issues.ts @@ -75,8 +75,6 @@ export function mockQuery(overrides: Partial<Query> = {}): Query { 'owaspAsvs-4.0': [], owaspAsvsLevel: '', projects: [], - resolutions: [], - resolved: false, rules: [], scopes: [], severities: [], @@ -84,8 +82,8 @@ export function mockQuery(overrides: Partial<Query> = {}): Query { impactSoftwareQualities: [], inNewCodePeriod: false, sonarsourceSecurity: [], + simpleStatuses: [], sort: '', - statuses: [], tags: [], types: [], ...overrides, diff --git a/server/sonar-web/src/main/js/helpers/query.ts b/server/sonar-web/src/main/js/helpers/query.ts index f5c5e426dce..66f319a44e8 100644 --- a/server/sonar-web/src/main/js/helpers/query.ts +++ b/server/sonar-web/src/main/js/helpers/query.ts @@ -29,7 +29,12 @@ export function queriesEqual(a: RawQuery, b: RawQuery): boolean { return false; } - return keysA.every((key) => isEqual(a[key], b[key])); + return keysA.every((key) => + isEqual( + Array.isArray(a[key]) ? a[key].sort() : a[key], + Array.isArray(b[key]) ? b[key].sort() : b[key], + ), + ); } export function cleanQuery(query: RawQuery): RawQuery { diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index 702d92e2759..44c433f0bd6 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -310,10 +310,10 @@ export function mockRawIssue(withLocations = false, overrides: Partial<RawIssue> project: 'myproject', rule: 'javascript:S1067', severity: IssueSeverity.Major, - status: IssueStatus.Open, - simpleStatus: IssueSimpleStatus.Open, textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 }, type: IssueType.CodeSmell, + status: IssueStatus.Open, + simpleStatus: IssueSimpleStatus.Open, transitions: [], scope: IssueScope.Main, cleanCodeAttributeCategory: CleanCodeAttributeCategory.Responsible, diff --git a/server/sonar-web/src/main/js/helpers/urls.ts b/server/sonar-web/src/main/js/helpers/urls.ts index 3b302c6fb82..aa9a3061748 100644 --- a/server/sonar-web/src/main/js/helpers/urls.ts +++ b/server/sonar-web/src/main/js/helpers/urls.ts @@ -20,6 +20,7 @@ import { isArray, mapValues, omitBy, pick } from 'lodash'; import { Path, To } from 'react-router-dom'; import { getProfilePath } from '../apps/quality-profiles/utils'; +import { DEFAULT_ISSUES_QUERY } from '../components/shared/utils'; import { BranchLike, BranchParameters } from '../types/branch-like'; import { ComponentQualifier, isApplication, isPortfolioLike } from '../types/component'; import { MeasurePageView } from '../types/measures'; @@ -423,7 +424,7 @@ export function getHomePageUrl(homepage: HomePage) { return '/projects'; case 'ISSUES': case 'MY_ISSUES': - return { pathname: '/issues', query: { resolved: 'false' } }; + return { pathname: '/issues', query: DEFAULT_ISSUES_QUERY }; } // should never happen, but just in case... diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index d0942e614b0..f22fa371396 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1069,6 +1069,12 @@ issue.status.TO_REVIEW=To Review issue.status.IN_REVIEW=In Review issue.status.REVIEWED=Reviewed +issue.simple_status.OPEN=Open +issue.simple_status.ACCEPTED=Accepted +issue.simple_status.CONFIRMED=Confirmed +issue.simple_status.FIXED=Fixed +issue.simple_status.FALSE_POSITIVE=False Positive + issue.scope.MAIN=Main code issue.scope.TEST=Test code @@ -1173,7 +1179,7 @@ issues.facet.types=Type issues.facet.severities=Severity issues.facet.scopes=Scope issues.facet.projects=Project -issues.facet.statuses=Status +issues.facet.simpleStatuses=Status issues.facet.hotspotStatuses=Hotspot Status issues.facet.assignees=Assignee issues.facet.files=File @@ -1181,7 +1187,6 @@ issues.facet.modules=Module issues.facet.directories=Directory issues.facet.tags=Tag issues.facet.rules=Rule -issues.facet.resolutions=Resolution issues.facet.languages=Language issues.facet.cleanCodeAttributeCategories=Clean Code Attribute issues.facet.impactSoftwareQualities=Software Quality |