@@ -0,0 +1,63 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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. | |||
*/ | |||
/* eslint-disable camelcase */ | |||
import { getCorsJSON } from '../helpers/request'; | |||
interface PrismicRef { | |||
id: string; | |||
ref: string; | |||
} | |||
export interface PrismicNews { | |||
data: { title: string }; | |||
last_publication_date: string; | |||
uid: string; | |||
} | |||
const PRISMIC_API_URL = 'https://sonarsource.cdn.prismic.io/api/v2'; | |||
export function fetchPrismicRefs() { | |||
return getCorsJSON(PRISMIC_API_URL).then((response: { refs: Array<PrismicRef> }) => { | |||
const master = response && response.refs.find(ref => ref.id === 'master'); | |||
if (!master) { | |||
return Promise.reject('No master ref found'); | |||
} | |||
return Promise.resolve(master); | |||
}); | |||
} | |||
export function fetchPrismicNews(data: { | |||
accessToken: string; | |||
ps?: number; | |||
ref: string; | |||
tag?: string; | |||
}) { | |||
const q = ['[[at(document.type, "blog_sonarsource_post")]]']; | |||
if (data.tag) { | |||
q.push(`[[at(document.tags,["${data.tag}"])]]`); | |||
} | |||
return getCorsJSON(PRISMIC_API_URL + '/documents/search', { | |||
access_token: data.accessToken, | |||
orderings: '[document.first_publication_date desc]', | |||
pageSize: data.ps || 1, | |||
q, | |||
ref: data.ref | |||
}).then(({ results }: { results: Array<PrismicNews> }) => results); | |||
} |
@@ -20,12 +20,13 @@ | |||
import * as React from 'react'; | |||
import * as PropTypes from 'prop-types'; | |||
import { Link } from 'react-router'; | |||
import ProductNewsMenuItem from './ProductNewsMenuItem'; | |||
import { SuggestionLink } from './SuggestionsProvider'; | |||
import { CurrentUser, isLoggedIn } from '../../types'; | |||
import { translate } from '../../../helpers/l10n'; | |||
import { getBaseUrl } from '../../../helpers/urls'; | |||
import { DropdownOverlay } from '../../../components/controls/Dropdown'; | |||
import { isSonarCloud } from '../../../helpers/system'; | |||
import { DropdownOverlay } from '../../../components/controls/Dropdown'; | |||
interface Props { | |||
currentUser: CurrentUser; | |||
@@ -87,17 +88,27 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { | |||
<React.Fragment> | |||
<li className="divider" /> | |||
<li> | |||
<a href="https://community.sonarsource.com/" rel="noopener noreferrer" target="_blank"> | |||
<a | |||
href="https://community.sonarsource.com/c/help/sc" | |||
rel="noopener noreferrer" | |||
target="_blank"> | |||
{translate('embed_docs.get_help')} | |||
</a> | |||
</li> | |||
<li className="divider" /> | |||
{this.renderTitle(translate('embed_docs.stay_connected'))} | |||
<li> | |||
{this.renderIconLink('https://about.sonarcloud.io/news/', 'sc-icon.svg', 'Product News')} | |||
{this.renderIconLink('https://twitter.com/sonarcloud', 'twitter-icon.svg', 'Twitter')} | |||
</li> | |||
<li> | |||
{this.renderIconLink('https://twitter.com/sonarcloud', 'twitter-icon.svg', 'Twitter')} | |||
{this.renderIconLink( | |||
'https://blog.sonarsource.com/product/SonarCloud', | |||
'sc-icon.svg', | |||
translate('embed_docs.news') | |||
)} | |||
</li> | |||
<li> | |||
<ProductNewsMenuItem tag="SonarCloud" /> | |||
</li> | |||
</React.Fragment> | |||
); | |||
@@ -125,7 +136,7 @@ export default class EmbedDocsPopup extends React.PureComponent<Props> { | |||
{this.renderIconLink( | |||
'https://www.sonarsource.com/resources/product-news/', | |||
'sq-icon.svg', | |||
'Product News' | |||
translate('embed_docs.news') | |||
)} | |||
</li> | |||
<li> |
@@ -34,6 +34,7 @@ interface State { | |||
} | |||
export default class EmbedDocsPopupHelper extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { helpOpen: false }; | |||
componentDidMount() { |
@@ -0,0 +1,131 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 { connect } from 'react-redux'; | |||
import { fetchPrismicRefs, fetchPrismicNews, PrismicNews } from '../../../api/news'; | |||
import { getGlobalSettingValue } from '../../../store/rootReducer'; | |||
import DateFormatter from '../../../components/intl/DateFormatter'; | |||
import ChevronRightIcon from '../../../components/icons-components/ChevronRightcon'; | |||
import PlaceholderBar from '../../../components/ui/PlaceholderBar'; | |||
interface OwnProps { | |||
tag?: string; | |||
} | |||
interface StateProps { | |||
accessToken?: string; | |||
} | |||
type Props = OwnProps & StateProps; | |||
interface State { | |||
loading: boolean; | |||
news?: PrismicNews; | |||
} | |||
export class ProductNewsMenuItem extends React.PureComponent<Props, State> { | |||
mounted = false; | |||
state: State = { loading: false }; | |||
componentDidMount() { | |||
this.mounted = true; | |||
this.fetchProductNews(); | |||
} | |||
componentWillUnmount() { | |||
this.mounted = false; | |||
} | |||
fetchProductNews = () => { | |||
const { accessToken, tag } = this.props; | |||
if (accessToken) { | |||
this.setState({ loading: true }); | |||
fetchPrismicRefs() | |||
.then(({ ref }) => fetchPrismicNews({ accessToken, ref, tag })) | |||
.then( | |||
news => { | |||
if (this.mounted) { | |||
this.setState({ news: news[0], loading: false }); | |||
} | |||
}, | |||
() => { | |||
if (this.mounted) { | |||
this.setState({ loading: false }); | |||
} | |||
} | |||
); | |||
} | |||
}; | |||
renderPlaceholder() { | |||
return ( | |||
<a className="rich-item new-loading"> | |||
<div className="flex-1"> | |||
<div className="display-inline-flex-center"> | |||
<h4>Latest news</h4> | |||
<span className="note spacer-left"> | |||
<PlaceholderBar color="#aaa" width={60} /> | |||
</span> | |||
</div> | |||
<p className="little-spacer-bottom"> | |||
<PlaceholderBar color="#aaa" width={84} /> <PlaceholderBar color="#aaa" width={48} />{' '} | |||
<PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={72} />{' '} | |||
<PlaceholderBar color="#aaa" width={24} /> <PlaceholderBar color="#aaa" width={48} /> | |||
</p> | |||
</div> | |||
<ChevronRightIcon className="flex-0" /> | |||
</a> | |||
); | |||
} | |||
render() { | |||
const link = 'https://blog.sonarsource.com/'; | |||
const { loading, news } = this.state; | |||
if (loading) { | |||
return this.renderPlaceholder(); | |||
} | |||
if (!news) { | |||
return null; | |||
} | |||
return ( | |||
<a className="rich-item" href={link + news.uid} rel="noopener noreferrer" target="_blank"> | |||
<div className="flex-1"> | |||
<div className="display-inline-flex-center"> | |||
<h4>Latest news</h4> | |||
<DateFormatter date={news.last_publication_date}> | |||
{formattedDate => <span className="note spacer-left">{formattedDate}</span>} | |||
</DateFormatter> | |||
</div> | |||
<p className="little-spacer-bottom">{news.data.title}</p> | |||
</div> | |||
<ChevronRightIcon className="flex-0" /> | |||
</a> | |||
); | |||
} | |||
} | |||
const mapStateToProps = (state: any): StateProps => ({ | |||
accessToken: (getGlobalSettingValue(state, 'sonar.prismic.accessToken') || {}).value | |||
}); | |||
export default connect<StateProps, {}, OwnProps>(mapStateToProps)(ProductNewsMenuItem); |
@@ -19,14 +19,34 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { shallow } from 'enzyme'; | |||
import EmbedDocsPopups from '../EmbedDocsPopup'; | |||
import EmbedDocsPopup from '../EmbedDocsPopup'; | |||
import { isSonarCloud } from '../../../../helpers/system'; | |||
jest.mock('../../../../helpers/system', () => ({ isSonarCloud: jest.fn().mockReturnValue(false) })); | |||
const suggestions = [{ link: '#', text: 'foo' }, { link: '#', text: 'bar' }]; | |||
it('should display suggestion links', () => { | |||
const context = {}; | |||
const wrapper = shallow( | |||
<EmbedDocsPopups | |||
<EmbedDocsPopup | |||
currentUser={{ isLoggedIn: true }} | |||
onClose={jest.fn()} | |||
suggestions={suggestions} | |||
/>, | |||
{ | |||
context | |||
} | |||
); | |||
wrapper.update(); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); | |||
it('should display correct links for SonarCloud', () => { | |||
(isSonarCloud as jest.Mock<any>).mockReturnValueOnce(true); | |||
const context = {}; | |||
const wrapper = shallow( | |||
<EmbedDocsPopup | |||
currentUser={{ isLoggedIn: true }} | |||
onClose={jest.fn()} | |||
suggestions={suggestions} |
@@ -0,0 +1,48 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 { ProductNewsMenuItem } from '../ProductNewsMenuItem'; | |||
import { fetchPrismicRefs, fetchPrismicNews } from '../../../../api/news'; | |||
import { waitAndUpdate } from '../../../../helpers/testUtils'; | |||
jest.mock('../../../../api/news', () => ({ | |||
fetchPrismicRefs: jest.fn().mockResolvedValue({ id: 'master', ref: 'master-ref' }), | |||
fetchPrismicNews: jest.fn().mockResolvedValue([ | |||
{ | |||
data: { title: 'My Product News' }, | |||
last_publication_date: '2018-04-06T12:07:19+0000', | |||
uid: 'my-product-news' | |||
} | |||
]) | |||
})); | |||
it('should load the product news', async () => { | |||
const wrapper = shallow(<ProductNewsMenuItem accessToken="token" tag="SonarCloud" />); | |||
expect(wrapper).toMatchSnapshot(); | |||
await waitAndUpdate(wrapper); | |||
expect(fetchPrismicRefs).toHaveBeenCalled(); | |||
expect(fetchPrismicNews).toHaveBeenCalledWith({ | |||
accessToken: 'token', | |||
ref: 'master-ref', | |||
tag: 'SonarCloud' | |||
}); | |||
expect(wrapper).toMatchSnapshot(); | |||
}); |
@@ -1,5 +1,127 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should display correct links for SonarCloud 1`] = ` | |||
<DropdownOverlay> | |||
<ul | |||
className="menu abs-width-240" | |||
> | |||
<React.Fragment> | |||
<li | |||
className="menu-header" | |||
> | |||
embed_docs.suggestion | |||
</li> | |||
<li | |||
key="0" | |||
> | |||
<Link | |||
onClick={[MockFunction]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="#" | |||
> | |||
foo | |||
</Link> | |||
</li> | |||
<li | |||
key="1" | |||
> | |||
<Link | |||
onClick={[MockFunction]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="#" | |||
> | |||
bar | |||
</Link> | |||
</li> | |||
<li | |||
className="divider" | |||
/> | |||
</React.Fragment> | |||
<li> | |||
<Link | |||
onClick={[MockFunction]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/documentation" | |||
> | |||
embed_docs.documentation | |||
</Link> | |||
</li> | |||
<li> | |||
<Link | |||
onClick={[MockFunction]} | |||
onlyActiveOnIndex={false} | |||
style={Object {}} | |||
to="/web_api" | |||
> | |||
api_documentation.page | |||
</Link> | |||
</li> | |||
<React.Fragment> | |||
<li | |||
className="divider" | |||
/> | |||
<li> | |||
<a | |||
href="https://community.sonarsource.com/c/help/sc" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
embed_docs.get_help | |||
</a> | |||
</li> | |||
<li | |||
className="divider" | |||
/> | |||
<li | |||
className="menu-header" | |||
> | |||
embed_docs.stay_connected | |||
</li> | |||
<li> | |||
<a | |||
href="https://twitter.com/sonarcloud" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<img | |||
alt="Twitter" | |||
className="spacer-right" | |||
height="18" | |||
src="/images/embed-doc/twitter-icon.svg" | |||
width="18" | |||
/> | |||
</a> | |||
</li> | |||
<li> | |||
<a | |||
href="https://blog.sonarsource.com/product/SonarCloud" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<img | |||
alt="embed_docs.news" | |||
className="spacer-right" | |||
height="18" | |||
src="/images/embed-doc/sc-icon.svg" | |||
width="18" | |||
/> | |||
embed_docs.news | |||
</a> | |||
</li> | |||
<li> | |||
<Connect(ProductNewsMenuItem) | |||
tag="SonarCloud" | |||
/> | |||
</li> | |||
</React.Fragment> | |||
</ul> | |||
</DropdownOverlay> | |||
`; | |||
exports[`should display suggestion links 1`] = ` | |||
<DropdownOverlay> | |||
<ul | |||
@@ -95,13 +217,13 @@ exports[`should display suggestion links 1`] = ` | |||
target="_blank" | |||
> | |||
<img | |||
alt="Product News" | |||
alt="embed_docs.news" | |||
className="spacer-right" | |||
height="18" | |||
src="/images/embed-doc/sq-icon.svg" | |||
width="18" | |||
/> | |||
Product News | |||
embed_docs.news | |||
</a> | |||
</li> | |||
<li> |
@@ -0,0 +1,95 @@ | |||
// Jest Snapshot v1, https://goo.gl/fbAQLP | |||
exports[`should load the product news 1`] = ` | |||
<a | |||
className="rich-item new-loading" | |||
> | |||
<div | |||
className="flex-1" | |||
> | |||
<div | |||
className="display-inline-flex-center" | |||
> | |||
<h4> | |||
Latest news | |||
</h4> | |||
<span | |||
className="note spacer-left" | |||
> | |||
<PlaceholderBar | |||
color="#aaa" | |||
width={60} | |||
/> | |||
</span> | |||
</div> | |||
<p | |||
className="little-spacer-bottom" | |||
> | |||
<PlaceholderBar | |||
color="#aaa" | |||
width={84} | |||
/> | |||
<PlaceholderBar | |||
color="#aaa" | |||
width={48} | |||
/> | |||
<PlaceholderBar | |||
color="#aaa" | |||
width={24} | |||
/> | |||
<PlaceholderBar | |||
color="#aaa" | |||
width={72} | |||
/> | |||
<PlaceholderBar | |||
color="#aaa" | |||
width={24} | |||
/> | |||
<PlaceholderBar | |||
color="#aaa" | |||
width={48} | |||
/> | |||
</p> | |||
</div> | |||
<ChevronRightIcon | |||
className="flex-0" | |||
/> | |||
</a> | |||
`; | |||
exports[`should load the product news 2`] = ` | |||
<a | |||
className="rich-item" | |||
href="https://blog.sonarsource.com/my-product-news" | |||
rel="noopener noreferrer" | |||
target="_blank" | |||
> | |||
<div | |||
className="flex-1" | |||
> | |||
<div | |||
className="display-inline-flex-center" | |||
> | |||
<h4> | |||
Latest news | |||
</h4> | |||
<DateFormatter | |||
date="2018-04-06T12:07:19+0000" | |||
/> | |||
</div> | |||
<p | |||
className="little-spacer-bottom" | |||
> | |||
My Product News | |||
</p> | |||
</div> | |||
<ChevronRightIcon | |||
className="flex-0" | |||
/> | |||
</a> | |||
`; |
@@ -54,6 +54,16 @@ | |||
transition: none; | |||
} | |||
.menu > li > a.rich-item { | |||
display: flex; | |||
align-items: center; | |||
border: 1px solid var(--gray80); | |||
border-radius: 4px; | |||
margin: 4px 10px; | |||
padding: 2px 8px; | |||
white-space: normal; | |||
} | |||
.menu .divider { | |||
height: 1px; | |||
margin: 6px 0; |
@@ -310,6 +310,10 @@ td.big-spacer-top { | |||
flex: 1; | |||
} | |||
.flex-0 { | |||
flex: 0 0 auto; | |||
} | |||
.flex-shrink { | |||
flex-shrink: 1; | |||
min-width: 0; |
@@ -0,0 +1,25 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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. | |||
*/ | |||
.placeholder-bar { | |||
display: inline-block; | |||
vertical-align: middle; | |||
height: 8px; | |||
background-color: currentColor; | |||
} |
@@ -0,0 +1,31 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 './PlaceholderBar.css'; | |||
interface Props { | |||
color?: string; | |||
width: number; | |||
height?: number; | |||
} | |||
export default function PlaceholderBar({ color, width, height }: Props) { | |||
return <span className="placeholder-bar" style={{ color, width, height }} />; | |||
} |
@@ -212,6 +212,23 @@ export function getJSON(url: string, data?: RequestData): Promise<any> { | |||
.then(parseJSON); | |||
} | |||
/** | |||
* Shortcut to do a CORS GET request and return responsejson | |||
*/ | |||
export function getCorsJSON(url: string, data?: RequestData): Promise<any> { | |||
return corsRequest(url) | |||
.setData(data) | |||
.submit() | |||
.then(response => { | |||
if (response.status >= 200 && response.status < 300) { | |||
return Promise.resolve(response); | |||
} else { | |||
return Promise.reject({ response }); | |||
} | |||
}) | |||
.then(parseJSON); | |||
} | |||
/** | |||
* Shortcut to do a POST request and return response json | |||
*/ |
@@ -2553,11 +2553,12 @@ organization.change_visibility_form.submit=Change Default Visibility | |||
# EMBEDED DOCS | |||
# | |||
#------------------------------------------------------------------------------ | |||
embed_docs.suggestion=Suggestions For This Page | |||
embed_docs.analyze_new_project=Analyze New Project | |||
embed_docs.documentation=Documentation | |||
embed_docs.get_help=Get Help | |||
embed_docs.news=Product News | |||
embed_docs.stay_connected=Stay Connected | |||
embed_docs.analyze_new_project=Analyze New Project | |||
embed_docs.suggestion=Suggestions For This Page | |||
#------------------------------------------------------------------------------ | |||
# |