From: David Cho-Lerat Date: Tue, 31 Oct 2023 10:49:01 +0000 (+0100) Subject: SONAR-20513 Use new ToastMessage for Open issues in IDE success/error messages X-Git-Tag: 10.3.0.82913~67 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=35dc4d8749aef166aa3b81ecf868f3589ab5421c;p=sonarqube.git SONAR-20513 Use new ToastMessage for Open issues in IDE success/error messages --- diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json index 3b34f44e7c2..ffb94d58ca8 100644 --- a/server/sonar-web/design-system/package.json +++ b/server/sonar-web/design-system/package.json @@ -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" } } diff --git a/server/sonar-web/design-system/src/components/index.ts b/server/sonar-web/design-system/src/components/index.ts index 4e103229287..180db1f3da7 100644 --- a/server/sonar-web/design-system/src/components/index.ts +++ b/server/sonar-web/design-system/src/components/index.ts @@ -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 index 00000000000..bbd4501f5bc --- /dev/null +++ b/server/sonar-web/design-system/src/components/toast-message/ToastMessage.tsx @@ -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 & React.RefAttributes, +) { + return ; +} + +export function ToastMessageContainer() { + return ( + <> + + + + + ); +} + +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 index 00000000000..a7047ed91cd --- /dev/null +++ b/server/sonar-web/design-system/src/components/toast-message/ToastMessageGlobalStyles.tsx @@ -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 ; +} + +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 index 00000000000..b54d6f40862 --- /dev/null +++ b/server/sonar-web/design-system/src/components/toast-message/__tests__/ToastMessage-test.tsx @@ -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(); +} 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 index 00000000000..cbf505db181 --- /dev/null +++ b/server/sonar-web/design-system/src/components/toast-message/__tests__/toast-utils-test.tsx @@ -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(error, { position: POSITION.TOP_LEFT as ToastPosition }); + + expect(toast).toHaveBeenCalledWith( +
+ error +
, + { icon: , type: 'error', position: POSITION.TOP_LEFT }, + ); + + addGlobalSuccessMessage('it worked'); + + expect(toast).toHaveBeenCalledWith( +
+ it worked +
, + { icon: , 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 index 00000000000..fe6d6a463ca --- /dev/null +++ b/server/sonar-web/design-system/src/components/toast-message/toast-utils.tsx @@ -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( +
+ {message} +
, + { + icon: level === MessageLevel.Error ? : , + type: level === MessageLevel.Error ? 'error' : 'success', + ...overrides, + }, + ); +} diff --git a/server/sonar-web/design-system/src/helpers/constants.ts b/server/sonar-web/design-system/src/helpers/constants.ts index ce1c4080f0e..8c332f82c73 100644 --- a/server/sonar-web/design-system/src/helpers/constants.ts +++ b/server/sonar-web/design-system/src/helpers/constants.ts @@ -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; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index 2c14207689d..c2484f63d14 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -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], diff --git a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx index 78cad5d9410..3966b35b46a 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -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() { >
+ diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx index 2c2a3faa56d..3e89755df69 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueOpenInIdeButton.tsx @@ -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( + + {translate('sonarlint-connected-mode-doc')} + + ), + }} + />, + ); const showSuccess = () => addGlobalSuccessMessage(translate('issues.open_in_ide.success')); diff --git a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx index f092b21b4d8..adda8abf21a 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/__tests__/IssueOpenInIdeButton-test.tsx @@ -20,8 +20,10 @@ 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( + + sonarlint-connected-mode-doc + + ), + }} + />, + ); expect(openSonarLintIssue).not.toHaveBeenCalled(); expect(addGlobalSuccessMessage).not.toHaveBeenCalled(); diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index c4a23833b5d..f9309a5d3d8 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -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" diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 90183ac2b36..53db5a185ee 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -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