1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

CRUD done

This commit is contained in:
Rokt33r
2015-10-15 10:46:22 +09:00
parent 9d2b64e82b
commit 832ca3347c
8 changed files with 317 additions and 46 deletions

View File

@@ -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,

View File

@@ -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 (
<a key={tag.id}>{tag.name}</a>
<a key={tag.name}>{tag.name}</a>
)
}) : (
<span className='noTags'>Not tagged yet</span>
@@ -50,7 +96,18 @@ export default class ArticleDetail extends React.Component {
let folderName = folder != null ? folder.name : '(unknown)'
return (
<div className='ArticleDetail show'>
<div className='ArticleDetail idle'>
{this.state.openDeleteConfirmMenu
? (
<div className='deleteConfirm'>
<div className='right'>
Are you sure to delete this article?
<button onClick={e => this.handleDeleteConfirmButtonClick(e)} className='primary'><i className='fa fa-fw fa-check'/> Sure</button>
<button onClick={e => this.handleDeleteCancleButtonClick(e)}><i className='fa fa-fw fa-times'/> Cancle</button>
</div>
</div>
)
: (
<div className='detailInfo'>
<div className='left'>
<div className='info'>
@@ -61,13 +118,15 @@ export default class ArticleDetail extends React.Component {
</div>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tags}</div>
</div>
<div className='right'>
<button><i className='fa fa-fw fa-edit'/></button>
<button><i className='fa fa-fw fa-trash'/></button>
<button onClick={e => this.handleEditButtonClick(e)}><i className='fa fa-fw fa-edit'/></button>
<button onClick={e => this.handleDeleteButtonClick(e)}><i className='fa fa-fw fa-trash'/></button>
<button><i className='fa fa-fw fa-share-alt'/></button>
</div>
</div>
)
}
<div className='detailBody'>
<div className='detailPanel'>
<div className='header'>
@@ -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 {
<div className='detailPanel'>
<div className='header'>
<div className='title'>
<input ref='title' valueLink={this.linkState('article.title')}/>
<input placeholder='Title' ref='title' valueLink={this.linkState('article.title')}/>
</div>
<Select ref='mode' onChange={value => this.handleModeChange(value)} clearable={false} options={modeOptions}placeholder='select mode...' value={this.state.article.mode} className='mode'/>
</div>

View File

@@ -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 (
<a key={tag.id}>#{tag.name}</a>
<a key={tag.name}>{tag.name}</a>
)
}) : (
<span>Not tagged yet</span>
@@ -19,7 +26,7 @@ export default class ArticleList extends React.Component {
return (
<div key={'article-' + article.id}>
<div className={'articleItem' + (activeArticle.id === article.id ? ' active' : '')}>
<div onClick={e => this.handleArticleClick(article.id)(e)} className={'articleItem' + (activeArticle.id === article.id ? ' active' : '')}>
<div className='top'>
<i className='fa fa-fw fa-square'/>
by <ProfileImage className='profileImage' size='20' email={article.User.email}/> {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
}

View File

@@ -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
}
}

View File

@@ -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 = (
<DebugPanel top right bottom>
<DevTools store={store} monitor={LogMonitor} />
<DevTools store={store} monitor={LogMonitor} visibleOnLoad={false}/>
</DebugPanel>
)
@@ -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

View File

@@ -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_UPDATE:
case ARTICLE_REFRESH:
{
let { userId, articles } = action.data
let teamKey = 'team-' + userId
let teamKey = genKey(userId)
localStorage.setItem(teamKey, JSON.stringify(articles))
state[teamKey] = articles
}
return state
case ARTICLE_UPDATE:
{
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

View File

@@ -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

View File

@@ -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
}