mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-08 03:18:36 +03:00
Merge pull request #101 from forensic-architecture/fix/filters-architeccture
Make filters architecture more transparent
This commit is contained in:
@@ -171,19 +171,20 @@ export function updateDistrict (district) {
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATE_TAGFILTERS = 'UPDATE_TAGFILTERS'
|
||||
export function updateTagFilters (tag) {
|
||||
export const CLEAR_FILTER = 'CLEAR_FILTER'
|
||||
export function clearFilter (filter) {
|
||||
return {
|
||||
type: UPDATE_TAGFILTERS,
|
||||
tag
|
||||
type: CLEAR_FILTER,
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATE_CATEGORYFILTERS = 'UPDATE_CATEGORYFILTERS'
|
||||
export function updateCategoryFilters (category) {
|
||||
export const TOGGLE_FILTER = 'TOGGLE_FILTER'
|
||||
export function toggleFilter (filter, value) {
|
||||
return {
|
||||
type: UPDATE_CATEGORYFILTERS,
|
||||
category
|
||||
type: TOGGLE_FILTER,
|
||||
filter,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,13 +218,6 @@ export function decrementNarrativeCurrent () {
|
||||
}
|
||||
}
|
||||
|
||||
export const RESET_ALLFILTERS = 'RESET_ALLFILTERS'
|
||||
export function resetAllFilters () {
|
||||
return {
|
||||
type: RESET_ALLFILTERS
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATE_SOURCE = 'UPDATE_SOURCE'
|
||||
export function updateSource (source) {
|
||||
return {
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import React from 'react'
|
||||
import Checkbox from './presentational/Checkbox'
|
||||
import copy from '../js/data/copy.json'
|
||||
|
||||
export default (props) => {
|
||||
function onClickCheckbox (obj, type) {
|
||||
obj.active = !obj.active
|
||||
props.onCategoryFilter(obj)
|
||||
}
|
||||
|
||||
function renderCategoryTree () {
|
||||
return (
|
||||
<div>
|
||||
{props.categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.category}
|
||||
isActive={cat.active}
|
||||
onClickCheckbox={() => onClickCheckbox(cat, 'category')}
|
||||
/>
|
||||
</li>)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[props.language].toolbar.categories}</h2>
|
||||
<p>{copy[props.language].toolbar.explore_by_category__description}</p>
|
||||
{renderCategoryTree()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import * as actions from '../actions'
|
||||
import MediaOverlay from './Overlay/Media'
|
||||
import LoadingOverlay from './Overlay/Loading'
|
||||
import Map from './Map.jsx'
|
||||
import Toolbar from './Toolbar.jsx'
|
||||
import Toolbar from './Toolbar/Layout'
|
||||
import CardStack from './CardStack.jsx'
|
||||
import NarrativeControls from './presentational/Narrative/Controls.js'
|
||||
import InfoPopUp from './InfoPopup.jsx'
|
||||
@@ -76,7 +76,11 @@ class Dashboard extends React.Component {
|
||||
|
||||
setNarrative (narrative) {
|
||||
// only handleSelect if narrative is not null
|
||||
if (narrative) { this.handleSelect([ narrative.steps[0] ]) }
|
||||
if (narrative) {
|
||||
this.props.actions.clearFilter('tags')
|
||||
this.props.actions.clearFilter('categories')
|
||||
this.handleSelect([ narrative.steps[0] ])
|
||||
}
|
||||
this.props.actions.updateNarrative(narrative)
|
||||
}
|
||||
|
||||
@@ -120,8 +124,8 @@ class Dashboard extends React.Component {
|
||||
<Toolbar
|
||||
isNarrative={!!app.narrative}
|
||||
methods={{
|
||||
onTagFilter: actions.updateTagFilters,
|
||||
onCategoryFilter: actions.updateCategoryFilters,
|
||||
onTagFilter: tag => actions.toggleFilter('tags', tag),
|
||||
onCategoryFilter: category => actions.toggleFilter('categories', category),
|
||||
onSelectNarrative: this.setNarrative
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -245,7 +245,7 @@ class Map extends React.Component {
|
||||
{this.renderShapes()}
|
||||
{this.renderNarratives()}
|
||||
{this.renderEvents()}
|
||||
{this.renderSelected()}
|
||||
{this.renderSelected()}
|
||||
</React.Fragment>
|
||||
) : null
|
||||
|
||||
@@ -263,7 +263,7 @@ function mapStateToProps (state) {
|
||||
domain: {
|
||||
locations: selectors.selectLocations(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
categories: selectors.selectCategories(state),
|
||||
categories: selectors.getCategories(state),
|
||||
sites: selectors.getSites(state),
|
||||
shapes: selectors.getShapes(state)
|
||||
},
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react'
|
||||
import Checkbox from './presentational/Checkbox'
|
||||
|
||||
class TagFilter extends React.Component {
|
||||
isActive () {
|
||||
if (this.props.isCategory) {
|
||||
return this.props.categoryFilters.includes(this.props.tag.id)
|
||||
}
|
||||
return this.props.tagFilters.includes(this.props.tag.id)
|
||||
}
|
||||
|
||||
onClickTag () {
|
||||
if (this.isActive()) {
|
||||
this.props.filter({
|
||||
tags: this.props.tagFilters.filter(element => element !== this.props.tag.id)
|
||||
})
|
||||
} else {
|
||||
this.props.filter({
|
||||
tags: this.props.tagFilters.concat(this.props.tag.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onClickCategory () {
|
||||
if (this.isActive()) {
|
||||
this.props.filter({
|
||||
categories: this.props.categoryFilters.filter(element => element !== this.props.tag.id)
|
||||
})
|
||||
} else {
|
||||
this.props.filter({
|
||||
categories: this.props.categoryFilters.concat(this.props.tag.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
renderTag () {
|
||||
const tag = this.props.tag
|
||||
let classes = (this.isActive()) ? 'tag-filter active' : 'tag-filter'
|
||||
let label = `${tag.name} ( ${tag.mentions} )`
|
||||
if (this.props.isShowTree) {
|
||||
label = `${tag.group} > ${tag.subgroup} > ${tag.name} ( ${tag.mentions} )`
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={this.props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={this.isActive()}
|
||||
label={label}
|
||||
onClickCheckbox={() => this.onClickTag()}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
renderCategory () {
|
||||
const category = this.props.categories[this.props.tag.id]
|
||||
let classes = (this.isActive()) ? 'tag-filter active' : 'tag-filter'
|
||||
|
||||
if (category) {
|
||||
return (
|
||||
<li
|
||||
key={this.props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={this.isActive()}
|
||||
label={`${category.name} ( ${category.counts} )`}
|
||||
onClickCheckbox={() => this.onClickCategory()}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return (<div />)
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.props.isCategory) return (this.renderCategory())
|
||||
return (this.renderTag())
|
||||
}
|
||||
}
|
||||
|
||||
export default TagFilter
|
||||
@@ -1,82 +0,0 @@
|
||||
import React from 'react'
|
||||
import Checkbox from './presentational/Checkbox'
|
||||
import copy from '../js/data/copy.json'
|
||||
|
||||
class TagListPanel extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
treeComponents: []
|
||||
}
|
||||
this.treeComponents = []
|
||||
this.newTagFilters = []
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.computeTree(this.props.tags)// .children[this.props.tagType]);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this.computeTree(nextProps.tags)// .children[nextProps.tagType]);
|
||||
}
|
||||
|
||||
onClickCheckbox (obj, type) {
|
||||
obj.active = !obj.active
|
||||
this.props.onTagFilter(obj)
|
||||
}
|
||||
|
||||
createNodeComponent (node, depth) {
|
||||
return (
|
||||
<li
|
||||
key={node.key.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: `${depth * 20}px` }}
|
||||
>
|
||||
<Checkbox
|
||||
label={node.key}
|
||||
isActive={node.active}
|
||||
onClickCheckbox={() => this.onClickCheckbox(node, 'tag')}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
traverseNodeAndCreateComponent (node, depth) {
|
||||
// add and create node component
|
||||
const newComponent = this.createNodeComponent(node, depth)
|
||||
this.treeComponents.push(newComponent)
|
||||
depth = depth + 1
|
||||
if (Object.keys(node.children).length > 0) {
|
||||
Object.values(node.children).forEach((childNode) => {
|
||||
this.traverseNodeAndCreateComponent(childNode, depth)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
computeTree (node) {
|
||||
this.treeComponents = []
|
||||
let depth = 0
|
||||
this.traverseNodeAndCreateComponent(node, depth)
|
||||
this.setState({ treeComponents: this.treeComponents })
|
||||
}
|
||||
|
||||
renderTree () {
|
||||
return (
|
||||
<div>
|
||||
{this.state.treeComponents.map(c => c)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[this.props.language].toolbar.tags}</h2>
|
||||
<p>{copy[this.props.language].toolbar.explore_by_tag__description}</p>
|
||||
{this.renderTree()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default TagListPanel
|
||||
@@ -349,7 +349,7 @@ function mapStateToProps (state) {
|
||||
isNarrative: !!state.app.narrative,
|
||||
domain: {
|
||||
datetimes: selectors.selectDatetimes(state),
|
||||
categories: selectors.selectCategories(state),
|
||||
categories: selectors.getCategories(state),
|
||||
narratives: state.domain.narratives
|
||||
},
|
||||
app: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import SitesIcon from './presentational/Icons/Sites'
|
||||
import CoverIcon from './presentational/Icons/Cover'
|
||||
import InfoIcon from './presentational/Icons/Info'
|
||||
import SitesIcon from '../presentational/Icons/Sites'
|
||||
import CoverIcon from '../presentational/Icons/Cover'
|
||||
import InfoIcon from '../presentational/Icons/Info'
|
||||
|
||||
function ToolbarBottomActions (props) {
|
||||
function BottomActions (props) {
|
||||
function renderToggles () {
|
||||
return [
|
||||
<div className='bottom-action-block'>
|
||||
@@ -34,4 +34,4 @@ function ToolbarBottomActions (props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default ToolbarBottomActions
|
||||
export default BottomActions
|
||||
38
src/components/Toolbar/CategoriesListPanel.js
Normal file
38
src/components/Toolbar/CategoriesListPanel.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import Checkbox from '../presentational/Checkbox'
|
||||
import copy from '../../js/data/copy.json'
|
||||
|
||||
export default ({
|
||||
categories,
|
||||
activeCategories,
|
||||
onCategoryFilter,
|
||||
language
|
||||
}) => {
|
||||
function renderCategoryTree () {
|
||||
return (
|
||||
<div>
|
||||
{categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.category}
|
||||
isActive={activeCategories.includes(cat.category)}
|
||||
onClickCheckbox={() => onCategoryFilter(cat.category)}
|
||||
/>
|
||||
</li>)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[language].toolbar.categories}</h2>
|
||||
<p>{copy[language].toolbar.explore_by_category__description}</p>
|
||||
{renderCategoryTree()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import * as actions from '../actions'
|
||||
import * as selectors from '../selectors'
|
||||
import * as actions from '../../actions'
|
||||
import * as selectors from '../../selectors'
|
||||
|
||||
import { Tabs, TabPanel } from 'react-tabs'
|
||||
import Search from './Search.jsx'
|
||||
import TagListPanel from './TagListPanel.jsx'
|
||||
import CategoriesListPanel from './CategoriesListPanel.jsx'
|
||||
import ToolbarBottomActions from './ToolbarBottomActions.jsx'
|
||||
import copy from '../js/data/copy.json'
|
||||
import { trimAndEllipse } from '../js/utilities.js'
|
||||
import Search from './Search'
|
||||
import TagListPanel from './TagListPanel'
|
||||
import CategoriesListPanel from './CategoriesListPanel'
|
||||
import BottomActions from './BottomActions'
|
||||
import copy from '../../js/data/copy.json'
|
||||
import { trimAndEllipse } from '../../js/utilities.js'
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -78,7 +78,7 @@ class Toolbar extends React.Component {
|
||||
<TabPanel>
|
||||
<CategoriesListPanel
|
||||
categories={this.props.categories}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
activeCategories={this.props.activeCategories}
|
||||
onCategoryFilter={this.props.methods.onCategoryFilter}
|
||||
language={this.props.language}
|
||||
/>
|
||||
@@ -94,7 +94,7 @@ class Toolbar extends React.Component {
|
||||
<TabPanel>
|
||||
<TagListPanel
|
||||
tags={this.props.tags}
|
||||
tagFilters={this.props.tagFilters}
|
||||
activeTags={this.props.activeTags}
|
||||
onTagFilter={this.props.methods.onTagFilter}
|
||||
language={this.props.language}
|
||||
/>
|
||||
@@ -164,7 +164,7 @@ class Toolbar extends React.Component {
|
||||
{(isCategories) ? this.renderToolbarTab(1, categoriesLabel, 'widgets') : null}
|
||||
{(isTags) ? this.renderToolbarTab(2, tagsLabel, 'filter_list') : null}
|
||||
</div>
|
||||
<ToolbarBottomActions
|
||||
<BottomActions
|
||||
info={{
|
||||
enabled: this.props.infoShowing,
|
||||
toggle: this.props.actions.toggleInfoPopup
|
||||
@@ -199,8 +199,8 @@ function mapStateToProps (state) {
|
||||
categories: selectors.getCategories(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
language: state.app.language,
|
||||
tagFilters: selectors.selectTagList(state),
|
||||
categoryFilters: selectors.selectCategories(state),
|
||||
activeTags: selectors.getActiveTags(state),
|
||||
activeCategories: selectors.getActiveCategories(state),
|
||||
viewFilters: state.app.filters.views,
|
||||
features: state.app.features,
|
||||
narrative: state.app.narrative,
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global fetch */
|
||||
import React from 'react'
|
||||
import copy from '../js/data/copy.json'
|
||||
import TagFilter from './TagFilter.jsx'
|
||||
import copy from '../../js/data/copy.json'
|
||||
import TagFilter from './TagFilter'
|
||||
|
||||
export default class Search extends React.Component {
|
||||
constructor (props) {
|
||||
82
src/components/Toolbar/TagFilter.js
Normal file
82
src/components/Toolbar/TagFilter.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import Checkbox from '../presentational/Checkbox'
|
||||
|
||||
function TagFilter (props) {
|
||||
function isActive () {
|
||||
if (props.isCategory) {
|
||||
return props.categoryFilters.includes(props.tag.id)
|
||||
}
|
||||
return props.tagFilters.includes(props.tag.id)
|
||||
}
|
||||
|
||||
function onClickTag () {
|
||||
if (isActive()) {
|
||||
props.filter({
|
||||
tags: props.tagFilters.filter(element => element !== props.tag.id)
|
||||
})
|
||||
} else {
|
||||
props.filter({
|
||||
tags: props.tagFilters.concat(props.tag.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onClickCategory () {
|
||||
if (isActive()) {
|
||||
props.filter({
|
||||
categories: props.categoryFilters.filter(element => element !== props.tag.id)
|
||||
})
|
||||
} else {
|
||||
props.filter({
|
||||
categories: props.categoryFilters.concat(props.tag.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function renderTag () {
|
||||
const tag = props.tag
|
||||
let classes = (isActive()) ? 'tag-filter active' : 'tag-filter'
|
||||
let label = `${tag.name} ( ${tag.mentions} )`
|
||||
if (props.isShowTree) {
|
||||
label = `${tag.group} > ${tag.subgroup} > ${tag.name} ( ${tag.mentions} )`
|
||||
}
|
||||
return (
|
||||
<li
|
||||
key={props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={isActive()}
|
||||
label={label}
|
||||
onClickCheckbox={() => onClickTag()}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function renderCategory () {
|
||||
const category = props.categories[props.tag.id]
|
||||
let classes = (isActive()) ? 'tag-filter active' : 'tag-filter'
|
||||
|
||||
if (category) {
|
||||
return (
|
||||
<li
|
||||
key={props.tag.id}
|
||||
className={classes}
|
||||
>
|
||||
<Checkbox
|
||||
isActive={isActive()}
|
||||
label={`${category.name} ( ${category.counts} )`}
|
||||
onClickCheckbox={onClickCategory}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
return (<div />)
|
||||
}
|
||||
|
||||
if (props.isCategory) return (renderCategory())
|
||||
return (renderTag())
|
||||
}
|
||||
|
||||
export default TagFilter
|
||||
45
src/components/Toolbar/TagListPanel.js
Normal file
45
src/components/Toolbar/TagListPanel.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react'
|
||||
import Checkbox from '../presentational/Checkbox'
|
||||
import copy from '../../js/data/copy.json'
|
||||
|
||||
function TagListPanel ({
|
||||
tags,
|
||||
activeTags,
|
||||
onTagFilter,
|
||||
language
|
||||
}) {
|
||||
function createNodeComponent (node, depth) {
|
||||
return (
|
||||
<li
|
||||
key={node.key.replace(/ /g, '_')}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: `${depth * 20}px` }}
|
||||
>
|
||||
<Checkbox
|
||||
label={node.key}
|
||||
isActive={activeTags.includes(node.key)}
|
||||
onClickCheckbox={() => onTagFilter(node.key)}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function renderTree () {
|
||||
/* NOTE: only render first layer of tags */
|
||||
return (
|
||||
<div>
|
||||
{Object.values(tags.children).map(tag => createNodeComponent(tag, 1))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='react-innertabpanel'>
|
||||
<h2>{copy[language].toolbar.tags}</h2>
|
||||
<p>{copy[language].toolbar.explore_by_tag__description}</p>
|
||||
{renderTree()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagListPanel
|
||||
@@ -80,6 +80,7 @@ function MapEvents ({ getCategoryColor, categories, projectPoint, styleLocation,
|
||||
const { x, y } = projectPoint([location.latitude, location.longitude])
|
||||
|
||||
// in narrative mode, only render events in narrative
|
||||
// TODO: move this to a selector
|
||||
if (narrative) {
|
||||
const { steps } = narrative
|
||||
const onlyIfInNarrative = e => steps.map(s => s.id).includes(e.id)
|
||||
|
||||
@@ -5,8 +5,8 @@ import { parseDate, toggleFlagAC } from '../js/utilities'
|
||||
import {
|
||||
UPDATE_HIGHLIGHTED,
|
||||
UPDATE_SELECTED,
|
||||
UPDATE_TAGFILTERS,
|
||||
UPDATE_CATEGORYFILTERS,
|
||||
CLEAR_FILTER,
|
||||
TOGGLE_FILTER,
|
||||
UPDATE_TIMERANGE,
|
||||
UPDATE_NARRATIVE,
|
||||
INCREMENT_NARRATIVE_CURRENT,
|
||||
@@ -118,45 +118,40 @@ function decrementNarrativeCurrent (appState, action) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateTagFilters (appState, action) {
|
||||
const tagFilters = appState.filters.tags.slice(0)
|
||||
const nextActiveState = action.tag.active
|
||||
|
||||
function traverseNode (node) {
|
||||
const tagFilter = tagFilters.find(tF => tF.key === node.key)
|
||||
node.active = nextActiveState
|
||||
if (!tagFilter) tagFilters.push(node)
|
||||
|
||||
if (node && Object.keys(node.children).length > 0) {
|
||||
Object.values(node.children).forEach((childNode) => { traverseNode(childNode) })
|
||||
function clearTagFilters (appState) {
|
||||
return {
|
||||
...appState,
|
||||
filters: {
|
||||
...appState.filters,
|
||||
tags: []
|
||||
}
|
||||
}
|
||||
|
||||
traverseNode(action.tag)
|
||||
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
tags: tagFilters
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function updateCategoryFilters (appState, action) {
|
||||
const categoryFilters = appState.filters.categories.slice(0)
|
||||
|
||||
const catFilter = categoryFilters.find(cF => cF.category === action.category.category)
|
||||
|
||||
if (!catFilter) {
|
||||
categoryFilters.push(action.category)
|
||||
function toggleFilter (appState, action) {
|
||||
let newTags = appState.filters[action.filter].slice(0)
|
||||
if (newTags.includes(action.value)) {
|
||||
newTags = newTags.filter(s => s !== action.value)
|
||||
} else {
|
||||
catFilter.active = (!!action.category.active)
|
||||
newTags.push(action.value)
|
||||
}
|
||||
return {
|
||||
...appState,
|
||||
filters: {
|
||||
...appState.filters,
|
||||
[action.filter]: newTags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
categories: categoryFilters
|
||||
})
|
||||
})
|
||||
function clearFilter (appState, action) {
|
||||
return {
|
||||
...appState,
|
||||
filters: {
|
||||
...appState.filters,
|
||||
[action.filter]: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTimeRange (appState, action) { // XXX
|
||||
@@ -169,20 +164,6 @@ function updateTimeRange (appState, action) { // XXX
|
||||
}
|
||||
}
|
||||
|
||||
function resetAllFilters (appState) { // XXX
|
||||
return Object.assign({}, appState, {
|
||||
filters: Object.assign({}, appState.filters, {
|
||||
tags: [],
|
||||
categories: [],
|
||||
timerange: [
|
||||
d3.timeParse('%Y-%m-%dT%H:%M:%S')('2014-09-25T12:00:00'),
|
||||
d3.timeParse('%Y-%m-%dT%H:%M:%S')('2014-09-28T12:00:00')
|
||||
]
|
||||
}),
|
||||
selected: []
|
||||
})
|
||||
}
|
||||
|
||||
function toggleLanguage (appState, action) {
|
||||
let otherLanguage = (appState.language === 'es-MX') ? 'en-US' : 'es-MX'
|
||||
return Object.assign({}, appState, {
|
||||
@@ -228,10 +209,10 @@ function app (appState = initial.app, action) {
|
||||
return updateHighlighted(appState, action)
|
||||
case UPDATE_SELECTED:
|
||||
return updateSelected(appState, action)
|
||||
case UPDATE_TAGFILTERS:
|
||||
return updateTagFilters(appState, action)
|
||||
case UPDATE_CATEGORYFILTERS:
|
||||
return updateCategoryFilters(appState, action)
|
||||
case CLEAR_FILTER:
|
||||
return clearFilter(appState, action)
|
||||
case TOGGLE_FILTER:
|
||||
return toggleFilter(appState, action)
|
||||
case UPDATE_TIMERANGE:
|
||||
return updateTimeRange(appState, action)
|
||||
case UPDATE_NARRATIVE:
|
||||
|
||||
16
src/selectors/helpers.js
Normal file
16
src/selectors/helpers.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { parseTimestamp } from '../js/utilities'
|
||||
/**
|
||||
* Some handy helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Given an event and a time range,
|
||||
* returns true/false if the event falls within timeRange
|
||||
*/
|
||||
export function isTimeRangedIn (event, timeRange) {
|
||||
const eventTime = parseTimestamp(event.timestamp)
|
||||
return (
|
||||
timeRange[0] < eventTime &&
|
||||
eventTime < timeRange[1]
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSelector } from 'reselect'
|
||||
import { parseTimestamp, compareTimestamp, insetSourceFrom } from '../js/utilities'
|
||||
import { compareTimestamp, insetSourceFrom } from '../js/utilities'
|
||||
import { isTaggedIn, isNoTags, isTaggedInWithCategory, isNoCategories, isTimeRangedIn } from './helpers'
|
||||
|
||||
// Input selectors
|
||||
export const getEvents = state => state.domain.events
|
||||
@@ -22,91 +23,28 @@ export const getShapes = state => {
|
||||
}
|
||||
export const getNotifications = state => state.domain.notifications
|
||||
export const getTagTree = state => state.domain.tags
|
||||
export const getTagsFilter = state => state.app.filters.tags
|
||||
export const getCategoriesFilter = state => state.app.filters.categories
|
||||
export const getActiveTags = state => state.app.filters.tags
|
||||
export const getActiveCategories = state => state.app.filters.categories
|
||||
export const getTimeRange = state => state.app.timeline.range
|
||||
export const selectNarrative = state => state.app.narrative
|
||||
|
||||
/**
|
||||
* Some handy helpers
|
||||
*/
|
||||
|
||||
/**
|
||||
* Given an event and all tags,
|
||||
* returns true/false if event has any tag that is active
|
||||
*/
|
||||
function isTaggedIn (event, tagFilters) {
|
||||
if (event.tags) {
|
||||
const isTagged = event.tags.some((tag) => {
|
||||
return tagFilters.find(tF => (tF.key === tag && tF.active))
|
||||
})
|
||||
return isTagged
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event and all categories,
|
||||
* returns true/false if event has a category that is active
|
||||
*/
|
||||
function isTaggedInWithCategory (event, categories) {
|
||||
if (event.category) {
|
||||
if (categories.find(c => (c.category === event.category && c.active))) return true
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if no tags are selected
|
||||
*/
|
||||
function isNoTags (tagFilters) {
|
||||
return (
|
||||
tagFilters.length === 0 ||
|
||||
!process.env.features.USE_TAGS ||
|
||||
tagFilters.every(t => !t.active)
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns true if no categories are selected
|
||||
*/
|
||||
function isNoCategories (categories) {
|
||||
return (
|
||||
categories.length === 0 ||
|
||||
!process.env.features.CATEGORIES_AS_TAGS ||
|
||||
categories.every(c => !c.active)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an event and a time range,
|
||||
* returns true/false if the event falls within timeRange
|
||||
*/
|
||||
function isTimeRangedIn (event, timeRange) {
|
||||
const eventTime = parseTimestamp(event.timestamp)
|
||||
return (
|
||||
timeRange[0] < eventTime &&
|
||||
eventTime < timeRange[1]
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Of all available events, selects those that fall within the time range,
|
||||
* and if TAGS are being used, select them if their tags are enabled
|
||||
* Of all available events, selects those that
|
||||
* 1. fall in time range
|
||||
* 2. exist in an active tag
|
||||
* 3. exist in an active category
|
||||
*/
|
||||
export const selectEvents = createSelector(
|
||||
[getEvents, getTagsFilter, getCategoriesFilter, getTimeRange],
|
||||
(events, tagFilters, categories, timeRange) => {
|
||||
[getEvents, getActiveTags, getActiveCategories, getTimeRange],
|
||||
(events, activeTags, activeCategories, timeRange) => {
|
||||
return events.reduce((acc, event) => {
|
||||
const isTagged = isTaggedIn(event, tagFilters) || isNoTags(tagFilters)
|
||||
const isTaggedWithCategory = isTaggedInWithCategory(event, categories) || isNoCategories(categories)
|
||||
const isTimeRanged = isTimeRangedIn(event, timeRange)
|
||||
const isMatchingTag = event.tags.map(tag => activeTags.includes(tag)).some(s => s)
|
||||
const isActiveTag = isMatchingTag || activeTags.length === 0
|
||||
const isActiveCategory = activeCategories.includes(event.category) || activeCategories.length === 0
|
||||
const isActiveTime = isTimeRangedIn(event, timeRange)
|
||||
|
||||
if (isTimeRanged && isTagged && isTaggedWithCategory) {
|
||||
const eventClone = Object.assign({}, event)
|
||||
acc[event.id] = eventClone
|
||||
if (isActiveTime && isActiveTag && isActiveCategory) {
|
||||
acc[event.id] = { ...event }
|
||||
}
|
||||
|
||||
return acc
|
||||
@@ -180,14 +118,14 @@ export const selectActiveNarrative = createSelector(
|
||||
export const selectLocations = createSelector(
|
||||
[selectEvents],
|
||||
(events) => {
|
||||
const selectedLocations = {}
|
||||
const activeLocations = {}
|
||||
events.forEach(event => {
|
||||
const location = event.location
|
||||
|
||||
if (selectedLocations[location]) {
|
||||
selectedLocations[location].events.push(event)
|
||||
if (activeLocations[location]) {
|
||||
activeLocations[location].events.push(event)
|
||||
} else {
|
||||
selectedLocations[location] = {
|
||||
activeLocations[location] = {
|
||||
label: location,
|
||||
events: [event],
|
||||
latitude: event.latitude,
|
||||
@@ -195,7 +133,8 @@ export const selectLocations = createSelector(
|
||||
}
|
||||
}
|
||||
})
|
||||
return Object.values(selectedLocations)
|
||||
|
||||
return Object.values(activeLocations)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -242,44 +181,3 @@ export const selectSelected = createSelector(
|
||||
return selected.map(insetSourceFrom(sources))
|
||||
}
|
||||
)
|
||||
|
||||
/*
|
||||
* Select categories, return them as a list
|
||||
*/
|
||||
export const selectCategories = createSelector(
|
||||
[getCategories],
|
||||
(categories) => {
|
||||
categories.map(cat => {
|
||||
cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active
|
||||
})
|
||||
return categories
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Given a tree of tags, return those tags as a list
|
||||
* Each node has been aware of its depth, and given an 'active' flag
|
||||
*/
|
||||
export const selectTagList = createSelector(
|
||||
[getTagTree],
|
||||
(tags) => {
|
||||
const tagList = []
|
||||
let depth = 0
|
||||
function traverseNode (node, depth) {
|
||||
node.active = (!node.hasOwnProperty('active')) ? false : node.active
|
||||
node.depth = depth
|
||||
|
||||
if (node.active) tagList.push(node)
|
||||
|
||||
if (Object.keys(node.children).length > 0) {
|
||||
Object.values(node.children).forEach((childNode) => {
|
||||
traverseNode(childNode, depth + 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
if (tags && tags !== undefined) {
|
||||
if (tags.key && tags.children) traverseNode(tags, depth)
|
||||
}
|
||||
return tagList
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user