]> source.dussan.org Git - sonarqube.git/commitdiff
MMF-3429 SonarWay smooth transition (#9791)
authorPhilippe Perrin <philippe.perrin@sonarsource.com>
Wed, 1 Nov 2023 10:31:03 +0000 (11:31 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 1 Nov 2023 20:02:35 +0000 (20:02 +0000)
Co-authored-by: Andrey Luiz <andrey.luiz@sonarsource.com>
Co-authored-by: Nolwenn Cadic <98824442+Nolwenn-cadic-sonarsource@users.noreply.github.com>
19 files changed:
server/sonar-web/design-system/src/components/SpotlightTour.tsx
server/sonar-web/design-system/src/components/__tests__/SpotlightTour-test.tsx
server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardNumber.tsx
server/sonar-web/src/main/js/apps/overview/branches/MeasuresCardPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/QualityGatePanelSection.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/QualityGatePanelSection-test.tsx
server/sonar-web/src/main/js/apps/overview/components/QualityGateConditions.tsx
server/sonar-web/src/main/js/apps/overview/components/QualityGateSimplifiedCondition.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/ZeroNewIssuesSimplificationGuide.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateSimplifiedCondition-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/pullRequests/PullRequestOverview.tsx
server/sonar-web/src/main/js/apps/overview/pullRequests/__tests__/PullRequestOverview-it.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/CaYCConditionsSimplificationGuide.tsx
server/sonar-web/src/main/js/apps/quality-gates/components/__tests__/QualityGate-it.tsx
server/sonar-web/src/main/js/types/users.ts
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

index 6e16231677621035a1ee3a7001a85cc3575e9fdf..fcc529ca98ba8cb434419259655f1a85d577d0b9 100644 (file)
@@ -139,11 +139,14 @@ function TooltipComponent({
       </div>
       <div>{step.content}</div>
       <div className="sw-flex sw-justify-between sw-items-center sw-mt-3">
-        <strong>
-          {stepXofYLabel
-            ? stepXofYLabel(index + 1, size)
-            : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
-        </strong>
+        {(stepXofYLabel || size > 1) && (
+          <strong>
+            {stepXofYLabel
+              ? stepXofYLabel(index + 1, size)
+              : intl.formatMessage({ id: 'guiding.step_x_of_y' }, { '0': index + 1, '1': size })}
+          </strong>
+        )}
+        <span />
         <div>
           {index > 0 && (
             <ButtonLink className="sw-mr-4" {...backProps}>
index 1d14f358b7b1571d48796dbd343cc98db3a1c8d2..4b0ab75076fa32f1f15f90dea04eda327c056d16 100644 (file)
@@ -30,39 +30,40 @@ it('should display the spotlight tour', async () => {
 
   expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
   expect(screen.getByRole('alertdialog')).toHaveTextContent(
-    'Trust The FooFoo bar is bazstep 1 of 5next'
+    'Trust The FooFoo bar is bazstep 1 of 5next',
   );
+  expect(screen.getByText('step 1 of 5')).toBeInTheDocument();
 
   await user.click(screen.getByRole('button', { name: 'next' }));
 
   expect(screen.getByRole('alertdialog')).toHaveTextContent(
-    'Trust The BazBaz foo is barstep 2 of 5go_backnext'
+    'Trust The BazBaz foo is barstep 2 of 5go_backnext',
   );
   expect(callback).toHaveBeenCalled();
 
   await user.click(screen.getByRole('button', { name: 'next' }));
 
   expect(screen.getByRole('alertdialog')).toHaveTextContent(
-    'Trust The BarBar baz is foostep 3 of 5go_backnext'
+    'Trust The BarBar baz is foostep 3 of 5go_backnext',
   );
 
   await user.click(screen.getByRole('button', { name: 'next' }));
 
   expect(screen.getByRole('alertdialog')).toHaveTextContent(
-    'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext'
+    'Trust The Foo 2Foo baz is barstep 4 of 5go_backnext',
   );
 
   await user.click(screen.getByRole('button', { name: 'go_back' }));
 
   expect(screen.getByRole('alertdialog')).toHaveTextContent(
-    'Trust The BarBar baz is foostep 3 of 5go_backnext'
+    'Trust The BarBar baz is foostep 3 of 5go_backnext',
   );
 
   await user.click(screen.getByRole('button', { name: 'next' }));
   await user.click(screen.getByRole('button', { name: 'next' }));
 
   expect(screen.getByRole('alertdialog')).toHaveTextContent(
-    'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose'
+    'Trust The Baz 2Baz bar is foostep 5 of 5go_backclose',
   );
 
   expect(screen.queryByRole('button', { name: 'next' })).not.toBeInTheDocument();
@@ -102,6 +103,23 @@ it('should allow the customization of button labels', async () => {
   expect(screen.getByRole('button', { name: 'close_me' })).toBeInTheDocument();
 });
 
+it('should not display steps counter when there is only one step and no render method', async () => {
+  renderSpotlightTour({
+    steps: [
+      {
+        target: '#step1',
+        content: 'Foo bar is baz',
+        title: 'Trust The Foo',
+        placement: 'top',
+      },
+    ],
+    stepXofYLabel: undefined,
+  });
+
+  expect(await screen.findByRole('alertdialog')).toBeInTheDocument();
+  expect(screen.queryByText('step 1 of 1')).not.toBeInTheDocument();
+});
+
 function renderSpotlightTour(props: Partial<SpotlightTourProps> = {}) {
   return renderWithContext(
     <div>
@@ -149,6 +167,6 @@ function renderSpotlightTour(props: Partial<SpotlightTourProps> = {}) {
         ]}
         {...props}
       />
-    </div>
+    </div>,
   );
 }
index 9c512ce6bd20327760b6317b7d66d4add65b0243..340dc2863c98cb80c7fcc7e108271988fb00ad05 100644 (file)
@@ -32,13 +32,22 @@ interface Props {
   value: string;
   failingConditionMetric: MetricKey;
   requireLabel: string;
+  guidingKeyOnError?: string;
 }
 
 export default function MeasuresCardNumber(
   props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>,
 ) {
-  const { label, value, failedConditions, url, failingConditionMetric, requireLabel, ...rest } =
-    props;
+  const {
+    label,
+    value,
+    failedConditions,
+    url,
+    failingConditionMetric,
+    requireLabel,
+    guidingKeyOnError,
+    ...rest
+  } = props;
 
   const failed = Boolean(
     failedConditions.find((condition) => condition.metric === failingConditionMetric),
@@ -51,6 +60,7 @@ export default function MeasuresCardNumber(
       metric={failingConditionMetric}
       label={label}
       failed={failed}
+      data-guiding-id={failed ? guidingKeyOnError : undefined}
       {...rest}
     >
       {failed && <TextError className="sw-font-regular sw-mt-2" text={requireLabel} />}
index f123188286c76152a5cccdb13a133151c327f7ee..b0bbc587093787f499f61a49c6197828cf7c6816 100644 (file)
@@ -72,6 +72,7 @@ export default function MeasuresCardPanel(props: React.PropsWithChildren<Props>)
             count: newViolations,
           },
         )}
+        guidingKeyOnError="overviewZeroNewIssuesSimplification"
       />
 
       <MeasuresCardNumber
index 6a7c54afdc034769fd4768da1a9c543bd7e48c99..349121228fc85e518e5ae9b22c701489a8c88e6e 100644 (file)
@@ -98,6 +98,7 @@ export function QualityGatePanel(props: QualityGatePanelProps) {
                       isLastStatus={qgStatusIdx === failedQgStatuses.length - 1}
                       key={qgStatus.key}
                       qgStatus={qgStatus}
+                      qualityGate={qualityGate}
                     />
                   ))}
                 </div>
index c9bb970d1020f25a581cda17916f0ee581113a2a..b7a6861c270e61f7e98ca556ed4864345fe8d705 100644 (file)
@@ -27,13 +27,16 @@ import {
   QualityGateStatus,
   QualityGateStatusConditionEnhanced,
 } from '../../../types/quality-gates';
+import { QualityGate } from '../../../types/types';
 import QualityGateConditions from '../components/QualityGateConditions';
+import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
 
 export interface QualityGatePanelSectionProps {
   branchLike?: BranchLike;
   isApplication?: boolean;
   isLastStatus?: boolean;
   qgStatus: QualityGateStatus;
+  qualityGate?: QualityGate;
 }
 
 function splitConditions(
@@ -54,7 +57,7 @@ function splitConditions(
 }
 
 export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
-  const { isApplication, isLastStatus, qgStatus } = props;
+  const { isApplication, isLastStatus, qgStatus, qualityGate } = props;
   const [collapsed, setCollapsed] = React.useState(false);
 
   const toggle = React.useCallback(() => {
@@ -101,10 +104,14 @@ export function QualityGatePanelSection(props: QualityGatePanelSectionProps) {
               </>
             )}
 
+            {qualityGate?.isBuiltIn && (
+              <ZeroNewIssuesSimplificationGuide qualityGate={qualityGate} />
+            )}
             <QualityGateConditions
               component={qgStatus}
               branchLike={qgStatus.branchLike}
               failedConditions={newCodeFailedConditions}
+              isBuiltInQualityGate={qualityGate?.isBuiltIn}
             />
           </>
         )}
index 05d84f0e2235588524efb9de6de527d01dcc8bec..e08c3845950a471ed8746601802bc4a53b04aa48 100644 (file)
 
 import { screen } from '@testing-library/react';
 import * as React from 'react';
+import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
+import { mockQualityGate, mockQualityGateStatus } from '../../../../helpers/mocks/quality-gates';
+import { mockLoggedInUser } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { byRole } from '../../../../helpers/testSelector';
 import { MetricKey } from '../../../../types/metrics';
 import { CaycStatus, Status } from '../../../../types/types';
+import { CurrentUser, NoticeType } from '../../../../types/users';
 import QualityGatePanelSection, { QualityGatePanelSectionProps } from '../QualityGatePanelSection';
 
 const failedConditions = [
@@ -31,7 +36,7 @@ const failedConditions = [
     measure: {
       metric: {
         id: 'metricId1',
-        key: 'metricKey1',
+        key: MetricKey.new_coverage,
         name: 'metricName1',
         type: 'metricType1',
       },
@@ -44,7 +49,7 @@ const failedConditions = [
     measure: {
       metric: {
         id: 'metricId2',
-        key: 'metricKey2',
+        key: MetricKey.security_hotspots,
         name: 'metricName2',
         type: 'metricType2',
       },
@@ -52,20 +57,33 @@ const failedConditions = [
     metric: MetricKey.security_hotspots,
     op: 'op2',
   },
+  {
+    level: 'ERROR' as Status,
+    measure: {
+      metric: {
+        id: 'metricId2',
+        key: MetricKey.new_violations,
+        name: 'metricName2',
+        type: 'metricType2',
+      },
+    },
+    metric: MetricKey.new_violations,
+    op: 'op2',
+  },
 ];
 
-const qgStatus = {
+const qgStatus = mockQualityGateStatus({
   caycStatus: CaycStatus.Compliant,
   failedConditions,
   key: 'qgStatusKey',
   name: 'qgStatusName',
   status: 'ERROR' as Status,
-};
+});
 
 it('should render correctly for an application with 1 new code condition and 1 overall code condition', async () => {
   renderQualityGatePanelSection();
 
-  expect(await screen.findByText('quality_gates.conditions.new_code_1')).toBeInTheDocument();
+  expect(await screen.findByText('quality_gates.conditions.new_code_x.2')).toBeInTheDocument();
   expect(await screen.findByText('quality_gates.conditions.overall_code_1')).toBeInTheDocument();
 });
 
@@ -79,6 +97,40 @@ it('should render correctly for a project with 1 new code condition', () => {
   expect(screen.queryByText('quality_gates.conditions.overall_code_1')).not.toBeInTheDocument();
 });
 
-function renderQualityGatePanelSection(props: Partial<QualityGatePanelSectionProps> = {}) {
-  return renderComponent(<QualityGatePanelSection isApplication qgStatus={qgStatus} {...props} />);
+it('should render correctly 0 New issues onboarding', async () => {
+  renderQualityGatePanelSection({
+    isApplication: false,
+    qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] },
+    qualityGate: mockQualityGate({ isBuiltIn: true }),
+  });
+
+  expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument();
+  expect(await byRole('alertdialog').find()).toBeInTheDocument();
+});
+
+it('should not render 0 New issues onboarding for user who dismissed it', async () => {
+  renderQualityGatePanelSection(
+    {
+      isApplication: false,
+      qgStatus: { ...qgStatus, failedConditions: [failedConditions[2]] },
+      qualityGate: mockQualityGate({ isBuiltIn: true }),
+    },
+    mockLoggedInUser({
+      dismissedNotices: { [NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]: true },
+    }),
+  );
+
+  expect(screen.queryByText('quality_gates.conditions.new_code_1')).not.toBeInTheDocument();
+  expect(await byRole('alertdialog').query()).not.toBeInTheDocument();
+});
+
+function renderQualityGatePanelSection(
+  props: Partial<QualityGatePanelSectionProps> = {},
+  currentUser: CurrentUser = mockLoggedInUser(),
+) {
+  return renderComponent(
+    <CurrentUserContextProvider currentUser={currentUser}>
+      <QualityGatePanelSection isApplication qgStatus={qgStatus} {...props} />
+    </CurrentUserContextProvider>,
+  );
 }
index b090edb64574cf385b5762acff1cb38b3b54b146..b4b0ad79cf305d1b4eced6549ffdee76aae58dee 100644 (file)
@@ -22,9 +22,11 @@ import { sortBy } from 'lodash';
 import * as React from 'react';
 import { translate } from '../../../helpers/l10n';
 import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
 import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
 import { Component } from '../../../types/types';
 import QualityGateCondition from './QualityGateCondition';
+import QualityGateSimplifiedCondition from './QualityGateSimplifiedCondition';
 
 const LEVEL_ORDER = ['ERROR', 'WARN'];
 
@@ -33,16 +35,25 @@ export interface QualityGateConditionsProps {
   component: Pick<Component, 'key'>;
   collapsible?: boolean;
   failedConditions: QualityGateStatusConditionEnhanced[];
+  isBuiltInQualityGate?: boolean;
 }
 
 const MAX_CONDITIONS = 5;
 
 export function QualityGateConditions(props: QualityGateConditionsProps) {
-  const { branchLike, collapsible, component, failedConditions } = props;
+  const { branchLike, collapsible, component, failedConditions, isBuiltInQualityGate } = props;
   const [collapsed, toggleCollapsed] = React.useState(Boolean(collapsible));
 
   const handleToggleCollapsed = React.useCallback(() => toggleCollapsed(!collapsed), [collapsed]);
 
+  const isSimplifiedCondition = React.useCallback(
+    (condition: QualityGateStatusConditionEnhanced) => {
+      const { metric } = condition.measure;
+      return metric.key === MetricKey.new_violations && isBuiltInQualityGate;
+    },
+    [isBuiltInQualityGate],
+  );
+
   const sortedConditions = sortBy(failedConditions, (condition) =>
     LEVEL_ORDER.indexOf(condition.level),
   );
@@ -62,11 +73,19 @@ export function QualityGateConditions(props: QualityGateConditionsProps) {
     <ul id="overview-quality-gate-conditions-list" className="sw-mb-2">
       {renderConditions.map((condition) => (
         <div key={condition.measure.metric.key}>
-          <QualityGateCondition
-            branchLike={branchLike}
-            component={component}
-            condition={condition}
-          />
+          {isSimplifiedCondition(condition) ? (
+            <QualityGateSimplifiedCondition
+              branchLike={branchLike}
+              component={component}
+              condition={condition}
+            />
+          ) : (
+            <QualityGateCondition
+              branchLike={branchLike}
+              component={component}
+              condition={condition}
+            />
+          )}
           <BasicSeparator />
         </div>
       ))}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/QualityGateSimplifiedCondition.tsx b/server/sonar-web/src/main/js/apps/overview/components/QualityGateSimplifiedCondition.tsx
new file mode 100644 (file)
index 0000000..549c9a6
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { Highlight, LinkBox } from 'design-system';
+import * as React from 'react';
+import { propsToIssueParams } from '../../../components/shared/utils';
+import { getBranchLikeQuery } from '../../../helpers/branch-like';
+import { translate } from '../../../helpers/l10n';
+import { formatMeasure, isDiffMetric, localizeMetric } from '../../../helpers/measures';
+import { getComponentIssuesUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../types/quality-gates';
+import { Component } from '../../../types/types';
+
+interface Props {
+  branchLike?: BranchLike;
+  component: Pick<Component, 'key'>;
+  condition: QualityGateStatusConditionEnhanced;
+}
+
+export default function QualityGateSimplifiedCondition({
+  branchLike,
+  component,
+  condition,
+}: Readonly<Props>) {
+  const getPrimaryText = () => {
+    const { measure } = condition;
+    const { metric } = measure;
+    const isDiff = isDiffMetric(metric.key);
+
+    const subText =
+      !isDiff && condition.period != null
+        ? `${localizeMetric(metric.key)} ${translate('quality_gates.conditions.new_code')}`
+        : localizeMetric(metric.key);
+
+    return subText;
+  };
+
+  const { measure } = condition;
+  const { metric } = measure;
+
+  const value = (condition.period ? measure.period?.value : measure.value) as string;
+
+  const formattedValue = formatMeasure(value, MetricType.ShortInteger, {
+    decimals: 0,
+    omitExtraDecimalZeros: metric.type === MetricType.Percent,
+  });
+
+  return (
+    <LinkBox
+      to={getComponentIssuesUrl(component.key, {
+        ...propsToIssueParams(condition.measure.metric.key, condition.period != null),
+        ...getBranchLikeQuery(branchLike),
+      })}
+    >
+      <div className="sw-flex sw-p-2 sw-items-baseline">
+        <Highlight className="sw-mx-4 sw-w-6 sw-my-0 sw-text-right">{formattedValue}</Highlight>
+        <Highlight
+          className="sw-text-ellipsis sw-pr-4"
+          data-guiding-id={
+            metric.key === MetricKey.new_violations
+              ? 'overviewZeroNewIssuesSimplification'
+              : undefined
+          }
+        >
+          {getPrimaryText()}
+        </Highlight>
+      </div>
+    </LinkBox>
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/ZeroNewIssuesSimplificationGuide.tsx b/server/sonar-web/src/main/js/apps/overview/components/ZeroNewIssuesSimplificationGuide.tsx
new file mode 100644 (file)
index 0000000..36fdb17
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import { SpotlightTour, SpotlightTourStep } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { dismissNotice } from '../../../api/users';
+import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
+import Link from '../../../components/common/Link';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
+import { QualityGate } from '../../../types/types';
+import { NoticeType } from '../../../types/users';
+
+interface Props {
+  qualityGate: QualityGate;
+}
+
+export default function ZeroNewIssuesSimplificationGuide({ qualityGate }: Readonly<Props>) {
+  const { currentUser, updateDismissedNotices } = React.useContext(CurrentUserContext);
+  const shouldRun =
+    currentUser.isLoggedIn &&
+    !currentUser.dismissedNotices[NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION];
+
+  const steps: SpotlightTourStep[] = [
+    {
+      target: `[data-guiding-id="overviewZeroNewIssuesSimplification"]`,
+      content: (
+        <>
+          <p className="sw-mb-4">
+            <FormattedMessage
+              id="overview.quality_gates.conditions.condition_simplification_tour.content1"
+              defaultMessage={translate(
+                'overview.quality_gates.conditions.condition_simplification_tour.content1',
+              )}
+              values={{
+                link: (
+                  <Link to={`/quality_gates/show/${qualityGate.name}`}>
+                    {translateWithParameters(
+                      'overview.quality_gates.conditions.condition_simplification_tour.content1.link',
+                      qualityGate.name,
+                    )}
+                  </Link>
+                ),
+              }}
+            />
+          </p>
+          <p>
+            {translate('overview.quality_gates.conditions.condition_simplification_tour.content2')}
+          </p>
+        </>
+      ),
+      title: translate('overview.quality_gates.conditions.condition_simplification_tour.title'),
+      placement: 'right',
+    },
+  ];
+
+  const onCallback = async (props: { action: string; type: string }) => {
+    if (props.action === 'close' && props.type === 'tour:end' && shouldRun) {
+      await dismissNotice(NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION);
+      updateDismissedNotices(NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, true);
+    }
+  };
+
+  return (
+    <SpotlightTour
+      run={shouldRun}
+      closeLabel={translate('dismiss')}
+      callback={onCallback}
+      steps={steps}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateSimplifiedCondition-test.tsx b/server/sonar-web/src/main/js/apps/overview/components/__tests__/QualityGateSimplifiedCondition-test.tsx
new file mode 100644 (file)
index 0000000..d875379
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+import { screen } from '@testing-library/react';
+import React from 'react';
+import { mockQualityGateStatusConditionEnhanced } from '../../../../helpers/mocks/quality-gates';
+import { mockMetric } from '../../../../helpers/testMocks';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { MetricKey, MetricType } from '../../../../types/metrics';
+import { QualityGateStatusConditionEnhanced } from '../../../../types/quality-gates';
+import QualityGateCondition from '../QualityGateCondition';
+import QualityGateSimplifiedCondition from '../QualityGateSimplifiedCondition';
+
+it('should show simplified condition', async () => {
+  renderQualityGateCondition({
+    condition: quickMock(MetricKey.new_violations, MetricType.Integer),
+  });
+  expect(await screen.findByText('metric.new_violations.name')).toBeInTheDocument();
+});
+
+function renderQualityGateCondition(props: Partial<QualityGateCondition['props']>) {
+  return renderComponent(
+    <QualityGateSimplifiedCondition
+      component={{ key: 'abcd-key' }}
+      condition={mockQualityGateStatusConditionEnhanced()}
+      {...props}
+    />,
+  );
+}
+
+function quickMock(
+  metric: MetricKey,
+  type = MetricType.Rating,
+  addPeriod = false,
+  value = '3',
+): QualityGateStatusConditionEnhanced {
+  return mockQualityGateStatusConditionEnhanced({
+    error: '1',
+    measure: {
+      metric: mockMetric({
+        key: metric,
+        name: metric,
+        type,
+      }),
+      value,
+      ...(addPeriod ? { period: { value, index: 1 } } : {}),
+    },
+    metric,
+    ...(addPeriod ? { period: 1 } : {}),
+  });
+}
index 9da9ac8c86b96da4dc495cb5eddd9e34bdb3a30c..5c0d965b20c00ffe9d683bdd9e1733c76b8dbddc 100644 (file)
@@ -22,16 +22,18 @@ import { uniq } from 'lodash';
 import * as React from 'react';
 import { useEffect, useState } from 'react';
 import { getMeasuresWithMetrics } from '../../../api/measures';
+import { fetchQualityGate, getGateForProject } from '../../../api/quality-gates';
 import { getBranchLikeQuery } from '../../../helpers/branch-like';
 import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
 import { isDefined } from '../../../helpers/types';
 import { useBranchStatusQuery } from '../../../queries/branch';
 import { PullRequest } from '../../../types/branch-like';
-import { Component, MeasureEnhanced } from '../../../types/types';
+import { Component, MeasureEnhanced, QualityGate } from '../../../types/types';
 import MeasuresCardPanel from '../branches/MeasuresCardPanel';
 import BranchQualityGate from '../components/BranchQualityGate';
 import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
 import MetaTopBar from '../components/MetaTopBar';
+import ZeroNewIssuesSimplificationGuide from '../components/ZeroNewIssuesSimplificationGuide';
 import '../styles.css';
 import { PR_METRICS, Status } from '../utils';
 import SonarLintAd from './SonarLintAd';
@@ -43,14 +45,18 @@ interface Props {
 
 export default function PullRequestOverview(props: Props) {
   const { component, branchLike } = props;
-  const [loadingMeasure, setLoadingMeasure] = useState(false);
+  const [isLoadingMeasures, setIsLoadingMeasures] = useState(false);
   const [measures, setMeasures] = useState<MeasureEnhanced[]>([]);
-  const { data: { conditions, ignoredConditions, status } = {}, isLoading } =
-    useBranchStatusQuery(component);
-  const loading = isLoading || loadingMeasure;
+  const {
+    data: { conditions, ignoredConditions, status } = {},
+    isLoading: isLoadingBranchStatusesData,
+  } = useBranchStatusQuery(component);
+  const [isLoadingQualityGate, setIsLoadingQualityGate] = useState(false);
+  const [qualityGate, setQualityGate] = useState<QualityGate>();
+  const isLoading = isLoadingBranchStatusesData || isLoadingMeasures || isLoadingQualityGate;
 
   useEffect(() => {
-    setLoadingMeasure(true);
+    setIsLoadingMeasures(true);
 
     const metricKeys =
       conditions !== undefined
@@ -64,17 +70,31 @@ export default function PullRequestOverview(props: Props) {
     getMeasuresWithMetrics(component.key, metricKeys, getBranchLikeQuery(branchLike)).then(
       ({ component, metrics }) => {
         if (component.measures) {
-          setLoadingMeasure(false);
+          setIsLoadingMeasures(false);
           setMeasures(enhanceMeasuresWithMetrics(component.measures || [], metrics));
         }
       },
       () => {
-        setLoadingMeasure(false);
+        setIsLoadingMeasures(false);
       },
     );
   }, [branchLike, component.key, conditions]);
 
-  if (loading) {
+  useEffect(() => {
+    async function fetchQualityGateDate() {
+      setIsLoadingQualityGate(true);
+
+      const qualityGate = await getGateForProject({ project: component.key });
+      const qgDetails = await fetchQualityGate({ name: qualityGate.name });
+
+      setQualityGate(qgDetails);
+      setIsLoadingQualityGate(false);
+    }
+
+    fetchQualityGateDate();
+  }, [component.key]);
+
+  if (isLoading) {
     return (
       <CenteredLayout>
         <div className="sw-p-6">
@@ -119,6 +139,8 @@ export default function PullRequestOverview(props: Props) {
             measures={measures}
           />
 
+          {qualityGate?.isBuiltIn && <ZeroNewIssuesSimplificationGuide qualityGate={qualityGate} />}
+
           <SonarLintAd status={status} />
         </div>
       </div>
index a9f02fd4f73dffdd4ce6d1f940ff3976cb74d7f1..3a80968c392baeb32f7616e204f41aac84829ac2 100644 (file)
 import { screen, waitFor } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import * as React from 'react';
-import { getQualityGateProjectStatus } from '../../../../api/quality-gates';
+import { fetchQualityGate, getQualityGateProjectStatus } from '../../../../api/quality-gates';
 import CurrentUserContextProvider from '../../../../app/components/current-user/CurrentUserContextProvider';
 import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
 import { mockComponent } from '../../../../helpers/mocks/component';
-import { mockQualityGateProjectCondition } from '../../../../helpers/mocks/quality-gates';
+import {
+  mockQualityGate,
+  mockQualityGateProjectCondition,
+} from '../../../../helpers/mocks/quality-gates';
 import { mockLoggedInUser, mockMeasure, mockMetric } from '../../../../helpers/testMocks';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
 import { byLabelText, byRole } from '../../../../helpers/testSelector';
@@ -32,6 +35,7 @@ import { ComponentPropsType } from '../../../../helpers/testUtils';
 import { ComponentQualifier } from '../../../../types/component';
 import { MetricKey, MetricType } from '../../../../types/metrics';
 import { CaycStatus } from '../../../../types/types';
+import { NoticeType } from '../../../../types/users';
 import PullRequestOverview from '../PullRequestOverview';
 
 jest.mock('../../../../api/measures', () => {
@@ -55,27 +59,25 @@ jest.mock('../../../../api/measures', () => {
           mockMeasure({
             metric: MetricKey.new_lines,
           }),
+          mockMeasure({
+            metric: MetricKey.new_violations,
+          }),
         ],
       },
       metrics: [
         mockMetric({ key: MetricKey.new_coverage }),
-        mockMetric({
-          key: MetricKey.duplicated_lines,
-        }),
+        mockMetric({ key: MetricKey.duplicated_lines }),
         mockMetric({ key: MetricKey.new_lines, type: MetricType.ShortInteger }),
-        mockMetric({
-          key: MetricKey.new_bugs,
-          type: MetricType.Integer,
-        }),
+        mockMetric({ key: MetricKey.new_bugs, type: MetricType.Integer }),
+        mockMetric({ key: MetricKey.new_violations }),
       ],
     }),
   };
 });
 
 jest.mock('../../../../api/quality-gates', () => {
-  const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus } = jest.requireActual(
-    '../../../../helpers/mocks/quality-gates',
-  );
+  const { mockQualityGateProjectStatus, mockQualityGateApplicationStatus, mockQualityGate } =
+    jest.requireActual('../../../../helpers/mocks/quality-gates');
   const { MetricKey } = jest.requireActual('../../../../types/metrics');
   return {
     getQualityGateProjectStatus: jest.fn().mockResolvedValue(
@@ -110,6 +112,8 @@ jest.mock('../../../../api/quality-gates', () => {
       }),
     ),
     getApplicationQualityGate: jest.fn().mockResolvedValue(mockQualityGateApplicationStatus()),
+    getGateForProject: jest.fn().mockResolvedValue(mockQualityGate({ isBuiltIn: true })),
+    fetchQualityGate: jest.fn().mockResolvedValue(mockQualityGate({ isBuiltIn: true })),
   };
 });
 
@@ -216,11 +220,68 @@ it('renders SL promotion', async () => {
   ).not.toBeInTheDocument();
 });
 
+it('should render correctly 0 New issues onboarding', async () => {
+  jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
+    status: 'ERROR',
+    conditions: [
+      mockQualityGateProjectCondition({
+        status: 'ERROR',
+        errorThreshold: '0',
+        metricKey: MetricKey.new_violations,
+        actualValue: '1',
+      }),
+    ],
+    caycStatus: CaycStatus.Compliant,
+    ignoredConditions: false,
+  });
+  jest.mocked(fetchQualityGate).mockResolvedValueOnce(mockQualityGate({ isBuiltIn: true }));
+
+  renderPullRequestOverview();
+
+  expect(
+    await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(),
+  ).toBeInTheDocument();
+  expect(await byRole('alertdialog').find()).toBeInTheDocument();
+});
+
+it('should not render 0 New issues onboarding when user dismissed it', async () => {
+  jest.mocked(getQualityGateProjectStatus).mockResolvedValueOnce({
+    status: 'ERROR',
+    conditions: [
+      mockQualityGateProjectCondition({
+        status: 'ERROR',
+        errorThreshold: '0',
+        metricKey: MetricKey.new_violations,
+        actualValue: '1',
+      }),
+    ],
+    caycStatus: CaycStatus.Compliant,
+    ignoredConditions: false,
+  });
+  jest.mocked(fetchQualityGate).mockResolvedValueOnce(mockQualityGate({ isBuiltIn: true }));
+
+  renderPullRequestOverview(
+    {},
+    mockLoggedInUser({
+      dismissedNotices: { [NoticeType.OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION]: true },
+    }),
+  );
+
+  await waitFor(async () =>
+    expect(
+      await byLabelText('overview.quality_gate_x.overview.gate.ERROR').find(),
+    ).toBeInTheDocument(),
+  );
+
+  expect(await byRole('alertdialog').query()).not.toBeInTheDocument();
+});
+
 function renderPullRequestOverview(
   props: Partial<ComponentPropsType<typeof PullRequestOverview>> = {},
+  currentUser = mockLoggedInUser(),
 ) {
   renderComponent(
-    <CurrentUserContextProvider currentUser={mockLoggedInUser()}>
+    <CurrentUserContextProvider currentUser={currentUser}>
       <PullRequestOverview
         branchLike={mockPullRequest()}
         component={mockComponent({
index b5cc148e0aea92824c6e6a0ec1caef354ad28dbc..858e964b1faa62f2036c1c53ca6c2611ea04eb98 100644 (file)
@@ -22,6 +22,7 @@ import { SpotlightTour, SpotlightTourStep } from 'design-system';
 import React from 'react';
 import { dismissNotice } from '../../../api/users';
 import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
+import DocumentationLink from '../../../components/common/DocumentationLink';
 import { translate } from '../../../helpers/l10n';
 import { NoticeType } from '../../../types/users';
 
@@ -32,23 +33,46 @@ export default function CaYCConditionsSimplificationGuide() {
     !currentUser.dismissedNotices[NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION];
 
   const steps: SpotlightTourStep[] = [
+    {
+      target: '[data-guiding-id="caycConditionsSimplification"]',
+      content: (
+        <p>{translate('quality_gates.cayc.condition_simplification_tour.page_1.content1')}</p>
+      ),
+      title: translate('quality_gates.cayc.condition_simplification_tour.page_1.title'),
+      placement: 'right',
+    },
+    {
+      target: '[data-guiding-id="caycConditionsSimplification"]',
+      content: (
+        <>
+          <p className="sw-mb-4">
+            {translate('quality_gates.cayc.condition_simplification_tour.page_2.content1')}
+          </p>
+          <p>{translate('quality_gates.cayc.condition_simplification_tour.page_2.content2')}</p>
+        </>
+      ),
+      title: translate('quality_gates.cayc.condition_simplification_tour.page_2.title'),
+      placement: 'right',
+    },
     {
       target: '[data-guiding-id="caycConditionsSimplification"]',
       content: (
         <>
           <p className="sw-mb-4">
-            {translate('quality_gates.cayc.condition_simplification_tour.content1')}
+            {translate('quality_gates.cayc.condition_simplification_tour.page_3.content1')}
           </p>
-          <p>{translate('quality_gates.cayc.condition_simplification_tour.content2')}</p>
+          <DocumentationLink to="/user-guide/issues/#resolutions">
+            {translate('quality_gates.cayc.condition_simplification_tour.page_3.content2')}
+          </DocumentationLink>
         </>
       ),
-      title: translate('quality_gates.cayc.condition_simplification_tour.title'),
+      title: translate('quality_gates.cayc.condition_simplification_tour.page_3.title'),
       placement: 'right',
     },
   ];
 
-  const onCallback = async (props: { action: string }) => {
-    if (props.action === 'close' && shouldRun) {
+  const onCallback = async (props: { action: string; type: string }) => {
+    if (props.action === 'close' && props.type === 'tour:end' && shouldRun) {
       await dismissNotice(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION);
       updateDismissedNotices(NoticeType.QG_CAYC_CONDITIONS_SIMPLIFICATION, true);
     }
@@ -56,6 +80,7 @@ export default function CaYCConditionsSimplificationGuide() {
 
   return (
     <SpotlightTour
+      continuous
       run={shouldRun}
       closeLabel={translate('dismiss')}
       callback={onCallback}
index 6ff8d46ff4cf74265cf603d6979d7379c9a0ac06..ee43cfe93fb71d6bef6c9085ef7cc9bf06ae6947 100644 (file)
@@ -426,6 +426,32 @@ it('should display CaYC condition simplification tour for users who didnt dismis
 
   expect(await byRole('alertdialog').find()).toBeInTheDocument();
 
+  expect(
+    byRole('alertdialog')
+      .byText('quality_gates.cayc.condition_simplification_tour.page_1.title')
+      .get(),
+  ).toBeInTheDocument();
+
+  await act(async () => {
+    await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get());
+  });
+
+  expect(
+    byRole('alertdialog')
+      .byText('quality_gates.cayc.condition_simplification_tour.page_2.title')
+      .get(),
+  ).toBeInTheDocument();
+
+  await act(async () => {
+    await user.click(byRole('alertdialog').byRole('button', { name: 'next' }).get());
+  });
+
+  expect(
+    byRole('alertdialog')
+      .byText('quality_gates.cayc.condition_simplification_tour.page_3.title')
+      .get(),
+  ).toBeInTheDocument();
+
   await act(async () => {
     await user.click(byRole('alertdialog').byRole('button', { name: 'dismiss' }).get());
   });
index 33ec00a96e1ec1e7e1ffa96496e751df967ea794..a292269835b50cb1e5424e2e22b694c207775639 100644 (file)
@@ -34,6 +34,7 @@ export enum NoticeType {
   SONARLINT_AD = 'sonarlintAd',
   ISSUE_GUIDE = 'issueCleanCodeGuide',
   QG_CAYC_CONDITIONS_SIMPLIFICATION = 'qualityGateCaYCConditionsSimplification',
+  OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = 'overviewZeroNewIssuesSimplification',
 }
 
 export interface LoggedInUser extends CurrentUser, UserActive {
index 68ba21b934b6ef3923a7499f53715be83dcbe284..d761fe011c43abd8174668870b48136f67e136a3 100644 (file)
  */
 package org.sonar.server.user.ws;
 
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.util.Optional;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.sonar.api.utils.System2;
 import org.sonar.db.DbTester;
 import org.sonar.db.property.PropertyDto;
@@ -33,7 +37,9 @@ import org.sonar.server.ws.WsActionTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.sonar.server.user.ws.DismissNoticeAction.AVAILABLE_NOTICE_KEYS;
 
+@RunWith(DataProviderRunner.class)
 public class DismissNoticeActionIT {
 
   @Rule
@@ -43,48 +49,6 @@ public class DismissNoticeActionIT {
 
   private final WsActionTester tester = new WsActionTester(new DismissNoticeAction(userSessionRule, db.getDbClient()));
 
-  @Test
-  public void dismiss_educationPrinciples() {
-    userSessionRule.logIn();
-
-    TestResponse testResponse = tester.newRequest()
-      .setParam("notice", "educationPrinciples")
-      .execute();
-
-    assertThat(testResponse.getStatus()).isEqualTo(204);
-
-    Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples");
-    assertThat(propertyDto).isPresent();
-  }
-
-  @Test
-  public void dismiss_sonarlintAd() {
-    userSessionRule.logIn();
-
-    TestResponse testResponse = tester.newRequest()
-      .setParam("notice", "sonarlintAd")
-      .execute();
-
-    assertThat(testResponse.getStatus()).isEqualTo(204);
-
-    Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.sonarlintAd");
-    assertThat(propertyDto).isPresent();
-  }
-
-  @Test
-  public void execute_whenNoticeIsIssueCleanCodeGuide_shouldDismissCorrespondingNotice() {
-    userSessionRule.logIn();
-
-    TestResponse testResponse = tester.newRequest()
-      .setParam("notice", "issueCleanCodeGuide")
-      .execute();
-
-    assertThat(testResponse.getStatus()).isEqualTo(204);
-
-    Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.issueCleanCodeGuide");
-    assertThat(propertyDto).isPresent();
-  }
-
   @Test
   public void authentication_is_required() {
     TestRequest testRequest = tester.newRequest()
@@ -114,7 +78,8 @@ public class DismissNoticeActionIT {
     assertThatThrownBy(testRequest::execute)
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage(
-        "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification]");
+        "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " +
+          "overviewZeroNewIssuesSimplification]");
   }
 
   @Test
@@ -125,11 +90,32 @@ public class DismissNoticeActionIT {
     assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent();
 
     TestResponse testResponse = tester.newRequest()
-      .setParam("notice", "sonarlintAd")
+      .setParam("notice", "educationPrinciples")
       .execute();
 
     assertThat(testResponse.getStatus()).isEqualTo(204);
     assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent();
   }
 
+  @Test
+  @UseDataProvider("noticeKeys")
+  public void dismiss_notice(String noticeKey) {
+    userSessionRule.logIn();
+
+    TestResponse testResponse = tester.newRequest()
+      .setParam("notice", noticeKey)
+      .execute();
+
+    assertThat(testResponse.getStatus()).isEqualTo(204);
+
+    Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices." + noticeKey);
+    assertThat(propertyDto).isPresent();
+  }
+
+  @DataProvider
+  public static Object[][] noticeKeys() {
+    return AVAILABLE_NOTICE_KEYS.stream()
+      .map(noticeKey -> new Object[] {noticeKey})
+      .toArray(Object[][]::new);
+  }
 }
index 352c599e537a7cb265d21d30e440e1278562d4a2..8a9b6c0de48c338b7e736ab94b80b30bd2b735c9 100644 (file)
@@ -38,8 +38,10 @@ public class DismissNoticeAction implements UsersWsAction {
   private static final String SONARLINT_AD = "sonarlintAd";
   private static final String ISSUE_CLEAN_CODE_GUIDE = "issueCleanCodeGuide";
   private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification";
+  private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification";
 
-  protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION);
+  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);
   public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices.";
 
   private final UserSession userSession;
@@ -86,14 +88,12 @@ public class DismissNoticeAction implements UsersWsAction {
         .setKey(paramKey)
         .build();
 
-      if (!dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) {
-        // already dismissed
-        response.noContent();
+      if (dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) {
+        PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey);
+        dbClient.propertiesDao().saveProperty(dbSession, property);
+        dbSession.commit();
       }
 
-      PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey);
-      dbClient.propertiesDao().saveProperty(dbSession, property);
-      dbSession.commit();
       response.noContent();
     }
   }
index 48dc14d393643e35568782aff41eda22722e0c9c..73e397be9d95b7e6c56af734538f262eb634e913 100644 (file)
@@ -2127,7 +2127,7 @@ quality_gates.copy=Copy Quality Gate
 quality_gates.cannot_set_default_no_cayc=You must make this quality gate Clean as You Code compliant to make this the default quality gate.
 quality_gates.cannot_copy_no_cayc=You must make this quality gate Clean as You Code compliant to copy.
 quality_gates.is_default_no_conditions=This is the default quality gate, but it has no configured conditions. Please configure at least 1 condition for this quality gate.
-quality_gates.is_built_in.description=The only Quality Gate you need to {link}
+quality_gates.is_built_in.description=The only quality gate you need to practice {link}
 quality_gates.conditions=Conditions
 quality_gates.conditions.help=Your project will fail the Quality Gate if it crosses any metric thresholds set for New Code or Overall Code.
 quality_gates.conditions.help.link=See also: Clean as You Code
@@ -2215,9 +2215,14 @@ quality_gates.cayc.review_update_modal.header=Fix "{0}" to comply with Clean as
 quality_gates.cayc.review_update_modal.confirm_text=Fix Quality Gate
 quality_gates.cayc.review_update_modal.description1=This quality gate will be updated to comply with {cayc_link}. Please review the changes below.
 quality_gates.cayc.review_update_modal.description2=All other conditions will be preserved
-quality_gates.cayc.condition_simplification_tour.title=One condition, zero issues
-quality_gates.cayc.condition_simplification_tour.content1=One single condition ensures that new code has 0 issues.
-quality_gates.cayc.condition_simplification_tour.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating.
+quality_gates.cayc.condition_simplification_tour.page_1.title='Clean as You Code' ready!
+quality_gates.cayc.condition_simplification_tour.page_1.content1=The conditions in this quality gate have been updated to ensure that any code added or changed is clean.
+quality_gates.cayc.condition_simplification_tour.page_2.title=One condition, zero issues
+quality_gates.cayc.condition_simplification_tour.page_2.content1=One single condition ensures that new code has no issues.
+quality_gates.cayc.condition_simplification_tour.page_2.content2=This condition replaces the three conditions on Security rating, Reliability rating and Maintainability rating.
+quality_gates.cayc.condition_simplification_tour.page_3.title=Resolve pending issues
+quality_gates.cayc.condition_simplification_tour.page_3.content1=Starting now, every issue in new code must be resolved for a project to pass this quality gate.
+quality_gates.cayc.condition_simplification_tour.page_3.content2=Learn more: Issue resolutions
 quality_gates.cayc.new_maintainability_rating.A=Technical debt ratio is less than {0}
 quality_gates.cayc.new_maintainability_rating=Technical debt ratio is greater than {1}
 quality_gates.cayc.new_reliability_rating.A=No bugs
@@ -3764,6 +3769,10 @@ overview.quality_gate.conditions.cayc.details=Fixing this quality gate will help
 overview.quality_gate.conditions.cayc.details_with_link=Fixing {link} will help you achieve a Clean Code state.
 overview.quality_gate.conditions.non_cayc.warning.link=this quality gate
 overview.quality_gate.conditions.cayc.link=Learn why
+overview.quality_gates.conditions.condition_simplification_tour.title=One condition, zero issues
+overview.quality_gates.conditions.condition_simplification_tour.content1=A new condition was introduced in {link} to ensure that new code has no issues.
+overview.quality_gates.conditions.condition_simplification_tour.content1.link={0} quality gate
+overview.quality_gates.conditions.condition_simplification_tour.content2=Starting now, every issue in new code must be resolved for a project to pass this quality gate.
 overview.quality_gate.application.non_cayc.projects_x={0} project(s) in this application use a Quality Gate that does not comply with Clean as You Code
 overview.quality_gate.show_project_conditions_x=Show failed conditions for project {0}
 overview.quality_gate.hide_project_conditions_x=Hide failed conditions for project {0}