]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20513 Use new ToastMessage for Open issues in IDE success/error messages
authorDavid Cho-Lerat <david.cho-lerat@sonarsource.com>
Tue, 31 Oct 2023 10:49:01 +0000 (11:49 +0100)
committersonartech <sonartech@sonarsource.com>
Tue, 31 Oct 2023 20:02:42 +0000 (20:02 +0000)
14 files changed:
server/sonar-web/design-system/package.json
server/sonar-web/design-system/src/components/index.ts
server/sonar-web/design-system/src/components/toast-message/ToastMessage.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/toast-message/ToastMessageGlobalStyles.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/toast-message/__tests__/ToastMessage-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/toast-message/__tests__/toast-utils-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/toast-message/toast-utils.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/helpers/constants.ts
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/app/components/GlobalContainer.tsx
server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx
server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx
server/sonar-web/yarn.lock
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 3b34f44e7c2810625f02bcd2e1b1e05539a46a71..ffb94d58ca8a000b3392dfa6fbf0acd877fdd34c 100644 (file)
@@ -87,6 +87,7 @@
     "highlight.js": "11.8.0",
     "highlightjs-apex": "1.2.0",
     "highlightjs-cobol": "0.3.3",
-    "highlightjs-sap-abap": "0.3.0"
+    "highlightjs-sap-abap": "0.3.0",
+    "react-toastify": "8.2.0"
   }
 }
index 4e1032292874c36df1cca4feb20ca4d6a7f53637..180db1f3da7eeea0db94690feb9629a2436e0efc 100644 (file)
@@ -101,4 +101,6 @@ export * from './lists';
 export * from './modal/Modal';
 export * from './popups';
 export * from './subnavigation';
+export { ToastMessageContainer } from './toast-message/ToastMessage';
+export * from './toast-message/toast-utils';
 export * from './visual-components';
