]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-23654 Design changes for Tour
authorViktor Vorona <viktor.vorona@sonarsource.com>
Mon, 25 Nov 2024 10:10:30 +0000 (11:10 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 26 Nov 2024 20:02:50 +0000 (20:02 +0000)
server/sonar-web/public/images/mode-tour/step4.png [new file with mode: 0644]
server/sonar-web/src/main/js/app/components/ModeTour.tsx
server/sonar-web/src/main/js/app/components/__tests__/ModeTour-test.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/public/images/mode-tour/step4.png b/server/sonar-web/public/images/mode-tour/step4.png
new file mode 100644 (file)
index 0000000..80a606a
Binary files /dev/null and b/server/sonar-web/public/images/mode-tour/step4.png differ
index 7c00716a6f21a125add1220aab343fff8fdd495a..8a9f745a1e74a25f4af1f472e75c60e556ef33d5 100644 (file)
@@ -34,6 +34,8 @@ import { NoticeType } from '../../types/users';
 import { useAppState } from './app-state/withAppStateContext';
 import { CurrentUserContext } from './current-user/CurrentUserContext';
 
+const MAX_STEPS = 4;
+
 export default function ModeTour() {
   const { currentUser, updateDismissedNotices } = useContext(CurrentUserContext);
   const appState = useAppState();
@@ -42,34 +44,8 @@ export default function ModeTour() {
   const [step, setStep] = useState(1);
   const [runManually, setRunManually] = useState(false);
 
-  const steps: SpotlightTourStep[] = [
-    {
-      target: '[data-guiding-id="mode-tour-1"]',
-      content: intl.formatMessage(
-        { id: 'mode_tour.step4.description' },
-        {
-          mode: intl.formatMessage({
-            id: `settings.mode.${isStandardMode ? 'standard' : 'mqr'}.name`,
-          }),
-          p1: (text) => <p>{text}</p>,
-          p: (text) => <p className="sw-mt-2">{text}</p>,
-          b: (text) => <b>{text}</b>,
-        },
-      ),
-      title: intl.formatMessage({ id: 'mode_tour.step4.title' }),
-      placement: 'bottom',
-    },
-    {
-      target: '[data-guiding-id="mode-tour-2"]',
-      title: intl.formatMessage({ id: 'mode_tour.step5.title' }),
-      content: null,
-      placement: 'left',
-      hideFooter: true,
-    },
-  ];
-
   const nextStep = () => {
-    if ((step === 3 && !isAdmin) || step === 4) {
+    if (step === MAX_STEPS) {
       document.dispatchEvent(new CustomEvent(CustomEvents.OpenHelpMenu));
       setTimeout(() => setStep(5));
     } else {
@@ -124,21 +100,51 @@ export default function ModeTour() {
     return null;
   }
 
-  const maxSteps = isAdmin ? 4 : 3;
+  const steps: SpotlightTourStep[] = [
+    ...(isAdmin
+      ? [
+          {
+            target: '[data-guiding-id="mode-tour-1"]',
+            content: intl.formatMessage(
+              { id: 'mode_tour.step4.description' },
+              {
+                mode: intl.formatMessage({
+                  id: `settings.mode.${isStandardMode ? 'standard' : 'mqr'}.name`,
+                }),
+                p1: (text) => <p>{text}</p>,
+                p: (text) => <p className="sw-mt-2">{text}</p>,
+                b: (text) => <b>{text}</b>,
+              },
+            ),
+            title: intl.formatMessage({ id: 'mode_tour.step4.title' }),
+            placement: 'bottom' as const,
+          },
+        ]
+      : []),
+    {
+      target: '[data-guiding-id="mode-tour-2"]',
+      title: intl.formatMessage({ id: 'mode_tour.step5.title' }),
+      content: null,
+      placement: 'left',
+      hideFooter: true,
+    },
+  ];
+
+  const maxModalSteps = isAdmin ? MAX_STEPS - 1 : MAX_STEPS;
 
   return (
     <>
       <Modal
         size={ModalSize.Wide}
-        isOpen={step <= 3}
+        isOpen={step <= maxModalSteps}
         onOpenChange={(isOpen) => isOpen === false && dismissTour()}
         title={
-          step < 4 &&
+          step <= maxModalSteps &&
           intl.formatMessage({ id: `mode_tour.step${step}.title` }, { version: appState.version })
         }
         content={
           <>
-            {step < 4 && (
+            {step <= maxModalSteps && (
               <>
                 <Image
                   alt={intl.formatMessage({ id: `mode_tour.step${step}.img_alt` })}
@@ -148,6 +154,9 @@ export default function ModeTour() {
                 {intl.formatMessage(
                   { id: `mode_tour.step${step}.description` },
                   {
+                    mode: intl.formatMessage({
+                      id: `settings.mode.${isStandardMode ? 'standard' : 'mqr'}.name`,
+                    }),
                     p1: (text) => <p>{text}</p>,
                     p: (text) => <p className="sw-mt-4">{text}</p>,
                     b: (text) => <b>{text}</b>,
@@ -155,7 +164,7 @@ export default function ModeTour() {
                 )}
                 <div className="sw-mt-6">
                   <b>
-                    {intl.formatMessage({ id: 'guiding.step_x_of_y' }, { 0: step, 1: maxSteps })}
+                    {intl.formatMessage({ id: 'guiding.step_x_of_y' }, { 0: step, 1: MAX_STEPS })}
                   </b>
                 </div>
               </>
@@ -183,13 +192,14 @@ export default function ModeTour() {
       <SpotlightTour
         callback={onToggle}
         steps={steps}
-        run={step > 3}
+        run={step > maxModalSteps}
         continuous
+        disableOverlay={step === 5}
         showProgress={step !== 5}
-        stepIndex={step - 4}
+        stepIndex={step - maxModalSteps - 1}
         nextLabel={intl.formatMessage({ id: 'next' })}
         stepXofYLabel={(x: number) =>
-          intl.formatMessage({ id: 'guiding.step_x_of_y' }, { 0: x + 3, 1: maxSteps })
+          intl.formatMessage({ id: 'guiding.step_x_of_y' }, { 0: x + maxModalSteps, 1: MAX_STEPS })
         }
       />
     </>
index 5c09dd0bb4911788d64434cbc42ce66adf4315a7..88e5f674860f26f1793b5b06e217ab29443414c6 100644 (file)
@@ -35,6 +35,7 @@ const ui = {
   step1Dialog: byRole('dialog', { name: /mode_tour.step1.title/ }),
   step2Dialog: byRole('dialog', { name: /mode_tour.step2.title/ }),
   step3Dialog: byRole('dialog', { name: /mode_tour.step3.title/ }),
+  step4Dialog: byRole('dialog', { name: /mode_tour.step4.title/ }),
   next: byRole('button', { name: 'next' }),
   later: byRole('button', { name: 'later' }),
   skip: byRole('button', { name: 'skip' }),
@@ -109,7 +110,7 @@ it('renders the tour for gateadmins', async () => {
   expect(ui.later.get()).toBeInTheDocument();
   expect(ui.next.query()).not.toBeInTheDocument();
   expect(ui.letsgo.get()).toBeInTheDocument();
-  expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.3');
+  expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.4');
   await user.click(ui.letsgo.get());
 
   expect(ui.step2Dialog.get()).toBeInTheDocument();
@@ -117,13 +118,19 @@ it('renders the tour for gateadmins', async () => {
   expect(ui.later.query()).not.toBeInTheDocument();
   expect(ui.next.get()).toBeInTheDocument();
   expect(ui.letsgo.query()).not.toBeInTheDocument();
-  expect(ui.step2Dialog.get()).toHaveTextContent('guiding.step_x_of_y.2.3');
+  expect(ui.step2Dialog.get()).toHaveTextContent('guiding.step_x_of_y.2.4');
   await user.click(ui.next.get());
 
   expect(ui.step3Dialog.get()).toBeInTheDocument();
   expect(ui.step2Dialog.query()).not.toBeInTheDocument();
   expect(ui.next.get()).toBeInTheDocument();
-  expect(ui.step3Dialog.get()).toHaveTextContent('guiding.step_x_of_y.3.3');
+  expect(ui.step3Dialog.get()).toHaveTextContent('guiding.step_x_of_y.3.4');
+  await user.click(ui.next.get());
+
+  expect(ui.step4Dialog.get()).toBeInTheDocument();
+  expect(ui.step3Dialog.query()).not.toBeInTheDocument();
+  expect(ui.next.get()).toBeInTheDocument();
+  expect(ui.step4Dialog.get()).toHaveTextContent('guiding.step_x_of_y.4.4');
   await user.click(ui.next.get());
 
   expect(ui.dialog.query()).not.toBeInTheDocument();
@@ -141,7 +148,7 @@ it('renders the tour for gateadmins', async () => {
   await user.click(ui.help.get());
   await user.click(ui.tourTrigger.get());
   expect(ui.step1Dialog.get()).toBeInTheDocument();
-  expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.3');
+  expect(ui.step1Dialog.get()).toHaveTextContent('guiding.step_x_of_y.1.4');
 });
 
 it('should not render the tour for regular users', async () => {
index de02b9536b6765ea950644f498fc7ea771d13488..715aee75ec3493f507b314a2d93003deb5ac2e9a 100644 (file)
@@ -5921,6 +5921,7 @@ mode_tour.step3.title=Multi-Quality Rule Mode
 mode_tour.step3.img_alt=Visual presentation of the new Software Quality badges: Security, Reliability and Maintainability and their connection to the corresponding Standard Experience badges
 mode_tour.step3.description=<p1>The new Multi-Quality Rule Mode aims to more accurately represent an issue's impact on all software qualities. </p1><p>It does this by assigning a separate severity to a rule for each software quality (Security, Reliability, and Maintainability), which replaces the types (Vulnerabilities, Bugs, and Code Smells). You can customize the severity level with appropriate permissions.</p><p>This approach focuses on ensuring the impact on all software qualities is clear, not just the one most severely impacted.</p>
 mode_tour.step4.title=Switch modes
-mode_tour.step4.description=<p1>You are currently in <b>{mode}</b>.</p1><p>To change it, go to Administration > Configuration > General Settings > Mode.</p><p>Your instance will start in the mode that most closely resembles the software version you are upgrading from.</p>
+mode_tour.step4.img_alt=Visual presentation of switch between Standard Experience and MQR mode in the Administation settings
+mode_tour.step4.description=<p1>You are currently in <b>{mode}</b>.</p1><p>It can be changed with sufficient permissions by going to Administration > Configuration > General Settings > Mode.</p><p>Your instance will start in the mode that most closely resembles the software version you are upgrading from.</p>
 mode_tour.step5.title=You can replay the tour from the help section