--- /dev/null
+/*
+ * 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);
+}
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;
<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>
);
{this.renderIconLink(
'https://www.sonarsource.com/resources/product-news/',
'sq-icon.svg',
- 'Product News'
+ translate('embed_docs.news')
)}
</li>
<li>
}
export default class EmbedDocsPopupHelper extends React.PureComponent<Props, State> {
+ mounted = false;
state: State = { helpOpen: false };
componentDidMount() {
--- /dev/null
+/*
+ * 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);
*/
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}
--- /dev/null
+/*
+ * 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();
+});
// 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"
+ />
+ Twitter
+ </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
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>
--- /dev/null
+// 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>
+`;
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;
flex: 1;
}
+.flex-0 {
+ flex: 0 0 auto;
+}
+
.flex-shrink {
flex-shrink: 1;
min-width: 0;
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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 }} />;
+}
.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
*/
# 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
#------------------------------------------------------------------------------
#