Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/adding-a-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-async-connect';

import { loadEntities, setCurrentUser } from 'core/actions';
import { fetchProfile } from 'core/api';
import { loadEntities, setCurrentUser } from 'search/actions';

class UserPage extends React.Component {
static propTypes = {
Expand Down
2 changes: 2 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const newWebpackConfig = Object.assign({}, webpackConfigProd, {
new webpack.NormalModuleReplacementPlugin(/config$/, 'core/client/config.js'),
// Substitutes client only config.
new webpack.NormalModuleReplacementPlugin(/core\/logger$/, 'core/client/logger.js'),
// Use the browser's window for window.
new webpack.NormalModuleReplacementPlugin(/core\/window/, 'core/browserWindow.js'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might make it possible to cover more code via karma we should look at that separately.

],
devtool: 'inline-source-map',
module: {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,13 @@
"camelcase": "3.0.0",
"classnames": "2.2.5",
"config": "1.20.4",
"dompurify": "0.8.0",
"express": "4.13.4",
"extract-text-webpack-plugin": "1.0.1",
"helmet": "2.1.0",
"isomorphic-fetch": "2.2.1",
"jed": "1.1.0",
"jsdom": "9.2.0",
"normalize.css": "4.1.1",
"normalizr": "2.1.0",
"piping": "0.3.2",
Expand Down
16 changes: 16 additions & 0 deletions src/core/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,19 @@ export function setJWT(token) {
payload: {token},
};
}

export function loadEntities(entities) {
return {
type: 'ENTITIES_LOADED',
payload: {entities},
};
}

export function setCurrentUser(username) {
return {
type: 'SET_CURRENT_USER',
payload: {
username,
},
};
}
8 changes: 4 additions & 4 deletions src/core/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import 'isomorphic-fetch';

const API_BASE = `${config.get('apiHost')}${config.get('apiPath')}`;

const addon = new Schema('addons', {idAttribute: 'slug'});
const user = new Schema('users', {idAttribute: 'username'});
export const addon = new Schema('addons', {idAttribute: 'slug'});
export const user = new Schema('users', {idAttribute: 'username'});

function makeQueryString(query) {
return url.format({query});
}

function callApi({endpoint, schema, params, auth = false, state = {}, method = 'get', body,
credentials}) {
export function callApi({endpoint, schema, params, auth = false, state = {}, method = 'get', body,
credentials}) {
const queryString = makeQueryString(params);
const options = {
headers: {},
Expand Down
1 change: 1 addition & 0 deletions src/core/browserWindow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default window;
4 changes: 4 additions & 0 deletions src/core/purify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import createDOMPurify from 'dompurify';
import universalWindow from 'core/window';

export default createDOMPurify(universalWindow);
23 changes: 22 additions & 1 deletion src/core/reducers/addons.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,28 @@ const initialState = {};
export default function addon(state = initialState, action) {
const { payload } = action;
if (payload && payload.entities && payload.entities.addons) {
return {...state, ...payload.entities.addons};
const newState = {...state};
Object.keys(payload.entities.addons).forEach((key) => {
const thisAddon = payload.entities.addons[key];
if (thisAddon.theme_data) {
newState[key] = {
...thisAddon,
...thisAddon.theme_data,
guid: `${thisAddon.id}@personas.mozilla.org`,
};
delete newState[key].theme_data;
} else {
if (thisAddon.current_version && thisAddon.current_version.files.length > 0) {
newState[key] = {
...thisAddon,
installURL: thisAddon.current_version.files[0].url,
};
} else {
newState[key] = thisAddon;
}
}
});
return newState;
}
return state;
}
8 changes: 8 additions & 0 deletions src/core/window.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { jsdom } from 'jsdom';

export default jsdom('', {
features: {
FetchExternalResources: false, // disables resource loading over HTTP / filesystem
ProcessExternalResources: false, // do not execute JS within script blocks
},
}).defaultView;
8 changes: 8 additions & 0 deletions src/disco/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function discoResults(results) {
return {
type: 'DISCO_RESULTS',
payload: {
results,
},
};
}
16 changes: 16 additions & 0 deletions src/disco/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Schema, arrayOf } from 'normalizr';

import { addon, callApi } from 'core/api';

export const discoResult = new Schema('discoResults', {idAttribute: (result) => result.addon.slug});
discoResult.addon = addon;


export function getDiscoveryAddons({ api }) {
return callApi({
endpoint: 'discovery',
schema: {results: arrayOf(discoResult)},
params: {lang: 'en-US'},
state: api,
});
}
50 changes: 34 additions & 16 deletions src/disco/components/Addon.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import classNames from 'classnames';
import React, { PropTypes } from 'react';
import { sprintf } from 'sprintf-js';
import translate from 'core/i18n/translate';
import purify from 'core/purify';

import themeAction, { getThemeData } from 'disco/themePreview';

Expand All @@ -18,26 +19,32 @@ import {

import 'disco/css/Addon.scss';

function sanitizeHTML(text, allowTags = []) {
// TODO: Accept tags to allow and run through dom-purify.
return {
__html: purify.sanitize(text, {ALLOWED_TAGS: allowTags}),
};
}

export class Addon extends React.Component {
static propTypes = {
accentcolor: PropTypes.string,
closeErrorAction: PropTypes.func,
description: PropTypes.string,
editorialDescription: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
footerURL: PropTypes.string,
headerURL: PropTypes.string,
heading: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
i18n: PropTypes.string.isRequired,
imageURL: PropTypes.string,
iconUrl: PropTypes.string,
id: PropTypes.string.isRequired,
previewURL: PropTypes.string,
name: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired,
status: PropTypes.oneOf(validInstallStates).isRequired,
subHeading: PropTypes.string,
textcolor: PropTypes.string,
themeAction: PropTypes.func,
themeURL: PropTypes.string,
type: PropTypes.oneOf(validAddonTypes).isRequired,
}

Expand All @@ -60,15 +67,15 @@ export class Addon extends React.Component {
}

getLogo() {
const { imageURL } = this.props;
const { iconUrl } = this.props;
if (this.props.type === EXTENSION_TYPE) {
return <div className="logo"><img src={imageURL} alt="" /></div>;
return <div className="logo"><img src={iconUrl} alt="" /></div>;
}
return null;
}

getThemeImage() {
const { i18n, name, themeURL } = this.props;
const { i18n, name, previewURL } = this.props;
if (this.props.type === THEME_TYPE) {
return (<a href="#" className="theme-image"
data-browsertheme={this.getBrowserThemeData()}
Expand All @@ -77,13 +84,24 @@ export class Addon extends React.Component {
onFocus={this.previewTheme}
onMouseOut={this.resetPreviewTheme}
onMouseOver={this.previewTheme}>
<img src={themeURL} alt={sprintf(i18n.gettext('Preview %(name)s'), {name})} /></a>);
<img src={previewURL} alt={sprintf(i18n.gettext('Preview %(name)s'), {name})} /></a>);
}
return null;
}

getDescription() {
return { __html: this.props.editorialDescription };
const { i18n, description, type } = this.props;
if (type === THEME_TYPE) {
return (
<p className="editorial-description">{i18n.gettext('Hover over the image to preview')}</p>
);
}
return (
<div
ref="editorialDescription"
className="editorial-description"
dangerouslySetInnerHTML={sanitizeHTML(description, ['blockquote', 'cite'])} />
Copy link
Contributor

@muffinresearch muffinresearch May 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to have some tests just to prove this is working as we expect. E.g. just in-case a later lib update changed something etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes. We had discussed that but I forgot about it. Thanks!

);
}

handleClick = (e) => {
Expand All @@ -99,10 +117,10 @@ export class Addon extends React.Component {
}

render() {
const { heading, slug, subHeading, type } = this.props;
const { heading, slug, type } = this.props;

if (!validAddonTypes.includes(type)) {
throw new Error('Invalid addon type');
throw new Error(`Invalid addon type "${type}"`);
}

const addonClasses = classNames('addon', {
Expand All @@ -116,11 +134,11 @@ export class Addon extends React.Component {
<div className="content">
{this.getError()}
<div className="copy">
<h2 ref="heading" className="heading">{heading} {subHeading ?
<span ref="sub-heading" className="sub-heading">{subHeading}</span> : null}</h2>
<p ref="editorial-description"
className="editorial-description"
dangerouslySetInnerHTML={this.getDescription()} />
<h2
ref="heading"
className="heading"
dangerouslySetInnerHTML={sanitizeHTML(heading, ['span'])} />
{this.getDescription()}
</div>
<div className="install-button">
<InstallButton slug={slug} />
Expand Down
2 changes: 1 addition & 1 deletion src/disco/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const validInstallStates = [

// Add-on types.
export const EXTENSION_TYPE = 'extension';
export const THEME_TYPE = 'theme';
export const THEME_TYPE = 'persona';
// These types are not used.
// export const DICT_TYPE = 'dictionary';
// export const SEARCH_TYPE = 'search';
Expand Down
29 changes: 22 additions & 7 deletions src/disco/containers/DiscoPane.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { connect } from 'react-redux';
import { asyncConnect } from 'redux-async-connect';
import { camelCaseProps } from 'core/utils';

import { getDiscoveryAddons } from 'disco/api';
import { discoResults } from 'disco/actions';
import { loadEntities } from 'core/actions';

import Addon from 'disco/components/Addon';
import translate from 'core/i18n/translate';

Expand All @@ -13,7 +17,7 @@ import videoMp4 from 'disco/video/AddOns.mp4';
import videoWebm from 'disco/video/AddOns.webm';


class DiscoPane extends React.Component {
export class DiscoPane extends React.Component {
static propTypes = {
i18n: PropTypes.object.isRequired,
results: PropTypes.arrayOf(PropTypes.object),
Expand Down Expand Up @@ -72,15 +76,26 @@ class DiscoPane extends React.Component {
}
}

function loadDataIfNeeded() {
/* istanbul ignore next */
return Promise.resolve();
function loadedAddons(state) {
return state.discoResults.map((result) => ({...result, ...state.addons[result.addon]}));
}

export function loadDataIfNeeded({ store: { dispatch, getState }}) {
const state = getState();
const addons = loadedAddons(state);
if (addons.length > 0) {
return Promise.resolve();
}
return getDiscoveryAddons({api: state.api})
.then(({ entities, result }) => {
dispatch(loadEntities(entities));
dispatch(discoResults(result.results.map((r) => entities.discoResults[r])));
});
}

function mapStateToProps(state) {
const { addons } = state;
export function mapStateToProps(state) {
return {
results: [addons['japanese-tattoo'], addons['awesome-screenshot-capture-']],
results: loadedAddons(state),
};
}

Expand Down
42 changes: 23 additions & 19 deletions src/disco/css/Addon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ $addon-padding: 20px;
line-height: 1.5;
margin-top: 20px;

blockquote {
margin: 0;
}

.editorial-description {
color: $secondary-font-color;
font-size: 12px;
margin: 0.5em 0;
}

&.theme {
display: block;

Expand Down Expand Up @@ -49,6 +59,19 @@ $addon-padding: 20px;
}
}

.heading {
color: $primary-font-color;
font-size: 18px;
font-weight: medium;
margin: 0;

span {
color: $secondary-font-color;
font-size: 14px;
font-weight: normal;
}
}

.content {
display: flex;
align-items: center;
Expand Down Expand Up @@ -88,25 +111,6 @@ $addon-padding: 20px;
padding: 30px 20px;
flex-grow: 1;

.heading {
color: $primary-font-color;
font-size: 18px;
font-weight: medium;
margin: 0;
}

.sub-heading {
color: $secondary-font-color;
font-size: 14px;
font-weight: normal;
}

.editorial-description {
color: $secondary-font-color;
font-size: 12px;
margin: 0.5em 0;
}

// Remove the bottom margin of the last element.
& :last-child {
margin-bottom: 0;
Expand Down
Loading