diff --git a/src/actions/index.js b/src/actions/index.js
index 70c501d..ce01025 100644
--- a/src/actions/index.js
+++ b/src/actions/index.js
@@ -136,11 +136,11 @@ export function updateDistrict(district) {
};
}
-export const UPDATE_FILTERS = 'UPDATE_FILTERS';
-export function updateFilters(filters) {
+export const UPDATE_TAGFILTERS = 'UPDATE_TIMEFILTERS';
+export function updateTagFilters(tag) {
return {
- type: UPDATE_FILTERS,
- filters: filters
+ type: UPDATE_TAGFILTERS,
+ tag
};
}
diff --git a/src/components/Checkbox.jsx b/src/components/Checkbox.jsx
index 1d186b2..2bdd8dc 100644
--- a/src/components/Checkbox.jsx
+++ b/src/components/Checkbox.jsx
@@ -1,9 +1,9 @@
import '../scss/main.scss';
import React from 'react';
-export default ({ label, isActive, onClickLabel, onClickCheckbox }) => (
+export default ({ label, isActive, onClickCheckbox }) => (
-
onClickLabel()}>{label}
+
onClickCheckbox()}>{label}
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx
index f16a91b..2a496a4 100644
--- a/src/components/Dashboard.jsx
+++ b/src/components/Dashboard.jsx
@@ -21,7 +21,8 @@ class Dashboard extends React.Component {
this.handleHighlight = this.handleHighlight.bind(this);
this.handleSelect = this.handleSelect.bind(this);
this.handleToggle = this.handleToggle.bind(this);
- this.handleFilter = this.handleFilter.bind(this);
+ this.handleTagFilter = this.handleTagFilter.bind(this);
+ this.handleTimeFilter = this.handleTimeFilter.bind(this);
}
componentDidMount() {
@@ -67,8 +68,12 @@ class Dashboard extends React.Component {
}
}
- handleFilter(filters) {
- this.props.actions.updateFilters(filters);
+ handleTagFilter(tag) {
+ this.props.actions.updateTagFilters(tag);
+ }
+
+ handleTimeFilter(timeRange) {
+ this.props.actions.updateTimeRange(timeRange);
}
handleToggle( key ) {
@@ -139,7 +144,7 @@ class Dashboard extends React.Component {
toolbarTab={this.props.ui.components.toolbarTab}
isView2d={this.props.ui.flags.isView2d}
- filter={this.handleFilter}
+ filter={this.handleTagFilter}
toggle={ (key) => this.handleToggle(key) }
actions={this.props.actions}
/>
@@ -152,7 +157,6 @@ class Dashboard extends React.Component {
isFetchingEvents={this.props.ui.flags.isFetchingEvents}
highlight={this.handleHighlight}
- filter={this.handleFilter}
toggle={this.handleToggle}
getCategoryGroup={category => this.getCategoryGroup(category)}
getCategoryGroupColor={category => this.getCategoryGroupColor(category)}
@@ -170,7 +174,7 @@ class Dashboard extends React.Component {
dom={this.props.ui.dom}
select={this.handleSelect}
- filter={this.handleFilter}
+ filter={this.handleTimeFilter}
highlight={this.handleHighlight}
toggle={() => this.handleToggle('TOGGLE_CARDSTACK')}
getCategoryGroup={category => this.getCategoryGroup(category)}
@@ -209,7 +213,7 @@ function mapStateToProps(state) {
categories: selectors.getFilteredCategories(state),
categoryGroups: selectors.getCategoryGroups(state),
sites: selectors.getSites(state),
- tags: selectors.getTags(state),
+ tags: selectors.getAllTags(state),
notifications: state.domain.notifications,
}),
diff --git a/src/components/Notification.jsx b/src/components/Notification.jsx
index e718b9d..8af75e9 100644
--- a/src/components/Notification.jsx
+++ b/src/components/Notification.jsx
@@ -31,12 +31,12 @@ export default class Notification extends React.Component{
if (this.props.isNotification) {
return (
- {this.props.notifications.map(not => (
+ {this.props.notifications.map(notification => (
this.toggleDetails() }>
-
{`${not.message}`}
+
{`${notification.message}`}
- {(not.items !== null) ? this.renderItems(not.items) : ''}
+ {(notification.items !== null) ? this.renderItems(notification.items) : ''}
))}
diff --git a/src/components/TagFilter.jsx b/src/components/TagFilter.jsx
index 0d91001..dda91ac 100644
--- a/src/components/TagFilter.jsx
+++ b/src/components/TagFilter.jsx
@@ -53,7 +53,6 @@ class TagFilter extends React.Component {
this.onClickTag()}
onClickCheckbox={() => this.onClickTag()}
/>
@@ -73,7 +72,6 @@ class TagFilter extends React.Component {
this.onClickCategory()}
onClickCheckbox={() => this.onClickCategory()}
/>
diff --git a/src/components/TagListPanel.jsx b/src/components/TagListPanel.jsx
index cfcb774..da5a6a7 100644
--- a/src/components/TagListPanel.jsx
+++ b/src/components/TagListPanel.jsx
@@ -21,26 +21,9 @@ class TagListPanel extends React.Component {
this.computeTree(nextProps.tags.children[nextProps.tagType]);
}
- traverseNodeAndCheckIt(node, depth, active) {
- // do something to node
- const tagFilter = this.newTagFilters.find(tagFilter => tagFilter.key === node.key)
- tagFilter.active = (depth === 0) ? !node.active : active;
- tagFilter.depth = depth;
- depth = depth + 1;
-
- if (Object.keys(tagFilter.children).length > 0) {
- Object.values(tagFilter.children).forEach((childNode) => {
- this.traverseNodeAndCheckIt(childNode, depth, tagFilters, tagFilter.active);
- });
- }
- }
-
onClickCheckbox(tag) {
- this.newTagFilters = this.props.tagFilters.slice(0);
- let depth = 0;
- if (tag.key && tag.children) this.traverseNodeAndCheckIt(tag, depth);
-
- this.props.filter({ tags: this.newTagFilters });
+ tag.active = !tag.active
+ this.props.filter(tag);
}
createNodeComponent (node, depth) {
diff --git a/src/js/timeline/timeline.js b/src/js/timeline/timeline.js
index 7226b28..662b0ab 100644
--- a/src/js/timeline/timeline.js
+++ b/src/js/timeline/timeline.js
@@ -56,7 +56,7 @@ export default function(app, ui) {
let selected = [];
let range = app.range;
- const filter = app.filter;
+ const timeFilter = app.filter;
const select = app.select;
const getCategoryLabel = app.getCategoryLabel;
const getCategoryGroupColor = app.getCategoryGroupColor;
@@ -230,9 +230,7 @@ export default function(app, ui) {
})
.on('end', () => {
toggleTransition(true);
- filter({
- range: scale.x.domain()
- });
+ timeFilter(scale.x.domain());
});
/*
@@ -367,9 +365,7 @@ export default function(app, ui) {
const domainF = d3.timeMinute.offset(newCentralTime, zoom.duration / 2);
scale.x.domain([domain0, domainF]);
- filter({
- range: scale.x.domain()
- });
+ timeFilter(scale.x.domain());
}
/**
@@ -392,9 +388,7 @@ export default function(app, ui) {
}
scale.x.domain([domain0, domainF]);
- filter({
- range: scale.x.domain()
- });
+ timeFilter(scale.x.domain());
}
function toggleTransition(isTransition) {
diff --git a/src/reducers/app.js b/src/reducers/app.js
index c252f82..d9f579b 100644
--- a/src/reducers/app.js
+++ b/src/reducers/app.js
@@ -3,7 +3,7 @@ import initial from '../store/initial.js';
import {
UPDATE_HIGHLIGHTED,
UPDATE_SELECTED,
- UPDATE_FILTERS,
+ UPDATE_TAGFILTERS,
UPDATE_TIMERANGE,
RESET_ALLFILTERS,
TOGGLE_LANGUAGE,
@@ -22,15 +22,34 @@ function updateSelected(appState, action) {
});
}
-function updateFilters(appState, action) { // XXX
+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); });
+ }
+ }
+
+ traverseNode(action.tag);
+
return Object.assign({}, appState, {
- filters: Object.assign({}, appState.filters, action.filters)
+ filters: Object.assign({}, appState.filters, {
+ tags: tagFilters
+ })
});
}
function updateTimeRange(appState, action) { // XXX
return Object.assign({}, appState, {
- filters: Object.assign({}, appState.filters, action.range),
+ filters: Object.assign({}, appState.filters, {
+ range: action.range
+ }),
});
}
@@ -70,8 +89,8 @@ function app(appState = initial.app, action) {
return updateHighlighted(appState, action);
case UPDATE_SELECTED:
return updateSelected(appState, action);
- case UPDATE_FILTERS:
- return updateFilters(appState, action);
+ case UPDATE_TAGFILTERS:
+ return updateTagFilters(appState, action);
case UPDATE_TIMERANGE:
return updateTimeRange(appState, action);
case RESET_ALLFILTERS:
diff --git a/src/reducers/utils/validators.js b/src/reducers/utils/validators.js
index b6c38b9..6355e1b 100644
--- a/src/reducers/utils/validators.js
+++ b/src/reducers/utils/validators.js
@@ -18,6 +18,34 @@ function makeError(type, id, message) {
}
}
+
+const isLeaf = node => (Object.keys(node.children).length === 0);
+const isDuplicate = (node, set) => { return (set.has(node.key)); };
+
+
+/*
+* Traverse a tag tree and check its duplicates
+*/
+function validateTree(node, parent, set, duplicates) {
+ // If it's a leaf, check that it's not duplicate
+ if (isLeaf(node)) {
+ if (isDuplicate(node, set)) {
+ duplicates.push({
+ id: node.key,
+ error: makeError('Tags', node.key, 'tag was found more than once in hierarchy. Ignoring duplicate.')
+ });
+ delete parent.children[node.key];
+ } else {
+ set.add(node.key);
+ }
+ } else {
+ // If it's not a leaf, simply keep going
+ Object.values(node.children).forEach((childNode) => {
+ validateTree(childNode, node, set, duplicates);
+ });
+ }
+}
+
/*
* Validate domain schema
*/
@@ -27,7 +55,7 @@ export function validate(domain) {
categories: [],
sites: [],
notifications: domain.notifications,
- tags: domain.tags
+ tags: {}
}
const discardedDomain = {
@@ -59,7 +87,7 @@ export function validate(domain) {
validateItem(site, 'sites', siteSchema);
});
- // Message the number of failed items
+ // Message the number of failed items in domain
Object.keys(discardedDomain).forEach(disc => {
const len = discardedDomain[disc].length;
if (len) {
@@ -69,7 +97,22 @@ export function validate(domain) {
type: 'error'
});
}
- })
+ });
+
+ // Validate uniqueness of tags
+ const tagSet = new Set([]);
+ const duplicateTags = [];
+ validateTree(domain.tags, {}, tagSet, duplicateTags);
+
+ // Duplicated tags
+ if (duplicateTags.length > 0) {
+ sanitizedDomain.notifications.push({
+ message: `Tags are required to be unique. Ignoring duplicates for now.`,
+ items: duplicateTags,
+ type: 'error'
+ });
+ }
+ sanitizedDomain.tags = domain.tags;
return sanitizedDomain;
}
diff --git a/src/scss/notification.scss b/src/scss/notification.scss
index 4b25d46..49a8280 100644
--- a/src/scss/notification.scss
+++ b/src/scss/notification.scss
@@ -57,15 +57,23 @@
overflow: hidden;
display: flex;
flex-direction: column;
+ border-radius: 3px;
+ margin-top: 10px;
+ padding: 10px;
+ background: $darkgrey;
+ color: $offwhite;
+ font-family: monospace;
&.true {
height: auto;
- transition: height 0.4s;
+ transition: height 0.4s, margin 0.4s;
}
&.false {
height: 0;
- transition: height 0.4s;
+ padding: 0;
+ margin: 0;
+ transition: height 0.4s, margin 0.4s;
}
}
}
diff --git a/src/selectors/index.js b/src/selectors/index.js
index f30edbd..cd0bb46 100644
--- a/src/selectors/index.js
+++ b/src/selectors/index.js
@@ -10,7 +10,7 @@ export const getSites = (state) => {
if (process.env.features.USE_SITES) return state.domain.sites;
return [];
}
-export const getTags = state => state.domain.tags;
+export const getAllTags = state => state.domain.tags;
export const getCategoriesFilter = state => state.app.filters.categories;
export const getTagsFilter = state => state.app.filters.tags;
@@ -105,20 +105,20 @@ export const getCategoryGroups = createSelector(
}
)
+
/**
* Given a tree of tags, return those tags as a list, where each node has been
* aware of its depth, and given an 'active' flag
*/
export const getTagFilters = createSelector(
- [getTags],
+ [getAllTags],
(tags) => {
- const allTags = [];
+ const allTagFilters = [];
let depth = 0;
function traverseNode(node, depth) {
- // do something to node
node.active = (!node.hasOwnProperty('active')) ? false : node.active;
node.depth = depth;
- allTags.push(node)
+ if (node.active) allTagFilters.push(node)
depth = depth + 1;
if (Object.keys(node.children).length > 0) {
@@ -129,6 +129,6 @@ export const getTagFilters = createSelector(
}
if (tags.key && tags.children) traverseNode(tags, depth)
- return allTags;
+ return allTagFilters;
}
)