Skip to content

Commit

Permalink
Refactor topic highlighter container (#45)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
normangilmore authored Dec 29, 2016
1 parent 9df2fa3 commit 554905f
Show file tree
Hide file tree
Showing 23 changed files with 368 additions and 460 deletions.
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
],
"plugins": [
"add-module-exports",
"transform-class-properties",
"transform-decorators-legacy",
"react-hot-loader/babel"
],
Expand Down
39 changes: 8 additions & 31 deletions app/actions/article.js
Original file line number Diff line number Diff line change
@@ -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
};
}
93 changes: 93 additions & 0 deletions app/actions/highlightTasks.js
Original file line number Diff line number Diff line change
@@ -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})
)
};
}
24 changes: 5 additions & 19 deletions app/actions/project.js
Original file line number Diff line number Diff line change
@@ -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]
}
};
}
24 changes: 5 additions & 19 deletions app/actions/topicPicker.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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
}
};
}
66 changes: 0 additions & 66 deletions app/components/Article/index.js

This file was deleted.

67 changes: 67 additions & 0 deletions app/components/TopicHighlighter/index.js
Original file line number Diff line number Diff line change
@@ -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 (<div>DONE</div>)

let loadingClass = this.props.article.isFetching ? 'loading' : '';

return (
<ReactCSSTransitionsGroup transitionName='fadein'
transitionAppear
transitionAppearTimeout={500}
transitionEnterTimeout={500}
transitionLeaveTimeout={500}>
<div className={loadingClass}></div>
<div className='topic-picker-wrapper'>
<TopicPicker topics={this.props.topics} />
</div>
<div className='article-wrapper'>
<Project />
<ReactCSSTransitionsGroup transitionName='fade-between'
transitionAppear
transitionAppearTimeout={500}
transitionEnterTimeout={500}
transitionLeaveTimeout={500}>
<div className="article" key={this.props.article.articleId}>
<div className='article__header-text'>
</div>
<div id='article-container'>
<HighlightTool
text={this.props.article.text}
topics={this.props.topics.results}
colors={colors}
currentTopicId={this.props.currentTopicId}
/>
</div>
</div>
</ReactCSSTransitionsGroup>
<br/>
<button onClick={this.onSaveAndNext}>Save and Next</button>
<div className="space"></div>
</div>
</ReactCSSTransitionsGroup>
);
}
};
Original file line number Diff line number Diff line change
@@ -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;
}

}
Loading

0 comments on commit 554905f

Please sign in to comment.