*/
import styled from '@emotion/styled';
+import classNames from 'classnames';
import {
ButtonSecondary,
Checkbox,
shouldOpenStandardsFacet,
} from '../utils';
import BulkChangeModal, { MAX_PAGE_SIZE } from './BulkChangeModal';
-import IssueHeader from './IssueHeader';
import IssueReviewHistoryAndComments from './IssueReviewHistoryAndComments';
import IssuesList from './IssuesList';
import IssuesSourceViewer from './IssuesSourceViewer';
import NoIssues from './NoIssues';
import NoMyIssues from './NoMyIssues';
import PageActions from './PageActions';
+import StyledHeader, { PSEUDO_SHADOW_HEIGHT } from './StyledHeader';
interface Props {
branchLike?: BranchLike;
) : (
<>
<A11ySkipTarget anchor="issues_main" />
- <div className="sw-flex sw-mb-6 sw-gap-4">
- <div className="sw-flex sw-w-full sw-items-center sw-justify-between">
+ <StyledHeader headerHeight={84}>
+ <div className="sw-p-6 sw-flex sw-w-full sw-items-center sw-justify-between">
{this.renderBulkChange()}
<PageActions
selectedIndex={selectedIndex}
/>
</div>
- </div>
+ </StyledHeader>
</>
);
}
paging,
loadingRule,
} = this.state;
+
+ const selectedIndex = this.getSelectedIndex();
const topMargin = openIssue ? '120px' : '150px'; // 60px(footer) + 90px(bulk change)/60px(navbar)
return (
<ScreenPositionHelper>
{({ top }) => (
<div
- className="it__layout-page-main-inner sw-overflow-y-auto"
+ className={classNames('it__layout-page-main-inner sw-pt-0', {
+ 'sw-overflow-y-auto': !(openIssue && openRuleDetails),
+ })}
style={{ height: `calc((100vh - ${top}px) - ${topMargin})` }}
>
+ {this.renderHeader({ openIssue, paging, selectedIndex })}
+
<DeferredSpinner loading={loadingRule}>
{/* eslint-disable-next-line local-rules/no-conditional-rendering-of-deferredspinner */}
{openIssue && openRuleDetails ? (
- <>
- <IssueHeader
- issue={openIssue}
- ruleDetails={openRuleDetails}
- branchLike={fillBranchLike(openIssue.branch, openIssue.pullRequest)}
- onIssueChange={this.handleIssueChange}
- />
-
- <IssueTabViewer
- ruleDetails={openRuleDetails}
- extendedDescription={openRuleDetails.htmlNote}
- ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
- issue={openIssue}
- selectedFlowIndex={this.state.selectedFlowIndex}
- selectedLocationIndex={this.state.selectedLocationIndex}
- 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
- activityTabContent={
- <IssueReviewHistoryAndComments
- issue={openIssue}
- onChange={this.handleIssueChange}
- />
- }
- />
- </>
+ <IssueTabViewer
+ ruleDetails={openRuleDetails as RuleDetails}
+ extendedDescription={openRuleDetails.htmlNote}
+ ruleDescriptionContextKey={openIssue.ruleDescriptionContextKey}
+ issue={openIssue}
+ selectedFlowIndex={this.state.selectedFlowIndex}
+ selectedLocationIndex={this.state.selectedLocationIndex}
+ onIssueChange={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}
+ />
+ }
+ scrollInTab
+ activityTabContent={
+ <IssueReviewHistoryAndComments
+ issue={openIssue}
+ onChange={this.handleIssueChange}
+ />
+ }
+ />
) : (
- <DeferredSpinner loading={loading} ariaLabel={translate('issues.loading_issues')}>
- {checkAll && paging && paging.total > MAX_PAGE_SIZE && (
- <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>
- )}
-
- {cannotShowOpenIssue && (!paging || paging.total > 0) && (
- <FlagMessage className="sw-mb-4" variant="warning">
- {translateWithParameters(
- 'issues.cannot_open_issue_max_initial_X_fetched',
- MAX_INITAL_FETCH
- )}
- </FlagMessage>
- )}
-
- {this.renderList()}
- </DeferredSpinner>
+ <div
+ className="sw-px-6 sw-pb-6"
+ style={{ marginTop: `-${PSEUDO_SHADOW_HEIGHT}px` }}
+ >
+ <DeferredSpinner
+ className="sw-mt-4"
+ loading={loading}
+ ariaLabel={translate('issues.loading_issues')}
+ >
+ {checkAll && paging && paging.total > MAX_PAGE_SIZE && (
+ <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>
+ )}
+
+ {cannotShowOpenIssue && (!paging || paging.total > 0) && (
+ <FlagMessage className="sw-mb-4" variant="warning">
+ {translateWithParameters(
+ 'issues.cannot_open_issue_max_initial_X_fetched',
+ MAX_INITAL_FETCH
+ )}
+ </FlagMessage>
+ )}
+
+ {this.renderList()}
+ </DeferredSpinner>
+ </div>
)}
</DeferredSpinner>
</div>
}
render() {
- const { openIssue, paging } = this.state;
- const selectedIndex = this.getSelectedIndex();
+ const { openIssue } = this.state;
return (
<PageWrapperStyle id="issues-page">
{this.renderSide(openIssue)}
- <MainContentStyle role="main" className="sw-relative sw-ml-12 sw-p-6 sw-flex-1">
- {this.renderHeader({ openIssue, paging, selectedIndex })}
-
+ <MainContentStyle className="sw-relative sw-ml-12 sw-flex-1">
{this.renderPage()}
</MainContentStyle>
</div>
background-color: ${themeColor('backgroundPrimary')};
`;
-const MainContentStyle = styled.div`
+const MainContentStyle = styled.main`
background-color: ${themeColor('subnavigation')};
border-left: ${themeBorder('default', 'filterbarBorder')};
border-right: ${themeBorder('default', 'filterbarBorder')};
--- /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 { Theme, themeColor, themeShadow } from 'design-system';
+
+interface StyledHeaderProps {
+ headerHeight: number;
+ theme?: Theme;
+}
+
+export const PSEUDO_SHADOW_HEIGHT = 16;
+
+// Box-shadow on scroll source: https://codepen.io/StijnDeWitt/pen/LryNxa
+export const StyledHeader = styled.div<StyledHeaderProps>`
+ position: sticky;
+ top: -${PSEUDO_SHADOW_HEIGHT}px;
+ z-index: 1;
+ -webkit-backface-visibility: hidden;
+ & > div {
+ position: sticky;
+ top: 0;
+ margin-top: -${PSEUDO_SHADOW_HEIGHT}px;
+ box-sizing: border-box;
+ background-color: ${themeColor('backgroundSecondary')};
+ z-index: 3;
+ }
+ &:before {
+ content: '';
+ display: block;
+ height: ${PSEUDO_SHADOW_HEIGHT}px;
+ position: sticky;
+ top: ${({ headerHeight }) => `calc(${headerHeight}px - ${PSEUDO_SHADOW_HEIGHT}px)`};
+ box-shadow: ${themeShadow('sm')};
+ }
+ &:after {
+ content: '';
+ display: block;
+ height: ${PSEUDO_SHADOW_HEIGHT}px;
+ position: sticky;
+ background: linear-gradient(
+ ${themeColor('backgroundSecondary')} 10%,
+ rgba(255, 255, 255, 0.8) 50%,
+ rgba(255, 255, 255, 0.4) 70%,
+ transparent
+ );
+ top: 0;
+ z-index: 2;
+ }
+`;
+
+export default StyledHeader;
import { CurrentUserContextInterface } from '../../app/components/current-user/CurrentUserContext';
import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext';
import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
+import IssueHeader from '../../apps/issues/components/IssueHeader';
+import StyledHeader from '../../apps/issues/components/StyledHeader';
+import { fillBranchLike } from '../../helpers/branch-like';
import { translate } from '../../helpers/l10n';
import { Issue, RuleDetails } from '../../types/types';
import { NoticeType } from '../../types/users';
import withLocation from '../hoc/withLocation';
import MoreInfoRuleDescription from './MoreInfoRuleDescription';
import RuleDescription from './RuleDescription';
-
import './style.css';
interface IssueTabViewerProps extends CurrentUserContextInterface {
location: Location;
selectedFlowIndex?: number;
selectedLocationIndex?: number;
- issue?: Issue;
+ issue: Issue;
+ onIssueChange: (issue: Issue) => void;
}
-
interface State {
tabs: Tab[];
selectedTab?: Tab;
const DEBOUNCE_FOR_SCROLL = 250;
export class IssueTabViewer extends React.PureComponent<IssueTabViewerProps, State> {
+ headerNode?: HTMLElement | null = null;
state: State = {
tabs: [],
};
};
render() {
- const { scrollInTab } = this.props;
+ const { scrollInTab, issue, ruleDetails } = this.props;
const { tabs, selectedTab } = this.state;
if (!tabs || tabs.length === 0 || !selectedTab) {
}
return (
- <>
- <div className="sw-mb-4">
- <ToggleButton
- role="tablist"
- value={selectedTab.key}
- options={tabs}
- onChange={this.handleSelectTabs}
- />
- </div>
- <ScreenPositionHelper>
- {({ top }) => (
+ <ScreenPositionHelper>
+ {({ top }) => (
+ <div
+ style={{
+ // We substract the footer height with padding (80) and the main layout padding (20)
+ // and the tabs padding (20)
+ maxHeight: scrollInTab ? `calc(100vh - ${top + 120}px)` : 'initial',
+ }}
+ className="sw-overflow-y-auto"
+ >
+ <StyledHeader headerHeight={this.headerNode?.clientHeight ?? 0} className="sw-z-normal">
+ <div className="sw-p-6 sw-pb-4" ref={(node) => (this.headerNode = node)}>
+ <IssueHeader
+ issue={issue}
+ ruleDetails={ruleDetails}
+ branchLike={fillBranchLike(issue.branch, issue.pullRequest)}
+ onIssueChange={this.props.onIssueChange}
+ />
+ <ToggleButton
+ role="tablist"
+ value={selectedTab.key}
+ options={tabs}
+ onChange={this.handleSelectTabs}
+ />
+ </div>
+ </StyledHeader>
<div
- style={{
- // We substract the footer height with padding (80) and the main layout padding (20)
- // and the tabs padding (20)
- maxHeight: scrollInTab ? `calc(100vh - ${top + 120}px)` : 'initial',
- }}
- className="sw-flex sw-flex-col"
+ className="sw-flex sw-flex-col sw-px-6"
role="tabpanel"
aria-labelledby={`tab-${selectedTab.key}`}
id={`tabpanel-${selectedTab.key}`}
// Preserve tabs state by always rendering all of them. Only hide them when not selected
tabs.map((tab) => (
<div
- className={classNames('sw-overflow-y-auto', {
+ className={classNames({
hidden: tab.key !== selectedTab.key,
})}
key={tab.key}
))
}
</div>
- )}
- </ScreenPositionHelper>
- </>
+ </div>
+ )}
+ </ScreenPositionHelper>
);
}
}