]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22148 Spotlight tour for cayc in branch overview
authorMathieu Suen <mathieu.suen@sonarsource.com>
Mon, 6 May 2024 15:41:13 +0000 (17:41 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 10 May 2024 20:02:47 +0000 (20:02 +0000)
24 files changed:
server/sonar-web/design-system/config/jest/CSSStub.js [new file with mode: 0644]
server/sonar-web/design-system/jest.config.js
server/sonar-web/design-system/package.json
server/sonar-web/design-system/src/components/SpotlightTour.tsx
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/package.json
server/sonar-web/src/main/js/app/components/__tests__/GlobalFooter-test.tsx
server/sonar-web/src/main/js/app/components/nav/component/branch-like/BranchLikeNavigation.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchMetaTopBar.tsx
server/sonar-web/src/main/js/apps/overview/branches/BranchOverviewRenderer.tsx
server/sonar-web/src/main/js/apps/overview/branches/CaycPromotionGuide.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/QualityGateStatusHeader.tsx
server/sonar-web/src/main/js/apps/overview/branches/ReplayTour.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/TabsPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/BranchOverview-it.tsx
server/sonar-web/src/main/js/apps/overview/components/LastAnalysisLabel.tsx
server/sonar-web/src/main/js/apps/system/components/__tests__/SystemApp-it.tsx
server/sonar-web/src/main/js/components/tutorials/test-utils.ts
server/sonar-web/src/main/js/types/users.ts
server/sonar-web/yarn.lock
server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/design-system/config/jest/CSSStub.js b/server/sonar-web/design-system/config/jest/CSSStub.js
new file mode 100644 (file)
index 0000000..e787e07
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+module.exports = {};
+
index 7201800d75ca68b97be362ddf00a9f27f42f972f..1e30ea835862fd21d8d1b93231df5100b409d138 100644 (file)
@@ -43,7 +43,7 @@ module.exports = {
   moduleNameMapper: {
     '^.+\\.(md|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
       '<rootDir>/config/jest/FileStub.js',
-    // '^.+\\.css$': '<rootDir>/config/jest/CSSStub.js',
+    '^.+\\.css$': '<rootDir>/config/jest/CSSStub.js',
   },
   setupFiles: [
     '<rootDir>/config/jest/SetupTestEnvironment.js',
index e81329f12e05854a7bc77fe26bb728fd8038bcca..fcb250ff299ccaf12845160abf1f68ecf296748f 100644 (file)
@@ -23,6 +23,7 @@
     "@babel/preset-typescript": "7.23.3",
     "@emotion/babel-plugin": "11.11.0",
     "@emotion/babel-plugin-jsx-pragmatic": "0.2.1",
+    "@sonarsource/echoes-react": "0.2.2",
     "@testing-library/dom": "9.3.4",
     "@testing-library/jest-dom": "6.4.2",
     "@testing-library/react": "14.2.1",
@@ -76,7 +77,7 @@
     "react-helmet-async": "2.0.4",
     "react-highlight-words": "0.20.0",
     "react-intl": "6.6.2",
-    "react-joyride": "2.7.2",
+    "react-joyride": "2.8.1",
     "react-modal": "3.16.1",
     "react-router-dom": "6.22.0",
     "react-select": "5.7.7",
index 62bc70df15bc3d8e317deeada0fdcea341481185..c5be29f5a4871330971b232a6944d420f28cb4f3 100644 (file)
@@ -19,6 +19,7 @@
  */
 import { keyframes } from '@emotion/react';
 import styled from '@emotion/styled';
+import { LinkStandalone } from '@sonarsource/echoes-react';
 import React from 'react';
 import { useIntl } from 'react-intl';
 import ReactJoyride, {
@@ -26,6 +27,7 @@ import ReactJoyride, {
   Step as JoyrideStep,
   TooltipRenderProps,
 } from 'react-joyride';
+import { LinkProps } from 'react-router-dom';
 import tw from 'twin.macro';
 import { GLOBAL_POPUP_Z_INDEX, PopupZLevel, themeColor } from '../helpers';
 import { findAnchor } from '../helpers/dom';
@@ -37,6 +39,8 @@ import { PopupWrapper } from './popups';
 type Placement = 'left' | 'right' | 'top' | 'bottom' | 'center';
 
 export interface SpotlightTourProps extends Omit<JoyrideProps, 'steps'> {
+  actionLabel?: string;
+  actionPath?: LinkProps['to'];
   backLabel?: string;
   closeLabel?: string;
   nextLabel?: string;
@@ -60,6 +64,8 @@ const DEFAULT_WIDTH = 315;
 const defultRect = new DOMRect(0, 0, 0, 0);
 
 function TooltipComponent({
+  actionLabel,
+  actionPath,
   continuous,
   index,
   step,
@@ -73,6 +79,8 @@ function TooltipComponent({
   tooltipProps,
   width = DEFAULT_WIDTH,
 }: TooltipRenderProps & {
+  actionLabel?: string;
+  actionPath?: LinkProps['to'];
   step: SpotlightTourStep;
   stepXofYLabel: SpotlightTourProps['stepXofYLabel'];
   width?: number;
@@ -154,6 +162,13 @@ function TooltipComponent({
         </WrapperButton>
       </div>
       <div>{step.content}</div>
+
+      {actionLabel && actionPath && (
+        <div className="sw-pt-4">
+          <LinkStandalone to={actionPath}>{actionLabel}</LinkStandalone>
+        </div>
+      )}
+
       <div className="sw-flex sw-justify-between sw-items-center sw-mt-4">
         {(stepXofYLabel || size > 1) && (
           <strong>
@@ -183,6 +198,8 @@ function TooltipComponent({
 
 export function SpotlightTour(props: SpotlightTourProps) {
   const {
+    actionLabel,
+    actionPath,
     steps,
     skipLabel,
     backLabel,
@@ -227,7 +244,15 @@ export function SpotlightTour(props: SpotlightTourProps) {
       }))}
       tooltipComponent={(
         tooltipProps: React.PropsWithChildren<TooltipRenderProps & { step: SpotlightTourStep }>,
-      ) => <TooltipComponent stepXofYLabel={stepXofYLabel} width={width} {...tooltipProps} />}
+      ) => (
+        <TooltipComponent
+          actionLabel={actionLabel}
+          actionPath={actionPath}
+          stepXofYLabel={stepXofYLabel}
+          width={width}
+          {...tooltipProps}
+        />
+      )}
       {...otherProps}
     />
   );
index f5f33fcac32b012c87749cdd8f81a3f4f48b300b..4c218db9f3d08aa6d6ee7ced1fa0b9ee24e96a2e 100644 (file)
@@ -525,6 +525,7 @@ export const lightTheme = {
     projectCardInfo: COLORS.blueGrey[35],
 
     // overview
+    backgroundPromotedSection: secondary.light,
     overviewCardDefaultIcon: secondary.light,
     iconOverviewIssue: COLORS.blueGrey[400],
     overviewCardWarningIcon: COLORS.yellow[50],
index 3261c94e24f7e1a805cc9a95b5fb7a4a1915952b..b95f40629a6cb93d709fcd8f52e7ed0594af67cb 100644 (file)
@@ -13,7 +13,7 @@
     "@primer/octicons-react": "19.8.0",
     "@react-spring/rafz": "9.7.3",
     "@react-spring/web": "9.7.3",
-    "@sonarsource/echoes-react": "0.2.1",
+    "@sonarsource/echoes-react": "0.2.2",
     "@tanstack/react-query": "5.18.1",
     "axios": "1.6.7",
     "classnames": "2.5.1",
@@ -38,7 +38,7 @@
     "react-helmet-async": "2.0.4",
     "react-highlight-words": "0.20.0",
     "react-intl": "6.6.2",
-    "react-joyride": "2.7.2",
+    "react-joyride": "2.8.1",
     "react-modal": "3.16.1",
     "react-router-dom": "6.22.0",
     "react-select": "5.7.7",
index f6dd26a1e964ebc9b4cb824b3e3cb36500e2e08c..d8ace2117cfa4911ae764375b7d7916fbf4d90ea 100644 (file)
@@ -132,9 +132,9 @@ const ui = {
   pluginsLink: byRole('link', { name: 'opens_in_new_window footer.plugins' }),
   apiLink: byRole('link', { name: 'footer.web_api' }),
   ltaDocumentationLinkActive: byRole('link', {
-    name: `footer.version.status.active open_in_new_window`,
+    name: `footer.version.status.active open_in_new_tab`,
   }),
   ltaDocumentationLinkInactive: byRole('link', {
-    name: `footer.version.status.inactive open_in_new_window`,
+    name: `footer.version.status.inactive open_in_new_tab`,
   }),
 };
index 5fa6f57702371cc5bb0fe3f1a6bbb5c8e3b60162..4e92f2c61071ca5dc9eaf22045eeb9df73a60f7e 100644 (file)
@@ -69,7 +69,10 @@ export function BranchLikeNavigation(props: BranchLikeNavigationProps) {
   return (
     <>
       <SlashSeparator className=" sw-mx-2" />
-      <div className="sw-flex sw-items-center it__branch-like-navigation-toggler-container">
+      <div
+        className="sw-flex sw-items-center it__branch-like-navigation-toggler-container"
+        data-spotlight-id="cayc-promotion-4"
+      >
         <Popup
           allowResizing
           overlay={
index a3ec7989032f8b1b97a0c933a73524549a993af6..61ec2cbf3bc215a9d9967685fe4f8a22e1a70ca2 100644 (file)
@@ -17,7 +17,8 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
-import { SeparatorCircleIcon } from 'design-system';
+import { IconSlideshow } from '@sonarsource/echoes-react';
+import { ButtonSecondary, SeparatorCircleIcon } from 'design-system';
 import React from 'react';
 import { useIntl } from 'react-intl';
 import { formatMeasure } from '~sonar-aligned/helpers/measures';
@@ -25,6 +26,8 @@ import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
 import { getCurrentPage } from '../../../app/components/nav/component/utils';
 import ComponentReportActions from '../../../components/controls/ComponentReportActions';
 import HomePageSelect from '../../../components/controls/HomePageSelect';
+import Tooltip from '../../../components/controls/Tooltip';
+import { translate } from '../../../helpers/l10n';
 import { findMeasure } from '../../../helpers/measures';
 import { Branch } from '../../../types/branch-like';
 import { Component, MeasureEnhanced } from '../../../types/types';
@@ -34,9 +37,17 @@ interface Props {
   component: Component;
   branch: Branch;
   measures: MeasureEnhanced[];
+  showTakeTheTourButton: boolean;
+  startTour?: () => void;
 }
 
-export default function BranchMetaTopBar({ branch, measures, component }: Readonly<Props>) {
+export default function BranchMetaTopBar({
+  branch,
+  measures,
+  component,
+  showTakeTheTourButton,
+  startTour,
+}: Readonly<Props>) {
   const intl = useIntl();
 
   const currentPage = getCurrentPage(component, branch) as HomePage;
@@ -66,6 +77,18 @@ export default function BranchMetaTopBar({ branch, measures, component }: Readon
       )}
       <HomePageSelect currentPage={currentPage} type="button" />
       <ComponentReportActions component={component} branch={branch} />
+      {showTakeTheTourButton && (
+        <Tooltip overlay={translate('overview.promoted_section.button_tooltip')}>
+          <ButtonSecondary
+            className="sw-pl-4 sw-shrink-0"
+            data-spotlight-id="take-tour-1"
+            onClick={startTour}
+          >
+            <IconSlideshow className="sw-mr-1" />
+            {translate('overview.promoted_section.button_primary')}
+          </ButtonSecondary>
+        </Tooltip>
+      )}
     </div>
   );
 
index e1194406c2e68a759206e21f1917480b60c45c7a..6021e0ec732edb52634cf008dff27d5470145a31 100644 (file)
@@ -25,30 +25,38 @@ import {
   PageContentFontWrapper,
 } from 'design-system';
 import * as React from 'react';
+import { useState } from 'react';
 import A11ySkipTarget from '~sonar-aligned/components/a11y/A11ySkipTarget';
 import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
 import { isPortfolioLike } from '~sonar-aligned/helpers/component';
 import { ComponentQualifier } from '~sonar-aligned/types/component';
+import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
 import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage';
 import { parseDate } from '../../../helpers/dates';
+import { translate } from '../../../helpers/l10n';
 import { areCCTMeasuresComputed, isDiffMetric } from '../../../helpers/measures';
 import { CodeScope } from '../../../helpers/urls';
+import { useDismissNoticeMutation } from '../../../queries/users';
 import { ApplicationPeriod } from '../../../types/application';
 import { Branch } from '../../../types/branch-like';
 import { Analysis, GraphType, MeasureHistory } from '../../../types/project-activity';
 import { QualityGateStatus } from '../../../types/quality-gates';
 import { Component, MeasureEnhanced, Metric, Period, QualityGate } from '../../../types/types';
+import { NoticeType } from '../../../types/users';
 import { AnalysisStatus } from '../components/AnalysisStatus';
 import LastAnalysisLabel from '../components/LastAnalysisLabel';
 import ActivityPanel from './ActivityPanel';
 import BranchMetaTopBar from './BranchMetaTopBar';
+import CaycPromotionGuide from './CaycPromotionGuide';
 import FirstAnalysisNextStepsNotif from './FirstAnalysisNextStepsNotif';
 import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
 import NewCodeMeasuresPanel from './NewCodeMeasuresPanel';
 import NoCodeWarning from './NoCodeWarning';
 import OverallCodeMeasuresPanel from './OverallCodeMeasuresPanel';
+import PromotedSection from './PromotedSection';
 import QualityGatePanel from './QualityGatePanel';
 import { QualityGateStatusTitle } from './QualityGateStatusTitle';
+import ReplayTourGuide from './ReplayTour';
 import SonarLintPromotion from './SonarLintPromotion';
 import { TabsPanel } from './TabsPanel';
 
@@ -96,6 +104,18 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
   const { query } = useLocation();
   const router = useRouter();
 
+  const { currentUser } = React.useContext(CurrentUserContext);
+
+  const { mutateAsync: dismissNotice } = useDismissNoticeMutation();
+
+  const [startTour, setStartTour] = useState(false);
+  const [tourCompleted, setTourCompleted] = useState(false);
+  const [showReplay, setShowReplay] = useState(false);
+  const [dismissedTour, setDismissedTour] = useState(
+    currentUser.isLoggedIn &&
+      !!currentUser.dismissedNotices[NoticeType.ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE],
+  );
+
   const tab = query.codeScope === CodeScope.Overall ? CodeScope.Overall : CodeScope.New;
   const leakPeriod = component.qualifier === ComponentQualifier.Application ? appLeak : period;
   const isNewCodeTab = tab === CodeScope.New;
@@ -126,6 +146,33 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
     />
   );
 
+  const dismissPromotedSection = () => {
+    dismissNotice(NoticeType.ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE);
+
+    setDismissedTour(true);
+    setShowReplay(true);
+  };
+
+  const closeTour = (action: string) => {
+    setStartTour(false);
+    if (action === 'skip' && !dismissedTour) {
+      dismissPromotedSection();
+    }
+
+    if (action === 'close' && !dismissedTour) {
+      dismissPromotedSection();
+      setTourCompleted(true);
+    }
+  };
+
+  const startTourGuide = () => {
+    if (!isNewCodeTab) {
+      selectTab(CodeScope.New);
+    }
+    setShowReplay(false);
+    setStartTour(true);
+  };
+
   return (
     <>
       <FirstAnalysisNextStepsNotif
@@ -135,6 +182,14 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
       />
       <LargeCenteredLayout>
         <PageContentFontWrapper>
+          <CaycPromotionGuide closeTour={closeTour} run={startTour} />
+          {showReplay && (
+            <ReplayTourGuide
+              closeTour={() => setShowReplay(false)}
+              run={showReplay}
+              tourCompleted={tourCompleted}
+            />
+          )}
           <div className="overview sw-my-6 sw-body-sm">
             <A11ySkipTarget anchor="overview_main" />
 
@@ -144,7 +199,27 @@ export default function BranchOverviewRenderer(props: BranchOverviewRendererProp
               <div>
                 {branch && (
                   <>
-                    <BranchMetaTopBar branch={branch} component={component} measures={measures} />
+                    {currentUser.isLoggedIn && (
+                      <PromotedSection
+                        content={translate('overview.promoted_section.content')}
+                        dismissed={dismissedTour ?? false}
+                        onDismiss={dismissPromotedSection}
+                        onPrimaryButtonClick={startTourGuide}
+                        primaryButtonLabel={translate('overview.promoted_section.button_primary')}
+                        secondaryButtonLabel={translate(
+                          'overview.promoted_section.button_secondary',
+                        )}
+                        title={translate('overview.promoted_section.title')}
+                      />
+                    )}
+
+                    <BranchMetaTopBar
+                      branch={branch}
+                      component={component}
+                      measures={measures}
+                      showTakeTheTourButton={dismissedTour && currentUser.isLoggedIn}
+                      startTour={startTourGuide}
+                    />
                     <BasicSeparator />
                   </>
                 )}
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/CaycPromotionGuide.tsx b/server/sonar-web/src/main/js/apps/overview/branches/CaycPromotionGuide.tsx
new file mode 100644 (file)
index 0000000..c0a713d
--- /dev/null
@@ -0,0 +1,134 @@
+/*
+ * 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 { SpotlightTour, SpotlightTourStep } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+
+interface Props {
+  closeTour: (action: string) => void;
+  run: boolean;
+}
+
+function CaycPromotionGuide(props: Readonly<Props>) {
+  const { run } = props;
+  const onToggle = ({ action, type }: { action: string; type: string }) => {
+    if (type === 'tour:end' && (action === 'close' || action === 'skip')) {
+      props.closeTour(action);
+    }
+  };
+
+  const constructContent = (first: string) => <p className="sw-mt-2">{translate(first)}</p>;
+
+  const constructContentLastStep = (first: string, second: string, third: string) => (
+    <>
+      <p className="sw-mt-2">
+        <FormattedMessage
+          defaultMessage={translate(first)}
+          id={first}
+          values={{
+            value: <strong>{translate('ide')}</strong>,
+          }}
+        />
+      </p>
+      <p className="sw-mt-2">
+        <FormattedMessage
+          defaultMessage={translate(second)}
+          id={second}
+          values={{
+            value: <strong>{translate('pull_request.small')}</strong>,
+          }}
+        />
+      </p>
+      <p className="sw-mt-2">
+        <FormattedMessage
+          defaultMessage={translate(third)}
+          id={third}
+          values={{
+            value: <strong>{translate('branch.small')}</strong>,
+          }}
+        />
+      </p>
+    </>
+  );
+
+  const steps: SpotlightTourStep[] = [
+    {
+      disableScrolling: false,
+      disableOverlayClose: true,
+      target: '[data-spotlight-id="cayc-promotion-1"]',
+      content: constructContent('guiding.cayc_promotion.1.content.1'),
+      title: translate('guiding.cayc_promotion.1.title'),
+      placement: 'left',
+    },
+    {
+      disableScrolling: true,
+      disableOverlayClose: true,
+      target: '[data-spotlight-id="cayc-promotion-2"]',
+      content: constructContent('guiding.cayc_promotion.2.content.1'),
+      title: translate('guiding.cayc_promotion.2.title'),
+      placement: 'left',
+    },
+    {
+      disableScrolling: true,
+      disableOverlayClose: true,
+      target: '[data-spotlight-id="cayc-promotion-3"]',
+      content: constructContent('guiding.cayc_promotion.3.content.1'),
+      title: translate('guiding.cayc_promotion.3.title'),
+      placement: 'right',
+    },
+    {
+      disableScrolling: true,
+      disableOverlayClose: true,
+      target: '[data-spotlight-id="cayc-promotion-4"]',
+      content: constructContentLastStep(
+        'guiding.cayc_promotion.4.content.1',
+        'guiding.cayc_promotion.4.content.2',
+        'guiding.cayc_promotion.4.content.3',
+      ),
+      title: translate('guiding.cayc_promotion.4.title'),
+      placement: 'right',
+      spotlightPadding: 0,
+    },
+  ];
+
+  return (
+    <SpotlightTour
+      disableOverlay={false}
+      disableScrolling
+      backLabel={translate('previous')}
+      callback={onToggle}
+      closeLabel={translate('complete')}
+      continuous
+      nextLabel={translate('next')}
+      run={run}
+      skipLabel={translate('skip')}
+      stepXofYLabel={(x: number, y: number) => translateWithParameters('guiding.step_x_of_y', x, y)}
+      steps={steps}
+      styles={{
+        options: {
+          zIndex: 1000,
+        },
+      }}
+    />
+  );
+}
+
+export default CaycPromotionGuide;
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx b/server/sonar-web/src/main/js/apps/overview/branches/PromotedSection.tsx
new file mode 100644 (file)
index 0000000..6e8d438
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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 styled from '@emotion/styled';
+import { IconX } from '@sonarsource/echoes-react';
+import {
+  ButtonPrimary,
+  ButtonSecondary,
+  InteractiveIcon,
+  themeBorder,
+  themeColor,
+} from 'design-system';
+import React, { useState } from 'react';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  content: string;
+  dismissed: boolean;
+  onDismiss: () => void;
+  onPrimaryButtonClick: () => void;
+  primaryButtonLabel: string;
+  secondaryButtonLabel: string;
+  title: string;
+}
+
+export default function PromotedSection({
+  content,
+  primaryButtonLabel,
+  secondaryButtonLabel,
+  title,
+  dismissed,
+  onDismiss,
+  onPrimaryButtonClick,
+}: Readonly<Props>) {
+  const [display, setDisplay] = useState(!dismissed);
+
+  const handlePrimaryButtonClick = () => {
+    setDisplay(false);
+    onPrimaryButtonClick();
+  };
+
+  const handleDismiss = () => {
+    setDisplay(false);
+    onDismiss();
+  };
+
+  if (!display) {
+    return null;
+  }
+
+  return (
+    <StyledWrapper className="sw-p-4 sw-pl-6 sw-my-6 sw-rounded-2 sw-w-8/12">
+      <div className="sw-flex sw-justify-between sw-mb-2">
+        <StyledTitle className="sw-body-md-highlight">{title}</StyledTitle>
+        <InteractiveIcon
+          Icon={IconX}
+          aria-label={translate('dismiss')}
+          onClick={handleDismiss}
+          size="small"
+        />
+      </div>
+      <p className="sw-body-sm sw-mb-4">{content}</p>
+      <div>
+        <ButtonPrimary className="sw-mr-2" onClick={handlePrimaryButtonClick}>
+          {primaryButtonLabel}
+        </ButtonPrimary>
+        <ButtonSecondary onClick={handleDismiss}>{secondaryButtonLabel}</ButtonSecondary>
+      </div>
+    </StyledWrapper>
+  );
+}
+
+const StyledWrapper = styled.div`
+  background-color: ${themeColor('backgroundPromotedSection')};
+  border: ${themeBorder('default')};
+`;
+
+const StyledTitle = styled.p`
+  color: ${themeColor('primary')};
+`;
index 2188c540a8e7545c13d6acf7ebbf69122a77c93d..c5a82ca02df25b98182a0dc8fdfa9a8d09011334 100644 (file)
@@ -33,7 +33,7 @@ export default function QualityGateStatusHeader(props: Props) {
   const intl = useIntl();
 
   return (
-    <div className="sw-flex sw-items-center sw-mb-4">
+    <div className="sw-flex sw-items-center sw-mb-4" data-spotlight-id="cayc-promotion-3">
       <QualityGateIndicator status={status} className="sw-mr-2" size="xl" />
       <div className="sw-flex sw-flex-col">
         <span className="sw-heading-lg">{translate('metric.level', status)}</span>
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/ReplayTour.tsx b/server/sonar-web/src/main/js/apps/overview/branches/ReplayTour.tsx
new file mode 100644 (file)
index 0000000..5ea043a
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * 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 { SpotlightTour, SpotlightTourStep } from 'design-system';
+import React from 'react';
+import { useDocUrl } from '../../../helpers/docs';
+import { translate } from '../../../helpers/l10n';
+
+interface Props {
+  closeTour: () => void;
+  run: boolean;
+  tourCompleted: boolean;
+}
+
+export default function ReplayTourGuide({ run, closeTour, tourCompleted }: Readonly<Props>) {
+  const onToggle = ({ action }: { action: string }) => {
+    if (action === 'skip' || action === 'close') {
+      closeTour();
+    }
+  };
+
+  const constructContent = (first: string) => <p className="sw-mt-2">{translate(first)}</p>;
+
+  const docUrl = useDocUrl('improving/clean-as-you-code/');
+
+  const steps: SpotlightTourStep[] = [
+    {
+      disableOverlayClose: true,
+      target: '[data-spotlight-id="take-tour-1"]',
+      content: constructContent('guiding.replay_tour_button.1.content'),
+      title: tourCompleted
+        ? translate('guiding.replay_tour_button.tour_completed.1.title')
+        : translate('guiding.replay_tour_button.1.title'),
+      placement: 'left',
+    },
+  ];
+
+  return (
+    <div>
+      <SpotlightTour
+        actionLabel={tourCompleted ? translate('learn_more.clean_code') : undefined}
+        actionPath={tourCompleted ? docUrl : undefined}
+        backLabel={translate('go_back')}
+        callback={onToggle}
+        closeLabel={translate('got_it')}
+        continuous
+        disableOverlay
+        nextLabel={translate('next')}
+        run={run}
+        skipLabel={translate('skip')}
+        stepXofYLabel={() => ''}
+        steps={steps}
+        width={350}
+      />
+    </div>
+  );
+}
index 0e8a0c68b55648fd4eb5148920c30ce69e8355ce..c699725ca81e89fcc5503ec380eecaa15be5dad5 100644 (file)
@@ -119,7 +119,11 @@ export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) {
   ];
 
   return (
-    <div className="sw-mt-3" data-testid="overview__measures-panel">
+    <div
+      className="sw-mt-3"
+      data-testid="overview__measures-panel"
+      data-spotlight-id="cayc-promotion-1"
+    >
       {loading ? (
         <div>
           <Spinner isLoading={loading} />
index 8c996f697c9258624dc8072d0b5588ea5b5e9559..7783cd67beadfcfb1bf2dd82c1e711111807c815 100644 (file)
@@ -29,10 +29,12 @@ import { MeasuresServiceMock } from '../../../../api/mocks/MeasuresServiceMock';
 import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
 import { QualityGatesServiceMock } from '../../../../api/mocks/QualityGatesServiceMock';
 import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock';
+import UsersServiceMock from '../../../../api/mocks/UsersServiceMock';
 import { PARENT_COMPONENT_KEY } from '../../../../api/mocks/data/ids';
 import { getProjectActivity } from '../../../../api/projectActivity';
 import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
+import { Header } from '../../../../app/components/nav/component/Header';
 import { parseDate } from '../../../../helpers/dates';
 import { mockMainBranch } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
@@ -51,6 +53,7 @@ let branchesHandler: BranchesServiceMock;
 let measuresHandler: MeasuresServiceMock;
 let applicationHandler: ApplicationServiceMock;
 let projectActivityHandler: ProjectActivityServiceMock;
+let usersHandler: UsersServiceMock;
 let timeMarchineHandler: TimeMachineServiceMock;
 let qualityGatesHandler: QualityGatesServiceMock;
 
@@ -59,6 +62,7 @@ beforeAll(() => {
   measuresHandler = new MeasuresServiceMock();
   applicationHandler = new ApplicationServiceMock();
   projectActivityHandler = new ProjectActivityServiceMock();
+  usersHandler = new UsersServiceMock();
   projectActivityHandler.setAnalysesList([
     mockAnalysis({ key: 'a1', detectedCI: 'Cirrus CI' }),
     mockAnalysis({ key: 'a2' }),
@@ -116,6 +120,7 @@ afterEach(() => {
   measuresHandler.reset();
   applicationHandler.reset();
   projectActivityHandler.reset();
+  usersHandler.reset();
   timeMarchineHandler.reset();
   qualityGatesHandler.reset();
   almHandler.reset();
@@ -453,6 +458,61 @@ describe('project overview', () => {
       expect(await screen.findByText('overview.missing_project_dataTRK')).toBeInTheDocument();
     },
   );
+
+  it('should dismiss CaYC promoted section', async () => {
+    qualityGatesHandler.setQualityGateProjectStatus(
+      mockQualityGateProjectStatus({
+        status: 'OK',
+      }),
+    );
+    const { user } = getPageObjects();
+    renderBranchOverview();
+
+    // Meta info
+    expect(await byText('overview.promoted_section.title').find()).toBeInTheDocument();
+
+    await user.click(
+      byRole('button', { name: 'overview.promoted_section.button_secondary' }).get(),
+    );
+
+    expect(byText('overview.promoted_section.title').query()).not.toBeInTheDocument();
+
+    expect(byText('guiding.replay_tour_button.1.title').get()).toBeInTheDocument();
+  });
+
+  it('should show CaYC tour', async () => {
+    qualityGatesHandler.setQualityGateProjectStatus(
+      mockQualityGateProjectStatus({
+        status: 'OK',
+      }),
+    );
+    const { user } = getPageObjects();
+    renderBranchOverview();
+
+    expect(await byText('overview.promoted_section.title').find()).toBeInTheDocument();
+
+    await user.click(byRole('button', { name: 'overview.promoted_section.button_primary' }).get());
+
+    expect(byText('overview.promoted_section.title').query()).not.toBeInTheDocument();
+
+    expect(await byText('guiding.cayc_promotion.1.title').find()).toBeInTheDocument();
+
+    await user.click(byRole('button', { name: 'next' }).get());
+
+    expect(byText('guiding.cayc_promotion.2.title').get()).toBeInTheDocument();
+
+    await user.click(byRole('button', { name: 'next' }).get());
+
+    expect(byText('guiding.cayc_promotion.3.title').get()).toBeInTheDocument();
+
+    await user.click(await byRole('button', { name: 'next' }).find());
+
+    expect(byText('guiding.cayc_promotion.4.title').get()).toBeInTheDocument();
+
+    await user.click(byRole('button', { name: 'complete' }).get());
+
+    expect(byText('guiding.replay_tour_button.tour_completed.1.title').get()).toBeInTheDocument();
+  });
 });
 
 describe('application overview', () => {
@@ -716,18 +776,17 @@ it.each([
 );
 
 function renderBranchOverview(props: Partial<BranchOverview['props']> = {}) {
+  const user = mockLoggedInUser();
+  const component = mockComponent({
+    breadcrumbs: [mockComponent({ key: 'foo' })],
+    key: 'foo',
+    name: 'Foo',
+    version: 'version-1.0',
+  });
   return renderComponent(
-    <CurrentUserContextProvider currentUser={mockLoggedInUser()}>
-      <BranchOverview
-        branch={mockMainBranch()}
-        component={mockComponent({
-          breadcrumbs: [mockComponent({ key: 'foo' })],
-          key: 'foo',
-          name: 'Foo',
-          version: 'version-1.0',
-        })}
-        {...props}
-      />
+    <CurrentUserContextProvider currentUser={user}>
+      <Header component={component} currentUser={user} />
+      <BranchOverview branch={mockMainBranch()} component={component} {...props} />
     </CurrentUserContextProvider>,
   );
 }
index 5fcb961d0f3e30b4981613a15f22cd02ee405386..22bd24d7b98bc7c3ece9cb0baaaf26300ee26839 100644 (file)
@@ -30,7 +30,7 @@ export default function LastAnalysisLabel({ analysisDate }: Readonly<Props>) {
   const intl = useIntl();
 
   return analysisDate ? (
-    <span>
+    <span className="sw-pl-4" data-spotlight-id="cayc-promotion-2">
       {intl.formatMessage(
         {
           id: 'overview.last_analysis_x',
index 20baab75062a165a43abd35e3349ef6c1a6e5002..2c04a93b43a0464390aa014dc4df7179f21ac061 100644 (file)
@@ -151,7 +151,7 @@ function getPageObjects() {
     versionLabel: (version?: string) =>
       version ? byText(/footer\.version\s*(\d.\d)/) : byText(/footer\.version/),
     ltaDocumentationLinkActive: byRole('link', {
-      name: `footer.version.status.active open_in_new_window`,
+      name: `footer.version.status.active open_in_new_tab`,
     }),
   };
 
index 14195ed84c098ac02736e9159ca65c30f7a2807e..591b1e8e12fca9ccad8de1c87400652b3e3b7f79 100644 (file)
@@ -50,7 +50,7 @@ export function getCommonNodes(ci: TutorialModes) {
     linkToRepo: byRole('link', {
       name: `onboarding.tutorial.with.${CI_TRANSLATE_MAP[ci]}.${
         ci === TutorialModes.GitHubActions ? 'secret' : 'variables'
-      }.intro.link open_in_new_window`,
+      }.intro.link open_in_new_tab`,
     }),
     allSetSentence: byText('onboarding.tutorial.ci_outro.done'),
   };
index 1e6078b05ff4686d50177f6f02ab0364055457f7..e58243d7a799e5e4201f529bfd66a1b9b0393f53 100644 (file)
@@ -36,6 +36,7 @@ export enum NoticeType {
   ISSUE_NEW_STATUS_AND_TRANSITION_GUIDE = 'issueNewIssueStatusAndTransitionGuide',
   QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification',
   OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification',
+  ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE = 'onboardingDismissCaycBranchSummaryGuide',
 }
 
 export interface LoggedInUser extends CurrentUser, UserActive {
index 146903e68e08ae6a3c435c3077bef2f51965c577..2ead1c25259693df01f21349bb0e12a89345d1da 100644 (file)
@@ -3377,25 +3377,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@gilbarbara/helpers@npm:^0.9.0":
-  version: 0.9.2
-  resolution: "@gilbarbara/helpers@npm:0.9.2"
-  dependencies:
-    "@gilbarbara/types": "npm:^0.2.2"
-    is-lite: "npm:^1.2.1"
-  checksum: 10/17a111aea44ce5368413042974d8d890f48244becb50bb51c9ba75f7c0dd6d3e918d2d2fbba445a9188993c2ab11a2ba72ed0a477219621a37a80b2998751bb9
-  languageName: node
-  linkType: hard
-
-"@gilbarbara/types@npm:^0.2.2":
-  version: 0.2.2
-  resolution: "@gilbarbara/types@npm:0.2.2"
-  dependencies:
-    type-fest: "npm:^4.1.0"
-  checksum: 10/fb71d2e577a48b68b2205146c4cc6180d4e1d175df8b37de47e7581feaeb68ff32918dade9ddb94627755f0d8270727ef7209a9b238bc3af79dc83493b14da5a
-  languageName: node
-  linkType: hard
-
 "@highlightjs/cdn-assets@npm:^11.9.0":
   version: 11.9.0
   resolution: "@highlightjs/cdn-assets@npm:11.9.0"
@@ -3820,6 +3801,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@material-symbols/font-400@npm:0.17.2":
+  version: 0.17.2
+  resolution: "@material-symbols/font-400@npm:0.17.2"
+  checksum: 10/f3dae732c3d16a1dcc4310f045608cd422e32e1b158189a546c2aeca3af449d3cb6d952a096caf267a1646f4b4ab98a19348735e5b39b60841e04c5a8b96339b
+  languageName: node
+  linkType: hard
+
 "@microsoft/api-extractor-model@npm:7.28.3":
   version: 7.28.3
   resolution: "@microsoft/api-extractor-model@npm:7.28.3"
@@ -4364,6 +4352,26 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@radix-ui/react-visually-hidden@npm:1.0.3":
+  version: 1.0.3
+  resolution: "@radix-ui/react-visually-hidden@npm:1.0.3"
+  dependencies:
+    "@babel/runtime": "npm:^7.13.10"
+    "@radix-ui/react-primitive": "npm:1.0.3"
+  peerDependencies:
+    "@types/react": "*"
+    "@types/react-dom": "*"
+    react: ^16.8 || ^17.0 || ^18.0
+    react-dom: ^16.8 || ^17.0 || ^18.0
+  peerDependenciesMeta:
+    "@types/react":
+      optional: true
+    "@types/react-dom":
+      optional: true
+  checksum: 10/2e9d0c8253f97e7d6ffb2e52a5cfd40ba719f813b39c3e2e42c496d54408abd09ef66b5aec4af9b8ab0553215e32452a5d0934597a49c51dd90dc39181ed0d57
+  languageName: node
+  linkType: hard
+
 "@react-spring/animated@npm:~9.7.3":
   version: 9.7.3
   resolution: "@react-spring/animated@npm:9.7.3"
@@ -4533,13 +4541,14 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@sonarsource/echoes-react@npm:0.2.1":
-  version: 0.2.1
-  resolution: "@sonarsource/echoes-react@npm:0.2.1"
+"@sonarsource/echoes-react@npm:0.2.2":
+  version: 0.2.2
+  resolution: "@sonarsource/echoes-react@npm:0.2.2"
   dependencies:
+    "@material-symbols/font-400": "npm:0.17.2"
     "@radix-ui/react-checkbox": "npm:1.0.4"
     "@radix-ui/react-radio-group": "npm:1.1.3"
-    material-symbols: "npm:0.17.0"
+    "@radix-ui/react-visually-hidden": "npm:1.0.3"
   peerDependencies:
     "@emotion/react": ^11.0.0
     "@emotion/styled": ^11.0.0
@@ -4547,7 +4556,7 @@ __metadata:
     react-dom: ^17.0.0 || ^18.0.0
     react-intl: ^6.0.0
     react-router-dom: ^6.0.0
-  checksum: 10/dccb03e27d0ff13d32923255b984807fce119a212728eb69cd16019a927e1014c07c7facef91e9306f270d0929b2b36ee03a452b99ca53d51ce5434c24fc6951
+  checksum: 10/ec5fed2ac473028ca489a8445190c5585ff7e41d9cad0221b315260647307c1c91d59efe0f7b708d099ca24d7a9005dcde49d91f1408d26d7c33ad6a8a80578a
   languageName: node
   linkType: hard
 
@@ -5749,7 +5758,7 @@ __metadata:
     "@primer/octicons-react": "npm:19.8.0"
     "@react-spring/rafz": "npm:9.7.3"
     "@react-spring/web": "npm:9.7.3"
-    "@sonarsource/echoes-react": "npm:0.2.1"
+    "@sonarsource/echoes-react": "npm:0.2.2"
     "@swc/core": "npm:1.4.0"
     "@swc/jest": "npm:0.2.36"
     "@tanstack/react-query": "npm:5.18.1"
@@ -5836,7 +5845,7 @@ __metadata:
     react-helmet-async: "npm:2.0.4"
     react-highlight-words: "npm:0.20.0"
     react-intl: "npm:6.6.2"
-    react-joyride: "npm:2.7.2"
+    react-joyride: "npm:2.8.1"
     react-modal: "npm:3.16.1"
     react-router-dom: "npm:6.22.0"
     react-select: "npm:5.7.7"
@@ -7676,6 +7685,7 @@ __metadata:
     "@babel/preset-typescript": "npm:7.23.3"
     "@emotion/babel-plugin": "npm:11.11.0"
     "@emotion/babel-plugin-jsx-pragmatic": "npm:0.2.1"
+    "@sonarsource/echoes-react": "npm:0.2.2"
     "@testing-library/dom": "npm:9.3.4"
     "@testing-library/jest-dom": "npm:6.4.2"
     "@testing-library/react": "npm:14.2.1"
@@ -7733,7 +7743,7 @@ __metadata:
     react-helmet-async: 2.0.4
     react-highlight-words: 0.20.0
     react-intl: 6.6.2
-    react-joyride: 2.7.2
+    react-joyride: 2.8.1
     react-modal: 3.16.1
     react-router-dom: 6.22.0
     react-select: 5.7.7
@@ -11701,13 +11711,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"material-symbols@npm:0.17.0":
-  version: 0.17.0
-  resolution: "material-symbols@npm:0.17.0"
-  checksum: 10/d432e18203d38b83a645783b03ac4b321d30cd6a8bec1aae5e47fccf4340387d4f7bc703d3aebb5a25639c316bb04f2027518e592869361d8bfbbd46ca8c9835
-  languageName: node
-  linkType: hard
-
 "memoize-one@npm:^4.0.0":
   version: 4.0.3
   resolution: "memoize-one@npm:4.0.3"
@@ -13112,26 +13115,25 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-joyride@npm:2.7.2":
-  version: 2.7.2
-  resolution: "react-joyride@npm:2.7.2"
+"react-joyride@npm:2.8.1":
+  version: 2.8.1
+  resolution: "react-joyride@npm:2.8.1"
   dependencies:
     "@gilbarbara/deep-equal": "npm:^0.3.1"
-    "@gilbarbara/helpers": "npm:^0.9.0"
     deep-diff: "npm:^1.0.2"
     deepmerge: "npm:^4.3.1"
-    is-lite: "npm:^1.2.0"
+    is-lite: "npm:^1.2.1"
     react-floater: "npm:^0.7.9"
     react-innertext: "npm:^1.1.5"
     react-is: "npm:^16.13.1"
     scroll: "npm:^3.0.1"
     scrollparent: "npm:^2.1.0"
     tree-changes: "npm:^0.11.2"
-    type-fest: "npm:^4.8.3"
+    type-fest: "npm:^4.15.0"
   peerDependencies:
     react: 15 - 18
     react-dom: 15 - 18
-  checksum: 10/1eb93d7edcfd662bc9a942b62430b5e92bf396002586d2c807dab5b5d599805209ee854ea1adb48d9a7855aada8dba6872be76ff6f2448820a42ca4c0be1661a
+  checksum: 10/55ff023104f708c3d4c17e2dcc27e3b54a268872b0baf60bd57ee41971e6e8a9e1d4e4c74d4dec084ec4b69eb21edcc985ef4979611a775327e856e03d1335d3
   languageName: node
   linkType: hard
 
@@ -14800,10 +14802,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"type-fest@npm:^4.1.0, type-fest@npm:^4.8.3":
-  version: 4.10.2
-  resolution: "type-fest@npm:4.10.2"
-  checksum: 10/2b1ad1270d9fabeeb506ba831d513caeb05bfc852e5e012511d785ce9dc68d773fe0a42bddf857a362c7f3406244809c5b8a698b743bb7617d4a8c470672087f
+"type-fest@npm:^4.15.0":
+  version: 4.18.2
+  resolution: "type-fest@npm:4.18.2"
+  checksum: 10/2c176de28384a247fac1503165774e874c15ac39434a775f32ecda3aef5a0cefcfa2f5fb670c3da1f81cf773c355999154078c8d9657db19b65de78334b27933
   languageName: node
   linkType: hard
 
index ffd1c2038c67396dcc2726f745daf1617867051c..41d34bd5a078849d1717d749104d833d178f800a 100644 (file)
@@ -79,7 +79,7 @@ public class DismissNoticeActionIT {
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage(
         "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " +
-          "overviewZeroNewIssuesSimplification, issueNewIssueStatusAndTransitionGuide]");
+          "overviewZeroNewIssuesSimplification, issueNewIssueStatusAndTransitionGuide, onboardingDismissCaycBranchSummaryGuide]");
   }
 
   @Test
index 19b248745381eac226cd36db57c67ccd915c4e7b..a064114abd55590b1fe44c3bb46a700dc4e0e2b4 100644 (file)
@@ -40,9 +40,10 @@ public class DismissNoticeAction implements UsersWsAction {
   private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification";
   private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification";
   private static final String ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE = "issueNewIssueStatusAndTransitionGuide";
+  private static final String ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE = "onboardingDismissCaycBranchSummaryGuide";
 
   protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION,
-    OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE);
+    OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE, ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE);
   public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices.";
   public static final String SUPPORT_FOR_NEW_NOTICE_MESSAGE = "Support for new notice '%s' was added.";
 
@@ -58,6 +59,7 @@ public class DismissNoticeAction implements UsersWsAction {
   public void define(WebService.NewController context) {
     WebService.NewAction action = context.createAction("dismiss_notice")
       .setDescription("Dismiss a notice for the current user. Silently ignore if the notice is already dismissed.")
+      .setChangelog(new Change("10.6", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE)))
       .setChangelog(new Change("10.4", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE)))
       .setChangelog(new Change("10.3", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION)))
       .setChangelog(new Change("10.2", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_CLEAN_CODE_GUIDE)))
index 6f05b21e39b906a429aeef7497225d11d1442bfd..46eb48c9a603eac537d53b0c7d4198f17ebb3de6 100644 (file)
@@ -30,6 +30,7 @@ beta=BETA
 blocker=Blocker
 bold=Bold
 branch=Branch
+branch.small=branch
 breadcrumbs=Breadcrumbs
 expand_breadcrumbs=Expand breadcrumbs
 by_=by
@@ -52,6 +53,7 @@ code=Code
 color=Color
 collapse_all=Collapse all
 compare=Compare
+complete=Complete
 component=Component
 configure=Configure
 confirm=Confirm
@@ -104,6 +106,7 @@ from=From
 global=Global
 github=GitHub
 go_back=Go back
+got_it=Got it
 help=Help
 here=here
 hide=Hide
@@ -118,6 +121,7 @@ language=Language
 last_analysis=Last Analysis
 learn_more=Learn More
 learn_more_x=Learn More: {link}
+learn_more.clean_code=Learn more: Clean as You Code
 library=Library
 line_number=Line Number
 links=Links
@@ -170,6 +174,7 @@ password=Password
 path=Path
 permalink=Permanent Link
 plugin=Plugin
+previous=Previous
 previous_=previous
 previous_month_x=previous month {month}
 project=Project
@@ -179,6 +184,7 @@ projects_=project(s)
 x_projects_={0} project(s)
 project_plural=projects
 projects_management=Projects Management
+pull_request.small=pull request
 quality_profile=Quality Profile
 raw=Raw
 recent_history=Recent History
@@ -4071,6 +4077,26 @@ overview.quality_profiles_update_after_sq_upgrade.link=See more details
 overview.activity.variations.new_analysis=New analysis:
 overview.activity.variations.first_analysis=First analysis:
 
+overview.promoted_section.title=Struggling with too many issues? Discover ‘Clean as You Code’!
+overview.promoted_section.content=Learn how to improve your code base by cleaning only new code.
+overview.promoted_section.button_primary=Take the Tour
+overview.promoted_section.button_secondary=Not now
+overview.promoted_section.button_tooltip=Learn how to improve your code base by cleaning only new code.
+
+guiding.cayc_promotion.1.title=The power of new code
+guiding.cayc_promotion.1.content.1=Cleaning only new code is easy and guarantees no debt will be added. As you change old code, it also gets cleaner over time. We call this ‘Clean as You Code’.
+guiding.cayc_promotion.2.title=Define your new code
+guiding.cayc_promotion.2.content.1=Your team decides when a new code period for your project should start, for example, each time a project is released.
+guiding.cayc_promotion.3.title=Green is clean
+guiding.cayc_promotion.3.content.1=Quality Gate Status tells you if your new code is clean or not. Keep it green as often as possible, and your project will always be production-ready.
+guiding.cayc_promotion.4.title=Clean at all levels
+guiding.cayc_promotion.4.content.1=With SonarLint, clean code as you write it in your {value}.
+guiding.cayc_promotion.4.content.2=When a feature is ready, analyze your {value} and make sure no issue is missed.
+guiding.cayc_promotion.4.content.3=Finally, rely on a thorough {value} analysis to ensure the new code is clean.
+guiding.replay_tour_button.1.title=Replay tour
+guiding.replay_tour_button.tour_completed.1.title=Tour complete!
+guiding.replay_tour_button.1.content=You can replay this product tour any time here.
+
 
 #------------------------------------------------------------------------------
 #