diff --git a/browser/main/HomePage.js b/browser/main/HomePage.js
index 015d5d5b..be4d6bfb 100644
--- a/browser/main/HomePage.js
+++ b/browser/main/HomePage.js
@@ -6,7 +6,7 @@ import ArticleNavigator from './HomePage/ArticleNavigator'
import ArticleTopBar from './HomePage/ArticleTopBar'
import ArticleList from './HomePage/ArticleList'
import ArticleDetail from './HomePage/ArticleDetail'
-import { findWhere, pick } from 'lodash'
+import { findWhere, findIndex, pick } from 'lodash'
import keygen from 'boost/keygen'
import { NEW } from './actions'
@@ -50,9 +50,20 @@ function remap (state) {
let activeUser = findWhere(users, {id: parseInt(status.userId, 10)})
if (activeUser == null) activeUser = users[0]
let articles = state.articles['team-' + activeUser.id]
- let activeArticle = findWhere(users, {id: status.articleId})
+ let activeArticle = findWhere(articles, {id: status.articleId})
if (activeArticle == null) activeArticle = articles[0]
+ // remove Unsaved new article if user is not CREATE_MODE
+ if (status.mode !== CREATE_MODE) {
+ let targetIndex = findIndex(articles, article => article.status === NEW)
+
+ if (targetIndex >= 0) articles.splice(targetIndex, 1)
+ }
+
+ // switching CREATE_MODE
+ // restrict
+ // 1. team have one folder at least
+ // or Change IDLE MODE
if (status.mode === CREATE_MODE && activeUser.Folders.length > 0) {
var newArticle = findWhere(articles, {status: 'NEW'})
if (newArticle == null) {
@@ -72,10 +83,6 @@ function remap (state) {
} else if (status.mode === CREATE_MODE) {
status.mode = IDLE_MODE
}
- if (status.mode !== CREATE_MODE && activeArticle != null && activeArticle.status === NEW) {
- articles.splice(articles.indexOf(activeArticle), 1)
- activeArticle = articles[0]
- }
return {
users,
diff --git a/browser/main/HomePage/ArticleDetail.js b/browser/main/HomePage/ArticleDetail.js
index 74e01c1d..677a1c53 100644
--- a/browser/main/HomePage/ArticleDetail.js
+++ b/browser/main/HomePage/ArticleDetail.js
@@ -4,10 +4,11 @@ import { findWhere, uniq } from 'lodash'
import ModeIcon from 'boost/components/ModeIcon'
import MarkdownPreview from 'boost/components/MarkdownPreview'
import CodeEditor from 'boost/components/CodeEditor'
-import { NEW, IDLE_MODE, CREATE_MODE, EDIT_MODE, switchMode } from '../actions'
+import { UNSYNCED, IDLE_MODE, CREATE_MODE, EDIT_MODE, switchMode, switchArticle, updateArticle, destroyArticle } from '../actions'
import aceModes from 'boost/ace-modes'
import Select from 'react-select'
import linkState from 'boost/linkState'
+import api from 'boost/api'
var modeOptions = aceModes.map(function (mode) {
return {
@@ -16,16 +17,27 @@ var modeOptions = aceModes.map(function (mode) {
}
})
+function makeInstantArticle (article) {
+ let instantArticle = Object.assign({}, article)
+ instantArticle.Tags = instantArticle.Tags.map(tag => tag.name)
+ return instantArticle
+}
+
export default class ArticleDetail extends React.Component {
constructor (props) {
super(props)
+
this.state = {
- article: Object.assign({}, props.activeArticle)
+ article: makeInstantArticle(props.activeArticle)
}
}
componentWillReceiveProps (nextProps) {
- this.setState({article: nextProps.activeArticle})
+ if (nextProps.activeArticle != null && nextProps.activeArticle.id !== this.state.article.id) {
+ this.setState({article: makeInstantArticle(nextProps.activeArticle)}, function () {
+ console.log('receive props')
+ })
+ }
}
renderEmpty () {
@@ -36,12 +48,46 @@ export default class ArticleDetail extends React.Component {
)
}
+ handleEditButtonClick (e) {
+ let { dispatch } = this.props
+ dispatch(switchMode(EDIT_MODE))
+ }
+
+ handleDeleteButtonClick (e) {
+ this.setState({openDeleteConfirmMenu: true})
+ }
+
+ handleDeleteConfirmButtonClick (e) {
+ let { dispatch, activeUser, activeArticle } = this.props
+
+ api.destroyArticle(activeArticle.id)
+ .then(res => {
+ console.log(res.body)
+ })
+ .catch(err => {
+ // connect failed need to queue data
+ if (err.code === 'ECONNREFUSED') {
+ return
+ }
+
+ if (err.status != null) throw err
+ else console.log(err)
+ })
+
+ dispatch(destroyArticle(activeUser.id, activeArticle.id))
+ this.setState({openDeleteConfirmMenu: false})
+ }
+
+ handleDeleteCancleButtonClick (e) {
+ this.setState({openDeleteConfirmMenu: false})
+ }
+
renderIdle () {
- let { status, activeArticle, activeUser } = this.props
+ let { activeArticle, activeUser } = this.props
let tags = activeArticle.Tags.length > 0 ? activeArticle.Tags.map(tag => {
return (
- {tag.name}
+ {tag.name}
)
}) : (
Not tagged yet
@@ -50,24 +96,37 @@ export default class ArticleDetail extends React.Component {
let folderName = folder != null ? folder.name : '(unknown)'
return (
-
-
-
-
-
{folderName}
- by {activeArticle.User.profileName}
- Created {moment(activeArticle.createdAt).format('YYYY/MM/DD')}
- Updated {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
+
+ {this.state.openDeleteConfirmMenu
+ ? (
+
+
+ Are you sure to delete this article?
+
+
+
-
{tags}
-
+ )
+ : (
+
+
+
+ {folderName}
+ by {activeArticle.User.profileName}
+ Created {moment(activeArticle.createdAt).format('YYYY/MM/DD')}
+ Updated {moment(activeArticle.updatedAt).format('YYYY/MM/DD')}
+
+
{tags}
+
+
+
+
+
+
+
+ )
+ }
-
-
-
-
-
-
@@ -86,7 +145,67 @@ export default class ArticleDetail extends React.Component {
}
handleSaveButtonClick (e) {
- console.log(this.state.article)
+ let { activeArticle } = this.props
+
+ if (typeof activeArticle.id === 'string') this.saveAsNew()
+ else this.save()
+ }
+
+ saveAsNew () {
+ let { dispatch, activeUser } = this.props
+ let article = this.state.article
+ let newArticle = Object.assign({}, article)
+ article.tags = article.Tags
+
+ api.createArticle(article)
+ .then(res => {
+ console.log(res.body)
+ })
+ .catch(err => {
+ // connect failed need to queue data
+ if (err.code === 'ECONNREFUSED') {
+ return
+ }
+
+ if (err.status != null) throw err
+ else console.log(err)
+ })
+
+ newArticle.status = UNSYNCED
+ newArticle.Tags = newArticle.Tags.map(tag => { return {name: tag} })
+
+ dispatch(updateArticle(activeUser.id, newArticle))
+ dispatch(switchMode(IDLE_MODE))
+ dispatch(switchArticle(article.id))
+ }
+
+ save () {
+ let { dispatch, activeUser } = this.props
+ let article = this.state.article
+ let newArticle = Object.assign({}, article)
+
+ article.tags = article.Tags
+
+ api.saveArticle(article)
+ .then(res => {
+ console.log(res.body)
+ })
+ .catch(err => {
+ // connect failed need to queue data
+ if (err.code === 'ECONNREFUSED') {
+ return
+ }
+
+ if (err.status != null) throw err
+ else console.log(err)
+ })
+
+ newArticle.status = UNSYNCED
+ newArticle.Tags = newArticle.Tags.map(tag => { return {name: tag} })
+
+ dispatch(updateArticle(activeUser.id, newArticle))
+ dispatch(switchMode(IDLE_MODE))
+ dispatch(switchArticle(article.id))
}
handleFolderIdChange (value) {
@@ -121,7 +240,7 @@ export default class ArticleDetail extends React.Component {
}
renderEdit () {
- let { status, activeUser } = this.props
+ let { activeUser } = this.props
let folderOptions = activeUser.Folders.map(folder => {
return {
@@ -146,7 +265,7 @@ export default class ArticleDetail extends React.Component {
diff --git a/browser/main/HomePage/ArticleList.js b/browser/main/HomePage/ArticleList.js
index d1fd0175..9338dbd0 100644
--- a/browser/main/HomePage/ArticleList.js
+++ b/browser/main/HomePage/ArticleList.js
@@ -2,16 +2,23 @@ import React, { PropTypes } from 'react'
import ProfileImage from 'boost/components/ProfileImage'
import ModeIcon from 'boost/components/ModeIcon'
import moment from 'moment'
-import { IDLE_MODE, CREATE_MODE, EDIT_MODE, NEW } from '../actions'
+import { IDLE_MODE, CREATE_MODE, EDIT_MODE, switchArticle, NEW } from '../actions'
export default class ArticleList extends React.Component {
+ handleArticleClick (id) {
+ let { dispatch } = this.props
+ return function (e) {
+ dispatch(switchArticle(id))
+ }
+ }
+
render () {
let { status, articles, activeArticle } = this.props
let articlesEl = articles.map(article => {
let tags = Array.isArray(article.Tags) && article.Tags.length > 0 ? article.Tags.map(tag => {
return (
-
#{tag.name}
+
{tag.name}
)
}) : (
Not tagged yet
@@ -19,7 +26,7 @@ export default class ArticleList extends React.Component {
return (
-
+
this.handleArticleClick(article.id)(e)} className={'articleItem' + (activeArticle.id === article.id ? ' active' : '')}>
by
{article.User.profileName}
@@ -48,5 +55,6 @@ export default class ArticleList extends React.Component {
ArticleList.propTypes = {
status: PropTypes.shape(),
articles: PropTypes.array,
- activeArticle: PropTypes.shape()
+ activeArticle: PropTypes.shape(),
+ dispatch: PropTypes.func
}
diff --git a/browser/main/actions.js b/browser/main/actions.js
index d2a128a6..1d469b22 100644
--- a/browser/main/actions.js
+++ b/browser/main/actions.js
@@ -1,17 +1,25 @@
+// Action types
export const USER_UPDATE = 'USER_UPDATE'
+export const ARTICLE_REFRESH = 'ARTICLE_REFRESH'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
+export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
+
export const SWITCH_USER = 'SWITCH_USER'
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_MODE = 'SWITCH_MODE'
+export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
+// Status - mode
export const IDLE_MODE = 'IDLE_MODE'
export const CREATE_MODE = 'CREATE_MODE'
export const EDIT_MODE = 'EDIT_MODE'
+// Article status
export const NEW = 'NEW'
export const SYNCING = 'SYNCING'
export const UNSYNCED = 'UNSYNCED'
+// DB
export function updateUser (user) {
return {
type: USER_UPDATE,
@@ -19,13 +27,28 @@ export function updateUser (user) {
}
}
-export function updateArticles (userId, articles) {
+export function refreshArticles (userId, articles) {
return {
- type: ARTICLE_UPDATE,
+ type: ARTICLE_REFRESH,
data: {userId, articles}
}
}
+export function updateArticle (userId, article) {
+ return {
+ type: ARTICLE_UPDATE,
+ data: {userId, article}
+ }
+}
+
+export function destroyArticle (userId, articleId) {
+ return {
+ type: ARTICLE_DESTROY,
+ data: { userId, articleId }
+ }
+}
+
+// Nav
export function switchUser (userId) {
return {
type: SWITCH_USER,
@@ -46,3 +69,10 @@ export function switchMode (mode) {
data: mode
}
}
+
+export function switchArticle (articleId) {
+ return {
+ type: SWITCH_ARTICLE,
+ data: articleId
+ }
+}
diff --git a/browser/main/index.js b/browser/main/index.js
index f7de8928..a3092cb6 100644
--- a/browser/main/index.js
+++ b/browser/main/index.js
@@ -1,7 +1,7 @@
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
-import { updateUser, updateArticles } from './actions'
+import { updateUser, refreshArticles } from './actions'
import reducer from './reducer'
import { fetchCurrentUser, fetchArticles } from 'boost/api'
import { Router, Route, IndexRoute } from 'react-router'
@@ -37,7 +37,7 @@ let finalCreateStore = compose(devTools(), persistState(window.location.href.mat
let store = finalCreateStore(reducer)
let devEl = (
-
+
)
@@ -67,7 +67,7 @@ React.render((
users.forEach(user => {
fetchArticles(user.id)
.then(res => {
- store.dispatch(updateArticles(user.id, res.body))
+ store.dispatch(refreshArticles(user.id, res.body))
})
.catch(err => {
if (err.status == null) throw err
diff --git a/browser/main/reducer.js b/browser/main/reducer.js
index 166f48e9..25de8a13 100644
--- a/browser/main/reducer.js
+++ b/browser/main/reducer.js
@@ -1,5 +1,6 @@
import { combineReducers } from 'redux'
-import { SWITCH_USER, SWITCH_FOLDER, SWITCH_MODE, USER_UPDATE, ARTICLE_UPDATE, IDLE_MODE, CREATE_MODE, EDIT_MODE } from './actions'
+import { findIndex } from 'lodash'
+import { SWITCH_USER, SWITCH_FOLDER, SWITCH_MODE, SWITCH_ARTICLE, USER_UPDATE, ARTICLE_REFRESH, ARTICLE_UPDATE, ARTICLE_DESTROY, IDLE_MODE, CREATE_MODE } from './actions'
const initialCurrentUser = JSON.parse(localStorage.getItem('currentUser'))
const initialStatus = {
@@ -29,14 +30,20 @@ function status (state, action) {
switch (action.type) {
case SWITCH_USER:
state.userId = action.data
- console.log(action)
+ state.mode = IDLE_MODE
state.folderId = null
return state
case SWITCH_FOLDER:
state.folderId = action.data
+ state.mode = IDLE_MODE
return state
case SWITCH_MODE:
state.mode = action.data
+ if (state.mode === CREATE_MODE) state.articleId = null
+ return state
+ case SWITCH_ARTICLE:
+ state.articleId = action.data
+ state.mode = IDLE_MODE
return state
default:
if (state == null) return initialStatus
@@ -44,13 +51,47 @@ function status (state, action) {
}
}
+function genKey (id) {
+ return 'team-' + id
+}
+
function articles (state, action) {
switch (action.type) {
+ case ARTICLE_REFRESH:
+ {
+ let { userId, articles } = action.data
+ let teamKey = genKey(userId)
+ localStorage.setItem(teamKey, JSON.stringify(articles))
+ state[teamKey] = articles
+ }
+
+ return state
case ARTICLE_UPDATE:
- let { userId, articles } = action.data
- let teamKey = 'team-' + userId
- localStorage.setItem(teamKey, JSON.stringify(articles))
- state[teamKey] = articles
+ {
+ let { userId, article } = action.data
+ let teamKey = genKey(userId)
+ let articles = JSON.parse(localStorage.getItem(teamKey))
+
+ let targetIndex = findIndex(articles, _article => article.id === _article.id)
+ if (targetIndex < 0) articles.unshift(article)
+ else articles.splice(targetIndex, 1, article)
+
+ localStorage.setItem(teamKey, JSON.stringify(articles))
+ state[teamKey] = articles
+ }
+ return state
+ case ARTICLE_DESTROY:
+ {
+ let { userId, articleId } = action.data
+ let teamKey = genKey(userId)
+ let articles = JSON.parse(localStorage.getItem(teamKey))
+
+ let targetIndex = findIndex(articles, _article => articleId === _article.id)
+ if (targetIndex >= 0) articles.splice(targetIndex, 1)
+
+ localStorage.setItem(teamKey, JSON.stringify(articles))
+ state[teamKey] = articles
+ }
return state
default:
if (state == null) return initialArticles
diff --git a/browser/styles/main/HomeContainer/components/ArticleDetail.styl b/browser/styles/main/HomeContainer/components/ArticleDetail.styl
index 3783ca2a..5f068590 100644
--- a/browser/styles/main/HomeContainer/components/ArticleDetail.styl
+++ b/browser/styles/main/HomeContainer/components/ArticleDetail.styl
@@ -10,6 +10,31 @@ noTagsColor = #999
border-left 1px solid borderColor
*
-webkit-user-select all
+ .deleteConfirm
+ width 100%
+ height 70px
+ .right
+ float right
+ button
+ cursor pointer
+ height 33px
+ padding 0 10px
+ margin-left 5px
+ font-size 14px
+ color inactiveTextColor
+ background-color darken(white, 5%)
+ border solid 1px borderColor
+ border-radius 5px
+ &:hover
+ background-color white
+ &.primary
+ border none
+ background-color brandColor
+ color white
+ &:hover
+ color white
+ background-color lighten(brandColor, 10%)
+
.detailInfo
height 70px
width 100%
@@ -116,7 +141,7 @@ noTagsColor = #999
font-size 32px
font-weight bold
outline none
- &.show
+ &.idle
.detailInfo
.left
right 99px
diff --git a/lib/api.js b/lib/api.js
index 91290feb..e2d1a9ca 100644
--- a/lib/api.js
+++ b/lib/api.js
@@ -29,6 +29,32 @@ export function fetchArticles (userId) {
})
}
+export function createArticle (input) {
+ return request
+ .post(apiUrl + 'articles/')
+ .set({
+ Authorization: 'Bearer ' + localStorage.getItem('token')
+ })
+ .send(input)
+}
+
+export function saveArticle (input) {
+ return request
+ .put(apiUrl + 'articles/' + input.id)
+ .set({
+ Authorization: 'Bearer ' + localStorage.getItem('token')
+ })
+ .send(input)
+}
+
+export function destroyArticle (articleId) {
+ return request
+ .del(apiUrl + 'articles/' + articleId)
+ .set({
+ Authorization: 'Bearer ' + localStorage.getItem('token')
+ })
+}
+
export function createTeam (input) {
return request
.post(apiUrl + 'teams')
@@ -70,3 +96,18 @@ export function sendEmail (input) {
})
.send(input)
}
+
+export default {
+ login,
+ signup,
+ fetchCurrentUser,
+ fetchArticles,
+ createArticle,
+ saveArticle,
+ destroyArticle,
+ createTeam,
+ searchUser,
+ setMember,
+ deleteMember,
+ sendEmail
+}