Browse Source

SONAR-20717 Save issue guide steps in sessionstorage

tags/10.5.0.89998
Viktor Vorona 2 months ago
parent
commit
4c28b0f609

+ 0
- 207
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesAppGuide-it.tsx View File

@@ -1,207 +0,0 @@
/*
* 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 userEvent from '@testing-library/user-event';
import React from 'react';
import { mockCurrentUser } from '../../../helpers/testMocks';
import { NoticeType } from '../../../types/users';
import {
branchHandler,
componentsHandler,
issuesHandler,
renderIssueApp,
renderProjectIssuesApp,
ui,
usersHandler,
} from '../test-utils';

jest.mock('../sidebar/Sidebar', () => {
const fakeSidebar = () => {
return <div data-guiding-id="issue-5" />;
};
return {
__esModule: true,
default: fakeSidebar,
Sidebar: fakeSidebar,
};
});

jest.mock('../../../components/common/ScreenPositionHelper', () => ({
__esModule: true,
default: class ScreenPositionHelper extends React.Component<{
children: (args: { top: number }) => React.ReactNode;
}> {
render() {
// eslint-disable-next-line testing-library/no-node-access
return this.props.children({ top: 10 });
}
},
}));

beforeEach(() => {
issuesHandler.reset();
componentsHandler.reset();
branchHandler.reset();
usersHandler.reset();
window.scrollTo = jest.fn();
window.HTMLElement.prototype.scrollTo = jest.fn();
});

it('should display guide', async () => {
const user = userEvent.setup();
renderIssueApp(
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: { [NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true },
}),
);

expect(await ui.guidePopup.find()).toBeInTheDocument();

expect(await ui.guidePopup.find()).toBeInTheDocument();
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.1.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.1.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.1.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.2.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.2.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.2.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.3.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.3.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.3.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.4.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.4.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.4.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.5.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.5.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.5.5');

expect(ui.guidePopup.byRole('button', { name: 'Next' }).query()).not.toBeInTheDocument();

await user.click(ui.guidePopup.byRole('button', { name: 'close' }).get());

expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide for those who dismissed it', async () => {
renderIssueApp(
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: {
[NoticeType.ISSUE_GUIDE]: true,
[NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true,
},
}),
);

expect((await ui.issueItems.findAll()).length).toBeGreaterThan(0);
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should skip guide', async () => {
const user = userEvent.setup();
renderIssueApp(
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: { [NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true },
}),
);

expect(await ui.guidePopup.find()).toBeInTheDocument();
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.1.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.1.5');

await user.click(ui.guidePopup.byRole('button', { name: 'skip' }).get());

expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide if issues need sync', async () => {
renderProjectIssuesApp(
undefined,
{ needIssueSync: true },
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: { [NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true },
}),
);

expect((await ui.issueItems.findAll()).length).toBeGreaterThan(0);
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide if user is not logged in', async () => {
renderIssueApp(mockCurrentUser({ isLoggedIn: false }));

expect((await ui.issueItems.findAll()).length).toBeGreaterThan(0);
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide if there are no issues', () => {
issuesHandler.setIssueList([]);
renderIssueApp(mockCurrentUser({ isLoggedIn: true }));

expect(ui.loading.query()).not.toBeInTheDocument();
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should show guide on issue page', async () => {
const user = userEvent.setup();
renderProjectIssuesApp(
'project/issues?issues=issue11&open=issue11&id=myproject',
undefined,
mockCurrentUser({ isLoggedIn: true }),
);

expect(await ui.guidePopup.find()).toBeInTheDocument();
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.1.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.2.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.3.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.4.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.5.5');

expect(ui.guidePopup.byRole('button', { name: 'Next' }).query()).not.toBeInTheDocument();

await user.click(ui.guidePopup.byRole('button', { name: 'close' }).get());

expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

+ 243
- 0
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesAppGuides-it.tsx View File

@@ -0,0 +1,243 @@
/*
* 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 userEvent from '@testing-library/user-event';
import React from 'react';
import { mockCurrentUser } from '../../../helpers/testMocks';
import { IssueTransition } from '../../../types/issues';
import { NoticeType } from '../../../types/users';
import {
branchHandler,
componentsHandler,
issuesHandler,
renderIssueApp,
renderProjectIssuesApp,
ui,
usersHandler,
} from '../test-utils';

jest.mock('../sidebar/Sidebar', () => {
const fakeSidebar = () => {
return <div data-guiding-id="issue-5" />;
};
return {
__esModule: true,
default: fakeSidebar,
Sidebar: fakeSidebar,
};
});

jest.mock('../../../components/common/ScreenPositionHelper', () => ({
__esModule: true,
default: class ScreenPositionHelper extends React.Component<{
children: (args: { top: number }) => React.ReactNode;
}> {
render() {
// eslint-disable-next-line testing-library/no-node-access
return this.props.children({ top: 10 });
}
},
}));

describe('issue guides', () => {
beforeEach(() => {
issuesHandler.reset();
componentsHandler.reset();
branchHandler.reset();
usersHandler.reset();
window.scrollTo = jest.fn();
window.HTMLElement.prototype.scrollTo = jest.fn();
});

describe('Issue Guide', () => {
it('should display guide', async () => {
const user = userEvent.setup();
renderIssueApp(
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: { [NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true },
}),
);

expect(await ui.guidePopup.find()).toBeInTheDocument();

expect(await ui.guidePopup.find()).toBeInTheDocument();
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.1.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.1.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.1.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.2.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.2.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.2.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.3.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.3.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.3.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.4.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.4.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.4.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.5.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.5.content');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.5.5');

expect(ui.guidePopup.byRole('button', { name: 'Next' }).query()).not.toBeInTheDocument();

await user.click(ui.guidePopup.byRole('button', { name: 'close' }).get());

expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide for those who dismissed it', async () => {
renderIssueApp(
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: {
[NoticeType.ISSUE_GUIDE]: true,
[NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true,
},
}),
);

expect((await ui.issueItems.findAll()).length).toBeGreaterThan(0);
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should skip guide', async () => {
const user = userEvent.setup();
renderIssueApp(
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: { [NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true },
}),
);

expect(await ui.guidePopup.find()).toBeInTheDocument();
expect(ui.guidePopup.get()).toHaveTextContent('guiding.issue_list.1.title');
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.1.5');

await user.click(ui.guidePopup.byRole('button', { name: 'skip' }).get());

expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide if issues need sync', async () => {
renderProjectIssuesApp(
undefined,
{ needIssueSync: true },
mockCurrentUser({
isLoggedIn: true,
dismissedNotices: { [NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE]: true },
}),
);

expect((await ui.issueItems.findAll()).length).toBeGreaterThan(0);
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide if user is not logged in', async () => {
renderIssueApp(mockCurrentUser({ isLoggedIn: false }));

expect((await ui.issueItems.findAll()).length).toBeGreaterThan(0);
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should not show guide if there are no issues', () => {
issuesHandler.setIssueList([]);
renderIssueApp(mockCurrentUser({ isLoggedIn: true }));

expect(ui.loading.query()).not.toBeInTheDocument();
expect(ui.guidePopup.query()).not.toBeInTheDocument();
});

it('should show guide on issue page and save its step on opened issue', async () => {
const user = userEvent.setup();
renderProjectIssuesApp('project/issues', undefined, mockCurrentUser({ isLoggedIn: true }));

expect(await ui.guidePopup.find()).toBeInTheDocument();
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.1.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.2.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.3.5');

await user.click(ui.issueItemAction1.get());

expect(await ui.guidePopup.find()).toHaveTextContent('guiding.step_x_of_y.3.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.4.5');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());

expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.5.5');

expect(ui.guidePopup.byRole('button', { name: 'Next' }).query()).not.toBeInTheDocument();

await user.click(ui.guidePopup.byRole('button', { name: 'close' }).get());

expect(ui.guidePopup.query()).not.toBeInTheDocument();
});
});

describe('Issue new status and transition guide', () => {
it('should save transition guide step', async () => {
const user = userEvent.setup();
issuesHandler.list[0].issue.transitions = [
IssueTransition.Accept,
IssueTransition.Confirm,
IssueTransition.Resolve,
IssueTransition.FalsePositive,
IssueTransition.WontFix,
];
renderProjectIssuesApp(
'project/issues',
undefined,
mockCurrentUser({ isLoggedIn: true, dismissedNotices: { [NoticeType.ISSUE_GUIDE]: true } }),
);

expect(await ui.guidePopup.find()).toBeInTheDocument();
await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());
expect(ui.guidePopup.get()).toHaveTextContent('guiding.step_x_of_y.2.3');

await user.click(ui.issueItemAction1.get());

expect(await ui.guidePopup.find()).toHaveTextContent('guiding.step_x_of_y.2.3');

await user.click(ui.guidePopup.byRole('button', { name: 'next' }).get());
await user.click(ui.guidePopup.byRole('button', { name: 'close' }).get());

expect(ui.guidePopup.query()).not.toBeInTheDocument();
});
});
});

+ 38
- 11
server/sonar-web/src/main/js/apps/issues/components/IssueGuide.tsx View File

@@ -18,8 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { SpotlightTour, SpotlightTourStep } from 'design-system';
import React from 'react';
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { CallBackProps } from 'react-joyride';
import { dismissNotice } from '../../../api/users';
import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
import DocLink from '../../../components/common/DocLink';
@@ -32,11 +33,13 @@ interface Props {
}

const PLACEMENT_RIGHT = 'right';
const SESSION_STORAGE_KEY = 'issueCleanCodeGuideStep';

const EXTRA_DELAY = 50;

export default function IssueGuide({ run }: Props) {
const { currentUser, updateDismissedNotices } = React.useContext(CurrentUserContext);
const [step, setStep] = useState(+(sessionStorage.getItem(SESSION_STORAGE_KEY) ?? 0));
const canRun = currentUser.isLoggedIn && !currentUser.dismissedNotices[NoticeType.ISSUE_GUIDE];

// IssueGuide can be called within context of a ScreenPositionHelper. When this happens,
@@ -57,21 +60,44 @@ export default function IssueGuide({ run }: Props) {
setStart(run);
}, SCREEN_POSITION_COMPUTE_DELAY + EXTRA_DELAY);
}
}, [canRun, run, start, setStart]);
}, [canRun, run, start]);

React.useEffect(() => {
if (start && canRun) {
sessionStorage.setItem(SESSION_STORAGE_KEY, step.toString());
}
}, [step, start, canRun]);

if (!start || !canRun) {
return null;
}

const onToggle = (props: { action: string }) => {
if (props.action === 'reset') {
dismissNotice(NoticeType.ISSUE_GUIDE)
.then(() => {
updateDismissedNotices(NoticeType.ISSUE_GUIDE, true);
})
.catch(() => {
/* noop */
});
const onToggle = (props: CallBackProps) => {
switch (props.action) {
case 'close':
case 'skip':
case 'reset':
sessionStorage.removeItem(SESSION_STORAGE_KEY);
dismissNotice(NoticeType.ISSUE_GUIDE)
.then(() => {
updateDismissedNotices(NoticeType.ISSUE_GUIDE, true);
})
.catch(() => {
/* noop */
});
break;
case 'next':
if (props.lifecycle === 'complete') {
setStep(step + 1);
}
break;
case 'prev':
if (props.lifecycle === 'complete') {
setStep(step - 1);
}
break;
default:
break;
}
};

