From e51d424fedcff2d69d5e83c911a96f30f598d8f8 Mon Sep 17 00:00:00 2001 From: stanislavh Date: Tue, 23 May 2023 14:39:49 +0200 Subject: [PATCH] SONAR-19236 Update status updated success modal --- .../design-system/src/components/index.ts | 1 + .../SecurityHotspotsAppRenderer.tsx | 36 +++++++-------- .../__tests__/SecurityHotspotsApp-it.tsx | 41 +++++++++++++++++ .../components/HotspotViewer.tsx | 30 +++++++++++-- .../components/HotspotViewerRenderer.tsx | 19 ++++++++ .../components/StatusUpdateSuccessModal.tsx | 45 +++++++++++++------ .../components/status/StatusSelection.tsx | 9 +--- .../js/apps/security-hotspots/constants.ts | 21 +++++++++ .../resources/org/sonar/l10n/core.properties | 3 +- 9 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 server/sonar-web/src/main/js/apps/security-hotspots/constants.ts diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 7e500593b08..0cf94260576 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -23,6 +23,7 @@ export * from './Avatar'; export { Badge } from './Badge'; export { BarChart } from './BarChart'; export * from './Card'; +export * from './Checkbox'; export * from './CodeSnippet'; export * from './CoverageIndicator'; export * from './DatePicker'; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx index 8af9b7bccfd..1e38cab7c16 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/SecurityHotspotsAppRenderer.tsx @@ -21,7 +21,6 @@ import { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { DeferredSpinner, - LAYOUT_FOOTER_HEIGHT, LAYOUT_GLOBAL_NAV_HEIGHT, LAYOUT_PROJECT_NAV_HEIGHT, LargeCenteredLayout, @@ -35,7 +34,6 @@ import A11ySkipTarget from '../../components/a11y/A11ySkipTarget'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; import { isBranch } from '../../helpers/branch-like'; import { translate } from '../../helpers/l10n'; -import useFollowScroll from '../../hooks/useFollowScroll'; import { BranchLike } from '../../types/branch-like'; import { ComponentQualifier } from '../../types/component'; import { MetricKey } from '../../types/metrics'; @@ -80,6 +78,8 @@ export interface SecurityHotspotsAppRendererProps { standards: Standards; } +const STICKY_HEADER_HEIGHT = 73; + export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRendererProps) { const { branchLike, @@ -105,11 +105,6 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe const isProject = component.qualifier === ComponentQualifier.Project; - const { top: topScroll } = useFollowScroll(); - const distanceFromBottom = topScroll + window.innerHeight - document.body.clientHeight; - const footerVisibleHeight = - distanceFromBottom > -LAYOUT_FOOTER_HEIGHT ? LAYOUT_FOOTER_HEIGHT + distanceFromBottom : 0; - return ( <> @@ -118,19 +113,13 @@ export default function SecurityHotspotsAppRenderer(props: SecurityHotspotsAppRe -
+
{isProject && ( - + - + {hotspots.length === 0 || !selectedHotspot ? ( ) : ( { + if (value) { + showDialog = value; + } +}); +jest.mocked(get).mockImplementation(() => showDialog); beforeAll(() => { Object.defineProperty(window, 'scrollTo', { @@ -221,6 +236,9 @@ describe('CRUD', () => { await user.click(ui.changeStatus.get()); }); + expect(ui.continueReviewingButton.get()).toBeInTheDocument(); + await user.click(ui.continueReviewingButton.get()); + await user.click(ui.activityTab.get()); expect(setSecurityHotspotStatus).toHaveBeenLastCalledWith('test-1', { comment: 'COMMENT-TEXT', @@ -345,6 +363,29 @@ describe('navigation', () => { }); }); +it('after status change, should be able to disable success dialog show', async () => { + const user = userEvent.setup(); + + renderSecurityHotspotsApp(); + await user.click(await ui.reviewButton.find()); + await user.click(ui.toReviewStatus.get()); + await act(async () => { + await user.click(ui.changeStatus.get()); + }); + + await user.click(ui.dontShowSuccessDialogCheckbox.get()); + expect(ui.dontShowSuccessDialogCheckbox.get()).toBeChecked(); + await user.click(ui.continueReviewingButton.get()); + + // Repeat status change and verify that dialog is not shown + await user.click(await ui.reviewButton.find()); + await user.click(ui.toReviewStatus.get()); + await act(async () => { + await user.click(ui.changeStatus.get()); + }); + expect(ui.continueReviewingButton.query()).not.toBeInTheDocument(); +}); + it('should be able to filter the hotspot list', async () => { const user = userEvent.setup(); renderSecurityHotspotsApp(); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx index cfb3f941efb..0a9e334bd08 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewer.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { getRuleDetails } from '../../../api/rules'; import { getSecurityHotspotDetails } from '../../../api/security-hotspots'; +import { get } from '../../../helpers/storage'; import { Standards } from '../../../types/security'; import { Hotspot, @@ -28,6 +29,7 @@ import { } from '../../../types/security-hotspots'; import { Component } from '../../../types/types'; import { RuleDescriptionSection } from '../../coding-rules/rule'; +import { SHOW_STATUS_DIALOG_STORAGE_KEY } from '../constants'; import { getStatusFilterFromStatusOption } from '../utils'; import HotspotViewerRenderer from './HotspotViewerRenderer'; @@ -35,6 +37,7 @@ interface Props { component: Component; hotspotKey: string; onSwitchStatusFilter: (option: HotspotStatusFilter) => void; + hotspotsReviewedMeasure?: string; onUpdateHotspot: (hotspotKey: string) => Promise; onLocationClick: (index: number) => void; selectedHotspotLocation?: number; @@ -46,6 +49,7 @@ interface State { ruleDescriptionSections?: RuleDescriptionSection[]; lastStatusChangedTo?: HotspotStatusOption; loading: boolean; + showStatusUpdateSuccessModal: boolean; } export default class HotspotViewer extends React.PureComponent { @@ -54,7 +58,7 @@ export default class HotspotViewer extends React.PureComponent { constructor(props: Props) { super(props); - this.state = { loading: false }; + this.state = { loading: false, showStatusUpdateSuccessModal: false }; } componentDidMount() { @@ -96,7 +100,10 @@ export default class HotspotViewer extends React.PureComponent { const { hotspotKey } = this.props; if (statusUpdate) { - this.setState({ lastStatusChangedTo: statusOption }); + this.setState({ + lastStatusChangedTo: statusOption, + showStatusUpdateSuccessModal: get(SHOW_STATUS_DIALOG_STORAGE_KEY) !== 'false', + }); await this.props.onUpdateHotspot(hotspotKey); } else { await this.fetchHotspot(); @@ -110,12 +117,27 @@ export default class HotspotViewer extends React.PureComponent { } }; + handleCloseStatusUpdateSuccessModal = () => { + this.setState({ showStatusUpdateSuccessModal: false }); + }; + render() { - const { component, selectedHotspotLocation, standards } = this.props; - const { hotspot, ruleDescriptionSections, loading } = this.state; + const { component, selectedHotspotLocation, standards, hotspotsReviewedMeasure } = this.props; + const { + hotspot, + ruleDescriptionSections, + loading, + showStatusUpdateSuccessModal, + lastStatusChangedTo, + } = this.state; return ( void; + lastStatusChangedTo?: HotspotStatusOption; + onCloseStatusUpdateSuccessModal: () => void; + showStatusUpdateSuccessModal: boolean; + loading: boolean; onUpdateHotspot: (statusUpdate?: boolean, statusOption?: HotspotStatusOption) => Promise; onLocationClick: (index: number) => void; @@ -51,6 +58,9 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { loading, selectedHotspotLocation, ruleDescriptionSections, + showStatusUpdateSuccessModal, + hotspotsReviewedMeasure, + lastStatusChangedTo, standards, } = props; @@ -60,6 +70,15 @@ export function HotspotViewerRenderer(props: HotspotViewerRendererProps) { <> + {showStatusUpdateSuccessModal && ( + + )} + {hotspot && (
{ + setIsChecked(value); + save(SHOW_STATUS_DIALOG_STORAGE_KEY, (!value).toString()); + }; + return ( - -
-

{translateWithParameters('hotspots.successful_status_change_to_x', statusLabel)}

-
+ + +

+ {translateWithParameters('hotspots.successful_status_change_to_x', statusLabel)} +

-
+
{closingHotspots && ( -

+

)}

+ + {translate('hotspots.success_dialog.do_not_show')} + -
- +
+ { + props.onSwitchFilterToStatusOfUpdatedHotspot(); + props.onClose(); + }} + > {translateWithParameters('hotspots.see_x_hotspots', statusLabel)} - - +
); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx index 63098478f33..53d9637b836 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/status/StatusSelection.tsx @@ -19,8 +19,6 @@ */ import * as React from 'react'; import { setSecurityHotspotStatus } from '../../../../api/security-hotspots'; -import { addGlobalSuccessMessage } from '../../../../helpers/globalMessages'; -import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { Hotspot, HotspotStatusOption } from '../../../../types/security-hotspots'; import { getStatusAndResolutionFromStatusOption, @@ -58,12 +56,7 @@ export default function StatusSelection(props: Props) { comment: comment || undefined, }); await props.onStatusOptionChange(status); - addGlobalSuccessMessage( - translateWithParameters( - 'hotspots.update.success', - translate('hotspots.status_option', status) - ) - ); + props.onClose(); } catch { setLoading(false); diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/constants.ts b/server/sonar-web/src/main/js/apps/security-hotspots/constants.ts new file mode 100644 index 00000000000..9a9ee84b9bf --- /dev/null +++ b/server/sonar-web/src/main/js/apps/security-hotspots/constants.ts @@ -0,0 +1,21 @@ +/* + * 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. + */ + +export const SHOW_STATUS_DIALOG_STORAGE_KEY = 'show_hotspot_success_status_dialog'; 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 f9438870a58..10cdf553426 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -862,7 +862,8 @@ hotspots.review_hotspot=Review Hotspot hotspots.assign.success=Security Hotspot was successfully assigned to {0} hotspots.assign.unassign.success=Security Hotspot was successfully unassigned -hotspots.update.success=Security Hotspot status was successfully changed to {0} +hotspots.update.success=Update successful +hotspots.success_dialog.do_not_show=Don't show this dialog next time #------------------------------------------------------------------------------ # -- 2.39.5