├── .gitignore
├── src
├── components
│ ├── IssueCommentList.scss
│ ├── IssueNewHeader.scss
│ ├── IssueDescription.scss
│ ├── IssueList.scss
│ ├── IssueListItem.scss
│ ├── IssueDescription.js
│ ├── IssueListHeader.scss
│ ├── IssueCommentList.js
│ ├── IssueListItem.js
│ ├── IssueList.js
│ ├── SelectModal.js
│ ├── IssueCommentForm.scss
│ ├── IssueCommentListItem.scss
│ ├── IssueNewHeader.js
│ ├── IssueDetailHeader.scss
│ ├── IssueCommentListItem.js
│ ├── IssueCommentForm.js
│ ├── IssueListHeader.js
│ └── IssueDetailHeader.js
├── containers
│ ├── IssueListContainer.scss
│ ├── IssueNewContainer.scss
│ ├── IssueDetailContainer.scss
│ ├── IssueContainer.js
│ ├── IssueNewContainer.js
│ ├── IssueListContainer.js
│ └── IssueDetailContainer.js
├── lib
│ ├── records
│ │ ├── IssueListManager.js
│ │ ├── IssueManager.js
│ │ ├── IssueNewManager.js
│ │ ├── IssueDetailManager.js
│ │ ├── User.js
│ │ ├── Label.js
│ │ ├── Comment.js
│ │ └── Issue.js
│ ├── constants
│ │ └── EndPoints.js
│ └── utils
│ │ └── nl2br.js
├── reducers
│ ├── issueApp.js
│ └── issue.js
├── stores
│ └── configureIssueStore.js
├── entries
│ └── issue.js
└── actions
│ ├── issueNew.js
│ ├── issue.js
│ └── issueDetail.js
├── server.babel.js
├── public
└── index.html
├── .babelrc
├── README.md
├── webpack-dev-server.babel.js
├── webpack.config.babel.js
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/src/components/IssueCommentList.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | margin-top: 10px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/components/IssueNewHeader.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | display: block;
3 | }
4 |
--------------------------------------------------------------------------------
/src/containers/IssueListContainer.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | margin: 50px 50px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/containers/IssueNewContainer.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | margin: 50px 50px;
3 | }
4 |
--------------------------------------------------------------------------------
/src/containers/IssueDetailContainer.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | margin: 50px 100px;
3 | }
4 |
5 | .main {
6 | padding: 30px 100px;
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/records/IssueListManager.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable'
2 |
3 | const _IssueListManager = Record({
4 | loading: false,
5 | })
6 |
7 | export default class IssueListManager extends _IssueListManager {
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib/records/IssueManager.js:
--------------------------------------------------------------------------------
1 | import { List, Record } from 'immutable'
2 |
3 | const _IssueManager = Record({
4 | users: new List(),
5 | labels: new List(),
6 | })
7 |
8 | export default class IssueManager extends _IssueManager {
9 | }
10 |
--------------------------------------------------------------------------------
/src/reducers/issueApp.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { routerReducer } from 'react-router-redux'
3 |
4 | import issue from './issue'
5 |
6 | export default combineReducers({
7 | issue,
8 | routing: routerReducer,
9 | })
10 |
--------------------------------------------------------------------------------
/src/lib/records/IssueNewManager.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable'
2 | import Issue from './Issue'
3 |
4 | const _IssueNewManager = Record({
5 | loading: false,
6 | issue: new Issue(),
7 | })
8 |
9 | export default class IssueNewManager extends _IssueNewManager {
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/IssueDescription.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | border: 1px solid DarkGray;
3 | border-radius: 3px;
4 | margin-bottom: 30px;
5 | }
6 |
7 | .header {
8 | background-color: LightGray;
9 | padding: 10px 10px;
10 | font-weight: 600;
11 | }
12 |
13 | .main {
14 | padding: 10px 10px;
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/records/IssueDetailManager.js:
--------------------------------------------------------------------------------
1 | import { Record } from 'immutable'
2 |
3 | const _IssueDetailManager = Record({
4 | isTitleEditing: false,
5 | loading: false,
6 | showUsersModal: false,
7 | showLabelsModal: false,
8 | })
9 |
10 | export default class IssueDetailManager extends _IssueDetailManager {
11 | }
12 |
--------------------------------------------------------------------------------
/server.babel.js:
--------------------------------------------------------------------------------
1 | import express from 'express'
2 | import path from 'path'
3 |
4 | const app = express()
5 | const root = path.join(__dirname, 'public')
6 |
7 | app.use('/', express.static(root))
8 |
9 | app.get('*', (req, res, _next) => {
10 | res.sendFile('index.html', { root })
11 | })
12 |
13 | app.listen(process.env.PORT || 8000)
14 |
--------------------------------------------------------------------------------
/src/lib/records/User.js:
--------------------------------------------------------------------------------
1 | import { List, Record } from 'immutable'
2 |
3 | const _User = Record({
4 | id: null,
5 | name: '',
6 | })
7 |
8 | export default class User extends _User {
9 | static fromJS(user = {}) {
10 | return (new this).merge({
11 | id: parseInt(user.id),
12 | name: user.name,
13 | })
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/constants/EndPoints.js:
--------------------------------------------------------------------------------
1 | export const API_HOSTS = {
2 | REACT_REDUX_SOKUSHU_API: 'https://react-redux-sokushu-api.herokuapp.com',
3 | }
4 |
5 | const END_POINTS = {
6 | ISSUES: `${API_HOSTS.REACT_REDUX_SOKUSHU_API}/issues`,
7 | USERS: `${API_HOSTS.REACT_REDUX_SOKUSHU_API}/users`,
8 | LABELS: `${API_HOSTS.REACT_REDUX_SOKUSHU_API}/labels`,
9 | }
10 |
11 | export default END_POINTS
12 |
--------------------------------------------------------------------------------
/src/lib/records/Label.js:
--------------------------------------------------------------------------------
1 | import { List, Record } from 'immutable'
2 |
3 | const _Label = Record({
4 | id: null,
5 | name: '',
6 | color_code: '',
7 | })
8 |
9 | export default class Label extends _Label {
10 | static fromJS(label = {}) {
11 | return (new this).merge({
12 | id: parseInt(label.id),
13 | name: label.name,
14 | color_code: label.color_code,
15 | })
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/lib/utils/nl2br.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | const newlineRegex = /(\r\n|\n\r|\r|\n)/g
3 |
4 | export default function nl2br(str) {
5 | if (typeof str !== 'string') {
6 | return ''
7 | }
8 |
9 | return str.split(newlineRegex).map((line, index) => {
10 | if (line.match(newlineRegex)) {
11 | return React.createElement('br', { key: index })
12 | } else {
13 | return line
14 | }
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Issues PRACTICE
9 |
10 |
11 |
12 | Issues index.html PRICTICE
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "stage-3", "react"],
3 | "plugins": [
4 | ["react-transform", {
5 | "transforms": [{
6 | "transform": "react-transform-hmr",
7 | "imports": ["react"],
8 | "locals": ["module"],
9 | }, {
10 | "transform": "react-transform-catch-errors",
11 | "imports": [
12 | "react",
13 | "redbox-react",
14 | ],
15 | }],
16 | }],
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/IssueList.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | border-bottom: 1px solid DarkGray;
3 | }
4 |
5 | .header {
6 | display: flex;
7 | background: LightGray;
8 | justify-content: center;
9 | align-items: center;
10 | }
11 |
12 | .base-row {
13 | align-items: center;
14 | text-align: center;
15 | }
16 |
17 | .row {
18 | composes: base-row;
19 | flex: 1;
20 | }
21 |
22 | .row-2 {
23 | composes: base-row;
24 | flex: 2;
25 | }
26 |
27 | .row-3 {
28 | composes: base-row;
29 | flex: 3;
30 | }
31 |
--------------------------------------------------------------------------------
/src/lib/records/Comment.js:
--------------------------------------------------------------------------------
1 | import { List, Record } from 'immutable'
2 |
3 | const _Comment = Record({
4 | id: null,
5 | userName: '',
6 | content: '',
7 | created: '',
8 | updated: '',
9 | })
10 |
11 | export default class Comment extends _Comment {
12 | static fromJS(comment = {}) {
13 | return (new this).merge({
14 | id: comment.id,
15 | userName: comment.user_name || comment.userName,
16 | content: comment.content,
17 | created: comment.created,
18 | updated: comment.updated,
19 | })
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/stores/configureIssueStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux'
2 | import thunk from 'redux-thunk'
3 | import reducer from '../reducers/issueApp'
4 |
5 |
6 | export default function configureStore(_initialData) {
7 | const store = createStore(
8 | reducer,
9 | undefined,
10 | // Middlewares
11 | compose(
12 | applyMiddleware(thunk)
13 | )
14 | )
15 |
16 | module.hot.accept('../reducers/issueApp', () => {
17 | store.replaceReducer(require('../reducers/issueApp').default)
18 | })
19 |
20 | return store
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/IssueListItem.scss:
--------------------------------------------------------------------------------
1 | .outer {
2 | :hover {
3 | background: Azure;
4 | cursor: pointer;
5 | }
6 | }
7 |
8 | .base {
9 | display: flex;
10 | flex-flow: row nowrap;
11 | justify-content: center;
12 | border-bottom: 1px solid LightGray;
13 | padding: 10px 0;
14 | }
15 |
16 | .base-row {
17 | align-items: center;
18 | text-align: center;
19 | }
20 |
21 | .row {
22 | composes: base-row;
23 | flex: 1;
24 | }
25 |
26 | .row-2 {
27 | composes: base-row;
28 | flex: 2;
29 | }
30 |
31 | .row-3 {
32 | composes: base-row;
33 | flex: 3;
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/IssueDescription.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import styles from './IssueDescription.scss'
5 |
6 | class IssueDescription extends Component {
7 | render() {
8 | const { issue } = this.props
9 |
10 | return (
11 |
12 |
13 | issue description
14 |
15 |
16 | { issue.content }
17 |
18 |
19 | )
20 | }
21 | }
22 |
23 | export default CSSModules(IssueDescription, styles)
24 |
--------------------------------------------------------------------------------
/src/components/IssueListHeader.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | display: flex;
3 | justify-content: center;
4 | margin-bottom: 10px;
5 | }
6 |
7 | .left {
8 | margin-right: auto;
9 | }
10 |
11 | .right {
12 | padding: 0
13 | }
14 |
15 | .item {
16 | margin-right: 10px;
17 | cursor: pointer;
18 | text-decoration: none;
19 |
20 | &:hover {
21 | text-decoration: underline;
22 | }
23 | }
24 |
25 | .modal-item {
26 | text-decoration: none;
27 | list-style: none;
28 | margin: 4px;
29 | cursor: pointer;
30 | }
31 |
32 | .modal-item-check {
33 | margin-left: 8px;
34 | color: red;
35 | }
36 |
37 | .modal-close-btn {
38 | text-align: right;
39 | cursor: pointer;
40 | margin: 0 16px;
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-redux-sokushu-practice
2 |
3 | このリポジトリのソースは[React + Reduxを使ったWebアプリケーション開発速習会@Wantedly](http://wantedly.connpass.com/event/33168/)のハンズオン用のものです。
4 |
5 | ソースの解説などは[Qiita記事](http://qiita.com/shimpeiws/private/df31e2d70cc67c68115d)をご覧ください。
6 |
7 | 完成済みのアプリケーションは[shimpeiws/react-redux-sokushu](https://github.com/shimpeiws/react-redux-sokushu)にあります。
8 |
9 | ## 起動
10 |
11 | ```
12 | npm install
13 | ```
14 |
15 | サーバの起動
16 |
17 | ```
18 | npm run serve
19 | ```
20 |
21 | webpack dev serverの起動
22 |
23 | ```
24 | npm run webpack:serve
25 | ```
26 |
27 | `localhost:8000` にアクセスし画面が表示されればOKです。
28 |
29 | 
30 |
--------------------------------------------------------------------------------
/src/components/IssueCommentList.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import IssueCommentListItem from './IssueCommentListItem'
5 |
6 | import styles from './IssueCommentList.scss'
7 |
8 | class IssueCommentList extends Component {
9 | render() {
10 | const { comments } = this.props
11 |
12 | return (
13 |
14 | {
15 | comments.map((comment) => {
16 | return ()
22 | })
23 | }
24 |
25 | )
26 | }
27 | }
28 |
29 | export default CSSModules(IssueCommentList, styles)
30 |
--------------------------------------------------------------------------------
/src/containers/IssueContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { Link } from 'react-router'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 |
6 | import {
7 | findInitialData,
8 | } from '../actions/issue'
9 |
10 | class IssueContainer extends Component {
11 | componentDidMount() {
12 | this.init()
13 | }
14 |
15 | init() {
16 | this.props.findInitialData()
17 | }
18 |
19 | render() {
20 | return (
21 |
22 | { this.props.children }
23 |
24 | )
25 | }
26 | }
27 |
28 | const mapStateToProps = (state, ownProps) => {
29 | return {}
30 | }
31 |
32 | const mapDispatchToProps = (dispatch) => {
33 | return bindActionCreators({
34 | findInitialData,
35 | }, dispatch)
36 | }
37 |
38 | export default connect(
39 | mapStateToProps,
40 | mapDispatchToProps
41 | )(IssueContainer)
42 |
--------------------------------------------------------------------------------
/webpack-dev-server.babel.js:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack'
2 | import express from 'express'
3 | import _ from 'lodash'
4 | import config, { serverPort, serverURI } from './webpack.config.babel.js'
5 |
6 | config.plugins.unshift(
7 | new webpack.HotModuleReplacementPlugin(),
8 | new webpack.NoErrorsPlugin()
9 | )
10 |
11 | const webpackDevModules = [
12 | `webpack-hot-middleware/client?path=${serverURI}/__webpack_hmr`,
13 | ]
14 |
15 | _.each(config.entry, (file, name) => {
16 | config.entry[name] = webpackDevModules.concat([file])
17 | })
18 |
19 | const compiler = webpack(config)
20 | const app = express()
21 |
22 | app.use(require('webpack-dev-middleware')(compiler, {
23 | noInfo: true,
24 | publicPath: config.output.publicPath,
25 | }))
26 |
27 | app.use(require('webpack-hot-middleware')(compiler))
28 |
29 | app.listen(serverPort, 'localhost', (err) => {
30 | if (err) {
31 | cosole.log(err)
32 | return
33 | }
34 |
35 | console.log(`Dev server is listening at ${serverURI}`)
36 | })
37 |
--------------------------------------------------------------------------------
/src/components/IssueListItem.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import styles from './IssueListItem.scss'
5 |
6 | class IssueListItem extends Component {
7 | onClickRow(e) {
8 | this.props.onClickRow(this.props.issue)
9 | }
10 |
11 | render() {
12 | const { issue } = this.props
13 |
14 | return(
15 |
16 |
17 |
{issue.id}
18 |
19 | {issue.title}
20 |
21 |
{issue.status}
22 |
23 | {
24 | issue.assignee.id ? (issue.assignee.name) : ("-")
25 | }
26 |
27 |
{issue.created}
28 |
{issue.updated}
29 |
30 |
31 | )
32 | }
33 | }
34 |
35 | export default CSSModules(IssueListItem, styles)
36 |
--------------------------------------------------------------------------------
/src/components/IssueList.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import IssueListItem from './IssueListItem'
5 | import styles from './IssueList.scss'
6 |
7 | class IssueList extends Component {
8 | render() {
9 | const { issues } = this.props
10 |
11 | return(
12 |
13 |
14 |
id
15 |
title
16 |
status
17 |
assignee
18 |
created
19 |
updated
20 |
21 | {
22 | issues.map((issue) => {
23 | return (
)
28 | })
29 | }
30 |
31 | )
32 | }
33 | }
34 |
35 | export default CSSModules(IssueList, styles)
36 |
--------------------------------------------------------------------------------
/src/components/SelectModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import Modal from 'react-modal'
5 |
6 | class SelectModal extends Component {
7 |
8 | render() {
9 | return
12 | {this.props.children}
13 |
14 | }
15 | }
16 |
17 | const styles = {
18 | overlay: {
19 | position: 'fixed',
20 | top: 0,
21 | left: 0,
22 | right: 0,
23 | bottom: 0,
24 | zIndex: 3000,
25 | backgroundColor: 'rgba(0, 0, 0, 0.4)',
26 | },
27 | content: {
28 | position: 'absolute',
29 | top: '50%',
30 | left: '50%',
31 | right: 'auto',
32 | bottom: 'auto',
33 | width: '880px',
34 | height: '681px',
35 | margin: '-340px -440px 0',
36 | padding: '0',
37 | border: 'none',
38 | background: '#fff',
39 | overflow: 'auto',
40 | WebkitOverflowScrolling: 'touch',
41 | borderRadius: '4px',
42 | outline: 'none',
43 | boxSizing: 'border-box',
44 | WebkitFontSmoothing: 'antialiased',
45 | }
46 | }
47 |
48 | export default SelectModal
49 |
--------------------------------------------------------------------------------
/webpack.config.babel.js:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import webpack from 'webpack'
3 | import ExtractTextPlugin from 'extract-text-webpack-plugin'
4 |
5 | export const serverPort = 8080
6 | export const serverURI = `http://localhost:${serverPort}`
7 |
8 | export default {
9 | entry: {
10 | issue: './src/entries/issue.js',
11 | },
12 |
13 | output: {
14 | path: path.join(__dirname, 'public/js'),
15 | filename: '[name].js',
16 | publicPath: `${serverURI}/assets/build/`,
17 | },
18 |
19 | resolve: {
20 | extensions: ['', '.js', '.jsx'],
21 | },
22 |
23 | plugins: [
24 | new webpack.HotModuleReplacementPlugin(),
25 | new webpack.NoErrorsPlugin(),
26 | new ExtractTextPlugin('[name].css'),
27 | ],
28 |
29 | module: {
30 | loaders: [
31 | {
32 | test: /\.jsx?$/,
33 | loader: 'babel',
34 | exclude: /node_modules/,
35 | },
36 | {
37 | test: /.s?css$/,
38 | loaders: [
39 | 'style',
40 | 'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
41 | 'resolve-url',
42 | 'sass',
43 | ],
44 | exclude: /node_modules/,
45 | },
46 | ],
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/entries/issue.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { createStore, combineReducers, applyMiddleware } from 'redux'
3 | import ReactDOM from 'react-dom'
4 | import { Router, Route, browserHistory, Link, IndexRoute } from 'react-router'
5 | import { syncHistoryWithStore } from 'react-router-redux'
6 | import { Provider } from 'react-redux'
7 | import thunk from 'redux-thunk'
8 | import _ from 'lodash'
9 | import 'babel-polyfill'
10 |
11 |
12 | import IssueContainer from '../containers/IssueContainer'
13 | import IssueListContainer from '../containers/IssueListContainer'
14 | import IssueDetailContainer from '../containers/IssueDetailContainer'
15 | import IssueNewContainer from '../containers/IssueNewContainer'
16 | import configureStore from '../stores/configureIssueStore'
17 |
18 | const store = configureStore()
19 | const history = syncHistoryWithStore(browserHistory, store)
20 |
21 | ReactDOM.render(
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ,
31 | document.getElementById('content')
32 | )
33 |
--------------------------------------------------------------------------------
/src/components/IssueCommentForm.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | border: 1px solid DarkGray;
3 | border-radius: 3px;
4 | margin-top: 30px;
5 | }
6 |
7 | .header {
8 | background-color: LightGray;
9 | padding: 10px 10px;
10 | display: flex;
11 | align-items: center;
12 | }
13 |
14 | .input-label {
15 | font-weight: 600;
16 | }
17 |
18 | .user-input {
19 | margin-left: 30px;
20 | }
21 |
22 | .main {
23 | background-color: white;
24 | padding: 10px 10px;
25 | }
26 |
27 | .comment-text {
28 | margin-top: 10px;
29 | width: 100%;
30 | height: 150px;
31 | rows: 10px;
32 | font-size: 15px;
33 | }
34 |
35 | .footer {
36 | display: flex;
37 | flex-direction: row;
38 | justify-content: flex-end;
39 | margin-bottom: 15px;
40 | }
41 |
42 | .close-issue-button {
43 | padding: 0 15px;
44 | height: 30px;
45 | line-height: 30px;
46 | font-size: 15px;
47 | border: 1px solid DarkGray;
48 | border-radius: 3px;
49 | text-align: center;
50 | margin-right: 15px;
51 | background: #eee;
52 | cursor: pointer;
53 | display: block;
54 | }
55 |
56 | .comment-button {
57 | composes: close-issue-button;
58 | background: green;
59 | color: white;
60 | }
61 |
62 | textarea, input {
63 | width: 100%;
64 | border-radius: 3px;
65 | background-color: #fff;
66 | border: solid 1px #e3e3e3;
67 | padding: 10px;
68 | box-sizing: border-box;
69 | display: block;
70 |
71 | &:disabled {
72 | background: #eee;
73 | }
74 | }
75 |
76 | input {
77 | padding: 5px;
78 | }
79 |
--------------------------------------------------------------------------------
/src/lib/records/Issue.js:
--------------------------------------------------------------------------------
1 | import { List, Record } from 'immutable'
2 |
3 | import Comment from './Comment'
4 | import User from './User'
5 | import Label from './Label'
6 |
7 | export const STATE = {
8 | CLOSE: 'close',
9 | OPEN: 'open',
10 | }
11 |
12 | const _Issue = Record({
13 | id: null,
14 | title: '',
15 | status: STATE.CLOSE,
16 | created: '',
17 | updated: '',
18 | comments: new List(),
19 | content: '',
20 | assignee: new User(),
21 | labels: new List(),
22 | })
23 |
24 | export default class Issue extends _Issue {
25 | static fromJS(issue = {}) {
26 | let comments = new List()
27 | let labels = new List()
28 |
29 | if (issue.comments) {
30 | comments = new List(issue.comments.map((comment) => {
31 | return Comment.fromJS(comment)
32 | }))
33 | }
34 |
35 | if (issue.labels) {
36 | labels = new List(issue.labels.map((label) => {
37 | return Label.fromJS(label)
38 | }))
39 | }
40 |
41 | return (new this).merge({
42 | id: parseInt(issue.id),
43 | title: issue.title,
44 | status: issue.status,
45 | created: issue.created,
46 | updated: issue.updated,
47 | content: issue.content,
48 | comments,
49 | labels,
50 | assignee: issue.assignee ? User.fromJS(issue.assignee) : new User(),
51 | })
52 | }
53 |
54 | isValidTitle() {
55 | return this.title.length > 0
56 | }
57 |
58 | isValidContent() {
59 | return this.content.length > 0
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/IssueCommentListItem.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | border: 1px solid DarkGray;
3 | border-radius: 3px;
4 | margin-bottom: 20px;
5 | }
6 |
7 | .header {
8 | background-color: LightGray;
9 | padding: 10px 10px;
10 | display: flex;
11 | }
12 |
13 | .header-name {
14 | flex: 4;
15 | font-weight: 600;
16 | }
17 |
18 | .header-date {
19 | flex: 12;
20 | font-size: 15px;
21 | font-weight: 400;
22 | }
23 |
24 | .actions {
25 | flex: 2;
26 | text-align: right;
27 | }
28 |
29 | .header-icon {
30 | cursor: pointer;
31 | margin-left: 15px;
32 | }
33 |
34 | .main {
35 | background-color: white;
36 | padding: 10px 10px;
37 | overflow: hidden;
38 |
39 | textarea {
40 | width: 100%;
41 | font-size: 14px;
42 | border-radius: 3px;
43 | background-color: #fff;
44 | border: solid 1px #e3e3e3;
45 | padding: 10px;
46 | box-sizing: border-box;
47 | display: block;
48 | }
49 | }
50 |
51 | .buttons {
52 | width: 300px;
53 | margin-top: 10px;
54 | float: right;
55 | text-align: right;
56 | overflow: hidden;
57 | }
58 |
59 | .cancel-button {
60 | float: right;
61 | padding: 0 15px;
62 | height: 30px;
63 | line-height: 30px;
64 | font-size: 15px;
65 | border: 1px solid DarkGray;
66 | border-radius: 3px;
67 | text-align: center;
68 | margin-left: 15px;
69 | background: red;
70 | color: white;
71 | cursor: pointer;
72 | display: block;
73 | }
74 |
75 | .comment-button {
76 | composes: cancel-button;
77 | background: green;
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/IssueNewHeader.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import styles from './IssueNewHeader.scss'
5 |
6 | class IssueNewHeader extends Component {
7 | constructor(props) {
8 | super(props)
9 | }
10 |
11 | onChangeTitle(e) {
12 | this.props.onChangeTitle(e.target.value)
13 | }
14 |
15 | onChangeContent(e) {
16 | this.props.onChangeContent(e.target.value)
17 | }
18 |
19 | onCreateIssue(e) {
20 | this.props.onCreateIssue()
21 | }
22 |
23 | render() {
24 | const {issue} = this.props.issueNewManager
25 | return (
26 |
27 |
28 | title:
29 |
34 |
35 |
36 |
37 | content:
38 |
43 |
44 |
45 | )
46 | }
47 | }
48 |
49 | IssueNewHeader.propTypes = {
50 | issueNewManager: PropTypes.object.isRequired,
51 | onChangeTitle: PropTypes.func.isRequired,
52 | onChangeContent: PropTypes.func.isRequired,
53 | onCreateIssue: PropTypes.func.isRequired,
54 | }
55 |
56 | export default CSSModules(IssueNewHeader, styles)
57 |
--------------------------------------------------------------------------------
/src/actions/issueNew.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import { Record, List } from 'immutable'
3 |
4 | import END_POINTS from '../lib/constants/EndPoints'
5 | import Issue from '../lib/records/Issue'
6 |
7 | const Actions = {
8 | SET_ISSUE: 'issue_new/set_issue',
9 | SET_LOADING: 'issue_new/set_loading',
10 | }
11 |
12 | export default Actions
13 |
14 | const TIMEOUT = 100000
15 |
16 | function initIssue(issue) {
17 | return Issue.fromJS(issue)
18 | }
19 |
20 | export function initializeIssue() {
21 | return setIssue(new Issue())
22 | }
23 |
24 | export function setIssue(issue) {
25 | return {
26 | type: Actions.SET_ISSUE,
27 | issue: issue
28 | }
29 | }
30 |
31 | export function createIssue(issue, router) {
32 | return async(dispatch) => {
33 | console.log('create Issue!')
34 | dispatch(setLoading(true))
35 | try {
36 | const newIssue = await createIssueRequest(issue)
37 | router.push(`/${newIssue.id}`)
38 | } catch (error) {
39 | console.log("error", error)
40 | }
41 | dispatch(setLoading(false))
42 | }
43 | }
44 |
45 | function setLoading(loading) {
46 | return {
47 | type: Actions.SET_LOADING,
48 | loading,
49 | }
50 | }
51 |
52 | async function createIssueRequest(issue) {
53 | const data = {
54 | issue: {
55 | title: issue.title,
56 | content: issue.content,
57 | }
58 | }
59 |
60 | const response = await $.ajax({
61 | url: END_POINTS.ISSUES,
62 | method: 'POST',
63 | dataType: 'json',
64 | data,
65 | timeout: TIMEOUT,
66 | })
67 | return initIssue(response)
68 | }
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-redux-sokushu",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "serve": "NODE_ENV=development babel-node server.babel.js",
8 | "webpack:serve": "NODE_ENV=development babel-node webpack-dev-server.babel.js"
9 | },
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "babel-polyfill": "^6.9.0",
14 | "immutable": "^3.8.1",
15 | "jquery": "^2.2.4",
16 | "lodash": "^4.13.1",
17 | "react": "^15.1.0",
18 | "react-dom": "^15.1.0",
19 | "react-loader": "^2.4.0",
20 | "react-modal": "^1.3.0",
21 | "react-redux": "^4.4.5",
22 | "react-router": "^2.4.1",
23 | "react-router-redux": "^4.0.4",
24 | "redux": "^3.5.2",
25 | "redux-thunk": "^2.1.0"
26 | },
27 | "devDependencies": {
28 | "babel-cli": "^6.9.0",
29 | "babel-core": "^6.9.0",
30 | "babel-loader": "^6.2.4",
31 | "babel-plugin-react-transform": "^2.0.2",
32 | "babel-preset-es2015": "^6.9.0",
33 | "babel-preset-react": "^6.3.13",
34 | "babel-preset-react-hmre": "^1.1.1",
35 | "babel-preset-stage-3": "^6.5.0",
36 | "css-loader": "^0.23.1",
37 | "express": "^4.14.0",
38 | "extract-text-webpack-plugin": "^1.0.1",
39 | "node-sass": "^3.7.0",
40 | "postcss-loader": "^0.9.1",
41 | "react-css-modules": "^3.7.6",
42 | "react-hot-loader": "^1.3.0",
43 | "react-transform-hmr": "^1.0.4",
44 | "resolve-url-loader": "^1.4.3",
45 | "sass-loader": "^3.2.0",
46 | "style-loader": "^0.13.1",
47 | "webpack": "^1.13.1",
48 | "webpack-dev-middleware": "^1.6.1",
49 | "webpack-dev-server": "^1.14.1",
50 | "webpack-hot-middleware": "^2.10.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/IssueDetailHeader.scss:
--------------------------------------------------------------------------------
1 | .base {
2 | border-bottom: 1px solid DarkGray;
3 | margin-bottom: 10px;
4 | }
5 |
6 | .state {
7 | color: #fff;
8 | height: 26px;
9 | line-height: 26px;
10 | font-size: 15px;
11 | font-weight: 600;
12 | padding: 0 8px;
13 | border-radius: 3px;
14 | display: inline-block;
15 | margin-top: 10px;
16 | i {
17 | margin-right: 6px;
18 | font-size: 14px;
19 | }
20 | }
21 |
22 | .state-open {
23 | composes: state;
24 | background: #6CC644;
25 | }
26 |
27 | .state-close {
28 | composes: state;
29 | background: #9DA0A4;
30 | }
31 |
32 | .title-wrapper {
33 | display: flex;
34 | align-items: center;
35 | position: relative;
36 | max-width: 80%;
37 | }
38 |
39 | .title {
40 | margin-top: 10px;
41 | font-size: 30px;
42 | font-weight: 600;
43 | word-break: break-all;
44 |
45 | input {
46 | width: 100%;
47 | font-size: 20px;
48 | border-radius: 3px;
49 | background-color: #fff;
50 | border: solid 1px #e3e3e3;
51 | padding: 10px;
52 | box-sizing: border-box;
53 | display: block;
54 | }
55 | }
56 |
57 | .edit-button {
58 | font-size: 15px;
59 | display: block;
60 | text-align: center;
61 | width: 50px;
62 | height: 30px;
63 | line-height: 30px;
64 | border-radius: 3px;
65 | color: DarkGray;
66 | cursor: pointer;
67 | position: absolute;
68 | right: -20%;
69 | top: 10px;
70 | }
71 |
72 | .assign-label-wrapper {
73 | display: flex;
74 | align-items: center;
75 | }
76 |
77 | .item-labels {
78 | flex: 1;
79 | font-size: 14px;
80 | color: #9DA0A4;
81 | margin-top: 10px;
82 | }
83 |
84 | .items {
85 | flex: 1;
86 | font-size: 18px;
87 | color: DimGray;
88 | margin-top: 5px;
89 | }
90 |
91 | .modal-item {
92 | text-decoration: none;
93 | list-style: none;
94 | margin: 4px;
95 | cursor: pointer;
96 | }
97 |
98 | .modal-item-check {
99 | margin-left: 8px;
100 | color: red;
101 | }
102 |
103 | .modal-close-btn {
104 | text-align: right;
105 | cursor: pointer;
106 | margin: 0 16px;
107 | }
108 |
--------------------------------------------------------------------------------
/src/containers/IssueNewContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { Link } from 'react-router'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import Loader from 'react-loader'
6 |
7 | import Issue from '../lib/records/Issue'
8 |
9 | import {
10 | setIssue,
11 | createIssue,
12 | initializeIssue,
13 | } from '../actions/issueNew'
14 |
15 | import IssueNewHeader from '../components/IssueNewHeader'
16 |
17 | import styles from './IssueNewContainer.scss'
18 |
19 | class IssueNewContainer extends Component {
20 |
21 | componentDidMount() {
22 | this.init()
23 | }
24 |
25 | init() {
26 | this.props.initializeIssue()
27 | }
28 |
29 | onChangeTitle(title) {
30 | this.props.setIssue(this.props.issueNewManager.issue.set('title', title))
31 | }
32 |
33 | onChangeContent(content) {
34 | this.props.setIssue(this.props.issueNewManager.issue.set('content', content))
35 | }
36 |
37 | onCreateIssue() {
38 | const issueNewManager = this.props.issueNewManager
39 | const issue = issueNewManager.issue
40 | if (!issue.isValidTitle() || !issue.isValidContent()) {
41 | return
42 | }
43 | if (!issueNewManager.loading) {
44 | this.props.createIssue(issueNewManager.issue, this.context.router)
45 | }
46 | }
47 |
48 | render() {
49 | const {issueNewManager} = this.props
50 | return (
51 |
52 |
53 | List Page
54 |
60 |
61 |
62 | )
63 | }
64 | }
65 |
66 | IssueNewContainer.contextTypes = {
67 | router: PropTypes.object.isRequired,
68 | }
69 |
70 | const mapStateToProps = (state, ownProps) => {
71 | return {
72 | issueNewManager: state.issue.issueNewManager,
73 | }
74 | }
75 |
76 | const mapDispatchToProps = (dispatch) => {
77 | return bindActionCreators({
78 | setIssue,
79 | createIssue,
80 | initializeIssue,
81 | }, dispatch)
82 | }
83 |
84 | export default connect(
85 | mapStateToProps,
86 | mapDispatchToProps
87 | )(IssueNewContainer, styles)
88 |
--------------------------------------------------------------------------------
/src/components/IssueCommentListItem.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import nl2br from '../lib/utils/nl2br'
5 |
6 | import styles from './IssueCommentListItem.scss'
7 |
8 | class IssueCommentListItem extends Component {
9 | constructor(props) {
10 | super(props)
11 | this.state = {
12 | isEditing: false,
13 | editingContent: this.props.comment.content,
14 | }
15 | }
16 |
17 | onClickEdit() {
18 | // TODO: implement
19 | }
20 |
21 | onClickCancel() {
22 | // TODO: implement
23 | }
24 |
25 | onClickSave() {
26 | // TODO: implement
27 | }
28 |
29 | onClickDelete() {
30 | // TODO: implement
31 | }
32 |
33 | onChangeContent(e) {
34 | this.setState({ editingContent: e.target.value })
35 | }
36 |
37 | render() {
38 | const { comment } = this.props
39 |
40 | return (
41 |
42 |
43 |
44 | {comment.userName}
45 |
46 |
47 | {comment.updated}
48 |
49 |
50 |
54 |
55 |
56 |
60 |
61 |
62 |
63 |
64 |
65 | { this.state.isEditing ? (
66 |
67 |
71 |
72 |
76 | Save
77 |
78 |
82 | Cancel
83 |
84 |
85 |
86 | ) : (
87 | nl2br(comment.content)
88 | )
89 | }
90 |
91 |
92 | )
93 | }
94 | }
95 |
96 | export default CSSModules(IssueCommentListItem, styles)
97 |
--------------------------------------------------------------------------------
/src/components/IssueCommentForm.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import Comment from '../lib/records/Comment'
5 | import { STATE } from '../lib/records/Issue'
6 |
7 | import styles from './IssueCommentForm.scss'
8 |
9 | class IssueCommentForm extends Component {
10 | constructor(props) {
11 | super(props)
12 | this.state = {
13 | userName: '',
14 | content: '',
15 | }
16 | }
17 |
18 | onClickComment() {
19 | const comment = Comment.fromJS(this.state)
20 | this.props.onClickComment(comment)
21 | }
22 |
23 | onClickChangeStatus(status) {
24 | this.props.onClickChangeStatus(
25 | this.props.issue.set('status', status)
26 | )
27 | }
28 |
29 | onChangeUserName(e) {
30 | this.setState({userName: e.target.value})
31 | }
32 |
33 | onChangeContent(e) {
34 | this.setState({content: e.target.value})
35 | }
36 |
37 | render() {
38 | const { issue } = this.props
39 | return (
40 |
41 |
42 |
43 | User Name
44 |
45 |
46 |
52 |
53 |
54 |
55 |
56 | Comment Here
57 |
58 |
64 |
65 |
66 | { issue.status === STATE.OPEN ? (
67 |
71 | Close Issue
72 |
) : (
76 | Re-open Issue
77 |
)
78 | }
79 |
83 | Comment
84 |
85 |
86 |
87 | )
88 | }
89 | }
90 |
91 | export default CSSModules(IssueCommentForm, styles)
92 |
--------------------------------------------------------------------------------
/src/reducers/issue.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 | import { List } from 'immutable'
3 |
4 | import Issue from '../lib/records/Issue'
5 | import IssueManager from '../lib/records/IssueManager'
6 | import IssueListManager from '../lib/records/IssueListManager'
7 | import IssueDetailManager from '../lib/records/IssueDetailManager'
8 | import IssueNewManager from '../lib/records/IssueNewManager'
9 |
10 | import IssueActions from '../actions/issue'
11 | import IssueDetailActions from '../actions/issueDetail'
12 | import IssueNewActions from '../actions/issueNew'
13 |
14 | function issueList(state = new List(), action) {
15 | switch (action.type) {
16 | case IssueActions.SET_ISSUES:
17 | return action.issues
18 | default:
19 | break // do nothing
20 | }
21 | return state
22 | }
23 |
24 | function issueDetail(state = new Issue(), action) {
25 | switch (action.type) {
26 | case IssueDetailActions.SET_ISSUE_DETAIL:
27 | return action.issueDetail
28 | case IssueDetailActions.SET_COMMENTS:
29 | return state.set('comments', action.comments)
30 | default:
31 | break // do nothing
32 | }
33 | return state
34 | }
35 |
36 | function issueManager(state = new IssueManager(), action) {
37 | switch (action.type) {
38 | case IssueActions.SET_USERS:
39 | return state.set('users', action.users)
40 | case IssueActions.SET_LABELS:
41 | return state.set('labels', action.labels)
42 | default:
43 | break // do nothing
44 | }
45 | return state
46 | }
47 |
48 | function issueListManager(state = new IssueListManager(), action) {
49 | switch (action.type) {
50 | case IssueActions.SET_LOADING:
51 | return state.set('loading', action.loading)
52 | default:
53 | break // do nothing
54 | }
55 | return state
56 | }
57 |
58 | function issueDetailManager(state = new IssueDetailManager(), action) {
59 | switch (action.type) {
60 | case IssueDetailActions.SET_TITLE_EDITING:
61 | return state.set('isTitleEditing', action.editing)
62 | case IssueDetailActions.SET_LOADING:
63 | return state.set('loading', action.loading)
64 | case IssueDetailActions.SET_SHOW_USERS_MODAL:
65 | return state.set('showUsersModal', action.show)
66 | case IssueDetailActions.SET_SHOW_LABELS_MODAL:
67 | return state.set('showLabelsModal', action.show)
68 | default:
69 | break // do nothing
70 | }
71 | return state
72 | }
73 |
74 | function issueNewManager(state = new IssueNewManager(), action) {
75 | switch (action.type) {
76 | case IssueNewActions.SET_ISSUE:
77 | return state.set('issue', action.issue)
78 | case IssueNewActions.SET_LOADING:
79 | return state.set('loading', action.loading)
80 | default:
81 | break // do nothing
82 | }
83 | return state
84 | }
85 |
86 |
87 | export default combineReducers({
88 | issueList,
89 | issueDetail,
90 | issueManager,
91 | issueListManager,
92 | issueDetailManager,
93 | issueNewManager,
94 | })
95 |
--------------------------------------------------------------------------------
/src/actions/issue.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import { Record, List } from 'immutable'
3 |
4 | import END_POINTS from '../lib/constants/EndPoints'
5 | import Issue from '../lib/records/Issue'
6 | import Label from '../lib/records/Label'
7 | import User from '../lib/records/User'
8 |
9 | const Actions = {
10 | SET_ISSUES: 'issue/set_issues',
11 | SET_LOADING: 'issue/set_loading',
12 | SET_USERS: 'issue_base/set_users',
13 | SET_LABELS: 'issue_base/set_labels',
14 | }
15 |
16 | export default Actions
17 |
18 | function initIssues(issues) {
19 | return new List(issues.map((issue) => {
20 | return Issue.fromJS(issue)
21 | }))
22 | }
23 |
24 | function initUsers(users) {
25 | return new List(users.map((user) => {
26 | return User.fromJS(user)
27 | }))
28 | }
29 |
30 | function initLabels(labels) {
31 | return new List(labels.map((label) => {
32 | return Label.fromJS(label)
33 | }))
34 | }
35 |
36 | async function findIssuesRequest(params={}) {
37 | const response = await $.ajax({
38 | url: END_POINTS.ISSUES,
39 | dataType: 'json',
40 | data: params,
41 | timeout: 100000,
42 | })
43 | return initIssues(response)
44 | }
45 |
46 | async function findUsersRequest() {
47 | const response = await $.ajax({
48 | url: END_POINTS.USERS,
49 | dataType: 'json',
50 | timeout: 100000,
51 | })
52 | return initUsers(response)
53 | }
54 |
55 | async function findLabelsRequest() {
56 | const response = await $.ajax({
57 | url: END_POINTS.LABELS,
58 | dataType: 'json',
59 | timeout: 100000,
60 | })
61 | return initLabels(response)
62 | }
63 |
64 | function setIssues(issues) {
65 | return {
66 | type: Actions.SET_ISSUES,
67 | issues,
68 | }
69 | }
70 |
71 | function setLoading(loading) {
72 | return {
73 | type: Actions.SET_LOADING,
74 | loading,
75 | }
76 | }
77 |
78 | function setUsers(users) {
79 | return {
80 | type: Actions.SET_USERS,
81 | users,
82 | }
83 | }
84 |
85 | function setLabels(labels) {
86 | return {
87 | type: Actions.SET_LABELS,
88 | labels,
89 | }
90 | }
91 |
92 | export function findInitialData() {
93 | return async(dispatch) => {
94 | try {
95 | const data = await Promise.all([
96 | findUsersRequest(),
97 | findLabelsRequest(),
98 | ])
99 | dispatch(setUsers(data[0]))
100 | dispatch(setLabels(data[1]))
101 | } catch(error) {
102 | console.log("error", error)
103 | }
104 | }
105 | }
106 |
107 | export function findIssues(params, options={}) {
108 | return async(dispatch) => {
109 | const skipLoading = !!options.skipLoading
110 |
111 | if (!skipLoading) { dispatch(setLoading(true)) }
112 | try {
113 | const issues = await findIssuesRequest(params)
114 | dispatch(setIssues(issues))
115 | } catch (error) {
116 | console.log("error", error)
117 | }
118 | dispatch(setLoading(false))
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/containers/IssueListContainer.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import React, { Component, PropTypes } from 'react'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import Loader from 'react-loader'
6 |
7 | import { STATE } from '../lib/records/Issue'
8 |
9 | import { findIssues } from '../actions/issue'
10 |
11 | import IssueListHeader from '../components/IssueListHeader'
12 | import IssueList from '../components/IssueList'
13 |
14 | import styles from './IssueListContainer.scss'
15 |
16 | class IssueListContainer extends Component {
17 | componentDidMount() {
18 | this.init()
19 | }
20 |
21 | componentWillReceiveProps(nextProps) {
22 | if (!_.isEqual(this.props.params, nextProps.params)) {
23 | this.props.findIssues(nextProps.params, { skipLoading: true })
24 | }
25 | }
26 |
27 | onClickRow(issue) {
28 | this.context.router.push(`/${issue.id}`)
29 | }
30 |
31 | onClickOpen() {
32 | this.search({ status: STATE.OPEN })
33 | }
34 |
35 | onClickClose() {
36 | this.search({ status: STATE.CLOSE })
37 | }
38 |
39 | init() {
40 | this.props.findIssues(this.searchParams())
41 | }
42 |
43 | searchParams() {
44 | const params = this.props.params
45 | params.status = params.status || STATE.OPEN
46 | return params
47 | }
48 |
49 | search(params = {}) {
50 | let query = {}
51 | _.each(_.extend({}, this.searchParams(), params), (value, key) => {
52 | query[key] = value
53 | })
54 | this.pushQuery(query)
55 | }
56 |
57 | pushQuery(query) {
58 | this.context.router.push({
59 | pathname: '/',
60 | query: query
61 | })
62 | }
63 |
64 | onChangeAssigneeFilter(user) {
65 | // TODO: implement
66 | }
67 |
68 | onChangeLabelFilter(label) {
69 | // TODO: implement
70 | }
71 |
72 | render() {
73 | const { params, issues, issueManager, issueListManager } = this.props
74 | const { assignee_id: userFilterId, label_ids: labelFilterId } = params
75 | console.log("issueManager", issueManager)
76 | return (
77 |
78 |
79 |
88 |
92 |
93 |
94 | )
95 | }
96 | }
97 |
98 | IssueListContainer.contextTypes = {
99 | router: PropTypes.object.isRequired,
100 | }
101 |
102 | const mapStateToProps = (state, ownProps) => {
103 | return {
104 | params: ownProps.location.query,
105 | issues: state.issue.issueList,
106 | issueManager: state.issue.issueManager,
107 | issueListManager: state.issue.issueListManager,
108 | }
109 | }
110 |
111 | const mapDispatchToProps = (dispatch) => {
112 | return bindActionCreators({
113 | findIssues,
114 | }, dispatch)
115 | }
116 |
117 | export default connect(
118 | mapStateToProps,
119 | mapDispatchToProps
120 | )(IssueListContainer, styles)
121 |
--------------------------------------------------------------------------------
/src/components/IssueListHeader.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import { Link } from 'react-router'
3 | import CSSModules from 'react-css-modules'
4 |
5 | import Modal from './SelectModal'
6 |
7 | import styles from './IssueListHeader.scss'
8 |
9 | class IssueListHeader extends Component {
10 | constructor(props) {
11 | super(props)
12 | this.state = {
13 | showAssigneeModal: false,
14 | showLabelModal: false,
15 | }
16 | }
17 |
18 | isAssigneeFilter(user) {
19 | return user.id === parseInt(this.props.userFilterId)
20 | }
21 |
22 | isLabelFilter(label) {
23 | return label.id === parseInt(this.props.labelFilterId)
24 | }
25 |
26 | onChangeAssigneeFilter(user) {
27 | // TODO: implement
28 | }
29 |
30 | onChangeLabelFilter(label) {
31 | // TODO: implement
32 | }
33 |
34 | onChangeAssigneeModal(show) {
35 | this.setState({
36 | showAssigneeModal: show,
37 | })
38 | }
39 |
40 | onChangeLabelModal(show) {
41 | this.setState({
42 | showLabelModal: show,
43 | })
44 | }
45 |
46 | render() {
47 | const {issueManager} = this.props
48 | const {showAssigneeModal, showLabelModal} = this.state
49 | return (
50 |
51 |
52 | Open
53 | Close
54 |
55 |
56 |
Assignee
59 |
62 |
63 | close
67 | {
68 | issueManager.users.map((user) => {
69 | return (
70 | - {user.name}
75 | { this.isAssigneeFilter(user) ? : (null)}
76 |
77 | )
78 | })
79 | }
80 |
81 |
82 |
Label
85 |
88 |
89 | close
93 | {
94 | issueManager.labels.map((label) => {
95 | return (
96 | - {label.name}
101 | { this.isLabelFilter(label) ? : (null)}
102 |
103 | )
104 | })
105 | }
106 |
107 |
108 |
Sort
109 |
Create Issue
110 |
111 |
112 | )
113 | }
114 | }
115 |
116 | export default CSSModules(IssueListHeader, styles)
117 |
--------------------------------------------------------------------------------
/src/components/IssueDetailHeader.js:
--------------------------------------------------------------------------------
1 | import React, {Component, PropTypes} from 'react'
2 | import CSSModules from 'react-css-modules'
3 |
4 | import { STATE } from './../lib/records/Issue'
5 |
6 | import Modal from './SelectModal'
7 |
8 | import styles from './IssueDetailHeader.scss'
9 |
10 | class IssueDetailHeader extends Component {
11 | constructor(props) {
12 | super(props)
13 | this.state = {
14 | title: this.props.issue.title,
15 | }
16 | }
17 |
18 | onChangeTitle(e) {
19 | this.setState({title: e.target.value})
20 | }
21 |
22 | onClickTitleSave() {
23 | const newIssue = this.props.issue.set('title', this.state.title)
24 | this.props.onClickTitleSave(newIssue)
25 | }
26 |
27 | isSelectedUser(user) {
28 | return this.props.issue.assignee !== null && user.id === this.props.issue.assignee.id
29 | }
30 |
31 | isSelectedLabel(label) {
32 | return this.props.issue.labels.map((l) => l.id).includes(label.id)
33 | }
34 |
35 | onAssigneeSelected(user, e) {
36 | // TODO: implement
37 | }
38 |
39 | onLabelSelected(label, e) {
40 | // TODO: implement
41 | }
42 |
43 | onChangeShowUsersModal(show) {
44 | // TODO: implement
45 | }
46 |
47 | onChangeShowLabelsModal(show) {
48 | // TODO: implement
49 | }
50 |
51 | render() {
52 | const { issue, issueManager, issueDetailManager } = this.props
53 | return (
54 |
55 |
56 | {
57 | issue.status === STATE.OPEN ?
58 | :
59 |
60 | }
61 | {issue.status}
62 |
63 |
64 | { this.props.isTitleEditing ? (
65 |
66 |
71 |
75 | Save
76 |
77 |
78 |
) : (
79 |
80 |
81 | {issue.title}
82 |
83 |
84 | Edit
85 |
86 |
87 | )
88 | }
89 |
90 |
91 |
92 | assign
93 |
94 |
95 | labels
96 |
97 |
98 |
99 |
102 | {
103 | issue.assignee.id ? (issue.assignee.name) : ("No Assignee")
104 | }
105 |
106 |
109 | {issue.labels.size > 0 ? issue.labels.map((label) => ({label.name})) : ("No Labels")}
110 |
111 |
112 |
113 |
114 | created
115 |
116 |
117 | updated
118 |
119 |
120 |
121 |
122 | {issue.created}
123 |
124 |
125 | {issue.updated}
126 |
127 |
128 |
129 | )
130 | }
131 | }
132 |
133 | export default CSSModules(IssueDetailHeader, styles)
134 |
--------------------------------------------------------------------------------
/src/containers/IssueDetailContainer.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react'
2 | import { Link } from 'react-router'
3 | import { connect } from 'react-redux'
4 | import { bindActionCreators } from 'redux'
5 | import Loader from 'react-loader'
6 |
7 | import IssueDetailHeader from '../components/IssueDetailHeader'
8 | import IssueCommentList from '../components/IssueCommentList'
9 | import IssueCommentForm from '../components/IssueCommentForm'
10 | import IssueDescription from '../components/IssueDescription'
11 |
12 | import {
13 | findIssueDetail,
14 | addComment,
15 | updateComment,
16 | deleteComment,
17 | changeTitleEditing,
18 | updateIssue,
19 | setShowUsersModal,
20 | setShowLabelsModal,
21 | } from '../actions/issueDetail'
22 |
23 | import styles from './IssueDetailContainer.scss'
24 |
25 | class IssueDetailContainer extends Component {
26 | componentDidMount() {
27 | this.init()
28 | }
29 |
30 | init() {
31 | this.props.findIssueDetail(this.props.params.id)
32 | }
33 |
34 | onClickCommentSave(comment) {
35 | // TODO: implement update
36 | this.props.addComment(this.props.issueDetail, comment)
37 | }
38 |
39 | onClickCommentDelete(comment) {
40 | // TODO: implement
41 | }
42 |
43 | onClickTitleEdit() {
44 | this.props.changeTitleEditing(true)
45 | }
46 |
47 | onClickTitleSave(issue) {
48 | this.props.changeTitleEditing(false)
49 | this.props.updateIssue(issue)
50 | }
51 |
52 | onClickChangeStatus(issue) {
53 | this.props.updateIssue(issue)
54 | }
55 |
56 | onAssigneeSelected(issue) {
57 | // TODO: implement
58 | }
59 |
60 | onLabelsSelected(issue) {
61 | // TODO: implement
62 | }
63 |
64 | onChangeShowUsersModal(show) {
65 | // TODO: implement
66 | }
67 |
68 | onChangeShowLabelsModal(show) {
69 | // TODO: implement
70 | }
71 |
72 | render() {
73 | const { issueDetail, issueDetailManager, issueManager } = this.props
74 | return (
75 |
76 |
List Page
77 |
78 |
90 |
91 |
94 |
99 |
104 |
105 |
106 |
107 | )
108 | }
109 | }
110 |
111 | IssueDetailContainer.contextTypes = {
112 | router: PropTypes.object.isRequired,
113 | }
114 |
115 | const mapStateToProps = (state, ownProps) => {
116 | return {
117 | issueDetail: state.issue.issueDetail,
118 | issueDetailManager: state.issue.issueDetailManager,
119 | issueManager: state.issue.issueManager,
120 | }
121 | }
122 |
123 | const mapDispatchToProps = (dispatch) => {
124 | return bindActionCreators({
125 | findIssueDetail,
126 | addComment,
127 | updateComment,
128 | deleteComment,
129 | changeTitleEditing,
130 | updateIssue,
131 | setShowUsersModal,
132 | setShowLabelsModal,
133 | }, dispatch)
134 | }
135 |
136 | export default connect(
137 | mapStateToProps,
138 | mapDispatchToProps
139 | )(IssueDetailContainer, styles)
140 |
--------------------------------------------------------------------------------
/src/actions/issueDetail.js:
--------------------------------------------------------------------------------
1 | import $ from 'jquery'
2 | import { Record, List } from 'immutable'
3 |
4 | import END_POINTS from '../lib/constants/EndPoints'
5 | import Issue from '../lib/records/Issue'
6 | import Comment from '../lib/records/Comment'
7 |
8 | const Actions = {
9 | SET_ISSUE_DETAIL: 'issue_detail/set_issue_detail',
10 | SET_COMMENTS: 'issue_detail/set_comments',
11 | SET_TITLE_EDITING: 'issue_detail/set_title_editing',
12 | SET_LOADING: 'issue_detail/set_loading',
13 | SET_SHOW_USERS_MODAL: 'issue_detail/set_show_users_modal',
14 | SET_SHOW_LABELS_MODAL: 'issue_detail/set_show_labels_modal',
15 | }
16 |
17 | export default Actions
18 |
19 | function initIssueDetail(issueDetail) {
20 | return Issue.fromJS(issueDetail)
21 | }
22 |
23 | async function findIssueDetailRequest(issueId) {
24 | const response = await $.ajax({
25 | url: `${END_POINTS.ISSUES}/${issueId}`,
26 | method: 'GET',
27 | dataType: 'json',
28 | timeout: 100000,
29 | })
30 | return initIssueDetail(response)
31 | }
32 |
33 | async function updateIssueRequest(issue) {
34 | const data = {
35 | issue: {
36 | id: issue.id,
37 | title: issue.title,
38 | status: issue.status,
39 | assignee_id: issue.assignee.id,
40 | label_ids: issue.labels.map((label) => label.id).toArray()
41 | }
42 | }
43 |
44 | const response = await $.ajax({
45 | url: `${END_POINTS.ISSUES}/${issue.id}`,
46 | method: 'PATCH',
47 | data,
48 | timeout: 100000,
49 | })
50 |
51 | return initIssueDetail(response)
52 | }
53 |
54 | function buildCommentRequestData(comment) {
55 | return {
56 | comment: {
57 | user_name: comment.userName,
58 | content: comment.content,
59 | },
60 | }
61 | }
62 |
63 | async function postCommentRequest(issue, comment) {
64 | const response = await $.ajax({
65 | url: `${END_POINTS.ISSUES}/${issue.id}/comments`,
66 | method: 'POST',
67 | data: buildCommentRequestData(comment),
68 | timeout: 100000,
69 | })
70 |
71 | return Comment.fromJS(response)
72 | }
73 |
74 | async function putCommentRequest(issue, comment) {
75 | const response = await $.ajax({
76 | url: `${END_POINTS.ISSUES}/${issue.id}/comments/${comment.id}`,
77 | method: 'PUT',
78 | data: buildCommentRequestData(comment),
79 | timeout: 100000,
80 | })
81 |
82 | return Comment.fromJS(response)
83 | }
84 |
85 | async function deleteCommentRequest(issue, comment) {
86 | // TODO: implement
87 | }
88 |
89 | function setIssueDetail(issueDetail) {
90 | return {
91 | type: Actions.SET_ISSUE_DETAIL,
92 | issueDetail,
93 | }
94 | }
95 |
96 | function setComments(comments) {
97 | return {
98 | type: Actions.SET_COMMENTS,
99 | comments,
100 | }
101 | }
102 |
103 | function setTitleEditing(editing) {
104 | return {
105 | type: Actions.SET_TITLE_EDITING,
106 | editing,
107 | }
108 | }
109 |
110 | function setLoading(loading) {
111 | return {
112 | type: Actions.SET_LOADING,
113 | loading,
114 | }
115 | }
116 |
117 | export function findIssueDetail(issueId) {
118 | return async(dispatch) => {
119 | dispatch(setLoading(true))
120 | try {
121 | const issueDetail = await findIssueDetailRequest(issueId)
122 | dispatch(setIssueDetail(issueDetail))
123 | } catch(error) {
124 | console.log("error", error)
125 | }
126 | dispatch(setLoading(false))
127 | }
128 | }
129 |
130 | export function addComment(issueDetail, comment) {
131 | return async(dispatch) => {
132 | const prevComments = issueDetail.comments
133 |
134 | try {
135 | const newComment = await postCommentRequest(issueDetail, comment)
136 | dispatch(setComments(prevComments.push(newComment)))
137 | } catch (error) {
138 | console.log("error", error)
139 | dispatch(setComments(prevComments)) // fallback to previous state
140 | }
141 | }
142 | }
143 |
144 | export function updateComment(issueDetail, comment) {
145 | return async(dispatch) => {
146 | const prevComments = issueDetail.comments
147 |
148 | try {
149 | const newComment = await putCommentRequest(issueDetail, comment)
150 | const nextComments = prevComments.update(
151 | prevComments.findIndex((target) => {
152 | return target.id === newComment.id
153 | }),
154 | (_comment) => {
155 | return newComment
156 | }
157 | )
158 | dispatch(setComments(nextComments))
159 | } catch (error) {
160 | console.log("error", error)
161 | dispatch(setComments(prevComments)) // fallback to previous state
162 | }
163 | }
164 | }
165 |
166 | export function deleteComment(issueDetail, comment) {
167 | return async(dispatch) => {
168 | // TODO: implement
169 | }
170 | }
171 |
172 | export function changeTitleEditing(editing) {
173 | return async(dispatch) => {
174 | dispatch(setTitleEditing(editing))
175 | }
176 | }
177 |
178 | export function updateIssue(issueDetail) {
179 | return async(dispatch) => {
180 | dispatch(setIssueDetail(issueDetail))
181 | try {
182 | await updateIssueRequest(issueDetail)
183 | } catch (error) {
184 | console.log("error", error)
185 | }
186 | }
187 | }
188 |
189 | export function setShowUsersModal(show) {
190 | // TODO: implement
191 | }
192 |
193 | export function setShowLabelsModal(show) {
194 | // TODO: implement
195 | }
196 |
--------------------------------------------------------------------------------