From 3356d0014fd32650a783f1aa3344c55c6a0d6012 Mon Sep 17 00:00:00 2001 From: Revanshu Paliwal Date: Thu, 8 Jun 2023 10:42:35 +0200 Subject: [PATCH] SONAR-19345 Layout changes for new MIUI issue pages --- .../src/components/FacetItem.tsx | 1 + .../js/app/components/GlobalContainer.tsx | 1 + .../js/apps/issues/__tests__/IssuesApp-it.tsx | 19 +- .../components/ComponentBreadcrumbs.tsx | 2 +- .../js/apps/issues/components/IssuesApp.tsx | 263 ++++++------ .../js/apps/issues/components/PageActions.tsx | 2 +- .../ComponentSourceSnippetGroupViewer.tsx | 3 +- .../issues-subnavigation/IssueLocation.tsx | 8 +- .../SubnavigationIssue.tsx | 3 +- .../js/apps/issues/sidebar/ListStyleFacet.tsx | 2 +- .../issues/sidebar/ListStyleFacetFooter.tsx | 12 +- .../ListStyleFacet-test.tsx.snap | 4 +- .../components/issue/components/IssueView.tsx | 2 - .../js/components/rules/IssueTabViewer.tsx | 378 ++++++++++++++++++ 14 files changed, 555 insertions(+), 145 deletions(-) create mode 100644 server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx diff --git a/server/sonar-web/design-system/src/components/FacetItem.tsx b/server/sonar-web/design-system/src/components/FacetItem.tsx index 62d5e38b150..9fbeff5e730 100644 --- a/server/sonar-web/design-system/src/components/FacetItem.tsx +++ b/server/sonar-web/design-system/src/components/FacetItem.tsx @@ -113,6 +113,7 @@ const StyledButton = styled(ButtonSecondary)<{ active?: boolean; small?: boolean & mark { background-color: ${themeColor('searchHighlight')}; + font-weight: 400; } } 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 85fe17a7f6f..f0ca8055c1f 100644 --- a/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx +++ b/server/sonar-web/src/main/js/app/components/GlobalContainer.tsx @@ -42,6 +42,7 @@ const TEMP_PAGELIST_WITH_NEW_BACKGROUND = [ '/dashboard', '/security_hotspots', '/component_measures', + '/project/issues', ]; export default function GlobalContainer() { diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx index dc72fc139bb..f2cf204eb5e 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx @@ -544,9 +544,8 @@ describe('issues item', () => { expect( await screen.findByRole('tab', { name: `coding_rules.description_section.title.root_cause`, - selected: true, }) - ).toBeInTheDocument(); + ).toHaveAttribute('aria-current', 'true'); }); it('should interact with flows and locations', async () => { @@ -823,17 +822,15 @@ describe('issues item', () => { expect( screen.queryByRole('tab', { name: `issue.tabs.${TabKeys.Code}`, - selected: true, }) - ).not.toBeInTheDocument(); + ).toHaveAttribute('aria-current', 'false'); await user.click(screen.getByRole('link', { name: 'location 1' })); expect( - screen.getByRole('tab', { + screen.queryByRole('tab', { name: `issue.tabs.${TabKeys.Code}`, - selected: true, }) - ).toBeInTheDocument(); + ).toHaveAttribute('aria-current', 'true'); // Select the same selected hotspot location should also navigate back to code page await user.click( @@ -842,17 +839,15 @@ describe('issues item', () => { expect( screen.queryByRole('tab', { name: `issue.tabs.${TabKeys.Code}`, - selected: true, }) - ).not.toBeInTheDocument(); + ).toHaveAttribute('aria-current', 'false'); await user.click(screen.getByRole('link', { name: 'location 1' })); expect( - screen.getByRole('tab', { + screen.queryByRole('tab', { name: `issue.tabs.${TabKeys.Code}`, - selected: true, }) - ).toBeInTheDocument(); + ).toHaveAttribute('aria-current', 'true'); }); it('should show issue tags if applicable', async () => { diff --git a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx index de50f050463..c6beaa5f4d1 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/ComponentBreadcrumbs.tsx @@ -58,7 +58,7 @@ export default function ComponentBreadcrumbs({ 'issues.on_file_x', `${displayProject ? issue.projectName + ', ' : ''}${componentName}` )} - className="sw-flex sw-box-border sw-body-sm sw-w-full sw-pb-1 sw-pt-6 sw-truncate" + className="sw-flex sw-box-border sw-body-sm sw-w-full sw-pb-2 sw-pt-4 sw-truncate" > {displayProject && ( diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index 2e578793b04..ef342cf6da9 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -18,8 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import classNames from 'classnames'; -import { ButtonSecondary, Checkbox, FlagMessage, ToggleButton } from 'design-system'; +import styled from '@emotion/styled'; +import { + ButtonSecondary, + Checkbox, + FlagMessage, + LargeCenteredLayout, + PageContentFontWrapper, + ToggleButton, + themeBorder, + themeColor, +} from 'design-system'; import { debounce, keyBy, omit, without } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; @@ -37,7 +46,7 @@ import ListFooter from '../../../components/controls/ListFooter'; 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 IssueTabViewer from '../../../components/rules/IssueTabViewer'; import '../../../components/search-navigator.css'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { @@ -963,11 +972,10 @@ export class App extends React.PureComponent {
- {warning} +
{warning}
{currentUser.isLoggedIn && (
@@ -1024,7 +1032,7 @@ export class App extends React.PureComponent { const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( @@ -1033,14 +1041,14 @@ export class App extends React.PureComponent { ); return ( - + {({ top }) => ( )} @@ -1150,11 +1156,10 @@ export class App extends React.PureComponent { return openIssue ? ( ) : ( -
-
-
- - + <> + +
+
{this.renderBulkChange()} { />
-
+ ); } @@ -1181,121 +1186,131 @@ export class App extends React.PureComponent { loadingRule, } = this.state; return ( -
- - {/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */} - {openIssue && openRuleDetails ? ( - <> - - - + {({ top }) => ( +
+ + {/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */} + {openIssue && openRuleDetails ? ( + <> + - } - scrollInTab - activityTabContent={ - - } - /> - - ) : ( - - {checkAll && paging && paging.total > MAX_PAGE_SIZE && ( - - {MAX_PAGE_SIZE} }} + selectedFlowIndex={this.state.selectedFlowIndex} + selectedLocationIndex={this.state.selectedLocationIndex} + codeTabContent={ + + } + scrollInTab + activityTabContent={ + + } /> - - )} - - {cannotShowOpenIssue && (!paging || paging.total > 0) && ( - + ) : ( + + {checkAll && paging && paging.total > MAX_PAGE_SIZE && ( + + + {MAX_PAGE_SIZE} }} + /> + + )} - className="sw-mb-4" - variant="warning" - > - {translateWithParameters( - 'issues.cannot_open_issue_max_initial_X_fetched', - MAX_INITAL_FETCH + + {cannotShowOpenIssue && (!paging || paging.total > 0) && ( + + {translateWithParameters( + 'issues.cannot_open_issue_max_initial_X_fetched', + MAX_INITAL_FETCH + )} + )} - - )} - {this.renderList()} + {this.renderList()} + + )} - )} - -
+
+ )} + ); } render() { - const { component } = this.props; const { openIssue, paging } = this.state; const selectedIndex = this.getSelectedIndex(); return ( -
- - - {openIssue ? ( - - ) : ( - - )} + + + +
+ -

{translate('issues.page')}

+ {openIssue ? ( + + ) : ( + + )} - {this.renderSide(openIssue)} +

{translate('issues.page')}

-
- {this.renderHeader({ openIssue, paging, selectedIndex })} + {this.renderSide(openIssue)} - {this.renderPage()} -
-
+ + {this.renderHeader({ openIssue, paging, selectedIndex })} + + {this.renderPage()} + +
+ + + ); } } @@ -1304,3 +1319,19 @@ export default withIndexationGuard( withRouter(withCurrentUserContext(withBranchStatusActions(withComponentContext(App)))), PageContext.Issues ); + +const PageWrapperStyle = styled.div` + background-color: ${themeColor('backgroundPrimary')}; +`; + +const MainContentStyle = styled.div` + background-color: ${themeColor('subnavigation')}; + border-left: ${themeBorder('default', 'filterbarBorder')}; + border-right: ${themeBorder('default', 'filterbarBorder')}; +`; + +const SideBarStyle = styled.div` + border-left: ${themeBorder('default', 'filterbarBorder')}; + border-right: ${themeBorder('default', 'filterbarBorder')}; + background-color: ${themeColor('backgroundSecondary')}; +`; diff --git a/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx b/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx index e37cdc3444c..e5ffb197962 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/PageActions.tsx @@ -36,7 +36,7 @@ export default function PageActions(props: PageActionsProps) { const { canSetHome, effortTotal, paging, selectedIndex } = props; return ( -
+
diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx index 799ec264bc2..413b72c5689 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx @@ -19,13 +19,12 @@ */ import styled from '@emotion/styled'; import classNames from 'classnames'; -import { FlagMessage, LineFinding, themeColor } from 'design-system'; +import { FlagMessage, IssueMessageHighlighting, LineFinding, themeColor } from 'design-system'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { getSources } from '../../../api/components'; import getCoverageStatus from '../../../components/SourceViewer/helpers/getCoverageStatus'; import { locationsByLine } from '../../../components/SourceViewer/helpers/indexing'; -import { IssueMessageHighlighting } from '../../../components/issue/IssueMessageHighlighting'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; diff --git a/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/IssueLocation.tsx b/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/IssueLocation.tsx index 88cee15e0b7..fa12fafb98e 100644 --- a/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/IssueLocation.tsx +++ b/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/IssueLocation.tsx @@ -71,7 +71,9 @@ export default function IssueLocation(props: Props) { text={locationType.toUpperCase()} /> )} - {normalizedMessage ?? translate('issue.unnamed_location')} + + {normalizedMessage ?? translate('issue.unnamed_location')} + @@ -94,6 +96,10 @@ const StyledLink = styled(BaseLink)` border: none; `; +const StyledLocationName = styled.span` + word-break: break-word; +`; + function getLocationType(message?: string) { if (message?.toLowerCase().startsWith('source')) { return 'source'; diff --git a/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/SubnavigationIssue.tsx b/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/SubnavigationIssue.tsx index e4ed91a30d3..7571092038c 100644 --- a/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/SubnavigationIssue.tsx +++ b/server/sonar-web/src/main/js/apps/issues/issues-subnavigation/SubnavigationIssue.tsx @@ -44,7 +44,7 @@ export default function SubnavigationIssue(props: ConciseIssueProps) { React.useEffect(() => { if (selected && element.current) { - const parent = document.querySelector('.layout-page-side') as HTMLMenuElement; + const parent = document.querySelector('nav.it__issues-nav-bar') as HTMLMenuElement; const rect = parent.getBoundingClientRect(); const offset = element.current.offsetTop - rect.height / HALF_DIVIDER + rect.top / HALF_DIVIDER; @@ -92,6 +92,7 @@ const IssueInfo = styled.div` `; const StyledIssueTitle = styled(BareButton)` + word-break: break-word; &:focus { background-color: ${themeColor('subnavigationSelected')}; } diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx index 721d3ce4835..52c90cc0b23 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacet.tsx @@ -367,7 +367,7 @@ export class ListStyleFacet extends React.Component, State> { const { searching, searchMaxResults, searchResults, searchPaging } = this.state; if (!searching && !searchResults?.length) { - return
{translate('no_results')}
; + return
{translate('no_results')}
; } if (!searchResults) { diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacetFooter.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacetFooter.tsx index 159b29209ed..1e469313e33 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacetFooter.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/ListStyleFacetFooter.tsx @@ -19,7 +19,7 @@ */ import { useTheme } from '@emotion/react'; -import { BaseLink, Theme, themeColor } from 'design-system'; +import { DiscreetLink, Theme, themeColor } from 'design-system'; import * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { formatMeasure } from '../../../helpers/measures'; @@ -49,13 +49,13 @@ export function ListStyleFacetFooter({ return (
{translateWithParameters('x_show', formatMeasure(nbShown, MetricType.Integer))} {hasMore && ( - { @@ -65,11 +65,11 @@ export function ListStyleFacetFooter({ to="#" > {translate('show_more')} - + )} {showLess && allShown && ( - { @@ -79,7 +79,7 @@ export function ListStyleFacetFooter({ to="#" > {translate('show_less')} - + )}
); diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap index 7fb1d3d6ccb..1e996ba29e7 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap @@ -412,7 +412,7 @@ exports[`should search 4`] = ` value="blabla" />
no_results
@@ -451,7 +451,7 @@ exports[`should search 5`] = ` value="blabla" />
no_results
diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx index 96713af892a..ca7250d3af1 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueView.tsx @@ -162,8 +162,6 @@ const IssueItem = styled.li` &.selected { border: ${themeBorder('default', 'tableRowSelected')}; - &:last-child { - } } &:hover { diff --git a/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx new file mode 100644 index 00000000000..952979cba94 --- /dev/null +++ b/server/sonar-web/src/main/js/components/rules/IssueTabViewer.tsx @@ -0,0 +1,378 @@ +/* + * 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 classNames from 'classnames'; +import { ToggleButton } from 'design-system'; +import { cloneDeep, debounce, groupBy } from 'lodash'; +import * as React from 'react'; +import { Location } from 'react-router-dom'; +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 { Issue, RuleDetails } from '../../types/types'; +import { NoticeType } from '../../types/users'; +import ScreenPositionHelper from '../common/ScreenPositionHelper'; +import withLocation from '../hoc/withLocation'; +import MoreInfoRuleDescription from './MoreInfoRuleDescription'; +import RuleDescription from './RuleDescription'; + +import './style.css'; + +interface IssueTabViewerProps extends CurrentUserContextInterface { + ruleDetails: RuleDetails; + extendedDescription?: string; + ruleDescriptionContextKey?: string; + codeTabContent?: React.ReactNode; + activityTabContent?: React.ReactNode; + scrollInTab?: boolean; + location: Location; + selectedFlowIndex?: number; + selectedLocationIndex?: number; + issue?: Issue; +} + +interface State { + tabs: Tab[]; + selectedTab?: Tab; + displayEducationalPrinciplesNotification?: boolean; + educationalPrinciplesNotificationHasBeenDismissed?: boolean; +} + +export interface Tab { + value: TabKeys; + key: TabKeys; + label: string; + content: React.ReactNode; + counter?: number; +} + +export enum TabKeys { + Code = 'code', + WhyIsThisAnIssue = 'why', + HowToFixIt = 'how_to_fix', + AssessTheIssue = 'assess_the_problem', + Activity = 'activity', + MoreInfo = 'more_info', +} + +const DEBOUNCE_FOR_SCROLL = 250; + +export class IssueTabViewer extends React.PureComponent { + state: State = { + tabs: [], + }; + + educationPrinciplesRef: React.RefObject; + + constructor(props: IssueTabViewerProps) { + super(props); + this.educationPrinciplesRef = React.createRef(); + this.checkIfEducationPrinciplesAreVisible = debounce( + this.checkIfEducationPrinciplesAreVisible, + DEBOUNCE_FOR_SCROLL + ); + } + + componentDidMount() { + this.setState((prevState) => this.computeState(prevState)); + this.attachScrollEvent(); + + const tabs = this.computeTabs(Boolean(this.state.displayEducationalPrinciplesNotification)); + + const query = new URLSearchParams(this.props.location.search); + if (query.has('why')) { + this.setState({ + selectedTab: tabs.find((tab) => tab.key === TabKeys.WhyIsThisAnIssue) || tabs[0], + }); + } + } + + componentDidUpdate(prevProps: IssueTabViewerProps, prevState: State) { + const { + ruleDetails, + ruleDescriptionContextKey, + currentUser, + issue, + selectedFlowIndex, + selectedLocationIndex, + } = this.props; + const { selectedTab } = this.state; + + if ( + prevProps.ruleDetails.key !== ruleDetails.key || + prevProps.ruleDescriptionContextKey !== ruleDescriptionContextKey || + prevProps.issue !== issue || + prevProps.selectedFlowIndex !== selectedFlowIndex || + prevProps.selectedLocationIndex !== selectedLocationIndex || + prevProps.currentUser !== currentUser + ) { + this.setState((pState) => + this.computeState( + pState, + prevProps.ruleDetails !== ruleDetails || + (prevProps.issue && issue && prevProps.issue.key !== issue.key) || + prevProps.selectedFlowIndex !== selectedFlowIndex || + prevProps.selectedLocationIndex !== selectedLocationIndex + ) + ); + } + + 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 = 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, + activityTabContent, + issue, + } = 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 += + '
' + extendedDescription; + } else { + descriptionSectionsByKey[RuleDescriptionSections.RESOURCES] = [ + { + key: RuleDescriptionSections.RESOURCES, + content: extendedDescription, + }, + ]; + } + } + + const tabs: Tab[] = [ + { + value: TabKeys.WhyIsThisAnIssue, + 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]) && ( + + ), + }, + { + value: TabKeys.AssessTheIssue, + key: TabKeys.AssessTheIssue, + label: translate('coding_rules.description_section.title', TabKeys.AssessTheIssue), + content: descriptionSectionsByKey[RuleDescriptionSections.ASSESS_THE_PROBLEM] && ( + + ), + }, + { + value: TabKeys.HowToFixIt, + key: TabKeys.HowToFixIt, + label: translate('coding_rules.description_section.title', TabKeys.HowToFixIt), + content: descriptionSectionsByKey[RuleDescriptionSections.HOW_TO_FIX] && ( + + ), + }, + { + value: TabKeys.Activity, + key: TabKeys.Activity, + label: translate('coding_rules.description_section.title', TabKeys.Activity), + content: activityTabContent, + counter: issue?.comments?.length, + }, + { + value: TabKeys.MoreInfo, + key: TabKeys.MoreInfo, + label: translate('coding_rules.description_section.title', TabKeys.MoreInfo), + content: ((educationPrinciples && educationPrinciples.length > 0) || + descriptionSectionsByKey[RuleDescriptionSections.RESOURCES]) && ( + + ), + counter: displayEducationalPrinciplesNotification ? 1 : undefined, + }, + ]; + + if (codeTabContent !== undefined) { + tabs.unshift({ + value: TabKeys.Code, + 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; + } + + return ( + <> +
+ +
+ + {({ top }) => ( +
+ { + // Preserve tabs state by always rendering all of them. Only hide them when not selected + tabs.map((tab) => ( +
+ {tab.content} +
+ )) + } +
+ )} +
+ + ); + } +} + +export default withCurrentUserContext(withLocation(IssueTabViewer)); -- 2.39.5