From cf7651008dc6326cd43624d92c45ed9c9b559283 Mon Sep 17 00:00:00 2001 From: Revanshu Paliwal Date: Wed, 3 May 2023 15:13:21 +0200 Subject: [PATCH] SONAR-19174 Migrating issue code viewer header and code expander section to MIUI --- .../src/components/__tests__/buttons-test.tsx | 41 ++++++ .../design-system/src/components/buttons.tsx | 26 ++++ .../src/components/icons/index.ts | 1 + .../design-system/src/theme/light.ts | 25 ++++ .../ComponentSourceSnippetGroupViewer.tsx | 38 +++-- .../IssueSourceViewerHeader.tsx | 132 +++++++++++------- .../SnippetViewer.tsx | 74 ++++++---- .../IssueSourceViewerHeader-test.tsx | 2 +- .../__tests__/SnippetViewer-test.tsx | 4 +- 9 files changed, 245 insertions(+), 98 deletions(-) create mode 100644 server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx diff --git a/server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx b/server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx new file mode 100644 index 00000000000..f2a0886ea84 --- /dev/null +++ b/server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx @@ -0,0 +1,41 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { render } from '../../helpers/testUtils'; +import { CodeViewerExpander } from '../buttons'; + +it('renders CodeViewerExpander correctly when direction is UP', () => { + render(Hello); + const content = screen.getByText('Hello'); + expect(content).toHaveStyle({ + 'border-top': 'none', + 'border-bottom': '1px solid rgb(221,221,221)', + }); +}); + +it('renders CodeViewerExpander correctly when direction is DOWN', () => { + render(Hello); + const content = screen.getByText('Hello'); + expect(content).toHaveStyle({ + 'border-bottom': 'none', + 'border-top': '1px solid rgb(221,221,221)', + }); +}); diff --git a/server/sonar-web/design-system/src/components/buttons.tsx b/server/sonar-web/design-system/src/components/buttons.tsx index c6d2ccd3ac8..6e955a2bb04 100644 --- a/server/sonar-web/design-system/src/components/buttons.tsx +++ b/server/sonar-web/design-system/src/components/buttons.tsx @@ -229,3 +229,29 @@ export const BareButton = styled.button` background-color: ${themeColor('dropdownMenuHover')}; } `; + +interface CodeViewerExpanderProps { + direction: 'UP' | 'DOWN'; +} + +export const CodeViewerExpander = styled(BareButton)` + ${tw`sw-flex sw-items-center sw-gap-2`} + ${tw`sw-px-2 sw-py-1`} + ${tw`sw-code`} + ${tw`sw-w-full`} + ${tw`sw-box-border`} + + color: ${themeContrast('codeLineEllipsis')}; + background-color: ${themeColor('codeLineEllipsis')}; + + &:hover { + color: ${themeContrast('codeLineEllipsisHover')}; + background-color: ${themeColor('codeLineEllipsisHover')}; + } + + border-top: ${(props) => + props.direction === 'DOWN' ? themeBorder('default', 'codeLineBorder') : 'none'}; + + border-bottom: ${(props) => + props.direction === 'UP' ? themeBorder('default', 'codeLineBorder') : 'none'}; +`; diff --git a/server/sonar-web/design-system/src/components/icons/index.ts b/server/sonar-web/design-system/src/components/icons/index.ts index 27e7dc2d9f6..492863fb778 100644 --- a/server/sonar-web/design-system/src/components/icons/index.ts +++ b/server/sonar-web/design-system/src/components/icons/index.ts @@ -27,6 +27,7 @@ export { ChevronRightIcon } from './ChevronRightIcon'; export { ClockIcon } from './ClockIcon'; export { CodeSmellIcon } from './CodeSmellIcon'; export { CommentIcon } from './CommentIcon'; +export { CopyIcon } from './CopyIcon'; export { DirectoryIcon } from './DirectoryIcon'; export { ExecutionFlowIcon } from './ExecutionFlowIcon'; export { FileIcon } from './FileIcon'; diff --git a/server/sonar-web/design-system/src/theme/light.ts b/server/sonar-web/design-system/src/theme/light.ts index 084037eb643..fb4ddc7a1d0 100644 --- a/server/sonar-web/design-system/src/theme/light.ts +++ b/server/sonar-web/design-system/src/theme/light.ts @@ -201,6 +201,25 @@ export const lightTheme = { codeLineCoveredUnderline: [...COLORS.green[500], 0.15], codeLineUncoveredUnderline: [...COLORS.red[500], 0.15], + codeLineHover: secondary.light, + codeLineHighlighted: COLORS.blueGrey[100], + codeLineNewCodeUnderline: [...COLORS.indigo[300], 0.15], + codeLineMeta: COLORS.blueGrey[300], + codeLineMetaHover: secondary.dark, + codeLineDuplication: secondary.default, + codeLineCovered: COLORS.green[300], + codeLineUncovered: danger.default, + codeLinePartiallyCoveredA: danger.default, + codeLinePartiallyCoveredB: COLORS.white, + codeLineIssueSquiggle: danger.lighter, + codeLineIssuePointerBorder: COLORS.white, + codeLineLocationHighlighted: [...COLORS.blueGrey[200], 0.6], + codeLineEllipsis: COLORS.white, + codeLineEllipsisHover: secondary.light, + codeLineIssueLocation: [...danger.lighter, 0.15], + codeLineIssueLocationSelected: [...danger.lighter, 0.5], + codeLineIssueMessageTooltip: secondary.darker, + // checkbox checkboxHover: COLORS.indigo[50], checkboxCheckedHover: primary.light, @@ -562,8 +581,14 @@ export const lightTheme = { toggleHover: secondary.darker, // code viewer + codeLineNewCodeUnderline: COLORS.indigo[500], + codeLineCoveredUnderline: COLORS.green[700], + codeLineUncoveredUnderline: COLORS.red[700], + codeLineEllipsis: COLORS.blueGrey[300], + codeLineEllipsisHover: secondary.dark, codeLineLocationMarker: COLORS.red[900], codeLineLocationMarkerSelected: COLORS.red[900], + codeLineIssueMessageTooltip: COLORS.blueGrey[25], // code snippet codeSnippetHighlight: danger.default, 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 194cdc3f7d1..93e16726fbc 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 @@ -17,13 +17,14 @@ * 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 { FlagMessage } 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 IssueMessageBox from '../../../components/issue/IssueMessageBox'; -import { Alert } from '../../../components/ui/Alert'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { BranchLike } from '../../../types/branch-like'; @@ -273,20 +274,26 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone return ( <> {issueIsClosed && ( - - - {translate('issue.status', issue.status)} ( - {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'}) - - ), - }} - /> - + +
+ + {translate('issue.status', issue.status)} ( + {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'}) + + ), + }} + /> +
+
)} ))} diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx index cf4dbc06364..e51e0e58fb2 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx @@ -17,14 +17,24 @@ * 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 classNames from 'classnames'; +import { + ChevronRightIcon, + CopyIcon, + DeferredSpinner, + HoverLink, + InteractiveIcon, + LightLabel, + Link, + ThemeProp, + UnfoldIcon, + themeColor, + withTheme, +} from 'design-system'; import * as React from 'react'; -import Link from '../../../components/common/Link'; -import { ButtonIcon } from '../../../components/controls/buttons'; -import { ClipboardIconButton } from '../../../components/controls/clipboard'; -import ExpandSnippetIcon from '../../../components/icons/ExpandSnippetIcon'; -import QualifierIcon from '../../../components/icons/QualifierIcon'; -import DeferredSpinner from '../../../components/ui/DeferredSpinner'; +import Tooltip from '../../../components/controls/Tooltip'; +import { ClipboardBase } from '../../../components/controls/clipboard'; import { getBranchLikeQuery } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { collapsedDirFromPath, fileFromPath } from '../../../helpers/path'; @@ -34,6 +44,8 @@ import { ComponentQualifier } from '../../../types/component'; import { SourceViewerFile } from '../../../types/types'; import './IssueSourceViewerHeader.css'; +export const INTERACTIVE_TOOLTIP_DELAY = 0.5; + export interface Props { branchLike: BranchLike | undefined; className?: string; @@ -45,7 +57,7 @@ export interface Props { sourceViewerFile: SourceViewerFile; } -export default function IssueSourceViewerHeader(props: Props) { +function IssueSourceViewerHeader(props: Props & ThemeProp) { const { branchLike, className, @@ -55,64 +67,83 @@ export default function IssueSourceViewerHeader(props: Props) { loading, onExpand, sourceViewerFile, + theme, } = props; const { measures, path, project, projectName, q } = sourceViewerFile; - const projectNameLabel = ( - <> - {projectName} - - ); - const isProjectRoot = q === ComponentQualifier.Project; + const borderColor = themeColor('codeLineBorder')({ theme }); + + const IssueSourceViewerStyle = styled.div` + border: 1px solid ${borderColor}; + border-bottom: none; + `; + return ( -
-
+
{displayProjectName && ( -
+ <> {linkToProject ? ( - - {projectNameLabel} - + {projectName} + ) : ( - projectNameLabel + {projectName} )} -
+ )} {!isProjectRoot && ( <> -
- {collapsedDirFromPath(path)} - {fileFromPath(path)} -
+ {displayProjectName && } + + {collapsedDirFromPath(path)} + {fileFromPath(path)} + -
- -
+ + {({ setCopyButton, copySuccess }) => { + return ( + + {translate(copySuccess ? 'copied_action' : 'copy_to_clipboard')} +
+ } + {...(copySuccess ? { visible: copySuccess } : undefined)} + > + + + ); + }} + )}
{!isProjectRoot && measures.issues !== undefined && (
)} - {expandable && ( - -
- - - -
-
+ + + {expandable && !loading && ( +
+ +
)} -
+ ); } + +export default withTheme(IssueSourceViewerHeader); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx index 2180aa4bf6a..7a0becf21bb 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx @@ -18,6 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import classNames from 'classnames'; +import { + CodeViewerExpander, + ThemeProp, + UnfoldDownIcon, + UnfoldUpIcon, + themeColor, + withTheme, +} from 'design-system'; import * as React from 'react'; import Line from '../../../components/SourceViewer/components/Line'; import { symbolsByLine } from '../../../components/SourceViewer/helpers/indexing'; @@ -26,7 +34,6 @@ import { optimizeHighlightedSymbols, optimizeLocationMessage, } from '../../../components/SourceViewer/helpers/lines'; -import ExpandSnippetIcon from '../../../components/icons/ExpandSnippetIcon'; import { translate } from '../../../helpers/l10n'; import { Duplication, @@ -40,7 +47,7 @@ import { import './SnippetViewer.css'; import { LINES_BELOW_ISSUE } from './utils'; -interface Props { +export interface SnippetViewerProps { component: SourceViewerFile; displayLineNumberOptions?: boolean; displaySCM?: boolean; @@ -60,9 +67,10 @@ interface Props { renderAdditionalChildInLine?: (line: SourceLine) => React.ReactNode | undefined; renderDuplicationPopup: (index: number, line: number) => React.ReactNode; snippet: SourceLine[]; + className?: string; } -export default class SnippetViewer extends React.PureComponent { +class SnippetViewer extends React.PureComponent { expandBlock = (direction: ExpandDirection) => () => this.props.expandBlock(this.props.index, direction); @@ -136,8 +144,16 @@ export default class SnippetViewer extends React.PureComponent { } render() { - const { component, displaySCM, issue, lastSnippetOfLastGroup, locationsByLine, snippet } = - this.props; + const { + component, + displaySCM, + issue, + lastSnippetOfLastGroup, + locationsByLine, + snippet, + theme, + className, + } = this.props; const lastLine = component.measures && component.measures.lines && parseInt(component.measures.lines, 10); @@ -154,26 +170,24 @@ export default class SnippetViewer extends React.PureComponent { const displayDuplications = Boolean(this.props.loadDuplications) && snippet.some((s) => !!s.duplicated); + const borderColor = themeColor('codeLineBorder')({ theme }); + return ( -
+
{snippet[0].line > 1 && ( -
- -
+ + + )} - 1, - 'expand-down': !lastLine || snippet[snippet.length - 1].line < lastLine, - })} - > +
{snippet.map((line, index) => this.renderLine({ @@ -190,18 +204,18 @@ export default class SnippetViewer extends React.PureComponent {
{(!lastLine || snippet[snippet.length - 1].line < lastLine) && ( -
- -
+ + + )}
); } } + +export default withTheme(SnippetViewer); diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx index d95876d1cde..fa075dc4c4c 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx @@ -26,7 +26,7 @@ import IssueSourceViewerHeader, { Props } from '../IssueSourceViewerHeader'; const ui = { expandAllLines: byRole('button', { name: 'source_viewer.expand_all_lines' }), - projectLink: byRole('link', { name: 'qualifier.TRK MyProject' }), + projectLink: byRole('link', { name: 'MyProject' }), projectName: byText('MyProject'), viewAllIssues: byRole('link', { name: 'source_viewer.view_all_issues' }), }; diff --git a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx index 71915f77d7a..cdbcdb84b88 100644 --- a/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx +++ b/server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-test.tsx @@ -24,7 +24,7 @@ import { byRole } from 'testing-library-selector'; import { mockSourceLine, mockSourceViewerFile } from '../../../../helpers/mocks/sources'; import { mockIssue } from '../../../../helpers/testMocks'; import { renderComponent } from '../../../../helpers/testReactTestingUtils'; -import SnippetViewer from '../SnippetViewer'; +import SnippetViewer, { SnippetViewerProps } from '../SnippetViewer'; beforeEach(() => { jest.clearAllMocks(); @@ -91,7 +91,7 @@ it('should render additional child in line', () => { expect(screen.getByTestId('additional-child')).toBeInTheDocument(); }); -function renderSnippetViewer(props: Partial = {}) { +function renderSnippetViewer(props: Partial = {}) { return renderComponent(