rewrite features as part of store + add HIGHLIGHT_GROUPS

This commit is contained in:
Lachlan Kermode
2020-05-19 14:08:03 +02:00
parent 154b62f924
commit e93d7d8831
16 changed files with 151 additions and 84 deletions

View File

@@ -11,18 +11,6 @@ module.exports = {
SHAPES_EXT: '/api/example/export_shapes/columns',
INCOMING_DATETIME_FORMAT: '%m/%d/%YT%H:%M',
// MAPBOX_TOKEN: 'pk.YOUR_MAPBOX_TOKEN',
features: {
USE_COVER: false,
USE_TAGS: false,
USE_SEARCH: false,
USE_SITES: true,
USE_SOURCES: true,
USE_SHAPES: false,
CATEGORIES_AS_TAGS: true,
/** setting this to true will 'graph' non-located events. TODO: document
* and rename **/
ASSOCIATIVE_EVENTS_BY_TAG: false
},
store: {
app: {
map: {
@@ -40,6 +28,17 @@ module.exports = {
selectedEvent: {}
// tiles: 'your-mapbox-account-name/x5678-map-id'
}
},
features: {
USE_COVER: false,
USE_TAGS: false,
USE_SEARCH: false,
USE_SITES: true,
USE_SOURCES: true,
USE_SHAPES: false,
CATEGORIES_AS_TAGS: true,
GRAPH_NONLOCATED: false,
HIGHLIGHT_GROUPS: false
}
}
}

View File

@@ -23,7 +23,8 @@ export function fetchDomain () {
return []
}
return dispatch => {
return (dispatch, getState) => {
const features = getState().features
dispatch(toggleFetchingDomain())
const eventPromise = fetch(EVENT_DATA_URL)
@@ -35,21 +36,21 @@ export function fetchDomain () {
.catch(() => handleError(domainMsg('categories')))
let narPromise = Promise.resolve([])
if (process.env.features.USE_NARRATIVES) {
if (features.USE_NARRATIVES) {
narPromise = fetch(NARRATIVE_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('narratives')))
}
let sitesPromise = Promise.resolve([])
if (process.env.features.USE_SITES) {
if (features.USE_SITES) {
sitesPromise = fetch(SITES_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('sites')))
}
let tagsPromise = Promise.resolve([])
if (process.env.features.USE_TAGS) {
if (features.USE_TAGS) {
if (!TAGS_URL) {
tagsPromise = Promise.resolve(handleError('USE_TAGS is true, but you have not provided a TAGS_EXT'))
} else {
@@ -60,7 +61,7 @@ export function fetchDomain () {
}
let sourcesPromise = Promise.resolve([])
if (process.env.features.USE_SOURCES) {
if (features.USE_SOURCES) {
if (!SOURCES_URL) {
sourcesPromise = Promise.resolve(handleError('USE_SOURCES is true, but you have not provided a SOURCES_EXT'))
} else {
@@ -71,7 +72,7 @@ export function fetchDomain () {
}
let shapesPromise = Promise.resolve([])
if (process.env.features.USE_SHAPES) {
if (features.USE_SHAPES) {
shapesPromise = fetch(SHAPES_URL)
.then(response => response.json())
.catch(() => handleError(domainMsg('shapes')))

View File

@@ -108,12 +108,12 @@ class Dashboard extends React.Component {
}
render () {
const { actions, app, domain, ui } = this.props
const { actions, app, domain, ui, features } = this.props
if (isMobile || window.innerWidth < 1000) {
return (
<div>
{process.env.features.USE_COVER && (
{features.USE_COVER && (
<StaticPage showing={app.flags.isCover}>
{/* enable USE_COVER in config.js features, and customise your header */}
{/* pass 'actions.toggleCover' as a prop to your custom header */}
@@ -193,7 +193,7 @@ class Dashboard extends React.Component {
}
/>
) : null}
{process.env.features.USE_COVER && (
{features.USE_COVER && (
<StaticPage showing={app.flags.isCover}>
{/* enable USE_COVER in config.js features, and customise your header */}
{/* pass 'actions.toggleCover' as a prop to your custom header */}

View File

@@ -34,7 +34,7 @@ class Map extends React.Component {
componentDidMount () {
if (this.map === null) {
this.initializeMap()
// this.initializeMap()
}
}
@@ -139,7 +139,7 @@ class Map extends React.Component {
const pane = this.map.getPanes().overlayPane
const { width, height } = this.getClientDims()
return (
return !!this.map ? (
<Portal node={pane}>
<svg
ref={this.svgRef}
@@ -149,7 +149,7 @@ class Map extends React.Component {
className='leaflet-svg'
/>
</Portal>
)
) : null
}
renderSites () {
@@ -183,6 +183,7 @@ class Map extends React.Component {
styles={this.props.ui.narratives}
onSelect={this.props.methods.onSelect}
onSelectNarrative={this.props.methods.onSelectNarrative}
features={this.props.features}
/>
)
}
@@ -266,8 +267,8 @@ function mapStateToProps (state) {
locations: selectors.selectLocations(state),
narratives: selectors.selectNarratives(state),
categories: selectors.getCategories(state),
sites: selectors.getSites(state),
shapes: selectors.getShapes(state)
sites: selectors.selectSites(state),
shapes: selectors.selectShapes(state)
},
app: {
views: state.app.filters.views,
@@ -285,7 +286,8 @@ function mapStateToProps (state) {
narratives: state.ui.style.narratives,
mapSelectedEvents: state.ui.style.selectedEvents,
shapes: state.ui.style.shapes
}
},
features: selectors.getFeatures(state)
}
}

View File

@@ -287,6 +287,7 @@ class Timeline extends React.Component {
const heightStyle = { height: dims.height }
const extraStyle = { ...heightStyle, ...foldedStyle }
const contentHeight = { height: dims.contentHeight }
const { categories } = this.props.domain
return (
<div className={classes} style={extraStyle}>
@@ -347,10 +348,17 @@ class Timeline extends React.Component {
narrative={this.props.app.narrative}
getDatetimeX={this.getDatetimeX}
getCategoryY={this.state.scaleY}
getHighlights={group => {
if (group === 'None') {
return []
}
return categories.map(c => c.group === group)
}}
getCategoryColor={this.props.methods.getCategoryColor}
transitionDuration={this.state.transitionDuration}
onSelect={this.props.methods.onSelect}
dims={dims}
features={this.props.features}
/>
</svg>
</div>
@@ -378,7 +386,8 @@ function mapStateToProps (state) {
ui: {
dom: state.ui.dom,
styles: state.ui.style.selectedEvents
}
},
features: selectors.getFeatures(state)
}
}

View File

@@ -8,7 +8,7 @@ function BottomActions (props) {
function renderToggles () {
return [
<div className='bottom-action-block'>
{process.env.features.USE_SITES ? <SitesIcon
{props.features.USE_SITES ? <SitesIcon
isActive={props.sites.enabled}
onClickHandler={props.sites.toggle}
/> : null}
@@ -20,7 +20,7 @@ function BottomActions (props) {
/>
</div>,
<div className='botttom-action-block'>
{process.env.features.USE_COVER ? <CoverIcon
{props.features.USE_COVER ? <CoverIcon
onClickHandler={props.cover.toggle}
/> : null}
</div>

View File

@@ -32,7 +32,7 @@ class Toolbar extends React.Component {
}
renderSearch () {
if (process.env.features.USE_SEARCH) {
if (this.props.features.USE_SEARCH) {
return (
<TabPanel>
<Search
@@ -73,7 +73,7 @@ class Toolbar extends React.Component {
}
renderToolbarCategoriesPanel () {
if (process.env.features.CATEGORIES_AS_TAGS) {
if (this.props.features.CATEGORIES_AS_TAGS) {
return (
<TabPanel>
<CategoriesListPanel
@@ -88,7 +88,7 @@ class Toolbar extends React.Component {
}
renderToolbarTagPanel () {
if (process.env.features.USE_TAGS &&
if (this.props.features.USE_TAGS &&
this.props.tags.children) {
return (
<TabPanel>
@@ -154,7 +154,7 @@ class Toolbar extends React.Component {
const tagsLabel = copy[this.props.language].toolbar.tags_label
const categoriesLabel = 'Categories' // TODO:
const isTags = this.props.tags && this.props.tags.children
const isCategories = process.env.features.CATEGORIES_AS_TAGS
const isCategories = this.props.features.CATEGORIES_AS_TAGS
return (
<div className='toolbar'>
@@ -176,6 +176,7 @@ class Toolbar extends React.Component {
cover={{
toggle: this.props.actions.toggleCover
}}
features={this.props.features}
/>
</div>
)
@@ -195,6 +196,7 @@ class Toolbar extends React.Component {
function mapStateToProps (state) {
return {
features: selectors.getFeatures(state),
tags: selectors.getTagTree(state),
categories: selectors.getCategories(state),
narratives: selectors.selectNarratives(state),
@@ -202,7 +204,6 @@ function mapStateToProps (state) {
activeTags: selectors.getActiveTags(state),
activeCategories: selectors.getActiveCategories(state),
viewFilters: state.app.filters.views,
features: state.app.features,
narrative: state.app.narrative,
sitesShowing: state.app.flags.isShowingSites,
infoShowing: state.app.flags.isInfopopup

View File

@@ -10,7 +10,7 @@ const defaultStyles = {
stroke: 'none'
}
function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives, projectPoint }) {
function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives, projectPoint, features }) {
function getNarrativeStyle (narrativeId) {
const styleName = (narrativeId && narrativeId in styles)
? narrativeId
@@ -153,7 +153,7 @@ function MapNarratives ({ styles, onSelectNarrative, svg, narrative, narratives,
return (
<g id={narrativeId} className='narrative'>
{(process.env.features.NARRATIVE_STEP_STYLES
{(features.NARRATIVE_STEP_STYLES
? renderBetweenMarked(n)
: renderFullNarrative(n)
)}

View File

@@ -1,7 +1,7 @@
import React from 'react'
export default ({
category,
highlights,
events,
x,
y,
@@ -10,14 +10,34 @@ export default ({
onSelect,
styleProps,
extraRender
}) => (
<rect
onClick={onSelect}
className='event'
x={x}
y={y}
style={styleProps}
width={width}
height={height}
/>
)
}) => {
if (highlights.length === 0) {
return (
<rect
onClick={onSelect}
className='event'
x={x}
y={y}
style={styleProps}
width={width}
height={height}
/>
)
}
const sectionHeight = height / highlights.length
return (
<React.Fragment>
{highlights.map((h, idx) => (
<rect
onClick={onSelect}
className='event'
x={x}
y={y - sectionHeight + (idx * sectionHeight)}
style={{ ...styleProps, opacity: h ? 0.3 : 0.05 }}
width={width}
height={sectionHeight}
/>
))}
</React.Fragment>
)
}

View File

@@ -7,8 +7,6 @@ import Project from './Project'
import { calcOpacity } from '../../../common/utilities'
import { sizes } from '../../../common/global'
const GRAPH_NONLOCATED = 'GRAPH_NONLOCATED' in process.env.features && process.env.features.GRAPH_NONLOCATED
function renderDot (event, styles, props) {
return <DatetimeDot
onSelect={props.onSelect}
@@ -22,7 +20,7 @@ function renderDot (event, styles, props) {
}
function renderBar (event, styles, props) {
const fillOpacity = GRAPH_NONLOCATED
const fillOpacity = props.features.GRAPH_NONLOCATED
? event.projectOffset >= 0 ? styles.opacity : 0.05
: 0.6
@@ -35,6 +33,7 @@ function renderBar (event, styles, props) {
width={sizes.eventDotR / 4}
height={props.dims.trackHeight}
styleProps={{ ...styles, fillOpacity }}
highlights={props.highlights}
/>
}
@@ -66,10 +65,11 @@ const TimelineEvents = ({
getDatetimeX,
getCategoryY,
getCategoryColor,
getHighlights,
onSelect,
transitionDuration,
// styleDatetime,
dims
dims,
features
}) => {
const narIds = narrative ? narrative.steps.map(s => s.id) : []
@@ -91,25 +91,28 @@ const TimelineEvents = ({
}
}
const colour = event.colour ? event.colour : getCategoryColor(event.category)
let colour = event.colour ? event.colour : getCategoryColor(event.category)
const styles = {
fill: colour,
fillOpacity: calcOpacity(1),
transition: `transform ${transitionDuration / 1000}s ease`
}
return renderShape(event, styles, {
x: getDatetimeX(event.timestamp),
y: (GRAPH_NONLOCATED && !event.latitude && !event.longitude)
y: (features.GRAPH_NONLOCATED && !event.latitude && !event.longitude)
? event.projectOffset >= 0 ? dims.trackHeight - event.projectOffset : dims.marginTop
: getCategoryY(event.category),
: getCategoryY ? getCategoryY(event.category) : () => null,
onSelect: () => onSelect([event]),
dims
dims,
highlights: features.HIGHLIGHT_GROUPS ? getHighlights(event.tags[0]) : [],
features
})
}
/* set `renderProjects` */
let renderProjects = () => null
if (GRAPH_NONLOCATED) {
if (features.GRAPH_NONLOCATED) {
renderProjects = function () {
return <React.Fragment>
{projects.map(project => <Project

7
src/reducers/features.js Normal file
View File

@@ -0,0 +1,7 @@
import initial from '../store/initial.js'
function features (featureState = initial.features, action) {
return featureState
}
export default features

View File

@@ -3,9 +3,11 @@ import { combineReducers } from 'redux'
import domain from './domain.js'
import app from './app.js'
import ui from './ui.js'
import features from './features.js'
export default combineReducers({
app,
domain,
ui
ui,
features
})

View File

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

View File

@@ -417,6 +417,7 @@
font-size: $normal;
font-family: Helvetica, 'Georgia', 'serif';
color: $midwhite;
overflow: hidden;
}
&:hover {

View File

@@ -1,8 +1,7 @@
import { createSelector } from 'reselect'
import { compareTimestamp, insetSourceFrom, dateMin, dateMax } from '../common/utilities'
import { isTimeRangedIn, shuffle } from './helpers'
import { isTimeRangedIn } from './helpers'
import { sizes } from '../common/global'
const GRAPH_NONLOCATED = 'GRAPH_NONLOCATED' in process.env.features && process.env.features.GRAPH_NONLOCATED
// Input selectors
export const getEvents = state => state.domain.events
@@ -11,18 +10,9 @@ export const getNarratives = state => state.domain.narratives
export const getActiveNarrative = state => state.app.narrative
export const getActiveStep = state => state.app.narrativeState.current
export const getSelected = state => state.app.selected
export const getSites = (state) => {
if (process.env.features.USE_SITES) return state.domain.sites.filter(s => !!(+s.enabled))
return []
}
export const getSources = state => {
if (process.env.features.USE_SOURCES) return state.domain.sources
return {}
}
export const getShapes = state => {
if (process.env.features.USE_SHAPES) return state.domain.shapes
return []
}
export const getSites = state => state.domain.sites
export const getSources = state => state.domain.sources
export const getShapes = state => state.domain.shapes
export const getNotifications = state => state.domain.notifications
export const getTagTree = state => state.domain.tags
export const getActiveTags = state => state.app.filters.tags
@@ -30,6 +20,24 @@ export const getActiveCategories = state => state.app.filters.categories
export const getTimeRange = state => state.app.timeline.range
export const getTimelineDimensions = state => state.app.timeline.dimensions
export const selectNarrative = state => state.app.narrative
export const getFeatures = state => state.features
export const selectSites = createSelector([getSites, getFeatures], (sites, features) => {
if (features.USE_SITES) {
return sites.filter(s => !!(+s.enabled))
}
return []
})
export const selectSources = createSelector([getSources, getFeatures], (sources, features) => {
if (features.USE_SOURCES) return sources
return {}
})
export const selectShapes = createSelector([getShapes, getFeatures], (shapes, features) => {
if (features.USE_SHAPES) return shapes
return []
})
/**
* Of all available events, selects those that
@@ -38,14 +46,14 @@ export const selectNarrative = state => state.app.narrative
* 3. exist in an active category
*/
export const selectEvents = createSelector(
[getEvents, getActiveTags, getActiveCategories, getTimeRange],
(events, activeTags, activeCategories, timeRange) => {
[getEvents, getActiveTags, getActiveCategories, getTimeRange, getFeatures],
(events, activeTags, activeCategories, timeRange, features) => {
return events.reduce((acc, event) => {
const isMatchingTag = (event.tags && event.tags.map(tag => activeTags.includes(tag)).some(s => s)) || activeTags.length === 0
const isActiveTag = isMatchingTag || activeTags.length === 0
const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0
let isActiveTime = isTimeRangedIn(event, timeRange)
isActiveTime = GRAPH_NONLOCATED ? ((!event.latitude && !event.longitude) || isActiveTime) : isActiveTime
isActiveTime = features.GRAPH_NONLOCATED ? ((!event.latitude && !event.longitude) || isActiveTime) : isActiveTime
if (isActiveTime && isActiveTag && isActiveCategory) {
acc[event.id] = { ...event }
@@ -60,9 +68,9 @@ export const selectEvents = createSelector(
* and if TAGS are being used, select them if their tags are enabled
*/
export const selectNarratives = createSelector(
[getEvents, getNarratives, getSources],
(events, narrativesMeta, sources) => {
if (!process.env.features.USE_NARRATIVES) {
[getEvents, getNarratives, getSources, getFeatures],
(events, narrativesMeta, sources, features) => {
if (!features.USE_NARRATIVES) {
return []
}
const narratives = {}
@@ -162,9 +170,9 @@ export const selectProjectedEvents = createSelector(
}
*/
export const selectEventsAndProjects = createSelector(
[selectEvents],
events => {
if (!GRAPH_NONLOCATED) {
[selectEvents, getFeatures],
(events, features) => {
if (!features.GRAPH_NONLOCATED) {
return [events, []]
}

View File

@@ -127,6 +127,19 @@ const initial = {
timeslider: 'timeslider',
map: 'map'
}
},
features: {
CATEGORIES_AS_TAGS: true,
USE_COVER: false,
USE_TAGS: false,
USE_SEARCH: false,
USE_SITES: false,
USE_SOURCES: false,
USE_SHAPES: false,
USE_NARRATIVES: false,
GRAPH_NONLOCATED: false,
HIGHLIGHT_GROUPS: false
}
}