]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-19174 Migrating issue code viewer header and code expander section to MIUI
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>
Wed, 3 May 2023 13:13:21 +0000 (15:13 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 1 Jun 2023 20:02:58 +0000 (20:02 +0000)
server/sonar-web/design-system/src/components/__tests__/buttons-test.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/buttons.tsx
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/design-system/src/theme/light.ts
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/ComponentSourceSnippetGroupViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/IssueSourceViewerHeader.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/SnippetViewer.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/IssueSourceViewerHeader-test.tsx
server/sonar-web/src/main/js/apps/issues/crossComponentSourceViewer/__tests__/SnippetViewer-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 (file)
index 0000000..f2a0886
--- /dev/null
@@ -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(<CodeViewerExpander direction="UP">Hello</CodeViewerExpander>);
+  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(<CodeViewerExpander direction="DOWN">Hello</CodeViewerExpander>);
+  const content = screen.getByText('Hello');
+  expect(content).toHaveStyle({
+    'border-bottom': 'none',
+    'border-top': '1px solid rgb(221,221,221)',
+  });
+});
index c6d2ccd3ac843e640bb8b14d117e6da8e3a2a40a..6e955a2bb049907f6a505d81da5e0746a088ab69 100644 (file)
@@ -229,3 +229,29 @@ export const BareButton = styled.button`
     background-color: ${themeColor('dropdownMenuHover')};
   }
 `;
+
+interface CodeViewerExpanderProps {
+  direction: 'UP' | 'DOWN';
+}
+
+export const CodeViewerExpander = styled(BareButton)<CodeViewerExpanderProps>`
+  ${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'};
+`;
index 27e7dc2d9f63d767cd8748cb159f7463f605343c..492863fb77823c3bc53dd4e3415dacdf5fba3a7f 100644 (file)
@@ -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';
index 084037eb6433d82660413b0ccfdf3216aa4a39c8..fb4ddc7a1d070348bc94c47e4eddcf8f927035df 100644 (file)
@@ -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,
index 194cdc3f7d13391130d985d964b71b0cd5603f6c..93e16726fbc02f2e1a6cb2dcf87a6a423daf97e7 100644 (file)
  * 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 && (
-          <Alert variant="success">
-            <FormattedMessage
-              id={closedIssueMessageKey}
-              defaultMessage={translate(closedIssueMessageKey)}
-              values={{
-                status: (
-                  <strong>
-                    {translate('issue.status', issue.status)} (
-                    {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
-                  </strong>
-                ),
-              }}
-            />
-          </Alert>
+          <FlagMessage
+            className="sw-mb-2 sw-flex"
+            variant="success"
+            ariaLabel={translate(closedIssueMessageKey)}
+          >
+            <div className="sw-block">
+              <FormattedMessage
+                id={closedIssueMessageKey}
+                defaultMessage={translate(closedIssueMessageKey)}
+                values={{
+                  status: (
+                    <strong>
+                      {translate('issue.status', issue.status)} (
+                      {issue.resolution ? translate('issue.resolution', issue.resolution) : '-'})
+                    </strong>
+                  ),
+                }}
+              />
+            </div>
+          </FlagMessage>
         )}
 
         <IssueSourceViewerHeader
@@ -337,6 +344,7 @@ export default class ComponentSourceSnippetGroupViewer extends React.PureCompone
             onLocationSelect={this.props.onLocationSelect}
             renderDuplicationPopup={this.renderDuplicationPopup}
             snippet={snippet}
+            className={classNames({ 'sw-mt-2': index !== 0 })}
           />
         ))}
       </>
index cf4dbc06364066fc34bc35d644b54f498df62eaf..e51e0e58fb2418ea7e9a370fa6d1b4e8b6f95ce8 100644 (file)
  * 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 = (
-    <>
-      <QualifierIcon qualifier={ComponentQualifier.Project} /> <span>{projectName}</span>
-    </>
-  );
-
   const isProjectRoot = q === ComponentQualifier.Project;
 
