From 4e9128476d2e840d42fc158166c2b6995af60ff0 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 14 Feb 2019 14:27:36 +0000 Subject: [PATCH 1/5] clean tag representation and filtering --- src/actions/index.js | 13 ++- src/components/Layout.js | 2 +- src/components/Map.jsx | 6 +- src/components/Search.jsx | 2 +- src/components/TagFilter.js | 82 +++++++++++++++++++ src/components/TagFilter.jsx | 84 -------------------- src/components/TagListPanel.js | 45 +++++++++++ src/components/TagListPanel.jsx | 82 ------------------- src/components/Toolbar.jsx | 4 +- src/components/presentational/Map/Events.jsx | 1 + src/reducers/app.js | 48 ++++++----- src/selectors/index.js | 80 ++++++++++++------- 12 files changed, 224 insertions(+), 225 deletions(-) create mode 100644 src/components/TagFilter.js delete mode 100644 src/components/TagFilter.jsx create mode 100644 src/components/TagListPanel.js delete mode 100644 src/components/TagListPanel.jsx diff --git a/src/actions/index.js b/src/actions/index.js index 8ee00c0..4f98eaa 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -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 } } diff --git a/src/components/Layout.js b/src/components/Layout.js index 8a26dd8..fcca00c 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -120,7 +120,7 @@ class Dashboard extends React.Component { ) : 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), diff --git a/src/components/Search.jsx b/src/components/Search.jsx index 30e6bba..d4ed508 100644 --- a/src/components/Search.jsx +++ b/src/components/Search.jsx @@ -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) { diff --git a/src/components/TagFilter.js b/src/components/TagFilter.js new file mode 100644 index 0000000..1e8b4ee --- /dev/null +++ b/src/components/TagFilter.js @@ -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 ( +
  • + onClickTag()} + /> +
  • + ) + } + + function renderCategory () { + const category = props.categories[props.tag.id] + let classes = (isActive()) ? 'tag-filter active' : 'tag-filter' + + if (category) { + return ( +
  • + +
  • + ) + } + return (
    ) + } + + if (props.isCategory) return (renderCategory()) + return (renderTag()) +} + +export default TagFilter diff --git a/src/components/TagFilter.jsx b/src/components/TagFilter.jsx deleted file mode 100644 index c6618fc..0000000 --- a/src/components/TagFilter.jsx +++ /dev/null @@ -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 ( -
  • - this.onClickTag()} - /> -
  • - ) - } - - renderCategory () { - const category = this.props.categories[this.props.tag.id] - let classes = (this.isActive()) ? 'tag-filter active' : 'tag-filter' - - if (category) { - return ( -
  • - this.onClickCategory()} - /> -
  • - ) - } - return (
    ) - } - - render () { - if (this.props.isCategory) return (this.renderCategory()) - return (this.renderTag()) - } -} - -export default TagFilter diff --git a/src/components/TagListPanel.js b/src/components/TagListPanel.js new file mode 100644 index 0000000..15bf809 --- /dev/null +++ b/src/components/TagListPanel.js @@ -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 ( +
  • + onTagFilter(node.key)} + /> +
  • + ) + } + + function renderTree () { + /* NOTE: only render first layer of tags */ + return ( +
    + {Object.values(tags.children).map(tag => createNodeComponent(tag, 1))} +
    + ) + } + + return ( +
    +

    {copy[language].toolbar.tags}

    +

    {copy[language].toolbar.explore_by_tag__description}

    + {renderTree()} +
    + ) +} + +export default TagListPanel diff --git a/src/components/TagListPanel.jsx b/src/components/TagListPanel.jsx deleted file mode 100644 index 72582ae..0000000 --- a/src/components/TagListPanel.jsx +++ /dev/null @@ -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 ( -
  • - this.onClickCheckbox(node, 'tag')} - /> -
  • - ) - } - - 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 ( -
    - {this.state.treeComponents.map(c => c)} -
    - ) - } - - render () { - return ( -
    -

    {copy[this.props.language].toolbar.tags}

    -

    {copy[this.props.language].toolbar.explore_by_tag__description}

    - {this.renderTree()} -
    - ) - } -} - -export default TagListPanel diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 9c8990f..81043ab 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -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, diff --git a/src/components/presentational/Map/Events.jsx b/src/components/presentational/Map/Events.jsx index 9640386..c5d5d3c 100644 --- a/src/components/presentational/Map/Events.jsx +++ b/src/components/presentational/Map/Events.jsx @@ -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) diff --git a/src/reducers/app.js b/src/reducers/app.js index 83f2187..0cab34b 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -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: diff --git a/src/selectors/index.js b/src/selectors/index.js index 7d3e0f8..ca5258d 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -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 +// } +// ) From 5174814244726f3e1891219ab110b8b8588c02ed Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 14 Feb 2019 15:27:48 +0000 Subject: [PATCH 2/5] clean category filtering --- src/actions/index.js | 24 +-- ...esListPanel.jsx => CategoriesListPanel.js} | 22 +-- src/components/Layout.js | 4 +- src/components/Map.jsx | 6 +- src/components/TagListPanel.js | 4 +- src/components/Timeline.jsx | 2 +- src/components/Toolbar.jsx | 12 +- ...tomActions.jsx => ToolbarBottomActions.js} | 0 src/reducers/app.js | 44 ++--- src/selectors/helpers.js | 16 ++ src/selectors/index.js | 158 ++---------------- 11 files changed, 78 insertions(+), 214 deletions(-) rename src/components/{CategoriesListPanel.jsx => CategoriesListPanel.js} (58%) rename src/components/{ToolbarBottomActions.jsx => ToolbarBottomActions.js} (100%) create mode 100644 src/selectors/helpers.js diff --git a/src/actions/index.js b/src/actions/index.js index 4f98eaa..1b24f49 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -171,26 +171,20 @@ export function updateDistrict (district) { } } -export const CLEAR_TAGFILTERS = 'CLEAR_TAGFILTERS' -export function clearTagFilters () { +export const CLEAR_FILTER = 'CLEAR_FILTER' +export function clearFilter (filter) { return { - type: CLEAR_TAGFILTERS + type: CLEAR_FILTER, + filter } } -export const TOGGLE_TAGFILTER = 'TOGGLE_TAGFILTER' -export function toggleTagFilter (tag) { +export const TOGGLE_FILTER = 'TOGGLE_FILTER' +export function toggleFilter (filter, value) { return { - type: TOGGLE_TAGFILTER, - tag - } -} - -export const UPDATE_CATEGORYFILTERS = 'UPDATE_CATEGORYFILTERS' -export function updateCategoryFilters (category) { - return { - type: UPDATE_CATEGORYFILTERS, - category + type: TOGGLE_FILTER, + filter, + value } } diff --git a/src/components/CategoriesListPanel.jsx b/src/components/CategoriesListPanel.js similarity index 58% rename from src/components/CategoriesListPanel.jsx rename to src/components/CategoriesListPanel.js index a295aa0..3f4a63c 100644 --- a/src/components/CategoriesListPanel.jsx +++ b/src/components/CategoriesListPanel.js @@ -2,16 +2,16 @@ 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) - } - +export default ({ + categories, + activeCategories, + onCategoryFilter, + language +}) => { function renderCategoryTree () { return (
    - {props.categories.map(cat => { + {categories.map(cat => { return (
  • { > onClickCheckbox(cat, 'category')} + isActive={activeCategories.includes(cat.category)} + onClickCheckbox={() => onCategoryFilter(cat.category)} />
  • ) })} @@ -30,8 +30,8 @@ export default (props) => { return (
    -

    {copy[props.language].toolbar.categories}

    -

    {copy[props.language].toolbar.explore_by_category__description}

    +

    {copy[language].toolbar.categories}

    +

    {copy[language].toolbar.explore_by_category__description}

    {renderCategoryTree()}
    ) diff --git a/src/components/Layout.js b/src/components/Layout.js index fcca00c..961d115 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -120,8 +120,8 @@ class Dashboard extends React.Component { actions.toggleFilter('tags', tag), + onCategoryFilter: category => actions.toggleFilter('categories', category), onSelectNarrative: this.setNarrative }} /> diff --git a/src/components/Map.jsx b/src/components/Map.jsx index a288514..23401da 100644 --- a/src/components/Map.jsx +++ b/src/components/Map.jsx @@ -203,7 +203,7 @@ class Map extends React.Component { return ( onTagFilter(node.key)} /> diff --git a/src/components/Timeline.jsx b/src/components/Timeline.jsx index 2f489ea..f05eeeb 100644 --- a/src/components/Timeline.jsx +++ b/src/components/Timeline.jsx @@ -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: { diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 81043ab..39a1fc2 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -7,8 +7,8 @@ import * as selectors from '../selectors' import { Tabs, TabPanel } from 'react-tabs' import Search from './Search.jsx' import TagListPanel from './TagListPanel' -import CategoriesListPanel from './CategoriesListPanel.jsx' -import ToolbarBottomActions from './ToolbarBottomActions.jsx' +import CategoriesListPanel from './CategoriesListPanel' +import ToolbarBottomActions from './ToolbarBottomActions' import copy from '../js/data/copy.json' import { trimAndEllipse } from '../js/utilities.js' @@ -78,7 +78,7 @@ class Toolbar extends React.Component { @@ -94,7 +94,7 @@ class Toolbar extends React.Component { @@ -199,8 +199,8 @@ function mapStateToProps (state) { categories: selectors.getCategories(state), narratives: selectors.selectNarratives(state), language: state.app.language, - tagFilters: selectors.getTagsFilter(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, diff --git a/src/components/ToolbarBottomActions.jsx b/src/components/ToolbarBottomActions.js similarity index 100% rename from src/components/ToolbarBottomActions.jsx rename to src/components/ToolbarBottomActions.js diff --git a/src/reducers/app.js b/src/reducers/app.js index 0cab34b..9fc7d0d 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -5,9 +5,8 @@ import { parseDate, toggleFlagAC } from '../js/utilities' import { UPDATE_HIGHLIGHTED, UPDATE_SELECTED, - CLEAR_TAGFILTERS, - TOGGLE_TAGFILTER, - UPDATE_CATEGORYFILTERS, + CLEAR_FILTER, + TOGGLE_FILTER, UPDATE_TIMERANGE, UPDATE_NARRATIVE, INCREMENT_NARRATIVE_CURRENT, @@ -129,39 +128,22 @@ function clearTagFilters (appState) { } } -function toggleTagFilter (appState, action) { - let newTags = appState.filters.tags.slice(0) - if (newTags.includes(action.tag)) { - newTags = newTags.filter(s => s !== action.tag) +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 { - newTags.push(action.tag) + newTags.push(action.value) } return { ...appState, filters: { ...appState.filters, - tags: newTags + [action.filter]: newTags } } } -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) - } else { - catFilter.active = (!!action.category.active) - } - - return Object.assign({}, appState, { - filters: Object.assign({}, appState.filters, { - categories: categoryFilters - }) - }) -} function updateTimeRange (appState, action) { // XXX return { @@ -232,12 +214,10 @@ function app (appState = initial.app, action) { return updateHighlighted(appState, action) case UPDATE_SELECTED: return updateSelected(appState, action) - case CLEAR_TAGFILTERS: - return clearTagFilters(appState) - case TOGGLE_TAGFILTER: - return toggleTagFilter(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: diff --git a/src/selectors/helpers.js b/src/selectors/helpers.js new file mode 100644 index 0000000..af6190c --- /dev/null +++ b/src/selectors/helpers.js @@ -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] + ) +} diff --git a/src/selectors/index.js b/src/selectors/index.js index ca5258d..e9a8399 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -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,92 +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 @@ -201,28 +138,6 @@ export const selectLocations = createSelector( } ) -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 - }) - } -) - /** * Group events by 'datetime'. Each datetime is an object: { @@ -266,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 -// } -// ) From 0758a0d56eb19e7e32529d350026ea40e51990f8 Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 14 Feb 2019 15:35:35 +0000 Subject: [PATCH 3/5] house Toolbar components in folder --- src/components/Layout.js | 2 +- .../BottomActions.js} | 10 +++++----- .../{ => Toolbar}/CategoriesListPanel.js | 4 ++-- src/components/{Toolbar.jsx => Toolbar/Layout.js} | 14 +++++++------- src/components/{Search.jsx => Toolbar/Search.js} | 2 +- src/components/{ => Toolbar}/TagFilter.js | 2 +- src/components/{ => Toolbar}/TagListPanel.js | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) rename src/components/{ToolbarBottomActions.js => Toolbar/BottomActions.js} (76%) rename src/components/{ => Toolbar}/CategoriesListPanel.js (90%) rename src/components/{Toolbar.jsx => Toolbar/Layout.js} (95%) rename src/components/{Search.jsx => Toolbar/Search.js} (97%) rename src/components/{ => Toolbar}/TagFilter.js (97%) rename src/components/{ => Toolbar}/TagListPanel.js (90%) diff --git a/src/components/Layout.js b/src/components/Layout.js index 961d115..6c1ce26 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -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' diff --git a/src/components/ToolbarBottomActions.js b/src/components/Toolbar/BottomActions.js similarity index 76% rename from src/components/ToolbarBottomActions.js rename to src/components/Toolbar/BottomActions.js index 8040948..9ccfb8d 100644 --- a/src/components/ToolbarBottomActions.js +++ b/src/components/Toolbar/BottomActions.js @@ -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 [
    @@ -34,4 +34,4 @@ function ToolbarBottomActions (props) { ) } -export default ToolbarBottomActions +export default BottomActions diff --git a/src/components/CategoriesListPanel.js b/src/components/Toolbar/CategoriesListPanel.js similarity index 90% rename from src/components/CategoriesListPanel.js rename to src/components/Toolbar/CategoriesListPanel.js index 3f4a63c..f0c3bf6 100644 --- a/src/components/CategoriesListPanel.js +++ b/src/components/Toolbar/CategoriesListPanel.js @@ -1,6 +1,6 @@ import React from 'react' -import Checkbox from './presentational/Checkbox' -import copy from '../js/data/copy.json' +import Checkbox from '../presentational/Checkbox' +import copy from '../../js/data/copy.json' export default ({ categories, diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar/Layout.js similarity index 95% rename from src/components/Toolbar.jsx rename to src/components/Toolbar/Layout.js index 39a1fc2..1ba407b 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar/Layout.js @@ -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 Search from './Search' import TagListPanel from './TagListPanel' import CategoriesListPanel from './CategoriesListPanel' -import ToolbarBottomActions from './ToolbarBottomActions' -import copy from '../js/data/copy.json' -import { trimAndEllipse } from '../js/utilities.js' +import BottomActions from './BottomActions' +import copy from '../../js/data/copy.json' +import { trimAndEllipse } from '../../js/utilities.js' class Toolbar extends React.Component { constructor (props) { @@ -164,7 +164,7 @@ class Toolbar extends React.Component { {(isCategories) ? this.renderToolbarTab(1, categoriesLabel, 'widgets') : null} {(isTags) ? this.renderToolbarTab(2, tagsLabel, 'filter_list') : null}
    - Date: Thu, 14 Feb 2019 15:40:38 +0000 Subject: [PATCH 4/5] clear tags and categories when switching to narrative --- src/components/Layout.js | 6 +++++- src/reducers/app.js | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/Layout.js b/src/components/Layout.js index 6c1ce26..7d4db2e 100644 --- a/src/components/Layout.js +++ b/src/components/Layout.js @@ -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) } diff --git a/src/reducers/app.js b/src/reducers/app.js index 9fc7d0d..b47db77 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -144,6 +144,15 @@ function toggleFilter (appState, action) { } } +function clearFilter (appState, action) { + return { + ...appState, + filters: { + ...appState.filters, + [action.filter]: [] + } + } +} function updateTimeRange (appState, action) { // XXX return { From 2a275f7bd4b4983eba06ae6c7726f6162081723f Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 14 Feb 2019 15:47:39 +0000 Subject: [PATCH 5/5] rm old reset action --- src/actions/index.js | 7 ------- src/reducers/app.js | 14 -------------- 2 files changed, 21 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index 1b24f49..8530bb8 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -218,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 { diff --git a/src/reducers/app.js b/src/reducers/app.js index b47db77..ce2e207 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -164,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, {