From 0c54e07f8f7c86b9788f1460d13b5e013df18d17 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Wed, 12 Dec 2018 15:52:10 +0000 Subject: [PATCH 1/8] fetchSelected -> updateSelected --- src/actions/index.js | 8 ++------ src/components/Dashboard.jsx | 2 +- src/store/initial.js | 14 ++++---------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 590f56d..ce48225 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -126,18 +126,14 @@ export function updateDomain(domain) { } -export function fetchSelected(selected) { - if (!selected || !selected.length || selected.length === 0) { - return updateSelected([]) - } +export function fetchSource(source) { return dispatch => { - dispatch(updateSelected(selected)) if (!SOURCES_URL) { dispatch(fetchSourceError('No source extension specified.')) } else { dispatch(toggleFetchingSources()) - fetch(SOURCES_URL) + fetch(`${SOURCES_URL}`) .then(response => { if (!response.ok) { throw new Error('No sources are available at the URL specified in the config specified.') diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 744b990..ef5e0d5 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -51,7 +51,7 @@ class Dashboard extends React.Component { let eventsToSelect = selected.map(event => this.getEventById(event.id)); eventsToSelect = eventsToSelect.sort((a, b) => parseDate(a.timestamp) - parseDate(b.timestamp)) - this.props.actions.fetchSelected(eventsToSelect) + this.props.actions.updateSelected(eventsToSelect) } } diff --git a/src/store/initial.js b/src/store/initial.js index dbe0dd7..03d8fef 100644 --- a/src/store/initial.js +++ b/src/store/initial.js @@ -106,7 +106,7 @@ const initial = { ui: { style: { categories: { - default: 'red', + default: 'yellow', // Add here other categories to differentiate by color, like: alpha: '#00ff00', beta: '#ff0000', @@ -115,17 +115,11 @@ const initial = { narratives: { default: { - style: 'dotted', // ['dotted', 'solid'] - opacity: 0.9, // range between 0 and 1 - stroke: 'red', // Any hex or rgb code + style: 'solid', // ['dotted', 'solid'] + opacity: 0.5, // range between 0 and 1 + stroke: 'transparent', // Any hex or rgb code strokeWidth: 2 }, - narrative_1: { - style: 'solid', // ['dotted', 'solid'] - opacity: 0.4, // range between 0 and 1 - stroke: '#f18f01', // Any hex or rgb code - strokeWidth: 2 - } } }, dom: { From fb84d6883bbb756550a62d6055d819fd1441cc46 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Wed, 12 Dec 2018 17:00:22 +0000 Subject: [PATCH 2/8] basic source schema reading data in --- src/actions/index.js | 27 +++++++++++++++++---------- src/components/Dashboard.jsx | 2 +- src/reducers/schema/sourceSchema.js | 17 +++++++++++++++++ src/reducers/utils/validators.js | 14 ++++++++++---- webpack.config.js | 3 ++- 5 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 src/reducers/schema/sourceSchema.js diff --git a/src/actions/index.js b/src/actions/index.js index ce48225..1264aa2 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -81,8 +81,21 @@ export function fetchDomain () { .catch(handleError('tags')) } - return Promise.all([eventPromise, catPromise, narPromise, - sitesPromise, tagsPromise]) + let sourcesPromise = Promise.resolve([]) + if (process.env.features.USE_SOURCES) { + sourcesPromise = fetch(SOURCES_URL) + .then(response => response.json()) + .catch(handleError('sources')) + } + + return Promise.all([ + eventPromise, + catPromise, + narPromise, + sitesPromise, + tagsPromise, + sourcesPromise + ]) .then(response => { dispatch(toggleFetchingDomain()) const result = { @@ -91,6 +104,7 @@ export function fetchDomain () { narratives: response[2], sites: response[3], tags: response[4], + sources: response[5], notifications } return result @@ -114,14 +128,7 @@ export const UPDATE_DOMAIN = 'UPDATE_DOMAIN' export function updateDomain(domain) { return { type: UPDATE_DOMAIN, - domain: { - events: domain.events, - categories: domain.categories, - tags: domain.tags, - sites: domain.sites, - narratives: domain.narratives, - notifications: domain.notifications - } + domain } } diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index ef5e0d5..479be0a 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -32,7 +32,7 @@ class Dashboard extends React.Component { componentDidMount() { if (!this.props.app.isMobile) { this.props.actions.fetchDomain() - .then((domain) => this.props.actions.updateDomain(domain)); + .then(domain => this.props.actions.updateDomain(domain)); } } diff --git a/src/reducers/schema/sourceSchema.js b/src/reducers/schema/sourceSchema.js new file mode 100644 index 0000000..81c99f5 --- /dev/null +++ b/src/reducers/schema/sourceSchema.js @@ -0,0 +1,17 @@ +import Joi from 'joi'; + +const sourceSchema = Joi.object().keys({ + id: Joi.string().required(), + path: Joi.string().required(), + type: Joi.string().allow(''), + affil_1: Joi.string().allow(''), + affil_2: Joi.string().allow(''), + url: Joi.string().allow(''), + title: Joi.string().allow(''), + parent: Joi.string(), + author: Joi.string().allow(''), + date: Joi.string(), + notes: Joi.string().allow('') +}); + +export default sourceSchema; diff --git a/src/reducers/utils/validators.js b/src/reducers/utils/validators.js index ecc8f87..9831962 100644 --- a/src/reducers/utils/validators.js +++ b/src/reducers/utils/validators.js @@ -1,9 +1,10 @@ import Joi from 'joi'; -import eventSchema from '../schema/eventSchema.js'; -import categorySchema from '../schema/categorySchema.js'; -import siteSchema from '../schema/siteSchema.js'; -import narrativeSchema from '../schema/narrativeSchema.js'; +import eventSchema from '../schema/eventSchema'; +import categorySchema from '../schema/categorySchema'; +import siteSchema from '../schema/siteSchema'; +import narrativeSchema from '../schema/narrativeSchema'; +import sourceSchema from '../schema/sourceSchema' import { capitalize } from './helpers.js'; @@ -59,6 +60,7 @@ export function validateDomain (domain) { categories: [], sites: [], narratives: [], + sources: [], notifications: domain.notifications, tags: {} } @@ -68,6 +70,7 @@ export function validateDomain (domain) { categories: [], sites: [], narratives: [], + sources: [], } function validateItem(item, domainClass, schema) { @@ -95,6 +98,9 @@ export function validateDomain (domain) { domain.narratives.forEach(narrative => { validateItem(narrative, 'narratives', narrativeSchema); }); + domain.sources.forEach(source => { + validateItem(source, 'sources', sourceSchema); + }) // Message the number of failed items in domain diff --git a/webpack.config.js b/webpack.config.js index 4706f03..768e021 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -64,7 +64,8 @@ const config = { 'features': { 'USE_TAGS': JSON.stringify(userConfig.features.USE_TAGS), 'USE_SEARCH': JSON.stringify(userConfig.features.USE_SEARCH), - 'USE_SITES': JSON.stringify(userConfig.features.USE_SITES) + 'USE_SITES': JSON.stringify(userConfig.features.USE_SITES), + 'USE_SOURCES': JSON.stringify(userConfig.features.USE_SOURCES) } } }), From 7b8b81c788ce238db17219d3f3fa59eae1ee83d2 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 13 Dec 2018 11:56:04 +0000 Subject: [PATCH 3/8] interpolate sources using selector --- src/components/CardStack.jsx | 2 +- src/selectors/index.js | 26 +++++++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/components/CardStack.jsx b/src/components/CardStack.jsx index 98185be..bb47d71 100644 --- a/src/components/CardStack.jsx +++ b/src/components/CardStack.jsx @@ -89,7 +89,7 @@ class CardStack extends React.Component { function mapStateToProps(state) { return { - selected: state.app.selected, + selected: selectors.selectSelected(state), sourceError: state.app.errors.source, language: state.app.language, isCardstack: state.app.flags.isCardstack, diff --git a/src/selectors/index.js b/src/selectors/index.js index 76f8f40..8969136 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -1,16 +1,19 @@ -import { - createSelector -} from 'reselect' +import { createSelector} from 'reselect' // Input selectors export const getEvents = state => state.domain.events; export const getLocations = state => state.domain.locations; export const getCategories = state => state.domain.categories; export const getNarratives = state => state.domain.narratives; +export const getSelected = state => state.app.selected; export const getSites = (state) => { if (process.env.features.USE_SITES) return state.domain.sites; return []; } +export const getSources = state => { + if (process.env.features.USE_SOURCES) return state.domain.sources; + return []; +} export const getNotifications = state => state.domain.notifications; export const getTagTree = state => state.domain.tags; export const getTagsFilter = state => state.app.filters.tags; @@ -152,6 +155,23 @@ export const selectLocations = createSelector( } ); +/** + * Of all the sources, select those that are relevant to the selected events. + */ +export const selectSelected = createSelector( + [getSelected, getSources], + (selected, sources) => { + if (selected.length === 0) { + return [] + } + const sourceIds = selected.map(e => e.source) + const srcs = sources.filter(s => s.id.indexOf(sourceIds) > -1) + return selected.map((s, idx) => ({ + ...s, + source: srcs[idx] + })) + } +) /* * Select categories, return them as a list From 8435df1430585b3679b791737ad6d04b71018a45 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 13 Dec 2018 13:35:11 +0000 Subject: [PATCH 4/8] load sources as object for more efficient access --- src/reducers/schema/sourceSchema.js | 4 +-- src/reducers/utils/validators.js | 53 ++++++++++++++++++----------- src/selectors/index.js | 6 ++-- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/reducers/schema/sourceSchema.js b/src/reducers/schema/sourceSchema.js index 81c99f5..3dc17dd 100644 --- a/src/reducers/schema/sourceSchema.js +++ b/src/reducers/schema/sourceSchema.js @@ -8,9 +8,9 @@ const sourceSchema = Joi.object().keys({ affil_2: Joi.string().allow(''), url: Joi.string().allow(''), title: Joi.string().allow(''), - parent: Joi.string(), + parent: Joi.string().allow(''), author: Joi.string().allow(''), - date: Joi.string(), + date: Joi.string().allow(''), notes: Joi.string().allow('') }); diff --git a/src/reducers/utils/validators.js b/src/reducers/utils/validators.js index 9831962..42bf3d3 100644 --- a/src/reducers/utils/validators.js +++ b/src/reducers/utils/validators.js @@ -60,7 +60,7 @@ export function validateDomain (domain) { categories: [], sites: [], narratives: [], - sources: [], + sources: {}, notifications: domain.notifications, tags: {} } @@ -73,34 +73,47 @@ export function validateDomain (domain) { sources: [], } - function validateItem(item, domainClass, schema) { + function validateArrayItem(item, domainKey, schema) { const result = Joi.validate(item, schema); if (result.error !== null) { const id = item.id || '-'; - const domainStr = capitalize(domainClass); + const domainStr = capitalize(domainKey); const error = makeError(domainStr, id, result.error.message); - discardedDomain[domainClass].push(Object.assign(item, { error })); + discardedDomain[domainKey].push(Object.assign(item, { error })); } else { - sanitizedDomain[domainClass].push(item); + sanitizedDomain[domainKey].push(item); } } - domain.events.forEach(event => { - validateItem(event, 'events', eventSchema); - }); - domain.categories.forEach(category => { - validateItem(category, 'categories', categorySchema); - }); - domain.sites.forEach(site => { - validateItem(site, 'sites', siteSchema); - }); - domain.narratives.forEach(narrative => { - validateItem(narrative, 'narratives', narrativeSchema); - }); - domain.sources.forEach(source => { - validateItem(source, 'sources', sourceSchema); - }) + function validateArray(items, domainKey, schema) { + items.forEach(item => { + validateArrayItem(item, domainKey, schema) + }) + } + + function validateObject(obj, domainKey, itemSchema) { + Object.keys(obj).forEach(key => { + const vl = obj[key] + const result = Joi.validate(vl, itemSchema) + if (result.error !== null) { + const id = vl.id || '-' + const domainStr = capitalize(domainKey) + discardedDomain[domainKey].push({ + ...vl, + error: makeError(domainStr, id, result.error.message) + }) + } else { + sanitizedDomain[domainKey][key] = vl + } + }) + } + + validateArray(domain.events, 'events', eventSchema); + validateArray(domain.categories, 'categories', categorySchema); + validateArray(domain.sites, 'sites', siteSchema); + validateArray(domain.narratives, 'narratives', narrativeSchema); + validateObject(domain.sources, 'sources', sourceSchema); // Message the number of failed items in domain diff --git a/src/selectors/index.js b/src/selectors/index.js index 8969136..9f5fa16 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -164,8 +164,10 @@ export const selectSelected = createSelector( if (selected.length === 0) { return [] } - const sourceIds = selected.map(e => e.source) - const srcs = sources.filter(s => s.id.indexOf(sourceIds) > -1) + const srcs = selected + .map(e => e.source) + .map(id => sources[id]) + return selected.map((s, idx) => ({ ...s, source: srcs[idx] From c85b0c9001ccdd4229c8ea809a516403d3a7b61b Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 13 Dec 2018 17:40:30 +0000 Subject: [PATCH 5/8] adjust selectors to handle array of sources --- src/reducers/schema/eventSchema.js | 2 +- src/selectors/index.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/reducers/schema/eventSchema.js b/src/reducers/schema/eventSchema.js index 9dd4f90..b497841 100644 --- a/src/reducers/schema/eventSchema.js +++ b/src/reducers/schema/eventSchema.js @@ -12,7 +12,7 @@ const eventSchema = Joi.object().keys({ type: Joi.string().allow(''), category: Joi.string().required(), narrative: Joi.string().allow(''), - source: Joi.string().allow(''), + sources: Joi.array(), tags: Joi.string().allow(''), comments: Joi.string().allow(''), timestamp: Joi.string().required(), diff --git a/src/selectors/index.js b/src/selectors/index.js index 9f5fa16..32a4e49 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -165,12 +165,14 @@ export const selectSelected = createSelector( return [] } const srcs = selected - .map(e => e.source) - .map(id => sources[id]) + .map(e => e.sources) + .map(_sources => + _sources.map(id => sources[id]) + ) return selected.map((s, idx) => ({ ...s, - source: srcs[idx] + sources: srcs[idx] })) } ) From cccbcb9e5456464cb971a13286bc998c1b486413 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 13 Dec 2018 17:47:35 +0000 Subject: [PATCH 6/8] infra for multiple sources in Card --- src/components/Card.jsx | 10 ++++++---- src/components/presentational/CardSource.js | 10 +++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/Card.jsx b/src/components/Card.jsx index 520b7b9..25e2d01 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -91,10 +91,12 @@ class Card extends React.Component { ({ + ...s, + error: this.props.sourceError + })), + ]} /> ) } diff --git a/src/components/presentational/CardSource.js b/src/components/presentational/CardSource.js index 95fdb06..9033877 100644 --- a/src/components/presentational/CardSource.js +++ b/src/components/presentational/CardSource.js @@ -3,10 +3,10 @@ import Spinner from './Spinner' import copy from '../../js/data/copy.json' -const CardSource = ({ source, language, isLoading, error }) => { +const CardSource = ({ sources, language, isLoading, error }) => { const source_lang = copy[language].cardstack.source - function renderSource() { + function renderSource(source) { return source.error ? (
{source.error}
) : ( @@ -15,7 +15,11 @@ const CardSource = ({ source, language, isLoading, error }) => { } function renderContent() { - return isLoading ? : renderSource() + return isLoading + ? + : sources.map( + source => renderSource(source) + ) } return ( From 7267f821abf88d3147531395ca5ae30d1539ef91 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 13 Dec 2018 17:53:13 +0000 Subject: [PATCH 7/8] factor sources map to Card from CardSource --- src/components/Card.jsx | 18 ++++++++--------- src/components/presentational/CardSource.js | 22 ++++++++++----------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/components/Card.jsx b/src/components/Card.jsx index 25e2d01..b85a144 100644 --- a/src/components/Card.jsx +++ b/src/components/Card.jsx @@ -86,19 +86,17 @@ class Card extends React.Component { ) } - renderSource() { - return ( + renderSources() { + return this.props.event.sources.map(source => ( ({ - ...s, - error: this.props.sourceError - })), - ]} + source={{ + ...source, + error: this.props.sourceError + }} /> - ) + )) } // NB: should be internaionalized. @@ -147,7 +145,7 @@ class Card extends React.Component { return (
{this.renderTags()} - {this.renderSource()} + {this.renderSources()} {this.renderNarrative()}
) diff --git a/src/components/presentational/CardSource.js b/src/components/presentational/CardSource.js index 9033877..4213abc 100644 --- a/src/components/presentational/CardSource.js +++ b/src/components/presentational/CardSource.js @@ -3,23 +3,21 @@ import Spinner from './Spinner' import copy from '../../js/data/copy.json' -const CardSource = ({ sources, language, isLoading, error }) => { - const source_lang = copy[language].cardstack.source +function renderSource(source) { + return source.error ? ( +
{source.error}
+ ) : ( +

TODO: display source properly.

+ ) +} - function renderSource(source) { - return source.error ? ( -
{source.error}
- ) : ( -

TODO: display source properly.

- ) - } +const CardSource = ({ source, language, isLoading, error }) => { + const source_lang = copy[language].cardstack.source function renderContent() { return isLoading ? - : sources.map( - source => renderSource(source) - ) + : renderSource(source) } return ( From e09983ce1e9b5d801446b620641a733890c110f3 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 13 Dec 2018 18:00:33 +0000 Subject: [PATCH 8/8] make card wider --- src/components/presentational/CardSource.js | 6 ++++-- src/scss/cardstack.scss | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/presentational/CardSource.js b/src/components/presentational/CardSource.js index 4213abc..fe67652 100644 --- a/src/components/presentational/CardSource.js +++ b/src/components/presentational/CardSource.js @@ -7,7 +7,9 @@ function renderSource(source) { return source.error ? (
{source.error}
) : ( -

TODO: display source properly.

+
+

{source.id}

+
) } @@ -21,7 +23,7 @@ const CardSource = ({ source, language, isLoading, error }) => { } return ( -
+

{source_lang}:

{renderContent()}
diff --git a/src/scss/cardstack.scss b/src/scss/cardstack.scss index 70393db..1879969 100644 --- a/src/scss/cardstack.scss +++ b/src/scss/cardstack.scss @@ -1,6 +1,8 @@ @import 'burger'; @import 'card'; +$card-width: 500px; + .card-stack { position: absolute; top: 10px; @@ -20,7 +22,7 @@ .card-stack-header { min-height: 38px; line-height: 38px; - width: 360px; + width: $card-width; box-sizing: border-box; padding: 0 5px; background: $black; @@ -61,7 +63,7 @@ } .card-stack-content { - width: 360px; + width: $card-width; ul { padding: 0;