From 459cbaf76d35b4138ab37cdeb7a7c53bf6e9e514 Mon Sep 17 00:00:00 2001 From: Ismail Cherri Date: Tue, 5 Nov 2024 17:11:15 +0100 Subject: [PATCH] SONAR-23531 Display severities in issue activity log based on current mode --- .../main/js/api/mocks/IssuesServiceMock.ts | 21 ++ .../issues/__tests__/IssuesAppActivity-it.tsx | 15 +- .../issues/components/IssueReviewHistory.tsx | 226 +++++++++--------- .../issue/components/IssueChangelogDiff.tsx | 19 +- .../issue/components/IssueTransition.tsx | 4 +- .../sonar-web/src/main/js/queries/issues.ts | 58 +++++ .../resources/org/sonar/l10n/core.properties | 1 + 7 files changed, 229 insertions(+), 115 deletions(-) create mode 100644 server/sonar-web/src/main/js/queries/issues.ts 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 55c970a84e9..d6f19cfa92b 100644 --- a/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts @@ -36,6 +36,7 @@ import { ASSIGNEE_ME, IssueDeprecatedStatus, IssueResolution, + IssueSeverity, IssueStatus, IssueTransition, IssueType, @@ -689,6 +690,26 @@ export default class IssuesServiceMock { }, ], }), + mockIssueChangelog({ + creationDate: '2018-12-01', + diffs: [ + { + key: 'severity', + newValue: IssueSeverity.Blocker, + oldValue: IssueSeverity.Major, + }, + ], + }), + mockIssueChangelog({ + creationDate: '2018-12-02', + diffs: [ + { + key: 'impactSeverity', + newValue: `${SoftwareQuality.Maintainability}:${SoftwareImpactSeverity.Blocker}`, + oldValue: `${SoftwareQuality.Maintainability}:${SoftwareImpactSeverity.High}`, + }, + ], + }), ], }); }; diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesAppActivity-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesAppActivity-it.tsx index d3b7f186f71..adb6c9a2cd8 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesAppActivity-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesAppActivity-it.tsx @@ -22,11 +22,13 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; import { mockRestUser } from '../../../helpers/testMocks'; +import { SettingsKey } from '../../../types/settings'; import { branchHandler, componentsHandler, issuesHandler, renderIssueApp, + settingsHandler, ui, usersHandler, } from '../test-utils'; @@ -62,6 +64,7 @@ beforeEach(() => { componentsHandler.reset(); branchHandler.reset(); usersHandler.reset(); + settingsHandler.reset(); usersHandler.users = [ mockRestUser({ login: 'bob.marley', @@ -114,7 +117,15 @@ it('should be able to add or update comment', async () => { expect(screen.queryByText('activity comment new')).not.toBeInTheDocument(); }); -it('should be able to show changelog', async () => { +it.each([ + ['MQR mode', 'true', 'issue.changelog.impactSeverity.MAINTAINABILITY.BLOCKER.HIGH'], + [ + 'Standard mode', + 'false', + 'issue.changelog.changed_to.issue.changelog.field.severity.BLOCKER (issue.changelog.was.MAJOR)', + ], +])('should be able to show changelog in %s', async (_, mode, message) => { + settingsHandler.set(SettingsKey.MQRMode, mode); const user = userEvent.setup(); issuesHandler.setIsAdmin(true); renderIssueApp(); @@ -139,6 +150,8 @@ it('should be able to show changelog', async () => { 'issue.changelog.changed_to.issue.changelog.field.issueStatus.ACCEPTED (issue.changelog.was.OPEN)', ), ).toBeInTheDocument(); + // Severities and Software Quality Severities + expect(screen.getByText(message)).toBeInTheDocument(); expect( screen.queryByText( 'issue.changelog.changed_to.issue.changelog.field.status.RESOLVED (issue.changelog.was.REOPENED)', diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx index 28bcc0adb64..bc3ac902a2c 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx @@ -19,13 +19,12 @@ */ import styled from '@emotion/styled'; +import { Button, ButtonVariety, Spinner, Text } from '@sonarsource/echoes-react'; import * as React from 'react'; import { - DangerButtonPrimary, DestructiveIcon, HtmlFormatter, InteractiveIcon, - LightLabel, Modal, PencilIcon, SafeHTMLInjection, @@ -33,11 +32,12 @@ import { TrashIcon, themeBorder, } from '~design-system'; -import { getIssueChangelog } from '../../../api/issues'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff'; import Avatar from '../../../components/ui/Avatar'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { useIssueChangelogQuery } from '../../../queries/issues'; +import { useStandardExperienceMode } from '../../../queries/settings'; import { ReviewHistoryType } from '../../../types/security-hotspots'; import { Issue, IssueChangelog } from '../../../types/types'; import HotspotCommentModal from '../../security-hotspots/components/HotspotCommentModal'; @@ -49,133 +49,139 @@ export interface HotspotReviewHistoryProps { onEditComment: (key: string, comment: string) => void; } -const getUpdatedChangelog = ({ changelog }: { changelog: IssueChangelog[] }) => +const getUpdatedChangelog = ( + { changelog }: { changelog: IssueChangelog[] }, + isStandardMode = false, +): IssueChangelog[] => changelog.map((changelogItem) => { const diffHasIssueStatusChange = changelogItem.diffs.some((diff) => diff.key === 'issueStatus'); + const filteredDiffs = changelogItem.diffs.filter((diff) => { + if (diffHasIssueStatusChange && ['resolution', 'status'].includes(diff.key)) { + return false; + } + return isStandardMode ? diff.key !== 'impactSeverity' : diff.key !== 'severity'; + }); + return { ...changelogItem, - // If the diff is an issue status change, we remove deprecated status and resolution diffs - diffs: changelogItem.diffs.filter( - (diff) => !(diffHasIssueStatusChange && ['resolution', 'status'].includes(diff.key)), - ), + diffs: filteredDiffs, }; }); export default function IssueReviewHistory(props: Readonly) { const { issue } = props; - const [changeLog, setChangeLog] = React.useState([]); - const history = useGetIssueReviewHistory(issue, changeLog); + const { data: isStandardMode } = useStandardExperienceMode(); const [editCommentKey, setEditCommentKey] = React.useState(''); const [deleteCommentKey, setDeleteCommentKey] = React.useState(''); - - React.useEffect(() => { - getIssueChangelog(issue.key).then( - ({ changelog }) => { - const updatedChangelog = getUpdatedChangelog({ changelog }); - - setChangeLog(updatedChangelog); - }, - () => {}, - ); - }, [issue]); + const { data: changelog = [], isLoading } = useIssueChangelogQuery(issue.key, { + select: (data) => getUpdatedChangelog(data, isStandardMode), + }); + const history = useGetIssueReviewHistory(issue, changelog); return ( -
    - {history.map((historyElt, historyIndex) => { - const { user, type, diffs, date, html, key, updatable, markdown } = historyElt; - return ( -
  • -
    - -
    - - - {user.name && ( -
    - - - {user.active ? user.name : translateWithParameters('user.x_deleted', user.name)} - -
    - )} - - {type === ReviewHistoryType.Creation && - translate('issue.activity.review_history.created')} - - {type === ReviewHistoryType.Comment && - translate('issue.activity.review_history.comment_added')} -
    - - {type === ReviewHistoryType.Diff && diffs && ( -
    - {diffs.map((diff, diffIndex) => ( - - ))} + +
      + {history.map(({ user, type, diffs, date, html, key, updatable, markdown }) => { + return ( +
    • +
      +
      - )} - - {type === ReviewHistoryType.Comment && key && html && markdown && ( -
      - - - - - {updatable && ( -
      - setEditCommentKey(key)} - size="small" - stopPropagation={false} - /> - setDeleteCommentKey(key)} - size="small" - stopPropagation={false} - /> + + {user.name !== undefined && ( +
      + + + {user.active + ? user.name + : translateWithParameters('user.x_deleted', user.name)} +
      )} - {editCommentKey === key && ( - setEditCommentKey('')} - onSubmit={(comment) => { - setEditCommentKey(''); - props.onEditComment(key, comment); - }} - /> - )} + {type === ReviewHistoryType.Creation && + translate('issue.activity.review_history.created')} - {deleteCommentKey === key && ( - setDeleteCommentKey('')} - body={

      {translate('issue.comment.delete_confirm_message')}

      } - primaryButton={ - { - setDeleteCommentKey(''); - props.onDeleteComment(key); - }} - > - {translate('delete')} - - } - secondaryButtonLabel={translate('cancel')} - /> - )} -
      - )} -
    • - ); - })} -
    + {type === ReviewHistoryType.Comment && + translate('issue.activity.review_history.comment_added')} + + + {type === ReviewHistoryType.Diff && diffs && ( +
    + {diffs.map((diff) => ( + + ))} +
    + )} + + {type === ReviewHistoryType.Comment && key && html && markdown && ( +
    + + + + + {updatable && ( +
    + setEditCommentKey(key)} + size="small" + stopPropagation={false} + /> + + setDeleteCommentKey(key)} + size="small" + stopPropagation={false} + /> +
    + )} + + {editCommentKey === key && ( + setEditCommentKey('')} + onSubmit={(comment) => { + setEditCommentKey(''); + props.onEditComment(key, comment); + }} + /> + )} + + {deleteCommentKey === key && ( + setDeleteCommentKey('')} + body={

    {translate('issue.comment.delete_confirm_message')}

    } + primaryButton={ + + } + secondaryButtonLabel={translate('cancel')} + /> + )} +
    + )} +
  • + ); + })} +
+ ); } diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx index d4af6b8303b..56626b5401f 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueChangelogDiff.tsx @@ -26,9 +26,7 @@ export interface IssueChangelogDiffProps { diff: TypeIssueChangelogDiff; } -export default function IssueChangelogDiff(props: Readonly) { - const { diff } = props; - +export default function IssueChangelogDiff({ diff }: Readonly) { const diffComputedValues = { newValue: diff.newValue ?? '', oldValue: diff.oldValue ?? '', @@ -79,6 +77,21 @@ export default function IssueChangelogDiff(props: Readonly + {translateWithParameters( + 'issue.changelog.impactSeverity', + softwareQuality, + newSeverity, + oldSeverity, + )} +

+ ); + } + if (diff.key === 'effort') { diffComputedValues.newValue = formatMeasure(diff.newValue, 'WORK_DUR'); diffComputedValues.oldValue = formatMeasure(diff.oldValue, 'WORK_DUR'); diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx index b839b72ce70..bfb03626961 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx @@ -28,9 +28,9 @@ import { PopupZLevel, SearchSelectDropdownControl, } from '~design-system'; -import { addIssueComment, setIssueTransition } from '../../../api/issues'; import { SESSION_STORAGE_TRANSITION_GUIDE_KEY } from '../../../apps/issues/components/IssueNewStatusAndTransitionGuide'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { useIssueCommentMutation, useIssueTransitionMutation } from '../../../queries/issues'; import { Issue } from '../../../types/types'; import StatusHelper from '../../shared/StatusHelper'; import { updateIssue } from '../actions'; @@ -49,6 +49,8 @@ export default function IssueTransition(props: Readonly) { const guideStepIndex = +(sessionStorage.getItem(SESSION_STORAGE_TRANSITION_GUIDE_KEY) ?? 0); const guideIsRunning = sessionStorage.getItem(SESSION_STORAGE_TRANSITION_GUIDE_KEY) !== null; const [transitioning, setTransitioning] = React.useState(false); + const { mutateAsync: setIssueTransition } = useIssueTransitionMutation(); + const { mutateAsync: addIssueComment } = useIssueCommentMutation(); async function handleSetTransition(transition: string, comment?: string) { setTransitioning(true); diff --git a/server/sonar-web/src/main/js/queries/issues.ts b/server/sonar-web/src/main/js/queries/issues.ts new file mode 100644 index 00000000000..ceda4430021 --- /dev/null +++ b/server/sonar-web/src/main/js/queries/issues.ts @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { QueryClient, queryOptions, useMutation, useQueryClient } from '@tanstack/react-query'; +import { addIssueComment, getIssueChangelog, setIssueTransition } from '../api/issues'; +import { createQueryHook } from './common'; + +const issuesQuery = { + changelog: (issueKey: string) => ['issue', issueKey, 'changelog'] as const, +}; + +export const useIssueChangelogQuery = createQueryHook((issueKey: string) => { + return queryOptions({ + queryKey: issuesQuery.changelog(issueKey), + queryFn: () => getIssueChangelog(issueKey), + }); +}); + +export function useIssueTransitionMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: { issue: string; transition: string }) => setIssueTransition(data), + onSuccess: ({ issue }) => { + invalidateIssueChangelog(issue.key, queryClient); + }, + }); +} + +export function useIssueCommentMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data: { issue: string; text: string }) => addIssueComment(data), + onSuccess: ({ issue }) => { + invalidateIssueChangelog(issue.key, queryClient); + }, + }); +} + +function invalidateIssueChangelog(issueKey: string, queryClient: QueryClient) { + queryClient.invalidateQueries({ queryKey: issuesQuery.changelog(issueKey) }); +} 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 2dbc5b2fcf2..8c9d4bb4903 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -1222,6 +1222,7 @@ issue.changelog.field.code_variants=Code Variants issue.changelog.field.type=Type issue.changelog.field.file=File issue.changelog.field.cleanCodeAttribute=Clean Code Attribute +issue.changelog.impactSeverity={0} severity changed to {1} (was {2}) #------------------------------------------------------------------------------ # -- 2.39.5