mirror of
https://github.com/bellingcat/ukraine-timemap.git
synced 2026-06-13 05:48:36 +03:00
Merge pull request #84 from forensic-architecture/topic/categories-as-tags
Topic/categories as tags
This commit is contained in:
@@ -14,7 +14,8 @@ module.exports = {
|
||||
USE_TAGS: false,
|
||||
USE_SEARCH: false,
|
||||
USE_SITES: true,
|
||||
USE_SOURCES: true
|
||||
USE_SOURCES: true,
|
||||
CATEGORIES_AS_TAGS: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +1,25 @@
|
||||
// TODO: move to util lib
|
||||
function urlFromEnv(ext) {
|
||||
if (process.env[ext]) {
|
||||
return `${process.env.SERVER_ROOT}${process.env[ext]}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
import { urlFromEnv } from '../js/utilities'
|
||||
|
||||
// TODO: relegate these URLs entirely to environment variables
|
||||
const EVENT_DATA_URL = urlFromEnv('EVENT_EXT');
|
||||
const CATEGORY_URL = urlFromEnv('CATEGORY_EXT');
|
||||
const TAG_URL = urlFromEnv('TAGS_EXT');
|
||||
const TAGS_URL = urlFromEnv('TAGS_EXT');
|
||||
const SOURCES_URL = urlFromEnv('SOURCES_EXT');
|
||||
const NARRATIVE_URL = urlFromEnv('NARRATIVE_EXT');
|
||||
const SITES_URL = urlFromEnv('SITES_EXT');
|
||||
const eventUrlMap = (event) => `${process.env.SERVER_ROOT}${process.env.EVENT_DESC_ROOT}/${(event.id) ? event.id : event}`;
|
||||
|
||||
const domainMsg = (domainType) => `Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.`
|
||||
|
||||
const DEBUG_GER = 'DEBUG_GER'
|
||||
function _debugger(value) {
|
||||
console.log(value)
|
||||
return {
|
||||
type: DEBUG_GER,
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Create an error notification object
|
||||
* Types: ['error', 'warning', 'good', 'neural']
|
||||
*/
|
||||
function makeError (type, id, message) {
|
||||
return {
|
||||
type: 'error',
|
||||
id,
|
||||
message: `${type} ${id}: ${message}`
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchDomain () {
|
||||
let notifications = []
|
||||
|
||||
function handleError (domainType) {
|
||||
return () => {
|
||||
notifications.push({
|
||||
message: `Something went wrong fetching ${domainType}. Check the URL or try disabling them in the config file.`,
|
||||
type: 'error'
|
||||
})
|
||||
return []
|
||||
}
|
||||
function handleError (message) {
|
||||
notifications.push({
|
||||
message,
|
||||
type: 'error'
|
||||
})
|
||||
return []
|
||||
}
|
||||
|
||||
return dispatch => {
|
||||
@@ -57,35 +28,43 @@ export function fetchDomain () {
|
||||
|
||||
const eventPromise = fetch(EVENT_DATA_URL)
|
||||
.then(response => response.json())
|
||||
.catch(handleError('events'))
|
||||
.catch(() => handleError('events'))
|
||||
|
||||
const catPromise = fetch(CATEGORY_URL)
|
||||
.then(response => response.json())
|
||||
.catch(handleError('categories'))
|
||||
.catch(() => handleError(domainMsg('categories')))
|
||||
|
||||
const narPromise = fetch(NARRATIVE_URL)
|
||||
.then(response => response.json())
|
||||
.catch(handleError('narratives'))
|
||||
.catch(() => handleError(domainMsg('narratives')))
|
||||
|
||||
let sitesPromise = Promise.resolve([])
|
||||
if (process.env.features.USE_SITES) {
|
||||
sitesPromise = fetch(SITES_URL)
|
||||
.then(response => response.json())
|
||||
.catch(handleError('sites'))
|
||||
.catch(() => handleError(domainMsg('sites')))
|
||||
}
|
||||
|
||||
let tagsPromise = Promise.resolve([])
|
||||
if (process.env.features.USE_TAGS) {
|
||||
tagsPromise = fetch(TAG_URL)
|
||||
.then(response => response.json())
|
||||
.catch(handleError('tags'))
|
||||
if (!TAGS_URL) {
|
||||
tagsPromise = Promise.resolve(handleError('USE_TAGS is true, but you have not provided a TAGS_EXT'))
|
||||
} else {
|
||||
tagsPromise = fetch(TAGS_URL)
|
||||
.then(response => response.json())
|
||||
.catch(() => handleError(domainMsg('tags')))
|
||||
}
|
||||
}
|
||||
|
||||
let sourcesPromise = Promise.resolve([])
|
||||
if (process.env.features.USE_SOURCES) {
|
||||
sourcesPromise = fetch(SOURCES_URL)
|
||||
.then(response => response.json())
|
||||
.catch(handleError('sources'))
|
||||
if (!SOURCES_URL) {
|
||||
sourcesPromise = Promise.resolve(makeError('USE_SOURCES is true, but you have not provided a SOURCES_EXT'))
|
||||
} else {
|
||||
sourcesPromise = fetch(SOURCES_URL)
|
||||
.then(response => response.json())
|
||||
.catch(() => handleError(domainMsg('sources')))
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
@@ -152,9 +131,6 @@ export function fetchSource(source) {
|
||||
return response.json()
|
||||
}
|
||||
})
|
||||
.then(sources => {
|
||||
dispatch(_debugger(sources))
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(fetchSourceError(err.message))
|
||||
dispatch(toggleFetchingSources())
|
||||
@@ -189,7 +165,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 +173,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 {
|
||||
|
||||
@@ -98,7 +98,8 @@ class Dashboard extends React.Component {
|
||||
<Toolbar
|
||||
isNarrative={!!app.narrative}
|
||||
methods={{
|
||||
onFilter: actions.updateTagFilters,
|
||||
onTagFilter: actions.updateTagFilters,
|
||||
onCategoryFilter: actions.updateCategoryFilters,
|
||||
onSelectNarrative: this.setNarrative
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,7 @@ function SourceOverlay ({ source, onCancel }) {
|
||||
<Img
|
||||
className='source-image'
|
||||
src={path}
|
||||
loader={<Spinner />}
|
||||
loader={<div style={{ width: '400px', height: '400px' }}><Spinner /></div>}
|
||||
unloader={<NoSource failedUrls={source.paths} />}
|
||||
/>
|
||||
</div>
|
||||
@@ -107,8 +107,8 @@ function SourceOverlay ({ source, onCancel }) {
|
||||
return (
|
||||
<div>
|
||||
{img ? img : ''}
|
||||
{vid ? `, ${vid}`: ''}
|
||||
{txt ? `, ${txt}`: ''}
|
||||
{(img && vid) ? `, ${vid}`: (vid || '')}
|
||||
{((img || vid) && txt) ? `, ${txt}`: (txt || '')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -145,6 +145,7 @@ function SourceOverlay ({ source, onCancel }) {
|
||||
<div className="mo-box">
|
||||
{title? <p><b>{title}</b></p> : null}
|
||||
<div>{_renderCounts(counts)}</div>
|
||||
<hr />
|
||||
{type ? <h4>Media type</h4> : null}
|
||||
{type ? <p><i className="material-icons left">perm_media</i>{type}</p> : null}
|
||||
{date ? <h4>Date</h4> : null}
|
||||
|
||||
@@ -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.filter(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 {
|
||||
<Checkbox
|
||||
label={node.key}
|
||||
isActive={node.active}
|
||||
onClickCheckbox={() => this.onClickCheckbox(node)}
|
||||
onClickCheckbox={() => this.onClickCheckbox(node, 'tag')}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
@@ -61,15 +63,42 @@ class TagListPanel extends React.Component {
|
||||
}
|
||||
|
||||
renderTree() {
|
||||
return this.state.treeComponents.map(c => c);
|
||||
return (
|
||||
<div>
|
||||
<h2>{copy[this.props.language].toolbar.tags}</h2>
|
||||
{this.state.treeComponents.map(c => c)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderCategoryTree() {
|
||||
return (
|
||||
<div>
|
||||
<h2>{copy[this.props.language].toolbar.categories}</h2>
|
||||
{this.props.categories.map(cat => {
|
||||
return (<li
|
||||
key={cat.category.replace(/ /g,"_")}
|
||||
className={'tag-filter active'}
|
||||
style={{ marginLeft: '20px' }}
|
||||
>
|
||||
<Checkbox
|
||||
label={cat.category}
|
||||
isActive={cat.active}
|
||||
onClickCheckbox={() => this.onClickCheckbox(cat, 'category')}
|
||||
/>
|
||||
</li>)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<div className="react-innertabpanel">
|
||||
<h2>Explore data by tag</h2>
|
||||
<p>Explore freely all the data by selecting tags.</p>
|
||||
<h2>{copy[this.props.language].toolbar.explore_by_tag__title}</h2>
|
||||
<p>{copy[this.props.language].toolbar.explore_by_tag__description}</p>
|
||||
{this.renderCategoryTree()}
|
||||
{this.renderTree()}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -82,7 +82,8 @@ class Timeline extends React.Component {
|
||||
}
|
||||
|
||||
makeScaleY(categories) {
|
||||
const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length);
|
||||
const tickHeight = 15;
|
||||
const catsYpos = categories.map((g, i) => (i + 1) * this.state.dims.trackHeight / categories.length + tickHeight / 2);
|
||||
return d3.scaleOrdinal()
|
||||
.domain(categories)
|
||||
.range(catsYpos);
|
||||
|
||||
@@ -25,7 +25,7 @@ class TimelineCategories extends React.Component {
|
||||
}
|
||||
|
||||
getY(idx) {
|
||||
return (idx + 1) * this.props.dims.trackHeight / this.props.categories.length
|
||||
return (idx + 1) * this.props.dims.trackHeight / this.props.categories.length + 7.5;
|
||||
}
|
||||
|
||||
renderCategory(category, idx) {
|
||||
|
||||
@@ -81,7 +81,8 @@ class Toolbar extends React.Component {
|
||||
categories={this.props.categories}
|
||||
tagFilters={this.props.tagFilters}
|
||||
categoryFilters={this.props.categoryFilters}
|
||||
filter={this.props.filter}
|
||||
onTagFilter={this.props.methods.onTagFilter}
|
||||
onCategoryFilter={this.props.methods.onCategoryFilter}
|
||||
language={this.props.language}
|
||||
/>
|
||||
</TabPanel>
|
||||
@@ -90,34 +91,18 @@ class Toolbar extends React.Component {
|
||||
return '';
|
||||
}
|
||||
|
||||
renderToolbarTab(_selected, label) {
|
||||
renderToolbarTab(_selected, label, icon_key) {
|
||||
const isActive = (this.state._selected === _selected);
|
||||
let classes = (isActive) ? 'toolbar-tab active' : 'toolbar-tab';
|
||||
|
||||
return (
|
||||
<div className={classes} onClick={() => { this.selectTab(_selected); }}>
|
||||
<i className="material-icons">timeline</i>
|
||||
<i className="material-icons">{icon_key}</i>
|
||||
<div className="tab-caption">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderToolbarTabs() {
|
||||
const title = copy[this.props.language].toolbar.title;
|
||||
const isTags = this.props.tags && (this.props.tags.children > 0);
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<div className="toolbar-header"><p>{title}</p></div>
|
||||
<div className="toolbar-tabs">
|
||||
{/*this.renderToolbarTab(0, 'search')*/}
|
||||
{this.renderToolbarTab(0, 'Focus stories')}
|
||||
{this.renderToolbarTab(1, 'Explore freely')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderToolbarPanels() {
|
||||
let classes = (this.state._selected >= 0) ? 'toolbar-panels' : 'toolbar-panels folded';
|
||||
return (
|
||||
@@ -125,7 +110,7 @@ class Toolbar extends React.Component {
|
||||
{this.renderClosePanel()}
|
||||
<Tabs selectedIndex={this.state._selected}>
|
||||
{this.renderToolbarNarrativePanel()}
|
||||
{/* {this.renderToolbarTagPanel()} */}
|
||||
{this.renderToolbarTagPanel()}}
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
@@ -150,15 +135,17 @@ class Toolbar extends React.Component {
|
||||
|
||||
renderToolbarTabs() {
|
||||
const title = copy[this.props.language].toolbar.title;
|
||||
const isTags = this.props.tags && (this.props.tags.children > 0);
|
||||
const narratives_label = copy[this.props.language].toolbar.narratives_label;
|
||||
const tags_label = copy[this.props.language].toolbar.tags_label;
|
||||
const isTags = this.props.tags && this.props.tags.children;
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<div className="toolbar-header"><p>{title}</p></div>
|
||||
<div className="toolbar-tabs">
|
||||
{/*this.renderToolbarTab(0, 'search')*/}
|
||||
{this.renderToolbarTab(0, 'Narratives')}
|
||||
{(isTags) ? this.renderToolbarTab(1, 'Explore by tag') : ''}
|
||||
{this.renderToolbarTab(0, narratives_label, 'timeline')}
|
||||
{(isTags) ? this.renderToolbarTab(1, tags_label, 'style') : ''}
|
||||
</div>
|
||||
<ToolbarBottomActions
|
||||
sites={{
|
||||
@@ -185,11 +172,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,
|
||||
|
||||
@@ -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",
|
||||
@@ -104,8 +108,14 @@
|
||||
"placeholder": "Search"
|
||||
}
|
||||
},
|
||||
"narrative_panel_title": "Focus narratives",
|
||||
"narrative_summary": "Here you can follow some curated stories we have found in the data."
|
||||
"narratives_label": "Narratives",
|
||||
"narrative_summary": "Here you can follow some curated stories we have found in the data.",
|
||||
"categories": "Categories",
|
||||
"tags": "Tags",
|
||||
"tags_label": "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": [
|
||||
|
||||
@@ -116,3 +116,11 @@ export function injectSource(id) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function urlFromEnv(ext) {
|
||||
if (process.env[ext]) {
|
||||
return `${process.env.SERVER_ROOT}${process.env[ext]}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -13,7 +13,7 @@ const eventSchema = Joi.object().keys({
|
||||
category: Joi.string().required(),
|
||||
narratives: Joi.array(),
|
||||
sources: Joi.array(),
|
||||
tags: Joi.string().allow(''),
|
||||
tags: Joi.array().allow(''),
|
||||
comments: Joi.string().allow(''),
|
||||
timestamp: Joi.string().required(),
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ $panel-width: 800px;
|
||||
$panel-height: 700px;
|
||||
$vimeo-width: $panel-width - 100;
|
||||
$vimeo-height: $panel-height / 2;
|
||||
|
||||
$padding: 20px;
|
||||
$header-inset: 10px;
|
||||
|
||||
@@ -21,7 +20,6 @@ $header-inset: 10px;
|
||||
}
|
||||
|
||||
.mo-container {
|
||||
background-color: rgba(239, 239, 239, 0.9);
|
||||
// max-width: $panel-width;
|
||||
// min-width: $panel-width;
|
||||
// max-height: $panel-height;
|
||||
@@ -29,7 +27,7 @@ $header-inset: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
max-height: 80vh;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 19px 19px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
|
||||
@@ -37,8 +35,35 @@ $header-inset: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mo-header {
|
||||
min-height: 42px;
|
||||
max-height: 42px;
|
||||
margin-bottom: 2px;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: black;
|
||||
color: white;
|
||||
|
||||
.mo-header-close {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: $header-inset + 8px;
|
||||
}
|
||||
|
||||
.mo-header-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: $padding;
|
||||
font-family: "Lato", Helvetica, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +94,7 @@ $header-inset: 10px;
|
||||
}
|
||||
|
||||
.mo-media-container {
|
||||
background-color: rgba(239, 239, 239, 0.9);
|
||||
box-sizing: border-box;
|
||||
min-width: 100%;
|
||||
max-height: 60vh;
|
||||
@@ -93,6 +119,13 @@ $header-inset: 10px;
|
||||
}
|
||||
|
||||
.mo-meta-container {
|
||||
background-color: rgba(239, 239, 239, 0.9);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
min-height: 100px;
|
||||
min-width: $panel-width;
|
||||
max-width: $panel-height;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
@@ -191,9 +224,8 @@ $header-inset: 10px;
|
||||
}
|
||||
|
||||
.source-image-container, .source-text-container {
|
||||
padding: 0 10em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: $padding;
|
||||
display: inline-block;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,10 @@
|
||||
stroke-dasharray: 1px 2px;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
/*transition: transform 0.2s ease;*/
|
||||
}
|
||||
|
||||
.event {
|
||||
cursor: pointer;
|
||||
opacity: .7;
|
||||
@@ -212,6 +216,7 @@
|
||||
stroke: $offwhite;
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5px 2px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.coevent {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -33,8 +34,7 @@ export const getTimeRange = state => state.app.filters.timerange
|
||||
*/
|
||||
function isTaggedIn(event, tagFilters) {
|
||||
if (event.tags) {
|
||||
const tagsInEvent = event.tags.split(",")
|
||||
const isTagged = tagsInEvent.some((tag) => {
|
||||
const isTagged = event.tags.some((tag) => {
|
||||
return tagFilters.find(tF => (tF.key === tag && tF.active))
|
||||
})
|
||||
return isTagged
|
||||
@@ -43,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
|
||||
*/
|
||||
@@ -54,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
|
||||
@@ -70,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
|
||||
}
|
||||
@@ -91,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 => {
|
||||
@@ -122,11 +145,6 @@ export const selectNarratives = createSelector(
|
||||
|
||||
steps.sort(compareTimestamp)
|
||||
|
||||
// steps.forEach((step, i) => {
|
||||
// narratives[key].byId[step.id].next = (i < steps.length - 2) ? steps[i + 1] : null
|
||||
// narratives[key].byId[step.id].prev = (i > 0) ? steps[i - 1] : null
|
||||
// })
|
||||
|
||||
if (narrativesMeta.find(n => n.id === key)) {
|
||||
narratives[key] = {
|
||||
...narrativesMeta.find(n => n.id === key),
|
||||
@@ -135,7 +153,9 @@ export const selectNarratives = createSelector(
|
||||
}
|
||||
})
|
||||
|
||||
return Object.values(narratives)
|
||||
// Return narratives in original order
|
||||
// + filter those that are undefined
|
||||
return narrativesMeta.map(n => narratives[n.id]).filter(d => d);
|
||||
})
|
||||
|
||||
/** Aggregate information about the narrative and the current step into
|
||||
@@ -230,7 +250,12 @@ export const selectSelected = createSelector(
|
||||
*/
|
||||
export const selectCategories = createSelector(
|
||||
[getCategories],
|
||||
(categories) => categories
|
||||
(categories) => {
|
||||
categories.map(cat => {
|
||||
cat.active = (!cat.hasOwnProperty('active')) ? false : cat.active
|
||||
});
|
||||
return categories;
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user