mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +00:00
using Repository class
This commit is contained in:
@@ -1,312 +0,0 @@
|
|||||||
const keygen = require('browser/lib/keygen')
|
|
||||||
const fs = require('fs')
|
|
||||||
const path = require('path')
|
|
||||||
const CSON = require('season')
|
|
||||||
const _ = require('lodash')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # Repo structure
|
|
||||||
*
|
|
||||||
* ```
|
|
||||||
* root
|
|
||||||
* |- data
|
|
||||||
* |-note1.cson
|
|
||||||
* |-note2.cson
|
|
||||||
* |-note3.cson
|
|
||||||
* |- boostrepo.json
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## `boostrepo.json`
|
|
||||||
*
|
|
||||||
* ```js
|
|
||||||
* {
|
|
||||||
* name: String,
|
|
||||||
* author: String, // Same convention of package.json, `John Doe <email@example.com> (http://example.com)`
|
|
||||||
* remotes: [{
|
|
||||||
* name: String,
|
|
||||||
* url: String, // url of git remote
|
|
||||||
* branch: String // if branch isn't set, it will try to use `master` branch.
|
|
||||||
* }],
|
|
||||||
* folders: [{
|
|
||||||
* key: String // Unique sha1 hash key to identify folder,
|
|
||||||
* name: String,
|
|
||||||
* color: String // All CSS color formats available.
|
|
||||||
* }]
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* ## `data` directory
|
|
||||||
*
|
|
||||||
* Every note will be saved here as a single CSON file to `git diff` efficiently.
|
|
||||||
* > This is because CSON supports Multiline string.
|
|
||||||
* File name of each cson file will be used to identify note.
|
|
||||||
* Commonly, Boostnote will automatically generate sha1 key and use it as a file name when creating a new note.
|
|
||||||
*
|
|
||||||
* ### `note.cson`
|
|
||||||
*
|
|
||||||
* ```cson
|
|
||||||
* name: String
|
|
||||||
* tags: [String] // tags
|
|
||||||
* folder: String // hash key of folder
|
|
||||||
* mode: String // syntax mode
|
|
||||||
* title: String
|
|
||||||
* content: String
|
|
||||||
* createdAt: Date
|
|
||||||
* updatedAt: Date
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # Resolve directory.
|
|
||||||
*
|
|
||||||
* If directory doesn't exist, it will try to make a new one.
|
|
||||||
* If failed return rejected promise
|
|
||||||
*
|
|
||||||
* @param {String} targetPath Target path of directory
|
|
||||||
* @return {Promise} [description]
|
|
||||||
*/
|
|
||||||
function _resolveDirectory (targetPath) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
// check the directory exists
|
|
||||||
fs.stat(targetPath, function (err, stat) {
|
|
||||||
// Reject errors except no suchfile
|
|
||||||
if (err != null && err.code !== 'ENOENT') {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle no suchfile error only
|
|
||||||
// Make new Folder by given path
|
|
||||||
if (err != null) {
|
|
||||||
return fs.mkdir(targetPath, function (err, stat) {
|
|
||||||
// If failed to make a new directory, reject it.
|
|
||||||
if (err != null) {
|
|
||||||
return reject(err)
|
|
||||||
}
|
|
||||||
resolve(targetPath)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the target is not a directory
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
return reject(new Error(targetPath + ' path is not a directory'))
|
|
||||||
}
|
|
||||||
resolve(targetPath)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function _generateDefaultRepoJSON (override) {
|
|
||||||
return Object.assign({
|
|
||||||
name: 'default',
|
|
||||||
remotes: [],
|
|
||||||
folders: [{
|
|
||||||
key: keygen(),
|
|
||||||
name: 'general',
|
|
||||||
color: 'green'
|
|
||||||
}]
|
|
||||||
}, override)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* # Resolve RepoJSON
|
|
||||||
*
|
|
||||||
* Every repository must have `boostrepo.json`
|
|
||||||
*
|
|
||||||
* If boostrepo.json doesn't exist, create new one.
|
|
||||||
*
|
|
||||||
* @param {[type]} targetPath [description]
|
|
||||||
* @return {[type]} [description]
|
|
||||||
*/
|
|
||||||
function _resolveRepoJSON (targetPath) {
|
|
||||||
return new Promise(function checkIfExists (resolve, reject) {
|
|
||||||
// If JSON doesn't exist, make a new one.
|
|
||||||
if (CSON.resolve(targetPath) == null) {
|
|
||||||
let newRepoJSON = _generateDefaultRepoJSON()
|
|
||||||
return CSON.writeFile(targetPath, newRepoJSON, function (err) {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
resolve(newRepoJSON)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
CSON.readFile(targetPath, function (err, obj) {
|
|
||||||
if (err != null) return reject(err)
|
|
||||||
resolve(obj)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all repository stats from localStorage
|
|
||||||
* it is stored to `repositories` key.
|
|
||||||
* if the data is corrupted, re-intialize it.
|
|
||||||
*
|
|
||||||
* @return {Array} registered repositories
|
|
||||||
* ```
|
|
||||||
* [{
|
|
||||||
* key: String,
|
|
||||||
* name: String,
|
|
||||||
* path: String // path of repository
|
|
||||||
* }]
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
function getAllRepoStats () {
|
|
||||||
let data
|
|
||||||
try {
|
|
||||||
data = JSON.parse(localStorage.getItem('repoStats'))
|
|
||||||
if (!_.isArray(data)) {
|
|
||||||
throw new Error('Data is corrupted. it must be an array.')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
data = []
|
|
||||||
_saveAllRepoStats(data)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save All Repos
|
|
||||||
*/
|
|
||||||
function _saveAllRepoStats (repoStats) {
|
|
||||||
localStorage.setItem('repoStats', JSON.stringify(repoStats))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add repository and return new Repo
|
|
||||||
* @param {Object} newRepo [description]
|
|
||||||
* ```
|
|
||||||
* {
|
|
||||||
* key: String,
|
|
||||||
* name: String,
|
|
||||||
* path: String,
|
|
||||||
* status: String,
|
|
||||||
* folders: [{
|
|
||||||
* key: String,
|
|
||||||
* color: String,
|
|
||||||
* name: String
|
|
||||||
* }],
|
|
||||||
* notes: [{
|
|
||||||
* key: String,
|
|
||||||
* title: String,
|
|
||||||
* content: String,
|
|
||||||
* folder: String,
|
|
||||||
* tags: [String],
|
|
||||||
* createdAt: Date,
|
|
||||||
* updatedAt: Date
|
|
||||||
* }]
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
function addRepo (newRepo) {
|
|
||||||
let { targetPath, name } = newRepo
|
|
||||||
targetPath = path.resolve(targetPath)
|
|
||||||
|
|
||||||
let repoStat, repoJSON
|
|
||||||
return _resolveDirectory(targetPath)
|
|
||||||
.then(function initializeRepo () {
|
|
||||||
let resolveDataDirectory = _resolveDirectory(path.resolve(targetPath, 'data'))
|
|
||||||
let resolveBoostrepoJSON = _resolveRepoJSON(path.resolve(targetPath, 'boostrepo.json'))
|
|
||||||
return Promise.all([resolveDataDirectory, resolveBoostrepoJSON])
|
|
||||||
})
|
|
||||||
.then(function saveToLocalStorage (data) {
|
|
||||||
let dataPath = data[0]
|
|
||||||
repoJSON = data[1]
|
|
||||||
|
|
||||||
let repoStats = getAllRepoStats()
|
|
||||||
|
|
||||||
// generate unique key
|
|
||||||
let key = keygen()
|
|
||||||
while (repoStats.some((repoStat) => repoStat.key === key)) {
|
|
||||||
key = keygen()
|
|
||||||
}
|
|
||||||
|
|
||||||
repoStat = {
|
|
||||||
key,
|
|
||||||
name: name,
|
|
||||||
path: targetPath
|
|
||||||
}
|
|
||||||
|
|
||||||
repoStats.push(repoStat)
|
|
||||||
_saveAllRepoStats(repoStats)
|
|
||||||
|
|
||||||
return dataPath
|
|
||||||
})
|
|
||||||
.then(function fetchNotes (dataPath) {
|
|
||||||
let noteNames = fs.readdirSync(dataPath)
|
|
||||||
let notes = noteNames
|
|
||||||
.map((noteName) => {
|
|
||||||
let notePath = path.resolve(dataPath, noteNames)
|
|
||||||
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
CSON.readFile(notePath, function (err, obj) {
|
|
||||||
if (err != null) {
|
|
||||||
console.log(err)
|
|
||||||
return resolve(null)
|
|
||||||
}
|
|
||||||
obj.key = path.basename(noteName, '.cson')
|
|
||||||
return resolve(obj)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.filter((note) => note != null)
|
|
||||||
|
|
||||||
return Promise.all(notes)
|
|
||||||
})
|
|
||||||
.then(function resolveRepo (notes) {
|
|
||||||
return Object.assign({}, repoJSON, repoStat, {
|
|
||||||
status: 'IDLE',
|
|
||||||
notes
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRepo (repository) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
|
||||||
try {
|
|
||||||
let repoStats = getAllRepoStats()
|
|
||||||
let targetIndex = _.findIndex(repoStats, {key: repository.key})
|
|
||||||
if (targetIndex > -1) {
|
|
||||||
repoStats.splice(targetIndex, 1)
|
|
||||||
}
|
|
||||||
_saveAllRepoStats(repoStats)
|
|
||||||
resolve(true)
|
|
||||||
} catch (err) {
|
|
||||||
reject(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRepos () {
|
|
||||||
let repoStats
|
|
||||||
try {
|
|
||||||
repoStats = JSON.parse(localStorage.getItem('repoStats'))
|
|
||||||
if (repoStats == null) repoStats = []
|
|
||||||
} catch (err) {
|
|
||||||
repoStats = []
|
|
||||||
}
|
|
||||||
return repoStats
|
|
||||||
.map((repoStat) => {
|
|
||||||
let repoJSON, notes
|
|
||||||
try {
|
|
||||||
repoJSON = CSON.readFileSync(path.resolve(repoStat.path, 'boostrepo.json'))
|
|
||||||
let notePaths = fs.readdirSync(path.resolve(repoStat.path, 'data'))
|
|
||||||
notes = notePaths.map((notePath) => CSON.readFileSync(notePath))
|
|
||||||
} catch (err) {
|
|
||||||
return Object.assign({}, repoStat, {
|
|
||||||
status: 'ERROR',
|
|
||||||
error: err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return Object.assign({}, repoJSON, repoStat, {
|
|
||||||
status: 'IDLE',
|
|
||||||
notes
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getAllRepoStats,
|
|
||||||
addRepo,
|
|
||||||
removeRepo,
|
|
||||||
getRepos
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './Repository.styl'
|
import styles from './RepositorySection.styl'
|
||||||
import actions from 'browser/main/actions'
|
import Repository from 'browser/lib/Repository'
|
||||||
import RepositoryManager from 'browser/lib/RepositoryManager'
|
|
||||||
|
|
||||||
class Repository extends React.Component {
|
class RepositorySection extends React.Component {
|
||||||
handleUnlinkButtonClick (e) {
|
handleUnlinkButtonClick (e) {
|
||||||
let { dispatch, repository } = this.props
|
let { dispatch, repository } = this.props
|
||||||
|
|
||||||
RepositoryManager.removeRepo(repository)
|
Repository.find(repository.key)
|
||||||
|
.then((repositoryInstance) => {
|
||||||
|
return repositoryInstance.unmount()
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(actions.removeRepo(repository))
|
dispatch({
|
||||||
|
type: 'REMOVE_REPOSITORY',
|
||||||
|
key: repository.key
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +45,7 @@ class Repository extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='Repository'
|
className='RepositorySection'
|
||||||
styleName='root'
|
styleName='root'
|
||||||
>
|
>
|
||||||
<div styleName='header'>
|
<div styleName='header'>
|
||||||
@@ -76,7 +81,7 @@ class Repository extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Repository.propTypes = {
|
RepositorySection.propTypes = {
|
||||||
repository: PropTypes.shape({
|
repository: PropTypes.shape({
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
folders: PropTypes.arrayOf(PropTypes.shape({
|
folders: PropTypes.arrayOf(PropTypes.shape({
|
||||||
@@ -86,4 +91,4 @@ Repository.propTypes = {
|
|||||||
dispatch: PropTypes.func
|
dispatch: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CSSModules(Repository, styles)
|
export default CSSModules(RepositorySection, styles)
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './SideNav.styl'
|
import styles from './SideNav.styl'
|
||||||
import actions from 'browser/main/actions'
|
|
||||||
import { openModal, isModalOpen } from 'browser/lib/modal'
|
import { openModal, isModalOpen } from 'browser/lib/modal'
|
||||||
import Preferences from '../../modal/Preferences'
|
import Preferences from '../../modal/Preferences'
|
||||||
import CreateNewFolder from '../../modal/CreateNewFolder'
|
import CreateNewFolder from '../../modal/CreateNewFolder'
|
||||||
import Repository from './Repository'
|
import RepositorySection from './RepositorySection'
|
||||||
import NewRepositoryModal from '../../modal/NewRepositoryModal'
|
import NewRepositoryModal from '../../modal/NewRepositoryModal'
|
||||||
|
|
||||||
const ipc = require('electron').ipcRenderer
|
const ipc = require('electron').ipcRenderer
|
||||||
@@ -39,13 +38,11 @@ class SideNav extends React.Component {
|
|||||||
handleFolderButtonClick (name) {
|
handleFolderButtonClick (name) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
let { dispatch } = this.props
|
let { dispatch } = this.props
|
||||||
dispatch(actions.switchFolder(name))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleAllFoldersButtonClick (e) {
|
handleAllFoldersButtonClick (e) {
|
||||||
let { dispatch } = this.props
|
let { dispatch } = this.props
|
||||||
dispatch(actions.setSearchFilter(''))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewRepositoryButtonClick (e) {
|
handleNewRepositoryButtonClick (e) {
|
||||||
@@ -55,8 +52,8 @@ class SideNav extends React.Component {
|
|||||||
render () {
|
render () {
|
||||||
let { repositories, dispatch } = this.props
|
let { repositories, dispatch } = this.props
|
||||||
let repositorieElements = repositories.map((repo) => {
|
let repositorieElements = repositories.map((repo) => {
|
||||||
return <Repository
|
return <RepositorySection
|
||||||
key={repo.name}
|
key={repo.key}
|
||||||
repository={repo}
|
repository={repo}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
/>
|
/>
|
||||||
@@ -108,17 +105,6 @@ class SideNav extends React.Component {
|
|||||||
|
|
||||||
SideNav.propTypes = {
|
SideNav.propTypes = {
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
status: PropTypes.shape({
|
|
||||||
folderId: PropTypes.number
|
|
||||||
}),
|
|
||||||
user: PropTypes.object,
|
|
||||||
folders: PropTypes.array,
|
|
||||||
allArticles: PropTypes.array,
|
|
||||||
articles: PropTypes.array,
|
|
||||||
modified: PropTypes.array,
|
|
||||||
activeArticle: PropTypes.shape({
|
|
||||||
key: PropTypes.string
|
|
||||||
}),
|
|
||||||
repositories: PropTypes.array
|
repositories: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
import React, { PropTypes} from 'react'
|
import React, { PropTypes} from 'react'
|
||||||
import { connect } from 'react-redux'
|
import { connect } from 'react-redux'
|
||||||
import ReactDOM from 'react-dom'
|
import ReactDOM from 'react-dom'
|
||||||
import { toggleTutorial } from '../actions'
|
|
||||||
import SideNav from './SideNav'
|
import SideNav from './SideNav'
|
||||||
import ArticleTopBar from './ArticleTopBar'
|
import ArticleTopBar from './ArticleTopBar'
|
||||||
import ArticleList from './ArticleList'
|
import ArticleList from './ArticleList'
|
||||||
import ArticleDetail from './ArticleDetail'
|
import ArticleDetail from './ArticleDetail'
|
||||||
import _ from 'lodash'
|
|
||||||
import { isModalOpen, closeModal } from 'browser/lib/modal'
|
import { isModalOpen, closeModal } from 'browser/lib/modal'
|
||||||
|
import Repository from 'browser/lib/Repository'
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const remote = electron.remote
|
const remote = electron.remote
|
||||||
|
|
||||||
const TEXT_FILTER = 'TEXT_FILTER'
|
|
||||||
const FOLDER_FILTER = 'FOLDER_FILTER'
|
|
||||||
const FOLDER_EXACT_FILTER = 'FOLDER_EXACT_FILTER'
|
|
||||||
const TAG_FILTER = 'TAG_FILTER'
|
|
||||||
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
class HomePage extends React.Component {
|
class HomePage extends React.Component {
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
// React自体のKey入力はfocusされていないElementからは動かないため、
|
let { dispatch } = this.props
|
||||||
// `window`に直接かける
|
|
||||||
this.keyHandler = e => this.handleKeyDown(e)
|
// Bind directly to window
|
||||||
window.addEventListener('keydown', this.keyHandler)
|
// this.keyHandler = (e) => this.handleKeyDown(e)
|
||||||
|
// window.addEventListener('keydown', this.keyHandler)
|
||||||
|
|
||||||
|
// Reload all data
|
||||||
|
Repository.loadAll()
|
||||||
|
.then((allData) => {
|
||||||
|
dispatch({type: 'INIT_ALL', data: allData})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@@ -32,216 +33,72 @@ class HomePage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDown (e) {
|
handleKeyDown (e) {
|
||||||
if (isModalOpen()) {
|
// if (isModalOpen()) {
|
||||||
if (e.keyCode === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
|
// if (e.keyCode === 13 && (OSX ? e.metaKey : e.ctrlKey)) {
|
||||||
remote.getCurrentWebContents().send('modal-confirm')
|
// remote.getCurrentWebContents().send('modal-confirm')
|
||||||
}
|
// }
|
||||||
if (e.keyCode === 27) closeModal()
|
// if (e.keyCode === 27) closeModal()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
let { status, dispatch } = this.props
|
// let { dispatch } = this.props
|
||||||
let { top, list } = this.refs
|
// let { top, list } = this.refs
|
||||||
let listElement = ReactDOM.findDOMNode(list)
|
// let listElement = ReactDOM.findDOMNode(list)
|
||||||
|
|
||||||
if (status.isTutorialOpen) {
|
// if (status.isTutorialOpen) {
|
||||||
dispatch(toggleTutorial())
|
// // dispatch(toggleTutorial())
|
||||||
e.preventDefault()
|
// e.preventDefault()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (e.keyCode === 13 && top.isInputFocused()) {
|
// if (e.keyCode === 13 && top.isInputFocused()) {
|
||||||
listElement.focus()
|
// listElement.focus()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
if (e.keyCode === 27 && top.isInputFocused()) {
|
// if (e.keyCode === 27 && top.isInputFocused()) {
|
||||||
if (status.search.length > 0) top.escape()
|
// // if (status.search.length > 0) top.escape()
|
||||||
else listElement.focus()
|
// // else listElement.focus()
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Search inputがfocusされていたら大体のキー入力は無視される。
|
// // Search inputがfocusされていたら大体のキー入力は無視される。
|
||||||
if (e.keyCode === 27) {
|
// if (e.keyCode === 27) {
|
||||||
if (document.activeElement !== listElement) {
|
// if (document.activeElement !== listElement) {
|
||||||
listElement.focus()
|
// listElement.focus()
|
||||||
} else {
|
// } else {
|
||||||
top.focusInput()
|
// top.focusInput()
|
||||||
}
|
// }
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
|
|
||||||
let { repositories } = this.props
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='HomePage'>
|
<div className='HomePage'>
|
||||||
<SideNav
|
<SideNav
|
||||||
ref='nav'
|
ref='nav'
|
||||||
dispatch={dispatch}
|
{...this.props}
|
||||||
repositories={repositories}
|
|
||||||
status={status}
|
|
||||||
user={user}
|
|
||||||
folders={folders}
|
|
||||||
allArticles={allArticles}
|
|
||||||
articles={articles}
|
|
||||||
modified={modified}
|
|
||||||
activeArticle={activeArticle}
|
|
||||||
/>
|
/>
|
||||||
<ArticleTopBar
|
<ArticleTopBar
|
||||||
ref='top'
|
ref='top'
|
||||||
dispatch={dispatch}
|
{...this.props}
|
||||||
status={status}
|
|
||||||
folders={folders}
|
|
||||||
/>
|
/>
|
||||||
<ArticleList
|
<ArticleList
|
||||||
ref='list'
|
ref='list'
|
||||||
dispatch={dispatch}
|
{...this.props}
|
||||||
folders={folders}
|
|
||||||
articles={articles}
|
|
||||||
modified={modified}
|
|
||||||
activeArticle={activeArticle}
|
|
||||||
/>
|
/>
|
||||||
<ArticleDetail
|
<ArticleDetail
|
||||||
ref='detail'
|
ref='detail'
|
||||||
dispatch={dispatch}
|
{...this.props}
|
||||||
status={status}
|
|
||||||
tags={tags}
|
|
||||||
user={user}
|
|
||||||
folders={folders}
|
|
||||||
modified={modified}
|
|
||||||
activeArticle={activeArticle}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore invalid key
|
|
||||||
function ignoreInvalidKey (key) {
|
|
||||||
return key.length > 0 && !key.match(/^\/\/$/) && !key.match(/^\/$/) && !key.match(/^#$/) && !key.match(/^--/)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build filter object by key
|
|
||||||
function buildFilter (key) {
|
|
||||||
if (key.match(/^\/\/.+/)) {
|
|
||||||
return {type: FOLDER_EXACT_FILTER, value: key.match(/^\/\/(.+)$/)[1]}
|
|
||||||
}
|
|
||||||
if (key.match(/^\/.+/)) {
|
|
||||||
return {type: FOLDER_FILTER, value: key.match(/^\/(.+)$/)[1]}
|
|
||||||
}
|
|
||||||
if (key.match(/^#(.+)/)) {
|
|
||||||
return {type: TAG_FILTER, value: key.match(/^#(.+)$/)[1]}
|
|
||||||
}
|
|
||||||
return {type: TEXT_FILTER, value: key}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isContaining (target, needle) {
|
|
||||||
return target.match(new RegExp(_.escapeRegExp(needle), 'i'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function startsWith (target, needle) {
|
|
||||||
return target.match(new RegExp('^' + _.escapeRegExp(needle), 'i'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function remap (state) {
|
|
||||||
let { user, folders, status } = state
|
|
||||||
let _articles = state.articles
|
|
||||||
|
|
||||||
let articles = _articles != null ? _articles.data : []
|
|
||||||
let modified = _articles != null ? _articles.modified : []
|
|
||||||
|
|
||||||
articles.sort((a, b) => {
|
|
||||||
let match = new Date(b.updatedAt) - new Date(a.updatedAt)
|
|
||||||
if (match === 0) match = b.title.localeCompare(a.title)
|
|
||||||
if (match === 0) match = b.key.localeCompare(a.key)
|
|
||||||
return match
|
|
||||||
})
|
|
||||||
let allArticles = articles.slice()
|
|
||||||
|
|
||||||
let tags = _.uniq(allArticles.reduce((sum, article) => {
|
|
||||||
if (!_.isArray(article.tags)) return sum
|
|
||||||
return sum.concat(article.tags)
|
|
||||||
}, []))
|
|
||||||
|
|
||||||
if (status.search.split(' ').some(key => key === '--unsaved')) articles = articles.filter(article => _.findWhere(modified, {key: article.key}))
|
|
||||||
// Filter articles
|
|
||||||
let filters = status.search.split(' ')
|
|
||||||
.map(key => key.trim())
|
|
||||||
.filter(ignoreInvalidKey)
|
|
||||||
.map(buildFilter)
|
|
||||||
|
|
||||||
let folderExactFilters = filters.filter(filter => filter.type === FOLDER_EXACT_FILTER)
|
|
||||||
let folderFilters = filters.filter(filter => filter.type === FOLDER_FILTER)
|
|
||||||
let textFilters = filters.filter(filter => filter.type === TEXT_FILTER)
|
|
||||||
let tagFilters = filters.filter(filter => filter.type === TAG_FILTER)
|
|
||||||
|
|
||||||
let targetFolders
|
|
||||||
if (folders != null) {
|
|
||||||
let exactTargetFolders = folders.filter(folder => {
|
|
||||||
return _.find(folderExactFilters, filter => filter.value.toLowerCase() === folder.name.toLowerCase())
|
|
||||||
})
|
|
||||||
let fuzzyTargetFolders = folders.filter(folder => {
|
|
||||||
return _.find(folderFilters, filter => startsWith(folder.name.replace(/_/g, ''), filter.value.replace(/_/g, '')))
|
|
||||||
})
|
|
||||||
targetFolders = status.targetFolders = exactTargetFolders.concat(fuzzyTargetFolders)
|
|
||||||
|
|
||||||
if (targetFolders.length > 0) {
|
|
||||||
articles = articles.filter(article => {
|
|
||||||
return _.findWhere(targetFolders, {key: article.FolderKey})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (textFilters.length > 0) {
|
|
||||||
articles = textFilters.reduce((articles, textFilter) => {
|
|
||||||
return articles.filter(article => {
|
|
||||||
return isContaining(article.title, textFilter.value) || isContaining(article.content, textFilter.value)
|
|
||||||
})
|
|
||||||
}, articles)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tagFilters.length > 0) {
|
|
||||||
articles = tagFilters.reduce((articles, tagFilter) => {
|
|
||||||
return articles.filter(article => {
|
|
||||||
return _.find(article.tags, tag => isContaining(tag, tagFilter.value))
|
|
||||||
})
|
|
||||||
}, articles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab active article
|
|
||||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
|
||||||
if (activeArticle == null) activeArticle = articles[0]
|
|
||||||
|
|
||||||
let { repositories } = state
|
|
||||||
|
|
||||||
return {
|
|
||||||
user,
|
|
||||||
folders,
|
|
||||||
status,
|
|
||||||
articles,
|
|
||||||
allArticles,
|
|
||||||
modified,
|
|
||||||
activeArticle,
|
|
||||||
tags,
|
|
||||||
repositories
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HomePage.propTypes = {
|
HomePage.propTypes = {
|
||||||
status: PropTypes.shape(),
|
|
||||||
user: PropTypes.shape({
|
|
||||||
name: PropTypes.string
|
|
||||||
}),
|
|
||||||
articles: PropTypes.array,
|
|
||||||
allArticles: PropTypes.array,
|
|
||||||
modified: PropTypes.array,
|
|
||||||
activeArticle: PropTypes.shape(),
|
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
folders: PropTypes.array,
|
|
||||||
tags: PropTypes.array,
|
|
||||||
repositories: PropTypes.array
|
repositories: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(remap)(HomePage)
|
export default connect((x) => x)(HomePage)
|
||||||
|
|||||||
@@ -1,200 +0,0 @@
|
|||||||
// Action types
|
|
||||||
export const USER_UPDATE = 'USER_UPDATE'
|
|
||||||
|
|
||||||
export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
|
|
||||||
export const ARTICLE_DESTROY = 'ARTICLE_DESTROY'
|
|
||||||
export const ARTICLE_SAVE = 'ARTICLE_SAVE'
|
|
||||||
export const ARTICLE_SAVE_ALL = 'ARTICLE_SAVE_ALL'
|
|
||||||
export const ARTICLE_CACHE = 'ARTICLE_CACHE'
|
|
||||||
export const ARTICLE_UNCACHE = 'ARTICLE_UNCACHE'
|
|
||||||
export const ARTICLE_UNCACHE_ALL = 'ARTICLE_UNCACHE_ALL'
|
|
||||||
|
|
||||||
export const FOLDER_CREATE = 'FOLDER_CREATE'
|
|
||||||
export const FOLDER_UPDATE = 'FOLDER_UPDATE'
|
|
||||||
export const FOLDER_DESTROY = 'FOLDER_DESTROY'
|
|
||||||
export const FOLDER_REPLACE = 'FOLDER_REPLACE'
|
|
||||||
|
|
||||||
export const SWITCH_FOLDER = 'SWITCH_FOLDER'
|
|
||||||
export const SWITCH_ARTICLE = 'SWITCH_ARTICLE'
|
|
||||||
export const SET_SEARCH_FILTER = 'SET_SEARCH_FILTER'
|
|
||||||
export const SET_TAG_FILTER = 'SET_TAG_FILTER'
|
|
||||||
export const CLEAR_SEARCH = 'CLEAR_SEARCH'
|
|
||||||
|
|
||||||
export const TOGGLE_TUTORIAL = 'TOGGLE_TUTORIAL'
|
|
||||||
|
|
||||||
// Article status
|
|
||||||
export const NEW = 'NEW'
|
|
||||||
|
|
||||||
export function updateUser (input) {
|
|
||||||
return {
|
|
||||||
type: USER_UPDATE,
|
|
||||||
data: input
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB
|
|
||||||
export function cacheArticle (key, article) {
|
|
||||||
return {
|
|
||||||
type: ARTICLE_CACHE,
|
|
||||||
data: { key, article }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uncacheArticle (key) {
|
|
||||||
return {
|
|
||||||
type: ARTICLE_UNCACHE,
|
|
||||||
data: { key }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function uncacheAllArticles () {
|
|
||||||
return {
|
|
||||||
type: ARTICLE_UNCACHE_ALL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveArticle (key, article, forceSwitch) {
|
|
||||||
return {
|
|
||||||
type: ARTICLE_SAVE,
|
|
||||||
data: { key, article, forceSwitch }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveAllArticles () {
|
|
||||||
return {
|
|
||||||
type: ARTICLE_SAVE_ALL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateArticle (article) {
|
|
||||||
return {
|
|
||||||
type: ARTICLE_UPDATE,
|
|
||||||
data: { article }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function destroyArticle (key) {
|
|
||||||
return {
|
|
||||||
type: ARTICLE_DESTROY,
|
|
||||||
data: { key }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createFolder (folder) {
|
|
||||||
return {
|
|
||||||
type: FOLDER_CREATE,
|
|
||||||
data: { folder }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateFolder (folder) {
|
|
||||||
return {
|
|
||||||
type: FOLDER_UPDATE,
|
|
||||||
data: { folder }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function destroyFolder (key) {
|
|
||||||
return {
|
|
||||||
type: FOLDER_DESTROY,
|
|
||||||
data: { key }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceFolder (a, b) {
|
|
||||||
return {
|
|
||||||
type: FOLDER_REPLACE,
|
|
||||||
data: {
|
|
||||||
a,
|
|
||||||
b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function switchFolder (folderName) {
|
|
||||||
return {
|
|
||||||
type: SWITCH_FOLDER,
|
|
||||||
data: folderName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function switchArticle (articleKey) {
|
|
||||||
return {
|
|
||||||
type: SWITCH_ARTICLE,
|
|
||||||
data: {
|
|
||||||
key: articleKey
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setSearchFilter (search) {
|
|
||||||
return {
|
|
||||||
type: SET_SEARCH_FILTER,
|
|
||||||
data: search
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setTagFilter (tag) {
|
|
||||||
return {
|
|
||||||
type: SET_TAG_FILTER,
|
|
||||||
data: tag
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSearch () {
|
|
||||||
return {
|
|
||||||
type: CLEAR_SEARCH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleTutorial () {
|
|
||||||
return {
|
|
||||||
type: TOGGLE_TUTORIAL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* v0.6.* Actions
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function addRepo (data) {
|
|
||||||
return {
|
|
||||||
type: 'ADD_REPOSITORY',
|
|
||||||
data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeRepo (data) {
|
|
||||||
return {
|
|
||||||
type: 'REMOVE_REPOSITORY',
|
|
||||||
data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
updateUser,
|
|
||||||
|
|
||||||
updateArticle,
|
|
||||||
destroyArticle,
|
|
||||||
cacheArticle,
|
|
||||||
uncacheArticle,
|
|
||||||
uncacheAllArticles,
|
|
||||||
saveArticle,
|
|
||||||
saveAllArticles,
|
|
||||||
|
|
||||||
createFolder,
|
|
||||||
updateFolder,
|
|
||||||
destroyFolder,
|
|
||||||
replaceFolder,
|
|
||||||
|
|
||||||
switchFolder,
|
|
||||||
switchArticle,
|
|
||||||
setSearchFilter,
|
|
||||||
setTagFilter,
|
|
||||||
clearSearch,
|
|
||||||
toggleTutorial,
|
|
||||||
|
|
||||||
// v0.6.*
|
|
||||||
addRepo,
|
|
||||||
removeRepo
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import React, { PropTypes } from 'react'
|
import React, { PropTypes } from 'react'
|
||||||
import CSSModules from 'browser/lib/CSSModules'
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
import styles from './NewRepositoryModal.styl'
|
import styles from './NewRepositoryModal.styl'
|
||||||
import linkState from 'browser/lib/linkState'
|
import Repository from 'browser/lib/Repository'
|
||||||
import RepositoryManager from 'browser/lib/RepositoryManager'
|
|
||||||
import store from 'browser/main/store'
|
import store from 'browser/main/store'
|
||||||
import actions from 'browser/main/actions'
|
|
||||||
|
|
||||||
const electron = require('electron')
|
const electron = require('electron')
|
||||||
const remote = electron.remote
|
const remote = electron.remote
|
||||||
@@ -81,13 +79,19 @@ class NewRepositoryModal extends React.Component {
|
|||||||
let targetPath = this.state.path
|
let targetPath = this.state.path
|
||||||
let name = this.state.name
|
let name = this.state.name
|
||||||
|
|
||||||
RepositoryManager
|
let repository = new Repository({
|
||||||
.addRepo({
|
name: name,
|
||||||
targetPath,
|
path: targetPath
|
||||||
name
|
})
|
||||||
})
|
|
||||||
.then((newRepo) => {
|
repository
|
||||||
store.dispatch(actions.addRepo(newRepo))
|
.mount()
|
||||||
|
.then(() => repository.load())
|
||||||
|
.then((data) => {
|
||||||
|
store.dispatch({
|
||||||
|
type: 'ADD_REPOSITORY',
|
||||||
|
repository: data
|
||||||
|
})
|
||||||
this.props.close()
|
this.props.close()
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -98,6 +102,15 @@ class NewRepositoryModal extends React.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChange (e) {
|
||||||
|
let name = this.refs.nameInput.value
|
||||||
|
let path = this.refs.pathInput.value
|
||||||
|
this.setState({
|
||||||
|
name,
|
||||||
|
path
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<div className='NewRepositoryModal'
|
<div className='NewRepositoryModal'
|
||||||
@@ -116,7 +129,8 @@ class NewRepositoryModal extends React.Component {
|
|||||||
<div styleName='body-section-label'>Repository Name</div>
|
<div styleName='body-section-label'>Repository Name</div>
|
||||||
<input styleName='body-section-input'
|
<input styleName='body-section-input'
|
||||||
ref='nameInput'
|
ref='nameInput'
|
||||||
valueLink={this.linkState('name')}
|
value={this.state.name}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -124,11 +138,13 @@ class NewRepositoryModal extends React.Component {
|
|||||||
<div styleName='body-section-label'>Repository Path</div>
|
<div styleName='body-section-label'>Repository Path</div>
|
||||||
<div styleName={!this.state.isPathSectionFocused ? 'body-section-path' : 'body-section-path--focus'}>
|
<div styleName={!this.state.isPathSectionFocused ? 'body-section-path' : 'body-section-path--focus'}>
|
||||||
<input styleName='body-section-path-input'
|
<input styleName='body-section-path-input'
|
||||||
valueLink={this.linkState('path')}
|
ref='pathInput'
|
||||||
|
value={this.state.path}
|
||||||
style={styles.body_section_path_input}
|
style={styles.body_section_path_input}
|
||||||
onFocus={(e) => this.handlePathFocus(e)}
|
onFocus={(e) => this.handlePathFocus(e)}
|
||||||
onBlur={(e) => this.handlePathBlur(e)}
|
onBlur={(e) => this.handlePathBlur(e)}
|
||||||
disabled={this.state.isBrowsingPath}
|
disabled={this.state.isBrowsingPath}
|
||||||
|
onChange={(e) => this.handleChange(e)}
|
||||||
/>
|
/>
|
||||||
<button styleName='body-section-path-button'
|
<button styleName='body-section-path-button'
|
||||||
onClick={(e) => this.handleBrowseButtonClick(e)}
|
onClick={(e) => this.handleBrowseButtonClick(e)}
|
||||||
@@ -170,6 +186,4 @@ NewRepositoryModal.propTypes = {
|
|||||||
close: PropTypes.func
|
close: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
NewRepositoryModal.prototype.linkState = linkState
|
|
||||||
|
|
||||||
export default CSSModules(NewRepositoryModal, styles)
|
export default CSSModules(NewRepositoryModal, styles)
|
||||||
|
|||||||
@@ -1,5 +1,59 @@
|
|||||||
import reducer from './reducer'
|
import { combineReducers, createStore } from 'redux'
|
||||||
import { createStore } from 'redux'
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repositories
|
||||||
|
* ```
|
||||||
|
* repositories = [{
|
||||||
|
* key: String,
|
||||||
|
* name: String,
|
||||||
|
* path: String, // path of repository
|
||||||
|
* status: String, // status of repository [IDLE, LOADING, READY, ERROR]
|
||||||
|
* folders: {
|
||||||
|
* name: String,
|
||||||
|
* color: String
|
||||||
|
* },
|
||||||
|
* notes: [{
|
||||||
|
* key: String,
|
||||||
|
* title: String,
|
||||||
|
* content: String,
|
||||||
|
* folder: String,
|
||||||
|
* tags: [String],
|
||||||
|
* createdAt: Date,
|
||||||
|
* updatedAt: Date
|
||||||
|
* }]
|
||||||
|
* }]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
const initialRepositories = []
|
||||||
|
|
||||||
|
function repositories (state = initialRepositories, action) {
|
||||||
|
console.log(action)
|
||||||
|
switch (action.type) {
|
||||||
|
case 'INIT_ALL':
|
||||||
|
return action.data.slice()
|
||||||
|
case 'ADD_REPOSITORY':
|
||||||
|
{
|
||||||
|
let repos = state.slice()
|
||||||
|
repos.push(action.repository)
|
||||||
|
return repos
|
||||||
|
}
|
||||||
|
case 'REMOVE_REPOSITORY':
|
||||||
|
{
|
||||||
|
let repos = state.slice()
|
||||||
|
let targetIndex = _.findIndex(repos, {key: action.key})
|
||||||
|
if (targetIndex > -1) {
|
||||||
|
repos.splice(targetIndex, 1)
|
||||||
|
}
|
||||||
|
return repos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
let reducer = combineReducers({
|
||||||
|
repositories
|
||||||
|
})
|
||||||
|
|
||||||
let store = createStore(reducer)
|
let store = createStore(reducer)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user