@@ -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,13 +446,8 @@ 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'), |
@@ -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( | |||
[ |
@@ -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'); |
@@ -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, | |||
}); | |||
}; | |||
@@ -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 ( |
@@ -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> |
@@ -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 |
@@ -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', () => { |
@@ -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={ |
@@ -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} |
@@ -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} | |||
/> | |||
); | |||
} | |||
} |
@@ -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')} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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" /> |
@@ -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> | |||
); | |||
} |
@@ -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> | |||
); | |||
} | |||
} |
@@ -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', |
@@ -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' }), |
@@ -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), |
@@ -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} |
@@ -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' } : {}), |
@@ -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, | |||
}; | |||
@@ -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, | |||
}; |
@@ -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 |
@@ -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, | |||
}; | |||
@@ -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), | |||
}; |
@@ -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', | |||
}); | |||
@@ -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', | |||
}); | |||
}); | |||
}); |
@@ -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) { |
@@ -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 }), | |||
}), | |||
); | |||
}); |
@@ -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, |
@@ -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, |
@@ -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 { |
@@ -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, |
@@ -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... |
@@ -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 |