diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2024-09-30 11:50:51 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-10-04 20:03:46 +0000 |
commit | 2124f36a93c6a190a58048e4dbd06b7983bca594 (patch) | |
tree | cd08db66794a2c9e556fa805b3031992bc9c725d | |
parent | 06a5e9b8e0edfae89364eccab2c4146f77cf0b83 (diff) | |
download | sonarqube-2124f36a93c6a190a58048e4dbd06b7983bca594.tar.gz sonarqube-2124f36a93c6a190a58048e4dbd06b7983bca594.zip |
SGB-163 Moving IssueDetails out of IssuesApp
-rw-r--r-- | server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx | 259 | ||||
-rw-r--r-- | server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx | 357 |
2 files changed, 375 insertions, 241 deletions
diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx new file mode 100644 index 00000000000..2e03f52fa6c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueDetails.tsx @@ -0,0 +1,259 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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 { Spinner } from '@sonarsource/echoes-react'; +import { + FlagMessage, + LargeCenteredLayout, + LAYOUT_FOOTER_HEIGHT, + PageContentFontWrapper, + themeBorder, + themeColor, +} from 'design-system'; +import React, { useEffect } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { getCve } from '../../../api/cves'; +import { getRuleDetails } from '../../../api/rules'; +import ScreenPositionHelper from '../../../components/common/ScreenPositionHelper'; +import { IssueSuggestionCodeTab } from '../../../components/rules/IssueSuggestionCodeTab'; +import IssueTabViewer from '../../../components/rules/IssueTabViewer'; +import { fillBranchLike } from '../../../helpers/branch-like'; +import { translate, translateWithParameters } from '../../../helpers/l10n'; +import A11ySkipTarget from '../../../sonar-aligned/components/a11y/A11ySkipTarget'; +import { isPortfolioLike } from '../../../sonar-aligned/helpers/component'; +import { ComponentQualifier } from '../../../sonar-aligned/types/component'; +import { Cve } from '../../../types/cves'; +import { Component, Issue, Paging, RuleDetails } from '../../../types/types'; +import SubnavigationIssuesList from '../issues-subnavigation/SubnavigationIssuesList'; +import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments'; +import IssuesSourceViewer from './IssuesSourceViewer'; + +interface IssueDetailsProps { + component?: Component; + fetchMoreIssues: () => void; + handleIssueChange: (issue: Issue) => void; + handleOpenIssue: (issueKey: string) => void; + issues: Issue[]; + loading: boolean; + loadingMore: boolean; + locationsNavigator: boolean; + openIssue: Issue; + paging?: Paging; + selectFlow: (flowIndex: number) => void; + selectLocation: (locationIndex: number) => void; + selected?: string; + selectedFlowIndex?: number; + selectedLocationIndex?: number; +} + +export default function IssueDetails({ + handleOpenIssue, + handleIssueChange, + openIssue, + component, + fetchMoreIssues, + selectFlow, + selectLocation, + issues, + loading, + loadingMore, + locationsNavigator, + paging, + selected, + selectedFlowIndex, + selectedLocationIndex, +}: Readonly<IssueDetailsProps>) { + const [loadingRule, setLoadingRule] = React.useState(false); + const [openRuleDetails, setOpenRuleDetails] = React.useState<RuleDetails | undefined>(undefined); + const [cve, setCve] = React.useState<Cve | undefined>(undefined); + const { canBrowseAllChildProjects, qualifier = ComponentQualifier.Project } = component ?? {}; + + useEffect(() => { + const loadRule = async () => { + setLoadingRule(true); + + const openRuleDetails = await getRuleDetails({ key: openIssue.rule }) + .then((response) => response.rule) + .catch(() => undefined); + + let cve: Cve | undefined; + if (typeof openIssue.cveId === 'string') { + cve = await getCve(openIssue.cveId); + } + setLoadingRule(false); + setOpenRuleDetails(openRuleDetails); + setCve(cve); + }; + loadRule().catch(() => undefined); + }, [openIssue.key, openIssue.rule, openIssue.cveId]); + + const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( + <FlagMessage + className="it__portfolio_warning sw-flex" + title={translate('issues.not_all_issue_show_why')} + variant="warning" + > + {translate('issues.not_all_issue_show')} + </FlagMessage> + ); + + return ( + <PageWrapperStyle id="issues-page"> + <LargeCenteredLayout> + <PageContentFontWrapper className="sw-typo-default"> + <div className="sw-w-full sw-flex" id="issues-page"> + <Helmet + defer={false} + title={openIssue.message} + titleTemplate={translateWithParameters( + 'page_title.template.with_category', + translate('issues.page'), + )} + /> + <h1 className="sw-sr-only">{translate('issues.page')}</h1> + + <SideBarStyle> + <ScreenPositionHelper className="sw-z-filterbar"> + {({ top }) => ( + <StyledNav + aria-label={translate('list_of_issues')} + data-testid="issues-nav-bar" + className="issues-nav-bar sw-overflow-y-auto" + style={{ height: `calc((100vh - ${top}px) - ${LAYOUT_FOOTER_HEIGHT}px)` }} + > + <div className="sw-w-[300px] lg:sw-w-[390px] sw-h-full"> + <A11ySkipTarget + anchor="issues_sidebar" + label={translate('issues.skip_to_list')} + weight={10} + /> + <div className="sw-h-full"> + {warning && <div className="sw-py-4">{warning}</div>} + + <SubnavigationIssuesList + fetchMoreIssues={fetchMoreIssues} + issues={issues} + loading={loading} + loadingMore={loadingMore} + onFlowSelect={selectFlow} + onIssueSelect={handleOpenIssue} + onLocationSelect={selectLocation} + paging={paging} + selected={selected} + selectedFlowIndex={selectedFlowIndex} + selectedLocationIndex={selectedLocationIndex} + /> + </div> + </div> + </StyledNav> + )} + </ScreenPositionHelper> + </SideBarStyle> + + <main className="sw-relative sw-flex-1 sw-min-w-0"> + <ScreenPositionHelper> + {({ top }) => ( + <StyledIssueWrapper + className="it__layout-page-main-inner sw-pt-0 details-open sw-ml-12" + style={{ height: `calc((100vh - ${top + LAYOUT_FOOTER_HEIGHT}px)` }} + > + <A11ySkipTarget anchor="issues_main" /> + + <Spinner isLoading={loadingRule}> + {openRuleDetails && ( + <IssueTabViewer + activityTabContent={ + <IssueReviewHistoryAndComments + issue={openIssue} + onChange={handleIssueChange} + /> + } + codeTabContent={ + <IssuesSourceViewer + branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)} + issues={issues} + locationsNavigator={locationsNavigator} + onIssueSelect={handleOpenIssue} + onLocationSelect={selectLocation} + openIssue={openIssue} + selectedFlowIndex={selectedFlowIndex} + selectedLocationIndex={selectedLocationIndex} + /> + } + suggestionTabContent={ + <IssueSuggestionCodeTab + branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)} + issue={openIssue} + language={openRuleDetails.lang} + /> + } + extendedDescription={openRuleDetails.htmlNote} + issue={openIssue} + onIssueChange={handleIssueChange} + ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey} + ruleDetails={openRuleDetails} + cve={cve} + selectedFlowIndex={selectedFlowIndex} + selectedLocationIndex={selectedLocationIndex} + /> + )} + </Spinner> + </StyledIssueWrapper> + )} + </ScreenPositionHelper> + </main> + </div> + </PageContentFontWrapper> + </LargeCenteredLayout> + </PageWrapperStyle> + ); +} + +const PageWrapperStyle = styled.div` + background-color: ${themeColor('backgroundPrimary')}; +`; + +const SideBarStyle = styled.div` + border-left: ${themeBorder('default', 'filterbarBorder')}; + border-right: ${themeBorder('default', 'filterbarBorder')}; + background-color: ${themeColor('backgroundSecondary')}; +`; + +const StyledIssueWrapper = styled.div` + &.details-open { + box-sizing: border-box; + border-radius: 4px; + border: ${themeBorder('default', 'filterbarBorder')}; + background-color: ${themeColor('filterbar')}; + border-bottom: none; + border-top: none; + } +`; + +const StyledNav = styled.nav` + /* +* On Firefox on Windows, the scrollbar hides the sidebar's content. +* Using 'scrollbar-gutter:stable' is a workaround to ensure consistency with other browsers. +* @see https://bugzilla.mozilla.org/show_bug.cgi?id=764076 +* @see https://discuss.sonarsource.com/t/unnecessary-horizontal-scrollbar-on-issues-page/14889/4 +*/ + scrollbar-gutter: stable; +`; 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 c9995f74bc7..2cd490c3c0b 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 @@ -20,7 +20,6 @@ import styled from '@emotion/styled'; import { Checkbox, Spinner } from '@sonarsource/echoes-react'; -import classNames from 'classnames'; import { ButtonSecondary, FlagMessage, @@ -41,9 +40,7 @@ import { getBranchLikeQuery, isPullRequest } from '~sonar-aligned/helpers/branch import { isPortfolioLike } from '~sonar-aligned/helpers/component'; import { ComponentQualifier } from '~sonar-aligned/types/component'; import { Location, RawQuery, Router } from '~sonar-aligned/types/router'; -import { getCve } from '../../../api/cves'; import { listIssues, searchIssues } from '../../../api/issues'; -import { getRuleDetails } from '../../../api/rules'; import withComponentContext from '../../../app/components/componentContext/withComponentContext'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import EmptySearch from '../../../components/common/EmptySearch'; @@ -53,11 +50,9 @@ import withIndexationContext, { WithIndexationContextProps, } from '../../../components/hoc/withIndexationContext'; import withIndexationGuard from '../../../components/hoc/withIndexationGuard'; -import { IssueSuggestionCodeTab } from '../../../components/rules/IssueSuggestionCodeTab'; -import IssueTabViewer from '../../../components/rules/IssueTabViewer'; import '../../../components/search-navigator.css'; import { DEFAULT_ISSUES_QUERY } from '../../../components/shared/utils'; -import { fillBranchLike, isSameBranchLike } from '../../../helpers/branch-like'; +import { isSameBranchLike } from '../../../helpers/branch-like'; import handleRequiredAuthentication from '../../../helpers/handleRequiredAuthentication'; import { parseIssueFromResponse } from '../../../helpers/issues'; import { isDropdown, isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; @@ -67,7 +62,6 @@ import { serializeDate } from '../../../helpers/query'; import { withBranchLikes } from '../../../queries/branch'; import { BranchLike } from '../../../types/branch-like'; import { isProject } from '../../../types/component'; -import { Cve } from '../../../types/cves'; import { ASSIGNEE_ME, Facet, @@ -77,10 +71,9 @@ import { ReferencedRule, } from '../../../types/issues'; import { SecurityStandard } from '../../../types/security'; -import { Component, Dict, Issue, Paging, RuleDetails } from '../../../types/types'; +import { Component, Dict, Issue, Paging } from '../../../types/types'; import { CurrentUser, UserBase } from '../../../types/users'; import * as actions from '../actions'; -import SubnavigationIssuesList from '../issues-subnavigation/SubnavigationIssuesList'; import { FiltersHeader } from '../sidebar/FiltersHeader'; import { Sidebar } from '../sidebar/Sidebar'; import '../styles.css'; @@ -100,12 +93,11 @@ import { shouldOpenStandardsFacet, } from '../utils'; import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal'; +import IssueDetails from './IssueDetails'; import IssueGuide from './IssueGuide'; import IssueNewStatusAndTransitionGuide from './IssueNewStatusAndTransitionGuide'; -import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments'; import IssuesList from './IssuesList'; import IssuesListTitle from './IssuesListTitle'; -import IssuesSourceViewer from './IssuesSourceViewer'; import NoIssues from './NoIssues'; import NoMyIssues from './NoMyIssues'; import PageActions from './PageActions'; @@ -124,20 +116,17 @@ export interface State { bulkChangeModal: boolean; checkAll?: boolean; checked: string[]; - cve?: Cve; effortTotal?: number; facets: Dict<Facet>; issues: Issue[]; loading: boolean; loadingFacets: Dict<boolean>; loadingMore: boolean; - loadingRule: boolean; locationsNavigator: boolean; myIssues: boolean; openFacets: Dict<boolean>; openIssue?: Issue; openPopup?: { issue: string; name: string }; - openRuleDetails?: RuleDetails; paging?: Paging; query: Query; referencedComponentsById: Dict<ReferencedComponent>; @@ -174,7 +163,6 @@ export class App extends React.PureComponent<Props, State> { loading: true, loadingFacets: {}, loadingMore: false, - loadingRule: false, locationsNavigator: false, myIssues: areMyIssuesSelected(props.location.query), openFacets: { @@ -230,7 +218,7 @@ export class App extends React.PureComponent<Props, State> { } } - componentDidUpdate(prevProps: Props, prevState: State) { + componentDidUpdate(prevProps: Props) { const { query } = this.props.location; const { query: prevQuery } = prevProps.location; const { openIssue } = this.state; @@ -257,10 +245,6 @@ export class App extends React.PureComponent<Props, State> { selectedLocationIndex: undefined, }); } - - if (this.state.openIssue && this.state.openIssue.key !== prevState.openIssue?.key) { - this.loadRule().catch(() => undefined); - } } componentWillUnmount() { @@ -373,29 +357,6 @@ export class App extends React.PureComponent<Props, State> { } }; - async loadRule() { - const { openIssue } = this.state; - - if (openIssue === undefined) { - return; - } - - this.setState({ loadingRule: true }); - - const openRuleDetails = await getRuleDetails({ key: openIssue.rule }) - .then((response) => response.rule) - .catch(() => undefined); - - let cve: Cve | undefined; - if (typeof openIssue.cveId === 'string') { - cve = await getCve(openIssue.cveId); - } - - if (this.mounted) { - this.setState({ loadingRule: false, openRuleDetails, cve }); - } - } - selectPreviousIssue = () => { const { issues } = this.state; const selectedIndex = this.getSelectedIndex(); @@ -1057,87 +1018,13 @@ export class App extends React.PureComponent<Props, State> { ); } - renderSide(openIssue: Issue | undefined) { - const { canBrowseAllChildProjects, qualifier = ComponentQualifier.Project } = - this.props.component ?? {}; - - const { - issues, - loading, - loadingMore, - paging, - selected, - selectedFlowIndex, - selectedLocationIndex, - } = this.state; - - const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( - <FlagMessage - className="it__portfolio_warning sw-flex" - title={translate('issues.not_all_issue_show_why')} - variant="warning" - > - {translate('issues.not_all_issue_show')} - </FlagMessage> - ); - - return ( - <SideBarStyle> - <ScreenPositionHelper className="sw-z-filterbar"> - {({ top }) => ( - <StyledNav - aria-label={openIssue ? translate('list_of_issues') : translate('filters')} - data-testid="issues-nav-bar" - className="issues-nav-bar sw-overflow-y-auto" - style={{ height: `calc((100vh - ${top}px) - ${LAYOUT_FOOTER_HEIGHT}px)` }} - > - <div className="sw-w-[300px] lg:sw-w-[390px] sw-h-full"> - <A11ySkipTarget - anchor="issues_sidebar" - label={ - openIssue - ? translate('issues.skip_to_list') - : translate('issues.skip_to_filters') - } - weight={10} - /> - - {openIssue ? ( - <div className="sw-h-full"> - {warning && <div className="sw-py-4">{warning}</div>} - - <SubnavigationIssuesList - fetchMoreIssues={this.fetchMoreIssues} - issues={issues} - loading={loading} - loadingMore={loadingMore} - onFlowSelect={this.selectFlow} - onIssueSelect={this.openIssue} - onLocationSelect={this.selectLocation} - paging={paging} - selected={selected} - selectedFlowIndex={selectedFlowIndex} - selectedLocationIndex={selectedLocationIndex} - /> - </div> - ) : ( - this.renderFacets(warning) - )} - </div> - </StyledNav> - )} - </ScreenPositionHelper> - </SideBarStyle> - ); - } - renderList() { const { branchLike, component, currentUser, branchLikes } = this.props; - const { issues, loading, loadingMore, openIssue, paging, query } = this.state; + const { issues, loading, loadingMore, paging, query } = this.state; const selectedIndex = this.getSelectedIndex(); const selectedIssue = selectedIndex !== undefined ? issues[selectedIndex] : undefined; - if (!paging || openIssue) { + if (!paging) { return null; } @@ -1193,113 +1080,50 @@ export class App extends React.PureComponent<Props, State> { ); } - renderHeader({ - openIssue, - paging, - }: { - openIssue: Issue | undefined; - paging: Paging | undefined; - }) { - return openIssue ? ( - <A11ySkipTarget anchor="issues_main" /> - ) : ( - <> - <A11ySkipTarget anchor="issues_main" /> - <div className="sw-p-6 sw-flex sw-w-full sw-items-center sw-justify-between sw-box-border"> - {this.renderBulkChange()} - - <PageActions - canSetHome={!this.props.component} - effortTotal={this.state.effortTotal} - paging={this.props.component?.needIssueSync ? undefined : paging} - /> - </div> - </> - ); - } - - renderPage() { - const { openRuleDetails, cve, checkAll, issues, loading, openIssue, paging, loadingRule } = - this.state; + renderIssueList() { + const { checkAll, loading, paging } = this.state; return ( <ScreenPositionHelper> {({ top }) => ( <StyledIssueWrapper - className={classNames('it__layout-page-main-inner sw-pt-0', { - 'sw-overflow-y-auto sw-pl-12': !(openIssue && openRuleDetails), - 'details-open sw-ml-12': openIssue && openRuleDetails, - })} + className="it__layout-page-main-inner sw-pt-0 sw-overflow-y-auto sw-pl-12" style={{ height: `calc((100vh - ${top + LAYOUT_FOOTER_HEIGHT}px)` }} > - {this.renderHeader({ openIssue, paging })} - - <Spinner isLoading={loadingRule}> - {openIssue && openRuleDetails ? ( - <IssueTabViewer - activityTabContent={ - <IssueReviewHistoryAndComments - issue={openIssue} - onChange={this.handleIssueChange} - /> - } - 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} - /> - } - suggestionTabContent={ - <IssueSuggestionCodeTab - branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)} - issue={openIssue} - language={openRuleDetails.lang} - /> - } - extendedDescription={openRuleDetails.htmlNote} - issue={openIssue} - onIssueChange={this.handleIssueChange} - ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey} - ruleDetails={openRuleDetails} - cve={cve} - selectedFlowIndex={this.state.selectedFlowIndex} - selectedLocationIndex={this.state.selectedLocationIndex} - /> - ) : ( - <div - className="sw-px-6 sw-pb-6" - style={{ marginTop: `-${PSEUDO_SHADOW_HEIGHT}px` }} - > - <Spinner - ariaLabel={translate('issues.loading_issues')} - className="sw-mt-4" - isLoading={loading} - > - {checkAll && paging && paging.total > MAX_PAGE_SIZE && ( - <div className="sw-mt-3"> - <FlagMessage variant="warning"> - <span> - <FormattedMessage - defaultMessage={translate('issue_bulk_change.max_issues_reached')} - id="issue_bulk_change.max_issues_reached" - values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }} - /> - </span> - </FlagMessage> - </div> - )} + <A11ySkipTarget anchor="issues_main" /> + <div className="sw-p-6 sw-flex sw-w-full sw-items-center sw-justify-between sw-box-border"> + {this.renderBulkChange()} + + <PageActions + canSetHome={!this.props.component} + effortTotal={this.state.effortTotal} + paging={this.props.component?.needIssueSync ? undefined : paging} + /> + </div> - {this.renderList()} - </Spinner> - </div> - )} - </Spinner> + <div className="sw-px-6 sw-pb-6" style={{ marginTop: `-${PSEUDO_SHADOW_HEIGHT}px` }}> + <Spinner + ariaLabel={translate('issues.loading_issues')} + className="sw-mt-4" + isLoading={loading} + > + {checkAll && paging && paging.total > MAX_PAGE_SIZE && ( + <div className="sw-mt-3"> + <FlagMessage variant="warning"> + <span> + <FormattedMessage + defaultMessage={translate('issue_bulk_change.max_issues_reached')} + id="issue_bulk_change.max_issues_reached" + values={{ max: <strong>{MAX_PAGE_SIZE}</strong> }} + /> + </span> + </FlagMessage> + </div> + )} + + {this.renderList()} + </Spinner> + </div> </StyledIssueWrapper> )} </ScreenPositionHelper> @@ -1307,41 +1131,92 @@ export class App extends React.PureComponent<Props, State> { } render() { - const { openIssue, issues } = this.state; + const { + openIssue, + issues, + selectedFlowIndex, + selectedLocationIndex, + loading, + loadingMore, + paging, + selected, + locationsNavigator, + } = this.state; const { component, location } = this.props; const open = getOpen(location.query); + const { canBrowseAllChildProjects, qualifier = ComponentQualifier.Project } = + this.props.component ?? {}; + const warning = !canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( + <FlagMessage + className="it__portfolio_warning sw-flex" + title={translate('issues.not_all_issue_show_why')} + variant="warning" + > + {translate('issues.not_all_issue_show')} + </FlagMessage> + ); + + if (openIssue) { + return ( + <IssueDetails + openIssue={openIssue} + component={component} + fetchMoreIssues={this.fetchMoreIssues} + handleIssueChange={this.handleIssueChange} + selectLocation={this.selectLocation} + selectedLocationIndex={selectedLocationIndex} + selectFlow={this.selectFlow} + selectedFlowIndex={selectedFlowIndex} + handleOpenIssue={this.openIssue} + issues={issues} + loading={loading} + loadingMore={loadingMore} + locationsNavigator={locationsNavigator} + paging={paging} + selected={selected} + /> + ); + } return ( <PageWrapperStyle id="issues-page"> <LargeCenteredLayout> <PageContentFontWrapper className="sw-typo-default"> <div className="sw-w-full sw-flex" id="issues-page"> - {openIssue ? ( - <Helmet - defer={false} - title={openIssue.message} - titleTemplate={translateWithParameters( - 'page_title.template.with_category', - translate('issues.page'), - )} - /> - ) : ( - <> - <Helmet defer={false} title={translate('issues.page')} /> - <IssueGuide run={!open && !component?.needIssueSync && issues.length > 0} /> - <IssueNewStatusAndTransitionGuide - run={!open && !component?.needIssueSync && issues.length > 0} - togglePopup={this.handlePopupToggle} - issues={issues} - /> - </> - )} + <Helmet defer={false} title={translate('issues.page')} /> + <IssueGuide run={!open && !component?.needIssueSync && issues.length > 0} /> + <IssueNewStatusAndTransitionGuide + run={!open && !component?.needIssueSync && issues.length > 0} + togglePopup={this.handlePopupToggle} + issues={issues} + /> <h1 className="sw-sr-only">{translate('issues.page')}</h1> - {this.renderSide(openIssue)} + <SideBarStyle> + <ScreenPositionHelper className="sw-z-filterbar"> + {({ top }) => ( + <StyledNav + aria-label={translate('filters')} + data-testid="issues-nav-bar" + className="issues-nav-bar sw-overflow-y-auto" + style={{ height: `calc((100vh - ${top}px) - ${LAYOUT_FOOTER_HEIGHT}px)` }} + > + <div className="sw-w-[300px] lg:sw-w-[390px] sw-h-full"> + <A11ySkipTarget + anchor="issues_sidebar" + label={translate('issues.skip_to_filters')} + weight={10} + /> + + {this.renderFacets(warning)} + </div> + </StyledNav> + )} + </ScreenPositionHelper> + </SideBarStyle> - <main className="sw-relative sw-flex-1 sw-min-w-0">{this.renderPage()}</main> + <main className="sw-relative sw-flex-1 sw-min-w-0">{this.renderIssueList()}</main> </div> </PageContentFontWrapper> </LargeCenteredLayout> |