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 ( -
- { email = node; }} - defaultValue='joe@example.com' - type='text' - label='email' - /> - { password = node; }} - defaultValue='password1' - type='password' - label='password' - /> - -
- ); -}; - -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 }) => ( - - -
-
-
My projects
-
-
- -
- - {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: - - - - - ); - } -} - - -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 }) => ( - - - - - - - - - - - {tasks.map(task => ( - - - - - - - ))} - -
TitleStatusAssigneeVersion
- {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

- - - - - - - - - - {users.map(user => ( - - - - - - ))} - -
NameEmail
- {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 ( +
+ { email = node; }} + defaultValue='joe@example.com' + type='text' + label='email' + /> + { password = node; }} + defaultValue='password1' + type='password' + label='password' + /> + +
+ ); +}); + +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: + -
+
this.setState({ assignee })} placeholder='-' - options={allUsers} + options={getUserOptions} />
@@ -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 ( + changeUserId(newValue.value || null)} + searchable={false} + clearable={false} + options={getUserOptions.concat([{ value: '', label: 'All' }])} + /> + ); +}); + diff --git a/frontend/src/containers/TasksList/TaskFilter/index.jsx b/frontend/src/containers/TasksList/TaskFilter/index.jsx new file mode 100644 index 0000000..513ae81 --- /dev/null +++ b/frontend/src/containers/TasksList/TaskFilter/index.jsx @@ -0,0 +1,31 @@ + +import React from 'react'; + +import { Button, Grid, Col, Row } from 'react-bootstrap'; + +import { UserSelect } from './UserSelect'; +import { StatusSelect } from './StatusSelect'; +import { observer } from 'mobx-react'; + +export const TaskFilter = observer(['taskList'], ({ + taskList: { makeSearch }, +}) => { + return ( +
+ + + + + + + + + + +
+ +
+
+ ); +}); + diff --git a/frontend/src/containers/TasksList/TaskPager/index.jsx b/frontend/src/containers/TasksList/TaskPager/index.jsx new file mode 100644 index 0000000..822abe4 --- /dev/null +++ b/frontend/src/containers/TasksList/TaskPager/index.jsx @@ -0,0 +1,24 @@ +import React, { PropTypes } from 'react'; +import { Pagination } from 'react-bootstrap'; +import { observer } from 'mobx-react'; + +export const TaskPager = observer(['taskList'], ({ taskList: { items, onSelect, page } }) => { + console.log('render pager'); + return ( +
+ {items > 1 && onSelect(data.eventKey)} + />} +
+ ); +}); + +TaskPager.propTypes = { + items: PropTypes.number, + activePage: PropTypes.number, + onSelect: PropTypes.func, +}; + diff --git a/frontend/src/containers/TasksList/TaskTable/index.jsx b/frontend/src/containers/TasksList/TaskTable/index.jsx new file mode 100644 index 0000000..8ee28fa --- /dev/null +++ b/frontend/src/containers/TasksList/TaskTable/index.jsx @@ -0,0 +1,45 @@ +import React, { PropTypes } from 'react'; +import { Table } from 'react-bootstrap'; +import { Link } from 'react-router'; + +import { observer } from 'mobx-react'; + +export const TaskTable = observer(['taskList'], ({ taskList: { tasks }, projectId }) => { + console.log('render table'); + return ( + + + + + + + + + + + {tasks.map(task => ( + + + + + + + ))} + +
TitleStatusAssigneeVersion
+ {task.title} + + {task.status} + + {task.assigneeName} + + {task.version} +
+ ); +}); + +TaskTable.propTypes = { + tasks: PropTypes.array, + projectId: PropTypes.string, +}; + diff --git a/frontend/src/containers/TasksList/index.jsx b/frontend/src/containers/TasksList/index.jsx new file mode 100644 index 0000000..c55af9a --- /dev/null +++ b/frontend/src/containers/TasksList/index.jsx @@ -0,0 +1,34 @@ + +import React, { Component } from 'react'; + +import { TaskTable } from './TaskTable/'; + +import { TaskFilter } from './TaskFilter/'; +import { TaskPager } from './TaskPager/'; + +import { observer } from 'mobx-react'; + +@observer(['taskList']) +class TaskList extends Component { + + componentDidMount() { + this.props.taskList.showPage(); + } + + render() { + const { + params: { projectId }, + } = this.props; + + return ( +
+ + + +
+ ); + } +} + + +export default TaskList; diff --git a/frontend/src/containers/TasksList/state.jsx b/frontend/src/containers/TasksList/state.jsx new file mode 100644 index 0000000..91ee8c7 --- /dev/null +++ b/frontend/src/containers/TasksList/state.jsx @@ -0,0 +1,58 @@ + +const toParams = (obj) => Object.keys(obj) + .filter(key => !!obj[key]) + .map(key => `${key}=${obj[key]}`) + .join('&'); + + +import axios from 'axios'; +import { observable, action, computed } from 'mobx'; + +class Store { + @observable userId = null; + @observable status = 'new'; + + @observable tasks = []; + @observable page = 1; + @observable items = 0; + + @action showPage = () => { + const { + page, + userId, + status, + } = this; + + const query = toParams({ + assignee: userId, + status, + }); + return axios.get(`/api/tasks/page/${page}/5?${query}`).then(response => { + const { items, count } = response.data; + this.tasks.replace(items); + this.items = Math.ceil(count / 10); + }); + } + + onSelect = (page) => { + this.page = page; + this.showPage(); + } + + makeSearch = () => { + this.page = 1; + this.showPage(); + } + + changeUserId = (userId) => { + this.userId = userId; + } + + changeStatus = (status) => { + this.status = status; + } +} + + +export default Store; + diff --git a/frontend/src/containers/Test/index.jsx b/frontend/src/containers/Test/index.jsx new file mode 100644 index 0000000..e45a974 --- /dev/null +++ b/frontend/src/containers/Test/index.jsx @@ -0,0 +1,137 @@ +import React, { Component } from 'react'; +import axios from 'axios'; + +function rest(propsName, url) { + return (WComponent) => { + class Wrapper extends Component { + constructor(props) { + super(props); + this.state = { + items: [], + loading: true, + item: null, + }; + } + + featchOne = (id) => { + const _url = url(this.props.params); + this.setState({ loading: true }); + return axios.get(`${_url}/${id}`) + .then(response => response.data) + .then(item => this.setState({ item, loading: false })); + } + + featch = () => { + return axios.get(url(this.props.params)) + .then(response => { + this.setState({ items: response.data }); + }); + } + add = (data) => { + return axios.post(url(this.props.params), data) + .then(response => response.data) + .then(item => this.push(item)); + } + + push = (item) => { + const items = this.state.items.concat([item]); + this.setState({ items }); + return item; + } + + removeLocal = (item) => { + const items = this.state.items.filter(_item => _item.id !== item.id); + this.setState({ items }); + } + + remove = (item) => { + return axios.delete(`${url(this.props.params)}/${item.id}`) + .then(() => this.removeLocal(item)); + } + + componentDidMount() { + } + render() { + const { + items, + item, + loading, + } = this.state; + + const props = { + [propsName]: { + items, + featch: this.featch, + add: this.add, + featchOne: this.featchOne, + remove: this.remove, + loading, + item, + }, + ...this.props, + }; + + return ( + + ); + } + } + + return Wrapper; + }; +} + + +@rest('comments', ({ id }) => `/api/tasks/${id}/comments`) +@rest('tasks', ({ id }) => '/api/tasks') +class Test extends Component { + componentDidMount() { + this.props.comments.featch(); + this.props.tasks.featchOne(this.props.params.id); + } + + addProject = (e) => { + const text = this.refs.input.value; + this.props.comments.add({ text, userName: 'vasa' }); + this.refs.input.value = ''; + } + + render() { + const { + items, + remove, + } = this.props.comments; + + const { + item, + loading, + } = this.props.tasks; + + const rows = items.map(comment => ( +
+ {comment.text} + +
+ )); + + return ( +
+ {!loading &&
+

{item.title}

+
} +
+ {rows} +
+
+ + +
+
+ ); + } +} + +export default Test; + diff --git a/frontend/src/containers/Users.jsx b/frontend/src/containers/Users.jsx deleted file mode 100644 index c26d8b2..0000000 --- a/frontend/src/containers/Users.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { UsersTable } from 'components/Users/'; -import { removeUser } from 'reduxApp/modules/users'; - -const UsersTableContainer = connect( - state => ({ - users: state.users.users, - currentUser: state.auth.user, - }), - { removeUser } -)(UsersTable); - -export { - UsersTableContainer, -}; diff --git a/frontend/src/containers/UsersList/UsersTable/index.jsx b/frontend/src/containers/UsersList/UsersTable/index.jsx new file mode 100644 index 0000000..2221db8 --- /dev/null +++ b/frontend/src/containers/UsersList/UsersTable/index.jsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Table, Button } from 'react-bootstrap'; + +import { observer } from 'mobx-react'; + +export default observer(['app', 'auth'], ({ + app: { users, removeUser }, + auth: { userId }, +}) => { + return ( +
+

All users

+ + + + + + + + + + {users.map(user => ( + + + + + + ))} + +
NameEmail
+ {user.name} + + {user.email} + +
+ +
+
+
+ ); +}); + diff --git a/frontend/src/containers/UsersList/index.jsx b/frontend/src/containers/UsersList/index.jsx new file mode 100644 index 0000000..64146a5 --- /dev/null +++ b/frontend/src/containers/UsersList/index.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +import UsersTable from './UsersTable/'; + +const UsersList = () => ( +
+ +
+); + +export default UsersList; diff --git a/frontend/src/containers/tasks.jsx b/frontend/src/containers/tasks.jsx deleted file mode 100644 index 70cf221..0000000 --- a/frontend/src/containers/tasks.jsx +++ /dev/null @@ -1,39 +0,0 @@ - -import { connect } from 'react-redux'; -import { removeTask, addComment, removeComent } from 'reduxApp/modules/tasks'; - -import { AddComentForm, CommentsList, TaskInfo, TaskTable } from 'components/Tasks/'; - - -const TaskInfoContainer = connect( - state => ({ - task: state.tasks.task, - }), - { removeTask } -)(TaskInfo); - -const CommentsListContainer = connect( - state => ({ - comments: state.tasks.comments, - }), - { removeComent } -)(CommentsList); - -const AddComentFormContainer = connect( - null, - { addComment } -)(AddComentForm); - -const TaskTableContainer = connect( - state => ({ - tasks: state.tasks.tasks, - projectId: state.router.params.projectId, - }) -)(TaskTable); - -export { - TaskInfoContainer, - CommentsListContainer, - AddComentFormContainer, - TaskTableContainer, -}; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index e27e58b..f314add 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,34 +1,30 @@ - -import React, { PropTypes } from 'react'; +import React from 'react'; import { render } from 'react-dom'; -import { Provider } from 'react-redux'; -import { ReduxRouter } from 'redux-router'; -import createStore from './reduxApp/create.jsx'; +import { Provider } from 'mobx-react'; + +import createStores from 'Stores'; + +import { browserHistory, Router } from 'react-router'; const getRoutes = require('./routes'); -const Root = ({ store }) => ( - - - {getRoutes(store)} - - +const Root = ({ stores }) => ( +
+ + + {getRoutes()} + + +
); -Root.propTypes = { - store: PropTypes.object.isRequired, -}; - window.onload = () => { const root = document.getElementById('app'); - try { - render(( - - ), root); - } catch (e) { - const RedBox = require('redbox-react').default; - render(, root); - } + const stores = createStores(); + stores.app.appStart(); + render(( + + ), root); }; diff --git a/frontend/src/pages/.DS_Store b/frontend/src/pages/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/frontend/src/pages/.DS_Store and /dev/null differ diff --git a/frontend/src/pages/NoMatch.jsx b/frontend/src/pages/NoMatch.jsx deleted file mode 100644 index 69ba806..0000000 --- a/frontend/src/pages/NoMatch.jsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { Link } from 'react-router'; -import { Grid, Row, Col } from 'react-bootstrap'; - -const NoMatch = () => ( -
- - - -

404

-
- back to app -
- -
-
-
-); - -export default NoMatch; diff --git a/frontend/src/pages/ProjectsList.jsx b/frontend/src/pages/ProjectsList.jsx deleted file mode 100644 index be91822..0000000 --- a/frontend/src/pages/ProjectsList.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { - ProjectsListContainer, ProjectPopupContainer, DashboardMenuContainer, -} from 'containers/ProjectsList'; - -import { Grid, Row, Col } from 'react-bootstrap'; - -const ProjectsListPage = () => ( -
- - - - - - - - - -
-); - -import need from 'utils/need'; -import { loadProducts } from 'reduxApp/modules/projects'; - -export default need(loadProducts)(ProjectsListPage); diff --git a/frontend/src/pages/TasksLayout/TaskDetails.jsx b/frontend/src/pages/TasksLayout/TaskDetails.jsx deleted file mode 100644 index 2af9927..0000000 --- a/frontend/src/pages/TasksLayout/TaskDetails.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - - -import { TaskInfoContainer, CommentsListContainer, AddComentFormContainer } from 'containers/tasks'; - - -const TaskDetails = () => ( -
- - - -
-); - -import need from 'utils/need'; -import { loadTask } from 'reduxApp/modules/tasks'; - -export default need(loadTask)(TaskDetails); diff --git a/frontend/src/pages/TasksLayout/TaskList.jsx b/frontend/src/pages/TasksLayout/TaskList.jsx deleted file mode 100644 index 265c675..0000000 --- a/frontend/src/pages/TasksLayout/TaskList.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; - -import { TaskTableContainer } from 'containers/tasks'; - -const TaskList = () => ( -
- -
-); - -import need from 'utils/need'; -import { loadTasks } from 'reduxApp/modules/tasks'; - -export default need(loadTasks)(TaskList); diff --git a/frontend/src/pages/TasksLayout/index.jsx b/frontend/src/pages/TasksLayout/index.jsx deleted file mode 100644 index e546fc1..0000000 --- a/frontend/src/pages/TasksLayout/index.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { MenuContainer } from 'containers/Auth'; -import { Grid, Col, Row } from 'react-bootstrap'; - -import TaskAdd from './TaskAdd'; -import TaskDetails from './TaskDetails'; -import TaskList from './TaskList'; - -const Layout = ({ header, main }) => ( -
- {header} - - - - {main} - - - -
-); - - -const TasksLayout = ({ children }) => ( - } - main={children} - /> -); - - -export { - TasksLayout, - TaskAdd, - TaskDetails, - TaskList, -}; diff --git a/frontend/src/pages/Test.jsx b/frontend/src/pages/Test.jsx deleted file mode 100644 index 81b9edd..0000000 --- a/frontend/src/pages/Test.jsx +++ /dev/null @@ -1,137 +0,0 @@ -import React, { Component } from 'react'; - -import { observable, computed, action } from 'mobx'; -import { observer } from "mobx-react"; - -const nextId = () => (new Date).getTime(); - - -class TodoList { - @observable todoList = []; - - @observable filter = 'all'; - - @action - changeFilter(newFilter) { - this.filter = newFilter; - } - - @computed get visableItems() { - const filter = this.filter; - if (filter === 'complieted') return this.todoList.filter(todo => !!todo.complieted); - if (filter === 'notComplieted') return this.todoList.filter(todo => !todo.complieted); - - return this.todoList; - } - - @action - addTodo(title) { - this.todoList.push({ title, id: nextId(), complieted: false }); - } - - @action removeTodo(todo) { - this.todoList.remove(todo); - } - - @action - toogleTodo(todo) { - todo.complieted = !todo.complieted; - } -} - -const todoList = new TodoList(); -todoList.addTodo('test1'); -todoList.addTodo('test2'); -todoList.addTodo('test3'); - -// const todoList = observable([ -// { -// title: 'lear mobx', -// id: 1, -// complieted: false, -// }, -// { -// title: 'write project with mobx', -// id: 2, -// complieted: false, -// }, -// ]); - - -// const addTodo = action((title) => { -// todoList.push({ title: title, id: nextId(), complieted: false }); -// }); - -// const removeTodo = action((todo) => { -// todoList.remove(todo); -// }); - -// const toogleTodo = action((todo) => { -// todo.complieted = !todo.complieted; -// }); - -const ListItem = observer(({ item, remove, toogle }) => { - return ( -
- toogle(item)}>{item.title} - -
- ); -}); - -const List = observer(({ todoList: { visableItems }, remove, toogle }) => { - return ( -
- {visableItems.map(item => ( - - ))} -
- ); -}); - -const AddForm = ({ add }) => { - let input; - - return ( -
- input = node} /> - -
- ); -}; - -const Filter = observer(({ todoList: { filter }, changeFilter }) => { - console.log('filter>', filter) - return ( -
-
changeFilter('all')}>all
-
changeFilter('complieted')}>complieted
-
changeFilter('notComplieted')}>not complited
-
- ); -}); - -import './index.css'; - -export default class Test extends Component { - render() { - return ( -
- todoList.changeFilter(filter)} - /> - todoList.removeTodo(item)} - toogle={todoList.toogleTodo} - /> - todoList.addTodo(title)} /> -
- ); - } -} diff --git a/frontend/src/pages/UsersList.jsx b/frontend/src/pages/UsersList.jsx deleted file mode 100644 index b4f0f5b..0000000 --- a/frontend/src/pages/UsersList.jsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -import { - DashboardMenuContainer, -} from 'containers/ProjectsList'; -import { Grid, Row, Col } from 'react-bootstrap'; -import { UsersTableContainer } from 'containers/Users'; - -const UsersList = () => ( -
- - - - - - - - -
-); - -import need from 'utils/need'; -import { fetchUsers } from 'reduxApp/modules/users'; -export default need(fetchUsers)(UsersList); diff --git a/frontend/src/pages/index.css b/frontend/src/pages/index.css deleted file mode 100644 index dcd996c..0000000 --- a/frontend/src/pages/index.css +++ /dev/null @@ -1,4 +0,0 @@ - -.active { - background: red; -} diff --git a/frontend/src/reduxApp/create.jsx b/frontend/src/reduxApp/create.jsx deleted file mode 100644 index 0003a8a..0000000 --- a/frontend/src/reduxApp/create.jsx +++ /dev/null @@ -1,26 +0,0 @@ - -import { reduxReactRouter } from 'redux-router'; - -import useScroll from 'scroll-behavior'; -import { createHistory } from 'history'; - -import { applyMiddleware, compose, createStore as _createStore } from 'redux'; -import thunk from 'redux-thunk'; -import rootReducer from './modules/reducer'; - -export default function createStore() { - const middleware = applyMiddleware(thunk); - - const createHistoryCustom = (options) => useScroll(createHistory({ - queryKey: false, ...options, - })); - - const createStoreWithMiddleware = compose( - middleware, - reduxReactRouter({ createHistory: createHistoryCustom }) - ); - - const _store = createStoreWithMiddleware(_createStore)(rootReducer); - - return _store; -} diff --git a/frontend/src/reduxApp/middleware/errors.jsx b/frontend/src/reduxApp/middleware/errors.jsx deleted file mode 100644 index 479a90a..0000000 --- a/frontend/src/reduxApp/middleware/errors.jsx +++ /dev/null @@ -1 +0,0 @@ -// TODO: add try catch and last log actions diff --git a/frontend/src/reduxApp/modules/auth.jsx b/frontend/src/reduxApp/modules/auth.jsx deleted file mode 100644 index 80603dc..0000000 --- a/frontend/src/reduxApp/modules/auth.jsx +++ /dev/null @@ -1,65 +0,0 @@ - -const initState = { - user: null, -}; - -export const LOGIN = 'LOGIN'; - -export function reducer(state = initState, action) { - switch (action.type) { - case LOGIN: - return state; - - case 'START_SESSION': - return { ...state, user: action.payload }; - - default: - return state; - } -} - -import http from 'utils/http'; - -import { push } from 'redux-router'; - - -export function startSession(payload) { - return { - type: 'START_SESSION', - payload, - }; -} - -export function logout() { - return (dispatch) => { - http.del('/api/session').then(() => { - dispatch({ type: 'LOGOUT' }); - dispatch(push('/login')); - }); - }; -} - -export function login(form) { - return (dispatch) => http.post('/api/login', form) - .then(user => { - dispatch(startSession(user)); - dispatch(push('/')); - }); -} - -export function registr(form) { - return (dispatch) => http.post('/api/users', form) - .then(user => { - dispatch(login(form)); - }); -} - - -export function checkAuth() { - return (dispatch) => { - http.get('/api/session') - .then(user => dispatch(startSession(user))) - .fail(() => dispatch(logout())); - }; -} -// TODO diff --git a/frontend/src/reduxApp/modules/projects.jsx b/frontend/src/reduxApp/modules/projects.jsx deleted file mode 100644 index d58a08f..0000000 --- a/frontend/src/reduxApp/modules/projects.jsx +++ /dev/null @@ -1,93 +0,0 @@ - -const initState = { - projects: [], - popupOpen: false, -}; - -export function reducer(state = initState, action) { - switch (action.type) { - case 'FETCH_PRODUCTS': - return { ...state, projects: action.payload }; - - case 'REMOVE_PROJECT': { - const projects = state.projects.filter(product => product.id !== action.payload.id); - return { ...state, projects }; - } - - case 'PROJECTS_OPEN_POPUP': - return { ...state, popupOpen: true }; - - case 'PROJECTS_CLOSE_POPUP': - return { ...state, popupOpen: false }; - - case 'ADD_PROJECT': { - const projects = state.projects.concat([action.payload]); - return { ...state, projects }; - } - - default: - return state; - } -} - -import http from 'utils/http'; -import { push } from 'redux-router'; -import { fetchUsers } from './users'; - -export function loadProducts() { - return (dispatch) => { - const users = dispatch(fetchUsers()); - const projects = http.get('/api/projects') - .then(payload => dispatch({ - type: 'FETCH_PRODUCTS', - payload, - })); - - return Promise.all([projects, users]); - }; -} - - -export function openPopup() { - return { - type: 'PROJECTS_OPEN_POPUP', - }; -} - -export function closePopup() { - return { - type: 'PROJECTS_CLOSE_POPUP', - }; -} - - -export function removeProject(product) { - return (dispatch) => - http.del(`/api/projects/${product.id}`) - .then(() => dispatch({ - type: 'REMOVE_PROJECT', - payload: product, - })); -} - -export function showAddProject() { - return (dispatch) => { - dispatch(push('/')); - dispatch(openPopup()); - }; -} - -export function addProject(title, userIds) { - return (dispatch) => { - http.post('/api/projects', { title }) - // .then(project => http.post(`/api/projects/${project.id}/users`, { users: userIds })) - .then(payload => { - dispatch({ - type: 'ADD_PROJECT', - payload, - }); - dispatch(closePopup()); - // this.setState({ title : '', userIds : [] }) - }); - }; -} diff --git a/frontend/src/reduxApp/modules/reducer.jsx b/frontend/src/reduxApp/modules/reducer.jsx deleted file mode 100644 index 5a5a34a..0000000 --- a/frontend/src/reduxApp/modules/reducer.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { combineReducers } from 'redux'; -import { routerStateReducer } from 'redux-router'; -import { reducer as formReducer } from 'redux-form'; - -import { reducer as auth } from './auth'; -import { reducer as projects } from './projects'; -import { reducer as tasks } from './tasks'; -import { reducer as users } from './users'; - -export default combineReducers({ - users, - projects, - tasks, - auth, - router: routerStateReducer, - form: formReducer, -}); diff --git a/frontend/src/reduxApp/modules/tasks.jsx b/frontend/src/reduxApp/modules/tasks.jsx deleted file mode 100644 index c7868e9..0000000 --- a/frontend/src/reduxApp/modules/tasks.jsx +++ /dev/null @@ -1,136 +0,0 @@ - -const initState = { - tasks: [], - comments: [], - task: null, - statuses: ['new', 'inprogress', 'testing', 'complited'], - versions: [], -}; - -export function reducer(state = initState, action) { - switch (action.type) { - case 'SET_TASKS': - return { ...state, tasks: action.payload }; - - case 'SET_TASK': - return { ...state, task: action.payload }; - - case 'SET_COMMENTS': - return { ...state, comments: action.payload }; - - case 'REMOVE_COMMENTS': { - const comments = state.comments.filter(coment => coment.id !== action.payload.id); - return { ...state, comments }; - } - case 'SET_VERSIONS': - return { ...state, versions: action.payload }; - - case 'ADD_VERSION': - return { ...state, versions: state.versions.concat([action.payload]) }; - - default: - return state; - } -} - -import { fetchUsers } from 'reduxApp/modules/users'; - -import { push } from 'redux-router'; -import http from 'utils/http'; - -function setTasks(payload) { - return { - type: 'SET_TASKS', - payload, - }; -} - -function setTask(payload) { - return { - type: 'SET_TASK', - payload, - }; -} - -function setComments(payload) { - return { - type: 'SET_COMMENTS', - payload, - }; -} - -export function loadComments(taskId) { - return (dispatch) => { - return http.get(`/api/tasks/${taskId}/comments`).then(json => dispatch(setComments(json))); - }; -} - -export function loadTasks() { - return (dispatch) => { - return http.get('/api/tasks').then(json => dispatch(setTasks(json))); - }; -} - - -export function addTask(form) { - return (dispatch, getState) => { - const { router: { params: { projectId } } } = getState(); - http.post('/api/tasks', form).then(() => dispatch(push(`/projects/${projectId}/tasks`))); - }; -} - -export function removeTask() { - return (dispatch, getState) => { - const { router: { params: { id, projectId } } } = getState(); - http.del(`/api/tasks/${id}`).then(() => dispatch(push(`/projects/${projectId}/tasks`))); - }; -} - -export function loadTask() { - return (dispatch, getState) => { - const { router: { params: { id } } } = getState(); - const taskRequest = http.get(`/api/tasks/${id}`).then(data => dispatch(setTask(data))); - const commentsRequest = dispatch(loadComments(id)); - - return Promise.all([taskRequest, commentsRequest]); - }; -} - -export function addComment(text) { - return (dispatch, getState) => { - const { - auth: { user: { name } }, - router: { params: { id } }, - } = getState(); - http.post(`/api/tasks/${id}/comments`, { text, userName: name }) - .then(() => dispatch(loadComments(id))); - }; -} - -export function removeComent(comment) { - return (dispatch, getState) => { - const { router: { params: { id } } } = getState(); - return http.del(`/api/tasks/${id}/comments/${comment.id}`) - .then(() => dispatch({ type: 'REMOVE_COMMENTS', payload: comment })); - }; -} - -export function addVersion() { - return (dispatch) => { - const title = prompt('Create new Version', ''); - if (title) { - http.post('/api/version', { title }) - .then(newVersion => dispatch({ type: 'ADD_VERSION', payload: newVersion })); - } - }; -} - -export function showAddTaskForm() { - return (dispatch) => { - const users = dispatch(fetchUsers()); - const versions = http.get('/api/version') - .then(data => dispatch({ type: 'SET_VERSIONS', payload: data })); - return Promise.all([users, versions]); - }; -} - diff --git a/frontend/src/reduxApp/modules/users.jsx b/frontend/src/reduxApp/modules/users.jsx deleted file mode 100644 index 992186d..0000000 --- a/frontend/src/reduxApp/modules/users.jsx +++ /dev/null @@ -1,36 +0,0 @@ - -const initState = { - users: [], -}; - -export function reducer(state = initState, action) { - switch (action.type) { - case 'FETCH_USERS': - return { ...state, users: action.payload }; - - case 'REMOVE_USER': { - const users = state.users.filter(user => user.id !== action.payload.id); - return { ...state, users }; - } - - default: - return state; - } -} - -import http from 'utils/http'; - -export function fetchUsers() { - return (dispatch) => http.get('/api/users') - .then(payload => dispatch({ - type: 'FETCH_USERS', - payload, - })); -} - -export function removeUser(user) { - return (dispatch) => http.del(`/api/users/${user.id}`) - .then(() => { - dispatch({ type: 'REMOVE_USER', payload: user }); - }); -} diff --git a/frontend/src/routes.jsx b/frontend/src/routes.jsx index 0917f84..84c27f9 100644 --- a/frontend/src/routes.jsx +++ b/frontend/src/routes.jsx @@ -1,44 +1,40 @@ import React from 'react'; -import { Router, Route, IndexRoute } from 'react-router'; +import { Route, IndexRoute } from 'react-router'; -import Login from 'pages/Login'; +import Login from 'containers/Login/'; +import ProjectsList from 'containers/ProjectsList/'; +import { requireAuthentication } from 'HOC/AuthenticatedComponent'; +import UsersList from 'containers/UsersList/'; +import NoMatch from 'containers/NoMatch/'; +import TasksLayout from 'containers/Application/TasksLayout/'; +import DashboardLayout from 'containers/Application/DashboardLayout/'; +import TasksList from 'containers/TasksList/'; +import TaskAdd from 'containers/TaskAdd/'; +import TaskDetails from 'containers/TaskDetails'; +import Test from 'containers/Test/'; +import App from 'components/Layouts/App'; -import ProjectsList from 'pages/ProjectsList'; -import { requireAuthentication } from 'containers/Auth'; -import UsersList from 'pages/UsersList'; -import NoMatch from 'pages/NoMatch'; -import { TasksLayout, TaskAdd, TaskDetails, TaskList } from 'pages/TasksLayout'; -import Test from 'pages/Test'; -const App = ({ children }) => ( +module.exports = () => (
- {children} + + + + + + + + + + + + + + + + + +
); - - - -module.exports = ({ dispatch, getState }) => { - return ( -
- - - - - - - - - - - - - - - - - -
- ); -}; diff --git a/frontend/src/utils/http.jsx b/frontend/src/utils/http.jsx deleted file mode 100644 index 587068e..0000000 --- a/frontend/src/utils/http.jsx +++ /dev/null @@ -1,39 +0,0 @@ -// TODO: axios - -var $ = require('jquery'); - - - -var get = function(url){ - return $.ajax({ - url : url, - type : 'GET', - dataType : 'json', - cache : false, - contentType:"application/json" - }) -} - -var post = function(url, data){ -// return $.post(url, data); - return $.ajax({ - url : url, - type : 'POST', - contentType:"application/json", - data : JSON.stringify(data) - }) -} - -var del = function(url){ - return $.ajax({ - url : url, - type : 'DELETE' - }) -} - - -module.exports = { - get : get, - post : post, - del : del -} diff --git a/test/README.md b/test/README.md deleted file mode 100644 index 839ffa9..0000000 --- a/test/README.md +++ /dev/null @@ -1,10 +0,0 @@ - *test* - - - - *test* - -- ещё один элемент ненумерованного списка -- ещё один элемент ненумерованного списка -- ещё один элемент ненумерованного списка -- ещё один элемент ненумерованного списка \ No newline at end of file