From d24ebe296819edc403d11c0d184499fb89369603 Mon Sep 17 00:00:00 2001 From: Adam Hillier Date: Thu, 24 Aug 2017 13:18:37 -0400 Subject: [PATCH] feat(sections): Add section ordering, and custom sections to the pref pane (#3216) Move TopStories config out of TopStoriesFeed and PreferencesPane and into SectionsManager.jsm. Underlying work for #3194. --- docs/v2-system-addon/sections.md | 90 +++++------ system-addon/common/Actions.jsm | 4 +- system-addon/common/Reducers.jsm | 21 ++- .../PreferencesPane/PreferencesPane.jsx | 55 ++++--- .../components/Sections/Sections.jsx | 6 +- system-addon/lib/SectionsManager.jsm | 109 +++++++++++-- system-addon/lib/TopStoriesFeed.jsm | 68 +++----- .../test/unit/common/Reducers.test.js | 38 ++++- .../components/PreferencesPane.test.jsx | 42 +++-- .../content-src/components/Sections.test.jsx | 12 +- .../test/unit/lib/SectionsManager.test.js | 153 ++++++++++++++---- .../test/unit/lib/TopStoriesFeed.test.js | 147 ++++++++--------- system-addon/test/unit/utils.js | 15 ++ 13 files changed, 479 insertions(+), 281 deletions(-) diff --git a/docs/v2-system-addon/sections.md b/docs/v2-system-addon/sections.md index 35e5007d5..98df80be9 100644 --- a/docs/v2-system-addon/sections.md +++ b/docs/v2-system-addon/sections.md @@ -1,66 +1,65 @@ # Sections in Activity Stream Each section in Activity Stream displays data from a corresponding section feed -in a standardised `Section` UI component. Section feeds are responsible for -registering/deregistering their UI component and supplying rows data (the sites -for the section to display). - -Activity Stream loads sections from the `SECTIONS` map in `ActivityStream.jsm`. -Configuration objects must be keyed by a unique section id and have the -properties `{feed, showByDefault}`, where `feed` is the section feed class. +in a standardised `Section` UI component. Each section feed is responsible for +listening to events and updating the section options such as the title, icon, +and rows (the cards for the section to display). The `Section` UI component displays the rows provided by the section feed. If no rows are available it displays an empty state consisting of an icon and a message. Optionally, the section may have a info option menu that is displayed when users hover over the info icon. +On load, `SectionsManager` and `SectionsFeed` in `SectionsManager.jsm` add the +sections configured in the `BUILT_IN_SECTIONS` map to the state. These sections +are initially disabled, so aren't visible. The section's feed may use the +methods provided by the `SectionsManager` to enable its section and update its +properties. + +The section configuration in `BUILT_IN_SECTIONS` constists of a generator +function keyed by the pref name for the section feed. The generator function +takes an `options` argument as the only parameter, which is passed the object +stored as serialised JSON in the pref `{feed_pref_name}.options`, or the empty +object if this doesn't exist. The generator returns a section configuration +object which may have the following properties: + +Property | Type | Description +--- | --- | --- +id | String | Non-optional unique id. +title | Localisation object | Has property `id`, the string localisation id, and optionaly a `values` object to fill in placeholders. +icon | String | Icon id. New icons should be added in icons.scss. +maxRows | Integer | Maximum number of rows of cards to display. Should be >= 1. +contextMenuOptions | Array of strings | The menu options to provide in the card context menus. +shouldHidePref | Boolean | If true, will the section preference in the preferences pane will not be shown. +pref | Object | Configures the section preference to show in the preferences pane. Has properties `titleString` and `descString`. +emptyState | Object | Configures the empty state of the section. Has properties `message` and `icon`. +infoOption | Object | Configures the info option. Has properties `header`, `body`, and `link`. + ## Section feeds -Each section feed is given the pref -`{Activity Stream pref branch}.feeds.section.{section_id}`. This pref turns the -section feed on and off. +Each section feed should be controlled by the pref `feeds.section.{section_id}`. -### Registering the section +### Enabling the section -The section feed must listen for the events `INIT` (dispatched -when Activity Stream is initialised) and `FEED_INIT` (dispatched when a feed is -re-enabled having been turned off, with the feed id as the `data`) and respond -by dispatching a `SECTION_REGISTER` action to enable the section's UI component. -The action's `data` object should have the following properties: +The section feed must listen for the events `INIT` (dispatched when Activity +Stream is initialised) and `FEED_INIT` (dispatched when a feed is re-enabled +having been turned off, with the feed id as the `data`). On these events it must +call `SectionsManager.enableSection(id)`. Care should be taken that this happens +only once `SectionsManager` has also initialised; the feed can use the method +`SectionsManager.onceInitialized()`. -```js -{ - id, // Unique section id - icon, // Section icon id - new icons should be added to icons.scss - title: {id, values}, // Title localisation id and placeholder values - maxCards, // Max number of site cards to dispay - contextMenuOptions, // Default context-menu options for cards - infoOption: { // Info option dialog - header: {id, values}, // Header localisation id and values - body: {id, values}, // Body localisation id and values - link: {href, id, values}, // Link href, localisation id and values - }, - emptyState: { // State if no cards are visible - message: {id, values}, // Message localisation id and values - icon // Icon id - new icons should be added to icons.scss - } -} -``` - -### Deregistering the section +### Disabling the section The section feed must have an `uninit` method. This is called when the section -feed is disabled by turning the section's pref off. - -In `uninit` the feed must broadcast a `SECTION_DEREGISTER` action with the -section id as the data. This will remove the section's UI component from every -existing Activity Stream page. +feed is disabled by turning the section's pref off. In `uninit` the feed must +call `SectionsManager.disableSection(id)`. This will remove the section's UI +component from every existing Activity Stream page. ### Updating the section rows -The section feed can dispatch a `SECTION_ROWS_UPDATE` action to update its rows. -The action's data object must be passed the section's `id` and an array `rows` -of sites to display. Each site object must have the following properties: +The section feed can call `SectionsManager.updateSection(id, options)` to update +section options. The `rows` array property of `options` stores the cards of +sites to display. Each card object may have the following properties: ```js { @@ -71,6 +70,3 @@ of sites to display. Each site object must have the following properties: url // Site url } ``` - -Optionally, rows can also be passed with the `SECTION_REGISTER` action if the -feed already has rows to display. diff --git a/system-addon/common/Actions.jsm b/system-addon/common/Actions.jsm index b39010e27..0cd24d0b5 100644 --- a/system-addon/common/Actions.jsm +++ b/system-addon/common/Actions.jsm @@ -56,8 +56,10 @@ for (const type of [ "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", + "SECTION_DISABLE", + "SECTION_ENABLE", "SECTION_REGISTER", - "SECTION_ROWS_UPDATE", + "SECTION_UPDATE", "SET_PREF", "SNIPPETS_DATA", "SNIPPETS_RESET", diff --git a/system-addon/common/Reducers.jsm b/system-addon/common/Reducers.jsm index 9965dad97..d5fb508fc 100644 --- a/system-addon/common/Reducers.jsm +++ b/system-addon/common/Reducers.jsm @@ -189,14 +189,27 @@ function Sections(prevState = INITIAL_STATE.Sections, action) { } return section; }); - // If section doesn't exist in prevState, create a new section object and - // append it to the sections state + + // Invariant: Sections array sorted in increasing order of property `order`. + // If section doesn't exist in prevState, create a new section object. If + // the section has an order, insert it at the correct place in the array. + // Otherwise, prepend it and set the order to be minimal. if (!hasMatch) { const initialized = action.data.rows && action.data.rows.length > 0; - newState.push(Object.assign({title: "", initialized, rows: []}, action.data)); + let order; + let index; + if (prevState.length > 0) { + order = action.data.order || prevState[0].order - 1; + index = newState.findIndex(section => section.order >= order); + } else { + order = action.data.order || 1; + index = 0; + } + const section = Object.assign({title: "", initialized, rows: [], order, enabled: false}, action.data); + newState.splice(index, 0, section); } return newState; - case at.SECTION_ROWS_UPDATE: + case at.SECTION_UPDATE: return prevState.map(section => { if (section && section.id === action.data.id) { return Object.assign({}, section, action.data); diff --git a/system-addon/content-src/components/PreferencesPane/PreferencesPane.jsx b/system-addon/content-src/components/PreferencesPane/PreferencesPane.jsx index eb1e23c0f..b7200db13 100644 --- a/system-addon/content-src/components/PreferencesPane/PreferencesPane.jsx +++ b/system-addon/content-src/components/PreferencesPane/PreferencesPane.jsx @@ -1,15 +1,20 @@ const React = require("react"); const {connect} = require("react-redux"); const {injectIntl, FormattedMessage} = require("react-intl"); -const {actionCreators: ac} = require("common/Actions.jsm"); +const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm"); + +const getFormattedMessage = message => + (typeof message === "string" ? {message} : ); const PreferencesInput = props => (
- {props.descStringId &&

} + {props.descString &&

+ {getFormattedMessage(props.descString)} +

}
); @@ -18,18 +23,9 @@ class PreferencesPane extends React.Component { super(props); this.state = {visible: false}; this.handleClickOutside = this.handleClickOutside.bind(this); - this.handleChange = this.handleChange.bind(this); + this.handlePrefChange = this.handlePrefChange.bind(this); + this.handleSectionChange = this.handleSectionChange.bind(this); this.togglePane = this.togglePane.bind(this); - - // TODO This is temporary until sections register their PreferenceInput component automatically - const optionJSON = props.Prefs.values["feeds.section.topstories.options"]; - if (optionJSON) { - try { - this.topStoriesOptions = JSON.parse(optionJSON); - } catch (e) { - console.error("Problem parsing feeds.section.topstories.options", e); // eslint-disable-line no-console - } - } } componentDidMount() { document.addEventListener("click", this.handleClickOutside); @@ -43,10 +39,16 @@ class PreferencesPane extends React.Component { this.togglePane(); } } - handleChange(event) { + handlePrefChange(event) { const target = event.target; this.props.dispatch(ac.SetPref(target.name, target.checked)); } + handleSectionChange(event) { + const target = event.target; + const id = target.name; + const type = target.checked ? at.SECTION_ENABLE : at.SECTION_DISABLE; + this.props.dispatch(ac.SendToMain({type, data: id})); + } togglePane() { this.setState({visible: !this.state.visible}); const event = this.state.visible ? "CLOSE_NEWTAB_PREFS" : "OPEN_NEWTAB_PREFS"; @@ -55,6 +57,7 @@ class PreferencesPane extends React.Component { render() { const props = this.props; const prefs = props.Prefs.values; + const sections = props.Sections; const isVisible = this.state.visible; return (
@@ -70,17 +73,19 @@ class PreferencesPane extends React.Component {

- + - + + + {sections + .filter(section => !section.shouldHidePref) + .map(({id, title, enabled, pref}) => + )} - {this.topStoriesOptions && !this.topStoriesOptions.hidden && - }