import { updateRule } from '../../../api/rules';
import FormattingTips from '../../../components/common/FormattingTips';
import { Button, ResetButtonLink } from '../../../components/controls/buttons';
+import RuleTabViewer from '../../../components/rules/RuleTabViewer';
import { translate, translateWithParameters } from '../../../helpers/l10n';
import { sanitizeString } from '../../../helpers/sanitize';
import { RuleDetails } from '../../../types/types';
import { RuleDescriptionSections } from '../rule';
import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal';
-import RuleTabViewer from './RuleTabViewer';
interface Props {
canWrite: boolean | undefined;
? ruleDetails.descriptionSections[0]
: undefined;
+ const introductionSection = ruleDetails.descriptionSections?.find(
+ section => section.key === RuleDescriptionSections.INTRODUCTION
+ )?.content;
+
return (
<div className="js-rule-description">
{defaultSection && (
/>
)}
- {hasDescriptionSection && !defaultSection && <RuleTabViewer ruleDetails={ruleDetails} />}
+ {hasDescriptionSection && !defaultSection && (
+ <>
+ {introductionSection && (
+ <div
+ className="rule-desc"
+ // eslint-disable-next-line react/no-danger
+ dangerouslySetInnerHTML={{ __html: sanitizeString(introductionSection) }}
+ />
+ )}
+ <RuleTabViewer ruleDetails={ruleDetails} />
+ </>
+ )}
{ruleDetails.isExternal && (
<div className="coding-rules-detail-description rule-desc markdown">
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 * as React from 'react';
-import TabViewer from '../../../components/rules/TabViewer';
-import { sanitizeString } from '../../../helpers/sanitize';
-import { RuleDetails } from '../../../types/types';
-import { RuleDescriptionSections } from '../rule';
-
-export interface RuleTabViewerProps {
- ruleDetails: RuleDetails;
-}
-
-export default function RuleTabViewer(props: RuleTabViewerProps) {
- const { ruleDetails } = props;
- const introduction = ruleDetails.descriptionSections?.find(
- section => section.key === RuleDescriptionSections.INTRODUCTION
- )?.content;
-
- return (
- <>
- {introduction && (
- <div
- className="rule-desc"
- // eslint-disable-next-line react/no-danger
- dangerouslySetInnerHTML={{ __html: sanitizeString(introduction) }}
- />
- )}
- <TabViewer ruleDetails={ruleDetails} />
- </>
- );
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 * as React from 'react';
-import TabViewer from '../../../components/rules/TabViewer';
-import { BranchLike } from '../../../types/branch-like';
-import { Issue, RuleDetails } from '../../../types/types';
-import IssueHeader from './IssueHeader';
-
-interface IssueViewerTabsProps {
- branchLike?: BranchLike;
- issue: Issue;
- codeTabContent: React.ReactNode;
- ruleDetails: RuleDetails;
- onIssueChange: (issue: Issue) => void;
-}
-
-export default function IssueViewerTabs(props: IssueViewerTabsProps) {
- const { ruleDetails, issue, codeTabContent, branchLike } = props;
- return (
- <>
- <IssueHeader
- issue={issue}
- ruleDetails={ruleDetails}
- branchLike={branchLike}
- onIssueChange={props.onIssueChange}
- />
- <TabViewer
- ruleDetails={ruleDetails}
- extendedDescription={ruleDetails.htmlNote}
- ruleDescriptionContextKey={issue.ruleDescriptionContextKey}
- codeTabContent={codeTabContent}
- scrollInTab={true}
- />
- </>
- );
-}
import Suggestions from '../../../components/embed-docs-modal/Suggestions';
import withIndexationGuard from '../../../components/hoc/withIndexationGuard';
import { Location, Router, withRouter } from '../../../components/hoc/withRouter';
+import RuleTabViewer from '../../../components/rules/RuleTabViewer';
import '../../../components/search-navigator.css';
import { Alert } from '../../../components/ui/Alert';
import DeferredSpinner from '../../../components/ui/DeferredSpinner';
STANDARDS
} from '../utils';
import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
+import IssueHeader from './IssueHeader';
import IssuesList from './IssuesList';
import IssuesSourceViewer from './IssuesSourceViewer';
-import IssueTabViewer from './IssueTabViewer';
import NoIssues from './NoIssues';
import NoMyIssues from './NoMyIssues';
import PageActions from './PageActions';
<div className="layout-page-main-inner">
<DeferredSpinner loading={loadingRule}>
{openIssue && openRuleDetails ? (
- <IssueTabViewer
- codeTabContent={
- <IssuesSourceViewer
- branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
- issues={issues}
- locationsNavigator={this.state.locationsNavigator}
- onIssueSelect={this.openIssue}
- onLocationSelect={this.selectLocation}
- openIssue={openIssue}
- selectedFlowIndex={this.state.selectedFlowIndex}
- selectedLocationIndex={this.state.selectedLocationIndex}
- />
- }
- issue={openIssue}
- ruleDetails={openRuleDetails}
- branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
- onIssueChange={this.handleIssueChange}
- />
+ <>
+ <IssueHeader
+ issue={openIssue}
+ ruleDetails={openRuleDetails}
+ branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
+ onIssueChange={this.handleIssueChange}
+ />
+ <RuleTabViewer
+ ruleDetails={openRuleDetails}
+ extendedDescription={openRuleDetails.htmlNote}
+ ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
+ codeTabContent={
+ <IssuesSourceViewer
+ branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
+ issues={issues}
+ locationsNavigator={this.state.locationsNavigator}
+ onIssueSelect={this.openIssue}
+ onLocationSelect={this.selectLocation}
+ openIssue={openIssue}
+ selectedFlowIndex={this.state.selectedFlowIndex}
+ selectedLocationIndex={this.state.selectedLocationIndex}
+ />
+ }
+ scrollInTab={true}
+ />
+ </>
) : (
<DeferredSpinner loading={loading} ariaLabel={translate('issues.loading_issues')}>
{checkAll && paging && paging.total > MAX_PAGE_SIZE && (
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { cloneDeep, debounce, groupBy } from 'lodash';
+import * as React from 'react';
+import { dismissNotice } from '../../api/users';
+import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
+import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
+import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import { translate } from '../../helpers/l10n';
+import { RuleDetails } from '../../types/types';
+import { NoticeType } from '../../types/users';
+import ScreenPositionHelper from '../common/ScreenPositionHelper';
+import BoxedTabs from '../controls/BoxedTabs';
+import MoreInfoRuleDescription from './MoreInfoRuleDescription';
+import RuleDescription from './RuleDescription';
+import './style.css';
+
+interface RuleTabViewerProps extends CurrentUserContextInterface {
+ ruleDetails: RuleDetails;
+ extendedDescription?: string;
+ ruleDescriptionContextKey?: string;
+ codeTabContent?: React.ReactNode;
+ scrollInTab?: boolean;
+}
+
+interface State {
+ tabs: Tab[];
+ selectedTab?: Tab;
+ displayEducationalPrinciplesNotification?: boolean;
+ educationalPrinciplesNotificationHasBeenDismissed?: boolean;
+}
+
+export interface Tab {
+ key: TabKeys;
+ label: React.ReactNode;
+ content: React.ReactNode;
+}
+
+export enum TabKeys {
+ Code = 'code',
+ WhyIsThisAnIssue = 'why',
+ HowToFixIt = 'how_to_fix',
+ AssessTheIssue = 'assess_the_problem',
+ MoreInfo = 'more_info'
+}
+
+const DEBOUNCE_FOR_SCROLL = 250;
+
+export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State> {
+ state: State = {
+ tabs: []
+ };
+
+ educationPrinciplesRef: React.RefObject<HTMLDivElement>;
+
+ constructor(props: RuleTabViewerProps) {
+ super(props);
+ this.educationPrinciplesRef = React.createRef();
+ this.checkIfEducationPrinciplesAreVisible = debounce(
+ this.checkIfEducationPrinciplesAreVisible,
+ DEBOUNCE_FOR_SCROLL
+ );
+ }
+
+ componentDidMount() {
+ this.setState(prevState => this.computeState(prevState));
+ this.attachScrollEvent();
+ }
+
+ componentDidUpdate(prevProps: RuleTabViewerProps, prevState: State) {
+ const { ruleDetails, codeTabContent, ruleDescriptionContextKey, currentUser } = this.props;
+ const { selectedTab } = this.state;
+
+ if (
+ prevProps.ruleDetails.key !== ruleDetails.key ||
+ prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey ||
+ prevProps.codeTabContent !== codeTabContent ||
+ prevProps.currentUser !== currentUser
+ ) {
+ this.setState(pState =>
+ this.computeState(
+ pState,
+ prevProps.ruleDetails !== ruleDetails || prevProps.codeTabContent !== codeTabContent
+ )
+ );
+ }
+
+ if (selectedTab?.key === TabKeys.MoreInfo) {
+ this.checkIfEducationPrinciplesAreVisible();
+ }
+
+ if (
+ prevState.selectedTab?.key === TabKeys.MoreInfo &&
+ prevState.displayEducationalPrinciplesNotification &&
+ prevState.educationalPrinciplesNotificationHasBeenDismissed
+ ) {
+ this.props.updateDismissedNotices(NoticeType.EDUCATION_PRINCIPLES, true);
+ }
+ }
+
+ componentWillUnmount() {
+ this.detachScrollEvent();
+ }
+
+ computeState = (prevState: State, resetSelectedTab: boolean = false) => {
+ const {
+ ruleDetails,
+ currentUser: { isLoggedIn, dismissedNotices }
+ } = this.props;
+
+ const displayEducationalPrinciplesNotification =
+ !!ruleDetails.educationPrinciples &&
+ ruleDetails.educationPrinciples.length > 0 &&
+ isLoggedIn &&
+ !dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
+ const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
+
+ return {
+ tabs,
+ selectedTab: resetSelectedTab || !prevState.selectedTab ? tabs[0] : prevState.selectedTab,
+ displayEducationalPrinciplesNotification
+ };
+ };
+
+ computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
+ const {
+ codeTabContent,
+ ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
+ ruleDescriptionContextKey,
+ extendedDescription
+ } = this.props;
+
+ // As we might tamper with the description later on, we clone to avoid any side effect
+ const descriptionSectionsByKey = cloneDeep(
+ groupBy(descriptionSections, section => section.key)
+ );
+
+ if (extendedDescription) {
+ if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]?.length > 0) {
+ // We add the extended description (htmlNote) in the first context, in case there are contexts
+ // Extended description will get reworked in future
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
+ '<br/>' + extendedDescription;
+ } else {
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
+ {
+ key: RuleDescriptionSections.RESOURCES,
+ content: extendedDescription
+ }
+ ];
+ }
+ }
+
+ const tabs: Tab[] = [
+ {
+ key: TabKeys.WhyIsThisAnIssue,
+ label:
+ ruleType === 'SECURITY_HOTSPOT'
+ ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
+ : translate('coding_rules.description_section.title.root_cause'),
+ content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
+ <RuleDescription
+ className="padded"
+ sections={
+ descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
+ descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
+ }
+ isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
+ defaultContextKey={ruleDescriptionContextKey}
+ />
+ )
+ },
+ {
+ key: TabKeys.AssessTheIssue,
+ label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
+ content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
+ <RuleDescription
+ className="padded"
+ sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
+ />
+ )
+ },
+ {
+ key: TabKeys.HowToFixIt,
+ label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
+ content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
+ <RuleDescription
+ className="padded"
+ sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
+ defaultContextKey={ruleDescriptionContextKey}
+ />
+ )
+ },
+ {
+ key: TabKeys.MoreInfo,
+ label: (
+ <>
+ {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
+ {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
+ </>
+ ),
+ content: ((educationPrinciples && educationPrinciples.length > 0) ||
+ descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
+ <MoreInfoRuleDescription
+ educationPrinciples={educationPrinciples}
+ sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
+ displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
+ educationPrinciplesRef={this.educationPrinciplesRef}
+ />
+ )
+ }
+ ];
+
+ if (codeTabContent !== undefined) {
+ tabs.unshift({
+ key: TabKeys.Code,
+ label: translate('issue.tabs', TabKeys.Code),
+ content: codeTabContent
+ });
+ }
+
+ return tabs.filter(tab => tab.content);
+ };
+
+ attachScrollEvent = () => {
+ document.addEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
+ capture: true
+ });
+ };
+
+ detachScrollEvent = () => {
+ document.removeEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
+ capture: true
+ });
+ };
+
+ checkIfEducationPrinciplesAreVisible = () => {
+ const {
+ displayEducationalPrinciplesNotification,
+ educationalPrinciplesNotificationHasBeenDismissed
+ } = this.state;
+
+ if (this.educationPrinciplesRef.current) {
+ const rect = this.educationPrinciplesRef.current.getBoundingClientRect();
+ const isVisible = rect.top <= (window.innerHeight || document.documentElement.clientHeight);
+
+ if (
+ isVisible &&
+ displayEducationalPrinciplesNotification &&
+ !educationalPrinciplesNotificationHasBeenDismissed
+ ) {
+ dismissNotice(NoticeType.EDUCATION_PRINCIPLES)
+ .then(() => {
+ this.detachScrollEvent();
+ this.setState({ educationalPrinciplesNotificationHasBeenDismissed: true });
+ })
+ .catch(() => {
+ /* noop */
+ });
+ }
+ }
+ };
+
+ handleSelectTabs = (currentTabKey: TabKeys) => {
+ this.setState(({ tabs }) => ({
+ selectedTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0]
+ }));
+ };
+
+ render() {
+ const { scrollInTab } = this.props;
+ const { tabs, selectedTab } = this.state;
+
+ if (!tabs || tabs.length === 0 || !selectedTab) {
+ return null;
+ }
+
+ const tabContent = tabs.find(t => t.key === selectedTab.key)?.content;
+
+ return (
+ <>
+ <div>
+ <BoxedTabs
+ className="big-spacer-top"
+ onSelect={this.handleSelectTabs}
+ selected={selectedTab.key}
+ tabs={tabs}
+ />
+ </div>
+ <ScreenPositionHelper>
+ {({ top }) => (
+ <div
+ style={{
+ // We substract the footer height with padding (80) and the main layout padding (20)
+ maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial'
+ }}
+ className="bordered display-flex-column">
+ {/* Adding a key to force re-rendering of the tab container, so that it resets the scroll position */}
+ <div className="overflow-y-auto spacer" key={selectedTab.key}>
+ {tabContent}
+ </div>
+ </div>
+ )}
+ </ScreenPositionHelper>
+ </>
+ );
+ }
+}
+
+export default withCurrentUserContext(RuleTabViewer);
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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 { cloneDeep, debounce, groupBy } from 'lodash';
-import * as React from 'react';
-import { dismissNotice } from '../../api/users';
-import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
-import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
-import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
-import { translate } from '../../helpers/l10n';
-import { RuleDetails } from '../../types/types';
-import { NoticeType } from '../../types/users';
-import ScreenPositionHelper from '../common/ScreenPositionHelper';
-import BoxedTabs from '../controls/BoxedTabs';
-import MoreInfoRuleDescription from './MoreInfoRuleDescription';
-import RuleDescription from './RuleDescription';
-import './style.css';
-
-interface TabViewerProps extends CurrentUserContextInterface {
- ruleDetails: RuleDetails;
- extendedDescription?: string;
- ruleDescriptionContextKey?: string;
- codeTabContent?: React.ReactNode;
- scrollInTab?: boolean;
-}
-
-interface State {
- tabs: Tab[];
- selectedTab?: Tab;
- displayEducationalPrinciplesNotification?: boolean;
- educationalPrinciplesNotificationHasBeenDismissed?: boolean;
-}
-
-export interface Tab {
- key: TabKeys;
- label: React.ReactNode;
- content: React.ReactNode;
-}
-
-export enum TabKeys {
- Code = 'code',
- WhyIsThisAnIssue = 'why',
- HowToFixIt = 'how_to_fix',
- AssessTheIssue = 'assess_the_problem',
- MoreInfo = 'more_info'
-}
-
-const DEBOUNCE_FOR_SCROLL = 250;
-
-export class TabViewer extends React.PureComponent<TabViewerProps, State> {
- state: State = {
- tabs: []
- };
-
- educationPrinciplesRef: React.RefObject<HTMLDivElement>;
-
- constructor(props: TabViewerProps) {
- super(props);
- this.educationPrinciplesRef = React.createRef();
- this.checkIfEducationPrinciplesAreVisible = debounce(
- this.checkIfEducationPrinciplesAreVisible,
- DEBOUNCE_FOR_SCROLL
- );
- }
-
- componentDidMount() {
- this.setState(prevState => this.computeState(prevState));
- this.attachScrollEvent();
- }
-
- componentDidUpdate(prevProps: TabViewerProps, prevState: State) {
- const { ruleDetails, codeTabContent, ruleDescriptionContextKey, currentUser } = this.props;
- const { selectedTab } = this.state;
-
- if (
- prevProps.ruleDetails.key !== ruleDetails.key ||
- prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey ||
- prevProps.codeTabContent !== codeTabContent ||
- prevProps.currentUser !== currentUser
- ) {
- this.setState(pState =>
- this.computeState(
- pState,
- prevProps.ruleDetails !== ruleDetails || prevProps.codeTabContent !== codeTabContent
- )
- );
- }
-
- if (selectedTab?.key === TabKeys.MoreInfo) {
- this.checkIfEducationPrinciplesAreVisible();
- }
-
- if (
- prevState.selectedTab?.key === TabKeys.MoreInfo &&
- prevState.displayEducationalPrinciplesNotification &&
- prevState.educationalPrinciplesNotificationHasBeenDismissed
- ) {
- this.props.updateDismissedNotices(NoticeType.EDUCATION_PRINCIPLES, true);
- }
- }
-
- componentWillUnmount() {
- this.detachScrollEvent();
- }
-
- computeState = (prevState: State, resetSelectedTab: boolean = false) => {
- const {
- ruleDetails,
- currentUser: { isLoggedIn, dismissedNotices }
- } = this.props;
-
- const displayEducationalPrinciplesNotification =
- !!ruleDetails.educationPrinciples &&
- ruleDetails.educationPrinciples.length > 0 &&
- isLoggedIn &&
- !dismissedNotices[NoticeType.EDUCATION_PRINCIPLES];
- const tabs = this.computeTabs(displayEducationalPrinciplesNotification);
-
- return {
- tabs,
- selectedTab: resetSelectedTab || !prevState.selectedTab ? tabs[0] : prevState.selectedTab,
- displayEducationalPrinciplesNotification
- };
- };
-
- computeTabs = (displayEducationalPrinciplesNotification: boolean) => {
- const {
- codeTabContent,
- ruleDetails: { descriptionSections, educationPrinciples, type: ruleType },
- ruleDescriptionContextKey,
- extendedDescription
- } = this.props;
-
- // As we might tamper with the description later on, we clone to avoid any side effect
- const descriptionSectionsByKey = cloneDeep(
- groupBy(descriptionSections, section => section.key)
- );
-
- if (extendedDescription) {
- if (descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]?.length > 0) {
- // We add the extended description (htmlNote) in the first context, in case there are contexts
- // Extended description will get reworked in future
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES][0].content +=
- '<br/>' + extendedDescription;
- } else {
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [
- {
- key: RuleDescriptionSections.RESOURCES,
- content: extendedDescription
- }
- ];
- }
- }
-
- const tabs: Tab[] = [
- {
- key: TabKeys.WhyIsThisAnIssue,
- label:
- ruleType === 'SECURITY_HOTSPOT'
- ? translate('coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT')
- : translate('coding_rules.description_section.title.root_cause'),
- content: (descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
- descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]) && (
- <RuleDescription
- className="padded"
- sections={
- descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] ||
- descriptionSectionsByKey[RuleDescriptionSections.ROOT_CAUSE]
- }
- isDefault={descriptionSectionsByKey[RuleDescriptionSections.DEFAULT] !== undefined}
- defaultContextKey={ruleDescriptionContextKey}
- />
- )
- },
- {
- key: TabKeys.AssessTheIssue,
- label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue),
- content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && (
- <RuleDescription
- className="padded"
- sections={descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM]}
- />
- )
- },
- {
- key: TabKeys.HowToFixIt,
- label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt),
- content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && (
- <RuleDescription
- className="padded"
- sections={descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX]}
- defaultContextKey={ruleDescriptionContextKey}
- />
- )
- },
- {
- key: TabKeys.MoreInfo,
- label: (
- <>
- {translate('coding_rules.description_section.title', TabKeys.MoreInfo)}
- {displayEducationalPrinciplesNotification && <div className="notice-dot" />}
- </>
- ),
- content: ((educationPrinciples && educationPrinciples.length > 0) ||
- descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && (
- <MoreInfoRuleDescription
- educationPrinciples={educationPrinciples}
- sections={descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]}
- displayEducationalPrinciplesNotification={displayEducationalPrinciplesNotification}
- educationPrinciplesRef={this.educationPrinciplesRef}
- />
- )
- }
- ];
-
- if (codeTabContent !== undefined) {
- tabs.unshift({
- key: TabKeys.Code,
- label: translate('issue.tabs', TabKeys.Code),
- content: codeTabContent
- });
- }
-
- return tabs.filter(tab => tab.content);
- };
-
- attachScrollEvent = () => {
- document.addEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
- capture: true
- });
- };
-
- detachScrollEvent = () => {
- document.removeEventListener('scroll', this.checkIfEducationPrinciplesAreVisible, {
- capture: true
- });
- };
-
- checkIfEducationPrinciplesAreVisible = () => {
- const {
- displayEducationalPrinciplesNotification,
- educationalPrinciplesNotificationHasBeenDismissed
- } = this.state;
-
- if (this.educationPrinciplesRef.current) {
- const rect = this.educationPrinciplesRef.current.getBoundingClientRect();
- const isVisible = rect.top <= (window.innerHeight || document.documentElement.clientHeight);
-
- if (
- isVisible &&
- displayEducationalPrinciplesNotification &&
- !educationalPrinciplesNotificationHasBeenDismissed
- ) {
- dismissNotice(NoticeType.EDUCATION_PRINCIPLES)
- .then(() => {
- this.detachScrollEvent();
- this.setState({ educationalPrinciplesNotificationHasBeenDismissed: true });
- })
- .catch(() => {
- /* noop */
- });
- }
- }
- };
-
- handleSelectTabs = (currentTabKey: TabKeys) => {
- this.setState(({ tabs }) => ({
- selectedTab: tabs.find(tab => tab.key === currentTabKey) || tabs[0]
- }));
- };
-
- render() {
- const { scrollInTab } = this.props;
- const { tabs, selectedTab } = this.state;
-
- if (!tabs || tabs.length === 0 || !selectedTab) {
- return null;
- }
-
- const tabContent = tabs.find(t => t.key === selectedTab.key)?.content;
-
- return (
- <>
- <div>
- <BoxedTabs
- className="big-spacer-top"
- onSelect={this.handleSelectTabs}
- selected={selectedTab.key}
- tabs={tabs}
- />
- </div>
- <ScreenPositionHelper>
- {({ top }) => (
- <div
- style={{
- // We substract the footer height with padding (80) and the main layout padding (20)
- maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial'
- }}
- className="bordered display-flex-column">
- {/* Adding a key to force re-rendering of the tab container, so that it resets the scroll position */}
- <div className="overflow-y-auto spacer" key={selectedTab.key}>
- {tabContent}
- </div>
- </div>
- )}
- </ScreenPositionHelper>
- </>
- );
- }
-}
-
-export default withCurrentUserContext(TabViewer);