From 554905f92ab9b804e3b84ae9bbdb427e33376e16 Mon Sep 17 00:00:00 2001 From: Norman Gilmore Date: Wed, 28 Dec 2016 16:35:35 -0800 Subject: [PATCH] Refactor topic highlighter container (#45) * Add babel plugin transform-class-properties to assist binding 'this' in React components. * PybossaHighlighter now saving highlights to Pybossa. * Restore old article reducer state to fix regression. Last commit with front-end calling old API. * Change front-end to use new paged highlighter_tasks2 endpoint. Code designed to precisely separate real Pybossa and mock Pybossa API calls. Remove old API fetches from front-end. * Remove Article component - it's only adding three divs. Merge into topicPicker. * Factor out real and mock API into container classes inheriting from TopicHighlighter. * HighlighterTasks container handles fetching, so TopicHighlighter is now a component. --- .babelrc | 1 + app/actions/article.js | 39 ++------ app/actions/highlightTasks.js | 93 +++++++++++++++++ app/actions/project.js | 24 +---- app/actions/topicPicker.js | 24 +---- app/components/Article/index.js | 66 ------------- app/components/TopicHighlighter/index.js | 67 +++++++++++++ .../{Article => TopicHighlighter}/styles.scss | 30 ++++-- app/containers/HighlighterTasks/index.js | 61 ++++++++++++ app/containers/PybossaHighlighter/index.js | 99 ------------------- app/containers/PybossaHighlighter/styles.scss | 21 ---- app/containers/TopicHighlighter/index.js | 93 ----------------- app/containers/TopicHighlighter/styles.scss | 21 ---- app/highlight.js | 4 +- app/pybossa/highlight.js | 62 ++++++------ app/reducers/article.js | 41 ++------ app/reducers/highlightTasks.js | 42 ++++++++ app/reducers/index.js | 4 +- app/reducers/project.js | 2 +- app/reducers/topicPicker.js | 20 ++-- app/routes.js | 6 +- package.json | 4 +- webpack/dev-server.js | 4 +- 23 files changed, 368 insertions(+), 460 deletions(-) create mode 100644 app/actions/highlightTasks.js delete mode 100644 app/components/Article/index.js create mode 100644 app/components/TopicHighlighter/index.js rename app/components/{Article => TopicHighlighter}/styles.scss (54%) create mode 100644 app/containers/HighlighterTasks/index.js delete mode 100644 app/containers/PybossaHighlighter/index.js delete mode 100644 app/containers/PybossaHighlighter/styles.scss delete mode 100644 app/containers/TopicHighlighter/index.js delete mode 100644 app/containers/TopicHighlighter/styles.scss create mode 100644 app/reducers/highlightTasks.js diff --git a/.babelrc b/.babelrc index f4f2b81..8c39600 100644 --- a/.babelrc +++ b/.babelrc @@ -6,6 +6,7 @@ ], "plugins": [ "add-module-exports", + "transform-class-properties", "transform-decorators-legacy", "react-hot-loader/babel" ], diff --git a/app/actions/article.js b/app/actions/article.js index b8077a1..32ffec7 100644 --- a/app/actions/article.js +++ b/app/actions/article.js @@ -1,36 +1,13 @@ -export function fetchArticle(articleId) { - return (dispatch) => { - dispatch({ type: 'FETCH_ARTICLE', articleId}); - let host = "http://localhost:5000"; - return fetch(host + `/api/articles/${articleId}/?format=json`) - .then(response => response.json()) - .then( - (response) => dispatch({ type: 'FETCH_ARTICLE_SUCCESS', response}), - (error) => dispatch({ type: 'FETCH_ARTICLE_FAIL', error}) - ); - }; -} - -export function postArticleHighlights(highlightsString, articleId) { - return (dispatch) => { - dispatch({ type: 'POST_HIGHLIGHTS'}); - - return fetch(`http://localhost:5000/api/postHighlights/${articleId}`, { - method: 'POST', - body: highlightsString - }) - .then(response => response.json()) - .then( - (response) => dispatch({ type: 'POST_HIGHLIGHTS_SUCCESS'}, response), - (error) => dispatch({ type: 'POST_HIGHLIGHTS_FAIL', error}) - ); +export function storeArticle(article) { + return { + type: 'FETCH_ARTICLE_SUCCESS', + response: article }; } -export function storeArticle(article) { - return (dispatch) => { - dispatch({ type: 'FETCH_ARTICLE_SUCCESS', - response: article - }); +export function storeSaveAndNext(saveAndNext) { + return { + type: 'POST_HIGHLIGHTS_CALLBACK', + saveAndNext: saveAndNext }; } diff --git a/app/actions/highlightTasks.js b/app/actions/highlightTasks.js new file mode 100644 index 0000000..197f2c9 --- /dev/null +++ b/app/actions/highlightTasks.js @@ -0,0 +1,93 @@ +import { storeProject } from 'actions/project'; +import { storeArticle, storeSaveAndNext } from 'actions/article'; +import { storeTopics } from 'actions/topicPicker'; + +import { normalize, Schema, arrayOf } from 'normalizr'; + +let taskSchema = new Schema('tasks'); +let taskList = { results: arrayOf(taskSchema) }; + +function storeTasks(dispatch, pagedTasks) { + // Until back-end returns task ids, copy article id as the task id + pagedTasks.results = pagedTasks.results.map( + (task) => ({...task, id: task.article.id}) + ); + // Normalize data structure and dispatch + const taskDatabase = normalize(pagedTasks, taskList); + // Replace or push task IDs to queue + dispatch({type: 'FETCH_HIGHLIGHT_TASKS_SUCCESS', + pagedTasks, + taskDatabase + }); + // Make a copy of the unique task Ids collected by normalize + let taskQueue = taskDatabase.result.results.slice(); + dispatch({type: 'UPDATE_HIGHLIGHT_TASK_QUEUE', taskQueue}); +} + +function postArticleHighlights(highlightsString, articleId) { + return (dispatch) => { + dispatch({ type: 'POST_HIGHLIGHTS'}); + + return fetch(`http://localhost:5000/api/postHighlights/${articleId}`, { + method: 'POST', + body: highlightsString + }) + .then(response => response.json()) + .then( + (response) => dispatch({ type: 'POST_HIGHLIGHTS_SUCCESS'}, response), + (error) => dispatch({ type: 'POST_HIGHLIGHTS_FAIL', error}) + ); + }; +} + +function presentTask(dispatch, getState) { + const taskReducer = getState().highlightTasks; + const taskDB = taskReducer.taskDatabase.entities.tasks; + let taskQueue = taskReducer.taskQueue.slice(); + if (taskQueue.length > 0) { + const taskId = taskQueue.shift(); + const task = taskDB[taskId]; + dispatch(storeProject(task.project)); + dispatch(storeArticle(task.article)); + dispatch(storeTopics(task.topics)); + // Dispatch an action to clear any existing highlights + // dispatch(XXXX); + dispatch({type: 'UPDATE_HIGHLIGHT_TASK_QUEUE', taskQueue}); + dispatch({type: 'CURRENT_HIGHLIGHT_TASK', taskId}); + + function onSaveAndNext(highlights) { + // dispatch save highight action which will return a promise, so + // promise.then( call this ) to load next task + // or better, deep copy highlights and don't wait to show next task + presentTask(dispatch, getState); + } + // Tricky part: We have loaded the task, now we also provide the + // callback that the U button can use to save the data and trigger + // loading the next task. + dispatch(storeSaveAndNext(onSaveAndNext)); + } else { + // TODO: update store with done flag and show nicely in UI + console.log('No more tasks.'); + throw new Error('No more tasks.'); + } +} + +export function runNextTask() { + return (dispatch, getState) => presentTask(dispatch, getState); +} + +export function fetchHighlightTasks(pageParam) { + return (dispatch, getState) => { + dispatch({type: 'FETCH_HIGHLIGHT_TASKS'}); + let host = "http://localhost:5000"; + let pageParam = pageParam ? pageParam : ''; + return fetch(host + `/api/highlighter_tasks2/?format=json${pageParam}`) + .then(response => response.json()) + .then(pagedTasks => { + storeTasks(dispatch, pagedTasks); + presentTask(dispatch, getState); + }, + error => dispatch({type: 'FETCH_HIGHLIGHT_TASKS_FAIL', error}) + ) + }; +} diff --git a/app/actions/project.js b/app/actions/project.js index 80a71de..1761ca6 100644 --- a/app/actions/project.js +++ b/app/actions/project.js @@ -1,22 +1,8 @@ -export function fetchProject() { - return (dispatch) => { - dispatch({ type: 'FETCH_PROJECT'}); - - return fetch(`http://localhost:5000/api/projects/?format=json`) - .then(response => response.json()) - .then( - (response) => dispatch({ type: 'FETCH_PROJECT_SUCCESS', response}), - (error) => dispatch({ type: 'FETCH_PROJECT_FAIL', error}) - ); - }; -} - export function storeProject(project) { - return (dispatch) => { - dispatch({ type: 'FETCH_PROJECT_SUCCESS', - response: { - results: [project] - } - }); + return { + type: 'FETCH_PROJECT_SUCCESS', + response: { + results: [project] } + }; } diff --git a/app/actions/topicPicker.js b/app/actions/topicPicker.js index 1174929..d5c71b9 100644 --- a/app/actions/topicPicker.js +++ b/app/actions/topicPicker.js @@ -1,16 +1,3 @@ -export function fetchTopics() { - return (dispatch) => { - dispatch({ type: 'FETCH_TOPICS'}); - - return fetch(`http://localhost:5000/api/topics/?format=json`) - .then(response => response.json()) - .then( - (response) => dispatch({ type: 'FETCH_TOPICS_SUCCESS', response}), - (error) => dispatch({ type: 'FETCH_TOPICS_FAIL', error}) - ); - }; -} - export function activateTopic(topicId) { return { type: 'ACTIVATE_TOPIC', @@ -19,11 +6,10 @@ export function activateTopic(topicId) { } export function storeTopics(topics) { - return (dispatch) => { - dispatch({ type: 'FETCH_TOPICS_SUCCESS', - response: { - results: topics - } - }); + return { + type: 'FETCH_TOPICS_SUCCESS', + response: { + results: topics + } }; } diff --git a/app/components/Article/index.js b/app/components/Article/index.js deleted file mode 100644 index 40dc63f..0000000 --- a/app/components/Article/index.js +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { addHighlight } from 'actions/article'; -import { connect } from 'react-redux'; - -import HighlightTool from 'components/HighlightTool'; -import { styles } from './styles.scss'; -import { colors } from 'utils/colors'; - -const mapDispatchToProps = dispatch => { - return { - onHighlight: (start, end, selectedText) => { - dispatch(addHighlight(start, end, selectedText)); - } - }; -} - -const mapStateToProps = state => { - return { - }; -} - -const Article = React.createClass({ - displayName: 'Article', - - propTypes: { - article: React.PropTypes.object.isRequired, - topics: React.PropTypes.object.isRequired, - currentTopicId: React.PropTypes.number.isRequired, - postArticleHighlights: React.PropTypes.func.isRequired - }, - - componentDidMount: function() { - }, - - serializeHighlights: function() { - let highlightsString = this.annotationsObject.serializeHighlights(); - this.props.postArticleHighlights(highlightsString, this.props.article.article_id); - }, - - render() { - let article = this.props.article; - let text = article.text; - - let fetchingClass = this.props.article.isFetching ? 'is-fetching' : ''; - return ( -
-
-
-
this.articleRef = ref} id='article-container'> - -
- -
- ); - } -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(Article); diff --git a/app/components/TopicHighlighter/index.js b/app/components/TopicHighlighter/index.js new file mode 100644 index 0000000..96c063a --- /dev/null +++ b/app/components/TopicHighlighter/index.js @@ -0,0 +1,67 @@ +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import ReactCSSTransitionsGroup from 'react-addons-css-transition-group'; + +import { colors } from 'utils/colors'; +import HighlightTool from 'components/HighlightTool'; +import TopicPicker from 'components/TopicPicker'; +import Project from 'components/Project'; + +import { styles } from './styles.scss'; + +export class TopicHighlighter extends Component { + constructor(props) { + super(props); + } + + // Babel plugin transform-class-properties allows us to use + // ES2016 property initializer syntax. So the arrow function + // will bind 'this' of the class. (React.createClass does automatically.) + onSaveAndNext = () => { + this.props.saveAndNext(this.props.highlights); + } + + render() { + // TODO: Detect if done + // return (
DONE
) + + let loadingClass = this.props.article.isFetching ? 'loading' : ''; + + return ( + +
+
+ +
+
+ + +
+
+
+
+ +
+
+
+
+ +
+
+
+ ); + } +}; diff --git a/app/components/Article/styles.scss b/app/components/TopicHighlighter/styles.scss similarity index 54% rename from app/components/Article/styles.scss rename to app/components/TopicHighlighter/styles.scss index cc28fb2..89c6edc 100644 --- a/app/components/Article/styles.scss +++ b/app/components/TopicHighlighter/styles.scss @@ -1,25 +1,41 @@ @import 'app/styles/_mixins.scss'; @import 'app/styles/_variables.scss'; +.topic-picker-wrapper { + padding-left: 0 !important; +} + +.article-wrapper { + margin: 2em 1em 2em 275px; +} + +.loading { + width: 100%; + height: 100%; + z-index: 100; + top: 0px; + left: 0px; + position: fixed; + background-color: rgba(30, 30, 30, 0.3); +} + +.space { + height: 100px; +} + .article { @include newspaper; transition: opacity $transition-slow; margin-bottom: 10px; line-height: 2.2; - &.is-fetching { - border: 2px dotted black; //TODO: REMOVE THESE TEMP STYLES - opacity: 0.5; - } - .article__header-text { padding: 10px; margin-bottom: 10px; } - + #article-container { padding: 1em; border: 1px solid black; } - } diff --git a/app/containers/HighlighterTasks/index.js b/app/containers/HighlighterTasks/index.js new file mode 100644 index 0000000..944ecb7 --- /dev/null +++ b/app/containers/HighlighterTasks/index.js @@ -0,0 +1,61 @@ +import { TopicHighlighter } from 'components/TopicHighlighter'; +import { bindActionCreators } from 'redux'; +import { connect } from 'react-redux'; + +import * as articleActionCreators from 'actions/article'; +import * as topicsActionCreators from 'actions/topicPicker'; +import * as projectActionCreators from 'actions/project'; + +// Actions for MockTopicHighlighter +import * as taskActionCreators from 'actions/highlightTasks'; +// API for RealTopicHighlighter +import runPybossaTasks from 'pybossa/highlight'; + +const assembledActionCreators = Object.assign( + {}, + articleActionCreators, + topicsActionCreators, + projectActionCreators, + taskActionCreators +); + +const mapStateToProps = state => { + return { + article: state.article.article, + saveAndNext: state.article.saveAndNext, + currentTopicId: state.topicPicker.currentTopicId, + topics: state.topicPicker.topics, + highlights: state.highlight.highlights + }; +} + +@connect ( + mapStateToProps, + dispatch => bindActionCreators(assembledActionCreators, dispatch) +) +export class MockHighlighter extends TopicHighlighter { + constructor(props) { + super(props); + } + + componentDidMount() { + this.props.fetchHighlightTasks(); + } +}; + +@connect ( + mapStateToProps, + dispatch => bindActionCreators(assembledActionCreators, dispatch) +) +export class RealHighlighter extends TopicHighlighter { + constructor(props) { + super(props); + } + + componentDidMount() { + runPybossaTasks(this.props.storeArticle, + this.props.storeProject, + this.props.storeTopics, + this.props.storeSaveAndNext); + } +}; diff --git a/app/containers/PybossaHighlighter/index.js b/app/containers/PybossaHighlighter/index.js deleted file mode 100644 index 996b078..0000000 --- a/app/containers/PybossaHighlighter/index.js +++ /dev/null @@ -1,99 +0,0 @@ -import React, {Component} from 'react'; -import { Link } from 'react-router'; -import { bindActionCreators } from 'redux'; -import ReactDOM from 'react-dom'; -import ReactCSSTransitionsGroup from 'react-addons-css-transition-group'; -import { connect } from 'react-redux'; - -import * as articleActionCreators from 'actions/article'; -import * as topicsActionCreators from 'actions/topicPicker'; -import * as projectActionCreators from 'actions/project'; - -const assembledActionCreators = Object.assign({}, articleActionCreators, topicsActionCreators, projectActionCreators) - -import Article from 'components/Article'; -import TopicPicker from 'components/TopicPicker'; -import Project from 'components/Project'; - -import pybossaHighlight from 'pybossa/highlight'; - -import { styles } from './styles.scss'; - -const mapStateToProps = state => { - return { - article: state.article.article, - currentArticle: state.article.currentArticle, - currentTopicId: state.topicPicker.currentTopicId, - nextArticle: state.article.nextArticle, - topics: state.topicPicker.topics - }; -} - -@connect ( - mapStateToProps, - dispatch => bindActionCreators(assembledActionCreators, dispatch) -) - -export class TopicHighlighter extends Component { - constructor(props) { - super(props); - this.state = { onSaveAndNext: () => {} }; - } - - saveAndNext() { - } - - componentDidMount() { - let onSaveAndNext = pybossaHighlight(this.props.storeArticle, - this.props.storeProject, - this.props.storeTopics); - this.setState({onSaveAndNext: onSaveAndNext}); - } - - componentWillReceiveProps(nextProps) { -// if (this.props.currentArticle != nextProps.routeParams.articleId) { -// } - } - - render() { - let current_article = this.props.currentArticle; - let article = this.props.article; -// if (this.props.nextArticle == undefined) { -// return (
DONE
) // TODO: Clean this up. -// } - - let loadingClass = this.props.article.isFetching ? 'loading' : ''; - - return ( - -
-
- -
-
- - - {
} - -
- -
-
-
- ); - } -}; diff --git a/app/containers/PybossaHighlighter/styles.scss b/app/containers/PybossaHighlighter/styles.scss deleted file mode 100644 index 146f227..0000000 --- a/app/containers/PybossaHighlighter/styles.scss +++ /dev/null @@ -1,21 +0,0 @@ -.topic-picker-wrapper { - padding-left: 0 !important; -} - -.article-wrapper { - margin: 2em 1em 2em 275px; -} - -.loading { - width: 100%; - height: 100%; - z-index: 100; - top: 0px; - left: 0px; - position: fixed; - background-color: rgba(30, 30, 30, 0.3); -} - -.space { - height: 100px; -} diff --git a/app/containers/TopicHighlighter/index.js b/app/containers/TopicHighlighter/index.js deleted file mode 100644 index 2395cad..0000000 --- a/app/containers/TopicHighlighter/index.js +++ /dev/null @@ -1,93 +0,0 @@ -import React, {Component} from 'react'; -import { Link } from 'react-router'; -import { bindActionCreators } from 'redux'; -import ReactDOM from 'react-dom'; -import ReactCSSTransitionsGroup from 'react-addons-css-transition-group'; -import { connect } from 'react-redux'; - -import * as articleActionCreators from 'actions/article'; -import * as topicsActionCreators from 'actions/topicPicker'; -import * as projectActionCreators from 'actions/project'; - -const assembledActionCreators = Object.assign({}, articleActionCreators, topicsActionCreators, projectActionCreators) - -import Article from 'components/Article'; -import TopicPicker from 'components/TopicPicker'; -import Project from 'components/Project'; - -import { styles } from './styles.scss'; - -const mapStateToProps = state => { - return { - article: state.article.article, - currentArticle: state.article.currentArticle, - currentTopicId: state.topicPicker.currentTopicId, - nextArticle: state.article.nextArticle, - topics: state.topicPicker.topics - }; -} - -@connect ( - mapStateToProps, - dispatch => bindActionCreators(assembledActionCreators, dispatch) -) - -export class TopicHighlighter extends Component { - constructor(props) { - super(props); - } - - componentDidMount() { - this.props.fetchArticle(this.props.routeParams.articleId); - this.props.fetchTopics(); - this.props.fetchProject(); - } - - componentWillReceiveProps(nextProps) { - if (this.props.currentArticle != nextProps.routeParams.articleId && !nextProps.article.isFetching){ - this.props.fetchArticle(nextProps.routeParams.articleId); - } - } - - render() { - let current_article = this.props.currentArticle; - let article = this.props.article; - if (this.props.nextArticle == undefined) { - return (
DONE
) // TODO: Clean this up. - } - - let loadingClass = this.props.article.isFetching ? 'loading' : ''; - - return ( - -
-
- -
-
- - - {
} - -
- -
-
-
- ); - } -}; diff --git a/app/containers/TopicHighlighter/styles.scss b/app/containers/TopicHighlighter/styles.scss deleted file mode 100644 index 146f227..0000000 --- a/app/containers/TopicHighlighter/styles.scss +++ /dev/null @@ -1,21 +0,0 @@ -.topic-picker-wrapper { - padding-left: 0 !important; -} - -.article-wrapper { - margin: 2em 1em 2em 275px; -} - -.loading { - width: 100%; - height: 100%; - z-index: 100; - top: 0px; - left: 0px; - position: fixed; - background-color: rgba(30, 30, 30, 0.3); -} - -.space { - height: 100px; -} diff --git a/app/highlight.js b/app/highlight.js index 431dd75..baffe6a 100755 --- a/app/highlight.js +++ b/app/highlight.js @@ -4,7 +4,7 @@ import React from 'react'; import { render } from 'react-dom'; import App from 'containers/App'; -import { TopicHighlighter } from 'containers/PybossaHighlighter'; +import { RealHighlighter } from 'containers/HighlighterTasks'; let elem = document.createElement('div'); elem.id = ('react-root'); @@ -12,7 +12,7 @@ document.body.appendChild(elem); render( - + , document.getElementById('react-root') ); diff --git a/app/pybossa/highlight.js b/app/pybossa/highlight.js index b16f127..72bcb4a 100644 --- a/app/pybossa/highlight.js +++ b/app/pybossa/highlight.js @@ -1,18 +1,31 @@ 'use strict'; -export default function pybossaHighlight(storeArticle, storeProject, storeTopics) { - function getProjectName() { - // Assuming an URL like this: - // http://crowdcrafting.org/project/TextThresherHighlighter/task/1532993 - var urlpath = window.location.pathname; - var elements = urlpath.split('/'); - if (elements.length >= 3 && elements[1] === 'project') { - return elements[2]; - } else { - return 'TextThresherHighlighter'; - } +// PybossaJS docs at http://pybossajs.readthedocs.io/en/latest/library.html + +// Key thing to understand is that runPybossaTasks is only run once +// per task queue, NOT once per task. +// However, runPybossaTask configures callback functions that run once +// per task. +// So functions provided to loadUserProgress, taskLoaded and presentTask +// retain access to the redux actions they need to update application +// state as part of their closure. + +function getProjectName() { + // Assuming an URL like this: + // http://crowdcrafting.org/project/TextThresherHighlighter/task/1532993 + var urlpath = window.location.pathname; + var elements = urlpath.split('/'); + if (elements.length >= 3 && elements[1] === 'project') { + return elements[2]; + } else { + return 'TextThresherHighlighter'; } +} +export default function runPybossaTasks(storeArticle, + storeProject, + storeTopics, + storeSave) { function loadUserProgress() { pybossa.userProgress(getProjectName()).done(function(data){ // Dispatch this info to the redux store for display @@ -39,34 +52,23 @@ export default function pybossaHighlight(storeArticle, storeProject, storeTopics storeTopics(task.info.topics); storeArticle(task.info.article); - var onSaveAndNext = function () { - // obtain answer from redux store - var highlights = [ - { articleHighlight: task.article.id, - case_number: 1, - offsets: [[10,18],[30,38]], - highlightText: ["New York", "Brooklyn"] - }, - { articleHighlight: task.article.id, - case_number: 2, - offsets: [[60,68],[70,78]], - highlightText: ["Bay Area", "East Bay"] - } - ]; + function onSaveAndNext(highlights) { pybossa.saveTask(task.id, highlights).done(function() { deferred.resolve(task); }); }; + // This is the tricky part. Each time we load a new task into + // the store, we also provide this callback that the UI button + // can use to call the function above to save the data and trigger + // loading the next task with the deferred.resolve(task) call. + storeSave(onSaveAndNext); } else { // Displatch to store saying we are done with tasks // storeTasksDone() #TODO - // Nothing more to fetch - return () => {}; + storeSave(null); } }); + // pybossa.setEndpoint('http://server/pybossa'); pybossa.run(getProjectName()); - - // TODO: return saveAndNext action - return () => {}; } diff --git a/app/reducers/article.js b/app/reducers/article.js index 72ebeb8..c056a2b 100644 --- a/app/reducers/article.js +++ b/app/reducers/article.js @@ -1,49 +1,22 @@ -import { getIntOfLength } from 'utils/math'; - -// #TODO: create endpoint to return these indices on the back-end -const ARTICLE_INDEX_ARRAY = [9, 11, 38, 53, 55, 202, 209, 236, 259]; - -function getNextArticle(articleId) { - ARTICLE_INDEX_ARRAY.splice(ARTICLE_INDEX_ARRAY.indexOf(articleId), 1); - return ARTICLE_INDEX_ARRAY[getIntOfLength(ARTICLE_INDEX_ARRAY)]; -} - -function getInitialState() { - return { +const initialState = { article: { text: "" }, - currentArticle: null, - nextArticle: null - }; -} - -const initialState = Object.assign({ - article: {}, - articleIndex: [], -}, getInitialState()); - + saveAndNext: null +}; export function article(state = initialState, action) { switch (action.type) { - case 'FETCH_ARTICLE': - let nextArticleIndex = getNextArticle(Number(action.articleId)); + case 'FETCH_ARTICLE_SUCCESS': return { ...state, - article: { - isFetching: true, - text: "" - }, - currentArticle: Number(action.articleId), - nextArticle: nextArticleIndex + article: action.response } - case 'FETCH_ARTICLE_SUCCESS': + case 'POST_HIGHLIGHTS_CALLBACK': return { ...state, - article: action.response + saveAndNext: action.saveAndNext } - case 'POST_HIGHLIGHTS': - return state default: return state; } diff --git a/app/reducers/highlightTasks.js b/app/reducers/highlightTasks.js new file mode 100644 index 0000000..2238a28 --- /dev/null +++ b/app/reducers/highlightTasks.js @@ -0,0 +1,42 @@ +const initialState = Object.assign({ + pagedTasks: {}, + taskDatabase: {}, + taskQueue: [], + currentTaskId: undefined +}, {}); + +export function highlightTasks(state = initialState, action) { + switch (action.type) { + case 'FETCH_HIGHLIGHT_TASKS': + return { + ...state, + pagedTasks: { + isFetching: true + }, + taskQueue: [], + currentTaskId: undefined + } + case 'FETCH_HIGHLIGHT_TASKS_SUCCESS': + return { + ...state, + pagedTasks: action.pagedTasks, + taskDatabase: action.taskDatabase, + currentTaskId: undefined + } + case 'UPDATE_HIGHLIGHT_TASK_QUEUE': + return { + ...state, + taskQueue: action.taskQueue.slice() + } + case 'CURRENT_HIGHLIGHT_TASK': + return { + ...state, + currentTaskId: action.taskId + } + case 'FETCH_HIGHLIGHT_TASKS_FAIL': + // TODO: Put a helpful error message in the UI. + throw action.error; + default: + return state; + } +} diff --git a/app/reducers/index.js b/app/reducers/index.js index c2ff347..39fccd3 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -4,13 +4,15 @@ import { topicPicker } from './topicPicker'; import { quiz } from './quiz'; import { project } from './project.js'; import { highlight } from './highlight'; +import { highlightTasks } from './highlightTasks'; const rootReducer = combineReducers({ article, topicPicker, quiz, project, - highlight + highlight, + highlightTasks }); export default rootReducer; diff --git a/app/reducers/project.js b/app/reducers/project.js index e68a8b0..fc22d61 100644 --- a/app/reducers/project.js +++ b/app/reducers/project.js @@ -16,4 +16,4 @@ export function project(state = initialState, action) { default: return state; } -} \ No newline at end of file +} diff --git a/app/reducers/topicPicker.js b/app/reducers/topicPicker.js index e80ab9e..915d9e2 100644 --- a/app/reducers/topicPicker.js +++ b/app/reducers/topicPicker.js @@ -17,17 +17,17 @@ function indexTopicById(topicList) { export function topicPicker(state = initialState, action) { switch (action.type) { case 'ACTIVATE_TOPIC': - return { - ...state, - currentTopicId: action.currentTopicId, - } + return { + ...state, + currentTopicId: action.currentTopicId, + } case 'FETCH_TOPICS_SUCCESS': - return { - ...state, - topics: action.response, - currentTopicId: (action.response.results.length > 0 ? action.response.results[0].id : 0), - lookupTopicById: indexTopicById(action.response.results) - } + return { + ...state, + topics: action.response, + currentTopicId: (action.response.results.length > 0 ? action.response.results[0].id : 0), + lookupTopicById: indexTopicById(action.response.results) + } default: return state; } diff --git a/app/routes.js b/app/routes.js index d0475c9..3912afd 100644 --- a/app/routes.js +++ b/app/routes.js @@ -2,12 +2,12 @@ import React from 'react'; import { Route } from 'react-router'; import App from 'containers/App'; -import { TopicHighlighter } from 'containers/TopicHighlighter'; +import { MockHighlighter } from 'containers/HighlighterTasks'; import {Quiz} from 'containers/Quiz'; export default ( - - + + ); diff --git a/package.json b/package.json index c19c0d2..6867c06 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "babel-eslint": "^7.1.0", "babel-loader": "^6.2.1", "babel-plugin-add-module-exports": "^0.2.1", + "babel-plugin-transform-class-properties": "^6.19.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.4.3", "babel-polyfill": "^6.6.1", @@ -49,7 +50,7 @@ "eslint-config-airbnb": "^13.0.0", "eslint-loader": "^1.6.1", "eslint-plugin-import": "^2.2.0", - "eslint-plugin-jsx-a11y": "^3.0.1", + "eslint-plugin-jsx-a11y": "^2.2.3", "eslint-plugin-react": "^6.7.1", "estraverse-fb": "^1.3.1", "exports-loader": "^0.6.2", @@ -64,6 +65,7 @@ "mocha-webpack": "^0.7.0", "moment": "^2.15.2", "node-sass": "^3.0.0-beta.7", + "normalizr": "^2.3.1", "on-build-webpack": "^0.1.0", "postcss-loader": "^1.1.1", "radium": "^0.18.1", diff --git a/webpack/dev-server.js b/webpack/dev-server.js index dff190d..9d271ac 100644 --- a/webpack/dev-server.js +++ b/webpack/dev-server.js @@ -25,7 +25,7 @@ var devServer = new WebpackDevServer(compiler, config.devServer); devServer.listen(config.devServer.port, config.devServer.host, function () { console.log('server available at:'.underline.red); - console.log(`${config.devServer.publicPath}#/article/0`.underline.yellow); - console.log(`${config.devServer.publicPath}#/quiz/0`.underline.green); + console.log(`${config.devServer.publicPath}#/highlighter`.underline.yellow); + console.log(`${config.devServer.publicPath}#/quiz`.underline.green); });