From a2d67088ee25e41e82836b41b5813c5f17f9567a Mon Sep 17 00:00:00 2001 From: 7PH Date: Mon, 30 Oct 2023 14:39:03 +0100 Subject: [PATCH] SONAR-20870 Update issue transitions to allow accepting issues --- .../design-system/src/components/Dropdown.tsx | 5 +- .../src/components/DropdownMenu.tsx | 35 ++++- .../input/SearchSelectDropdownControl.tsx | 4 +- .../src/components/input/index.ts | 1 + .../main/js/api/mocks/IssuesServiceMock.ts | 42 +++-- .../src/main/js/api/mocks/data/issues.ts | 18 ++- .../js/apps/issues/__tests__/IssueApp-it.tsx | 39 +++-- .../__tests__/BulkChangeModal-it.tsx | 13 +- .../security-hotspots/components/Assignee.tsx | 3 +- .../__snapshots__/loadIssues-test.ts.snap | 2 + .../{StatusIcon.tsx => SimpleStatusIcon.tsx} | 12 +- .../components/issue/__tests__/Issue-it.tsx | 14 +- .../src/main/js/components/issue/actions.ts | 8 +- .../issue/components/IssueActionsBar.tsx | 40 +---- .../issue/components/IssueAssign.tsx | 2 +- .../issue/components/IssueCommentAction.tsx | 2 - .../issue/components/IssueTransition.tsx | 145 ++++++++---------- .../issue/components/IssueTransitionItem.tsx | 86 +++++++++++ .../components/IssueTransitionOverlay.tsx | 136 ++++++++++++++++ .../src/main/js/components/issue/helpers.ts | 37 +++++ .../components/issue/popups/CommentPopup.tsx | 4 +- .../js/components/shared/StatusHelper.tsx | 12 +- .../sonar-web/src/main/js/helpers/issues.ts | 12 +- .../src/main/js/helpers/testMocks.ts | 11 +- server/sonar-web/src/main/js/types/issues.ts | 12 +- server/sonar-web/src/main/js/types/types.ts | 5 +- .../resources/org/sonar/l10n/core.properties | 46 ++++-- 27 files changed, 529 insertions(+), 217 deletions(-) rename server/sonar-web/src/main/js/components/icons/{StatusIcon.tsx => SimpleStatusIcon.tsx} (77%) create mode 100644 server/sonar-web/src/main/js/components/issue/components/IssueTransitionItem.tsx create mode 100644 server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx create mode 100644 server/sonar-web/src/main/js/components/issue/helpers.ts diff --git a/server/sonar-web/design-system/src/components/Dropdown.tsx b/server/sonar-web/design-system/src/components/Dropdown.tsx index 6c03ec66f85..3bd340bcdf9 100644 --- a/server/sonar-web/design-system/src/components/Dropdown.tsx +++ b/server/sonar-web/design-system/src/components/Dropdown.tsx @@ -67,7 +67,10 @@ export class Dropdown extends React.PureComponent, State> { if (!prevState.open && this.state.open && this.props.onOpen) { this.props.onOpen(); } - if (props.openDropdown !== this.props.openDropdown && this.props.openDropdown) { + if ( + props.openDropdown !== this.props.openDropdown && + typeof this.props.openDropdown === 'boolean' + ) { this.setState({ open: this.props.openDropdown }); } } diff --git a/server/sonar-web/design-system/src/components/DropdownMenu.tsx b/server/sonar-web/design-system/src/components/DropdownMenu.tsx index e87d873e219..bf983364dc3 100644 --- a/server/sonar-web/design-system/src/components/DropdownMenu.tsx +++ b/server/sonar-web/design-system/src/components/DropdownMenu.tsx @@ -68,6 +68,7 @@ interface ListItemProps { onFocus?: VoidFunction; onPointerEnter?: VoidFunction; onPointerLeave?: VoidFunction; + selected?: boolean; } type ItemLinkProps = Omit & @@ -76,12 +77,22 @@ type ItemLinkProps = Omit & }; export function ItemLink(props: ItemLinkProps) { - const { children, className, disabled, icon, isExternal, onClick, innerRef, to, ...liProps } = - props; + const { + children, + className, + disabled, + icon, + isExternal, + onClick, + selected, + innerRef, + to, + ...liProps + } = props; return (
  • - + {icon} {children} @@ -336,6 +353,10 @@ const itemStyle = (props: ThemedProps) => css` ${tw`sw-cursor-not-allowed`}; } + &.selected { + background-color: ${themeColor('selectOptionSelected')(props)}; + } + & > svg { ${tw`sw-mr-2`} } diff --git a/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx b/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx index 71c64b2a831..175a5b9c09f 100644 --- a/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx +++ b/server/sonar-web/design-system/src/components/input/SearchSelectDropdownControl.tsx @@ -104,7 +104,7 @@ export function SearchSelectDropdownControl(props: SearchSelectDropdownControlPr ); } -const StyledControl = styled.div` +export const StyledControl = styled.div` color: ${themeContrast('inputBackground')}; background: ${themeColor('inputBackground')}; border: ${themeBorder('default', 'inputBorder')}; @@ -121,7 +121,7 @@ const StyledControl = styled.div` &.is-discreet { ${tw`sw-border-none`}; - ${tw`sw-p-0`}; + ${tw`sw-px-1`}; ${tw`sw-w-auto sw-h-auto`}; background: inherit; diff --git a/server/sonar-web/design-system/src/components/input/index.ts b/server/sonar-web/design-system/src/components/input/index.ts index ca90354aa44..740a84caa85 100644 --- a/server/sonar-web/design-system/src/components/input/index.ts +++ b/server/sonar-web/design-system/src/components/input/index.ts @@ -31,3 +31,4 @@ export * from './MultiSelectMenu'; export * from './RadioButton'; export * from './SearchSelect'; export * from './SearchSelectDropdown'; +export * from './SearchSelectDropdownControl'; 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 30c1a8dbf01..581b44578d2 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -39,7 +39,7 @@ import { import { SearchRulesResponse } from '../../types/coding-rules'; import { ASSIGNEE_ME, - IssueResolution, + IssueSimpleStatus, IssueStatus, IssueTransition, IssueType, @@ -525,45 +525,39 @@ export default class IssuesServiceMock { }; handleSetIssueTransition = (data: { issue: string; transition: string }) => { - const statusMap: { [key: string]: IssueStatus } = { - [IssueTransition.Confirm]: IssueStatus.Confirmed, - [IssueTransition.UnConfirm]: IssueStatus.Reopened, - [IssueTransition.Resolve]: IssueStatus.Resolved, - [IssueTransition.WontFix]: IssueStatus.Resolved, - [IssueTransition.FalsePositive]: IssueStatus.Resolved, + const simpleStatusMap: { [key: string]: IssueSimpleStatus } = { + [IssueTransition.Accept]: IssueSimpleStatus.Accepted, + [IssueTransition.Confirm]: IssueSimpleStatus.Confirmed, + [IssueTransition.UnConfirm]: IssueSimpleStatus.Open, + [IssueTransition.Resolve]: IssueSimpleStatus.Fixed, + [IssueTransition.WontFix]: IssueSimpleStatus.Accepted, + [IssueTransition.FalsePositive]: IssueSimpleStatus.FalsePositive, }; + const transitionMap: Dict = { - [IssueStatus.Reopened]: [ - IssueTransition.Confirm, - IssueTransition.Resolve, - IssueTransition.FalsePositive, - IssueTransition.WontFix, - ], - [IssueStatus.Open]: [ + [IssueSimpleStatus.Open]: [ + IssueTransition.Accept, IssueTransition.Confirm, IssueTransition.Resolve, IssueTransition.FalsePositive, IssueTransition.WontFix, ], - [IssueStatus.Confirmed]: [ + [IssueSimpleStatus.Confirmed]: [ + IssueTransition.Accept, IssueTransition.Resolve, IssueTransition.UnConfirm, IssueTransition.FalsePositive, IssueTransition.WontFix, ], - [IssueStatus.Resolved]: [IssueTransition.Reopen], - }; - - const resolutionMap: Dict = { - [IssueTransition.WontFix]: IssueResolution.WontFix, - [IssueTransition.FalsePositive]: IssueResolution.FalsePositive, + [IssueSimpleStatus.FalsePositive]: [IssueTransition.Reopen], + [IssueSimpleStatus.Accepted]: [IssueTransition.Reopen], + [IssueSimpleStatus.Fixed]: [IssueTransition.Reopen], }; return this.getActionsResponse( { - status: statusMap[data.transition], - transitions: transitionMap[statusMap[data.transition]], - resolution: resolutionMap[data.transition], + simpleStatus: simpleStatusMap[data.transition], + transitions: transitionMap[simpleStatusMap[data.transition]], }, data.issue, ); 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 5a3085a5b80..f3d42d750bf 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 @@ -31,7 +31,9 @@ import { IssueResolution, IssueScope, IssueSeverity, + IssueSimpleStatus, IssueStatus, + IssueTransition, IssueType, RawIssue, } from '../../../types/issues'; @@ -296,7 +298,13 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa issue: mockRawIssue(false, { key: ISSUE_2, actions: Object.values(IssueActions), - transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], + transitions: [ + IssueTransition.Accept, + IssueTransition.Confirm, + IssueTransition.Resolve, + IssueTransition.FalsePositive, + IssueTransition.WontFix, + ], component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_2][0]}`, message: 'Fix that', rule: ISSUE_TO_RULE[ISSUE_2], @@ -312,6 +320,7 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa ruleDescriptionContextKey: 'spring', resolution: IssueResolution.Unresolved, status: IssueStatus.Open, + simpleStatus: IssueSimpleStatus.Open, }), snippets: keyBy( [ @@ -354,7 +363,12 @@ export function mockIssuesList(baseComponentKey = PARENT_COMPONENT_KEY): IssueDa issue: mockRawIssue(false, { key: ISSUE_4, actions: Object.values(IssueActions), - transitions: ['confirm', 'resolve', 'falsepositive', 'wontfix'], + transitions: [ + IssueTransition.Confirm, + IssueTransition.Resolve, + IssueTransition.FalsePositive, + IssueTransition.WontFix, + ], component: `${baseComponentKey}:${ISSUE_TO_FILES[ISSUE_4][0]}`, message: 'Issue with tags', rule: ISSUE_TO_RULE[ISSUE_4], diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx index 8ac1afbdaec..d333b922e69 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx @@ -154,36 +154,43 @@ describe('issue app', () => { // Get a specific issue list item const listItem = within(await screen.findByRole('region', { name: 'Fix that' })); - // Change issue status - expect(listItem.getByText('issue.status.OPEN')).toBeInTheDocument(); + expect(listItem.getByText('issue.simple_status.OPEN')).toBeInTheDocument(); await act(async () => { - await user.click(listItem.getByText('issue.status.OPEN')); + await user.click(listItem.getByText('issue.simple_status.OPEN')); }); + expect(listItem.getByText('issue.transition.accept')).toBeInTheDocument(); expect(listItem.getByText('issue.transition.confirm')).toBeInTheDocument(); - expect(listItem.getByText('issue.transition.resolve')).toBeInTheDocument(); await act(async () => { await user.click(listItem.getByText('issue.transition.confirm')); }); - expect( - listItem.getByLabelText('issue.transition.status_x_click_to_change.issue.status.CONFIRMED'), - ).toBeInTheDocument(); - // As won't fix + expect(listItem.getByRole('textbox')).toBeInTheDocument(); + await act(async () => { - await user.click(listItem.getByText('issue.status.CONFIRMED')); - await user.click(listItem.getByText('issue.transition.wontfix')); + await user.type(listItem.getByRole('textbox'), 'test'); + await user.click(listItem.getByText('resolve')); }); - // Comment should open and close - expect(listItem.getByRole('button', { name: 'issue.comment.formlink' })).toBeInTheDocument(); + + expect( + listItem.getByLabelText( + 'issue.transition.status_x_click_to_change.issue.simple_status.CONFIRMED', + ), + ).toBeInTheDocument(); + + // Change status again await act(async () => { - await user.keyboard('test'); - await user.click(listItem.getByRole('button', { name: 'issue.comment.formlink' })); + await user.click(listItem.getByText('issue.simple_status.CONFIRMED')); + await user.click(listItem.getByText('issue.transition.accept')); + await user.click(listItem.getByText('resolve')); }); + expect( - listItem.queryByRole('button', { name: 'issue.comment.submit' }), - ).not.toBeInTheDocument(); + listItem.getByLabelText( + 'issue.transition.status_x_click_to_change.issue.simple_status.ACCEPTED', + ), + ).toBeInTheDocument(); // Assign issue to a different user await act(async () => { diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx index 03caf6a4ddb..70e4a21149a 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/BulkChangeModal-it.tsx @@ -26,6 +26,7 @@ import CurrentUserContextProvider from '../../../../app/components/current-user/ import { mockIssue, mockLoggedInUser } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; import { ComponentPropsType } from '../../../../helpers/testUtils'; +import { IssueTransition } from '../../../../types/issues'; import { Issue } from '../../../../types/types'; import { CurrentUser } from '../../../../types/users'; import BulkChangeModal, { MAX_PAGE_SIZE } from '../BulkChangeModal'; @@ -70,11 +71,11 @@ it('should render tags correctly', async () => { it('should render transitions correctly', async () => { renderBulkChangeModal([ - mockIssue(false, { actions: ['set_transition'], transitions: ['Transition1'] }), + mockIssue(false, { actions: ['set_transition'], transitions: [IssueTransition.FalsePositive] }), ]); expect(await screen.findByText('issue.transition')).toBeInTheDocument(); - expect(await screen.findByText('issue.transition.Transition1')).toBeInTheDocument(); + expect(await screen.findByText('issue.transition.falsepositive')).toBeInTheDocument(); }); it('should disable the submit button unless some change is configured', async () => { @@ -108,12 +109,12 @@ it('should properly submit', async () => { mockIssue(false, { actions: ['assign', 'set_transition', 'set_tags', 'set_type', 'set_severity', 'comment'], key: 'issue1', - transitions: ['Transition1', 'Transition2'], + transitions: [IssueTransition.Accept, IssueTransition.FalsePositive], }), mockIssue(false, { actions: ['assign', 'set_transition', 'set_tags', 'set_type', 'set_severity', 'comment'], key: 'issue2', - transitions: ['Transition1', 'Transition2'], + transitions: [IssueTransition.Accept, IssueTransition.FalsePositive], }), ], { @@ -136,7 +137,7 @@ it('should properly submit', async () => { await user.click(await screen.findByText('Toto')); // Transition - await user.click(await screen.findByText('issue.transition.Transition2')); + await user.click(await screen.findByText('issue.transition.accept')); // Add a tag await act(async () => { @@ -161,7 +162,7 @@ it('should properly submit', async () => { add_tags: 'tag1,tag2', assign: 'toto', comment: 'some comment', - do_transition: 'Transition2', + do_transition: 'accept', sendNotifications: true, }); }); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/Assignee.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/Assignee.tsx index 8b0bd1d189d..8394bbce39a 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/Assignee.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/Assignee.tsx @@ -68,7 +68,8 @@ export default function Assignee(props: Props) { const controlLabel = assigneeUser ? ( <> - {renderAvatar(assigneeUser?.name, assigneeUser.avatar)} {assigneeUser.name} + {' '} + {assigneeUser.name} ) : ( UNASSIGNED.label diff --git a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap index 20f82563531..2aba8958d02 100644 --- a/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap +++ b/server/sonar-web/src/main/js/components/SourceViewer/helpers/__tests__/__snapshots__/loadIssues-test.ts.snap @@ -33,6 +33,7 @@ exports[`loadIssues should load issues with listIssues if re-indexing 1`] = ` "projectQualifier": "TRK", "rule": "squid:S4797", "secondaryLocations": [], + "simpleStatus": "OPEN", "status": "OPEN", "tags": [ "cert", @@ -95,6 +96,7 @@ exports[`loadIssues should load issues with searchIssues if not re-indexing 1`] "ruleName": "Handling files is security-sensitive", "ruleStatus": "READY", "secondaryLocations": [], + "simpleStatus": "OPEN", "status": "OPEN", "tags": [ "cert", diff --git a/server/sonar-web/src/main/js/components/icons/StatusIcon.tsx b/server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx similarity index 77% rename from server/sonar-web/src/main/js/components/icons/StatusIcon.tsx rename to server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx index bbdca971b2b..6e25c4f2b49 100644 --- a/server/sonar-web/src/main/js/components/icons/StatusIcon.tsx +++ b/server/sonar-web/src/main/js/components/icons/SimpleStatusIcon.tsx @@ -25,14 +25,20 @@ import { StatusResolvedIcon, } from 'design-system'; import * as React from 'react'; +import { IssueSimpleStatus } from '../../types/issues'; import { Dict } from '../../types/types'; import { IconProps } from './Icon'; interface Props extends IconProps { - status: string; + simpleStatus: IssueSimpleStatus; } const statusIcons: Dict<(props: IconProps) => React.ReactElement> = { + [IssueSimpleStatus.Accepted]: StatusConfirmedIcon, + [IssueSimpleStatus.Confirmed]: StatusConfirmedIcon, + [IssueSimpleStatus.FalsePositive]: StatusResolvedIcon, + [IssueSimpleStatus.Fixed]: StatusResolvedIcon, + [IssueSimpleStatus.Open]: StatusOpenIcon, closed: StatusResolvedIcon, confirm: StatusConfirmedIcon, confirmed: StatusConfirmedIcon, @@ -49,8 +55,8 @@ const statusIcons: Dict<(props: IconProps) => React.ReactElement> = { wontfix: StatusResolvedIcon, }; -export default function StatusIcon({ status, ...iconProps }: Props) { - const DesiredStatusIcon = statusIcons[status.toLowerCase()]; +export default function SimpleStatusIcon({ simpleStatus, ...iconProps }: Props) { + const DesiredStatusIcon = statusIcons[simpleStatus.toLowerCase()]; return DesiredStatusIcon ? : null; } diff --git a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx index 4527ef3e43b..eb9c9880faa 100644 --- a/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx +++ b/server/sonar-web/src/main/js/components/issue/__tests__/Issue-it.tsx @@ -32,7 +32,7 @@ import { ComponentPropsType } from '../../../helpers/testUtils'; import { IssueActions, IssueSeverity, - IssueStatus, + IssueSimpleStatus, IssueTransition, IssueType, } from '../../../types/issues'; @@ -97,7 +97,7 @@ describe('updating', () => { it('should allow updating the status', async () => { const { ui } = getPageObject(); const issue = mockRawIssue(false, { - status: IssueStatus.Open, + simpleStatus: IssueSimpleStatus.Open, transitions: [IssueTransition.Confirm, IssueTransition.UnConfirm], }); issuesHandler.setIssueList([{ issue, snippets: {} }]); @@ -105,8 +105,8 @@ describe('updating', () => { issue: mockIssue(false, { ...pick(issue, 'key', 'status', 'transitions') }), }); - await ui.updateStatus(IssueStatus.Open, IssueTransition.Confirm); - expect(ui.updateStatusBtn(IssueStatus.Confirmed).get()).toBeInTheDocument(); + await ui.updateStatus(IssueSimpleStatus.Open, IssueTransition.Confirm); + expect(ui.updateStatusBtn(IssueSimpleStatus.Confirmed).get()).toBeInTheDocument(); }); it('should allow assigning', async () => { @@ -244,8 +244,8 @@ function getPageObject() { setSeverityBtn: (severity: IssueSeverity) => byText(`severity.${severity}`), // Status - updateStatusBtn: (currentStatus: IssueStatus) => - byLabelText(`issue.transition.status_x_click_to_change.issue.status.${currentStatus}`), + updateStatusBtn: (currentStatus: IssueSimpleStatus) => + byLabelText(`issue.transition.status_x_click_to_change.issue.simple_status.${currentStatus}`), setStatusBtn: (transition: IssueTransition) => byText(`issue.transition.${transition}`), // Assignee @@ -297,7 +297,7 @@ function getPageObject() { await user.click(selectors.setSeverityBtn(newSeverity).get()); }); }, - async updateStatus(currentStatus: IssueStatus, transition: IssueTransition) { + async updateStatus(currentStatus: IssueSimpleStatus, transition: IssueTransition) { await user.click(selectors.updateStatusBtn(currentStatus).get()); await act(async () => { await user.click(selectors.setStatusBtn(transition).get()); diff --git a/server/sonar-web/src/main/js/components/issue/actions.ts b/server/sonar-web/src/main/js/components/issue/actions.ts index 65952a7af1a..299e7e24b2c 100644 --- a/server/sonar-web/src/main/js/components/issue/actions.ts +++ b/server/sonar-web/src/main/js/components/issue/actions.ts @@ -27,13 +27,13 @@ export const updateIssue = ( resultPromise: Promise, oldIssue?: Issue, newIssue?: Issue, -) => { +): Promise => { const optimisticUpdate = oldIssue !== undefined && newIssue !== undefined; if (optimisticUpdate) { - onChange(newIssue!); + onChange(newIssue); } - resultPromise.then( + return resultPromise.then( (response) => { if (!optimisticUpdate) { const issue = parseIssueFromResponse( @@ -47,7 +47,7 @@ export const updateIssue = ( }, (param) => { if (optimisticUpdate) { - onChange(oldIssue!); + onChange(oldIssue); } throwGlobalError(param); }, diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx index 067fdc45339..6934289484d 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueActionsBar.tsx @@ -19,8 +19,7 @@ */ import * as React from 'react'; -import { translate } from '../../../helpers/l10n'; -import { IssueActions, IssueResolution, IssueType as IssueTypeEnum } from '../../../types/issues'; +import { IssueActions } from '../../../types/issues'; import { Issue } from '../../../types/types'; import SoftwareImpactPillList from '../../shared/SoftwareImpactPillList'; import IssueAssign from './IssueAssign'; @@ -40,11 +39,6 @@ interface Props { showSonarLintBadge?: boolean; } -interface State { - commentAutoTriggered: boolean; - commentPlaceholder: string; -} - export default function IssueActionsBar(props: Props) { const { issue, @@ -56,45 +50,26 @@ export default function IssueActionsBar(props: Props) { showSonarLintBadge, } = props; - const [commentState, setCommentState] = React.useState({ - commentAutoTriggered: false, - commentPlaceholder: '', - }); + const [commentPlaceholder, setCommentPlaceholder] = React.useState(''); - const toggleComment = (open: boolean, placeholder = '', autoTriggered = false) => { - setCommentState({ - commentPlaceholder: placeholder, - commentAutoTriggered: autoTriggered, - }); + const toggleComment = (open: boolean, placeholder = '') => { + setCommentPlaceholder(placeholder); togglePopup('comment', open); }; - const handleTransition = (issue: Issue) => { - onChange(issue); - - if ( - issue.resolution === IssueResolution.FalsePositive || - (issue.resolution === IssueResolution.WontFix && issue.type !== IssueTypeEnum.SecurityHotspot) - ) { - toggleComment(true, translate('issue.comment.explain_why'), true); - } - }; - const canAssign = issue.actions.includes(IssueActions.Assign); const canComment = issue.actions.includes(IssueActions.Comment); - const hasTransitions = issue.transitions.length > 0; return (
      -
    • +
    • @@ -132,8 +107,7 @@ export default function IssueActionsBar(props: Props) { {canComment && ( { open={!!this.props.currentPopup} overlay={ ; + issue: Pick; onChange: (issue: Issue) => void; togglePopup: (popup: string, show?: boolean) => void; } -function SingleValueFactory(issue: Props['issue']) { - return function SingleValue< - V, - Option extends LabelValueSelectOption, - IsMulti extends boolean = false, - Group extends GroupBase
    + + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx new file mode 100644 index 00000000000..f434902fa0f --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransitionOverlay.tsx @@ -0,0 +1,136 @@ +/* + * 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 { + ButtonPrimary, + ButtonSecondary, + InputTextArea, + ItemDivider, + PageContentFontWrapper, + Spinner, +} from 'design-system'; +import * as React from 'react'; +import { useState } from 'react'; +import { useIntl } from 'react-intl'; +import { translate } from '../../../helpers/l10n'; +import { IssueActions, IssueTransition } from '../../../types/issues'; +import { Issue } from '../../../types/types'; +import { isTransitionDeprecated, isTransitionHidden, transitionRequiresComment } from '../helpers'; +import { IssueTransitionItem } from './IssueTransitionItem'; + +export type Props = { + issue: Pick; + onClose: () => void; + onSetTransition: (transition: IssueTransition, comment?: string) => void; + loading?: boolean; +}; + +export function IssueTransitionOverlay(props: Readonly) { + const { issue, onClose, onSetTransition, loading } = props; + + const intl = useIntl(); + + const [comment, setComment] = useState(''); + const [selectedTransition, setSelectedTransition] = useState(); + + const hasCommentAction = issue.actions.includes(IssueActions.Comment); + + function selectTransition(transition: IssueTransition) { + if (!transitionRequiresComment(transition) || !hasCommentAction) { + onSetTransition(transition); + } else { + setSelectedTransition(transition); + } + } + + function handleResolve() { + if (selectedTransition) { + onSetTransition(selectedTransition, comment); + } + } + + // Filter out hidden transitions and separate deprecated transitions in a different list + const filteredTransitions = issue.transitions.filter( + (transition) => !isTransitionHidden(transition), + ); + const filteredTransitionsRecommended = filteredTransitions.filter( + (t) => !isTransitionDeprecated(t), + ); + const filteredTransitionsDeprecated = filteredTransitions.filter(isTransitionDeprecated); + + return ( +
      + {filteredTransitionsRecommended.map((transition) => ( + + ))} + {filteredTransitionsRecommended.length > 0 && filteredTransitionsDeprecated.length > 0 && ( + + )} + {filteredTransitionsDeprecated.map((transition) => ( + + ))} + + {selectedTransition && ( + <> + +
      + + {intl.formatMessage({ id: 'issue.transition.comment' })} + + setComment(event.currentTarget.value)} + placeholder={translate( + 'issue.transition.comment.placeholder', + selectedTransition ?? '', + )} + rows={5} + value={comment} + size="auto" + className="sw-mt-2 sw-resize-y sw-w-full" + /> + +
      + {translate('resolve')} + {translate('cancel')} +
      +
      +
      + + )} + + {!selectedTransition && loading && ( +
      + +
      + )} +
    + ); +} diff --git a/server/sonar-web/src/main/js/components/issue/helpers.ts b/server/sonar-web/src/main/js/components/issue/helpers.ts new file mode 100644 index 00000000000..62789992482 --- /dev/null +++ b/server/sonar-web/src/main/js/components/issue/helpers.ts @@ -0,0 +1,37 @@ +/* + * 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 { IssueTransition } from '../../types/issues'; + +export function isTransitionDeprecated(transition: IssueTransition) { + return transition === IssueTransition.Confirm || transition === IssueTransition.Resolve; +} + +export function isTransitionHidden(transition: IssueTransition) { + return transition === IssueTransition.WontFix; +} + +export function transitionRequiresComment(transition: IssueTransition) { + return [ + IssueTransition.Accept, + IssueTransition.Confirm, + IssueTransition.FalsePositive, + IssueTransition.Resolve, + ].includes(transition); +} diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx index 0f6ae104b85..d667c2a1032 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentPopup.tsx @@ -29,7 +29,6 @@ export interface CommentPopupProps { toggleComment: (visible: boolean) => void; placeholder: string; placement?: PopupPlacement; - autoTriggered?: boolean; } export default class CommentPopup extends React.PureComponent { @@ -38,7 +37,7 @@ export default class CommentPopup extends React.PureComponent }; render() { - const { comment, autoTriggered } = this.props; + const { comment } = this.props; return ( @@ -49,7 +48,6 @@ export default class CommentPopup extends React.PureComponent onSaveComment={this.props.onComment} showFormatHelp comment={comment?.markdown} - autoTriggered={autoTriggered} /> diff --git a/server/sonar-web/src/main/js/components/shared/StatusHelper.tsx b/server/sonar-web/src/main/js/components/shared/StatusHelper.tsx index 392a6c2e8c6..90ea9cf83b2 100644 --- a/server/sonar-web/src/main/js/components/shared/StatusHelper.tsx +++ b/server/sonar-web/src/main/js/components/shared/StatusHelper.tsx @@ -18,22 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import StatusIcon from '../../components/icons/StatusIcon'; import { translate } from '../../helpers/l10n'; +import { IssueSimpleStatus } from '../../types/issues'; +import SimpleStatusIcon from '../icons/SimpleStatusIcon'; interface Props { className?: string; - resolution: string | undefined; - status: string; + simpleStatus: IssueSimpleStatus; } export default function StatusHelper(props: Props) { - const resolution = props.resolution && ` (${translate('issue.resolution', props.resolution)})`; return ( - - {translate('issue.status', props.status)} - {resolution} + + {translate('issue.simple_status', props.simpleStatus)} ); } diff --git a/server/sonar-web/src/main/js/helpers/issues.ts b/server/sonar-web/src/main/js/helpers/issues.ts index bd4ba2385df..54db9489922 100644 --- a/server/sonar-web/src/main/js/helpers/issues.ts +++ b/server/sonar-web/src/main/js/helpers/issues.ts @@ -19,7 +19,7 @@ */ import { BugIcon, CodeSmellIcon, SecurityHotspotIcon, VulnerabilityIcon } from 'design-system'; import { flatten, sortBy } from 'lodash'; -import { IssueType, RawIssue } from '../types/issues'; +import { IssueSimpleStatus, IssueStatus, IssueType, RawIssue } from '../types/issues'; import { MetricKey } from '../types/metrics'; import { Dict, Flow, FlowLocation, FlowType, Issue, TextRange } from '../types/types'; import { UserBase } from '../types/users'; @@ -160,6 +160,16 @@ export function parseIssueFromResponse( ...splitFlows(issue, components), ...prepareClosed(issue), ...ensureTextRange(issue), + simpleStatus: + issue.simpleStatus ?? + { + [IssueStatus.Open]: IssueSimpleStatus.Open, + [IssueStatus.Reopened]: IssueSimpleStatus.Open, + [IssueStatus.Closed]: IssueSimpleStatus.Fixed, + [IssueStatus.Resolved]: IssueSimpleStatus.Fixed, + [IssueStatus.Confirmed]: IssueSimpleStatus.Confirmed, + }[issue.status] ?? + IssueSimpleStatus.Open, } as Issue; } diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index b0fece2070a..702d92e2759 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -32,7 +32,14 @@ import { } from '../types/clean-code-taxonomy'; import { RuleRepository } from '../types/coding-rules'; import { EditionKey } from '../types/editions'; -import { IssueScope, IssueSeverity, IssueStatus, IssueType, RawIssue } from '../types/issues'; +import { + IssueScope, + IssueSeverity, + IssueSimpleStatus, + IssueStatus, + IssueType, + RawIssue, +} from '../types/issues'; import { Language } from '../types/languages'; import { MetricKey, MetricType } from '../types/metrics'; import { Notification } from '../types/notifications'; @@ -304,6 +311,7 @@ export function mockRawIssue(withLocations = false, overrides: Partial rule: 'javascript:S1067', severity: IssueSeverity.Major, status: IssueStatus.Open, + simpleStatus: IssueSimpleStatus.Open, textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 }, type: IssueType.CodeSmell, transitions: [], @@ -358,6 +366,7 @@ export function mockIssue(withLocations = false, overrides: Partial = {}) secondaryLocations: [], severity: IssueSeverity.Major, status: IssueStatus.Open, + simpleStatus: IssueSimpleStatus.Open, textRange: { startLine: 25, endLine: 26, startOffset: 0, endOffset: 15 }, transitions: [], type: 'BUG', diff --git a/server/sonar-web/src/main/js/types/issues.ts b/server/sonar-web/src/main/js/types/issues.ts index 4c14ed7c0d7..6dcba48dc6f 100644 --- a/server/sonar-web/src/main/js/types/issues.ts +++ b/server/sonar-web/src/main/js/types/issues.ts @@ -66,6 +66,14 @@ export enum IssueStatus { Closed = 'CLOSED', } +export enum IssueSimpleStatus { + Open = 'OPEN', + Fixed = 'FIXED', + Confirmed = 'CONFIRMED', + Accepted = 'ACCEPTED', + FalsePositive = 'FALSE_POSITIVE', +} + export enum IssueActions { SetType = 'set_type', SetTags = 'set_tags', @@ -75,6 +83,7 @@ export enum IssueActions { } export enum IssueTransition { + Accept = 'accept', Confirm = 'confirm', UnConfirm = 'unconfirm', Resolve = 'resolve', @@ -112,7 +121,7 @@ export interface RawFlowLocation { export interface RawIssue { actions: string[]; - transitions: string[]; + transitions: IssueTransition[]; tags?: string[]; assignee?: string; author?: string; @@ -140,6 +149,7 @@ export interface RawIssue { message?: string; severity: string; status: string; + simpleStatus: IssueSimpleStatus; textRange?: TextRange; type: IssueType; scope: string; diff --git a/server/sonar-web/src/main/js/types/types.ts b/server/sonar-web/src/main/js/types/types.ts index 570a3f239a4..c0d8973b325 100644 --- a/server/sonar-web/src/main/js/types/types.ts +++ b/server/sonar-web/src/main/js/types/types.ts @@ -25,7 +25,7 @@ import { SoftwareQuality, } from './clean-code-taxonomy'; import { ComponentQualifier, Visibility } from './component'; -import { MessageFormatting } from './issues'; +import { IssueSimpleStatus, IssueTransition, MessageFormatting } from './issues'; import { NewCodeDefinitionType } from './new-code-definition'; import { UserActive, UserBase } from './users'; @@ -291,9 +291,10 @@ export interface Issue { secondaryLocations: FlowLocation[]; severity: string; status: string; + simpleStatus: IssueSimpleStatus; tags?: string[]; textRange?: TextRange; - transitions: string[]; + transitions: IssueTransition[]; type: IssueType; } 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 f24fae9f333..d0942e614b0 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -192,6 +192,7 @@ reset_verb=Reset reset_to_default=Reset To Default reset_date=Reset dates resolution=Resolution +resolve=Resolve restart=Restart restore=Restore result=Result @@ -923,22 +924,34 @@ issue.severity.severity_x_click_to_change=Severity: {0}, click to change issue.transition.community_plug_link=SonarSource Community issue.transition.status_x_click_to_change=Issue status: {0}, click to change issue.transition=Transition +issue.transition.accept=Accept +issue.transition.accept.description="Won't fix now" issue.transition.confirm=Confirm -issue.transition.confirm.description=This issue has been reviewed and something should be done eventually to handle it. -issue.transition.unconfirm=Unconfirm -issue.transition.unconfirm.description=This issue should be reviewed again to decide what to do with it. -issue.transition.resolve=Resolve as fixed -issue.transition.resolve.description=This issue has been fixed in the code and is waiting for the next analysis to close it - or reopen it if it was not actually fixed. -issue.transition.falsepositive=Resolve as false positive -issue.transition.falsepositive.description=This issue can be suppressed as it was not raised accurately. Please report false-positives to the {community_plug_link}! -issue.transition.reopen=Reopen -issue.transition.reopen.description=This issue is not resolved, and should be reviewed again. +issue.transition.confirm.description=Deprecated +issue.transition.confirm.deprecated_tooltip.1=The Confirm action is deprecated. +issue.transition.confirm.deprecated_tooltip.2=The next analysis result will show if the issue has been fixed, otherwise it will reopen it automatically. +issue.transition.confirm.deprecated_tooltip.3=If you were using Confirm to communicate with team members, consider assigning the issue or using comments and tags instead. +issue.transition.confirm.deprecated_tooltip.4=If you have reviewed this issue but cannot fix it now, consider marking it as Accepted. +issue.transition.unconfirm=Open +issue.transition.unconfirm.description=Reopen issue +issue.transition.resolve=Fixed +issue.transition.resolve.description=Deprecated +issue.transition.resolve.deprecated_tooltip.1=The Resolve as Fixed action is deprecated. +issue.transition.resolve.deprecated_tooltip.2=The next analysis result will show if the issue has been fixed, otherwise it will reopen the issue automatically. +issue.transition.resolve.deprecated_tooltip.3=If you were using Resolve as Fixed to communicate with team members that an issue is being fixed, consider assigning it or using comments and tags instead. +issue.transition.falsepositive=False Positive +issue.transition.falsepositive.description=Analysis is mistaken +issue.transition.reopen=Open +issue.transition.reopen.description=Reopen issue +issue.transition.comment=Status change comment +issue.transition.comment.placeholder.accept=Share why (optional) +issue.transition.comment.placeholder.confirm=Share why this is confirmed (optional) +issue.transition.comment.placeholder.resolve=Share why this is fixed (optional) +issue.transition.comment.placeholder.falsepositive=Share why this is a false positive (optional) issue.transition.close=Close issue.transition.close.description= -issue.transition.wontfix=Resolve as won't fix -issue.transition.wontfix.description=This issue can be suppressed because the rule is irrelevant in this context. -issue.transition.setinreview=Set as In Review -issue.transition.setinreview.description=A review is in progress to check for a vulnerability +issue.transition.wontfix=Won't Fix +issue.transition.wontfix.description=Deprecated issue.transition.openasvulnerability=Open as Vulnerability issue.transition.openasvulnerability.description=There's a Vulnerability in the code that must be fixed issue.transition.resolveasreviewed=Resolve as Reviewed @@ -1040,6 +1053,13 @@ issue.clean_code_attribute.TRUSTWORTHY=Not trustworthy issue.clean_code_attribute.TRUSTWORTHY.title=This is a responsibility issue, the code is not trustworthy enough. issue.clean_code_attribute.TRUSTWORTHY.advice=To be trustworthy, the code needs to abstain from revealing or hard-coding private information. +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.status.ACCEPTED=Accepted issue.status.REOPENED=Reopened issue.status.RESOLVED=Resolved issue.status.OPEN=Open -- 2.39.5