diff --git a/server/sonar-web/design-system/src/components/toast-message/ToastMessage.tsx b/server/sonar-web/design-system/src/components/toast-message/ToastMessage.tsx
new file mode 100644 (file)
index 0000000..bbd4501
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * 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 styled from '@emotion/styled';
+import { Slide, ToastContainer, ToastContainerProps, toast } from 'react-toastify';
+import tw from 'twin.macro';
+import { TOAST_AUTOCLOSE_DELAY } from '../../helpers/constants';
+import { themeBorder, themeColor } from '../../helpers/theme';
+import { ToastMessageGlobalStyles } from './ToastMessageGlobalStyles';
+
+/** This wrapper function is required to remove the react-toastify theme prop as we use our own Themes */
+function WrappedToastContainer(
+  props: Omit<ToastContainerProps, 'theme'> & React.RefAttributes<HTMLDivElement>,
+) {
+  return <ToastContainer {...props} />;
+}
+
+export function ToastMessageContainer() {
+  return (
+    <>
+      <ToastMessageGlobalStyles />
+
+      <StyledToastContainer
+        autoClose={TOAST_AUTOCLOSE_DELAY}
+        closeOnClick
+        draggable
+        hideProgressBar
+        limit={0}
+        newestOnTop={false}
+        pauseOnFocusLoss
+        pauseOnHover
+        position={toast.POSITION.TOP_RIGHT}
+        rtl={false}
+        transition={Slide}
+      />
+    </>
+  );
+}
+
+const StyledToastContainer = styled(WrappedToastContainer)`
+  .Toastify__toast {
+    ${tw`sw-p-0`}
+  }
+
+  .Toastify__toast-body {
+    ${tw`sw-p-0 sw-m-0`}
+  }
+
+  .Toastify__close-button {
+    ${tw`sw-pt-3 sw-pr-3`}
+    color: ${themeColor('toastCloseIcon')};
+    opacity: 1;
+  }
+
+  .Toastify__toast-theme--light {
+    ${tw`sw-inline-flex`}
+    ${tw`sw-min-h-10`}
+    ${tw`sw-rounded-1`}
+    ${tw`sw-mb-2`}
+
+    background-color: ${themeColor('toast')};
+    border: ${themeBorder('default')};
+
+    .Toastify__toast-icon {
+      align-items: center;
+      justify-content: center;
+      width: 38px;
+      height: calc(100%);
+    }
+
+    &.Toastify__toast--default,
+    &.Toastify__toast--info,
+    &.Toastify__toast--success,
+    &.Toastify__toast--warning,
+    &.Toastify__toast--error {
+      color: ${themeColor('toastText')};
+    }
+
+    &.Toastify__toast--default,
+    &.Toastify__toast--info {
+      border-color: ${themeColor('toastInfoBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastInfoIconBackground')};
+      }
+    }
+
+    .Toastify__progress-bar--info {
+      background-color: ${themeColor('toastInfoBorder')};
+    }
+
+    &.Toastify__toast--success {
+      border-color: ${themeColor('toastSuccessBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastSuccessIconBackground')};
+      }
+    }
+
+    .Toastify__progress-bar--success {
+      background-color: ${themeColor('toastSuccessBorder')};
+    }
+
+    &.Toastify__toast--warning {
+      border-color: ${themeColor('toastWarningBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastWarningIconBackground')};
+      }
+    }
+
+    .Toastify__progress-bar--warning {
+      background-color: ${themeColor('toastWarningBorder')};
+    }
+
+    &.Toastify__toast--error {
+      border-color: ${themeColor('toastErrorBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastErrorIconBackground')};
+      }
+    }
+
+    .Toastify__progress-bar--error {
+      background-color: ${themeColor('toastErrorBorder')};
+    }
+  }
+
+  .Toastify__toast-theme--colored {
+    .Toastify__progress-bar--default {
+      background-color: ${themeColor('toastInfoBorder')};
+    }
+
+    &.Toastify__toast--info {
+      background-color: ${themeColor('toastInfoBorder')};
+      border-color: ${themeColor('toastInfoBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastInfoBorder')};
+      }
+    }
+
+    .Toastify__progress-bar--info {
+      background-color: ${themeColor('toastInfoIconBackground')};
+    }
+
+    &.Toastify__toast--success {
+      background-color: ${themeColor('toastSuccessBorder')};
+      border-color: ${themeColor('toastSuccessBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastSuccessBorder')};
+      }
+    }
+
+    .Toastify__progress-bar--success {
+      background-color: ${themeColor('toastSuccessIconBackground')};
+    }
+
+    &.Toastify__toast--warning {
+      background-color: ${themeColor('toastWarningBorder')};
+      border-color: ${themeColor('toastWarningBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastWarningBorder')};
+      }
+    }
+
+    .Toastify__progress-bar--warning {
+      background-color: ${themeColor('toastWarningIconBackground')};
+    }
+
+    &.Toastify__toast--error {
+      background-color: ${themeColor('toastErrorBorder')};
+      border-color: ${themeColor('toastErrorBorder')};
+
+      .Toastify__toast-icon {
+        background-color: ${themeColor('toastErrorBorder')};
+      }
+    }
+
+    .Toastify__progress-bar--error {
+      background-color: ${themeColor('toastErrorIconBackground')};
+    }
+  }
+`;
diff --git a/server/sonar-web/design-system/src/components/toast-message/ToastMessageGlobalStyles.tsx b/server/sonar-web/design-system/src/components/toast-message/ToastMessageGlobalStyles.tsx
new file mode 100644 (file)
index 0000000..a7047ed
--- /dev/null
@@ -0,0 +1,679 @@
+/*
+ * 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 { Global, css } from '@emotion/react';
+
+export function ToastMessageGlobalStyles() {
+  return <Global styles={globalStyles()} />;
+}
+
+const globalStyles = () => css`
+  :root {
+    --toastify-color-light: #fff;
+    --toastify-color-dark: #121212;
+    --toastify-color-info: #3498db;
+    --toastify-color-success: #07bc0c;
+    --toastify-color-warning: #f1c40f;
+    --toastify-color-error: #e74c3c;
+    --toastify-color-transparent: rgba(255, 255, 255, 0.7);
+    --toastify-icon-color-info: var(--toastify-color-info);
+    --toastify-icon-color-success: var(--toastify-color-success);
+    --toastify-icon-color-warning: var(--toastify-color-warning);
+    --toastify-icon-color-error: var(--toastify-color-error);
+    --toastify-toast-width: 320px;
+    --toastify-toast-background: #fff;
+    --toastify-toast-min-height: 64px;
+    --toastify-toast-max-height: 800px;
+    --toastify-font-family: sans-serif;
+    --toastify-z-index: 9999;
+    --toastify-text-color-light: #757575;
+    --toastify-text-color-dark: #fff;
+    --toastify-text-color-info: #fff;
+    --toastify-text-color-success: #fff;
+    --toastify-text-color-warning: #fff;
+    --toastify-text-color-error: #fff;
+    --toastify-spinner-color: #616161;
+    --toastify-spinner-color-empty-area: #e0e0e0;
+    --toastify-color-progress-light: linear-gradient(
+      to right,
+      #4cd964,
+      #5ac8fa,
+      #007aff,
+      #34aadc,
+      #5856d6,
+      #ff2d55
+    );
+    --toastify-color-progress-dark: #bb86fc;
+    --toastify-color-progress-info: var(--toastify-color-info);
+    --toastify-color-progress-success: var(--toastify-color-success);
+    --toastify-color-progress-warning: var(--toastify-color-warning);
+    --toastify-color-progress-error: var(--toastify-color-error);
+  }
+
+  .Toastify__toast-container {
+    z-index: var(--toastify-z-index);
+    -webkit-transform: translate3d(0, 0, var(--toastify-z-index) px);
+    position: fixed;
+    padding: 4px;
+    width: var(--toastify-toast-width);
+    box-sizing: border-box;
+    color: #fff;
+  }
+  .Toastify__toast-container--top-left {
+    top: 1em;
+    left: 1em;
+  }
+  .Toastify__toast-container--top-center {
+    top: 1em;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+  .Toastify__toast-container--top-right {
+    top: 1em;
+    right: 1em;
+  }
+  .Toastify__toast-container--bottom-left {
+    bottom: 1em;
+    left: 1em;
+  }
+  .Toastify__toast-container--bottom-center {
+    bottom: 1em;
+    left: 50%;
+    transform: translateX(-50%);
+  }
+  .Toastify__toast-container--bottom-right {
+    bottom: 1em;
+    right: 1em;
+  }
+
+  @media only screen and (max-width: 480px) {
+    .Toastify__toast-container {
+      width: 100vw;
+      padding: 0;
+      left: 0;
+      margin: 0;
+    }
+    .Toastify__toast-container--top-left,
+    .Toastify__toast-container--top-center,
+    .Toastify__toast-container--top-right {
+      top: 0;
+      transform: translateX(0);
+    }
+    .Toastify__toast-container--bottom-left,
+    .Toastify__toast-container--bottom-center,
+    .Toastify__toast-container--bottom-right {
+      bottom: 0;
+      transform: translateX(0);
+    }
+    .Toastify__toast-container--rtl {
+      right: 0;
+      left: initial;
+    }
+  }
+  .Toastify__toast {
+    position: relative;
+    min-height: var(--toastify-toast-min-height);
+    box-sizing: border-box;
+    margin-bottom: 1rem;
+    padding: 8px;
+    border-radius: 4px;
+    box-shadow:
+      0 1px 10px 0 rgba(0, 0, 0, 0.1),
+      0 2px 15px 0 rgba(0, 0, 0, 0.05);
+    display: -ms-flexbox;
+    display: flex;
+    -ms-flex-pack: justify;
+    justify-content: space-between;
+    max-height: var(--toastify-toast-max-height);
+    overflow: hidden;
+    font-family: var(--toastify-font-family);
+    cursor: pointer;
+    direction: ltr;
+  }
+  .Toastify__toast--rtl {
+    direction: rtl;
+  }
+  .Toastify__toast-body {
+    margin: auto 0;
+    -ms-flex: 1 1 auto;
+    flex: 1 1 auto;
+    padding: 6px;
+    display: -ms-flexbox;
+    display: flex;
+    -ms-flex-align: center;
+    align-items: center;
+  }
+  .Toastify__toast-body > div:last-child {
+    -ms-flex: 1;
+    flex: 1;
+  }
+  .Toastify__toast-icon {
+    -webkit-margin-end: 10px;
+    margin-inline-end: 10px;
+    width: 20px;
+    -ms-flex-negative: 0;
+    flex-shrink: 0;
+    display: -ms-flexbox;
+    display: flex;
+  }
+
+  .Toastify--animate {
+    animation-fill-mode: both;
+    animation-duration: 0.7s;
+  }
+
+  .Toastify--animate-icon {
+    animation-fill-mode: both;
+    animation-duration: 0.3s;
+  }
+
+  @media only screen and (max-width: 480px) {
+    .Toastify__toast {
+      margin-bottom: 0;
+      border-radius: 0;
+    }
+  }
+  .Toastify__toast-theme--dark {
+    background: var(--toastify-color-dark);
+    color: var(--toastify-text-color-dark);
+  }
+  .Toastify__toast-theme--light {
+    background: var(--toastify-color-light);
+    color: var(--toastify-text-color-light);
+  }
+  .Toastify__toast-theme--colored.Toastify__toast--default {
+    background: var(--toastify-color-light);
+    color: var(--toastify-text-color-light);
+  }
+  .Toastify__toast-theme--colored.Toastify__toast--info {
+    color: var(--toastify-text-color-info);
+    background: var(--toastify-color-info);
+  }
+  .Toastify__toast-theme--colored.Toastify__toast--success {
+    color: var(--toastify-text-color-success);
+    background: var(--toastify-color-success);
+  }
+  .Toastify__toast-theme--colored.Toastify__toast--warning {
+    color: var(--toastify-text-color-warning);
+    background: var(--toastify-color-warning);
+  }
+  .Toastify__toast-theme--colored.Toastify__toast--error {
+    color: var(--toastify-text-color-error);
+    background: var(--toastify-color-error);
+  }
+
+  .Toastify__progress-bar-theme--light {
+    background: var(--toastify-color-progress-light);
+  }
+  .Toastify__progress-bar-theme--dark {
+    background: var(--toastify-color-progress-dark);
+  }
+  .Toastify__progress-bar--info {
+    background: var(--toastify-color-progress-info);
+  }
+  .Toastify__progress-bar--success {
+    background: var(--toastify-color-progress-success);
+  }
+  .Toastify__progress-bar--warning {
+    background: var(--toastify-color-progress-warning);
+  }
+  .Toastify__progress-bar--error {
+    background: var(--toastify-color-progress-error);
+  }
+  .Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,
+  .Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,
+  .Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,
+  .Toastify__progress-bar-theme--colored.Toastify__progress-bar--error {
+    background: var(--toastify-color-transparent);
+  }
+
+  .Toastify__close-button {
+    color: #fff;
+    background: transparent;
+    outline: none;
+    border: none;
+    padding: 0;
+    cursor: pointer;
+    opacity: 0.7;
+    transition: 0.3s ease;
+    -ms-flex-item-align: start;
+    align-self: flex-start;
+  }
+  .Toastify__close-button--light {
+    color: #000;
+    opacity: 0.3;
+  }
+  .Toastify__close-button > svg {
+    fill: currentColor;
+    height: 16px;
+    width: 14px;
+  }
+  .Toastify__close-button:hover,
+  .Toastify__close-button:focus {
+    opacity: 1;
+  }
+
+  @keyframes Toastify__trackProgress {
+    0% {
+      transform: scaleX(1);
+    }
+    100% {
+      transform: scaleX(0);
+    }
+  }
+  .Toastify__progress-bar {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 5px;
+    z-index: var(--toastify-z-index);
+    opacity: 0.7;
+    transform-origin: left;
+  }
+  .Toastify__progress-bar--animated {
+    animation: Toastify__trackProgress linear 1 forwards;
+  }
+  .Toastify__progress-bar--controlled {
+    transition: transform 0.2s;
+  }
+  .Toastify__progress-bar--rtl {
+    right: 0;
+    left: initial;
+    transform-origin: right;
+  }
+
+  .Toastify__spinner {
+    width: 20px;
+    height: 20px;
+    box-sizing: border-box;
+    border: 2px solid;
+    border-radius: 100%;
+    border-color: var(--toastify-spinner-color-empty-area);
+    border-right-color: var(--toastify-spinner-color);
+    animation: Toastify__spin 0.65s linear infinite;
+  }
+
+  @keyframes Toastify__bounceInRight {
+    from,
+    60%,
+    75%,
+    90%,
+    to {
+      animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+    }
+    from {
+      opacity: 0;
+      transform: translate3d(3000px, 0, 0);
+    }
+    60% {
+      opacity: 1;
+      transform: translate3d(-25px, 0, 0);
+    }
+    75% {
+      transform: translate3d(10px, 0, 0);
+    }
+    90% {
+      transform: translate3d(-5px, 0, 0);
+    }
+    to {
+      transform: none;
+    }
+  }
+  @keyframes Toastify__bounceOutRight {
+    20% {
+      opacity: 1;
+      transform: translate3d(-20px, 0, 0);
+    }
+    to {
+      opacity: 0;
+      transform: translate3d(2000px, 0, 0);
+    }
+  }
+  @keyframes Toastify__bounceInLeft {
+    from,
+    60%,
+    75%,
+    90%,
+    to {
+      animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+    }
+    0% {
+      opacity: 0;
+      transform: translate3d(-3000px, 0, 0);
+    }
+    60% {
+      opacity: 1;
+      transform: translate3d(25px, 0, 0);
+    }
+    75% {
+      transform: translate3d(-10px, 0, 0);
+    }
+    90% {
+      transform: translate3d(5px, 0, 0);
+    }
+    to {
+      transform: none;
+    }
+  }
+  @keyframes Toastify__bounceOutLeft {
+    20% {
+      opacity: 1;
+      transform: translate3d(20px, 0, 0);
+    }
+    to {
+      opacity: 0;
+      transform: translate3d(-2000px, 0, 0);
+    }
+  }
+  @keyframes Toastify__bounceInUp {
+    from,
+    60%,
+    75%,
+    90%,
+    to {
+      animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+    }
+    from {
+      opacity: 0;
+      transform: translate3d(0, 3000px, 0);
+    }
+    60% {
+      opacity: 1;
+      transform: translate3d(0, -20px, 0);
+    }
+    75% {
+      transform: translate3d(0, 10px, 0);
+    }
+    90% {
+      transform: translate3d(0, -5px, 0);
+    }
+    to {
+      transform: translate3d(0, 0, 0);
+    }
+  }
+  @keyframes Toastify__bounceOutUp {
+    20% {
+      transform: translate3d(0, -10px, 0);
+    }
+    40%,
+    45% {
+      opacity: 1;
+      transform: translate3d(0, 20px, 0);
+    }
+    to {
+      opacity: 0;
+      transform: translate3d(0, -2000px, 0);
+    }
+  }
+  @keyframes Toastify__bounceInDown {
+    from,
+    60%,
+    75%,
+    90%,
+    to {
+      animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+    }
+    0% {
+      opacity: 0;
+      transform: translate3d(0, -3000px, 0);
+    }
+    60% {
+      opacity: 1;
+      transform: translate3d(0, 25px, 0);
+    }
+    75% {
+      transform: translate3d(0, -10px, 0);
+    }
+    90% {
+      transform: translate3d(0, 5px, 0);
+    }
+    to {
+      transform: none;
+    }
+  }
+  @keyframes Toastify__bounceOutDown {
+    20% {
+      transform: translate3d(0, 10px, 0);
+    }
+    40%,
+    45% {
+      opacity: 1;
+      transform: translate3d(0, -20px, 0);
+    }
+    to {
+      opacity: 0;
+      transform: translate3d(0, 2000px, 0);
+    }
+  }
+  .Toastify__bounce-enter--top-left,
+  .Toastify__bounce-enter--bottom-left {
+    animation-name: Toastify__bounceInLeft;
+  }
+  .Toastify__bounce-enter--top-right,
+  .Toastify__bounce-enter--bottom-right {
+    animation-name: Toastify__bounceInRight;
+  }
+  .Toastify__bounce-enter--top-center {
+    animation-name: Toastify__bounceInDown;
+  }
+  .Toastify__bounce-enter--bottom-center {
+    animation-name: Toastify__bounceInUp;
+  }
+
+  .Toastify__bounce-exit--top-left,
+  .Toastify__bounce-exit--bottom-left {
+    animation-name: Toastify__bounceOutLeft;
+  }
+  .Toastify__bounce-exit--top-right,
+  .Toastify__bounce-exit--bottom-right {
+    animation-name: Toastify__bounceOutRight;
+  }
+  .Toastify__bounce-exit--top-center {
+    animation-name: Toastify__bounceOutUp;
+  }
+  .Toastify__bounce-exit--bottom-center {
+    animation-name: Toastify__bounceOutDown;
+  }
+
+  @keyframes Toastify__zoomIn {
+    from {
+      opacity: 0;
+      transform: scale3d(0.3, 0.3, 0.3);
+    }
+    50% {
+      opacity: 1;
+    }
+  }
+  @keyframes Toastify__zoomOut {
+    from {
+      opacity: 1;
+    }
+    50% {
+      opacity: 0;
+      transform: scale3d(0.3, 0.3, 0.3);
+    }
+    to {
+      opacity: 0;
+    }
+  }
+  .Toastify__zoom-enter {
+    animation-name: Toastify__zoomIn;
+  }
+
+  .Toastify__zoom-exit {
+    animation-name: Toastify__zoomOut;
+  }
+
+  @keyframes Toastify__flipIn {
+    from {
+      transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+      animation-timing-function: ease-in;
+      opacity: 0;
+    }
+    40% {
+      transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+      animation-timing-function: ease-in;
+    }
+    60% {
+      transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
+      opacity: 1;
+    }
+    80% {
+      transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
+    }
+    to {
+      transform: perspective(400px);
+    }
+  }
+  @keyframes Toastify__flipOut {
+    from {
+      transform: perspective(400px);
+    }
+    30% {
+      transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
+      opacity: 1;
+    }
+    to {
+      transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
+      opacity: 0;
+    }
+  }
+  .Toastify__flip-enter {
+    animation-name: Toastify__flipIn;
+  }
+
+  .Toastify__flip-exit {
+    animation-name: Toastify__flipOut;
+  }
+
+  @keyframes Toastify__slideInRight {
+    from {
+      transform: translate3d(110%, 0, 0);
+      visibility: visible;
+    }
+    to {
+      transform: translate3d(0, 0, 0);
+    }
+  }
+  @keyframes Toastify__slideInLeft {
+    from {
+      transform: translate3d(-110%, 0, 0);
+      visibility: visible;
+    }
+    to {
+      transform: translate3d(0, 0, 0);
+    }
+  }
+  @keyframes Toastify__slideInUp {
+    from {
+      transform: translate3d(0, 110%, 0);
+      visibility: visible;
+    }
+    to {
+      transform: translate3d(0, 0, 0);
+    }
+  }
+  @keyframes Toastify__slideInDown {
+    from {
+      transform: translate3d(0, -110%, 0);
+      visibility: visible;
+    }
+    to {
+      transform: translate3d(0, 0, 0);
+    }
+  }
+  @keyframes Toastify__slideOutRight {
+    from {
+      transform: translate3d(0, 0, 0);
+    }
+    to {
+      visibility: hidden;
+      transform: translate3d(110%, 0, 0);
+    }
+  }
+  @keyframes Toastify__slideOutLeft {
+    from {
+      transform: translate3d(0, 0, 0);
+    }
+    to {
+      visibility: hidden;
+      transform: translate3d(-110%, 0, 0);
+    }
+  }
+  @keyframes Toastify__slideOutDown {
+    from {
+      transform: translate3d(0, 0, 0);
+    }
+    to {
+      visibility: hidden;
+      transform: translate3d(0, 500px, 0);
+    }
+  }
+  @keyframes Toastify__slideOutUp {
+    from {
+      transform: translate3d(0, 0, 0);
+    }
+    to {
+      visibility: hidden;
+      transform: translate3d(0, -500px, 0);
+    }
+  }
+  .Toastify__slide-enter--top-left,
+  .Toastify__slide-enter--bottom-left {
+    animation-name: Toastify__slideInLeft;
+  }
+  .Toastify__slide-enter--top-right,
+  .Toastify__slide-enter--bottom-right {
+    animation-name: Toastify__slideInRight;
+  }
+  .Toastify__slide-enter--top-center {
+    animation-name: Toastify__slideInDown;
+  }
+  .Toastify__slide-enter--bottom-center {
+    animation-name: Toastify__slideInUp;
+  }
+
+  .Toastify__slide-exit--top-left,
+  .Toastify__slide-exit--bottom-left {
+    animation-name: Toastify__slideOutLeft;
+  }
+  .Toastify__slide-exit--top-right,
+  .Toastify__slide-exit--bottom-right {
+    animation-name: Toastify__slideOutRight;
+  }
+  .Toastify__slide-exit--top-center {
+    animation-name: Toastify__slideOutUp;
+  }
+  .Toastify__slide-exit--bottom-center {
+    animation-name: Toastify__slideOutDown;
+  }
+
+  @keyframes Toastify__spin {
+    from {
+      transform: rotate(0deg);
+    }
+    to {
+      transform: rotate(360deg);
+    }
+  }
+
+  /*# sourceMappingURL=ReactToastify.css.map */
+`;
diff --git a/server/sonar-web/design-system/src/components/toast-message/__tests__/ToastMessage-test.tsx b/server/sonar-web/design-system/src/components/toast-message/__tests__/ToastMessage-test.tsx
new file mode 100644 (file)
index 0000000..b54d6f4
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * 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 { ToastContainer } from 'react-toastify';
+import { TOAST_AUTOCLOSE_DELAY } from '../../../helpers/constants';
+import { render } from '../../../helpers/testUtils';
+import { ToastMessageContainer } from '../ToastMessage';
+
+jest.mock('react-toastify', () => ({
+  Slide: 'mock slide',
+  ToastContainer: jest.fn(() => null),
+  toast: { POSITION: { TOP_RIGHT: 'mock top right' } },
+}));
+
+it('should render the ToastMessageContainer', () => {
+  setupWithProps();
+
+  expect(ToastContainer).toHaveBeenCalledWith(
+    {
+      autoClose: TOAST_AUTOCLOSE_DELAY,
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+      className: expect.any(String),
+      closeOnClick: true,
+      draggable: true,
+      hideProgressBar: true,
+      limit: 0,
+      newestOnTop: false,
+      pauseOnFocusLoss: true,
+      pauseOnHover: true,
+      position: 'mock top right',
+      rtl: false,
+      transition: 'mock slide',
+    },
+    {},
+  );
+});
+
+function setupWithProps() {
+  return render(<ToastMessageContainer />);
+}
diff --git a/server/sonar-web/design-system/src/components/toast-message/__tests__/toast-utils-test.tsx b/server/sonar-web/design-system/src/components/toast-message/__tests__/toast-utils-test.tsx
new file mode 100644 (file)
index 0000000..cbf505d
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * 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 { toast, ToastPosition } from 'react-toastify';
+import { FlagErrorIcon, FlagSuccessIcon } from '../../icons';
+import {
+  addGlobalErrorMessage,
+  addGlobalSuccessMessage,
+  dismissAllGlobalMessages,
+} from '../toast-utils';
+
+jest.mock('react-toastify', () => ({
+  toast: jest.fn(),
+}));
+
+it('should call react-toastify with the right args', () => {
+  const POSITION = { TOP_LEFT: 'top-left', TOP_RIGHT: 'top-right' };
+
+  toast.POSITION = POSITION as typeof toast.POSITION;
+
+  addGlobalErrorMessage(<span>error</span>, { position: POSITION.TOP_LEFT as ToastPosition });
+
+  expect(toast).toHaveBeenCalledWith(
+    <div className="fs-mask sw-body-sm sw-p-3 sw-pb-4" data-test="global-message__ERROR">
+      <span>error</span>
+    </div>,
+    { icon: <FlagErrorIcon />, type: 'error', position: POSITION.TOP_LEFT },
+  );
+
+  addGlobalSuccessMessage('it worked');
+
+  expect(toast).toHaveBeenCalledWith(
+    <div className="fs-mask sw-body-sm sw-p-3 sw-pb-4" data-test="global-message__SUCCESS">
+      it worked
+    </div>,
+    { icon: <FlagSuccessIcon />, type: 'success' },
+  );
+
+  toast.dismiss = jest.fn();
+
+  dismissAllGlobalMessages();
+
+  expect(toast.dismiss).toHaveBeenCalled();
+});
diff --git a/server/sonar-web/design-system/src/components/toast-message/toast-utils.tsx b/server/sonar-web/design-system/src/components/toast-message/toast-utils.tsx
new file mode 100644 (file)
index 0000000..fe6d6a4
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * 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 { ReactNode } from 'react';
+import { ToastOptions, toast } from 'react-toastify';
+import { FlagErrorIcon, FlagSuccessIcon } from '../icons';
+
+export interface Message {
+  level: MessageLevel;
+  message: string;
+}
+
+export enum MessageLevel {
+  Error = 'ERROR',
+  Success = 'SUCCESS',
+}
+
+export function addGlobalErrorMessage(message: ReactNode, overrides?: ToastOptions) {
+  return createToast(message, MessageLevel.Error, overrides);
+}
+
+export function addGlobalSuccessMessage(message: ReactNode, overrides?: ToastOptions) {
+  return createToast(message, MessageLevel.Success, overrides);
+}
+
+export function dismissAllGlobalMessages() {
+  toast.dismiss();
+}
+
+function createToast(message: ReactNode, level: MessageLevel, overrides?: ToastOptions) {
+  return toast(
+    <div className="fs-mask sw-body-sm sw-p-3 sw-pb-4" data-test={`global-message__${level}`}>
+      {message}
+    </div>,
+    {
+      icon: level === MessageLevel.Error ? <FlagErrorIcon /> : <FlagSuccessIcon />,
+      type: level === MessageLevel.Error ? 'error' : 'success',
+      ...overrides,
+    },
+  );
+}
index ce1c4080f0ea07e46ed263e88c3d89c857b64264..8c332f82c73388a001a54969e7b83c8fe182ae4e 100644 (file)
@@ -76,3 +76,5 @@ export const OPACITY_20_PERCENT = 0.2;
 export const OPACITY_75_PERCENT = 0.75;
 
 export const GLOBAL_POPUP_Z_INDEX = 5000;
+
+export const TOAST_AUTOCLOSE_DELAY = 5000;
index 2c14207689d491e0ac972141f17ce06ca8f03de4..c2484f63d142317694558765836b7ac3888250ca 100644 (file)
@@ -109,6 +109,23 @@ export const lightTheme = {
     popup: COLORS.white,
     popupBorder: secondary.default,
 
+    // Toasts
+    toast: COLORS.white,
+    toastText: secondary.darker,
+    toastCloseIcon: secondary.dark,
+
+    toastErrorBorder: danger.light,
+    toastErrorIconBackground: danger.lightest,
+
+    toastWarningBorder: COLORS.yellow[400],
+    toastWarningIconBackground: COLORS.yellow[50],
+
+    toastSuccessBorder: COLORS.yellowGreen[400],
+    toastSuccessIconBackground: COLORS.yellowGreen[50],
+
+    toastInfoBorder: COLORS.blue[400],
+    toastInfoIconBackground: COLORS.blue[50],
+
     // spotlight
     spotlightPulseBackground: primary.default,
     spotlightBackgroundColor: COLORS.blueGrey[50],
index 78cad5d94103a1a68bb67ee893149ef935cd2f17..3966b35b46a1816826b24e1b61b974da57746565 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { ThemeProvider } from '@emotion/react';
 import classNames from 'classnames';
-import { lightTheme } from 'design-system';
+import { lightTheme, ToastMessageContainer } from 'design-system';
 import * as React from 'react';
 import { Outlet, useLocation } from 'react-router-dom';
 import A11yProvider from '../../components/a11y/A11yProvider';
@@ -28,14 +28,14 @@ import SuggestionsProvider from '../../components/embed-docs-modal/SuggestionsPr
 import NCDAutoUpdateMessage from '../../components/new-code-definition/NCDAutoUpdateMessage';
 import Workspace from '../../components/workspace/Workspace';
 import GlobalFooter from './GlobalFooter';
-import StartupModal from './StartupModal';
-import SystemAnnouncement from './SystemAnnouncement';
 import IndexationContextProvider from './indexation/IndexationContextProvider';
 import IndexationNotification from './indexation/IndexationNotification';
 import LanguagesContextProvider from './languages/LanguagesContextProvider';
 import MetricsContextProvider from './metrics/MetricsContextProvider';
 import GlobalNav from './nav/global/GlobalNav';
 import PromotionNotification from './promotion-notification/PromotionNotification';
+import StartupModal from './StartupModal';
+import SystemAnnouncement from './SystemAnnouncement';
 import UpdateNotification from './update-notification/UpdateNotification';
 
 const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [
@@ -85,6 +85,7 @@ export default function GlobalContainer() {
             >
               <div className="page-container">
                 <Workspace>
+                  <ToastMessageContainer />
                   <IndexationContextProvider>
                     <LanguagesContextProvider>
                       <MetricsContextProvider>
index 2c2a3faa56d2d9ba6986f3fe398b7c22c70c8529..3e89755df695578f0d3ef9f6dc76dfc058fe7388 100644 (file)
@@ -25,9 +25,12 @@ import {
   ItemButton,
   PopupPlacement,
   PopupZLevel,
+  addGlobalErrorMessage,
+  addGlobalSuccessMessage,
 } from 'design-system';
 import * as React from 'react';
-import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../helpers/globalMessages';
+import { FormattedMessage } from 'react-intl';
+import DocumentationLink from '../../../components/common/DocumentationLink';
 import { translate } from '../../../helpers/l10n';
 import { openIssue as openSonarLintIssue, probeSonarLintServers } from '../../../helpers/sonarlint';
 import { Ide } from '../../../types/sonarlint';
@@ -43,7 +46,19 @@ interface State {
   mounted: boolean;
 }
 
-const showError = () => addGlobalErrorMessage(translate('issues.open_in_ide.failure'));
+const showError = () =>
+  addGlobalErrorMessage(
+    <FormattedMessage
+      id="issues.open_in_ide.failure"
+      values={{
+        link: (
+          <DocumentationLink to="user-guide/sonarlint-connected-mode/">
+            {translate('sonarlint-connected-mode-doc')}
+          </DocumentationLink>
+        ),
+      }}
+    />,
+  );
 
 const showSuccess = () => addGlobalSuccessMessage(translate('issues.open_in_ide.success'));
 
index f092b21b4d8ec6d80967c94d62ccc3841c5a0279..adda8abf21af49f56580e3f989d0904e7071f3e9 100644 (file)
 
 import { act, screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
+import { addGlobalErrorMessage, addGlobalSuccessMessage } from 'design-system';
 import * as React from 'react';
-import { addGlobalErrorMessage, addGlobalSuccessMessage } from '../../../../helpers/globalMessages';
+import { FormattedMessage } from 'react-intl';
+import DocumentationLink from '../../../../components/common/DocumentationLink';
 import {
   openIssue as openSonarLintIssue,
   probeSonarLintServers,
@@ -34,7 +36,8 @@ jest.mock('../../../../helpers/sonarlint', () => ({
   probeSonarLintServers: jest.fn(),
 }));
 
-jest.mock('../../../../helpers/globalMessages', () => ({
+jest.mock('design-system', () => ({
+  ...jest.requireActual('design-system'),
   addGlobalErrorMessage: jest.fn(),
   addGlobalSuccessMessage: jest.fn(),
 }));
@@ -76,7 +79,18 @@ it('handles button click with no ide found', async () => {
 
   expect(probeSonarLintServers).toHaveBeenCalledWith();
 
-  expect(addGlobalErrorMessage).toHaveBeenCalledWith('issues.open_in_ide.failure');
+  expect(addGlobalErrorMessage).toHaveBeenCalledWith(
+    <FormattedMessage
+      id="issues.open_in_ide.failure"
+      values={{
+        link: (
+          <DocumentationLink to="user-guide/sonarlint-connected-mode/">
+            sonarlint-connected-mode-doc
+          </DocumentationLink>
+        ),
+      }}
+    />,
+  );
 
   expect(openSonarLintIssue).not.toHaveBeenCalled();
   expect(addGlobalSuccessMessage).not.toHaveBeenCalled();
index c4a23833b5dbf5a4391a486937e73e1a0877b73b..f9309a5d3d8286b6b954ab32e0759e7f99039c97 100644 (file)
@@ -6814,6 +6814,7 @@ __metadata:
     postcss: 8.4.29
     postcss-calc: 9.0.1
     postcss-custom-properties: 12.1.11
+    react-toastify: 8.2.0
     twin.macro: 3.4.0
     typescript: 5.2.2
     vite: 4.4.9
@@ -11801,6 +11802,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"react-toastify@npm:8.2.0":
+  version: 8.2.0
+  resolution: "react-toastify@npm:8.2.0"
+  dependencies:
+    clsx: ^1.1.1
+  peerDependencies:
+    react: ">=16"
+    react-dom: ">=16"
+  checksum: 670f1176fb9fd247c7ce0cad22e72578cbfa356dd1920a810ec4aa19faa4dab16db9efbdaaf761bdd368c4fad5c3d7f15568d5328b7b3c8699539064e88fcd4b
+  languageName: node
+  linkType: hard
+
 "react-transition-group@npm:^4.3.0":
   version: 4.4.2
   resolution: "react-transition-group@npm:4.4.2"
index 90183ac2b36c001c2bfb80e513dba9489e88ff71..53db5a185eeb7163208a5476b6d7a3a581dd75ff 100644 (file)
@@ -1095,7 +1095,7 @@ issues.not_all_issue_show=Not all issues are included
 issues.not_all_issue_show_why=You do not have access to all projects in this portfolio
 
 issues.open_in_ide.success=Success. Switch to your IDE to see the issue.
-issues.open_in_ide.failure=Unable to open the issue in the IDE. Please check the documentation about SonarLint Connected Mode.
+issues.open_in_ide.failure=Unable to open the issue in the IDE. Please check the {link}.
 
 issue.activity.review_history.created=Created Issue
 issue.activity.review_history.comment_added=added a comment
@@ -3326,6 +3326,7 @@ sonarlint-connection.success.step=Go back to your IDE to complete the setup.
 
 sonarlint-connection.unspecified-ide=an unspecified IDE
 
+sonarlint-connected-mode-doc=documentation about SonarLint Connected Mode
 #------------------------------------------------------------------------------
 #
 # HELP