Merge pull request #50 from forensic-architecture/topic/sources

Topic/sources
This commit is contained in:
Lachlan Kermode
2018-12-14 11:09:28 +00:00
committed by GitHub
12 changed files with 135 additions and 71 deletions

View File

@@ -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,30 +128,19 @@ 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
}
}
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.')

View File

@@ -86,17 +86,17 @@ class Card extends React.Component {
)
}
renderSource() {
return (
renderSources() {
return this.props.event.sources.map(source => (
<CardSource
isLoading={this.props.isLoading}
language={this.props.language}
source={{
...this.props.source,
...source,
error: this.props.sourceError
}}
/>
)
))
}
// NB: should be internaionalized.
@@ -145,7 +145,7 @@ class Card extends React.Component {
return (
<div className="card-bottomhalf">
{this.renderTags()}
{this.renderSource()}
{this.renderSources()}
{this.renderNarrative()}
</div>
)

View File

@@ -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,

View File

@@ -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));
}
}
@@ -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)
}
}

View File

@@ -3,23 +3,27 @@ import Spinner from './Spinner'
import copy from '../../js/data/copy.json'
function renderSource(source) {
return source.error ? (
<div><small>{source.error}</small></div>
) : (
<div>
<p>{source.id}</p>
</div>
)
}
const CardSource = ({ source, language, isLoading, error }) => {
const source_lang = copy[language].cardstack.source
function renderSource() {
return source.error ? (
<div><small>{source.error}</small></div>
) : (
<div><p>TODO: display source properly.</p></div>
)
}
function renderContent() {
return isLoading ? <Spinner/> : renderSource()
return isLoading
? <Spinner/>
: renderSource(source)
}
return (
<div className="card-col card-cell source">
<div className="card-row card-cell source">
<h4>{source_lang}: </h4>
{renderContent()}
</div>

View File

@@ -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(),

View File

@@ -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().allow(''),
author: Joi.string().allow(''),
date: Joi.string().allow(''),
notes: Joi.string().allow('')
});
export default sourceSchema;

View File

@@ -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,33 +70,50 @@ export function validateDomain (domain) {
categories: [],
sites: [],
narratives: [],
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);
});
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

View File

@@ -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;

View File

@@ -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,27 @@ 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 srcs = selected
.map(e => e.sources)
.map(_sources =>
_sources.map(id => sources[id])
)
return selected.map((s, idx) => ({
...s,
sources: srcs[idx]
}))
}
)
/*
* Select categories, return them as a list

View File

@@ -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: {

View File

@@ -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)
}
}
}),