Bug 1520514 - Add memoized selector to add feed data and spocs to layout
This commit is contained in:
Родитель
6967f2e7a0
Коммит
7529c0a4bc
|
@ -6,6 +6,7 @@ import {ImpressionStats} from "content-src/components/DiscoveryStreamImpressionS
|
|||
import {List} from "content-src/components/DiscoveryStreamComponents/List/List";
|
||||
import React from "react";
|
||||
import {SectionTitle} from "content-src/components/DiscoveryStreamComponents/SectionTitle/SectionTitle";
|
||||
import {selectLayoutRender} from "content-src/lib/selectLayoutRender";
|
||||
import {TopSites} from "content-src/components/DiscoveryStreamComponents/TopSites/TopSites";
|
||||
|
||||
// According to the Pocket API endpoint specs, `component.properties.items` is a required property with following values:
|
||||
|
@ -27,19 +28,19 @@ export class _DiscoveryStreamBase extends React.PureComponent {
|
|||
return (<SectionTitle />);
|
||||
case "CardGrid":
|
||||
return (<CardGrid
|
||||
title={component.header.title}
|
||||
title={component.header && component.header.title}
|
||||
data={component.data}
|
||||
feed={component.feed}
|
||||
style={component.properties.style}
|
||||
items={component.properties.items} />);
|
||||
case "Hero":
|
||||
const feed = this.props.DiscoveryStream.feeds[component.feed.url];
|
||||
const items = Math.min(component.properties.items, MAX_ROWS_HERO);
|
||||
const rows = feed ? feed.data.recommendations.slice(0, items) : [];
|
||||
const rows = component.data ? component.data.recommendations.slice(0, items) : [];
|
||||
return (
|
||||
<ImpressionStats rows={rows} dispatch={this.props.dispatch} source={component.type}>
|
||||
<Hero
|
||||
title={component.header.title}
|
||||
feed={component.feed}
|
||||
title={component.header && component.header.title}
|
||||
data={component.data}
|
||||
style={component.properties.style}
|
||||
items={items} />
|
||||
</ImpressionStats>
|
||||
|
@ -56,10 +57,10 @@ export class _DiscoveryStreamBase extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {layout} = this.props.DiscoveryStream;
|
||||
const {layoutRender} = this.props.DiscoveryStream;
|
||||
return (
|
||||
<div className="discovery-stream ds-layout">
|
||||
{layout.map((row, rowIndex) => (
|
||||
{layoutRender.map((row, rowIndex) => (
|
||||
<div key={`row-${rowIndex}`} className={`ds-column ds-column-${row.width}`}>
|
||||
{row.components.map((component, componentIndex) => (
|
||||
<div key={`component-${componentIndex}`}>
|
||||
|
@ -73,4 +74,13 @@ export class _DiscoveryStreamBase extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
export const DiscoveryStreamBase = connect(state => ({DiscoveryStream: state.DiscoveryStream}))(_DiscoveryStreamBase);
|
||||
function transform(state) {
|
||||
return {
|
||||
DiscoveryStream: {
|
||||
...state.DiscoveryStream,
|
||||
layoutRender: selectLayoutRender(state),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const DiscoveryStreamBase = connect(transform)(_DiscoveryStreamBase);
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import {connect} from "react-redux";
|
||||
import {DSCard} from "../DSCard/DSCard.jsx";
|
||||
import React from "react";
|
||||
|
||||
export class _CardGrid extends React.PureComponent {
|
||||
export class CardGrid extends React.PureComponent {
|
||||
render() {
|
||||
const feed = this.props.DiscoveryStream.feeds[this.props.feed.url];
|
||||
const {data} = this.props;
|
||||
|
||||
// Handle a render before feed has been fetched by displaying nothing
|
||||
if (!feed) {
|
||||
if (!data) {
|
||||
return (
|
||||
<div />
|
||||
);
|
||||
}
|
||||
|
||||
let cards = feed.data.recommendations.slice(0, this.props.items).map((rec, index) => (
|
||||
let cards = data.recommendations.slice(0, this.props.items).map((rec, index) => (
|
||||
<DSCard
|
||||
key={`dscard-${index}`}
|
||||
image_src={rec.image_src}
|
||||
|
@ -31,9 +30,7 @@ export class _CardGrid extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_CardGrid.defaultProps = {
|
||||
CardGrid.defaultProps = {
|
||||
style: `border`,
|
||||
items: 4, // Number of stories to display
|
||||
};
|
||||
|
||||
export const CardGrid = connect(state => ({DiscoveryStream: state.DiscoveryStream}))(_CardGrid);
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
import {connect} from "react-redux";
|
||||
import {DSCard} from "../DSCard/DSCard.jsx";
|
||||
import React from "react";
|
||||
|
||||
export class _Hero extends React.PureComponent {
|
||||
export class Hero extends React.PureComponent {
|
||||
render() {
|
||||
const feed = this.props.DiscoveryStream.feeds[this.props.feed.url];
|
||||
const {data} = this.props;
|
||||
|
||||
// Handle a render before feed has been fetched by displaying nothing
|
||||
if (!feed) {
|
||||
if (!data || !data.recommendations) {
|
||||
return (
|
||||
<div />
|
||||
);
|
||||
}
|
||||
|
||||
let [heroRec, ...otherRecs] = feed.data.recommendations;
|
||||
let [heroRec, ...otherRecs] = data.recommendations;
|
||||
let truncateText = (text, cap) => `${text.substring(0, cap)}${text.length > cap ? `...` : ``}`;
|
||||
|
||||
let cards = otherRecs.slice(1, this.props.items).map((rec, index) => (
|
||||
|
@ -48,9 +47,8 @@ export class _Hero extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
_Hero.defaultProps = {
|
||||
Hero.defaultProps = {
|
||||
data: {},
|
||||
style: `border`,
|
||||
items: 1, // Number of stories to display
|
||||
};
|
||||
|
||||
export const Hero = connect(state => ({DiscoveryStream: state.DiscoveryStream}))(_Hero);
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import {createSelector} from "reselect";
|
||||
|
||||
export const selectLayoutRender = createSelector(
|
||||
// Selects layout, feeds, spocs so that we only recompute if
|
||||
// any of these values change.
|
||||
[
|
||||
state => state.DiscoveryStream.layout,
|
||||
state => state.DiscoveryStream.feeds,
|
||||
state => state.DiscoveryStream.spocs,
|
||||
],
|
||||
|
||||
// Adds data to each component from feeds. This function only re-runs if one of the inputs change.
|
||||
// TODO: calculate spocs
|
||||
function layoutRender(layout, feeds, spocs) {
|
||||
return layout.map(row => ({
|
||||
...row,
|
||||
|
||||
// Loops through all the components and adds a .data property
|
||||
// containing data from feeds
|
||||
components: row.components.map(component => {
|
||||
if (!component.feed || !feeds[component.feed.url]) {
|
||||
return component;
|
||||
}
|
||||
return {...component, data: feeds[component.feed.url].data};
|
||||
}),
|
||||
}));
|
||||
}
|
||||
);
|
|
@ -10159,6 +10159,11 @@
|
|||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"reselect": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz",
|
||||
"integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA=="
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz",
|
||||
|
|
|
@ -13,7 +13,8 @@
|
|||
"react-dom": "16.2.0",
|
||||
"react-intl": "2.4.0",
|
||||
"react-redux": "5.1.0",
|
||||
"redux": "4.0.1"
|
||||
"redux": "4.0.1",
|
||||
"reselect": "4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "15.17.0",
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import {combineReducers, createStore} from "redux";
|
||||
import {actionTypes as at} from "common/Actions.jsm";
|
||||
import {reducers} from "common/Reducers.jsm";
|
||||
import {selectLayoutRender} from "content-src/lib/selectLayoutRender";
|
||||
|
||||
const FAKE_LAYOUT = [{width: 3, components: [{type: "foo", feed: {url: "foo.com"}}]}];
|
||||
const FAKE_FEEDS = {"foo.com": {data: ["foo", "bar"]}};
|
||||
|
||||
describe("selectLayoutRender", () => {
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(combineReducers(reducers));
|
||||
});
|
||||
|
||||
it("should return an empty array given initial state", () => {
|
||||
const result = selectLayoutRender(store.getState());
|
||||
assert.deepEqual(result, []);
|
||||
});
|
||||
|
||||
it("should add .data property from feeds to each compontent in .layout", () => {
|
||||
store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: FAKE_LAYOUT}});
|
||||
store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE, data: FAKE_FEEDS});
|
||||
|
||||
const result = selectLayoutRender(store.getState());
|
||||
|
||||
assert.lengthOf(result, 1);
|
||||
assert.propertyVal(result[0], "width", 3);
|
||||
assert.deepEqual(result[0].components[0], {type: "foo", feed: {url: "foo.com"}, data: ["foo", "bar"]});
|
||||
});
|
||||
|
||||
it("should return layout property without data if feed isn't available", () => {
|
||||
store.dispatch({type: at.DISCOVERY_STREAM_LAYOUT_UPDATE, data: {layout: FAKE_LAYOUT}});
|
||||
store.dispatch({type: at.DISCOVERY_STREAM_FEEDS_UPDATE, data: {}});
|
||||
|
||||
const result = selectLayoutRender(store.getState());
|
||||
|
||||
assert.lengthOf(result, 1);
|
||||
assert.propertyVal(result[0], "width", 3);
|
||||
assert.deepEqual(result[0].components[0], FAKE_LAYOUT[0].components[0]);
|
||||
});
|
||||
});
|
Загрузка…
Ссылка в новой задаче