]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11830 Prevent branch names from disappearing in portfolio projects
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 18 Jun 2019 06:53:08 +0000 (08:53 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 28 Jun 2019 06:45:42 +0000 (08:45 +0200)
server/sonar-web/src/main/js/apps/code/code.css
server/sonar-web/src/main/js/apps/code/components/ComponentName.tsx
server/sonar-web/src/main/js/apps/code/components/Truncated.tsx [deleted file]
server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap [new file with mode: 0644]

index e9edb5007df820ce554dcd0921af9b1c8042c1e6..709cea43cf6b28c9a5852e57d8a6d3786c66c269 100644 (file)
   box-sizing: border-box;
 }
 
-.code-truncated {
-  display: inline-block;
-  vertical-align: text-top;
-  max-width: 50vw;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
 .code-name-cell {
   max-width: 0;
 }
 
-.code-name-cell .code-truncated {
-  max-width: 100%;
+@media (max-width: 1200px) {
+  .code-name-cell .badge,
+  .code-name-cell .outline-badge {
+    display: none;
+  }
 }
 
 .code-search {
index ed43ecf8d5fc8cb4090971d6f037c0f873727c00..03669f6c250edc61967dd92c6ff77c606c16beb6 100644 (file)
  */
 import * as React from 'react';
 import { Link } from 'react-router';
-import Truncated from './Truncated';
 import * as theme from '../../../app/theme';
 import QualifierIcon from '../../../components/icons-components/QualifierIcon';
 import { getBranchLikeQuery } from '../../../helpers/branches';
 import LongLivingBranchIcon from '../../../components/icons-components/LongLivingBranchIcon';
 import { translate } from '../../../helpers/l10n';
 
-function getTooltip(component: T.ComponentMeasure) {
+export function getTooltip(component: T.ComponentMeasure) {
   const isFile = component.qualifier === 'FIL' || component.qualifier === 'UTS';
   if (isFile && component.path) {
     return component.path + '\n\n' + component.key;
@@ -35,7 +34,7 @@ function getTooltip(component: T.ComponentMeasure) {
   }
 }
 
-function mostCommitPrefix(strings: string[]) {
+export function mostCommonPrefix(strings: string[]) {
   const sortedStrings = strings.slice(0).sort();
   const firstString = sortedStrings[0];
   const firstStringLength = firstString.length;
@@ -50,7 +49,7 @@ function mostCommitPrefix(strings: string[]) {
   return prefix.substr(0, prefix.length - lastPrefixPart.length);
 }
 
-interface Props {
+export interface Props {
   branchLike?: T.BranchLike;
   canBrowse?: boolean;
   component: T.ComponentMeasure;
@@ -58,68 +57,81 @@ interface Props {
   rootComponent: T.ComponentMeasure;
 }
 
-export default class ComponentName extends React.PureComponent<Props> {
-  render() {
-    const { branchLike, component, rootComponent, previous, canBrowse = false } = this.props;
-    const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR';
-    const prefix =
-      areBothDirs && previous !== undefined
-        ? mostCommitPrefix([component.name + '/', previous.name + '/'])
-        : '';
-    const name = prefix ? (
-      <span>
-        <span style={{ color: theme.secondFontColor }}>{prefix}</span>
-        <span>{component.name.substr(prefix.length)}</span>
-      </span>
-    ) : (
-      component.name
-    );
+export default function ComponentName({
+  branchLike,
+  component,
+  rootComponent,
+  previous,
+  canBrowse = false
+}: Props) {
+  const areBothDirs = component.qualifier === 'DIR' && previous && previous.qualifier === 'DIR';
+  const prefix =
+    areBothDirs && previous !== undefined
+      ? mostCommonPrefix([component.name + '/', previous.name + '/'])
+      : '';
+  const name = prefix ? (
+    <span>
+      <span style={{ color: theme.secondFontColor }}>{prefix}</span>
+      <span>{component.name.substr(prefix.length)}</span>
+    </span>
+  ) : (
+    component.name
+  );
 
-    let inner = null;
+  let inner = null;
 
-    if (component.refKey && component.qualifier !== 'SVW') {
-      const branch = rootComponent.qualifier === 'APP' ? { branch: component.branch } : {};
-      inner = (
-        <Link
-          className="link-with-icon"
-          to={{ pathname: '/dashboard', query: { id: component.refKey, ...branch } }}>
-          <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
-        </Link>
-      );
-    } else if (canBrowse) {
-      const query = { id: rootComponent.key, ...getBranchLikeQuery(branchLike) };
-      if (component.key !== rootComponent.key) {
-        Object.assign(query, { selected: component.key });
-      }
-      inner = (
-        <Link className="link-with-icon" to={{ pathname: '/code', query }}>
-          <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
-        </Link>
-      );
-    } else {
-      inner = (
-        <span>
-          <QualifierIcon qualifier={component.qualifier} /> {name}
-        </span>
-      );
+  if (component.refKey && component.qualifier !== 'SVW') {
+    const branch = rootComponent.qualifier === 'APP' ? { branch: component.branch } : {};
+    inner = (
+      <Link
+        className="link-with-icon"
+        to={{ pathname: '/dashboard', query: { id: component.refKey, ...branch } }}>
+        <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
+      </Link>
+    );
+  } else if (canBrowse) {
+    const query = { id: rootComponent.key, ...getBranchLikeQuery(branchLike) };
+    if (component.key !== rootComponent.key) {
+      Object.assign(query, { selected: component.key });
     }
+    inner = (
+      <Link className="link-with-icon" to={{ pathname: '/code', query }}>
+        <QualifierIcon qualifier={component.qualifier} /> <span>{name}</span>
+      </Link>
+    );
+  } else {
+    inner = (
+      <span>
+        <QualifierIcon qualifier={component.qualifier} /> {name}
+      </span>
+    );
+  }
 
-    if (rootComponent.qualifier === 'APP') {
-      inner = (
-        <>
+  if (rootComponent.qualifier === 'APP') {
+    return (
+      <span className="max-width-100 display-inline-flex-center">
+        <span className="text-ellipsis" title={getTooltip(component)}>
           {inner}
-          {component.branch ? (
-            <>
-              <LongLivingBranchIcon className="spacer-left little-spacer-right" />
-              <span className="note">{component.branch}</span>
-            </>
-          ) : (
-            <span className="spacer-left outline-badge">{translate('branches.main_branch')}</span>
-          )}
-        </>
-      );
-    }
-
-    return <Truncated title={getTooltip(component)}>{inner}</Truncated>;
+        </span>
+        {component.branch ? (
+          <span className="text-ellipsis spacer-left">
+            <LongLivingBranchIcon className="little-spacer-right" />
+            <span className="note">{component.branch}</span>
+          </span>
+        ) : (
+          <span className="spacer-left outline-badge flex-1">
+            {translate('branches.main_branch')}
+          </span>
+        )}
+      </span>
+    );
+  } else {
+    return (
+      <span
+        className="max-width-100 display-inline-block text-ellipsis"
+        title={getTooltip(component)}>
+        {inner}
+      </span>
+    );
   }
 }
diff --git a/server/sonar-web/src/main/js/apps/code/components/Truncated.tsx b/server/sonar-web/src/main/js/apps/code/components/Truncated.tsx
deleted file mode 100644 (file)
index 1c7bd74..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2019 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 * as React from 'react';
-
-interface Props {
-  children: React.ReactNode;
-  title: string;
-}
-
-export default function Truncated({ children, title }: Props) {
-  return (
-    <span className="code-truncated" title={title}>
-      {children}
-    </span>
-  );
-}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx b/server/sonar-web/src/main/js/apps/code/components/__tests__/ComponentName-test.tsx
new file mode 100644 (file)
index 0000000..b8ca181
--- /dev/null
@@ -0,0 +1,107 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 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 * as React from 'react';
+import { shallow } from 'enzyme';
+import ComponentName, { getTooltip, mostCommonPrefix, Props } from '../ComponentName';
+import { mockMainBranch, mockComponentMeasure } from '../../../../helpers/testMocks';
+
+describe('#getTooltip', () => {
+  it('should correctly format component information', () => {
+    expect(getTooltip(mockComponentMeasure(true))).toMatchSnapshot();
+    expect(getTooltip(mockComponentMeasure(true, { qualifier: 'UTS' }))).toMatchSnapshot();
+    expect(getTooltip(mockComponentMeasure(true, { path: undefined }))).toMatchSnapshot();
+    expect(getTooltip(mockComponentMeasure(false))).toMatchSnapshot();
+  });
+});
+
+describe('#mostCommonPrefix', () => {
+  it('should correctly find the common path prefix', () => {
+    expect(mostCommonPrefix(['src/main/ts/tests', 'src/main/java/tests'])).toEqual('src/main/');
+    expect(mostCommonPrefix(['src/main/ts/app', 'src/main/ts/app'])).toEqual('src/main/ts/');
+    expect(mostCommonPrefix(['src/main/ts', 'lib/main/ts'])).toEqual('');
+  });
+});
+
+describe('#ComponentName', () => {
+  it('should render correctly for files', () => {
+    expect(shallowRender()).toMatchSnapshot();
+    expect(shallowRender({ canBrowse: true })).toMatchSnapshot();
+    expect(
+      shallowRender({ rootComponent: mockComponentMeasure(false, { qualifier: 'TRK' }) })
+    ).toMatchSnapshot();
+    expect(
+      shallowRender({ rootComponent: mockComponentMeasure(false, { qualifier: 'APP' }) })
+    ).toMatchSnapshot();
+    expect(
+      shallowRender({
+        component: mockComponentMeasure(true, { branch: 'foo' }),
+        rootComponent: mockComponentMeasure(false, { qualifier: 'APP' })
+      })
+    ).toMatchSnapshot();
+  });
+
+  it('should render correctly for dirs', () => {
+    expect(
+      shallowRender({
+        component: mockComponentMeasure(false, { name: 'src/main/ts/app', qualifier: 'DIR' }),
+        previous: mockComponentMeasure(false, { name: 'src/main/ts/tests', qualifier: 'DIR' })
+      })
+    ).toMatchSnapshot();
+    expect(
+      shallowRender({
+        component: mockComponentMeasure(false, { name: 'src', qualifier: 'DIR' }),
+        previous: mockComponentMeasure(false, { name: 'lib', qualifier: 'DIR' })
+      })
+    ).toMatchSnapshot();
+  });
+
+  it('should render correctly for refs', () => {
+    expect(
+      shallowRender({
+        component: mockComponentMeasure(false, {
+          branch: 'foo',
+          refKey: 'src/main/ts/app',
+          qualifier: 'TRK'
+        })
+      })
+    ).toMatchSnapshot();
+    expect(
+      shallowRender({
+        component: mockComponentMeasure(false, {
+          branch: 'foo',
+          refKey: 'src/main/ts/app',
+          qualifier: 'TRK'
+        }),
+        rootComponent: mockComponentMeasure(false, { qualifier: 'APP' })
+      })
+    ).toMatchSnapshot();
+  });
+});
+
+function shallowRender(props: Partial<Props> = {}) {
+  return shallow(
+    <ComponentName
+      branchLike={mockMainBranch()}
+      component={mockComponentMeasure(true)}
+      rootComponent={mockComponentMeasure()}
+      {...props}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap b/server/sonar-web/src/main/js/apps/code/components/__tests__/__snapshots__/ComponentName-test.tsx.snap
new file mode 100644 (file)
index 0000000..ff35466
--- /dev/null
@@ -0,0 +1,276 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`#ComponentName should render correctly for dirs 1`] = `
+<span
+  className="max-width-100 display-inline-block text-ellipsis"
+  title="src/main/ts/app
+
+foo"
+>
+  <span>
+    <QualifierIcon
+      qualifier="DIR"
+    />
+     
+    <span>
+      <span
+        style={
+          Object {
+            "color": "#777",
+          }
+        }
+      >
+        src/main/ts/
+      </span>
+      <span>
+        app
+      </span>
+    </span>
+  </span>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for dirs 2`] = `
+<span
+  className="max-width-100 display-inline-block text-ellipsis"
+  title="src
+
+foo"
+>
+  <span>
+    <QualifierIcon
+      qualifier="DIR"
+    />
+     
+    src
+  </span>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for files 1`] = `
+<span
+  className="max-width-100 display-inline-block text-ellipsis"
+  title="src/index.tsx
+
+foo:src/index.tsx"
+>
+  <span>
+    <QualifierIcon
+      qualifier="FIL"
+    />
+     
+    index.tsx
+  </span>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for files 2`] = `
+<span
+  className="max-width-100 display-inline-block text-ellipsis"
+  title="src/index.tsx
+
+foo:src/index.tsx"
+>
+  <Link
+    className="link-with-icon"
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to={
+      Object {
+        "pathname": "/code",
+        "query": Object {
+          "id": "foo",
+          "selected": "foo:src/index.tsx",
+        },
+      }
+    }
+  >
+    <QualifierIcon
+      qualifier="FIL"
+    />
+     
+    <span>
+      index.tsx
+    </span>
+  </Link>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for files 3`] = `
+<span
+  className="max-width-100 display-inline-block text-ellipsis"
+  title="src/index.tsx
+
+foo:src/index.tsx"
+>
+  <span>
+    <QualifierIcon
+      qualifier="FIL"
+    />
+     
+    index.tsx
+  </span>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for files 4`] = `
+<span
+  className="max-width-100 display-inline-flex-center"
+>
+  <span
+    className="text-ellipsis"
+    title="src/index.tsx
+
+foo:src/index.tsx"
+  >
+    <span>
+      <QualifierIcon
+        qualifier="FIL"
+      />
+       
+      index.tsx
+    </span>
+  </span>
+  <span
+    className="spacer-left outline-badge flex-1"
+  >
+    branches.main_branch
+  </span>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for files 5`] = `
+<span
+  className="max-width-100 display-inline-flex-center"
+>
+  <span
+    className="text-ellipsis"
+    title="src/index.tsx
+
+foo:src/index.tsx"
+  >
+    <span>
+      <QualifierIcon
+        qualifier="FIL"
+      />
+       
+      index.tsx
+    </span>
+  </span>
+  <span
+    className="text-ellipsis spacer-left"
+  >
+    <LongLivingBranchIcon
+      className="little-spacer-right"
+    />
+    <span
+      className="note"
+    >
+      foo
+    </span>
+  </span>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for refs 1`] = `
+<span
+  className="max-width-100 display-inline-block text-ellipsis"
+  title="Foo
+
+foo"
+>
+  <Link
+    className="link-with-icon"
+    onlyActiveOnIndex={false}
+    style={Object {}}
+    to={
+      Object {
+        "pathname": "/dashboard",
+        "query": Object {
+          "id": "src/main/ts/app",
+        },
+      }
+    }
+  >
+    <QualifierIcon
+      qualifier="TRK"
+    />
+     
+    <span>
+      Foo
+    </span>
+  </Link>
+</span>
+`;
+
+exports[`#ComponentName should render correctly for refs 2`] = `
+<span
+  className="max-width-100 display-inline-flex-center"
+>
+  <span
+    className="text-ellipsis"
+    title="Foo
+
+foo"
+  >
+    <Link
+      className="link-with-icon"
+      onlyActiveOnIndex={false}
+      style={Object {}}
+      to={
+        Object {
+          "pathname": "/dashboard",
+          "query": Object {
+            "branch": "foo",
+            "id": "src/main/ts/app",
+          },
+        }
+      }
+    >
+      <QualifierIcon
+        qualifier="TRK"
+      />
+       
+      <span>
+        Foo
+      </span>
+    </Link>
+  </span>
+  <span
+    className="text-ellipsis spacer-left"
+  >
+    <LongLivingBranchIcon
+      className="little-spacer-right"
+    />
+    <span
+      className="note"
+    >
+      foo
+    </span>
+  </span>
+</span>
+`;
+
+exports[`#getTooltip should correctly format component information 1`] = `
+"src/index.tsx
+
+foo:src/index.tsx"
+`;
+
+exports[`#getTooltip should correctly format component information 2`] = `
+"src/index.tsx
+
+foo:src/index.tsx"
+`;
+
+exports[`#getTooltip should correctly format component information 3`] = `
+"index.tsx
+
+foo:src/index.tsx"
+`;
+
+exports[`#getTooltip should correctly format component information 4`] = `
+"Foo
+
+foo"
+`;