diff --git a/src/actions/index.js b/src/actions/index.js index da6fe64..c8bdf0e 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -189,7 +189,7 @@ export function updateDistrict(district) { } } -export const UPDATE_TAGFILTERS = 'UPDATE_TIMEFILTERS' +export const UPDATE_TAGFILTERS = 'UPDATE_TAGFILTERS' export function updateTagFilters(tag) { return { type: UPDATE_TAGFILTERS, @@ -197,6 +197,14 @@ export function updateTagFilters(tag) { } } +export const UPDATE_CATEGORYFILTERS = 'UPDATE_CATEGORYFILTERS' +export function updateCategoryFilters(category) { + return { + type: UPDATE_CATEGORYFILTERS, + category + } +} + export const UPDATE_TIMERANGE = 'UPDATE_TIMERANGE'; export function updateTimeRange(timerange) { return { diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx index 077e0f2..e0a59aa 100644 --- a/src/components/Dashboard.jsx +++ b/src/components/Dashboard.jsx @@ -98,7 +98,8 @@ class Dashboard extends React.Component { diff --git a/src/components/TagListPanel.jsx b/src/components/TagListPanel.jsx index 1ca46e7..3406140 100644 --- a/src/components/TagListPanel.jsx +++ b/src/components/TagListPanel.jsx @@ -1,5 +1,6 @@ import React from 'react'; import Checkbox from './presentational/Checkbox'; +import copy from '../js/data/copy.json'; class TagListPanel extends React.Component { @@ -20,9 +21,10 @@ class TagListPanel extends React.Component { this.computeTree(nextProps.tags);//.children[nextProps.tagType]); } - onClickCheckbox(tag) { - tag.active = !tag.active - this.props.onFilter(tag); + onClickCheckbox(obj, type) { + obj.active = !obj.active + if (type === 'category') this.props.onCategoryFilter(obj); + if (type === 'tag') this.props.onTagFilter(obj); } createNodeComponent (node, depth) { @@ -35,7 +37,7 @@ class TagListPanel extends React.Component { this.onClickCheckbox(node)} + onClickCheckbox={() => this.onClickCheckbox(node, 'tag')} /> ); @@ -61,15 +63,42 @@ class TagListPanel extends React.Component { } renderTree() { - return this.state.treeComponents.map(c => c); + return ( +
+

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

+ {this.state.treeComponents.map(c => c)} +
+ ) + } + + renderCategoryTree() { + return ( +
+

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

+ {this.props.categories.map(cat => { + return (
  • + this.onClickCheckbox(cat, 'category')} + /> +
  • ) + }) + } +
    + ) } render() { - return (
    -

    Explore data by tag

    -

    Explore freely all the data by selecting tags.

    +

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

    +

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

    + {this.renderCategoryTree()} {this.renderTree()}
    ); diff --git a/src/components/Toolbar.jsx b/src/components/Toolbar.jsx index 53b0808..4826445 100644 --- a/src/components/Toolbar.jsx +++ b/src/components/Toolbar.jsx @@ -81,7 +81,8 @@ class Toolbar extends React.Component { categories={this.props.categories} tagFilters={this.props.tagFilters} categoryFilters={this.props.categoryFilters} - onFilter={this.props.methods.onFilter} + onTagFilter={this.props.methods.onTagFilter} + onCategoryFilter={this.props.methods.onCategoryFilter} language={this.props.language} /> @@ -185,11 +186,11 @@ class Toolbar extends React.Component { function mapStateToProps(state) { return { tags: selectors.getTagTree(state), - categories: selectors.selectCategories(state), + categories: selectors.getCategories(state), narratives: selectors.selectNarratives(state), language: state.app.language, tagFilters: selectors.selectTagList(state), - categoryFilter: state.app.filters.categories, + categoryFilters: selectors.selectCategories(state), viewFilters: state.app.filters.views, features: state.app.features, narrative: state.app.narrative, diff --git a/src/js/data/copy.json b/src/js/data/copy.json index 039a50d..6453a2a 100644 --- a/src/js/data/copy.json +++ b/src/js/data/copy.json @@ -18,6 +18,10 @@ }, "toolbar": { "title": "TITLE", + "categories": "Categories", + "tags": "Tags", + "explore_by_tag__title": "Explore by tag or category", + "explore_by_tag__description": "Selecting tags or categories, you'll see only those events that are tagged accordingly. If you select nothing, as well as everything, all data will be displayed.", "panels": { "mentions": { "title": "Personas", @@ -105,7 +109,11 @@ } }, "narrative_panel_title": "Focus narratives", - "narrative_summary": "Here you can follow some curated stories we have found in the data." + "narrative_summary": "Here you can follow some curated stories we have found in the data.", + "categories": "Categories", + "tags": "Tags", + "explore_by_tag__title": "Explore by tag or category", + "explore_by_tag__description": "Selecting tags or categories, you'll see only those events that are tagged accordingly. If you select nothing, as well as everything, all data will be displayed." }, "timeline": { "zooms": [ diff --git a/src/reducers/app.js b/src/reducers/app.js index a17ec9a..b7164ea 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -1,11 +1,10 @@ import initial from '../store/initial.js' -import { parseDate } from '../js/utilities.js' - import { UPDATE_HIGHLIGHTED, UPDATE_SELECTED, UPDATE_TAGFILTERS, + UPDATE_CATEGORYFILTERS, UPDATE_TIMERANGE, UPDATE_NARRATIVE, INCREMENT_NARRATIVE_CURRENT, @@ -136,6 +135,25 @@ function updateTagFilters(appState, action) { }) } +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 Object.assign({}, appState, { filters: Object.assign({}, appState.filters, { @@ -254,6 +272,8 @@ function app(appState = initial.app, action) { return updateSelected(appState, action) case UPDATE_TAGFILTERS: return updateTagFilters(appState, action) + case UPDATE_CATEGORYFILTERS: + return updateCategoryFilters(appState, action) case UPDATE_TIMERANGE: return updateTimeRange(appState, action) case UPDATE_NARRATIVE: diff --git a/src/selectors/index.js b/src/selectors/index.js index aec7e5c..2367b35 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -20,6 +20,7 @@ export const getSources = 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 getTimeRange = state => state.app.filters.timerange @@ -42,6 +43,19 @@ function isTaggedIn(event, tagFilters) { } } +/** + * 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 */ @@ -53,6 +67,17 @@ function isNoTags(tagFilters) { ) } +/* +* 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 @@ -69,14 +94,15 @@ function isTimeRangedIn(event, timeRange) { * and if TAGS are being used, select them if their tags are enabled */ export const selectEvents = createSelector( - [getEvents, getTagsFilter, getTimeRange], - (events, tagFilters, timeRange) => { + [getEvents, getTagsFilter, getCategoriesFilter, getTimeRange], + (events, tagFilters, categories, 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) - if (isTimeRanged && isTagged) { + if (isTimeRanged && isTagged && isTaggedWithCategory) { const eventClone = Object.assign({}, event) acc[event.id] = eventClone } @@ -90,16 +116,14 @@ export const selectEvents = createSelector( * and if TAGS are being used, select them if their tags are enabled */ export const selectNarratives = createSelector( - [getEvents, getNarratives, getTagsFilter, getTimeRange, getSources], - (events, narrativesMeta, tagFilters, timeRange, sources) => { + [getEvents, getNarratives, getSources], + (events, narrativesMeta, sources) => { const narratives = {} const narrativeSkeleton = id => ({ id, steps: [] }) /* populate narratives dict with events */ events.forEach(evt => { - const isTagged = isTaggedIn(evt, tagFilters) || isNoTags(tagFilters) - const isTimeRanged = isTimeRangedIn(evt, timeRange) const isInNarrative = evt.narratives.length > 0 evt.narratives.forEach(narrative => { @@ -229,7 +253,13 @@ export const selectSelected = createSelector( */ export const selectCategories = createSelector( [getCategories], - (categories) => categories + (categories) => { + categories.map(cat => { + cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active + }); + console.log(categories) + return categories; + } )