From 5174814244726f3e1891219ab110b8b8588c02ed Mon Sep 17 00:00:00 2001 From: Lachlan Kermode Date: Thu, 14 Feb 2019 15:27:48 +0000 Subject: [PATCH] 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 -// } -// )