├── api
├── models
│ ├── __init__.py
│ └── task.py
├── routes
│ ├── __init__.py
│ └── task.py
└── __init__.py
├── .jshintrc
├── run.py
├── .gitignore
├── app
├── public
│ ├── js
│ │ ├── dispatcher
│ │ │ └── AppDispatcher.js
│ │ ├── contants
│ │ │ └── TaskConstants.js
│ │ ├── components
│ │ │ ├── BaseComponent.js
│ │ │ ├── Header.js
│ │ │ ├── DraggedItem.js
│ │ │ ├── Footer.js
│ │ │ ├── NewTask.js
│ │ │ ├── TodoApp.js
│ │ │ ├── TaskListItem.js
│ │ │ └── TaskList.js
│ │ ├── app.js
│ │ ├── utils
│ │ │ └── TaskAPI.js
│ │ ├── actions
│ │ │ └── TaskActions.js
│ │ └── stores
│ │ │ └── TaskStore.js
│ ├── images
│ │ └── drag_icon.png
│ └── css
│ │ └── main.css
├── __mocks__
│ └── superagent.js
├── support
│ └── preprocessor.js
├── views
│ └── index.ejs
├── package.json
├── server.js
└── __tests__
│ ├── footer-test.js
│ ├── newTask-test.js
│ ├── taskActions-test.js
│ ├── taskStore-test.js
│ ├── taskListItem-test.js
│ └── taskList-test.js
├── requirements.txt
└── README.md
/api/models/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/api/routes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true
3 | }
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | from api import app
2 | app.run(debug=True)
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | __pycache__
3 | node_modules
4 |
5 | bundle.js
6 | bundle.min.js
7 |
--------------------------------------------------------------------------------
/app/public/js/dispatcher/AppDispatcher.js:
--------------------------------------------------------------------------------
1 | import {Dispatcher} from 'flux';
2 |
3 | export default new Dispatcher();
--------------------------------------------------------------------------------
/app/__mocks__/superagent.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | get: jest.genMockFunction(),
3 | post: jest.genMockFunction()
4 | };
--------------------------------------------------------------------------------
/app/public/images/drag_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nilpath/flask-react-todo/HEAD/app/public/images/drag_icon.png
--------------------------------------------------------------------------------
/app/support/preprocessor.js:
--------------------------------------------------------------------------------
1 | var ReactTools = require('react-tools');
2 |
3 | module.exports = {
4 | process: function(src) {
5 | return ReactTools.transform(src);
6 | }
7 | };
--------------------------------------------------------------------------------
/api/models/task.py:
--------------------------------------------------------------------------------
1 | from api import db
2 |
3 | class Task(db.Document):
4 |
5 | description = db.StringField()
6 | order = db.IntField()
7 | done = db.BoolField(default=False)
8 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==0.10.1
2 | Flask-Cors==2.0.1
3 | Flask-MongoAlchemy==0.7.1
4 | itsdangerous==0.24
5 | Jinja2==2.7.3
6 | MarkupSafe==0.23
7 | MongoAlchemy==0.19
8 | pymongo==2.8.1
9 | six==1.9.0
10 | Werkzeug==0.10.4
11 | wheel==0.24.0
12 |
--------------------------------------------------------------------------------
/app/public/js/contants/TaskConstants.js:
--------------------------------------------------------------------------------
1 | import keyMirror from 'keymirror';
2 |
3 | export default keyMirror({
4 | SET_TODOS: null,
5 | CREATE_TODO: null,
6 | SAVE_TODO: null,
7 | SAVE_TODOS: null,
8 | ADD_TODO: null,
9 | REORDER: null,
10 | });
--------------------------------------------------------------------------------
/app/public/js/components/BaseComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class BaseComponent extends React.Component {
4 |
5 | constructor(props) {
6 | super(props);
7 | }
8 |
9 | _bind(methods) {
10 | methods.forEach( method => this[method] = this[method].bind(this) );
11 | };
12 |
13 | }
--------------------------------------------------------------------------------
/app/public/js/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseComponent from './BaseComponent.js';
3 |
4 | export default class Header extends BaseComponent {
5 |
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render() {
11 | return (
12 |
15 | );
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app/public/js/app.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TaskAPI from './utils/TaskAPI.js';
3 | import TaskStore from './stores/TaskStore.js';
4 | import TodoApp from './components/TodoApp.js';
5 |
6 | window.onload = () => {
7 | let tasks = document.getElementById('state').innerHTML;
8 |
9 | React.render(
10 | ,
11 | document.getElementById('app')
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/app/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 | Todo
4 |
5 |
6 |
7 |
8 |
9 | <%-app-%>
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #Flask-React-Todo
2 |
3 | ##Getting Started
4 |
5 | This project is divided into two parts. A python app, built using Flask and MongoDB, which contains the API. The second part is a node app that is serving the web app built in React.
6 |
7 | ### API
8 | To start the API server you'll need to install the required dependencies using ```pip install > requirements.txt``` by standing in the project root.
9 |
10 | After the dependencies have been installed. Run ```python run.py``` to start the server. It runs on port 5000
11 |
12 | ### Web App
13 | To start the Node App ```cd app/``` and run ```npm install``` to install the required dependences.
14 |
15 | After that can use ```npm start``` to start the server on port 8001 and start waching the repository for changes to the React App. You will also be able to run the tests by running ```npm test```.
16 |
17 |
--------------------------------------------------------------------------------
/app/public/js/utils/TaskAPI.js:
--------------------------------------------------------------------------------
1 | import request from 'superagent';
2 | import TaskActions from '../actions/TaskActions.js';
3 |
4 | const BASE_API_URL = 'http://localhost:5000/api/tasks';
5 |
6 | export default {
7 |
8 | fetchTasks(callback) {
9 | request
10 | .get(BASE_API_URL)
11 | .end((err, res) => {
12 | if(!err && callback) {
13 | callback(res.body);
14 | }
15 | });
16 | },
17 |
18 | updateTask(task, callback) {
19 | let url = `${BASE_API_URL}/${task._id}`;
20 | request
21 | .put(url)
22 | .send(task)
23 | .end((err, res) => {
24 | if(err) {
25 | console.log(err);
26 | }
27 | });
28 | },
29 |
30 | createTask(newTask, callback) {
31 | request
32 | .post(BASE_API_URL)
33 | .send(newTask)
34 | .end((err, res) => {
35 | if(callback) {
36 | callback(err, res);
37 | }
38 | });
39 | }
40 |
41 | };
--------------------------------------------------------------------------------
/app/public/js/components/DraggedItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseComponent from './BaseComponent.js';
3 | import classNames from 'classnames';
4 |
5 | const ReactPropTypes = React.PropTypes;
6 |
7 | export default class DraggedItem extends BaseComponent {
8 |
9 | constructor(props) {
10 | super(props);
11 | }
12 |
13 | render() {
14 | let classes = classNames({
15 | 'todo-list__item': true,
16 | 'todo-list__item--dragging': true,
17 | 'todo-list__item--done': this.props.task.done
18 | });
19 |
20 | let styles = {
21 | position: 'absolute',
22 | top: this.props.draggingInfo.top,
23 | left: this.props.draggingInfo.left
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | DraggedItem.PropTypes = {
36 | task: ReactPropTypes.object.isRequired
37 | };
--------------------------------------------------------------------------------
/app/public/js/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseComponent from './BaseComponent.js';
3 | import TaskActions from '../actions/TaskActions.js';
4 |
5 | const ReactPropTypes = React.PropTypes;
6 |
7 | export default class Footer extends BaseComponent {
8 |
9 | constructor(props) {
10 | super(props);
11 | this._bind(['_completeAll']);
12 | }
13 |
14 | render() {
15 | let count = this.props.remainingItems.length;
16 | let plural = count === 1 ? '' : 's';
17 | let remainingText = `${count} item${plural} left`;
18 |
19 | return (
20 |
26 | );
27 |
28 | }
29 |
30 | _completeAll(event) {
31 | event.preventDefault();
32 | TaskActions.completeAll(this.props.tasks);
33 | }
34 |
35 | }
36 |
37 | Footer.propTypes = {
38 | tasks: ReactPropTypes.array.isRequired,
39 | remainingItems: ReactPropTypes.array.isRequired
40 | };
--------------------------------------------------------------------------------
/api/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, jsonify, make_response
2 | from flask.ext.mongoalchemy import MongoAlchemy
3 | from flask.ext.cors import CORS
4 | import datetime
5 | import json
6 | from bson.objectid import ObjectId
7 | from werkzeug import Response
8 |
9 | app = Flask(__name__)
10 | app.config['MONGOALCHEMY_DATABASE'] = 'flask-react-todo'
11 | db = MongoAlchemy(app)
12 | cors = CORS(app)
13 |
14 | class MongoJsonEncoder(json.JSONEncoder):
15 | def default(self, obj):
16 | if isinstance(obj, (datetime.datetime, datetime.date)):
17 | return obj.isoformat()
18 | elif isinstance(obj, ObjectId):
19 | return str(obj)
20 | return json.JSONEncoder.default(self, obj)
21 |
22 | def jsonify(*args, **kwargs):
23 | return Response(json.dumps(*args, cls=MongoJsonEncoder), mimetype='application/json', **kwargs)
24 |
25 |
26 | @app.errorhandler(404)
27 | def not_found(error):
28 | return make_response(jsonify({'error': 'Not found'}), 404)
29 |
30 | @app.errorhandler(400)
31 | def bad_request(error):
32 | return make_response(jsonify({'error': 'Bad request'}), 400)
33 |
34 | @app.route('/')
35 | def index():
36 | return "Hello, World!"
37 |
38 | from api.routes import task
--------------------------------------------------------------------------------
/app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flask-react-todo-app",
3 | "version": "0.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "./node_modules/.bin/babel-node server.js",
8 | "build": "watchify -o public/js/bundle.js -v -d public/js/app.js",
9 | "test": "jest"
10 | },
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "babel": "^5.5.8",
15 | "body-parser": "^1.12.4",
16 | "classnames": "^2.1.2",
17 | "ejs": "^2.3.1",
18 | "express": "^4.12.4",
19 | "flux": "^2.0.3",
20 | "keymirror": "^0.1.1",
21 | "node-jsx": "^0.13.3",
22 | "object-assign": "^2.0.0",
23 | "path": "^0.11.14",
24 | "react": "^0.13.3",
25 | "superagent": "^1.2.0"
26 | },
27 | "devDependencies": {
28 | "babel-jest": "^5.3.0",
29 | "babelify": "^6.1.2",
30 | "browserify": "^10.2.3",
31 | "jasmine-react": "^1.1.0",
32 | "jest-cli": "^0.4.7",
33 | "reactify": "^1.1.1",
34 | "uglify-js": "^2.4.23",
35 | "watchify": "^3.2.1"
36 | },
37 | "browserify": {
38 | "transform": [
39 | "babelify",
40 | "reactify"
41 | ]
42 | },
43 | "jest": {
44 | "scriptPreprocessor": "/node_modules/babel-jest",
45 | "unmockedModulePathPatterns": [
46 | "/node_modules/react"
47 | ]
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/public/js/components/NewTask.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseComponent from './BaseComponent.js';
3 | import TaskActions from '../actions/TaskActions.js';
4 |
5 | export default class NewTask extends BaseComponent {
6 |
7 | constructor(props) {
8 | super(props);
9 | this._bind([
10 | '_defaultState',
11 | '_handleChange',
12 | '_addTask'
13 | ]);
14 |
15 | this.state = this._defaultState();
16 | }
17 |
18 | render() {
19 | let description = this.state.description;
20 |
21 | return (
22 |
33 | );
34 | }
35 |
36 | _defaultState() {
37 | return {
38 | description: ''
39 | };
40 | }
41 |
42 | _handleChange(event) {
43 | this.setState({description: event.target.value});
44 | }
45 |
46 | _addTask(event) {
47 | event.preventDefault();
48 | if(!!this.state.description) {
49 | TaskActions.createTask({description: this.state.description});
50 | this.setState(this._defaultState());
51 | }
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/app/public/js/components/TodoApp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseComponent from './BaseComponent.js';
3 | import NewTask from './NewTask.js';
4 | import TaskList from './TaskList.js';
5 | import Footer from './Footer.js';
6 | import Header from './Header.js';
7 | import TaskStore from '../stores/TaskStore.js';
8 |
9 | const ReactPropTypes = React.PropTypes;
10 |
11 | function getState() {
12 | return {
13 | tasks: TaskStore.getTasks(),
14 | remainingItems: TaskStore.remainingItems()
15 | };
16 | }
17 |
18 | export default class TodoApp extends BaseComponent {
19 |
20 | constructor(props) {
21 | super(props);
22 | this._bind(['_onChange']);
23 |
24 | let tasks = this.props.tasks || [];
25 | TaskStore.setTasks(tasks);
26 | this.state = getState();
27 | }
28 |
29 | componentDidMount() {
30 | TaskStore.addChangeListener(this._onChange);
31 | }
32 |
33 | componentWillUnmount() {
34 | TaskStore.removeChangeListener(this._onChange);
35 | }
36 |
37 | render() {
38 | return (
39 |
40 |
41 |
42 |
43 |
47 |
48 | );
49 |
50 | }
51 |
52 | _onChange() {
53 | this.setState(getState());
54 | }
55 |
56 | }
57 |
58 | TodoApp.propTypes = {
59 | tasks: ReactPropTypes.array
60 | };
--------------------------------------------------------------------------------
/app/public/js/components/TaskListItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseComponent from './BaseComponent.js';
3 | import classNames from 'classnames';
4 | import TaskActions from '../actions/TaskActions.js';
5 |
6 | const ReactPropTypes = React.PropTypes;
7 |
8 | export default class TaskListItem extends BaseComponent {
9 |
10 | constructor(props) {
11 | super(props);
12 | this._bind(['_toggleDone']);
13 | }
14 |
15 | render() {
16 | let task = this.props.task;
17 | let classes = classNames({
18 | 'todo-list__item': true,
19 | 'todo-list__item--done': task.done
20 | });
21 |
22 | return (
23 |
31 |
38 |
39 |
40 | );
41 |
42 | }
43 |
44 | _toggleDone() {
45 | TaskActions.toggleDone(this.props.task);
46 | }
47 |
48 | }
49 |
50 | TaskListItem.propTypes = {
51 | task: ReactPropTypes.object.isRequired,
52 | order: ReactPropTypes.number.isRequired,
53 | onDragStart: ReactPropTypes.func.isRequired,
54 | onDragOver: ReactPropTypes.func.isRequired,
55 | onDragEnd: ReactPropTypes.func.isRequired
56 | };
--------------------------------------------------------------------------------
/app/public/js/actions/TaskActions.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/AppDispatcher.js';
2 | import TaskConstants from '../contants/TaskConstants.js';
3 |
4 | export default {
5 |
6 | setTasks(tasks) {
7 | AppDispatcher.dispatch({
8 | actionType: TaskConstants.SET_TODOS,
9 | data: tasks
10 | });
11 | },
12 |
13 | saveTask(newTask) {
14 | AppDispatcher.dispatch({
15 | actionType: TaskConstants.SAVE_TODO,
16 | data: newTask
17 | });
18 | },
19 |
20 | createTask(newTask) {
21 | AppDispatcher.dispatch({
22 | actionType: TaskConstants.CREATE_TODO,
23 | data: newTask
24 | });
25 | },
26 |
27 | addTask(task) {
28 | AppDispatcher.dispatch({
29 | actionType: TaskConstants.ADD_TODO,
30 | data: task
31 | });
32 | },
33 |
34 | reorder(task, from, to) {
35 | AppDispatcher.dispatch({
36 | actionType: TaskConstants.REORDER,
37 | data: { task, from, to }
38 | });
39 | },
40 |
41 | saveTasks(tasks) {
42 | AppDispatcher.dispatch({
43 | actionType: TaskConstants.SAVE_TODOS,
44 | data: tasks
45 | });
46 | },
47 |
48 | toggleDone(task) {
49 | task.done = !task.done;
50 |
51 | AppDispatcher.dispatch({
52 | actionType: TaskConstants.SAVE_TODO,
53 | data: task
54 | });
55 | },
56 |
57 | completeAll(tasks) {
58 |
59 | let toUpdate = tasks
60 | .filter(task => !task.done)
61 | .map(task => {
62 | task.done = true;
63 | return task;
64 | });
65 |
66 | AppDispatcher.dispatch({
67 | actionType: TaskConstants.SAVE_TODOS,
68 | data: toUpdate
69 | });
70 | }
71 |
72 | };
--------------------------------------------------------------------------------
/app/server.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import path from 'path';
3 | import request from 'superagent';
4 | import react from 'react';
5 | import TaskAPI from './public/js/utils/TaskAPI.js';
6 | import todoapp from './public/js/components/TodoApp.js';
7 | import bodyParser from 'body-parser';
8 |
9 | let port = 8001;
10 | let app = express();
11 | let TodoApp = react.createFactory(todoapp);
12 |
13 | app.use(bodyParser.json());
14 | app.use(bodyParser.urlencoded({
15 | extended: true
16 | }));
17 |
18 | app.use(express.static(path.join(__dirname, 'public')));
19 | app.set('views', path.join(__dirname, 'views'));
20 | app.set('view engine', 'ejs');
21 |
22 | // routes
23 | app.get('/', (req, res) => {
24 |
25 | TaskAPI.fetchTasks(tasks => {
26 | let html = react.renderToString(
27 | TodoApp({
28 | tasks: tasks
29 | })
30 | );
31 |
32 | res.render('index.ejs', {
33 | app: html,
34 | state: JSON.stringify(tasks)
35 | });
36 | });
37 |
38 | });
39 |
40 | app.post('/tasks/new', (req, res) => {
41 | let description = req.body.description;
42 |
43 | if(description) {
44 | let task = {description};
45 | TaskAPI.createTask(task, () => res.redirect('/'));
46 | }
47 |
48 | });
49 |
50 | app.post('/tasks/:id/toggle', (req, res) => {
51 | let _id = req.params.id;
52 | let done = !!req.body.done;
53 | let order = Number(req.body.order);
54 | let description = req.body.description;
55 | let task = {_id, description, order, done};
56 |
57 | if(_id) {
58 | TaskAPI.updateTask(task, () => res.redirect('/'));
59 | }
60 | });
61 |
62 | app.post('/tasks/allDone', (req, res) => {
63 |
64 | TaskAPI.fetchTasks(tasks => {
65 | let undone = tasks.filter(task => !tasks.done );
66 |
67 | undone.forEach(task => {
68 | task.done = true;
69 | TaskAPI.updateTask(task);
70 | });
71 |
72 | res.redirect('/');
73 |
74 | });
75 |
76 | });
77 |
78 | app.listen(port, () => console.log('listening on port: ', port));
--------------------------------------------------------------------------------
/app/__tests__/footer-test.js:
--------------------------------------------------------------------------------
1 | jest.dontMock('../public/js/components/BaseComponent.js');
2 | jest.dontMock('../public/js/components/Footer.js');
3 | jest.dontMock('../public/js/actions/TaskActions.js');
4 |
5 | var React = require('react/addons'),
6 | Footer = require('../public/js/components/Footer.js'),
7 | TaskActions = require('../public/js/actions/TaskActions.js'),
8 | TestUtils = React.addons.TestUtils;
9 |
10 | describe('Footer: ', function () {
11 |
12 | var tasks;
13 |
14 | beforeEach(function () {
15 |
16 | tasks = [
17 | {description: 'test task one', done: false},
18 | {description: 'test task two', done: false},
19 | {description: 'test task three', done: false}
20 | ];
21 |
22 | });
23 |
24 | function renderComponent() {
25 | var remaining = tasks.filter(function(task){ return !task.done; });
26 |
27 | return TestUtils.renderIntoDocument(
28 |
29 | );
30 | }
31 |
32 | it('should show the number of tasks remaining', function () {
33 | var FooterElement = renderComponent();
34 | var countSpan = TestUtils.findRenderedDOMComponentWithTag(FooterElement, 'span');
35 |
36 | expect(countSpan.getDOMNode().textContent).toEqual('3 items left');
37 | });
38 |
39 | it('should use singular when there is only one task left', function () {
40 | tasks[0].done = true;
41 | tasks[1].done = true;
42 | var FooterElement = renderComponent();
43 | var countSpan = TestUtils.findRenderedDOMComponentWithTag(FooterElement, 'span');
44 |
45 | expect(countSpan.getDOMNode().textContent).toEqual('1 item left');
46 | });
47 |
48 | it('should contain a mark all as complete button', function () {
49 | var FooterElement = renderComponent();
50 | var button = TestUtils.findRenderedDOMComponentWithTag(FooterElement, 'button');
51 |
52 | expect(button.getDOMNode().textContent).toEqual('Mark all as complete');
53 | });
54 |
55 | it('mark all as complete button should complete all tasks', function () {
56 | var spy = spyOn(TaskActions, 'completeAll');
57 | var FooterElement = renderComponent();
58 | var button = TestUtils.findRenderedDOMComponentWithTag(FooterElement, 'button');
59 | TestUtils.Simulate.click(button);
60 |
61 | expect(spy).toHaveBeenCalled();
62 | });
63 |
64 | });
--------------------------------------------------------------------------------
/api/routes/task.py:
--------------------------------------------------------------------------------
1 | from flask import abort, request
2 |
3 | from api import app
4 | from api import jsonify
5 | from api.models.task import Task
6 |
7 | def find_task(task_id):
8 | return Task.query.get(task_id)
9 |
10 | def next_order():
11 | task = Task.query.descending(Task.order).first()
12 | if task is None:
13 | return 0
14 |
15 | return task.order + 1
16 |
17 | def valid_request(request):
18 | if not request.json:
19 | return False
20 | if 'description' in request.json and type(request.json['description']) is not str:
21 | return False
22 | if 'done' in request.json and type(request.json['done']) is not bool:
23 | return False
24 | if 'order' in request.json and type(request.json['order']) is not int:
25 | return False
26 |
27 | return True
28 |
29 | @app.route('/api/tasks', methods=['GET'])
30 | def get_tasks():
31 | tasks = Task.query.ascending(Task.order).all()
32 | results = []
33 | for task in tasks:
34 | results.append(task.wrap())
35 | return jsonify(results)
36 |
37 | @app.route('/api/tasks/', methods=['GET'])
38 | def get_task(task_id):
39 | task = find_task(task_id)
40 | if task is None:
41 | abort(404)
42 | result = task.wrap()
43 |
44 | return jsonify(result)
45 |
46 | @app.route('/api/tasks', methods=['POST'])
47 | def create_task():
48 | if not request.json or not 'description' in request.json:
49 | abort(400)
50 | task = Task(description=request.json['description'], order=next_order(), done=False)
51 | task.save()
52 | result = task.wrap()
53 |
54 | return jsonify(result), 201
55 |
56 | @app.route('/api/tasks/', methods=['PUT'])
57 | def update_task(task_id):
58 | task = find_task(task_id)
59 | if task is None:
60 | abort(404)
61 | if not valid_request(request):
62 | abort(400)
63 |
64 | task.description = request.json['description']
65 | task.order = request.json['order']
66 | task.done = request.json['done']
67 | task.save()
68 | result = task.wrap()
69 |
70 | return jsonify(result)
71 |
72 | @app.route('/api/tasks/', methods=['DELETE'])
73 | def delete_task(task_id):
74 | task = find_task(task_id)
75 | if task is None:
76 | abort(404)
77 | task.remove()
78 | return jsonify({'result': True})
--------------------------------------------------------------------------------
/app/public/js/stores/TaskStore.js:
--------------------------------------------------------------------------------
1 | import AppDispatcher from '../dispatcher/AppDispatcher.js';
2 | import {EventEmitter} from 'events';
3 | import TaskConstants from '../contants/TaskConstants.js';
4 | import TaskActions from '../actions/TaskActions.js';
5 | import TaskAPI from '../utils/TaskAPI.js';
6 | import assign from 'object-assign';
7 |
8 | let _tasks = [];
9 |
10 | function setTasks(data) {
11 | _tasks = data;
12 | }
13 |
14 | function createTask(newTask) {
15 | TaskAPI.createTask(newTask, (err, res) => {
16 | if(!err) {
17 | TaskActions.addTask(res.body);
18 | }
19 | });
20 | }
21 |
22 | function addTask(task) {
23 | _tasks.push(task);
24 | }
25 |
26 | function updateTask(task) {
27 | TaskAPI.updateTask(task);
28 | }
29 |
30 | function updateAll(tasks) {
31 | tasks.forEach(task => updateTask(task));
32 | }
33 |
34 | function reorder(task, from, to) {
35 | _tasks.splice(to, 0, _tasks.splice(from,1)[0]);
36 | }
37 |
38 | let TaskStore = assign({}, EventEmitter.prototype, {
39 |
40 | getTasks() {
41 | return _tasks;
42 | },
43 |
44 | //Stores should in general not have setters.
45 | //Used for syncing state with server rendered react app.
46 | setTasks(tasks) {
47 | _tasks = tasks;
48 | },
49 |
50 | remainingItems() {
51 | return _tasks.filter(task => !task.done);
52 | },
53 |
54 | emitChange() {
55 | this.emit('change');
56 | },
57 |
58 | addChangeListener(callback) {
59 | this.addListener('change', callback);
60 | },
61 |
62 | removeChangeListener(callback) {
63 | this.removeListener('change', callback);
64 | }
65 |
66 | });
67 |
68 | AppDispatcher.register(payload => {
69 | switch (payload.actionType) {
70 | case TaskConstants.SET_TODOS:
71 | setTasks(payload.data);
72 | break;
73 |
74 | case TaskConstants.CREATE_TODO:
75 | createTask(payload.data);
76 | break;
77 |
78 | case TaskConstants.ADD_TODO:
79 | addTask(payload.data);
80 | break;
81 |
82 | case TaskConstants.SAVE_TODO:
83 | updateTask(payload.data);
84 | break;
85 |
86 | case TaskConstants.COMPLETE_ALL:
87 | updateAll(payload.data);
88 | break;
89 |
90 | case TaskConstants.REORDER:
91 | reorder(payload.data.task, payload.data.from, payload.data.to);
92 | break;
93 |
94 | case TaskConstants.SAVE_TODOS:
95 | updateAll(payload.data);
96 | break;
97 |
98 | default:
99 | break;
100 | }
101 |
102 | TaskStore.emitChange();
103 |
104 | });
105 |
106 | export default TaskStore;
--------------------------------------------------------------------------------
/app/__tests__/newTask-test.js:
--------------------------------------------------------------------------------
1 | jest.dontMock('../public/js/components/BaseComponent.js');
2 | jest.dontMock('../public/js/components/NewTask.js');
3 | jest.dontMock('../public/js/actions/TaskActions.js');
4 |
5 | var React = require('react/addons'),
6 | NewTask = require('../public/js/components/NewTask.js'),
7 | TaskActions = require('../public/js/actions/TaskActions.js'),
8 | TestUtils = React.addons.TestUtils;
9 |
10 | describe('NewTask: ', function () {
11 |
12 | function renderComponent() {
13 | return TestUtils.renderIntoDocument(
14 |
15 | );
16 | }
17 |
18 | describe('test input', function () {
19 |
20 | it('should update state.description when the value in the input changes', function () {
21 | var NewTaskElement = renderComponent();
22 | var newDescription = 'test description';
23 | var input = TestUtils.findRenderedDOMComponentWithTag(NewTaskElement, 'input');
24 | TestUtils.Simulate.change(input, {target: {value: newDescription}});
25 |
26 | expect(NewTaskElement.state.description).toEqual(newDescription);
27 | });
28 |
29 | it('should include a placeholder', function () {
30 | var NewTaskElement = renderComponent();
31 | var input = TestUtils.findRenderedDOMComponentWithTag(NewTaskElement, 'input');
32 |
33 | expect(input.getDOMNode().getAttribute('placeholder')).toEqual('What needs to be done?');
34 | });
35 |
36 | it('should include a value equal to state.description', function () {
37 | var NewTaskElement = renderComponent();
38 | var input = TestUtils.findRenderedDOMComponentWithTag(NewTaskElement, 'input');
39 |
40 | NewTaskElement.setState({
41 | description: 'test description'
42 | });
43 |
44 | expect(input.getDOMNode().getAttribute('value')).toEqual(NewTaskElement.state.description);
45 | });
46 |
47 | });
48 |
49 | describe('add button', function () {
50 |
51 | var createTaskSpy;
52 |
53 | beforeEach(function (){
54 | createTaskSpy = spyOn(TaskActions, 'createTask');
55 | });
56 |
57 | it('should not call createTask action if the state.description is empty', function () {
58 | var NewTaskElement = renderComponent();
59 | var button = TestUtils.findRenderedDOMComponentWithTag(NewTaskElement, 'button');
60 | TestUtils.Simulate.click(button);
61 |
62 | expect(createTaskSpy).not.toHaveBeenCalled();
63 | expect(NewTaskElement.state.description).toEqual('');
64 | });
65 |
66 | it('should call createTask action when clicked if the state.description has a value', function () {
67 | var NewTaskElement = renderComponent();
68 | var button = TestUtils.findRenderedDOMComponentWithTag(NewTaskElement, 'button');
69 | var newDescription = 'test description';
70 |
71 | NewTaskElement.setState({
72 | description: newDescription
73 | });
74 | TestUtils.Simulate.click(button);
75 |
76 | expect(createTaskSpy).toHaveBeenCalledWith({
77 | description: newDescription
78 | });
79 | });
80 |
81 | it('should reset state.description when clicked', function () {
82 | var NewTaskElement = renderComponent();
83 | var button = TestUtils.findRenderedDOMComponentWithTag(NewTaskElement, 'button');
84 | var newDescription = 'test description';
85 |
86 | NewTaskElement.setState({
87 | description: newDescription
88 | });
89 | TestUtils.Simulate.click(button);
90 |
91 | expect(NewTaskElement.state.description).not.toEqual(newDescription);
92 | expect(NewTaskElement.state.description).toEqual('');
93 | });
94 |
95 | });
96 |
97 | });
--------------------------------------------------------------------------------
/app/public/css/main.css:
--------------------------------------------------------------------------------
1 | *, *::before, *::after {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | background: #f6f5f5;
7 | padding: 10px;
8 | color: #333;
9 | font-family: 'Open Sans', sans-serif;
10 | }
11 |
12 | .todo {
13 | max-width: 500px;
14 | margin: 95px auto 0;
15 | background: #fff;
16 | border: 1px solid #e4e4e4;
17 | border-radius: 5px;
18 | }
19 |
20 | .todo--right {
21 | float: right;
22 | }
23 |
24 | .todo__checkbox:not(:checked),
25 | .todo__checkbox:checked {
26 | position: absolute;
27 | left: -9999px;
28 | }
29 |
30 | .todo__checkbox:not(:checked) + label,
31 | .todo__checkbox:checked + label {
32 | position: relative;
33 | padding-left: 25px;
34 | cursor: pointer;
35 | }
36 |
37 | .todo__checkbox:not(:checked) + label:before,
38 | .todo__checkbox:checked + label:before {
39 | content: '';
40 | position: absolute;
41 | left:0;
42 | top: 4px;
43 | width: 17px;
44 | height: 17px;
45 | border: 1px solid #888;
46 | box-shadow: 0 0 0 2px #d8d9da;
47 | background: #efefed;
48 | border-radius: 3px;
49 | }
50 |
51 | .todo__checkbox:checked + label:before {
52 | content: '\2714';
53 | position: absolute;
54 | font-size: 12px;
55 | color: #0b4164;
56 | background-color: #278ab7;
57 | box-shadow: 0 0 0 2px #ccc;
58 | text-shadow: 0px 1px 1px rgba(255,255,255,0.5);
59 | padding-left: 2px;
60 | }
61 |
62 |
63 | .todo-header {
64 | text-align: center;
65 | border-bottom: 1px dashed #d2d0d0;
66 | }
67 |
68 | .todo-footer {
69 | border-top: 1px dashed #d2d0d0;
70 | padding: 20px;
71 | color: #999;
72 | font-size: 13px;
73 | }
74 |
75 | .todo-footer form {
76 | margin: 0;
77 | }
78 |
79 | .todo-footer__link {
80 | border: none;
81 | background: transparent;
82 | color: #448ccb;
83 | font-size: 13px;
84 | }
85 |
86 | .todo-add {
87 | padding: 20px;
88 | margin: 0;
89 | }
90 |
91 | .todo-add__input {
92 | max-width: 300px;
93 | width: 100%;
94 | border: 1px solid #b7b6b6;
95 | border-radius: 3px;
96 | background: #fff;
97 | padding: 10px;
98 | font-size: 14px;
99 | margin-right: 10px;
100 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.2) inset;
101 | }
102 |
103 | .todo-add__button {
104 | max-width: 145px;
105 | width: 100%;
106 | border: 1px solid #999;
107 | border-radius: 3px;
108 | padding: 10px;
109 | background: #ffffff; /* Old browsers */
110 | background: -webkit-linear-gradient(top, #ffffff 0%,#ebeae9 1%,#eae9e8 50%,#dfdedd 51%,#d7d5d4 100%);
111 | background: -ms-linear-gradient(top, #ffffff 0%,#ebeae9 1%,#eae9e8 50%,#dfdedd 51%,#d7d5d4 100%);
112 | background: linear-gradient(to bottom, #ffffff 0%,#ebeae9 1%,#eae9e8 50%,#dfdedd 51%,#d7d5d4 100%);
113 | color: #454545;
114 | font-size: 13px;
115 | font-weight: bold;
116 | text-shadow: 1px -1px 1px rgba(0,0,0,0.2), -1px 1px 1px rgba(255,255,255,1);
117 | }
118 |
119 | .todo-list {
120 | margin: 0;
121 | padding: 0;
122 | list-style: none;
123 | }
124 |
125 | .todo-list--empty {
126 | text-align: center;
127 | color: #999;
128 | padding: 80px 20px;
129 | }
130 |
131 | .todo-list__item {
132 | padding: 15px 20px;
133 | }
134 |
135 | .todo-list__item--dragging {
136 |
137 | }
138 |
139 | .todo-list__item:hover {
140 | background-image: url('../images/drag_icon.png');
141 | background-repeat: no-repeat;
142 | background-position: 96% 50%;
143 | cursor: move;
144 | cursor: -webkit-grab; cursor: -moz-grab;
145 | }
146 |
147 | .todo-list__item--done {
148 | text-decoration: line-through;
149 | color: #999;
150 | }
151 |
152 | .todo-list__item:nth-child(odd) {
153 | background-color: #f4f7fa;
154 | }
155 |
156 | .todo-list__item--dragging,
157 | .todo-list__item--dragging:nth-child(odd) {
158 | background-color: #eee;
159 | opacity: 0.7;
160 | }
161 |
162 |
--------------------------------------------------------------------------------
/app/__tests__/taskActions-test.js:
--------------------------------------------------------------------------------
1 | jest.dontMock('../public/js/actions/TaskActions.js');
2 | jest.dontMock('../public/js/dispatcher/AppDispatcher.js');
3 | jest.dontMock('../public/js/contants/TaskConstants.js');
4 | jest.dontMock('keymirror');
5 |
6 | var TaskActions = require('../public/js/actions/TaskActions.js'),
7 | AppDispatcher = require('../public/js/dispatcher/AppDispatcher.js'),
8 | TaskConstants = require('../public/js/contants/TaskConstants.js');
9 |
10 | describe('TaskActions: ', function () {
11 |
12 | var dispatchSpy, tasks;
13 |
14 | beforeEach(function () {
15 | dispatchSpy = spyOn(AppDispatcher, 'dispatch');
16 |
17 | tasks = [
18 | {description: 'test task one', done: false},
19 | {description: 'test task two', done: false},
20 | {description: 'test task three', done: false}
21 | ];
22 | });
23 |
24 | it('#setTasks should dispatch SET_TODOS action', function (){
25 | TaskActions.setTasks(tasks);
26 | expect(dispatchSpy).toHaveBeenCalledWith({
27 | actionType: TaskConstants.SET_TODOS,
28 | data: tasks
29 | });
30 | });
31 |
32 | it('#saveTask should dispatch SAVE_TODO action', function (){
33 | var newTask = {description: 'test description'};
34 | TaskActions.saveTask(newTask);
35 | expect(dispatchSpy).toHaveBeenCalledWith({
36 | actionType: TaskConstants.SAVE_TODO,
37 | data: newTask
38 | });
39 | });
40 |
41 | it('#createTask should dispatch CREATE_TODO action', function (){
42 | var newTask = {description: 'test description'};
43 | TaskActions.createTask(newTask);
44 | expect(dispatchSpy).toHaveBeenCalledWith({
45 | actionType: TaskConstants.CREATE_TODO,
46 | data: newTask
47 | });
48 | });
49 |
50 | it('#addTask should dispatch ADD_TODO action', function (){
51 | TaskActions.addTask(tasks[0]);
52 | expect(dispatchSpy).toHaveBeenCalledWith({
53 | actionType: TaskConstants.ADD_TODO,
54 | data: tasks[0]
55 | });
56 | });
57 |
58 | it('#reorder should dispatch REORDER action', function (){
59 | var from = 1;
60 | var to = 0;
61 | TaskActions.reorder(tasks[0], from, to);
62 | expect(dispatchSpy).toHaveBeenCalledWith({
63 | actionType: TaskConstants.REORDER,
64 | data: {
65 | task: tasks[0],
66 | from: from,
67 | to: to
68 | }
69 | });
70 | });
71 |
72 | it('#saveTasks should dispatch SAVE_TODOS action', function (){
73 | TaskActions.saveTasks(tasks);
74 | expect(dispatchSpy).toHaveBeenCalledWith({
75 | actionType: TaskConstants.SAVE_TODOS,
76 | data: tasks
77 | });
78 | });
79 |
80 | describe('#toggleDone', function () {
81 |
82 | it('should dispatch SAVE_TODO action', function (){
83 | TaskActions.toggleDone(tasks[0]);
84 | expect(dispatchSpy).toHaveBeenCalledWith({
85 | actionType: TaskConstants.SAVE_TODO,
86 | data: tasks[0]
87 | });
88 | });
89 |
90 | it('should toggle task.done false to true', function () {
91 | tasks[0].done = false;
92 | TaskActions.toggleDone(tasks[0]);
93 | expect(tasks[0].done).toEqual(true);
94 | });
95 |
96 | it('should toggle task.done true to false', function () {
97 | tasks[0].done = true;
98 | TaskActions.toggleDone(tasks[0]);
99 | expect(tasks[0].done).toEqual(false);
100 | });
101 |
102 | });
103 |
104 | describe('#completeAll', function () {
105 |
106 | it('should dispatch SAVE_TODOS action', function (){
107 | TaskActions.completeAll(tasks);
108 | expect(dispatchSpy).toHaveBeenCalledWith({
109 | actionType: TaskConstants.SAVE_TODOS,
110 | data: tasks
111 | });
112 | });
113 |
114 | it('should only dispatch tasks that have not been done', function (){
115 | tasks[1].done = true;
116 | var unfinishedTasks = [tasks[0], tasks[2]];
117 | TaskActions.completeAll(tasks);
118 | expect(dispatchSpy).toHaveBeenCalledWith({
119 | actionType: TaskConstants.SAVE_TODOS,
120 | data: unfinishedTasks
121 | });
122 | });
123 |
124 | it('should toggle task.done false to true', function () {
125 | TaskActions.completeAll(tasks);
126 | expect(tasks[0].done).toEqual(true);
127 | expect(tasks[1].done).toEqual(true);
128 | expect(tasks[2].done).toEqual(true);
129 | });
130 |
131 | });
132 |
133 | });
--------------------------------------------------------------------------------
/app/public/js/components/TaskList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BaseComponent from './BaseComponent.js';
3 | import TaskListItem from './TaskListItem.js';
4 | import DraggedItem from './DraggedItem.js';
5 | import TaskActions from '../actions/TaskActions.js';
6 |
7 | const ReactPropTypes = React.PropTypes;
8 |
9 | export default class TaskList extends BaseComponent {
10 |
11 | constructor(props) {
12 | super(props);
13 | this._bind([
14 | '_renderTaskListItems',
15 | '_setDraggingIndex',
16 | '_setDragging',
17 | '_setDraggingOrigin',
18 | '_setDraggingInfo',
19 | '_updateTaskOrders',
20 | '_calculateToIndex',
21 | '_onDragStart',
22 | '_onDragOver',
23 | '_onDragEnd'
24 | ]);
25 |
26 | this.state = {
27 | draggingInfo: {},
28 | draggingOrigin: {}
29 | };
30 | }
31 |
32 | render() {
33 | let tasks = this.props.tasks;
34 | let taskList = ({this._renderTaskListItems(tasks)}
);
35 | let emptyList = (Empty
);
36 | let list = tasks.length > 0 ? taskList : emptyList;
37 |
38 | return (
39 | list
40 | );
41 | }
42 |
43 | _renderTaskListItems(tasks) {
44 | let dragStart = this._onDragStart;
45 | let dragOver = this._onDragOver;
46 | let dragEnd = this._onDragEnd;
47 |
48 | let items = tasks.map((task, index) => {
49 | return (
50 |
58 | );
59 | });
60 |
61 | if(this.state.dragging) {
62 | items.push(
63 |
68 | );
69 | }
70 |
71 | return items;
72 | }
73 |
74 | _setDraggingIndex(index) {
75 | this.setState({draggingIndex: index});
76 | }
77 |
78 | _setDragging(task) {
79 | this.setState({
80 | dragging: task
81 | });
82 | }
83 |
84 | _setDraggingOrigin(originX, originY, elementX, elementY) {
85 | this.setState({
86 | draggingOrigin: {
87 | originX: originX,
88 | originY: originY,
89 | elementX: elementX,
90 | elementY: elementY
91 | }
92 | });
93 | }
94 |
95 | _setDraggingInfo(pageY) {
96 | let deltaY = pageY - this.state.draggingOrigin.originY;
97 |
98 | this.setState({
99 | draggingInfo: {
100 | left: this.state.draggingOrigin.elementX,
101 | top: this.state.draggingOrigin.elementY + deltaY + document.body.scrollTop
102 | }
103 | });
104 | }
105 |
106 | _updateTaskOrders() {
107 | let tasks = this.props.tasks;
108 | tasks.forEach(function(task, key) {
109 | task.order = key;
110 | });
111 | }
112 |
113 | _calculateToIndex(from, event) {
114 | let current = event.currentTarget;
115 | let to = Number(current.dataset.id);
116 |
117 | if((event.clientY - current.offsetTop) > (current.offsetHeight / 2)) to++;
118 | if(from < to) to--;
119 |
120 | return to;
121 | }
122 |
123 | _onDragStart(event) {
124 | let index = Number(event.currentTarget.dataset.id);
125 | let rect = event.currentTarget.getBoundingClientRect();
126 |
127 | this._setDraggingIndex(index);
128 | this._setDragging(this.props.tasks[index]);
129 | this._setDraggingOrigin(event.pageX, event.pageY, rect.left, rect.top);
130 |
131 | event.dataTransfer.effectAllowed = 'move';
132 | event.dataTransfer.setData('text/html', null);
133 |
134 | }
135 |
136 | _onDragOver(event) {
137 | event.preventDefault();
138 |
139 | let from = this.state.draggingIndex;
140 | let to = this._calculateToIndex(from, event);
141 |
142 | this._setDraggingInfo(event.pageY);
143 |
144 | if(from !== to) {
145 | TaskActions.reorder(this.props.tasks[from], from, to);
146 | this._setDraggingIndex(to);
147 | }
148 |
149 | }
150 |
151 | _onDragEnd() {
152 | this._updateTaskOrders();
153 | this._setDraggingIndex(undefined);
154 | this._setDraggingOrigin(); //undefined
155 | this._setDragging(undefined);
156 | this.setState({
157 | draggingInfo: {}
158 | });
159 | TaskActions.saveTasks(this.props.tasks);
160 | }
161 |
162 | }
163 |
164 | TaskList.propTypes = {
165 | tasks: ReactPropTypes.array.isRequired
166 | };
--------------------------------------------------------------------------------
/app/__tests__/taskStore-test.js:
--------------------------------------------------------------------------------
1 | jest.dontMock('../public/js/contants/TaskConstants.js');
2 | jest.dontMock('../public/js/stores/TaskStore.js');
3 | jest.dontMock('../public/js/utils/TaskAPI.js');
4 | jest.dontMock('object-assign');
5 | jest.dontMock('keymirror');
6 |
7 | var TaskConstants = require('../public/js/contants/TaskConstants.js');
8 | var AppDispatcher;
9 | var TaskStore;
10 | var TaskAPI;
11 | var callback;
12 | var tasks;
13 | var mockActions;
14 |
15 | function setMockActions(tasks) {
16 | return {
17 | setTodosAction: {
18 | actionType: TaskConstants.SET_TODOS,
19 | data: tasks
20 | },
21 | createTodoAction: {
22 | actionType: TaskConstants.CREATE_TODO,
23 | data: {description: 'test description'}
24 | },
25 | addTodoAction: {
26 | actionType: TaskConstants.ADD_TODO,
27 | data: tasks[0]
28 | },
29 | saveTodoAction: {
30 | actionType: TaskConstants.SAVE_TODO,
31 | data: tasks[0]
32 | },
33 | saveTodosAction: {
34 | actionType: TaskConstants.SAVE_TODOS,
35 | data: tasks
36 | },
37 | completeAllAction: {
38 | actionType: TaskConstants.COMPLETE_ALL,
39 | data: tasks
40 | },
41 | reorderAction: {
42 | actionType: TaskConstants.REORDER,
43 | data: {
44 | task: tasks[0],
45 | from: 0,
46 | to: 2
47 | }
48 | }
49 | };
50 | }
51 |
52 | describe('TaskStore: ', function () {
53 |
54 | beforeEach(function () {
55 | AppDispatcher = require('../public/js/dispatcher/AppDispatcher.js');
56 | TaskStore = require('../public/js/stores/TaskStore.js');
57 | TaskAPI = require('../public/js/utils/TaskAPI.js');
58 | callback = AppDispatcher.register.mock.calls[0][0];
59 |
60 | tasks = [
61 | {description: 'test task one', done: false},
62 | {description: 'test task two', done: false},
63 | {description: 'test task three', done: false}
64 | ];
65 |
66 | mockActions = setMockActions(tasks);
67 | });
68 |
69 | it('registers a callback with the AppDispatcher', function () {
70 | expect(AppDispatcher.register.mock.calls.length).toEqual(1);
71 | });
72 |
73 | it('should init without any tasks', function () {
74 | var tasks = TaskStore.getTasks();
75 | expect(tasks).toEqual([]);
76 | });
77 |
78 | it('#setTasks should update the tasks', function() {
79 | TaskStore.setTasks(tasks);
80 | var updatedTasks = TaskStore.getTasks();
81 | expect(updatedTasks).toEqual(tasks);
82 | });
83 |
84 | it('#remainingItems should return the tasks that have done set as false', function () {
85 | tasks[1].done = true;
86 | TaskStore.setTasks(tasks);
87 | var remaining = TaskStore.remainingItems();
88 | expect(remaining).toEqual([tasks[0], tasks[2]]);
89 | });
90 |
91 | it('should update tasks when responding to SET_TODOS action', function () {
92 | callback(mockActions.setTodosAction);
93 | var updatedTasks = TaskStore.getTasks();
94 | expect(updatedTasks).toEqual(tasks);
95 | });
96 |
97 | it('should call the server when responding to CREATE_TODO action', function () {
98 | var createTaskSpy = spyOn(TaskAPI, 'createTask');
99 | callback(mockActions.createTodoAction);
100 | expect(createTaskSpy).toHaveBeenCalledWith(mockActions.createTodoAction.data, function(){});
101 | });
102 |
103 | it('should update the tasks list with a new task when responding to ADD_TODO', function () {
104 | callback(mockActions.addTodoAction);
105 | var updatedTasks = TaskStore.getTasks();
106 | expect(updatedTasks).toEqual([tasks[0]]);
107 | });
108 |
109 | it('should call the server when responding to SAVE_TODO action', function() {
110 | var updateTaskSpy = spyOn(TaskAPI, 'updateTask');
111 | callback(mockActions.saveTodoAction);
112 | expect(updateTaskSpy).toHaveBeenCalledWith(mockActions.saveTodoAction.data);
113 | });
114 |
115 | it('should call the server once for each task when responding to COMPLETE_ALL action', function() {
116 | var updateTaskSpy = spyOn(TaskAPI, 'updateTask');
117 | callback(mockActions.completeAllAction);
118 | expect(updateTaskSpy).toHaveBeenCalled();
119 | expect(updateTaskSpy.callCount).toEqual(mockActions.completeAllAction.data.length);
120 | });
121 |
122 | it('should call the server once for each task when responding to SAVE_TODOS action', function () {
123 | var updateTaskSpy = spyOn(TaskAPI, 'updateTask');
124 | callback(mockActions.saveTodosAction);
125 | expect(updateTaskSpy).toHaveBeenCalled();
126 | expect(updateTaskSpy.callCount).toEqual(mockActions.saveTodosAction.data.length);
127 | });
128 |
129 | it('should reorder the tasks when responding to REORDER action', function() {
130 | TaskStore.setTasks(tasks);
131 | callback(mockActions.reorderAction);
132 | var updatedTasks = TaskStore.getTasks();
133 | expect(updatedTasks[0].description).toEqual('test task two');
134 | expect(updatedTasks[1].description).toEqual('test task three');
135 | expect(updatedTasks[2].description).toEqual('test task one');
136 | });
137 |
138 | });
--------------------------------------------------------------------------------
/app/__tests__/taskListItem-test.js:
--------------------------------------------------------------------------------
1 | jest.dontMock('../public/js/components/BaseComponent.js');
2 | jest.dontMock('../public/js/components/TaskListItem.js');
3 | jest.dontMock('../public/js/actions/TaskActions.js');
4 | jest.dontMock('classnames');
5 |
6 | var React = require('react/addons'),
7 | TaskListItem = require('../public/js/components/TaskListItem.js'),
8 | TaskActions = require('../public/js/actions/TaskActions.js'),
9 | TestUtils = React.addons.TestUtils;
10 |
11 |
12 | describe('TaskListItem: ', function () {
13 |
14 | var task, order, onDragStartFn, onDragOverFn, onDragEndFn;
15 |
16 | beforeEach(function () {
17 | task = {
18 | _id: 'asd123',
19 | description: 'test task one',
20 | done: false,
21 | order: 1
22 | };
23 |
24 | order = 0;
25 | onDragStartFn = jasmine.createSpy('onDragStartFn');
26 | onDragOverFn = jasmine.createSpy('onDragOverFn');
27 | onDragEndFn = jasmine.createSpy('onDragEndFn');
28 | });
29 |
30 | function renderComponent() {
31 |
32 | return TestUtils.renderIntoDocument(
33 |
40 | );
41 | }
42 |
43 | it('should render a li element that data-id equal to props.order', function () {
44 | var TaskListItemElement = renderComponent();
45 | var li = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'li');
46 |
47 | expect(Number(li.getDOMNode().getAttribute('data-id'))).toEqual(order);
48 | });
49 |
50 | it('should add class not todo-list__item--done when task.done is false', function () {
51 | var TaskListItemElement = renderComponent();
52 | var li = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'li');
53 |
54 | expect(li.getDOMNode().className).not.toMatch('todo-list__item--done');
55 | });
56 |
57 | it('should add class todo-list__item--done when a task is done', function () {
58 | task.done = true;
59 | var TaskListItemElement = renderComponent();
60 | var li = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'li');
61 |
62 | expect(li.getDOMNode().className).toMatch('todo-list__item--done');
63 | });
64 |
65 | it('should contain a label with the task description', function () {
66 | var TaskListItemElement = renderComponent();
67 | var label = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'label');
68 |
69 | expect(label.getDOMNode().textContent).toEqual(task.description);
70 | });
71 |
72 | describe('draggable', function () {
73 |
74 | it('should render a li element that is draggable', function () {
75 | var TaskListItemElement = renderComponent();
76 | var li = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'li');
77 |
78 | expect(li.getDOMNode().getAttribute('draggable')).toBeTruthy();
79 | });
80 |
81 | it('should call onDragStart prop when responding to drag start event', function () {
82 | var TaskListItemElement = renderComponent();
83 | var li = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'li');
84 | TestUtils.Simulate.dragStart(li);
85 |
86 | expect(onDragStartFn).toHaveBeenCalled();
87 | });
88 |
89 | it('should call onDragOver prop when responding to drag over event', function () {
90 | var TaskListItemElement = renderComponent();
91 | var li = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'li');
92 | TestUtils.Simulate.dragOver(li);
93 |
94 | expect(onDragOverFn).toHaveBeenCalled();
95 | });
96 |
97 | it('should call onDragEnd prop when responding to drag end event', function () {
98 | var TaskListItemElement = renderComponent();
99 | var li = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'li');
100 | TestUtils.Simulate.dragEnd(li);
101 |
102 | expect(onDragEndFn).toHaveBeenCalled();
103 | });
104 |
105 | });
106 |
107 | describe('checkbox ', function () {
108 |
109 | it('should not be checked if the task.done is false', function () {
110 | var TaskListItemElement = renderComponent();
111 | var checkbox = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'input');
112 |
113 | expect(checkbox.getDOMNode().getAttribute('checked')).toEqual(null);
114 | });
115 |
116 | it('should be checked if the task is done', function () {
117 | task.done = true;
118 | var TaskListItemElement = renderComponent();
119 | var checkbox = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'input');
120 |
121 | expect(checkbox.getDOMNode().getAttribute('checked')).toEqual('');
122 | });
123 |
124 | it('should call TaskAction toggleDone when changing state', function () {
125 | var spy = spyOn(TaskActions, 'toggleDone');
126 | var TaskListItemElement = renderComponent();
127 | var checkbox = TestUtils.findRenderedDOMComponentWithTag(TaskListItemElement, 'input');
128 | TestUtils.Simulate.change(checkbox);
129 |
130 | expect(spy).toHaveBeenCalled();
131 | });
132 |
133 |
134 |
135 | });
136 |
137 | });
--------------------------------------------------------------------------------
/app/__tests__/taskList-test.js:
--------------------------------------------------------------------------------
1 | jest.dontMock('../public/js/components/BaseComponent.js');
2 | jest.dontMock('../public/js/components/TaskList.js');
3 | jest.dontMock('../public/js/components/TaskListItem.js');
4 | jest.dontMock('../public/js/components/DraggedItem.js');
5 | jest.dontMock('../public/js/actions/TaskActions.js');
6 |
7 | var React = require('react/addons'),
8 | TaskList = require('../public/js/components/TaskList.js'),
9 | DraggedItem = require('../public/js/components/DraggedItem.js'),
10 | TaskActions = require('../public/js/actions/TaskActions.js'),
11 | TestUtils = React.addons.TestUtils;
12 |
13 | describe('TaskList: ', function () {
14 |
15 | var tasks;
16 |
17 | beforeEach(function () {
18 |
19 | tasks = [
20 | {description: 'test task one', done: false},
21 | {description: 'test task two', done: false},
22 | {description: 'test task three', done: false}
23 | ];
24 |
25 | });
26 |
27 | function renderComponent() {
28 | return TestUtils.renderIntoDocument(
29 |
30 | );
31 | }
32 |
33 | it('should render a empty list if props.task is empty', function () {
34 | tasks = [];
35 | var TaskListElement = renderComponent();
36 | var emptyList = TestUtils.findRenderedDOMComponentWithTag(TaskListElement, 'div');
37 |
38 | expect(emptyList.getDOMNode().textContent).toEqual('Empty');
39 | });
40 |
41 | it('should render the same number of todos that there are props.tasks', function () {
42 | var TaskListElement = renderComponent();
43 | var todos = TestUtils.scryRenderedDOMComponentsWithTag(TaskListElement, 'li');
44 |
45 | expect(todos.length).toEqual(tasks.length);
46 | });
47 |
48 | it('should render an and DraggedItem if dragging is set on state', function (){
49 | var TaskListElement = renderComponent();
50 | TaskListElement.setState({dragging: tasks[0]});
51 |
52 | var draggedItem = TestUtils.findRenderedComponentWithType(TaskListElement, DraggedItem);
53 | });
54 |
55 | describe('drag start event', function () {
56 |
57 | var event;
58 |
59 | beforeEach(function () {
60 | event = {
61 | pageX: 1,
62 | pageY: 2,
63 | currentTarget: {
64 | dataset: {id: 1},
65 | getBoundingClientRect: jasmine.createSpy('getBoundingClientRect').andReturn({
66 | left: 0,
67 | top: 0
68 | })
69 | },
70 | dataTransfer: {
71 | setData: jasmine.createSpy('setData')
72 | }
73 | };
74 | });
75 |
76 | it('should set the draggingIndex from the currentTarget data-id', function () {
77 | var TaskListElement = renderComponent();
78 | TaskListElement._onDragStart(event);
79 |
80 | expect(TaskListElement.state.draggingIndex).toEqual(event.currentTarget.dataset.id);
81 | });
82 |
83 | it('should set the dragging task', function () {
84 | var TaskListElement = renderComponent();
85 | TaskListElement._onDragStart(event);
86 |
87 | expect(TaskListElement.state.dragging).toBe(tasks[event.currentTarget.dataset.id]);
88 | });
89 |
90 | it('should set the draggingOrigin', function () {
91 | var TaskListElement = renderComponent();
92 | TaskListElement._onDragStart(event);
93 |
94 | expect(TaskListElement.state.draggingOrigin).toEqual({
95 | originX: 1,
96 | originY: 2,
97 | elementX: 0,
98 | elementY: 0
99 | });
100 | });
101 |
102 | it('should set dataTransfer on the event', function () {
103 | var TaskListElement = renderComponent();
104 | TaskListElement._onDragStart(event);
105 |
106 | expect(event.dataTransfer.effectAllowed).toEqual('move');
107 | expect(event.dataTransfer.setData).toHaveBeenCalledWith('text/html', null);
108 | });
109 |
110 | });
111 |
112 | describe('drag over event', function () {
113 | var TaskListElement, calculateToSpy, draggingIndexSpy, reorderSpy, event;
114 |
115 | beforeEach(function () {
116 | event = {
117 | pageY: 4,
118 | preventDefault: jasmine.createSpy('preventDefault'),
119 | currentTarget: {
120 | dataset: {id: 1}
121 | }
122 | };
123 |
124 | TaskListElement = renderComponent();
125 | calculateToSpy = spyOn(TaskListElement, '_calculateToIndex').andReturn(2);
126 | reorderSpy = spyOn(TaskActions, 'reorder');
127 | draggingIndexSpy = spyOn(TaskListElement, '_setDraggingIndex');
128 | });
129 |
130 | it('should call event.preventDefault', function () {
131 | TaskListElement._onDragOver(event);
132 |
133 | expect(event.preventDefault).toHaveBeenCalled();
134 | });
135 |
136 | it('should set draggingInfo', function () {
137 | TaskListElement.setState({
138 | draggingOrigin: {
139 | elementX: 0,
140 | elementY: 1,
141 | originY: 1
142 | }
143 | });
144 |
145 | TaskListElement._onDragOver(event);
146 |
147 | expect(TaskListElement.state.draggingInfo).toEqual({
148 | left: 0,
149 | top: 4
150 | });
151 | });
152 |
153 | it('should trigger a reorder if dragging over a new target', function () {
154 | TaskListElement.setState({
155 | draggingIndex: 1
156 | });
157 | TaskListElement._onDragOver(event);
158 | expect(reorderSpy).toHaveBeenCalledWith(tasks[1], 1, 2);
159 | });
160 |
161 | it('should update the draggingIndex', function () {
162 | TaskListElement._onDragOver(event);
163 |
164 | expect(draggingIndexSpy).toHaveBeenCalledWith(2);
165 | });
166 |
167 | });
168 |
169 | describe('drag end event', function () {
170 |
171 | var saveTodosSpy;
172 |
173 | beforeEach(function (){
174 | saveTodosSpy = spyOn(TaskActions, 'saveTasks');
175 | });
176 |
177 | it('should update each task.order after the index', function () {
178 | tasks = [
179 | {order: 1},
180 | {order: 0}
181 | ];
182 | TaskListElement = renderComponent();
183 | TaskListElement._onDragEnd();
184 |
185 | expect(tasks[0].order).toEqual(0);
186 | expect(tasks[1].order).toEqual(1);
187 | });
188 |
189 | it('should reset state', function () {
190 | TaskListElement = renderComponent();
191 | TaskListElement.setState({
192 | dragging: tasks[0],
193 | draggingIndex: 0,
194 | draggingInfo: {
195 | left: 5,
196 | top: 5
197 | },
198 | draggingOrigin: {
199 | originX: 7,
200 | originY: 6,
201 | elementX: 5,
202 | elementY: 4
203 | }
204 | });
205 | TaskListElement._onDragEnd();
206 | expect(TaskListElement.state).toEqual({
207 | dragging: undefined,
208 | draggingIndex: undefined,
209 | draggingInfo: {},
210 | draggingOrigin: {
211 | elementX: undefined,
212 | elementY: undefined,
213 | originX: undefined,
214 | originY: undefined
215 | }
216 | });
217 | });
218 |
219 | });
220 |
221 | });
--------------------------------------------------------------------------------