diff --git a/.DS_Store b/.DS_Store
index 0d09ff4..549bdf7 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/api/server.js b/api/server.js
index 4babe2e..2cfa7a3 100644
--- a/api/server.js
+++ b/api/server.js
@@ -39,13 +39,18 @@ ORM.init(app, function(e){
app.use('/api/projects', ORM.REST('projects'))
- app.use('/api/tasks/:task/comments', CRUD.filters('task'), ORM.REST('comments'));
+ app.use('/api/tasks/:task/comments', CRUD.foreignKey('task'), ORM.REST('comments'));
- app.use('/api/tasks', ORM.REST('tasks'))
+ app.use('/api/tasks', CRUD.query(['status', 'assignee', 'version']), ORM.REST('tasks'))
app.use('/api/users', ORM.REST('users'))
app.use('/api/version', ORM.REST('version'))
+ app.get('/api/test', function(req, res){
+ setTimeout(function(){
+ res.json({ title: 'test' });
+ }, 4000);
+ })
// html5 history api
app.use(fallback('index.html', { root: root }))
diff --git a/api/services/CRUD.js b/api/services/CRUD.js
index 1e8d17d..1067e65 100644
--- a/api/services/CRUD.js
+++ b/api/services/CRUD.js
@@ -16,6 +16,21 @@ module.exports = function (modelName, foreignKey) {
req.db[modelName].find());
next();
},
+ getPage: function(req, res, next) {
+ var page = req.param('page');
+ var limit = req.param('limit') || 10;
+
+ res.ormQuery = applyFilters(
+ req.filters,
+ req.db[modelName].find({ skip: page * limit, limit: limit }));
+ next();
+ },
+ count: function(req, res, next) {
+ res.count = applyFilters(
+ req.filters,
+ req.db[modelName].count());
+ next();
+ },
findOne: function (req, res, next) {
res.ormQuery = applyFilters(
req.filters,
@@ -46,7 +61,7 @@ module.exports = function (modelName, foreignKey) {
//TODO: make more complex query
-module.exports.filters = function(foreignKey){
+module.exports.foreignKey = function(foreignKey){
return function (req, res, next) {
if (foreignKey){
req.filters = req.filters || {};
@@ -56,13 +71,26 @@ module.exports.filters = function(foreignKey){
if (req.body){
req.body[foreignKey] = value;
}
-
}
//console.log(req.body, req.params, foreignKey)
next();
}
};
+module.exports.query = function(names) {
+ return function(req, res, next) {
+ req.filters = req.filters || {};
+
+ names.forEach(function(name){
+ if (req.query[name]) { //TODO: fix 0
+ req.filters[name] = req.query[name];
+ }
+ })
+
+ next();
+ }
+}
+
module.exports.exec = function (req, res, next) {
var cb = function(err, result) {
@@ -76,6 +104,22 @@ module.exports.exec = function (req, res, next) {
res.ormQuery.exec(cb);
};
+//TODO: create generec way for requests
+module.exports.execPage = function(req, res, next) {
+ var cb = function(results, err){
+ if (err) {
+ return next(err);
+ }
+ res.result = {
+ items: results[0],
+ count: results[1]
+ }
+
+ next();
+ }
+ Promise.all([res.ormQuery, res.count]).then(cb);
+}
+
module.exports.returnJSON = function (req, res, next) {
diff --git a/api/services/ORM.js b/api/services/ORM.js
index 91362f7..d62a66c 100644
--- a/api/services/ORM.js
+++ b/api/services/ORM.js
@@ -128,9 +128,10 @@ var exports = {
var service = CRUD(collectionName, foreignKey);
-
return router
.get('/', service.findAll)
+ .get('/page/:page', service.getPage, service.count, CRUD.execPage, CRUD.returnJSON)
+ .get('/page/:page/:limit', service.getPage, service.count, CRUD.execPage, CRUD.returnJSON)
.get('/:id', service.findOne)
.post('/', service.add)
.put('/:id', service.updateOne)
diff --git a/frontend/.DS_Store b/frontend/.DS_Store
index 1d3ce65..5eea83b 100644
Binary files a/frontend/.DS_Store and b/frontend/.DS_Store differ
diff --git a/frontend/package.json b/frontend/package.json
index 1641887..1e7e6a6 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,6 +11,7 @@
"author": "",
"license": "ISC",
"dependencies": {
+ "axios": "^0.14.0",
"babel-core": "^6.3.26",
"babel-eslint": "6.0.4",
"babel-loader": "^6.2.1",
@@ -33,7 +34,7 @@
"json-loader": "^0.5.4",
"lodash": "^3.10.1",
"mobx": "^2.1.3",
- "mobx-react": "^3.0.4",
+ "mobx-react": "^3.5.6",
"mobx-react-devtools": "^4.0.2",
"moment": "^2.10.6",
"react": "0.14.3",
@@ -47,6 +48,7 @@
"react-slider": "^0.5.1",
"redbox-react": "1.3.0",
"redux": "3.5.2",
+ "redux-axios-middleware": "^3.0.0",
"redux-form": "5.2.3",
"redux-router": "2.0.0",
"redux-thunk": "2.0.1",
diff --git a/frontend/src/.DS_Store b/frontend/src/.DS_Store
index 16cc086..255486b 100644
Binary files a/frontend/src/.DS_Store and b/frontend/src/.DS_Store differ
diff --git a/frontend/src/HOC/AuthenticatedComponent.jsx b/frontend/src/HOC/AuthenticatedComponent.jsx
new file mode 100644
index 0000000..65f2904
--- /dev/null
+++ b/frontend/src/HOC/AuthenticatedComponent.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+
+import { observer } from 'mobx-react';
+
+export function requireAuthentication(Component) {
+
+ @observer(['auth'])
+ class AuthenticatedComponent extends React.Component {
+ componentWillMount() {
+ this.props.auth.checkAuth();
+ }
+
+ // componentWillReceiveProps(nextProps) {
+ // this.checkAuth();
+ // }
+
+ // checkAuth() {
+ // // if (!this.props.isAuthenticated) {
+ // // let redirectAfterLogin = this.props.location.pathname
+ // // this.props.dispatch(pushState(null, `/login?next=${redirectAfterLogin}`))
+ // // }
+ // this.props.auth.checkAuth()
+ // // .then(() => {
+ // // debugger
+ // // })
+ // .catch(() => {
+ // debugger
+ // this.props.router.push('/login');
+ // });
+ // }
+
+ render() {
+ const { isAuthenticated } = this.props.auth;
+ return (
+
+ {isAuthenticated === true
+ ?
+ : null
+ }
+
+ );
+ }
+ }
+
+ return AuthenticatedComponent;
+
+};
diff --git a/frontend/src/utils/need.jsx b/frontend/src/HOC/loading.jsx
similarity index 57%
rename from frontend/src/utils/need.jsx
rename to frontend/src/HOC/loading.jsx
index 85057f1..5bbd306 100644
--- a/frontend/src/utils/need.jsx
+++ b/frontend/src/HOC/loading.jsx
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
// TODO:
// inteegrate something like this
// https://github.com/Rezonans/redux-async-connect
-const need = (action) => (Component) => {
+const loading = (action) => (Component) => {
const Wrapper = React.createClass({
getInitialState() {
return {
@@ -16,9 +16,17 @@ const need = (action) => (Component) => {
const {
params,
location: { query },
+ dispatch,
} = this.props;
- this.props.action(params, query).then(() => this.setState({ loading: false }));
+ if (Array.isArray(action)) {
+ Promise.all(
+ action.map(oneAction => dispatch(oneAction(params, query)))
+ ).then(() => this.setState({ loading: false }));
+
+ } else {
+ dispatch(action(params, query)).then(() => this.setState({ loading: false }));
+ }
},
render() {
@@ -28,9 +36,7 @@ const need = (action) => (Component) => {
},
});
- return connect(null, {
- action,
- })(Wrapper);
+ return connect(null)(Wrapper);
};
-export default need;
+export default loading;
diff --git a/frontend/src/Stores/App.jsx b/frontend/src/Stores/App.jsx
new file mode 100644
index 0000000..556d40a
--- /dev/null
+++ b/frontend/src/Stores/App.jsx
@@ -0,0 +1,39 @@
+import axios from 'axios';
+import { observable, computed } from 'mobx';
+
+class App {
+ @observable users = [];
+ @computed get getUserOptions() {
+ return this.users.map(user => ({ value: user.id, label: user.name }));
+ }
+
+ @observable statuses = ['new', 'inprogress', 'testing', 'complited'];
+ @computed get getStatusesOptions() {
+ return this.statuses.map(str => ({ value: str, label: str }));
+ }
+
+ @observable currentUser = null;
+ @computed get userId() {
+ if (!this.currentUser) return null;
+
+ return this.currentUser.id;
+ }
+
+ appStart = () => {
+ return this.loadUsers();
+ }
+
+ loadUsers = () => {
+ return axios.get('/api/users')
+ .then(response => {
+ this.users.replace(response.data);
+ });
+ }
+
+ removeUser = ({ id }) => {
+ return axios.delete(`/api/users/${id}`)
+ .then(() => this.loadUsers());
+ }
+}
+
+export default App;
diff --git a/frontend/src/Stores/Auth.jsx b/frontend/src/Stores/Auth.jsx
new file mode 100644
index 0000000..15e7217
--- /dev/null
+++ b/frontend/src/Stores/Auth.jsx
@@ -0,0 +1,48 @@
+import axios from 'axios';
+import { observable, computed } from 'mobx';
+
+class App {
+ @observable user = null;
+ @computed get userId() {
+ if (!this.user) return null;
+
+ return this.user.id;
+ }
+
+ @computed get isAuthenticated() {
+ return !!this.user;
+ }
+
+
+ checkAuth = () => {
+ return axios.get('/api/session')
+ .then((response) => {
+ this.user = response.data;
+ })
+ .catch(() => this.logout());
+ }
+
+
+ login = (form) => {
+ return axios.post('/api/login', form)
+ .then(response => {
+ this.user = response.data;
+ window.location = '/';
+ });
+ }
+
+ registr = (form) => {
+ return axios.post('/api/users', form)
+ .then(response => this.login(response.data));
+ }
+
+ logout = () => {
+ return axios.delete('/api/session')
+ .then(() => {
+ this.user = null;
+ window.location = '/login';
+ });
+ }
+}
+
+export default App;
diff --git a/frontend/src/Stores/index.jsx b/frontend/src/Stores/index.jsx
new file mode 100644
index 0000000..07a392e
--- /dev/null
+++ b/frontend/src/Stores/index.jsx
@@ -0,0 +1,22 @@
+
+import App from 'Stores/App';
+import Auth from 'Stores/Auth';
+import TaskList from 'containers/TasksList/state';
+import TaskDetails from 'containers/TaskDetails/state';
+import TaskAdd from 'containers/TaskAdd/state';
+import ProjectsList from 'containers/ProjectsList/state';
+
+const createStores = (initState) => {
+ const stores = {
+ app: new App(),
+ taskList: new TaskList(),
+ taskDetails: new TaskDetails(),
+ taskAdd: new TaskAdd(),
+ projectsList: new ProjectsList(),
+ auth: new Auth(),
+ };
+
+ return stores;
+};
+
+export default createStores;
diff --git a/frontend/src/components/Layouts/App.jsx b/frontend/src/components/Layouts/App.jsx
new file mode 100644
index 0000000..a0936f5
--- /dev/null
+++ b/frontend/src/components/Layouts/App.jsx
@@ -0,0 +1,9 @@
+import React from 'react';
+
+const App = ({ children }) => (
+
+ {children}
+
+);
+
+export default App;
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/components/Layouts/Login.jsx
similarity index 58%
rename from frontend/src/pages/Login.jsx
rename to frontend/src/components/Layouts/Login.jsx
index ad13b81..624fc53 100644
--- a/frontend/src/pages/Login.jsx
+++ b/frontend/src/components/Layouts/Login.jsx
@@ -1,15 +1,14 @@
import React from 'react';
import { Grid, Row, Col } from 'react-bootstrap';
-import { LoginFormContainer, RegisterFormContainer } from 'containers/Login';
-const Login = () => (
+const Login = ({ left, right }) => (
-
+ {left}
-
+ {right}
diff --git a/frontend/src/components/Layouts/Main.jsx b/frontend/src/components/Layouts/Main.jsx
new file mode 100644
index 0000000..f3511eb
--- /dev/null
+++ b/frontend/src/components/Layouts/Main.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Grid, Col, Row } from 'react-bootstrap';
+
+const Main = ({ header, children }) => (
+
+ {header}
+
+
+
+ {children}
+
+
+
+
+);
+
+export default Main;
diff --git a/frontend/src/components/Layouts/index.css b/frontend/src/components/Layouts/index.css
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/components/Login/index.jsx b/frontend/src/components/Login/index.jsx
deleted file mode 100644
index dc416a5..0000000
--- a/frontend/src/components/Login/index.jsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-
-import { Button, Input } from 'react-bootstrap';
-import './index.css';
-
-const LoginForm = ({ login }) => {
- let email;
- let password;
- return (
-
- );
-};
-
-LoginForm.propTypes = {
- login: PropTypes.func,
-};
-
-class RegisterForm extends Component {
- static propTypes = {
- registr: PropTypes.func,
- }
-
- regist = () => {
- this.props.registr({
- email: this.refs.email.getValue(),
- password: this.refs.password.getValue(),
- name: this.refs.name.getValue(),
- });
- }
-
- render() {
- return (
-
- );
- }
-}
-
-
-export {
- RegisterForm,
- LoginForm,
-};
diff --git a/frontend/src/components/Menu/index.jsx b/frontend/src/components/Menu/index.jsx
deleted file mode 100644
index e58e45b..0000000
--- a/frontend/src/components/Menu/index.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React from 'react';
-import { Link } from 'react-router';
-import { Navbar, NavBrand, Nav, NavItem } from 'react-bootstrap';
-
-import { LinkContainer } from 'react-router-bootstrap';
-
-export const Menu = ({
- projectId,
- logout,
-}) => (
-
-
- Task-tracker
-
-
-
-
-);
-
-
-export const DashboardMenu = ({
- logout,
- addProject,
-}) => (
-
-
- Task-tracker
-
-
-
-
-);
diff --git a/frontend/src/components/Projects/index.jsx b/frontend/src/components/Projects/index.jsx
deleted file mode 100644
index f22813d..0000000
--- a/frontend/src/components/Projects/index.jsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import React from 'react';
-import { Button, Label, ListGroupItem, ListGroup, Modal, Input } from 'react-bootstrap';
-import { Link } from 'react-router';
-import Select from 'react-select';
-
-const ProductItem = ({ product, removeProject }) => {
- const users = (product.users || [])
- .map(user => );
-
- return (
-
-
-
- {product.title}
-
-
-
-
-
- {users}
-
-
-
- );
-};
-
-const ProjectsList = ({ openPopup, projects, removeProject }) => (
-
-
-
-
-
-
- {projects.map(product => (
- )
- )}
-
-
-);
-import 'react-select/dist/react-select.css';
-const LinkedStateMixin = require('react-addons-linked-state-mixin');
-
-const ProjectPopup = React.createClass({
- mixins: [LinkedStateMixin],
- getInitialState() {
- return {
- title: '',
- userIds: [],
- };
- },
- onChange(value) {
- this.setState({
- userIds: value,
- });
- },
-
- render() {
- const {
- users,
- addProject,
- } = this.props;
-
- const allUsers = users.map(user => (
- { value: user.id, label: user.name })
- );
- const create = () => {
- const {
- userIds,
- title,
- } = this.state;
- const ids = userIds.map(id => parseInt(id, 10));
- addProject(title, ids);
- };
-
- return (
-
-
-
- Team:
-
-
-
-
-
-
-
- );
- },
-});
-
-export {
- ProductItem,
- ProjectsList,
- ProjectPopup,
-};
diff --git a/frontend/src/components/Tasks/index.jsx b/frontend/src/components/Tasks/index.jsx
deleted file mode 100644
index 53c725f..0000000
--- a/frontend/src/components/Tasks/index.jsx
+++ /dev/null
@@ -1,128 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-
-import { Button, Input } from 'react-bootstrap';
-import { Link } from 'react-router';
-
-import { Table } from 'react-bootstrap';
-
-class AddComentForm extends Component {
-
- state = { value: '' }
-
- static propTypes = {
- addComment: PropTypes.func,
- }
-
- onChange = (e) => {
- this.setState({ value: e.target.value });
- }
-
- addComment = () => {
- this.props.addComment(this.state.value);
- this.setState({ value: '' });
- }
-
- render() {
- return (
-
-
-
- );
- }
-}
-
-
-const CommentsList = ({ comments, removeComent }) => (
-
- {comments.map(comment => (
-
-
- {comment.userName}
-
-
-
-
{comment.text}
-
-
- ))}
-
-);
-
-CommentsList.propTypes = {
- comments: PropTypes.array,
- removeComent: PropTypes.func,
-};
-
-
-const TaskInfo = ({ task: { title, description }, removeTask }) => (
-
-
-
{title}
-
- {description}
-
-
-
-
-);
-
-TaskInfo.propTypes = {
- task: PropTypes.object,
- removeTask: PropTypes.func,
-};
-
-
-const TaskTable = ({ tasks, projectId }) => (
-
-
-
- | Title |
- Status |
- Assignee |
- Version |
-
-
-
- {tasks.map(task => (
-
- |
- {task.title}
- |
-
- {task.status}
- |
-
- {task.assigneeName}
- |
-
- {task.version}
- |
-
- ))}
-
-
-);
-
-TaskTable.propTypes = {
- tasks: PropTypes.array,
- projectId: PropTypes.string,
-};
-
-export {
- AddComentForm,
- CommentsList,
- TaskInfo,
- TaskTable,
-};
diff --git a/frontend/src/components/Users/index.jsx b/frontend/src/components/Users/index.jsx
deleted file mode 100644
index a0dfe17..0000000
--- a/frontend/src/components/Users/index.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { Component } from 'react';
-import { Table, Button } from 'react-bootstrap';
-
-export class UsersTable extends Component {
- render() {
- const { users, removeUser, currentUser } = this.props;
- return (
-
-
All users
-
-
-
- | Name |
- Email |
- |
-
-
-
- {users.map(user => (
-
- |
- {user.name}
- |
-
- {user.email}
- |
-
-
-
-
- |
-
- ))}
-
-
-
- );
- }
-}
diff --git a/frontend/src/containers/Application/DashboardLayout/DashboardMenu.jsx b/frontend/src/containers/Application/DashboardLayout/DashboardMenu.jsx
new file mode 100644
index 0000000..e177784
--- /dev/null
+++ b/frontend/src/containers/Application/DashboardLayout/DashboardMenu.jsx
@@ -0,0 +1,30 @@
+
+import React from 'react';
+import { Link } from 'react-router';
+import { Navbar, NavBrand, Nav, NavItem } from 'react-bootstrap';
+
+import { LinkContainer } from 'react-router-bootstrap';
+
+import { observer } from 'mobx-react';
+
+const DashboardMenu = observer(['projectsList', 'auth'], ({
+ auth: { logout },
+ projectsList: { openPopup },
+}) => (
+
+
+ Task-tracker
+
+
+
+
+));
+
+export default DashboardMenu;
diff --git a/frontend/src/containers/Application/DashboardLayout/index.jsx b/frontend/src/containers/Application/DashboardLayout/index.jsx
new file mode 100644
index 0000000..f670c0b
--- /dev/null
+++ b/frontend/src/containers/Application/DashboardLayout/index.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import DashboardMenu from './DashboardMenu';
+import Main from 'components/Layouts/Main';
+
+const TasksLayout = ({ children }) => (
+ }
+ >
+ {children}
+
+);
+
+
+export default TasksLayout;
diff --git a/frontend/src/containers/Application/TasksLayout/TasksMenu.jsx b/frontend/src/containers/Application/TasksLayout/TasksMenu.jsx
new file mode 100644
index 0000000..8ff4772
--- /dev/null
+++ b/frontend/src/containers/Application/TasksLayout/TasksMenu.jsx
@@ -0,0 +1,38 @@
+
+import React from 'react';
+import { Link, withRouter } from 'react-router';
+import { Navbar, NavBrand, Nav, NavItem } from 'react-bootstrap';
+
+import { LinkContainer } from 'react-router-bootstrap';
+
+import { observer } from 'mobx-react';
+
+const TasksMenu = observer(['auth'], ({
+ projectId,
+ auth: { logout },
+}) => {
+ return (
+
+
+ Task-tracker
+
+
+
+
+ )
+});
+
+
+export default TasksMenu;//withRouter(TasksMenu);
+
diff --git a/frontend/src/containers/Application/TasksLayout/index.jsx b/frontend/src/containers/Application/TasksLayout/index.jsx
new file mode 100644
index 0000000..a1cfd48
--- /dev/null
+++ b/frontend/src/containers/Application/TasksLayout/index.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+
+import TasksMenu from './TasksMenu';
+import Main from 'components/Layouts/Main';
+
+const TasksLayout = ({ children, params: { projectId } }) => (
+ }
+ >
+ {children}
+
+);
+
+
+export default TasksLayout;
diff --git a/frontend/src/containers/Auth.jsx b/frontend/src/containers/Auth.jsx
deleted file mode 100644
index 3abe969..0000000
--- a/frontend/src/containers/Auth.jsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-//import { pushState } from 'redux-router';
-
-import { Menu } from 'components/Menu/';
-
-import { checkAuth, logout } from 'reduxApp/modules/auth';
-
-export const MenuContainer = connect(
- state => ({
- projectId: state.router.params.projectId,
- router: state.router,
- }),
- { logout }
-)(Menu);
-
-export function requireAuthentication(Component) {
- class AuthenticatedComponent extends React.Component {
- componentWillMount() {
- this.props.checkAuth();
- }
-
- // componentWillReceiveProps(nextProps) {
- // this.checkAuth();
- // }
-
- // checkAuth() {
- // if (!this.props.isAuthenticated) {
- // let redirectAfterLogin = this.props.location.pathname
- // this.props.dispatch(pushState(null, `/login?next=${redirectAfterLogin}`))
- // }
- // }
-
- render() {
-
- return (
-
- {this.props.isAuthenticated === true
- ?
- : null
- }
-
- );
- }
- }
-
-
- return connect(
- (state) => ({
- isAuthenticated: !!state.auth.user,
- }),
- { checkAuth }
- )(AuthenticatedComponent);
-};
diff --git a/frontend/src/containers/Login.jsx b/frontend/src/containers/Login.jsx
deleted file mode 100644
index 8088605..0000000
--- a/frontend/src/containers/Login.jsx
+++ /dev/null
@@ -1,21 +0,0 @@
-
-import { connect } from 'react-redux';
-import { login, registr } from 'reduxApp/modules/auth';
-
-import { LoginForm, RegisterForm } from 'components/Login/';
-
-const LoginFormContainer = connect(
- null,
- { login }
-)(LoginForm);
-
-
-const RegisterFormContainer = connect(
- null,
- { registr }
-)(RegisterForm);
-
-export {
- LoginFormContainer,
- RegisterFormContainer,
-};
diff --git a/frontend/src/components/Login/index.css b/frontend/src/containers/Login/LoginForm/index.css
similarity index 88%
rename from frontend/src/components/Login/index.css
rename to frontend/src/containers/Login/LoginForm/index.css
index f184bde..1d79e14 100644
--- a/frontend/src/components/Login/index.css
+++ b/frontend/src/containers/Login/LoginForm/index.css
@@ -1,5 +1,5 @@
-.login-form {
+.register-form {
padding: 10px;
border: 1px solid #ddd;
border-top: none;
diff --git a/frontend/src/containers/Login/LoginForm/index.jsx b/frontend/src/containers/Login/LoginForm/index.jsx
new file mode 100644
index 0000000..d72da43
--- /dev/null
+++ b/frontend/src/containers/Login/LoginForm/index.jsx
@@ -0,0 +1,43 @@
+
+import React, { PropTypes } from 'react';
+
+import { Button, Input } from 'react-bootstrap';
+import './index.css';
+
+
+import { observer } from 'mobx-react';
+
+const LoginForm = observer(['auth'], ({ auth: { login } }) => {
+ let email;
+ let password;
+ return (
+
+ );
+});
+
+LoginForm.propTypes = {
+ login: PropTypes.func,
+};
+
+export default LoginForm;
diff --git a/frontend/src/containers/Login/RegisterForm/index.css b/frontend/src/containers/Login/RegisterForm/index.css
new file mode 100644
index 0000000..1d79e14
--- /dev/null
+++ b/frontend/src/containers/Login/RegisterForm/index.css
@@ -0,0 +1,8 @@
+
+.register-form {
+ padding: 10px;
+ border: 1px solid #ddd;
+ border-top: none;
+ border-radius: 5px;
+ border-radius: 0 0 5px 5px;
+}
diff --git a/frontend/src/containers/Login/RegisterForm/index.jsx b/frontend/src/containers/Login/RegisterForm/index.jsx
new file mode 100644
index 0000000..66fbafd
--- /dev/null
+++ b/frontend/src/containers/Login/RegisterForm/index.jsx
@@ -0,0 +1,35 @@
+
+import React, { Component, PropTypes } from 'react';
+
+import { Button, Input } from 'react-bootstrap';
+import './index.css';
+
+import { observer } from 'mobx-react';
+
+@observer(['auth'])
+class RegisterForm extends Component {
+ static propTypes = {
+ registr: PropTypes.func,
+ }
+
+ regist = () => {
+ this.props.auth.registr({
+ email: this.refs.email.getValue(),
+ password: this.refs.password.getValue(),
+ name: this.refs.name.getValue(),
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default RegisterForm;
diff --git a/frontend/src/containers/Login/index.jsx b/frontend/src/containers/Login/index.jsx
new file mode 100644
index 0000000..b87d6a4
--- /dev/null
+++ b/frontend/src/containers/Login/index.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import LoginLayout from 'components/Layouts/Login';
+import LoginForm from './LoginForm/';
+import RegisterForm from './RegisterForm/';
+
+const Login = () => (
+ }
+ right={}
+ />
+);
+
+export default Login;
diff --git a/frontend/src/containers/NoMatch/index.jsx b/frontend/src/containers/NoMatch/index.jsx
new file mode 100644
index 0000000..478e6bc
--- /dev/null
+++ b/frontend/src/containers/NoMatch/index.jsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { Link } from 'react-router';
+
+import Main from 'components/Layouts/Main';
+
+const NoMatch = () => (
+
+ 404
+
+ back to app
+
+
+);
+
+export default NoMatch;
diff --git a/frontend/src/containers/ProjectsList.jsx b/frontend/src/containers/ProjectsList.jsx
deleted file mode 100644
index 09d0dd3..0000000
--- a/frontend/src/containers/ProjectsList.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import { ProjectsList, ProjectPopup } from 'components/Projects/';
-import { DashboardMenu } from 'components/Menu/';
-
-import {
- fetchProducts, removeProject,
- showAddProject,
- openPopup, closePopup, addProject, fetchUsers,
-} from 'reduxApp/modules/projects';
-import { logout } from 'reduxApp/modules/auth';
-
-
-const DashboardMenuContainer = connect(
- null,
- { addProject: showAddProject, logout }
-)(DashboardMenu);
-
-const ProjectsListContainer = connect(
- state => ({
- projects: state.projects.projects,
- }),
- { fetchProducts, removeProject, openPopup, fetchUsers }
-)(ProjectsList);
-
-const ProjectPopupContainer = connect(
- state => ({
- show: state.projects.popupOpen,
- users: state.users.users,
- }),
- { onHide: closePopup, addProject }
-)(ProjectPopup);
-
-export {
- DashboardMenuContainer,
- ProjectsListContainer,
- ProjectPopupContainer,
-};
diff --git a/frontend/src/containers/ProjectsList/AddProjectModal/index.jsx b/frontend/src/containers/ProjectsList/AddProjectModal/index.jsx
new file mode 100644
index 0000000..0798dd7
--- /dev/null
+++ b/frontend/src/containers/ProjectsList/AddProjectModal/index.jsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { Button, Label, ListGroupItem, ListGroup, Modal, Input } from 'react-bootstrap';
+import { Link } from 'react-router';
+import Select from 'react-select';
+
+
+import 'react-select/dist/react-select.css';
+const LinkedStateMixin = require('react-addons-linked-state-mixin');
+
+import { observer } from 'mobx-react';
+
+const AddProjectModal = observer(['projectsList', 'app'], React.createClass({
+ mixins: [LinkedStateMixin],
+ getInitialState() {
+ return {
+ title: '',
+ userIds: [],
+ };
+ },
+ onChange(value) {
+ this.setState({
+ userIds: value,
+ });
+ },
+
+ render() {
+ const {
+ addProject,
+ popupOpen,
+ closePopup,
+ } = this.props.projectsList;
+
+ const { users } = this.props.app;
+
+ const allUsers = users.map(user => (
+ { value: user.id, label: user.name })
+ );
+ const create = () => {
+ const {
+ userIds,
+ title,
+ } = this.state;
+ const ids = userIds.map(item => parseInt(item.value, 10));
+ addProject(title, ids);
+ };
+
+ return (
+
+
+
+ Team:
+
+
+
+
+
+
+
+ );
+ },
+}));
+
+
+export default AddProjectModal;
+
diff --git a/frontend/src/containers/ProjectsList/ProjectsList/index.jsx b/frontend/src/containers/ProjectsList/ProjectsList/index.jsx
new file mode 100644
index 0000000..3d60d29
--- /dev/null
+++ b/frontend/src/containers/ProjectsList/ProjectsList/index.jsx
@@ -0,0 +1,57 @@
+import React from 'react';
+import { Button, Label, ListGroupItem, ListGroup } from 'react-bootstrap';
+import { Link } from 'react-router';
+
+const ProductItem = ({ product, removeProject }) => {
+ const users = (product.users || [])
+ .map(user => );
+
+ return (
+
+
+
+ {product.title}
+
+
+
+
+
+ {users}
+
+
+
+ );
+};
+import { observer } from 'mobx-react';
+
+
+const ProjectsList = observer(['projectsList'], ({
+ projectsList: { projects, removeProject },
+}) => (
+
+
+
+
+
+
+ {projects.map(product => (
+ )
+ )}
+
+
+));
+
+export default ProjectsList;
+
diff --git a/frontend/src/containers/ProjectsList/index.jsx b/frontend/src/containers/ProjectsList/index.jsx
new file mode 100644
index 0000000..d499279
--- /dev/null
+++ b/frontend/src/containers/ProjectsList/index.jsx
@@ -0,0 +1,23 @@
+import React, { Component } from 'react';
+
+import ProjectsList from './ProjectsList/';
+import AddProjectModal from './AddProjectModal/';
+import { observer } from 'mobx-react';
+
+@observer(['projectsList'])
+class ProjectsListPage extends Component {
+ componentDidMount() {
+ this.props.projectsList.showPage();
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default ProjectsListPage;
diff --git a/frontend/src/containers/ProjectsList/state.jsx b/frontend/src/containers/ProjectsList/state.jsx
new file mode 100644
index 0000000..47ba727
--- /dev/null
+++ b/frontend/src/containers/ProjectsList/state.jsx
@@ -0,0 +1,41 @@
+
+import axios from 'axios';
+import { observable, action, computed } from 'mobx';
+
+class Store {
+ @observable projects = [];
+ @observable popupOpen = false;
+
+ showPage = () => {
+ this.getAllProjects();
+ }
+
+ openPopup = () => {
+ this.popupOpen = true;
+ }
+
+ closePopup = () => {
+ this.popupOpen = false;
+ }
+
+ addProject = (title, userIds) => {
+ return axios.post('/api/projects', { title })
+ .then(() => this.getAllProjects())
+ .then(() => this.closePopup());
+ }
+
+ removeProject = ({ id }) => {
+ return axios.delete(`/api/projects/${id}`)
+ .then(() => this.getAllProjects());
+ }
+
+ getAllProjects = () => {
+ return axios.get('/api/projects')
+ .then(response => {
+ this.projects.replace(response.data);
+ });
+ }
+}
+
+
+export default Store;
diff --git a/frontend/src/containers/TaskAdd/TaskAddForm/index.css b/frontend/src/containers/TaskAdd/TaskAddForm/index.css
new file mode 100644
index 0000000..4b15aa8
--- /dev/null
+++ b/frontend/src/containers/TaskAdd/TaskAddForm/index.css
@@ -0,0 +1,8 @@
+
+.task-add-from__status .Select {
+ z-index: 4;
+}
+
+.task-add-from__user-select .Select {
+ z-index: 3;
+}
diff --git a/frontend/src/pages/TasksLayout/TaskAdd.jsx b/frontend/src/containers/TaskAdd/TaskAddForm/index.jsx
similarity index 70%
rename from frontend/src/pages/TasksLayout/TaskAdd.jsx
rename to frontend/src/containers/TaskAdd/TaskAddForm/index.jsx
index 59dd0b0..ef446d3 100644
--- a/frontend/src/pages/TasksLayout/TaskAdd.jsx
+++ b/frontend/src/containers/TaskAdd/TaskAddForm/index.jsx
@@ -4,45 +4,45 @@ import { Button, Input } from 'react-bootstrap';
import 'react-select/dist/react-select.css';
import Select from 'react-select';
-class TaskAdd extends Component {
+import './index.css';
+
+import { observer } from 'mobx-react';
+
+@observer(['taskAdd', 'app'])
+class TaskAddForm extends Component {
constructor(props) {
super(props);
this.state = { status: 'new', assignee: props.assignee, version: 'week1' };
}
add() {
- const { addTask, users } = this.props;
+ const { getUserOptions } = this.props.app;
+ const { addTask } = this.props.taskAdd;
const title = this.refs.input.getValue();
const description = this.refs.description.getValue();
const { status, assignee, version } = this.state;
- const assigneeUser = users.find(user => user.id === assignee);
+ const assigneeUser = getUserOptions.find(user => user.value === assignee.value);
addTask({
title,
description,
status,
- assignee,
+ assignee: assignee.value,
version,
- assigneeName: assigneeUser.name,
+ assigneeName: assigneeUser.label,
});
}
render() {
- const { statuses, users, addVersion, versions } = this.props;
- const options = statuses.map(status => (
- { value: status, label: status }
- ));
-
- const allUsers = users.map(user => (
- { value: user.id, label: user.name })
- );
+ const { getStatusesOptions, getUserOptions } = this.props.app;
+ const { addVersion, versions } = this.props.taskAdd;
const allVersion = versions.map(version => ({ value: version.title, label: version.title }));
return (
-
+
-
+
@@ -89,16 +89,4 @@ class TaskAdd extends Component {
}
}
-import { connect } from 'react-redux';
-import { addTask, addVersion, showAddTaskForm } from 'reduxApp/modules/tasks';
-
-import need from 'utils/need';
-
-export default connect(
- state => ({
- statuses: state.tasks.statuses,
- users: state.users.users,
- versions: state.tasks.versions,
- assignee: state.auth.user ? state.auth.user.id : null,
- })
-, { addTask, addVersion })(need(showAddTaskForm)(TaskAdd));
+export default TaskAddForm;
diff --git a/frontend/src/containers/TaskAdd/index.jsx b/frontend/src/containers/TaskAdd/index.jsx
new file mode 100644
index 0000000..2a37e15
--- /dev/null
+++ b/frontend/src/containers/TaskAdd/index.jsx
@@ -0,0 +1,25 @@
+import React, { Component } from 'react';
+import TaskAddForm from './TaskAddForm/';
+
+import { observer } from 'mobx-react';
+
+@observer(['taskAdd'])
+class TaskAdd extends Component {
+ componentDidMount() {
+ const {
+ params,
+ taskAdd,
+ } = this.props;
+
+ taskAdd.showPage(params);
+ }
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+export default TaskAdd;
diff --git a/frontend/src/containers/TaskAdd/state.jsx b/frontend/src/containers/TaskAdd/state.jsx
new file mode 100644
index 0000000..0b8b660
--- /dev/null
+++ b/frontend/src/containers/TaskAdd/state.jsx
@@ -0,0 +1,35 @@
+import axios from 'axios';
+
+import { observable } from 'mobx';
+class Store {
+ @observable versions = [];
+ params = null;
+
+ showPage = (params) => {
+ this.params = params;
+ this.getAllVersions();
+ }
+
+ getAllVersions = () => {
+ return axios.get('/api/version')
+ .then(response => {
+ this.versions.replace(response.data);
+ });
+ }
+
+ addVersion = () => {
+ const title = prompt('Create new Version', '');
+ axios.post('/api/version', { title })
+ .then(() => this.getAllVersions());
+ }
+
+ addTask = (data) => {
+ const { projectId } = this.params;
+ axios.post('/api/tasks', data)
+ .then(() => {
+ window.location = `/projects/${projectId}/tasks`;
+ });
+ }
+}
+
+export default Store;
diff --git a/frontend/src/containers/TaskDetails/AddComentForm/index.jsx b/frontend/src/containers/TaskDetails/AddComentForm/index.jsx
new file mode 100644
index 0000000..d33fa69
--- /dev/null
+++ b/frontend/src/containers/TaskDetails/AddComentForm/index.jsx
@@ -0,0 +1,42 @@
+
+import React, { Component, PropTypes } from 'react';
+import { Button, Input } from 'react-bootstrap';
+
+import { observer } from 'mobx-react';
+
+@observer(['taskDetails'])
+class AddComentForm extends Component {
+
+ state = { value: '' }
+
+ static propTypes = {
+ addComment: PropTypes.func,
+ }
+
+ onChange = (e) => {
+ this.setState({ value: e.target.value });
+ }
+
+ addComment = () => {
+ this.props.taskDetails.addComment(this.props.taskId, this.state.value);
+ this.setState({ value: '' });
+ }
+
+ render() {
+ return (
+
+
+
+ );
+ }
+}
+
+export default AddComentForm;
diff --git a/frontend/src/containers/TaskDetails/CommentsList/index.jsx b/frontend/src/containers/TaskDetails/CommentsList/index.jsx
new file mode 100644
index 0000000..d6964db
--- /dev/null
+++ b/frontend/src/containers/TaskDetails/CommentsList/index.jsx
@@ -0,0 +1,35 @@
+
+import React, { PropTypes } from 'react';
+import { Button } from 'react-bootstrap';
+
+import { observer } from 'mobx-react';
+
+const CommentsList = observer(['taskDetails'], ({
+ taskDetails: { comments, deleteComment },
+ taskId,
+}) => (
+
+ {comments.map(comment => (
+
+
+ {comment.userName}
+
+
+
+
{comment.text}
+
+
+ ))}
+
+));
+
+CommentsList.propTypes = {
+ comments: PropTypes.array,
+ deleteComment: PropTypes.func,
+};
+
+export default CommentsList;
diff --git a/frontend/src/containers/TaskDetails/TaskInfo/index.jsx b/frontend/src/containers/TaskDetails/TaskInfo/index.jsx
new file mode 100644
index 0000000..7288fcc
--- /dev/null
+++ b/frontend/src/containers/TaskDetails/TaskInfo/index.jsx
@@ -0,0 +1,32 @@
+
+import React, { PropTypes } from 'react';
+import { Button } from 'react-bootstrap';
+
+import { observer } from 'mobx-react';
+
+const TaskInfo = observer(['taskDetails'], ({
+ taskDetails: { task: { title, description }, deleteTask },
+ projectId,
+}) => {
+ return (
+
+
+
{title}
+
+ {description}
+
+
+
+
+ );
+});
+
+
+TaskInfo.propTypes = {
+ task: PropTypes.object,
+ deleteTask: PropTypes.func,
+};
+
+export default TaskInfo;
+
+
diff --git a/frontend/src/containers/TaskDetails/index.jsx b/frontend/src/containers/TaskDetails/index.jsx
new file mode 100644
index 0000000..59d99c9
--- /dev/null
+++ b/frontend/src/containers/TaskDetails/index.jsx
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+
+import TaskInfo from './TaskInfo/';
+import AddComentForm from './AddComentForm/';
+import CommentsList from './CommentsList/';
+
+import { observer } from 'mobx-react';
+
+@observer(['taskDetails'])
+class TaskDetails extends Component {
+ state = {
+ loading: true,
+ }
+
+ componentDidMount() {
+ const {
+ params,
+ taskDetails,
+ } = this.props;
+
+ taskDetails.showPage(params).then(() => {
+ this.setState({ loading: false });
+ });
+ }
+
+ render() {
+ const {
+ params: { projectId, id },
+ } = this.props;
+
+ return (
+
+ {!this.state.loading &&
}
+
+ );
+ }
+}
+
+export default TaskDetails;
diff --git a/frontend/src/containers/TaskDetails/state.jsx b/frontend/src/containers/TaskDetails/state.jsx
new file mode 100644
index 0000000..f88907a
--- /dev/null
+++ b/frontend/src/containers/TaskDetails/state.jsx
@@ -0,0 +1,49 @@
+
+import axios from 'axios';
+
+import { observable } from 'mobx';
+class Store {
+ @observable task = null;
+ @observable comments = [];
+
+ showPage = ({ id }) => {
+ return Promise.all([
+ this.loadComments(id),
+ this.loadTask(id),
+ ]);
+ }
+
+ loadTask = (id) => {
+ return axios.get(`/api/tasks/${id}`)
+ .then(response => {
+ this.task = response.data;
+ });
+ }
+
+ deleteTask = (projectId) => {
+ const { id } = this.task;
+ axios.delete(`/api/tasks/${id}`)
+ .then(() => {
+ window.location = `/projects/${projectId}/tasks`;
+ });
+ }
+
+ addComment = (taskId, text) => {
+ axios.post(`/api/tasks/${taskId}/comments`, { text, userName: 'vasa' })
+ .then(() => this.loadComments(taskId));
+ }
+
+ loadComments = (taskId) => {
+ return axios.get(`/api/tasks/${taskId}/comments/page/0/5`)
+ .then((response) => {
+ this.comments.replace(response.data.items);
+ });
+ }
+
+ deleteComment = (taskId, comment) => {
+ axios.delete(`/api/tasks/${taskId}/comments/${comment.id}`)
+ .then(() => this.loadComments(taskId));
+ }
+}
+
+export default Store;
diff --git a/frontend/src/containers/TasksList/TaskFilter/StatusSelect.jsx b/frontend/src/containers/TasksList/TaskFilter/StatusSelect.jsx
new file mode 100644
index 0000000..0413a39
--- /dev/null
+++ b/frontend/src/containers/TasksList/TaskFilter/StatusSelect.jsx
@@ -0,0 +1,22 @@
+
+import React from 'react';
+import Select from 'react-select';
+
+import { observer } from 'mobx-react';
+
+export const StatusSelect = observer(['taskList', 'app'], ({
+ taskList: { status, changeStatus },
+ app: { getStatusesOptions },
+}) => {
+ console.log('render status');
+ return (
+