+  const borderColor = themeColor('codeLineBorder')({ theme });
+
+  const IssueSourceViewerStyle = styled.div`
+    border: 1px solid ${borderColor};
+    border-bottom: none;
+  `;
+
   return (
-    <div
+    <IssueSourceViewerStyle
       className={classNames(
-        'issue-source-viewer-header display-flex-row display-flex-space-between',
+        'sw-flex sw-justify-space-between sw-items-center sw-px-4 sw-py-3 sw-text-sm',
         className
       )}
       role="separator"
       aria-label={sourceViewerFile.path}
     >
-      <div className="display-flex-center flex-1">
+      <div className="sw-flex-1">
         {displayProjectName && (
-          <div className="spacer-right">
+          <>
             {linkToProject ? (
-              <a
-                className="link-no-underline"
-                href={getPathUrlAsString(getBranchLikeUrl(project, branchLike))}
+              <HoverLink
+                to={getPathUrlAsString(getBranchLikeUrl(project, branchLike))}
+                className="sw-mr-2"
               >
-                {projectNameLabel}
-              </a>
+                <LightLabel>{projectName}</LightLabel>
+              </HoverLink>
             ) : (
-              projectNameLabel
+              <LightLabel className="sw-ml-1 sw-mr-2">{projectName}</LightLabel>
             )}
-          </div>
+          </>
         )}
 
         {!isProjectRoot && (
           <>
-            <div className="spacer-right">
-              <QualifierIcon qualifier={q} /> <span>{collapsedDirFromPath(path)}</span>
-              <span className="component-name-file">{fileFromPath(path)}</span>
-            </div>
+            {displayProjectName && <ChevronRightIcon className="sw-mr-2" />}
+            <LightLabel>
+              {collapsedDirFromPath(path)}
+              {fileFromPath(path)}
+            </LightLabel>
 
-            <div className="spacer-right">
-              <ClipboardIconButton
-                className="button-link link-no-underline"
-                copyValue={path}
-                aria-label={translate('source_viewer.click_to_copy_filepath')}
-              />
-            </div>
+            <ClipboardBase>
+              {({ setCopyButton, copySuccess }) => {
+                return (
+                  <Tooltip
+                    mouseEnterDelay={INTERACTIVE_TOOLTIP_DELAY}
+                    overlay={
+                      <div className="sw-w-abs-150 sw-text-center">
+                        {translate(copySuccess ? 'copied_action' : 'copy_to_clipboard')}
+                      </div>
+                    }
+                    {...(copySuccess ? { visible: copySuccess } : undefined)}
+                  >
+                    <InteractiveIcon
+                      Icon={CopyIcon}
+                      aria-label={translate('source_viewer.click_to_copy_filepath')}
+                      data-clipboard-text={path}
+                      className="sw-h-6 sw-mr-2"
+                      innerRef={setCopyButton}
+                    />
+                  </Tooltip>
+                );
+              }}
+            </ClipboardBase>
           </>
         )}
       </div>
 
       {!isProjectRoot && measures.issues !== undefined && (
         <div
-          className={classNames('flex-0 big-spacer-left', {
-            'little-spacer-right': !expandable || loading,
+          className={classNames('sw-ml-4', {
+            'sw-mr-1': !expandable || loading,
           })}
         >
           <Link
@@ -127,19 +158,20 @@ export default function IssueSourceViewerHeader(props: Props) {
         </div>
       )}
 
-      {expandable && (
-        <DeferredSpinner className="little-spacer-right" loading={loading}>
-          <div className="flex-0 big-spacer-left">
-            <ButtonIcon
-              aria-label={translate('source_viewer.expand_all_lines')}
-              className="js-actions"
-              onClick={onExpand}
-            >
-              <ExpandSnippetIcon />
-            </ButtonIcon>
-          </div>
-        </DeferredSpinner>
+      <DeferredSpinner className="sw-mr-1" loading={loading} />
+
+      {expandable && !loading && (
+        <div className="sw-ml-4">
+          <InteractiveIcon
+            Icon={UnfoldIcon}
+            aria-label={translate('source_viewer.expand_all_lines')}
+            className="sw-h-6"
+            onClick={onExpand}
+          />
+        </div>
       )}
-    </div>
+    </IssueSourceViewerStyle>
   );
 }
+
+export default withTheme(IssueSourceViewerHeader);
index 2180aa4bf6a32a048902c9995e6f5b7f820b34a1..7a0becf21bb9611bc4f186fadef797319cb4b9b2 100644 (file)
  * 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<Props> {
+class SnippetViewer extends React.PureComponent<SnippetViewerProps & ThemeProp> {
   expandBlock = (direction: ExpandDirection) => () =>
     this.props.expandBlock(this.props.index, direction);
 
@@ -136,8 +144,16 @@ export default class SnippetViewer extends React.PureComponent<Props> {
   }
 
   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<Props> {
     const displayDuplications =
       Boolean(this.props.loadDuplications) && snippet.some((s) => !!s.duplicated);
 
+    const borderColor = themeColor('codeLineBorder')({ theme });
+
     return (
-      <div className="source-viewer-code snippet">
+      <div
+        className={classNames('source-viewer-code', className)}
+        style={{ border: `1px solid ${borderColor}` }}
+      >
         <div>
           {snippet[0].line > 1 && (
-            <div className="expand-block expand-block-above">
-              <button
-                aria-label={translate('source_viewer.expand_above')}
-                onClick={this.expandBlock('up')}
-                type="button"
-              >
-                <ExpandSnippetIcon />
-              </button>
-            </div>
+            <CodeViewerExpander
+              direction="UP"
+              className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2"
+              onClick={this.expandBlock('up')}
+            >
+              <UnfoldUpIcon aria-label={translate('source_viewer.expand_above')} />
+            </CodeViewerExpander>
           )}
-          <table
-            className={classNames('source-table', {
-              'expand-up': snippet[0].line > 1,
-              'expand-down': !lastLine || snippet[snippet.length - 1].line < lastLine,
-            })}
-          >
+          <table>
             <tbody>
               {snippet.map((line, index) =>
                 this.renderLine({
@@ -190,18 +204,18 @@ export default class SnippetViewer extends React.PureComponent<Props> {
             </tbody>
           </table>
           {(!lastLine || snippet[snippet.length - 1].line < lastLine) && (
-            <div className="expand-block expand-block-below">
-              <button
-                aria-label={translate('source_viewer.expand_below')}
-                onClick={this.expandBlock('down')}
-                type="button"
-              >
-                <ExpandSnippetIcon />
-              </button>
-            </div>
+            <CodeViewerExpander
+              className="sw-flex sw-justify-start sw-items-center sw-py-1 sw-px-2"
+              onClick={this.expandBlock('down')}
+              direction="DOWN"
+            >
+              <UnfoldDownIcon aria-label={translate('source_viewer.expand_below')} />
+            </CodeViewerExpander>
           )}
         </div>
       </div>
     );
   }
 }
+
+export default withTheme(SnippetViewer);
index d95876d1cde791f6d93ea8aae275f338ac1978bd..fa075dc4c4c8121a7624e44566ccf60a1d86b495 100644 (file)
@@ -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' }),
 };
index 71915f77d7a87b9f88612e2d8df7d3074752e18c..cdbcdb84b88872390520e2ca184f928dc577d1ae 100644 (file)
@@ -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<SnippetViewer['props']> = {}) {
+function renderSnippetViewer(props: Partial<SnippetViewerProps> = {}) {
   return renderComponent(
     <SnippetViewer
       component={mockSourceViewerFile()}