mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-11 21:08:36 +03:00
clean tag representation and filtering
This commit is contained in:
@@ -171,10 +171,17 @@ export function updateDistrict (district) {
|
||||
}
|
||||
}
|
||||
|
||||
export const UPDATE_TAGFILTERS = 'UPDATE_TAGFILTERS'
|
||||
export function updateTagFilters (tag) {
|
||||
export const CLEAR_TAGFILTERS = 'CLEAR_TAGFILTERS'
|
||||
export function clearTagFilters () {
|
||||
return {
|
||||
type: UPDATE_TAGFILTERS,
|
||||
type: CLEAR_TAGFILTERS
|
||||
}
|
||||
}
|
||||
|
||||
export const TOGGLE_TAGFILTER = 'TOGGLE_TAGFILTER'
|
||||
export function toggleTagFilter (tag) {
|
||||
return {
|
||||
type: TOGGLE_TAGFILTER,
|
||||
tag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ class Dashboard extends React.Component {
|
||||
<Toolbar
|
||||
isNarrative={!!app.narrative}
|
||||
methods={{
|
||||
onTagFilter: actions.updateTagFilters,
|
||||
onTagFilter: actions.toggleTagFilter,
|
||||
onCategoryFilter: actions.updateCategoryFilters,
|
||||
onSelectNarrative: this.setNarrative
|
||||
}}
|
||||
|
||||
@@ -203,7 +203,7 @@ class Map extends React.Component {
|
||||
return (
|
||||
<Events
|
||||
svg={this.svgRef.current}
|
||||
locations={this.props.domain.locations}
|
||||
locations={this.props.domain.visibleLocations}
|
||||
styleLocation={this.styleLocation}
|
||||
categories={this.props.domain.categories}
|
||||
projectPoint={this.projectPoint}
|
||||
@@ -245,7 +245,7 @@ class Map extends React.Component {
|
||||
{this.renderShapes()}
|
||||
{this.renderNarratives()}
|
||||
{this.renderEvents()}
|
||||
{this.renderSelected()}
|
||||
{this.renderSelected()}
|
||||
</React.Fragment>
|
||||
) : null
|
||||
|
||||
@@ -261,7 +261,7 @@ class Map extends React.Component {
|
||||
function mapStateToProps (state) {
|
||||
return {
|
||||
domain: {
|
||||
locations: selectors.selectLocations(state),
|
||||
visibleLocations: selectors.selectVisibleLocations(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
categories: selectors.selectCategories(state),
|
||||
sites: selectors.getSites(state),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* global fetch */
|
||||
import React from 'react'
|
||||
import copy from '../js/data/copy.json'
|
||||
import TagFilter from './TagFilter.jsx'
|
||||
import TagFilter from './TagFilter'
|
||||
|
||||
export default class Search extends React.Component {
|
||||
constructor (props) {
|
||||
|
||||
82
src/components/TagFilter.js
Normal file
82
src/components/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
|
||||
@@ -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
|
||||
45
src/components/TagListPanel.js
Normal file
45
src/components/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,
|
||||
tagFilters,
|
||||
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={tagFilters.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
|
||||
@@ -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
|
||||
@@ -6,7 +6,7 @@ import * as selectors from '../selectors'
|
||||
|
||||
import { Tabs, TabPanel } from 'react-tabs'
|
||||
import Search from './Search.jsx'
|
||||
import TagListPanel from './TagListPanel.jsx'
|
||||
import TagListPanel from './TagListPanel'
|
||||
import CategoriesListPanel from './CategoriesListPanel.jsx'
|
||||
import ToolbarBottomActions from './ToolbarBottomActions.jsx'
|
||||
import copy from '../js/data/copy.json'
|
||||
@@ -199,7 +199,7 @@ function mapStateToProps (state) {
|
||||
categories: selectors.getCategories(state),
|
||||
narratives: selectors.selectNarratives(state),
|
||||
language: state.app.language,
|
||||
tagFilters: selectors.selectTagList(state),
|
||||
tagFilters: selectors.getTagsFilter(state),
|
||||
categoryFilters: selectors.selectCategories(state),
|
||||
viewFilters: state.app.filters.views,
|
||||
features: state.app.features,
|
||||
|
||||
@@ -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,7 +5,8 @@ import { parseDate, toggleFlagAC } from '../js/utilities'
|
||||
import {
|
||||
UPDATE_HIGHLIGHTED,
|
||||
UPDATE_SELECTED,
|
||||
UPDATE_TAGFILTERS,
|
||||
CLEAR_TAGFILTERS,
|
||||
TOGGLE_TAGFILTER,
|
||||
UPDATE_CATEGORYFILTERS,
|
||||
UPDATE_TIMERANGE,
|
||||
UPDATE_NARRATIVE,
|
||||
@@ -118,27 +119,30 @@ 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 toggleTagFilter (appState, action) {
|
||||
let newTags = appState.filters.tags.slice(0)
|
||||
if (newTags.includes(action.tag)) {
|
||||
newTags = newTags.filter(s => s !== action.tag)
|
||||
} else {
|
||||
newTags.push(action.tag)
|
||||
}
|
||||
return {
|
||||
...appState,
|
||||
filters: {
|
||||
...appState.filters,
|
||||
tags: newTags
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateCategoryFilters (appState, action) {
|
||||
@@ -228,8 +232,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 CLEAR_TAGFILTERS:
|
||||
return clearTagFilters(appState)
|
||||
case TOGGLE_TAGFILTER:
|
||||
return toggleTagFilter(appState, action)
|
||||
case UPDATE_CATEGORYFILTERS:
|
||||
return updateCategoryFilters(appState, action)
|
||||
case UPDATE_TIMERANGE:
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 getTimeRange = state => state.app.timeline.range
|
||||
export const selectNarrative = state => state.app.narrative
|
||||
|
||||
/**
|
||||
* Some handy helpers
|
||||
@@ -180,14 +181,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 +196,30 @@ export const selectLocations = createSelector(
|
||||
}
|
||||
}
|
||||
})
|
||||
return Object.values(selectedLocations)
|
||||
|
||||
return Object.values(activeLocations)
|
||||
}
|
||||
)
|
||||
|
||||
export const selectVisibleLocations = createSelector(
|
||||
[selectLocations, getTagsFilter, selectNarrative],
|
||||
(locations, filters, narrative) => {
|
||||
if (filters.length === 0) {
|
||||
return locations
|
||||
}
|
||||
|
||||
return locations.map(loc => {
|
||||
loc.events = loc.events.filter(ev => {
|
||||
let isShowing = false
|
||||
ev.tags.forEach(tag => {
|
||||
if (filters.includes(tag)) {
|
||||
isShowing = true
|
||||
}
|
||||
})
|
||||
return isShowing
|
||||
})
|
||||
return loc
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -260,26 +284,26 @@ export const selectCategories = createSelector(
|
||||
* 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
|
||||
}
|
||||
)
|
||||
// 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