@@ -18,14 +18,14 @@ | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import * as React from 'react'; | |||
import { sortBy } from 'lodash'; | |||
import { Link } from 'react-router'; | |||
import DateFromNow from '../../../components/intl/DateFromNow'; | |||
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; | |||
import HelpTooltip from '../../../components/controls/HelpTooltip'; | |||
import Level from '../../../components/ui/Level'; | |||
import ProjectLinkIcon from '../../../components/icons-components/ProjectLinkIcon'; | |||
import Tooltip from '../../../components/controls/Tooltip'; | |||
import MetaLink from '../../overview/meta/MetaLink'; | |||
import { orderLinks } from '../../projectLinks/utils'; | |||
import { translateWithParameters, translate } from '../../../helpers/l10n'; | |||
interface Props { | |||
@@ -33,7 +33,20 @@ interface Props { | |||
} | |||
export default function ProjectCard({ project }: Props) { | |||
const links = sortBy(project.links, 'type'); | |||
const { links } = project; | |||
const orderedLinks: T.ProjectLink[] = orderLinks( | |||
links.map((link, i) => { | |||
const { href, name, type } = link; | |||
return { | |||
id: `link-${i}`, | |||
name, | |||
type, | |||
url: href | |||
}; | |||
}) | |||
); | |||
const { lastAnalysisDate } = project; | |||
return ( | |||
@@ -72,20 +85,11 @@ export default function ProjectCard({ project }: Props) { | |||
<Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link> | |||
</h3> | |||
{links.length > 0 && ( | |||
{orderedLinks.length > 0 && ( | |||
<div className="account-project-links"> | |||
<ul className="list-inline"> | |||
{links.map(link => ( | |||
<li key={link.type}> | |||
<a | |||
className="link-with-icon" | |||
href={link.href} | |||
rel="nofollow" | |||
target="_blank" | |||
title={link.name}> | |||
<ProjectLinkIcon type={link.type} /> | |||
</a> | |||
</li> | |||
{orderedLinks.map(link => ( | |||
<MetaLink iconOnly={true} key={link.id} link={link} /> | |||
))} | |||
</ul> | |||
</div> |
@@ -19,68 +19,60 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import { Link } from 'react-router'; | |||
import ProjectCard from '../ProjectCard'; | |||
import Level from '../../../../components/ui/Level'; | |||
const BASE = { key: 'key', links: [], name: 'name' }; | |||
it('should render key and name', () => { | |||
const project = { ...BASE }; | |||
const output = shallow(<ProjectCard project={project} />); | |||
expect(output.find('.account-project-key').text()).toBe('key'); | |||
const wrapper = shallowRender(); | |||
expect(wrapper.find('.account-project-key').text()).toBe('key'); | |||
expect( | |||
output | |||
wrapper | |||
.find('.account-project-name') | |||
.find(Link) | |||
.find('Link') | |||
.prop('children') | |||
).toBe('name'); | |||
}); | |||
it('should render description', () => { | |||
const project = { ...BASE, description: 'bla' }; | |||
const output = shallow(<ProjectCard project={project} />); | |||
expect(output.find('.account-project-description').text()).toBe('bla'); | |||
const wrapper = shallowRender({ description: 'bla' }); | |||
expect(wrapper.find('.account-project-description').text()).toBe('bla'); | |||
}); | |||
it('should not render optional fields', () => { | |||
const project = { ...BASE }; | |||
const output = shallow(<ProjectCard project={project} />); | |||
expect(output.find('.account-project-description').length).toBe(0); | |||
expect(output.find('.account-project-quality-gate').length).toBe(0); | |||
expect(output.find('.account-project-links').length).toBe(0); | |||
const wrapper = shallowRender(); | |||
expect(wrapper.find('.account-project-description').length).toBe(0); | |||
expect(wrapper.find('.account-project-quality-gate').length).toBe(0); | |||
expect(wrapper.find('.account-project-links').length).toBe(0); | |||
}); | |||
it('should render analysis date', () => { | |||
const project = { ...BASE, lastAnalysisDate: '2016-05-17' }; | |||
const output = shallow(<ProjectCard project={project} />); | |||
expect(output.find('.account-project-analysis DateFromNow')).toHaveLength(1); | |||
const wrapper = shallowRender({ lastAnalysisDate: '2016-05-17' }); | |||
expect(wrapper.find('.account-project-analysis DateFromNow')).toHaveLength(1); | |||
}); | |||
it('should not render analysis date', () => { | |||
const project = { ...BASE }; | |||
const output = shallow(<ProjectCard project={project} />); | |||
expect(output.find('.account-project-analysis').text()).toContain( | |||
const wrapper = shallowRender(); | |||
expect(wrapper.find('.account-project-analysis').text()).toContain( | |||
'my_account.projects.never_analyzed' | |||
); | |||
}); | |||
it('should render quality gate status', () => { | |||
const project = { ...BASE, qualityGate: 'ERROR' }; | |||
const output = shallow(<ProjectCard project={project} />); | |||
const wrapper = shallowRender({ qualityGate: 'ERROR' }); | |||
expect( | |||
output | |||
wrapper | |||
.find('.account-project-quality-gate') | |||
.find(Level) | |||
.find('Level') | |||
.prop('level') | |||
).toBe('ERROR'); | |||
}); | |||
it('should render links', () => { | |||
const project = { | |||
...BASE, | |||
links: [{ name: 'n', type: 't', href: 'h' }] | |||
}; | |||
const output = shallow(<ProjectCard project={project} />); | |||
expect(output.find('.account-project-links').find('li').length).toBe(1); | |||
const wrapper = shallowRender({ | |||
links: [{ name: 'name', type: 'type', href: 'href' }] | |||
}); | |||
expect(wrapper.find('MetaLink').length).toBe(1); | |||
}); | |||
function shallowRender(project: Partial<T.MyProject> = {}) { | |||
return shallow(<ProjectCard project={{ key: 'key', links: [], name: 'name', ...project }} />); | |||
} |
@@ -0,0 +1,35 @@ | |||
/* | |||
* 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. | |||
*/ | |||
.copy-paste-link .overview-key { | |||
width: 80%; | |||
} | |||
.copy-paste-link .close { | |||
color: #000; | |||
border-bottom: 0; | |||
height: 100%; | |||
display: inline-block; | |||
margin-left: 5px; | |||
box-sizing: border-box; | |||
} | |||
.copy-paste-link .close svg { | |||
vertical-align: sub; | |||
} |
@@ -22,8 +22,10 @@ import { getLinkName } from '../../projectLinks/utils'; | |||
import ProjectLinkIcon from '../../../components/icons-components/ProjectLinkIcon'; | |||
import isValidUri from '../../../app/utils/isValidUri'; | |||
import ClearIcon from '../../../components/icons-components/ClearIcon'; | |||
import './MetaLink.css'; | |||
interface Props { | |||
iconOnly?: boolean; | |||
link: T.ProjectLink; | |||
} | |||
@@ -42,7 +44,8 @@ export default class MetaLink extends React.PureComponent<Props, State> { | |||
}; | |||
render() { | |||
const { link } = this.props; | |||
const { iconOnly, link } = this.props; | |||
const linkTitle = getLinkName(link); | |||
return ( | |||
<li> | |||
<a | |||
@@ -50,9 +53,10 @@ export default class MetaLink extends React.PureComponent<Props, State> { | |||
href={link.url} | |||
onClick={!isValidUri(link.url) ? this.handleClick : undefined} | |||
rel="nofollow noreferrer noopener" | |||
target="_blank"> | |||
target="_blank" | |||
title={linkTitle}> | |||
<ProjectLinkIcon className="little-spacer-right" type={link.type} /> | |||
{getLinkName(link)} | |||
{!iconOnly && linkTitle} | |||
</a> | |||
{this.state.expanded && ( | |||
<div className="little-spacer-top copy-paste-link"> |
@@ -31,6 +31,7 @@ it('should match snapshot', () => { | |||
}; | |||
expect(shallow(<MetaLink link={link} />)).toMatchSnapshot(); | |||
expect(shallow(<MetaLink iconOnly={true} link={link} />)).toMatchSnapshot(); | |||
}); | |||
it('should render dangerous links as plaintext', () => { |
@@ -7,6 +7,7 @@ exports[`should expand and collapse link 1`] = ` | |||
href="scm:git:git@github.com" | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Foo" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
@@ -24,6 +25,7 @@ exports[`should expand and collapse link 2`] = ` | |||
href="scm:git:git@github.com" | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Foo" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
@@ -41,6 +43,7 @@ exports[`should expand and collapse link 3`] = ` | |||
href="scm:git:git@github.com" | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Foo" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
@@ -58,6 +61,7 @@ exports[`should match snapshot 1`] = ` | |||
href="http://example.com" | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Foo" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
@@ -68,6 +72,23 @@ exports[`should match snapshot 1`] = ` | |||
</li> | |||
`; | |||
exports[`should match snapshot 2`] = ` | |||
<li> | |||
<a | |||
className="link-with-icon" | |||
href="http://example.com" | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Foo" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" | |||
type="foo" | |||
/> | |||
</a> | |||
</li> | |||
`; | |||
exports[`should render dangerous links as plaintext 1`] = ` | |||
<li> | |||
<a | |||
@@ -76,6 +97,7 @@ exports[`should render dangerous links as plaintext 1`] = ` | |||
onClick={[Function]} | |||
rel="nofollow noreferrer noopener" | |||
target="_blank" | |||
title="Dangerous" | |||
> | |||
<ProjectLinkIcon | |||
className="little-spacer-right" |
@@ -474,23 +474,6 @@ | |||
background-color: transparent !important; | |||
} | |||
.copy-paste-link .overview-key { | |||
width: 90%; | |||
} | |||
.copy-paste-link .close { | |||
color: #000; | |||
border-bottom: 0; | |||
height: 100%; | |||
display: inline-block; | |||
margin-left: 5px; | |||
box-sizing: border-box; | |||
} | |||
.copy-paste-link .close svg { | |||
vertical-align: sub; | |||
} | |||
.overview-deleted-profile, | |||
.overview-deprecated-rules { | |||
margin: 4px -6px 4px; |
@@ -31,7 +31,7 @@ export function orderLinks<T extends NameAndType>(links: T[]) { | |||
const [provided, unknown] = partition<T>(links, isProvided); | |||
return [ | |||
...sortBy(provided, link => PROVIDED_TYPES.indexOf(link.type)), | |||
...sortBy(unknown, link => link.name!.toLowerCase()) | |||
...sortBy(unknown, link => link.name && link.name.toLowerCase()) | |||
]; | |||
} | |||