@@ -144,6 +170,7 @@ export default function IssueGuide({ run }: Props) {
steps={steps}
run={run}
continuous
stepIndex={step}
skipLabel={translate('skip')}
backLabel={translate('go_back')}
closeLabel={translate('close')}

+ 64
- 61
server/sonar-web/src/main/js/apps/issues/components/IssueNewStatusAndTransitionGuide.tsx View File

@@ -18,10 +18,9 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { SpotlightTour, SpotlightTourStep } from 'design-system';
import React from 'react';
import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import { CallBackProps } from 'react-joyride';
import { createSharedStoreHook } from 'shared-store-hook';
import { useCurrentUser } from '../../../app/components/current-user/CurrentUserContext';
import DocumentationLink from '../../../components/common/DocumentationLink';
import { SCREEN_POSITION_COMPUTE_DELAY } from '../../../components/common/ScreenPositionHelper';
@@ -30,13 +29,6 @@ import { IssueTransition } from '../../../types/issues';
import { Issue } from '../../../types/types';
import { NoticeType } from '../../../types/users';

export const useAcceptGuideState = createSharedStoreHook<{
stepIndex: number;
guideIsRunning: boolean;
}>({
initialState: { stepIndex: 0, guideIsRunning: false },
});

interface Props {
run?: boolean;
togglePopup: (issue: string, popup: string, show?: boolean) => void;
@@ -45,15 +37,19 @@ interface Props {

const PLACEMENT_RIGHT = 'right';
const DOC_LINK = '/user-guide/issues/#statuses';
export const SESSION_STORAGE_TRANSITION_GUIDE_KEY = 'issueNewStatusAndTransitionGuideStep';
const EXTRA_DELAY = 100;
const GUIDE_WIDTH = 360;

export default function IssueNewStatusAndTransitionGuide(props: Readonly<Props>) {
const { run, issues } = props;
const { run, issues, togglePopup } = props;
const { currentUser, updateDismissedNotices } = useCurrentUser();
const { mutateAsync: dismissNotice } = useDismissNoticeMutation();
const intl = useIntl();
const [{ guideIsRunning, stepIndex }, { setPartialState, resetState }] = useAcceptGuideState();
const [step, setStep] = useState(
+(sessionStorage.getItem(SESSION_STORAGE_TRANSITION_GUIDE_KEY) ?? 0),
);
const [start, setStart] = React.useState(false);

const issueWithAcceptTransition = issues.find((issue) =>
issue.transitions.includes(IssueTransition.Accept),
@@ -71,67 +67,74 @@ export default function IssueNewStatusAndTransitionGuide(props: Readonly<Props>)
// to ensure proper positioning of the SpotlightTour in the context of ScreenPositionHelper,
// then start the tour.
React.useEffect(() => {
// If should start the tour and the tour is not started yet
if (!guideIsRunning && canRun) {
// Should start the tour if it is not started yet
if (!start && canRun) {
setTimeout(() => {
// Scroll to issue. This ensures proper rendering of the SpotlightTour.
document
.querySelector(`[data-guiding-id="issue-transition-${issueWithAcceptTransition.key}"]`)
?.scrollIntoView({ behavior: 'instant', block: 'center' });
// Start the tour
setPartialState({ guideIsRunning: true });
if (step !== 0) {
togglePopup(issueWithAcceptTransition.key, 'transition', true);
setTimeout(() => {
setStart(run);
}, 0);
} else {
setStart(run);
}
}, SCREEN_POSITION_COMPUTE_DELAY + EXTRA_DELAY);
}
}, [canRun, guideIsRunning, setPartialState, issueWithAcceptTransition]);
}, [canRun, run, step, start, togglePopup, issueWithAcceptTransition]);

// We reset the state all the time so that the tour can be restarted when user revisits the page.
// This has effect only when user is ignored guide.
React.useEffect(() => {
return resetState;
}, [resetState]);
if (start && canRun) {
sessionStorage.setItem(SESSION_STORAGE_TRANSITION_GUIDE_KEY, step.toString());
}
}, [step, start, canRun]);

if (!issueWithAcceptTransition || !guideIsRunning) {
if (!canRun || !start) {
return null;
}

const dismissTour = async () => {
if (userCompletedStatusGuide) {
return;
}
try {
await dismissNotice(NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE);
updateDismissedNotices(NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE, true);
} catch {
// ignore
}
};
const handleTourCallback = async ({ action, type, index }: CallBackProps) => {
if (type === 'step:after') {
// Open dropdown when going into step 1 and dismiss notice (we assume that the user has read the notice)
if (action === 'next' && index === 0) {
props.togglePopup(issueWithAcceptTransition.key, 'transition', true);
setTimeout(() => {
setPartialState({ stepIndex: index + 1 });
dismissTour();
}, 0);
return;
}
// Close dropdown when going into step 0 from step 1
if (action === 'prev' && index === 1) {
props.togglePopup(issueWithAcceptTransition.key, 'transition', false);
}
setPartialState({ stepIndex: action === 'prev' ? index - 1 : index + 1 });
return;
}
// When the tour is finished or skipped.
if (action === 'reset' || action === 'skip' || action === 'close') {
props.togglePopup(issueWithAcceptTransition.key, 'transition', false);
await dismissTour();
const onToggle = (props: CallBackProps) => {
const { action, lifecycle, index } = props;
switch (action) {
case 'close':
case 'skip':
case 'reset':
togglePopup(issueWithAcceptTransition.key, 'transition', false);
sessionStorage.removeItem(SESSION_STORAGE_TRANSITION_GUIDE_KEY);
dismissNotice(NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE)
.then(() => {
updateDismissedNotices(NoticeType.ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE, true);
})
.catch(() => {
/* noop */
});
break;
case 'next':
if (lifecycle === 'complete') {
if (index === 0) {
togglePopup(issueWithAcceptTransition.key, 'transition', true);
setTimeout(() => {
setStep(step + 1);
}, 0);
} else {
setStep(step + 1);
}
}
break;
case 'prev':
if (lifecycle === 'complete') {
if (index === 1) {
togglePopup(issueWithAcceptTransition.key, 'transition', false);
}
setStep(step - 1);
}
break;
default:
break;
}
};

@@ -173,10 +176,10 @@ export default function IssueNewStatusAndTransitionGuide(props: Readonly<Props>)
return (
<SpotlightTour
width={GUIDE_WIDTH}
callback={handleTourCallback}
callback={onToggle}
steps={steps}
stepIndex={stepIndex}
run={guideIsRunning}
stepIndex={step}
run={run}
continuous
skipLabel={intl.formatMessage({ id: 'skip' })}
backLabel={intl.formatMessage({ id: 'go_back' })}

server/sonar-web/src/main/js/apps/issues/__tests__/IssuesNewStatusAndTransitionGuide-it.tsx → server/sonar-web/src/main/js/apps/issues/components/__tests__/IssuesNewStatusAndTransitionGuide-test.tsx View File

@@ -20,17 +20,23 @@
import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import CurrentUserContextProvider from '../../../app/components/current-user/CurrentUserContextProvider';
import IssueTransitionComponent from '../../../components/issue/components/IssueTransition';
import { mockCurrentUser, mockIssue } from '../../../helpers/testMocks';
import { renderComponent } from '../../../helpers/testReactTestingUtils';
import { IssueTransition } from '../../../types/issues';
import { Issue } from '../../../types/types';
import { NoticeType } from '../../../types/users';
import IssueNewStatusAndTransitionGuide from '../components/IssueNewStatusAndTransitionGuide';
import { issuesHandler, ui } from '../test-utils';
import IssuesServiceMock from '../../../../api/mocks/IssuesServiceMock';
import UsersServiceMock from '../../../../api/mocks/UsersServiceMock';
import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
import IssueTransitionComponent from '../../../../components/issue/components/IssueTransition';
import { mockCurrentUser, mockIssue } from '../../../../helpers/testMocks';
import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { IssueTransition } from '../../../../types/issues';
import { Issue } from '../../../../types/types';
import { NoticeType } from '../../../../types/users';
import { ui } from '../../test-utils';
import IssueNewStatusAndTransitionGuide from '../IssueNewStatusAndTransitionGuide';

const usersHandler = new UsersServiceMock();
const issuesHandler = new IssuesServiceMock(usersHandler);

beforeEach(() => {
usersHandler.reset();
issuesHandler.reset();
});


+ 3
- 3
server/sonar-web/src/main/js/components/issue/components/IssueTransition.tsx View File

@@ -28,7 +28,7 @@ import {
} from 'design-system';
import * as React from 'react';
import { addIssueComment, setIssueTransition } from '../../../api/issues';
import { useAcceptGuideState } from '../../../apps/issues/components/IssueNewStatusAndTransitionGuide';
import { SESSION_STORAGE_TRANSITION_GUIDE_KEY } from '../../../apps/issues/components/IssueNewStatusAndTransitionGuide';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { Issue } from '../../../types/types';
import StatusHelper from '../../shared/StatusHelper';
@@ -45,8 +45,8 @@ interface Props {
export default function IssueTransition(props: Readonly<Props>) {
const { isOpen, issue, onChange, togglePopup } = props;

const [{ stepIndex: guideStepIndex, guideIsRunning }] = useAcceptGuideState();
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);

async function handleSetTransition(transition: string, comment?: string) {

Loading…
Cancel
Save