Merging in develop branch

This commit is contained in:
efarooqui
2020-10-20 15:26:15 -07:00
12 changed files with 122 additions and 77 deletions

View File

@@ -4,7 +4,7 @@ import { urlFromEnv } from '../common/utilities'
// TODO: relegate these URLs entirely to environment variables
// const CONFIG_URL = urlFromEnv('CONFIG_EXT')
const EVENT_DATA_URL = urlFromEnv('EVENTS_EXT')
const CATEGORY_URL = urlFromEnv('CATEGORIES_EXT')
// const CATEGORY_URL = urlFromEnv('CATEGORIES_EXT')
const ASSOCIATIONS_URL = urlFromEnv('ASSOCIATIONS_EXT')
const SOURCES_URL = urlFromEnv('SOURCES_EXT')
const SITES_URL = urlFromEnv('SITES_EXT')
@@ -42,13 +42,6 @@ export function fetchDomain () {
)
).then(results => results.flatMap(t => t))
let catPromise = Promise.resolve([])
if (features.USE_CATEGORIES) {
catPromise = fetch(CATEGORY_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('categories')))
}
let associationsPromise = Promise.resolve([])
if (features.USE_ASSOCIATIONS) {
if (!ASSOCIATIONS_URL) {
@@ -87,7 +80,6 @@ export function fetchDomain () {
return Promise.all([
eventPromise,
catPromise,
associationsPromise,
sourcesPromise,
sitesPromise,
@@ -96,17 +88,17 @@ export function fetchDomain () {
.then(response => {
const result = {
events: response[0],
categories: response[1],
associations: response[2],
sources: response[3],
sites: response[4],
shapes: response[5],
associations: response[1],
sources: response[2],
sites: response[3],
shapes: response[4],
notifications
}
if (Object.values(result).some(resp => resp.hasOwnProperty('error'))) {
throw new Error('Some URLs returned negative. If you are in development, check the server is running')
}
dispatch(toggleFetchingDomain())
dispatch(setInitialCategories(result.associations))
return result
})
.catch(err => {
@@ -212,6 +204,14 @@ export function setNotLoading () {
}
}
export const SET_INITIAL_CATEGORIES = 'SET_INITIAL_CATEGORIES'
export function setInitialCategories (values) {
return {
type: SET_INITIAL_CATEGORIES,
values
}
}
export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE'
export function updateTimeRange (timerange) {
return {

View File

@@ -1,2 +1,5 @@
export const FILTER_MODE = 'FILTER'
export const NARRATIVE_MODE = 'NARRATIVE'
export const ASSOCIATION_MODES = {
CATEGORY: 'CATEGORY',
NARRATIVE: 'NARRATIVE',
FILTER: 'FILTER'
}

View File

@@ -63,6 +63,18 @@ export function trimAndEllipse (string, stringNum) {
return string
}
export function getEventCategories (event, categories) {
const matchedCategories = []
if (event.associations && event.associations.length > 0) {
event.associations.reduce((acc, val) => {
const foundCategory = categories.find(cat => cat.id === val)
if (foundCategory) acc.push(foundCategory)
return acc
}, matchedCategories)
}
return matchedCategories
}
/**
* Inset the full source represenation from 'allSources' into an event. The
* function is 'curried' to allow easy use with maps. To use for a single

View File

@@ -78,7 +78,7 @@ class Timeline extends React.Component {
makeScaleY (categories, trackHeight, marginTop) {
const { features } = this.props
if (features.GRAPH_NONLOCATED && features.GRAPH_NONLOCATED.categories) {
categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.category))
categories = categories.filter(cat => !features.GRAPH_NONLOCATED.categories.includes(cat.id))
}
const catHeight = trackHeight / (categories.length)
const shiftUp = trackHeight / (categories.length) / 2
@@ -87,7 +87,8 @@ class Timeline extends React.Component {
const catsYpos = categories.map((g, i) => {
return ((i + 1) * catHeight) - shiftUp + marginShift + manualAdjustment
})
const catMap = categories.map(c => c.category)
const catMap = categories.map(c => c.id)
return (cat) => {
const idx = catMap.indexOf(cat)
return catsYpos[idx]
@@ -268,11 +269,16 @@ class Timeline extends React.Component {
getY (event) {
const { features, domain } = this.props
const { USE_CATEGORIES, GRAPH_NONLOCATED } = features
const { categories } = domain
if (!USE_CATEGORIES) { return this.state.dims.trackHeight / 2 }
const categoriesExist = categories && categories.length > 0
const { GRAPH_NONLOCATED } = features
if (!categoriesExist) { return this.state.dims.trackHeight / 2 }
const { category, project } = event
if (GRAPH_NONLOCATED && GRAPH_NONLOCATED.categories.includes(category)) {
return this.state.dims.marginTop + domain.projects[project].offset + this.props.ui.eventRadius
}
@@ -335,7 +341,7 @@ class Timeline extends React.Component {
onDragStart={() => { this.onDragStart() }}
onDrag={() => { this.onDrag() }}
onDragEnd={() => { this.onDragEnd() }}
categories={this.props.domain.categories}
categories={this.props.app.activeCategories}
features={this.props.features}
/>
<Handles
@@ -361,6 +367,7 @@ class Timeline extends React.Component {
<Events
events={this.props.domain.events}
projects={this.props.domain.projects}
categories={this.props.domain.categories}
styleDatetime={this.styleDatetime}
narrative={this.props.app.narrative}
getDatetimeX={this.getDatetimeX}
@@ -399,6 +406,7 @@ function mapStateToProps (state) {
narratives: state.domain.narratives
},
app: {
activeCategories: selectors.getActiveCategories(state),
selected: state.app.selected,
language: state.app.language,
timeline: state.app.timeline,

View File

@@ -26,11 +26,10 @@ class TimelineCategories extends React.Component {
renderCategory (cat, idx) {
const { features, dims } = this.props
const { category } = cat
const strokeWidth = 1 // dims.trackHeight / (this.props.categories.length + 1)
if (features.GRAPH_NONLOCATED &&
features.GRAPH_NONLOCATED.categories &&
features.GRAPH_NONLOCATED.categories.includes(category)) {
features.GRAPH_NONLOCATED.categories.includes(cat)) {
return null
}
@@ -40,26 +39,27 @@ class TimelineCategories extends React.Component {
class='tick'
style={{ strokeWidth }}
opacity='0.5'
transform={`translate(0,${this.props.getCategoryY(category)})`}
transform={`translate(0,${this.props.getCategoryY(cat)})`}
>
<line x1={dims.marginLeft} x2={dims.width - dims.width_controls} />
</g>
<g class='tick' opacity='1' transform={`translate(0,${this.props.getCategoryY(category)})`}>
<text x={dims.marginLeft - 5} dy='0.32em'>{category}</text>
<g class='tick' opacity='1' transform={`translate(0,${this.props.getCategoryY(cat)})`}>
<text x={dims.marginLeft - 5} dy='0.32em'>{cat}</text>
</g>
</React.Fragment>
)
}
render () {
const { dims } = this.props
const categories = this.props.features.USE_CATEGORIES
const { dims, categories } = this.props
const categoriesExist = categories && categories.length > 0
const renderedCategories = categoriesExist
? this.props.categories.map((cat, idx) => this.renderCategory(cat, idx))
: this.renderCategory('Events', 0)
return (
<g class='yAxis'>
{categories}
{renderedCategories}
<rect
ref={this.grabRef}
class='drag-grabber'

View File

@@ -13,14 +13,14 @@ export default ({
<div>
{categories.map(cat => {
return (<li
key={cat.category.replace(/ /g, '_')}
key={cat.id.replace(/ /g, '_')}
className={'filter-filter active'}
style={{ marginLeft: '20px' }}
>
<Checkbox
label={cat.category}
isActive={activeCategories.includes(cat.category)}
onClickCheckbox={() => onCategoryFilter(cat.category)}
label={cat.id}
isActive={activeCategories.includes(cat.id)}
onClickCheckbox={() => onCategoryFilter(cat.id)}
/>
</li>)
})}

View File

@@ -4,7 +4,7 @@ import DatetimeBar from './DatetimeBar'
import DatetimeSquare from './DatetimeSquare'
import DatetimeStar from './DatetimeStar'
import Project from './Project'
import { calcOpacity } from '../../../common/utilities'
import { calcOpacity, getEventCategories } from '../../../common/utilities'
function renderDot (event, styles, props) {
return <DatetimeDot
@@ -60,6 +60,7 @@ function renderStar (event, styles, props) {
const TimelineEvents = ({
events,
projects,
categories,
narrative,
getDatetimeX,
getY,
@@ -75,14 +76,14 @@ const TimelineEvents = ({
}) => {
const narIds = narrative ? narrative.steps.map(s => s.id) : []
function renderEvent (event) {
function renderEvent (aggregated, event) {
if (narrative) {
if (!(narIds.includes(event.id))) {
return null
}
}
const isDot = (!!event.location && !!event.longitude) || (features.GRAPH_NONLOCATED && event.projectOffset !== -1)
let renderShape = isDot ? renderDot : renderBar
if (event.shape) {
if (event.shape === 'bar') {
@@ -96,23 +97,33 @@ const TimelineEvents = ({
}
}
const eventY = getY(event)
let colour = event.colour ? event.colour : getCategoryColor(event.category)
const styles = {
fill: colour,
fillOpacity: eventY > 0 ? calcOpacity(1) : 0,
transition: `transform ${transitionDuration / 1000}s ease`
}
const relatedCategories = getEventCategories(event, categories)
return renderShape(event, styles, {
x: getDatetimeX(event.datetime),
y: eventY,
eventRadius,
onSelect: () => onSelect(event),
dims,
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.associations[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [],
features
})
if (relatedCategories && relatedCategories.length > 0) {
relatedCategories.forEach(cat => {
const eventY = getY({ ...event, category: cat.id })
let colour = event.colour ? event.colour : getCategoryColor(cat.id)
const styles = {
fill: colour,
fillOpacity: eventY > 0 ? calcOpacity(1) : 0,
transition: `transform ${transitionDuration / 1000}s ease`
}
aggregated.push(
renderShape(event, styles, {
x: getDatetimeX(event.datetime),
y: eventY,
eventRadius,
onSelect: () => onSelect(event),
dims,
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.filters[features.HIGHLIGHT_GROUPS.filterIndexIndicatingGroup]) : [],
features
})
)
})
}
return aggregated
}
let renderProjects = () => null
@@ -136,7 +147,7 @@ const TimelineEvents = ({
clipPath={'url(#clip)'}
>
{renderProjects()}
{events.map(event => renderEvent(event))}
{events.reduce(renderEvent, [])}
</g>
)
}

View File

@@ -1,4 +1,5 @@
import initial from '../store/initial.js'
import { ASSOCIATION_MODES } from '../common/constants'
import { toggleFlagAC } from '../common/utilities'
import {
@@ -22,6 +23,7 @@ import {
FETCH_SOURCE_ERROR,
SET_LOADING,
SET_NOT_LOADING,
SET_INITIAL_CATEGORIES,
UPDATE_SEARCH_QUERY
} from '../actions'
@@ -113,13 +115,14 @@ function toggleFilter (appState, action) {
if (!(action.value instanceof Array)) {
action.value = [action.value]
}
const { filter: associationType } = action
let newFilters = appState.associations.filters.slice(0)
let newAssociations = appState.associations[associationType].slice(0)
action.value.forEach(vl => {
if (newFilters.includes(vl)) {
newFilters = newFilters.filter(s => s !== vl)
if (newAssociations.includes(vl)) {
newAssociations = newAssociations.filter(s => s !== vl)
} else {
newFilters.push(vl)
newAssociations.push(vl)
}
})
@@ -127,7 +130,7 @@ function toggleFilter (appState, action) {
...appState,
associations: {
...appState.associations,
filters: newFilters
[associationType]: newAssociations
}
}
}
@@ -218,6 +221,21 @@ function setNotLoading (appState) {
}
}
function setInitialCategories (appState, action) {
const categories = action.values.reduce((acc, val) => {
if (val.mode === ASSOCIATION_MODES.CATEGORY) acc.push(val.id)
return acc
}, [])
return {
...appState,
associations: {
...appState.associations,
categories: categories
}
}
}
function updateSearchQuery (appState, action) {
return {
...appState,
@@ -269,6 +287,8 @@ function app (appState = initial.app, action) {
return setLoading(appState)
case SET_NOT_LOADING:
return setNotLoading(appState)
case SET_INITIAL_CATEGORIES:
return setInitialCategories(appState, action)
case UPDATE_SEARCH_QUERY:
return updateSearchQuery(appState, action)
default:

View File

@@ -1,9 +0,0 @@
import Joi from 'joi'
const categorySchema = Joi.object().keys({
category: Joi.string().required(),
description: Joi.string(),
group: Joi.string()
})
export default categorySchema

View File

@@ -23,7 +23,7 @@ function createEventSchema (custom) {
type: Joi.string().allow(''),
category: Joi.string().allow(''),
category_full: Joi.string().allow(''),
associations: Joi.array(),
associations: Joi.array().required().default([]),
sources: Joi.array(),
comments: Joi.string().allow(''),
time_display: Joi.string().allow(''),

View File

@@ -1,7 +1,6 @@
import Joi from 'joi'
import createEventSchema from './eventSchema'
import categorySchema from './categorySchema'
import siteSchema from './siteSchema'
import associationsSchema from './associationsSchema'
import sourceSchema from './sourceSchema'
@@ -47,7 +46,6 @@ function findDuplicateAssociations (associations) {
export function validateDomain (domain, features) {
const sanitizedDomain = {
events: [],
categories: [],
sites: [],
associations: [],
sources: {},
@@ -61,7 +59,6 @@ export function validateDomain (domain, features) {
const discardedDomain = {
events: [],
categories: [],
sites: [],
associations: [],
sources: [],
@@ -110,7 +107,6 @@ export function validateDomain (domain, features) {
const eventSchema = createEventSchema(features.CUSTOM_EVENT_FIELDS)
validateArray(domain.events, 'events', eventSchema)
validateArray(domain.categories, 'categories', categorySchema)
validateArray(domain.sites, 'sites', siteSchema)
validateArray(domain.associations, 'associations', associationsSchema)
validateObject(domain.sources, 'sources', sourceSchema)

View File

@@ -1,18 +1,18 @@
import { createSelector } from 'reselect'
import { insetSourceFrom, dateMin, dateMax, isLatitude, isLongitude } from '../common/utilities'
import { isTimeRangedIn } from './helpers'
import { FILTER_MODE, NARRATIVE_MODE } from '../common/constants'
import { ASSOCIATION_MODES } from '../common/constants'
// Input selectors
export const getEvents = state => state.domain.events
export const getCategories = state => state.domain.categories
export const getNarratives = state => state.domain.associations.filter(item => item.mode === NARRATIVE_MODE)
export const getCategories = state => state.domain.associations.filter(item => item.mode === ASSOCIATION_MODES.CATEGORY)
export const getNarratives = state => state.domain.associations.filter(item => item.mode === ASSOCIATION_MODES.NARRATIVE)
export const getActiveNarrative = state => state.app.associations.narrative
export const getSelected = state => state.app.selected
export const getSites = state => state.domain.sites
export const getSources = state => state.domain.sources
export const getShapes = state => state.domain.shapes
export const getFilters = state => state.domain.associations.filter(item => item.mode === FILTER_MODE)
export const getFilters = state => state.domain.associations.filter(item => item.mode === ASSOCIATION_MODES.FILTER)
export const getNotifications = state => state.domain.notifications
export const getActiveFilters = state => state.app.associations.filters
export const getActiveCategories = state => state.app.associations.categories
@@ -55,7 +55,11 @@ export const selectEvents = createSelector(
.some(s => s)
) || activeFilters.length === 0
const isActiveFilter = isMatchingFilter || activeFilters.length === 0
const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0
const isActiveCategory = (event.associations &&
event.associations.map(association =>
activeCategories.includes(association))
.some(s => s)
) || activeCategories.length === 0
let isActiveTime = isTimeRangedIn(event, timeRange)
isActiveTime = features.GRAPH_NONLOCATED
? ((!event.latitude && !event.longitude) || isActiveTime)