"react-helmet-async": "1.3.0",
"react-highlight-words": "0.20.0",
"react-intl": "6.4.4",
+ "react-joyride": "2.5.5",
"react-modal": "3.16.1",
"react-router-dom": "6.11.2",
"react-select": "5.7.3",
--- /dev/null
+/*
+ * 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 { Suspense } from 'react';
+import ReactJoyride, { Props as JoyrideProps, TooltipRenderProps } from 'react-joyride';
+import tw from 'twin.macro';
+import { PopupZLevel } from '../helpers';
+import { Spinner } from './DeferredSpinner';
+import { ButtonPrimary, ButtonSecondary, WrapperButton } from './buttons';
+import { CloseIcon } from './icons';
+import { PopupWrapper } from './popups';
+
+type Props = JoyrideProps;
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
+(window as any).global = (window as any).global ?? {};
+
+const Popup = styled(PopupWrapper)`
+ position: relative;
+ width: 300px;
+ border: none;
+ ${tw`sw-body-sm`}
+ ${tw`sw-p-3`}
+`;
+
+function TooltipComponent({
+ continuous,
+ index,
+ step,
+ size,
+ isLastStep,
+ backProps,
+ skipProps,
+ closeProps,
+ primaryProps,
+ tooltipProps,
+}: TooltipRenderProps) {
+ return (
+ <Popup zLevel={PopupZLevel.Absolute} {...tooltipProps}>
+ <div className="sw-flex sw-justify-between">
+ <b className="sw-mb-2">{step.title}</b>
+ <WrapperButton
+ className="sw-w-[30px] sw-h-[30px] sw--mt-2 sw--mr-2 sw-flex sw-justify-center"
+ {...skipProps}
+ >
+ <CloseIcon className="sw-mr-0" />
+ </WrapperButton>
+ </div>
+ <div>{step.content}</div>
+ <div className="sw-flex sw-justify-between sw-items-center sw-mt-3">
+ <b>
+ {index + 1} of {size}
+ </b>
+ <div>
+ {index > 0 && (
+ <ButtonSecondary className="sw-mr-2" {...backProps}>
+ {backProps.title}
+ </ButtonSecondary>
+ )}
+ {continuous && !isLastStep && (
+ <ButtonPrimary {...primaryProps}>{primaryProps.title}</ButtonPrimary>
+ )}
+ {(!continuous || isLastStep) && (
+ <ButtonPrimary {...closeProps}>{closeProps.title}</ButtonPrimary>
+ )}
+ </div>
+ </div>
+ </Popup>
+ );
+}
+
+export function Guide(props: Props) {
+ return (
+ <Suspense fallback={<Spinner />}>
+ <ReactJoyride
+ scrollDuration={0}
+ scrollOffset={250}
+ tooltipComponent={TooltipComponent}
+ {...props}
+ />
+ </Suspense>
+ );
+}
icon?: React.ReactNode;
innerRef?: React.Ref<HTMLButtonElement>;
isExternal?: LinkProps['isExternal'];
- onClick?: (event?: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => unknown;
+ onClick?: (event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>) => unknown;
preventDefault?: boolean;
reloadDocument?: LinkProps['reloadDocument'];
export { FlagMessage } from './FlagMessage';
export * from './FlowStep';
export * from './GenericAvatar';
+export * from './Guide';
export * from './HighlightedSection';
export { Histogram } from './Histogram';
export { HotspotRating } from './HotspotRating';
}
}
-const PopupWrapper = styled.div<{ zLevel: PopupZLevel }>`
+export const PopupWrapper = styled.div<{ zLevel: PopupZLevel }>`
position: ${({ zLevel }) => (zLevel === PopupZLevel.Global ? 'fixed' : 'absolute')};
background-color: ${themeColor('popup')};
color: ${themeContrast('popup')};
"react-helmet-async": "1.3.0",
"react-highlight-words": "0.20.0",
"react-intl": "6.4.4",
+ "react-joyride": "2.5.5",
"react-modal": "3.16.1",
"react-router-dom": "6.11.2",
"react-select": "5.7.3",
--- /dev/null
+/*
+ * 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.
+ */
+
+export default class LocalStorageMock {
+ store: Record<string, string>;
+
+ constructor() {
+ this.store = {};
+ }
+
+ clear() {
+ this.store = {};
+ }
+
+ getItem(key: string) {
+ return this.store[key] || null;
+ }
+
+ setItem(key: string, value: string) {
+ this.store[key] = String(value);
+ }
+
+ removeItem(key: string) {
+ delete this.store[key];
+ }
+
+ get length() {
+ return Object.keys(this.store).length;
+ }
+
+ key(index: number) {
+ return Object.keys(this.store)[index] || null;
+ }
+}
--- /dev/null
+/*
+ * 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 { Guide } from 'design-system';
+import React, { useEffect } from 'react';
+import { FormattedMessage } from 'react-intl';
+import { dismissNotice } from '../../../api/users';
+import { CurrentUserContext } from '../../../app/components/current-user/CurrentUserContext';
+import DocLink from '../../../components/common/DocLink';
+import { translate } from '../../../helpers/l10n';
+import { NoticeType } from '../../../types/users';
+
+interface Props {
+ run?: boolean;
+}
+
+export default function IssueListGuide({ run }: Props) {
+ const { currentUser } = React.useContext(CurrentUserContext);
+ const hasLocalStorageValue = localStorage.getItem(NoticeType.ISSUE_GUIDE) === 'true';
+
+ useEffect(() => {
+ if (!currentUser.dismissedNotices.IssueListGuiding && hasLocalStorageValue) {
+ dismissNotice(NoticeType.ISSUE_GUIDE);
+ }
+ }, [currentUser, hasLocalStorageValue]);
+
+ if (hasLocalStorageValue || currentUser.dismissedNotices.IssueListGuiding) {
+ return null;
+ }
+
+ const onToggle = (props: { action: string }) => {
+ if (props.action === 'reset') {
+ if (currentUser.isLoggedIn) {
+ dismissNotice(NoticeType.ISSUE_GUIDE);
+ } else {
+ localStorage.setItem(NoticeType.ISSUE_GUIDE, 'true');
+ }
+ }
+ };
+
+ const constructContent = (
+ first: string,
+ second: string,
+ extraContent?: string | React.ReactNode
+ ) => (
+ <>
+ <span>{translate(first)}</span>
+ <br />
+ <br />
+ <span>{translate(second)}</span>
+ {extraContent ?? null}
+ </>
+ );
+
+ const steps = [
+ {
+ target: '[data-guiding-id="issuelist-1"]',
+ content: constructContent('guiding.issue_list.1.content.1', 'guiding.issue_list.1.content.2'),
+ title: translate('guiding.issue_list.1.title'),
+ placement: 'right' as const,
+ disableBeacon: true,
+ floaterProps: {
+ disableAnimation: true,
+ },
+ },
+ {
+ target: '[data-guiding-id="issuelist-2"]',
+ content: constructContent('guiding.issue_list.2.content.1', 'guiding.issue_list.2.content.2'),
+ title: translate('guiding.issue_list.2.title'),
+ placement: 'right' as const,
+ disableBeacon: true,
+ floaterProps: {
+ disableAnimation: true,
+ },
+ },
+ {
+ target: '[data-guiding-id="issuelist-3"]',
+ content: constructContent('guiding.issue_list.2.content.1', 'guiding.issue_list.2.content.2'),
+ title: translate('guiding.issue_list.3.title'),
+ placement: 'right' as const,
+ disableBeacon: true,
+ floaterProps: {
+ disableAnimation: true,
+ },
+ },
+ {
+ target: '[data-guiding-id="issuelist-4"]',
+ content: constructContent(
+ 'guiding.issue_list.4.content.1',
+ 'guiding.issue_list.4.content.2',
+ <ul className="sw-mt-2 sw-pl-5 sw-list-disc">
+ <li>{translate('guiding.issue_list.4.content.list.1')}</li>
+ <li>{translate('guiding.issue_list.4.content.list.2')}</li>
+ <li>{translate('guiding.issue_list.4.content.list.3')}</li>
+ </ul>
+ ),
+ title: translate('guiding.issue_list.4.title'),
+ disableScrolling: true,
+ disableBeacon: true,
+ floaterProps: {
+ disableAnimation: true,
+ },
+ },
+ {
+ target: 'body',
+ content: (
+ <FormattedMessage
+ id="guiding.issue_list.5.content"
+ defaultMessage={translate('guiding.issue_list.5.content')}
+ values={{
+ link: (
+ <DocLink to="/user-guide/clean-code" className="sw-capitalize">
+ {translate('documentation')}
+ </DocLink>
+ ),
+ }}
+ />
+ ),
+ title: translate('guiding.issue_list.5.title'),
+ placement: 'center' as const,
+ disableBeacon: true,
+ floaterProps: {
+ disableAnimation: true,
+ },
+ },
+ ];
+
+ return <Guide callback={onToggle} steps={steps} run={run} continuous />;
+}
shouldOpenStandardsFacet,
} from '../utils';
import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
+import IssueListGuide from './IssueListGuide';
import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments';
import IssuesList from './IssuesList';
import IssuesSourceViewer from './IssuesSourceViewer';
render() {
const { openIssue } = this.state;
+ const { component, location } = this.props;
+ const open = getOpen(location.query);
return (
<PageWrapperStyle id="issues-page">
<PageContentFontWrapper className="sw-body-sm">
<div className="sw-w-full sw-flex" id="issues-page">
<Suggestions suggestions="issues" />
+ <IssueListGuide run={!open && !component?.needIssueSync} />
{openIssue ? (
<Helmet
const { categories = [], ...rest } = props;
return (
- <SimpleListStyleFacet
- property="cleanCodeAttributeCategory"
- itemNamePrefix="issue.clean_code_attribute_category"
- listItems={CATEGORIES}
- selectedItems={categories}
- {...rest}
- />
+ <div data-guiding-id="issuelist-1">
+ <SimpleListStyleFacet
+ property="cleanCodeAttributeCategory"
+ itemNamePrefix="issue.clean_code_attribute_category"
+ listItems={CATEGORIES}
+ selectedItems={categories}
+ {...rest}
+ />
+ </div>
);
}
const { qualities = [], ...rest } = props;
return (
- <SimpleListStyleFacet
- property="impactSoftwareQuality"
- itemNamePrefix="issue.software_quality"
- listItems={QUALITIES}
- selectedItems={qualities}
- {...rest}
- />
+ <div data-guiding-id="issuelist-2">
+ <SimpleListStyleFacet
+ property="impactSoftwareQuality"
+ itemNamePrefix="issue.software_quality"
+ listItems={QUALITIES}
+ selectedItems={qualities}
+ {...rest}
+ />
+ </div>
);
}
ruleFacetList: byRole('list', { name: 'issues.facet.rules' }),
ruleFacetSearch: byPlaceholderText('search.search_for_rules'),
tagFacetSearch: byPlaceholderText('search.search_for_tags'),
+
+ guidePopup: byRole('alertdialog'),
};
export async function waitOnDataLoaded() {
export enum NoticeType {
EDUCATION_PRINCIPLES = 'educationPrinciples',
SONARLINT_AD = 'sonarlintAd',
+ ISSUE_GUIDE = 'issueCleanCodeGuide',
}
export interface LoggedInUser extends CurrentUser, UserActive {
languageName: node
linkType: hard
+"@gilbarbara/deep-equal@npm:^0.1.1":
+ version: 0.1.2
+ resolution: "@gilbarbara/deep-equal@npm:0.1.2"
+ checksum: 78d4e76d36cbee639c008a63be52c1ac803212ff2560e55f68d2b8b2a6ac5e746c1976854cf101483ca18a9911aed2349da147b7756be43e75efb95e3f24468b
+ languageName: node
+ linkType: hard
+
"@humanwhocodes/config-array@npm:^0.11.10":
version: 0.11.10
resolution: "@humanwhocodes/config-array@npm:0.11.10"
react-helmet-async: 1.3.0
react-highlight-words: 0.20.0
react-intl: 6.4.4
+ react-joyride: 2.5.5
react-modal: 3.16.1
react-router-dom: 6.11.2
react-select: 5.7.3
languageName: node
linkType: hard
+"deepmerge@npm:^4.3.1":
+ version: 4.3.1
+ resolution: "deepmerge@npm:4.3.1"
+ checksum: 2024c6a980a1b7128084170c4cf56b0fd58a63f2da1660dcfe977415f27b17dbe5888668b59d0b063753f3220719d5e400b7f113609489c90160bb9a5518d052
+ languageName: node
+ linkType: hard
+
"define-properties@npm:^1.1.2, define-properties@npm:^1.1.3":
version: 1.1.3
resolution: "define-properties@npm:1.1.3"
react-helmet-async: 1.3.0
react-highlight-words: 0.20.0
react-intl: 6.4.4
+ react-joyride: 2.5.5
react-modal: 3.16.1
react-router-dom: 6.11.2
react-select: 5.7.3
languageName: node
linkType: hard
-"exenv@npm:^1.2.0":
+"exenv@npm:^1.2.0, exenv@npm:^1.2.2":
version: 1.2.2
resolution: "exenv@npm:1.2.2"
checksum: a894f3b60ab8419e0b6eec99c690a009c8276b4c90655ccaf7d28faba2de3a6b93b3d92210f9dc5efd36058d44f04098f6bbccef99859151104bfd49939904dc
languageName: node
linkType: hard
+"is-lite@npm:^0.8.2":
+ version: 0.8.2
+ resolution: "is-lite@npm:0.8.2"
+ checksum: 0ee62cb238c2a044f58d1cd139fb0b48026c407ec8625ee6572b417f164e17ec937f0a0785f466e320749a796c316a3b78dcb4b520f7ddd4b9de38ad5a23d70f
+ languageName: node
+ linkType: hard
+
+"is-lite@npm:^0.9.2":
+ version: 0.9.2
+ resolution: "is-lite@npm:0.9.2"
+ checksum: 8c4d2c58cf99a8289715925c0c3175dadf63e5ad293ad395ce650430ce90afe533a84ad0ffdeed0ce277dabdae63acdd3dac5d9b629bd61c3c0c620e2376f26e
+ languageName: node
+ linkType: hard
+
"is-map@npm:^2.0.1, is-map@npm:^2.0.2":
version: 2.0.2
resolution: "is-map@npm:2.0.2"
languageName: node
linkType: hard
+"popper.js@npm:^1.16.0":
+ version: 1.16.1
+ resolution: "popper.js@npm:1.16.1"
+ checksum: c56ae5001ec50a77ee297a8061a0221d99d25c7348d2e6bcd3e45a0d0f32a1fd81bca29d46cb0d4bdf13efb77685bd6a0ce93f9eb3c608311a461f945fffedbe
+ languageName: node
+ linkType: hard
+
"postcss-calc@npm:9.0.1":
version: 9.0.1
resolution: "postcss-calc@npm:9.0.1"
languageName: node
linkType: hard
+"react-floater@npm:^0.7.6":
+ version: 0.7.6
+ resolution: "react-floater@npm:0.7.6"
+ dependencies:
+ deepmerge: ^4.2.2
+ exenv: ^1.2.2
+ is-lite: ^0.8.2
+ popper.js: ^1.16.0
+ prop-types: ^15.8.1
+ react-proptype-conditional-require: ^1.0.4
+ tree-changes: ^0.9.1
+ peerDependencies:
+ react: 15 - 18
+ react-dom: 15 - 18
+ checksum: 8268e14fbdf9393b39300f3c90ea2de382782f1d959176579e30841095a73f9240ec05fce3ec89e8b4e58cbc46c9b043b2ad0b338f5c5d3b91168b16a4282ac1
+ languageName: node
+ linkType: hard
+
"react-helmet-async@npm:1.3.0":
version: 1.3.0
resolution: "react-helmet-async@npm:1.3.0"
languageName: node
linkType: hard
+"react-joyride@npm:2.5.5":
+ version: 2.5.5
+ resolution: "react-joyride@npm:2.5.5"
+ dependencies:
+ deepmerge: ^4.3.1
+ exenv: ^1.2.2
+ is-lite: ^0.9.2
+ prop-types: ^15.8.1
+ react-floater: ^0.7.6
+ react-is: ^16.13.1
+ scroll: ^3.0.1
+ scrollparent: ^2.1.0
+ tree-changes: ^0.9.2
+ peerDependencies:
+ react: 15 - 18
+ react-dom: 15 - 18
+ checksum: ca4a18631e12638e10f7ebb063880f66a984a4edc0fe81f87649ac1ec32c3790a03c6cbe10f189c029d64af52eef4173042c676024987accb0aeaa4e1216bb04
+ languageName: node
+ linkType: hard
+
"react-lifecycles-compat@npm:^3.0.0, react-lifecycles-compat@npm:^3.0.4":
version: 3.0.4
resolution: "react-lifecycles-compat@npm:3.0.4"
languageName: node
linkType: hard
+"react-proptype-conditional-require@npm:^1.0.4":
+ version: 1.0.4
+ resolution: "react-proptype-conditional-require@npm:1.0.4"
+ checksum: 78f82d15b2c77c14fd8fbcbbed279850df3a856984aacd519ee6c2162e034b114b8ac47c00157b84ef7c98c0711d933a0177d9d54555629cf381f54341bb0e8f
+ languageName: node
+ linkType: hard
+
"react-refresh@npm:^0.14.0":
version: 0.14.0
resolution: "react-refresh@npm:0.14.0"
languageName: node
linkType: hard
+"scroll@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "scroll@npm:3.0.1"
+ checksum: e6b045347adace30035073882e6ef2af7e1c81dd611faf3a578ca8cd0d1a3a9da54932dd97ed6fd99c9573351c758fa50e6d8ed4afb5bd3a33794b6c48d25922
+ languageName: node
+ linkType: hard
+
+"scrollparent@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "scrollparent@npm:2.1.0"
+ checksum: 646cfdaf981f94b52f1020cc26b18ff1a751c2fa4e40777f6dfac314500d365b4db3fec39fd06be7b683f0a1dbd29df8ce1629db50381e9072433465b2f446fa
+ languageName: node
+ linkType: hard
+
"select@npm:^1.1.2":
version: 1.1.2
resolution: "select@npm:1.1.2"
languageName: node
linkType: hard
+"tree-changes@npm:^0.9.1, tree-changes@npm:^0.9.2":
+ version: 0.9.3
+ resolution: "tree-changes@npm:0.9.3"
+ dependencies:
+ "@gilbarbara/deep-equal": ^0.1.1
+ is-lite: ^0.8.2
+ checksum: 86d890b18e83f2a20e7257982aec62efa186abbb08de4cead1c8062c50793f5b3c5fd09f98d9f4b8784b921e830687c0ea9bdb42ef4abb2eb9d6782d8c56a673
+ languageName: node
+ linkType: hard
+
"ts-interface-checker@npm:^0.1.9":
version: 0.1.13
resolution: "ts-interface-checker@npm:0.1.13"
component_report.subscribe_x_success=Subscription successful. You will receive a {0} report for this {1} by email.
component_report.unsubscribe_x_success=Subscription successfully canceled. You won't receive a {0} report for this {1} by email.
component_report.unsubscribe_success=Subscription successfully canceled. You won't receive these reports by email anymore.
+
+
+#------------------------------------------------------------------------------
+#
+# GUIDING
+#
+#------------------------------------------------------------------------------
+guiding.issue_list.1.title=Introducing Clean Code attributes
+guiding.issue_list.1.content.1=Clean Code attributes are the characteristic that your code must have to be considered Clean Code.
+guiding.issue_list.1.content.2=You can now filter by these attributes to evaluate why your code is breaking away from being clean.
+guiding.issue_list.2.title=Introducing Software Qualities
+guiding.issue_list.2.content.1=A software quality is a characteristic of software that contributes to its lasting value.
+guiding.issue_list.2.content.2=You can now filter by these qualities to evaluate the areas in your software that are impacted by the introduction of code that isn't clean.
+guiding.issue_list.3.title=Severity and Software Qualities
+guiding.issue_list.3.content.1=Severities are now directly tied to the software quality impacted. This means that one software quality impacted has one severity.
+guiding.issue_list.3.content.2=There are three only 3 levels: high, medium, and low.
+guiding.issue_list.4.title=Type and old severity deprecated
+guiding.issue_list.4.content.1=Issue types and the old severities are deprecated and can no longer be modified.
+guiding.issue_list.4.content.2=You can now filter issues by:
+guiding.issue_list.4.content.list.1=Clean Code attributes
+guiding.issue_list.4.content.list.2=Software qualities
+guiding.issue_list.4.content.list.3=The severity of the software quality
+guiding.issue_list.5.title=Learn more
+guiding.issue_list.5.content=You can learn more about the approach to Clean Code in the {link}
+
+