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

No edit mode

This commit is contained in:
Rokt33r
2015-12-28 00:59:36 +09:00
parent 013a1b4f51
commit 2537b6ba09
9 changed files with 240 additions and 364 deletions

View File

@@ -133,7 +133,7 @@ export default class ModeSelect extends React.Component {
return ( return (
<div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}> <div className={className + ' idle'} onClick={e => this.handleIdleSelectClick(e)}>
<ModeIcon mode={modeName}/> <ModeIcon mode={modeName}/>{mode.label}
</div> </div>
) )
} }

View File

@@ -2,25 +2,16 @@ import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import moment from 'moment' import moment from 'moment'
import _ from 'lodash' import _ from 'lodash'
import ModeIcon from 'browser/components/ModeIcon'
import MarkdownPreview from 'browser/components/MarkdownPreview' import MarkdownPreview from 'browser/components/MarkdownPreview'
import CodeEditor from 'browser/components/CodeEditor' import CodeEditor from 'browser/components/CodeEditor'
import { import {
IDLE_MODE,
EDIT_MODE,
switchMode,
switchArticle,
switchFolder, switchFolder,
clearSearch, cacheArticle,
lockStatus, saveArticle,
unlockStatus, destroyArticle
updateArticle,
destroyArticle,
NEW
} from '../../actions' } from '../../actions'
import linkState from 'browser/lib/linkState' import linkState from 'browser/lib/linkState'
import FolderMark from 'browser/components/FolderMark' import FolderMark from 'browser/components/FolderMark'
import TagLink from '../TagLink'
import TagSelect from 'browser/components/TagSelect' import TagSelect from 'browser/components/TagSelect'
import ModeSelect from 'browser/components/ModeSelect' import ModeSelect from 'browser/components/ModeSelect'
import activityRecord from 'browser/lib/activityRecord' import activityRecord from 'browser/lib/activityRecord'
@@ -123,45 +114,31 @@ export default class ArticleDetail extends React.Component {
clearInterval(this.refreshTimer) clearInterval(this.refreshTimer)
} }
componentDidUpdate (prevProps) { componentWillReceiveProps (nextProps) {
let isModeChanged = prevProps.status.mode !== this.props.status.mode if (nextProps.activeArticle == null || this.props.activeArticle == null || nextProps.activeArticle.key !== this.props.activeArticle.key) {
if (isModeChanged && this.props.status.mode === EDIT_MODE) { let nextArticle = nextProps.activeArticle
ReactDOM.findDOMNode(this.refs.title).focus() let nextModified = nextArticle != null ? _.findWhere(nextProps.modified, {key: nextArticle.key}) : null
let article = Object.assign({}, nextProps.activeArticle, nextModified)
this.setState({
article
})
} }
} }
componentWillReceiveProps (nextProps) { cacheArticle () {
let nextState = {} let { dispatch } = this.props
let isArticleChanged = nextProps.activeArticle != null && (nextProps.activeArticle.key !== this.state.article.key) dispatch(cacheArticle(this.props.activeArticle.key, this.state.article))
let isModeChanged = nextProps.status.mode !== this.props.status.mode
// Reset article input
if (isArticleChanged || (isModeChanged && nextProps.status.mode !== IDLE_MODE)) {
Object.assign(nextState, {
article: makeInstantArticle(nextProps.activeArticle)
})
}
// Clean state
if (isModeChanged) {
Object.assign(nextState, {
openDeleteConfirmMenu: false,
previewMode: false,
isArticleEdited: false,
isTagChanged: false,
isTitleChanged: false,
isContentChanged: false
})
}
this.setState(nextState)
} }
renderEmpty () { renderEmpty () {
return ( return (
<div className='ArticleDetail empty'> <div className='ArticleDetail empty'>
Command() + Enter to create a new post <div className='ArticleDetail-empty-box'>
<div className='ArticleDetail-empty-box-message'>Command() + N<br/>to create a new post</div>
</div>
</div> </div>
) )
} }
@@ -176,123 +153,46 @@ export default class ArticleDetail extends React.Component {
handleSaveButtonClick (e) { handleSaveButtonClick (e) {
let { dispatch, folders, status } = this.props let { dispatch, folders, status } = this.props
let article = this.state.article
let newArticle = Object.assign({}, article)
let folder = _.findWhere(folders, {key: article.FolderKey}) let targetFolderKey = this.state.article.FolderKey
if (folder == null) return false dispatch(saveArticle(this.props.activeArticle.key, this.state.article), true)
if (status.targetFolders.length > 0) {
dispatch(unlockStatus()) let targetFolder = _.findWhere(folders, {key: targetFolderKey})
dispatch(switchFolder(targetFolder.name))
newArticle.status = null
newArticle.updatedAt = new Date()
newArticle.title = newArticle.title.trim()
if (newArticle.createdAt == null) {
newArticle.createdAt = new Date()
if (newArticle.title.length === 0) {
newArticle.title = `Created at ${moment(newArticle.createdAt).format('YYYY/MM/DD HH:mm')}`
}
activityRecord.emit('ARTICLE_CREATE', {mode: newArticle.mode})
} else {
activityRecord.emit('ARTICLE_UPDATE', {mode: newArticle.mode})
} }
dispatch(updateArticle(newArticle))
dispatch(switchMode(IDLE_MODE))
// Folder filterがかかっている時に、
// Searchを初期化し、更新先のFolder filterをかける
// かかれていない時に
// Searchを初期化する
if (status.targetFolders.length > 0) dispatch(switchFolder(folder.name))
else dispatch(clearSearch())
dispatch(switchArticle(newArticle.key))
} }
handleFolderKeyChange (e) { handleFolderKeyChange (e) {
let article = this.state.article let article = this.state.article
article.FolderKey = e.target.value article.FolderKey = e.target.value
this.setState({article: article}) this.setState({article: article}, () => this.cacheArticle())
} }
handleTitleChange (e) { handleTitleChange (e) {
let { article } = this.state let { article } = this.state
article.title = e.target.value article.title = e.target.value
let _isTitleChanged = article.title !== this.props.activeArticle.title
let { isTagChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTitleChanged || isTagChanged || isContentChanged || isModeChanged
this.setState({ this.setState({
article, article
isTitleChanged: _isTitleChanged, }, () => this.cacheArticle())
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
} }
handleTagsChange (newTag, tags) { handleTagsChange (newTag, tags) {
let article = this.state.article let article = this.state.article
article.tags = tags article.tags = tags
let _isTagChanged = _.difference(article.tags, this.props.activeArticle.tags).length > 0 || _.difference(this.props.activeArticle.tags, article.tags).length > 0
let { isTitleChanged, isContentChanged, isArticleEdited, isModeChanged } = this.state
let _isArticleEdited = _isTagChanged || isTitleChanged || isContentChanged || isModeChanged
this.setState({ this.setState({
article, article
isTagChanged: _isTagChanged, }, () => this.cacheArticle())
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
} }
handleModeChange (value) { handleModeChange (value) {
let { article } = this.state let { article } = this.state
article.mode = value article.mode = value
let _isModeChanged = article.mode !== this.props.activeArticle.mode
let { isTagChanged, isContentChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isModeChanged || isTagChanged || isContentChanged || isTitleChanged
this.setState({ this.setState({
article, article
previewMode: false, }, () => this.cacheArticle())
isModeChanged: _isModeChanged,
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
} }
handleModeSelectBlur () { handleModeSelectBlur () {
@@ -302,34 +202,12 @@ export default class ArticleDetail extends React.Component {
} }
handleContentChange (e, value) { handleContentChange (e, value) {
let { status } = this.props
if (status.mode === IDLE_MODE) {
return
}
let { article } = this.state let { article } = this.state
article.content = value article.content = value
let _isContentChanged = article.content !== this.props.activeArticle.content
let { isTagChanged, isModeChanged, isArticleEdited, isTitleChanged } = this.state
let _isArticleEdited = _isContentChanged || isTagChanged || isModeChanged || isTitleChanged
this.setState({ this.setState({
article, article
isContentChanged: _isContentChanged, }, () => this.cacheArticle())
isArticleEdited: _isArticleEdited
}, () => {
if (isArticleEdited !== _isArticleEdited) {
let { dispatch } = this.props
if (_isArticleEdited) {
console.log('lockit')
dispatch(lockStatus())
} else {
console.log('unlockit')
dispatch(unlockStatus())
}
}
})
} }
handleTogglePreviewButtonClick (e) { handleTogglePreviewButtonClick (e) {
@@ -373,14 +251,16 @@ export default class ArticleDetail extends React.Component {
} }
render () { render () {
let { folders, status, tags, activeArticle, user } = this.props let { folders, status, tags, activeArticle, modified, user } = this.props
if (activeArticle == null) return this.renderEmpty()
let folderOptions = folders.map(folder => { let folderOptions = folders.map(folder => {
return ( return (
<option key={folder.key} value={folder.key}>{folder.name}</option> <option key={folder.key} value={folder.key}>{folder.name}</option>
) )
}) })
let isUnsaved = !!_.findWhere(modified, {key: activeArticle.key})
return ( return (
<div className='ArticleDetail'> <div className='ArticleDetail'>
<div className='ArticleDetail-info'> <div className='ArticleDetail-info'>
@@ -392,7 +272,13 @@ export default class ArticleDetail extends React.Component {
> >
{folderOptions} {folderOptions}
</select> </select>
{this.state.isArticleEdited ? ' (edited)' : ''} <span className='ArticleDetail-info-status'
children={
isUnsaved
? <span> <span className='unsaved-mark'></span> Unsaved</span>
: `Created : ${moment(this.state.article.createdAt).format('YYYY/MM/DD')} Updated : ${moment(this.state.article.updatedAt).format('YYYY/MM/DD')}`
}
/>
<div className='ArticleDetail-info-control'> <div className='ArticleDetail-info-control'>
<ShareButton <ShareButton
@@ -404,8 +290,8 @@ export default class ArticleDetail extends React.Component {
<i className='fa fa-fw fa-clipboard'/><span className='tooltip'>Copy to clipboard</span> <i className='fa fa-fw fa-clipboard'/><span className='tooltip'>Copy to clipboard</span>
</button> </button>
<button onClick={e => this.handleEditButtonClick(e)}> <button onClick={e => this.handleSaveButtonClick(e)}>
<i className='fa fa-fw fa-edit'/><span className='tooltip'>Save ( + s)</span> <i className='fa fa-fw fa-save'/><span className='tooltip'>Save ( + s)</span>
</button> </button>
<button onClick={e => this.handleDeleteButtonClick(e)}> <button onClick={e => this.handleDeleteButtonClick(e)}>
<i className='fa fa-fw fa-trash'/><span className='tooltip'>Delete</span> <i className='fa fa-fw fa-trash'/><span className='tooltip'>Delete</span>
@@ -426,6 +312,15 @@ export default class ArticleDetail extends React.Component {
<div className='ArticleDetail-panel'> <div className='ArticleDetail-panel'>
<div className='ArticleDetail-panel-header'> <div className='ArticleDetail-panel-header'>
<div className='ArticleDetail-panel-header-title'>
<input
onKeyDown={e => this.handleTitleKeyDown(e)}
placeholder='(Untitled)'
ref='title'
value={this.state.article.title}
onChange={e => this.handleTitleChange(e)}
/>
</div>
<ModeSelect <ModeSelect
ref='mode' ref='mode'
onChange={e => this.handleModeChange(e)} onChange={e => this.handleModeChange(e)}
@@ -433,17 +328,6 @@ export default class ArticleDetail extends React.Component {
className='ArticleDetail-panel-header-mode' className='ArticleDetail-panel-header-mode'
onBlur={() => this.handleModeSelectBlur()} onBlur={() => this.handleModeSelectBlur()}
/> />
<div className='ArticleDetail-panel-header-title'>
<input
onKeyDown={e => this.handleTitleKeyDown(e)}
placeholder={this.state.article.createdAt == null
? `Created at ${moment().format('YYYY/MM/DD HH:mm')}`
: 'Title'}
ref='title'
value={this.state.article.title}
onChange={e => this.handleTitleChange(e)}
/>
</div>
</div> </div>
{status.isTutorialOpen ? modeSelectTutorialElement : null} {status.isTutorialOpen ? modeSelectTutorialElement : null}
@@ -466,6 +350,7 @@ export default class ArticleDetail extends React.Component {
ArticleDetail.propTypes = { ArticleDetail.propTypes = {
status: PropTypes.shape(), status: PropTypes.shape(),
activeArticle: PropTypes.shape(), activeArticle: PropTypes.shape(),
modified: PropTypes.array,
user: PropTypes.shape(), user: PropTypes.shape(),
folders: PropTypes.array, folders: PropTypes.array,
tags: PropTypes.array, tags: PropTypes.array,

View File

@@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'
import ReactDOM from 'react-dom' import ReactDOM from 'react-dom'
import ModeIcon from 'browser/components/ModeIcon' import ModeIcon from 'browser/components/ModeIcon'
import moment from 'moment' import moment from 'moment'
import { switchArticle, NEW } from '../actions' import { switchArticle } from '../actions'
import FolderMark from 'browser/components/FolderMark' import FolderMark from 'browser/components/FolderMark'
import TagLink from './TagLink' import TagLink from './TagLink'
import _ from 'lodash' import _ from 'lodash'
@@ -64,40 +64,58 @@ export default class ArticleList extends React.Component {
handleArticleClick (article) { handleArticleClick (article) {
let { dispatch } = this.props let { dispatch } = this.props
return function (e) { return function (e) {
if (article.status === NEW) return null
dispatch(switchArticle(article.key)) dispatch(switchArticle(article.key))
} }
} }
render () { render () {
let { articles, activeArticle, folders } = this.props let { articles, modified, activeArticle, folders } = this.props
let articleElements = articles.map(article => { let articleElements = articles.map(article => {
let modifiedArticle = _.findWhere(modified, {key: article.key})
let originalArticle = article
if (modifiedArticle) {
article = Object.assign({}, article, modifiedArticle)
}
let tagElements = Array.isArray(article.tags) && article.tags.length > 0 let tagElements = Array.isArray(article.tags) && article.tags.length > 0
? article.tags.map(tag => { ? article.tags.map(tag => {
return (<TagLink key={tag} tag={tag}/>) return (<TagLink key={tag} tag={tag}/>)
}) })
: (<span>Not tagged yet</span>) : (<span>Not tagged yet</span>)
let folder = _.findWhere(folders, {key: article.FolderKey}) let folder = _.findWhere(folders, {key: article.FolderKey})
let folderChanged = originalArticle.FolderKey !== article.FolderKey
let originalFolder = folderChanged ? _.findWhere(folders, {key: originalArticle.FolderKey}) : null
let title = article.status !== NEW let title = article.title.trim().length === 0
? article.title.trim().length === 0
? <small>(Untitled)</small> ? <small>(Untitled)</small>
: article.title : article.title
: '(New article)'
return ( return (
<div key={'article-' + article.key}> <div key={'article-' + article.key}>
<div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}> <div onClick={e => this.handleArticleClick(article)(e)} className={'articleItem' + (activeArticle.key === article.key ? ' active' : '')}>
<div className='top'> <div className='top'>
{folder != null {folder != null
? <span className='folderName'><FolderMark color={folder.color}/>{folder.name}</span> ? folderChanged
? <span className='folderName'>
<FolderMark color={originalFolder.color}/>{originalFolder.name}
->
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span className='folderName'>
<FolderMark color={folder.color}/>{folder.name}
</span>
: <span><FolderMark color={-1}/>Unknown</span> : <span><FolderMark color={-1}/>Unknown</span>
} }
<span className='updatedAt'>{article.status != null ? article.status : moment(article.updatedAt).fromNow()}</span> <span className='updatedAt'
children={
modifiedArticle != null
? <span><span className='unsaved-mark'></span> Unsaved</span>
: moment(article.updatedAt).fromNow()
}
/>
</div> </div>
<div className='middle'> <div className='middle'>
<ModeIcon className='mode' mode={article.mode}/> <div className='title'>{title}</div> <ModeIcon className='mode' mode={article.mode}/> <div className='title' children={title}/>
</div> </div>
<div className='bottom'> <div className='bottom'>
<div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div> <div className='tags'><i className='fa fa-fw fa-tags'/>{tagElements}</div>
@@ -119,6 +137,7 @@ export default class ArticleList extends React.Component {
ArticleList.propTypes = { ArticleList.propTypes = {
folders: PropTypes.array, folders: PropTypes.array,
articles: PropTypes.array, articles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(), activeArticle: PropTypes.shape(),
dispatch: PropTypes.func dispatch: PropTypes.func
} }

View File

@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import { findWhere } from 'lodash' import { findWhere } from 'lodash'
import { setSearchFilter, switchFolder, switchMode, switchArticle, updateArticle, clearNewArticle, EDIT_MODE } from '../actions' import { setSearchFilter, switchFolder, saveArticle } from '../actions'
import { openModal } from 'browser/lib/modal' import { openModal } from 'browser/lib/modal'
import FolderMark from 'browser/components/FolderMark' import FolderMark from 'browser/components/FolderMark'
import Preferences from '../modal/Preferences' import Preferences from '../modal/Preferences'
@@ -71,20 +71,17 @@ export default class ArticleNavigator extends React.Component {
: folders[0].key : folders[0].key
let newArticle = { let newArticle = {
id: null,
key: keygen(), key: keygen(),
title: '', title: '',
content: '', content: '',
mode: 'markdown', mode: 'markdown',
tags: [], tags: [],
FolderKey: FolderKey, FolderKey: FolderKey,
status: 'NEW' craetedAt: new Date(),
updatedAt: new Date()
} }
dispatch(clearNewArticle()) dispatch(saveArticle(newArticle.key, newArticle, true))
dispatch(updateArticle(newArticle))
dispatch(switchArticle(newArticle.key, true))
dispatch(switchMode(EDIT_MODE))
} }
handleNewFolderButton (e) { handleNewFolderButton (e) {

View File

@@ -1,10 +1,10 @@
import React, { PropTypes} from 'react' import React, { PropTypes} from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { EDIT_MODE, IDLE_MODE, toggleTutorial } from './actions' import { toggleTutorial } from '../actions'
import ArticleNavigator from './HomePage/ArticleNavigator' import ArticleNavigator from './ArticleNavigator'
import ArticleTopBar from './HomePage/ArticleTopBar' import ArticleTopBar from './ArticleTopBar'
import ArticleList from './HomePage/ArticleList' import ArticleList from './ArticleList'
import ArticleDetail from './HomePage/ArticleDetail' import ArticleDetail from './ArticleDetail'
import _ from 'lodash' import _ from 'lodash'
import { isModalOpen, closeModal } from 'browser/lib/modal' import { isModalOpen, closeModal } from 'browser/lib/modal'
@@ -48,65 +48,27 @@ class HomePage extends React.Component {
return return
} }
switch (status.mode) { // `detail`の`openDeleteConfirmMenu`が`true`なら呼ばれない。
if (e.keyCode === 27 || (e.keyCode === 70 && cmdOrCtrl)) {
top.focusInput()
}
case EDIT_MODE: if (e.keyCode === 38) {
if (e.keyCode === 27) { list.selectPriorArticle()
detail.handleCancelButtonClick() }
}
if ((e.keyCode === 13 && (cmdOrCtrl)) || (e.keyCode === 83 && (cmdOrCtrl))) {
detail.handleSaveButtonClick()
}
if (e.keyCode === 80 && cmdOrCtrl) {
detail.handleTogglePreviewButtonClick()
}
if (e.keyCode === 78 && cmdOrCtrl) {
nav.handleNewPostButtonClick()
e.preventDefault()
}
break
case IDLE_MODE:
if (e.keyCode === 69) {
detail.handleEditButtonClick()
e.preventDefault()
}
if (e.keyCode === 68) {
detail.handleDeleteButtonClick()
}
// `detail`の`openDeleteConfirmMenu`の時。 if (e.keyCode === 40) {
if (detail.state.openDeleteConfirmMenu) { list.selectNextArticle()
if (e.keyCode === 27) { }
detail.handleDeleteCancelButtonClick()
}
if (e.keyCode === 13 && cmdOrCtrl) {
detail.handleDeleteConfirmButtonClick()
}
break
}
// `detail`の`openDeleteConfirmMenu`が`true`なら呼ばれない。 if (e.keyCode === 78 && cmdOrCtrl) {
if (e.keyCode === 27 || (e.keyCode === 70 && cmdOrCtrl)) { nav.handleNewPostButtonClick()
top.focusInput() e.preventDefault()
}
if (e.keyCode === 38) {
list.selectPriorArticle()
}
if (e.keyCode === 40) {
list.selectNextArticle()
}
if ((e.keyCode === 65 && !e.metaKey && !e.ctrlKey) || (e.keyCode === 13 && cmdOrCtrl) || (e.keyCode === 78 && cmdOrCtrl)) {
nav.handleNewPostButtonClick()
e.preventDefault()
}
} }
} }
render () { render () {
let { dispatch, status, user, articles, allArticles, activeArticle, folders, tags, filters } = this.props let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags, filters } = this.props
return ( return (
<div className='HomePage'> <div className='HomePage'>
@@ -128,6 +90,7 @@ class HomePage extends React.Component {
dispatch={dispatch} dispatch={dispatch}
folders={folders} folders={folders}
articles={articles} articles={articles}
modified={modified}
status={status} status={status}
activeArticle={activeArticle} activeArticle={activeArticle}
/> />
@@ -136,6 +99,7 @@ class HomePage extends React.Component {
dispatch={dispatch} dispatch={dispatch}
user={user} user={user}
activeArticle={activeArticle} activeArticle={activeArticle}
modified={modified}
folders={folders} folders={folders}
status={status} status={status}
tags={tags} tags={tags}
@@ -174,9 +138,12 @@ function startsWith (target, needle) {
} }
function remap (state) { function remap (state) {
let { user, folders, articles, status } = state let { user, folders, status } = state
let _articles = state.articles
let articles = _articles != null ? _articles.data : []
let modified = _articles != null ? _articles.modified : []
if (articles == null) articles = []
articles.sort((a, b) => { articles.sort((a, b) => {
return new Date(b.updatedAt) - new Date(a.updatedAt) return new Date(b.updatedAt) - new Date(a.updatedAt)
}) })
@@ -239,8 +206,9 @@ function remap (state) {
user, user,
folders, folders,
status, status,
allArticles,
articles, articles,
allArticles,
modified,
activeArticle, activeArticle,
tags, tags,
filters: { filters: {
@@ -258,6 +226,7 @@ HomePage.propTypes = {
}), }),
articles: PropTypes.array, articles: PropTypes.array,
allArticles: PropTypes.array, allArticles: PropTypes.array,
modified: PropTypes.array,
activeArticle: PropTypes.shape(), activeArticle: PropTypes.shape(),
dispatch: PropTypes.func, dispatch: PropTypes.func,
folders: PropTypes.array, folders: PropTypes.array,

View File

@@ -4,24 +4,20 @@ export const USER_UPDATE = 'USER_UPDATE'
export const CLEAR_NEW_ARTICLE = 'CLEAR_NEW_ARTICLE' export const CLEAR_NEW_ARTICLE = 'CLEAR_NEW_ARTICLE'
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE' export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY' export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
export const ARTICLE_SAVE = 'ARTICLE_SAVE'
export const ARTICLE_CACHE = 'ARTICLE_CACHE'
export const FOLDER_CREATE = 'FOLDER_CREATE' export const FOLDER_CREATE = 'FOLDER_CREATE'
export const FOLDER_UPDATE = 'FOLDER_UPDATE' export const FOLDER_UPDATE = 'FOLDER_UPDATE'
export const FOLDER_DESTROY = 'FOLDER_DESTROY' export const FOLDER_DESTROY = 'FOLDER_DESTROY'
export const FOLDER_REPLACE = 'FOLDER_REPLACE' export const FOLDER_REPLACE = 'FOLDER_REPLACE'
export const SWITCH_FOLDER = 'SWITCH_FOLDER' export const SWITCH_FOLDER = 'SWITCH_FOLDER'
export const SWITCH_MODE = 'SWITCH_MODE'
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE' export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER' export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
export const SET_TAG_FILTER = 'SET_TAG_FILTER' export const SET_TAG_FILTER = 'SET_TAG_FILTER'
export const CLEAR_SEARCH = 'CLEAR_SEARCH' export const CLEAR_SEARCH = 'CLEAR_SEARCH'
export const LOCK_STATUS = 'LOCK_STATUS'
export const UNLOCK_STATUS = 'UNLOCK_STATUS'
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
// Status - mode export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
export const IDLE_MODE = 'IDLE_MODE'
export const EDIT_MODE = 'EDIT_MODE'
// Article status // Article status
export const NEW = 'NEW' export const NEW = 'NEW'
@@ -40,6 +36,20 @@ export function clearNewArticle () {
} }
} }
export function cacheArticle (key, article) {
return {
type: ARTICLE_CACHE,
data: { key, article }
}
}
export function saveArticle (key, article, forceSwitch) {
return {
type: ARTICLE_SAVE,
data: { key, article, forceSwitch }
}
}
export function updateArticle (article) { export function updateArticle (article) {
return { return {
type: ARTICLE_UPDATE, type: ARTICLE_UPDATE,
@@ -92,19 +102,11 @@ export function switchFolder (folderName) {
} }
} }
export function switchMode (mode) { export function switchArticle (articleKey) {
return {
type: SWITCH_MODE,
data: mode
}
}
export function switchArticle (articleKey, isNew) {
return { return {
type: SWITCH_ARTICLE, type: SWITCH_ARTICLE,
data: { data: {
key: articleKey, key: articleKey
isNew: isNew
} }
} }
} }
@@ -129,18 +131,6 @@ export function clearSearch () {
} }
} }
export function lockStatus () {
return {
type: LOCK_STATUS
}
}
export function unlockStatus () {
return {
type: UNLOCK_STATUS
}
}
export function toggleTutorial () { export function toggleTutorial () {
return { return {
type: TOGGLE_TUTORIAL type: TOGGLE_TUTORIAL
@@ -152,17 +142,16 @@ export default {
clearNewArticle, clearNewArticle,
updateArticle, updateArticle,
destroyArticle, destroyArticle,
cacheArticle,
saveArticle,
createFolder, createFolder,
updateFolder, updateFolder,
destroyFolder, destroyFolder,
replaceFolder, replaceFolder,
switchFolder, switchFolder,
switchMode,
switchArticle, switchArticle,
setSearchFilter, setSearchFilter,
setTagFilter, setTagFilter,
clearSearch, clearSearch,
lockStatus,
unlockStatus,
toggleTutorial toggleTutorial
} }

View File

@@ -3,13 +3,10 @@ import _ from 'lodash'
import { import {
// Status action type // Status action type
SWITCH_FOLDER, SWITCH_FOLDER,
SWITCH_MODE,
SWITCH_ARTICLE, SWITCH_ARTICLE,
SET_SEARCH_FILTER, SET_SEARCH_FILTER,
SET_TAG_FILTER, SET_TAG_FILTER,
CLEAR_SEARCH, CLEAR_SEARCH,
LOCK_STATUS,
UNLOCK_STATUS,
TOGGLE_TUTORIAL, TOGGLE_TUTORIAL,
// user // user
@@ -18,38 +15,33 @@ import {
// Article action type // Article action type
ARTICLE_UPDATE, ARTICLE_UPDATE,
ARTICLE_DESTROY, ARTICLE_DESTROY,
CLEAR_NEW_ARTICLE, ARTICLE_CACHE,
ARTICLE_SAVE,
// Folder action type // Folder action type
FOLDER_CREATE, FOLDER_CREATE,
FOLDER_UPDATE, FOLDER_UPDATE,
FOLDER_DESTROY, FOLDER_DESTROY,
FOLDER_REPLACE, FOLDER_REPLACE
// view mode
IDLE_MODE
} from './actions' } from './actions'
import dataStore from 'browser/lib/dataStore' import dataStore from 'browser/lib/dataStore'
import keygen from 'browser/lib/keygen' import keygen from 'browser/lib/keygen'
import activityRecord from 'browser/lib/activityRecord' import activityRecord from 'browser/lib/activityRecord'
import { openModal } from 'browser/lib/modal'
import EditedAlert from './modal/EditedAlert'
const initialStatus = { const initialStatus = {
mode: IDLE_MODE,
search: '', search: '',
isTutorialOpen: false, isTutorialOpen: false,
isStatusLocked: false isStatusLocked: false
} }
let data = dataStore.getData() let data = dataStore.getData()
let initialArticles = data.articles let initialArticles = {
data: data.articles,
modified: []
}
let initialFolders = data.folders let initialFolders = data.folders
let initialUser = dataStore.getUser().user let initialUser = dataStore.getUser().user
let isStatusLocked = false
let isCreatingNew = false
function user (state = initialUser, action) { function user (state = initialUser, action) {
switch (action.type) { switch (action.type) {
case USER_UPDATE: case USER_UPDATE:
@@ -140,58 +132,84 @@ function folders (state = initialFolders, action) {
return state return state
} }
} }
let isCleaned = true
function articles (state = initialArticles, action) {
state = state.slice()
if (!isCreatingNew && !isCleaned) { function compareArticle (original, modified) {
state = state.filter(article => article.status !== 'NEW') var keys = _.keys(_.pick(modified, ['mode', 'title', 'tags', 'content', 'FolderKey']))
isCleaned = true
} return keys.reduce((sum, key) => {
if (original[key] !== modified[key]) {
if (sum == null) {
sum = {
key: original.key
}
}
sum[key] = modified[key]
}
return sum
}, null)
}
function articles (state = initialArticles, action) {
switch (action.type) { switch (action.type) {
case SWITCH_ARTICLE: case ARTICLE_CACHE:
if (action.data.isNew) { {
isCleaned = false let modified = action.data.article
} let targetKey = action.data.key
if (!isStatusLocked && !action.data.isNew) { let originalIndex = _.findIndex(state.data, _article => targetKey === _article.key)
isCreatingNew = false if (originalIndex === -1) return state
if (!isCleaned) { let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
state = state.filter(article => article.status !== 'NEW')
isCleaned = true modified = compareArticle(state.data[originalIndex], modified)
if (modified == null) {
if (modifiedIndex !== -1) state.modified.splice(modifiedIndex, 1)
return state
} }
if (modifiedIndex === -1) state.modified.push(modified)
else Object.assign(state.modified[modifiedIndex], modified)
return state
} }
return state case ARTICLE_SAVE:
case SWITCH_FOLDER: {
case SET_SEARCH_FILTER: let targetKey = action.data.key
case SET_TAG_FILTER: let override = action.data.article
case CLEAR_SEARCH: let modifiedIndex = _.findIndex(state.modified, _article => targetKey === _article.key)
if (!isStatusLocked) { let modified = modifiedIndex !== -1 ? state.modified.splice(modifiedIndex, 1)[0] : null
isCreatingNew = false
if (!isCleaned) { let targetIndex = _.findIndex(state.data, _article => targetKey === _article.key)
state = state.filter(article => article.status !== 'NEW') // Make a new if target article is not found.
isCleaned = true if (targetIndex === -1) {
state.data.push(Object.assign({
title: '',
content: '',
mode: 'markdown',
tags: [],
craetedAt: new Date()
}, modified, override, {key: targetKey, updatedAt: new Date()}))
return state
} }
Object.assign(state.data[targetIndex], modified, override, {key: targetKey, updatedAt: new Date()})
dataStore.setArticles(state.data)
return state
} }
return state
case CLEAR_NEW_ARTICLE:
return state.filter(article => article.status !== 'NEW')
case ARTICLE_UPDATE: case ARTICLE_UPDATE:
{ {
let article = action.data.article let article = action.data.article
let targetIndex = _.findIndex(state, _article => article.key === _article.key) let targetIndex = _.findIndex(state.data, _article => article.key === _article.key)
if (targetIndex < 0) state.unshift(article) if (targetIndex < 0) state.data.unshift(article)
else Object.assign(state[targetIndex], article) else Object.assign(state.data[targetIndex], article)
if (article.status !== 'NEW') dataStore.setArticles(state) dataStore.setArticles(state.data)
else isCreatingNew = true
return state return state
} }
case ARTICLE_DESTROY: case ARTICLE_DESTROY:
{ {
let articleKey = action.data.key let articleKey = action.data.key
let targetIndex = _.findIndex(state, _article => articleKey === _article.key) let targetIndex = _.findIndex(state.data, _article => articleKey === _article.key)
if (targetIndex >= 0) state.splice(targetIndex, 1) if (targetIndex >= 0) state.splice(targetIndex, 1)
dataStore.setArticles(state) dataStore.setArticles(state)
@@ -217,47 +235,34 @@ function status (state = initialStatus, action) {
case TOGGLE_TUTORIAL: case TOGGLE_TUTORIAL:
state.isTutorialOpen = !state.isTutorialOpen state.isTutorialOpen = !state.isTutorialOpen
return state return state
case LOCK_STATUS:
isStatusLocked = state.isStatusLocked = true
return state
case UNLOCK_STATUS:
isStatusLocked = state.isStatusLocked = false
return state
} }
// if status locked, status become unmutable
if (state.isStatusLocked) {
openModal(EditedAlert, {action})
return state
}
switch (action.type) { switch (action.type) {
case SWITCH_FOLDER: case ARTICLE_SAVE:
state.mode = IDLE_MODE if (action.data.forceSwitch) {
state.search = `//${action.data} ` let article = action.data.article
state.articleKey = article.key
state.search = ''
}
return state return state
case SWITCH_MODE: case SWITCH_FOLDER:
state.mode = action.data state.search = `//${action.data} `
return state return state
case SWITCH_ARTICLE: case SWITCH_ARTICLE:
state.articleKey = action.data.key state.articleKey = action.data.key
state.mode = IDLE_MODE
return state return state
case SET_SEARCH_FILTER: case SET_SEARCH_FILTER:
state.search = action.data state.search = action.data
state.mode = IDLE_MODE
return state return state
case SET_TAG_FILTER: case SET_TAG_FILTER:
state.search = `#${action.data}` state.search = `#${action.data}`
state.mode = IDLE_MODE
return state return state
case CLEAR_SEARCH: case CLEAR_SEARCH:
state.search = '' state.search = ''
state.mode = IDLE_MODE
return state return state
default: default:

View File

@@ -15,7 +15,6 @@ infoButton()
padding 0 padding 0
&:hover &:hover
color inherit color inherit
.ArticleDetail .ArticleDetail
absolute right bottom absolute right bottom
top 60px top 60px
@@ -24,6 +23,17 @@ infoButton()
background-color #E6E6E6 background-color #E6E6E6
border-top 1px solid borderColor border-top 1px solid borderColor
border-left 1px solid borderColor border-left 1px solid borderColor
&.empty
.ArticleDetail-empty-box
line-height 72px
font-size 42px
height 320px
display flex
align-items center
.ArticleDetail-empty-box-message
text-align center
width 100%
color inactiveTextColor
* *
-webkit-user-select none -webkit-user-select none
.ArticleDetail-info .ArticleDetail-info
@@ -50,6 +60,10 @@ infoButton()
&>.tutorial &>.tutorial
position fixed position fixed
z-index 35 z-index 35
.ArticleDetail-info-status
padding 0 5px
.unsaved-mark
color brandColor
.ArticleDetail-info-control .ArticleDetail-info-control
float right float right
.ShareButton .ShareButton
@@ -174,30 +188,26 @@ infoButton()
z-index 30 z-index 30
background-color white background-color white
absolute top bottom absolute top bottom
left 10px right 10px
display block display block
height 33px height 33px
margin-top 14px margin-top 14px
width 33px width 120px
margin-right 15px margin-right 15px
border solid 1px transparent border solid 1px borderColor
border-radius 5px border-radius 5px
transition width 0.15s transition width 0.15s
&.idle &.idle
cursor pointer cursor pointer
&:hover &:hover
background-color darken(white, 5%) background-color darken(white, 5%)
border-color borderColor
.ModeIcon .ModeIcon
float left padding 0 5px
width 100%
line-height 33px line-height 33px
text-align center
&.edit &.edit
width 150px
border-color iptFocusBorderColor border-color iptFocusBorderColor
input input
width 150px width 120px
line-height 31px line-height 31px
padding 0 10px padding 0 10px
border none border none
@@ -206,7 +216,7 @@ infoButton()
font-size 14px font-size 14px
.ModeSelect-options .ModeSelect-options
position fixed position fixed
width 150px width 120px
z-index 10 z-index 10
border 1px solid borderColor border 1px solid borderColor
border-radius 5px border-radius 5px
@@ -228,8 +238,8 @@ infoButton()
&:hover &:hover
background-color darken(white, 10%) background-color darken(white, 10%)
.ArticleDetail-panel-header-title .ArticleDetail-panel-header-title
absolute left top right absolute left top
left 33px right 120px
padding 0 15px padding 0 15px
background-color transparent background-color transparent
input input

View File

@@ -33,6 +33,8 @@ articleItemColor = #777
.updatedAt .updatedAt
float right float right
line-height 20px line-height 20px
.unsaved-mark
color brandColor
.middle .middle
padding 3px 0 7px padding 3px 0 7px
font-size 16px font-size 16px