mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 10:16:26 +00:00
Redesign SideNav & implement add repo func
This commit is contained in:
295
browser/lib/RepositoryManager.js
Normal file
295
browser/lib/RepositoryManager.js
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
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('repositories'))
|
||||||
|
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 setLoalStorage (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 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,
|
||||||
|
getRepos
|
||||||
|
}
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import React, { PropTypes } from 'react'
|
|
||||||
import { findWhere } from 'lodash'
|
|
||||||
import { setSearchFilter, switchFolder, uncacheArticle, saveAllArticles, switchArticle, clearSearch } from '../actions'
|
|
||||||
import { openModal, isModalOpen } from 'browser/lib/modal'
|
|
||||||
import FolderMark from 'browser/components/FolderMark'
|
|
||||||
import Preferences from '../modal/Preferences'
|
|
||||||
import CreateNewFolder from '../modal/CreateNewFolder'
|
|
||||||
import _ from 'lodash'
|
|
||||||
import ModeIcon from 'browser/components/ModeIcon'
|
|
||||||
|
|
||||||
const ipc = require('electron').ipcRenderer
|
|
||||||
|
|
||||||
const BRAND_COLOR = '#18AF90'
|
|
||||||
const OSX = global.process.platform === 'darwin'
|
|
||||||
|
|
||||||
const preferenceTutorialElement = (
|
|
||||||
<svg width='300' height='300' className='tutorial'>
|
|
||||||
<text x='15' y='30' fill={BRAND_COLOR} fontSize='24'>Preference</text>
|
|
||||||
<svg x='-30' y='-270' width='400' height='400'>
|
|
||||||
<path fill='white' d='M165.9,297c5.3,0,10.6,0.1,15.8,0.1c3.3,0,7.7,0.8,10.7-1c2.3-1.4,3.1-4,4.5-6.2c3.5-5.5,9.6-5.2,14.6-1.9
|
|
||||||
c4.6,3.1,8.7,8,8.4,13.8c-0.3,5.2-3.3,10.1-6.1,14.3c-3.1,4.7-6.6,7-12.2,7.9c-5.2,0.8-11.7,1.6-15.4-3
|
|
||||||
c-6.6-8.2,2.1-20.5,7.4-27.1c6.5-8.1,20.1-14,26.4-2.1c5.4,10.3-3.1,21.7-13,24.8c-5.7,1.8-11,0.9-16.2-1.9c-2-1.1-5-2.6-6.6-4.4
|
|
||||||
c-3.9-4.3-0.3-8.2,2.5-11.2c1.3-1.4-0.8-3.6-2.1-2.1c-2.7,2.9-5.8,6.6-5.1,10.9c0.7,4.4,5.6,6.9,9,8.9c8.6,5.1,18.7,4.8,26.8-1.2
|
|
||||||
c7.3-5.4,11.6-15,8-23.7c-3.3-8.1-11.7-11.8-20-9c-12.5,4.1-33.7,33.5-15.9,43.1c6.8,3.7,19.8,1.8,25.3-3.6
|
|
||||||
c6.1-5.8,12.1-17.2,9.5-25.7c-2.6-8.4-13.7-17-22.6-13.3c-1.6,0.7-3,1.7-4.1,3c-1.6,1.9-2.2,5.1-4.1,6.6c-3.1,2.4-10.1,1-13.7,1
|
|
||||||
c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
|
|
||||||
</svg>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const newFolderTutorialElement = (
|
|
||||||
<svg width='800' height='500' className='tutorial'>
|
|
||||||
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
|
|
||||||
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
|
|
||||||
<svg x='50' y='10' width='300' height='400'>
|
|
||||||
<path fill='white' d='M94.1,10.9C77.7,15.6,62,22.7,47.8,32.1c-13.6,9-27.7,20.4-37.1,33.9c-1.1,1.6,1.5,3.1,2.6,1.5
|
|
||||||
C22.6,54.1,37,42.7,50.6,33.8c13.7-8.8,28.6-15.5,44.2-20C96.7,13.3,95.9,10.4,94.1,10.9L94.1,10.9z'/>
|
|
||||||
<path fill='white' d='M71.1,8.6c7.9,1.6,15.8,3.2,23.6,4.7c-0.1-0.9-0.2-1.8-0.4-2.7c-4.6,3.4-5.4,7.7-4.4,13.2
|
|
||||||
c0.8,4.4,0.8,10.9,5.6,12.8c1.8,0.7,2.6-2.2,0.8-2.9c-2.3-1-2.6-6.2-3-8.3c-0.9-4.5-1.7-9,2.5-12.1c0.9-0.7,1-2.5-0.4-2.7
|
|
||||||
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
|
|
||||||
</svg>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
export default class ArticleNavigator extends React.Component {
|
|
||||||
constructor (props) {
|
|
||||||
super(props)
|
|
||||||
this.newFolderHandler = e => {
|
|
||||||
if (isModalOpen()) return true
|
|
||||||
this.handleNewFolderButton(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
ipc.on('nav-new-folder', this.newFolderHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
ipc.removeListener('nav-new-folder', this.newFolderHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePreferencesButtonClick (e) {
|
|
||||||
openModal(Preferences)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNewFolderButton (e) {
|
|
||||||
let { user } = this.props
|
|
||||||
openModal(CreateNewFolder, {user: user})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFolderButtonClick (name) {
|
|
||||||
return e => {
|
|
||||||
let { dispatch } = this.props
|
|
||||||
dispatch(switchFolder(name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAllFoldersButtonClick (e) {
|
|
||||||
let { dispatch } = this.props
|
|
||||||
dispatch(setSearchFilter(''))
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUnsavedItemClick (article) {
|
|
||||||
let { dispatch } = this.props
|
|
||||||
return e => {
|
|
||||||
let { articles } = this.props
|
|
||||||
let isInArticleList = articles.some(_article => _article.key === article.key)
|
|
||||||
if (!isInArticleList) dispatch(clearSearch())
|
|
||||||
dispatch(switchArticle(article.key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleUncacheButtonClick (article) {
|
|
||||||
let { dispatch } = this.props
|
|
||||||
return e => {
|
|
||||||
dispatch(uncacheArticle(article.key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSaveAllClick (e) {
|
|
||||||
let { dispatch } = this.props
|
|
||||||
dispatch(saveAllArticles())
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
let { status, user, folders, allArticles, modified, activeArticle } = this.props
|
|
||||||
let { targetFolders } = status
|
|
||||||
if (targetFolders == null) targetFolders = []
|
|
||||||
|
|
||||||
let modifiedElements = modified.map(modifiedArticle => {
|
|
||||||
let originalArticle = _.findWhere(allArticles, {key: modifiedArticle.key})
|
|
||||||
if (originalArticle == null) return false
|
|
||||||
let combinedArticle = Object.assign({}, originalArticle, modifiedArticle)
|
|
||||||
|
|
||||||
let className = 'ArticleNavigator-unsaved-list-item'
|
|
||||||
if (activeArticle && activeArticle.key === combinedArticle.key) className += ' active'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={modifiedArticle.key} onClick={e => this.handleUnsavedItemClick(combinedArticle)(e)} className={className}>
|
|
||||||
<div className='ArticleNavigator-unsaved-list-item-label'>
|
|
||||||
<ModeIcon mode={combinedArticle.mode}/>
|
|
||||||
{combinedArticle.title.trim().length > 0
|
|
||||||
? combinedArticle.title
|
|
||||||
: <span className='ArticleNavigator-unsaved-list-item-label-untitled'>(Untitled)</span>}
|
|
||||||
</div>
|
|
||||||
<button onClick={e => this.handleUncacheButtonClick(combinedArticle)(e)} className='ArticleNavigator-unsaved-list-item-discard-button'><i className='fa fa-times'/></button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}).filter(modifiedArticle => modifiedArticle).sort((a, b) => a.updatedAt - b.updatedAt)
|
|
||||||
let hasModified = modifiedElements.length > 0
|
|
||||||
|
|
||||||
let folderElememts = folders.map((folder, index) => {
|
|
||||||
let isActive = findWhere(targetFolders, {key: folder.key})
|
|
||||||
let articleCount = allArticles.filter(article => article.FolderKey === folder.key && article.status !== 'NEW').length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button onClick={e => this.handleFolderButtonClick(folder.name)(e)} key={'folder-' + folder.key} className={isActive ? 'active' : ''}>
|
|
||||||
<FolderMark color={folder.color}/> {folder.name} <span className='articleCount'>{articleCount}</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div tabIndex='1' className='ArticleNavigator'>
|
|
||||||
<div className='userInfo'>
|
|
||||||
<div className='userProfileName'>{user.name}</div>
|
|
||||||
<div className='userName'>localStorage</div>
|
|
||||||
<button onClick={e => this.handlePreferencesButtonClick(e)} className='settingBtn'>
|
|
||||||
<i className='fa fa-fw fa-chevron-down'/>
|
|
||||||
<span className='tooltip'>Preferences</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{status.isTutorialOpen ? preferenceTutorialElement : null}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*<div className={'ArticleNavigator-unsaved' + (hasModified ? '' : ' hide')}>
|
|
||||||
<div className='ArticleNavigator-unsaved-header'>Work in progress</div>
|
|
||||||
<div className='ArticleNavigator-unsaved-list'>
|
|
||||||
{modifiedElements}
|
|
||||||
</div>
|
|
||||||
<div className='ArticleNavigator-unsaved-control'>
|
|
||||||
<button onClick={e => this.handleSaveAllClick()} className='ArticleNavigator-unsaved-control-save-all-button' disabled={modifiedElements.length === 0}>Save all</button>
|
|
||||||
</div>
|
|
||||||
</div>*/}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className={'ArticleNavigator-folders expand'}>
|
|
||||||
{status.isTutorialOpen ? newFolderTutorialElement : null}
|
|
||||||
<div className='ArticleNavigator-folders-header'>
|
|
||||||
<div className='title'>Folders</div>
|
|
||||||
<button onClick={e => this.handleNewFolderButton(e)} className='addBtn'>
|
|
||||||
<i className='fa fa-fw fa-plus'/>
|
|
||||||
<span className='tooltip'>Create a new folder ({OSX ? '⌘' : '^'} + Shift + n)</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className='folderList'>
|
|
||||||
<button onClick={e => this.handleAllFoldersButtonClick(e)} className={targetFolders.length === 0 ? 'active' : ''}>All folders</button>
|
|
||||||
{folderElememts}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ArticleNavigator.propTypes = {
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
75
browser/main/HomePage/SideNav/Repository.js
Normal file
75
browser/main/HomePage/SideNav/Repository.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './Repository.styl'
|
||||||
|
|
||||||
|
class Repository extends React.Component {
|
||||||
|
render () {
|
||||||
|
let { repository } = this.props
|
||||||
|
let folderElements = repository.folders.map((folder) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={folder.name}
|
||||||
|
styleName='folder'
|
||||||
|
>
|
||||||
|
<div styleName='folder-label'>
|
||||||
|
<i className='fa fa-cube'/> {folder.name}
|
||||||
|
</div>
|
||||||
|
<div styleName='folder-control'>
|
||||||
|
<button styleName='folder-control-button'>
|
||||||
|
<i className='fa fa-pencil'/>
|
||||||
|
</button>
|
||||||
|
<button styleName='folder-control-button'>
|
||||||
|
<i className='fa fa-trash'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='Repository'
|
||||||
|
styleName='root'
|
||||||
|
>
|
||||||
|
<div styleName='header'>
|
||||||
|
<div styleName='header-name'>
|
||||||
|
<i className='fa fa-archive'/> {repository.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='header-control'>
|
||||||
|
<button styleName='header-control-button'
|
||||||
|
>
|
||||||
|
<i className='fa fa-unlink'/>
|
||||||
|
</button>
|
||||||
|
<button styleName='header-control-button'
|
||||||
|
>
|
||||||
|
<i className='fa fa-pencil'/>
|
||||||
|
</button>
|
||||||
|
<button styleName='header-control-button'
|
||||||
|
>
|
||||||
|
<i className='fa fa-angle-down'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{folderElements}
|
||||||
|
|
||||||
|
<button styleName='newFolderButton'
|
||||||
|
>
|
||||||
|
<i className='fa fa-plus'/> New Folder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repository.propTypes = {
|
||||||
|
repository: PropTypes.shape({
|
||||||
|
name: PropTypes.string,
|
||||||
|
folders: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
name: PropTypes.string
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(Repository, styles)
|
||||||
90
browser/main/HomePage/SideNav/Repository.styl
Normal file
90
browser/main/HomePage/SideNav/Repository.styl
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
.root
|
||||||
|
margin-top 15px
|
||||||
|
margin-bottom 15px
|
||||||
|
|
||||||
|
.header
|
||||||
|
position relative
|
||||||
|
height 33px
|
||||||
|
&:hover
|
||||||
|
background-color alpha(white, 0.1)
|
||||||
|
|
||||||
|
.header-name
|
||||||
|
position absolute
|
||||||
|
left 0
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
right 72px
|
||||||
|
padding-left 10px
|
||||||
|
line-height 33px
|
||||||
|
color white
|
||||||
|
|
||||||
|
.header-control
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
right 5px
|
||||||
|
width 72px
|
||||||
|
|
||||||
|
.header-control-button
|
||||||
|
width 24px
|
||||||
|
padding 0
|
||||||
|
margin-top 4.5px
|
||||||
|
height 24px
|
||||||
|
border none
|
||||||
|
border-radius 5px
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
&:hover
|
||||||
|
background-color alpha(white, 0.1)
|
||||||
|
&:active
|
||||||
|
background-color $brand-color
|
||||||
|
|
||||||
|
.folder
|
||||||
|
height 33px
|
||||||
|
width 100%
|
||||||
|
position relative
|
||||||
|
&:hover
|
||||||
|
background-color alpha(white 0.1)
|
||||||
|
|
||||||
|
.folder-label
|
||||||
|
position absolute
|
||||||
|
left 0
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
right 48px
|
||||||
|
padding-left 20px
|
||||||
|
line-height 33px
|
||||||
|
color white
|
||||||
|
|
||||||
|
.folder-control
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
bottom 0
|
||||||
|
right 0
|
||||||
|
width 48px
|
||||||
|
|
||||||
|
.folder-control-button
|
||||||
|
width 24px
|
||||||
|
height 24px
|
||||||
|
margin-top 4.5px
|
||||||
|
border none
|
||||||
|
border-radius 5px
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
&:hover
|
||||||
|
background-color alpha(white, 0.1)
|
||||||
|
&:active
|
||||||
|
background-color $brand-color
|
||||||
|
|
||||||
|
.newFolderButton
|
||||||
|
height 33px
|
||||||
|
width 100%
|
||||||
|
border none
|
||||||
|
padding-left 20px
|
||||||
|
text-align left
|
||||||
|
background-color transparent
|
||||||
|
color white
|
||||||
|
&:hover
|
||||||
|
background-color alpha(white, 0.1)
|
||||||
|
&:active
|
||||||
|
background-color $brand-color
|
||||||
63
browser/main/HomePage/SideNav/SideNav.styl
Normal file
63
browser/main/HomePage/SideNav/SideNav.styl
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
$nav-border-color = #838383
|
||||||
|
$nav-background-color = #353535
|
||||||
|
|
||||||
|
.root
|
||||||
|
absolute top bottom left
|
||||||
|
width 200px
|
||||||
|
border-right solid 1px $nav-border-color
|
||||||
|
background-color $nav-background-color
|
||||||
|
|
||||||
|
.menu
|
||||||
|
margin-top 30px
|
||||||
|
margin-bottom 15px
|
||||||
|
|
||||||
|
.menu-button
|
||||||
|
height 33px
|
||||||
|
padding 0 10px
|
||||||
|
font-size 14px
|
||||||
|
width 100%
|
||||||
|
text-align left
|
||||||
|
border none
|
||||||
|
color white
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
background-color alph(white, 0.1)
|
||||||
|
&:active
|
||||||
|
background-color $brand-color
|
||||||
|
|
||||||
|
.repositoryList
|
||||||
|
absolute left right
|
||||||
|
top 145px
|
||||||
|
border-top solid 1px $nav-border-color
|
||||||
|
|
||||||
|
.control
|
||||||
|
absolute bottom left right
|
||||||
|
height 33px
|
||||||
|
border-top solid 1px $nav-border-color
|
||||||
|
|
||||||
|
.control-newRepositoryButton
|
||||||
|
absolute top bottom left
|
||||||
|
width 167px
|
||||||
|
padding 0 10px
|
||||||
|
text-align left
|
||||||
|
border-width 0 1px 0 0
|
||||||
|
border-color $nav-border-color
|
||||||
|
border-style solid
|
||||||
|
color white
|
||||||
|
background-color transparent
|
||||||
|
&:hover
|
||||||
|
background-color alph(white, 0.1)
|
||||||
|
&:active
|
||||||
|
background-color $brand-color
|
||||||
|
|
||||||
|
.control-toggleButton
|
||||||
|
absolute top bottom right
|
||||||
|
width 33px
|
||||||
|
color white
|
||||||
|
background-color transparent
|
||||||
|
border none
|
||||||
|
&:hover
|
||||||
|
background-color alph(white, 0.1)
|
||||||
|
&:active
|
||||||
|
background-color $brand-color
|
||||||
|
|
||||||
156
browser/main/HomePage/SideNav/index.js
Normal file
156
browser/main/HomePage/SideNav/index.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './SideNav.styl'
|
||||||
|
import actions from 'browser/main/actions'
|
||||||
|
import { openModal, isModalOpen } from 'browser/lib/modal'
|
||||||
|
import Preferences from '../../modal/Preferences'
|
||||||
|
import CreateNewFolder from '../../modal/CreateNewFolder'
|
||||||
|
import Repository from './Repository'
|
||||||
|
import NewRepositoryModal from '../../modal/NewRepositoryModal'
|
||||||
|
|
||||||
|
const ipc = require('electron').ipcRenderer
|
||||||
|
|
||||||
|
const BRAND_COLOR = '#18AF90'
|
||||||
|
const OSX = global.process.platform === 'darwin'
|
||||||
|
|
||||||
|
const preferenceTutorialElement = (
|
||||||
|
<svg width='300' height='300' className='tutorial'>
|
||||||
|
<text x='15' y='30' fill={BRAND_COLOR} fontSize='24'>Preference</text>
|
||||||
|
<svg x='-30' y='-270' width='400' height='400'>
|
||||||
|
<path fill='white' d='M165.9,297c5.3,0,10.6,0.1,15.8,0.1c3.3,0,7.7,0.8,10.7-1c2.3-1.4,3.1-4,4.5-6.2c3.5-5.5,9.6-5.2,14.6-1.9
|
||||||
|
c4.6,3.1,8.7,8,8.4,13.8c-0.3,5.2-3.3,10.1-6.1,14.3c-3.1,4.7-6.6,7-12.2,7.9c-5.2,0.8-11.7,1.6-15.4-3
|
||||||
|
c-6.6-8.2,2.1-20.5,7.4-27.1c6.5-8.1,20.1-14,26.4-2.1c5.4,10.3-3.1,21.7-13,24.8c-5.7,1.8-11,0.9-16.2-1.9c-2-1.1-5-2.6-6.6-4.4
|
||||||
|
c-3.9-4.3-0.3-8.2,2.5-11.2c1.3-1.4-0.8-3.6-2.1-2.1c-2.7,2.9-5.8,6.6-5.1,10.9c0.7,4.4,5.6,6.9,9,8.9c8.6,5.1,18.7,4.8,26.8-1.2
|
||||||
|
c7.3-5.4,11.6-15,8-23.7c-3.3-8.1-11.7-11.8-20-9c-12.5,4.1-33.7,33.5-15.9,43.1c6.8,3.7,19.8,1.8,25.3-3.6
|
||||||
|
c6.1-5.8,12.1-17.2,9.5-25.7c-2.6-8.4-13.7-17-22.6-13.3c-1.6,0.7-3,1.7-4.1,3c-1.6,1.9-2.2,5.1-4.1,6.6c-3.1,2.4-10.1,1-13.7,1
|
||||||
|
c-4,0-7.9,0-11.9-0.1C164,294,164,297,165.9,297L165.9,297z'/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
const newFolderTutorialElement = (
|
||||||
|
<svg width='800' height='500' className='tutorial'>
|
||||||
|
<text x='30' y='110' fill={BRAND_COLOR} fontSize='24'>Create a new folder!!</text>
|
||||||
|
<text x='50' y='135' fill={BRAND_COLOR} fontSize='16'>{'press ' + (OSX ? '`⌘ + Shift + n`' : '`^ + Shift + n`')}</text>
|
||||||
|
<svg x='50' y='10' width='300' height='400'>
|
||||||
|
<path fill='white' d='M94.1,10.9C77.7,15.6,62,22.7,47.8,32.1c-13.6,9-27.7,20.4-37.1,33.9c-1.1,1.6,1.5,3.1,2.6,1.5
|
||||||
|
C22.6,54.1,37,42.7,50.6,33.8c13.7-8.8,28.6-15.5,44.2-20C96.7,13.3,95.9,10.4,94.1,10.9L94.1,10.9z'/>
|
||||||
|
<path fill='white' d='M71.1,8.6c7.9,1.6,15.8,3.2,23.6,4.7c-0.1-0.9-0.2-1.8-0.4-2.7c-4.6,3.4-5.4,7.7-4.4,13.2
|
||||||
|
c0.8,4.4,0.8,10.9,5.6,12.8c1.8,0.7,2.6-2.2,0.8-2.9c-2.3-1-2.6-6.2-3-8.3c-0.9-4.5-1.7-9,2.5-12.1c0.9-0.7,1-2.5-0.4-2.7
|
||||||
|
C87.5,9,79.6,7.4,71.8,5.9C70,5.4,69.2,8.3,71.1,8.6L71.1,8.6z'/>
|
||||||
|
</svg>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
class SideNav extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
this.newFolderHandler = e => {
|
||||||
|
if (isModalOpen()) return true
|
||||||
|
this.handleNewFolderButton(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
ipc.on('nav-new-folder', this.newFolderHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
ipc.removeListener('nav-new-folder', this.newFolderHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePreferencesButtonClick (e) {
|
||||||
|
openModal(Preferences)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewFolderButton (e) {
|
||||||
|
let { user } = this.props
|
||||||
|
openModal(CreateNewFolder, {user: user})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFolderButtonClick (name) {
|
||||||
|
return e => {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
dispatch(actions.switchFolder(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAllFoldersButtonClick (e) {
|
||||||
|
let { dispatch } = this.props
|
||||||
|
dispatch(actions.setSearchFilter(''))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleNewRepositoryButtonClick (e) {
|
||||||
|
openModal(NewRepositoryModal)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
let { repositories } = this.props
|
||||||
|
let repositorieElements = repositories.map((repo) => {
|
||||||
|
return <Repository
|
||||||
|
key={repo.name}
|
||||||
|
repository={repo}
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='SideNav'
|
||||||
|
styleName='root'
|
||||||
|
tabIndex='1'
|
||||||
|
>
|
||||||
|
<div styleName='menu'>
|
||||||
|
<button styleName='menu-button'
|
||||||
|
>
|
||||||
|
<i className='fa fa-history'/> Recents
|
||||||
|
</button>
|
||||||
|
<button styleName='menu-button'
|
||||||
|
>
|
||||||
|
<i className='fa fa-star'/> Favorited
|
||||||
|
</button>
|
||||||
|
<button styleName='menu-button'
|
||||||
|
>
|
||||||
|
<i className='fa fa-list'/> All posts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='repositoryList'>
|
||||||
|
{repositories.length > 0 ? repositorieElements : (
|
||||||
|
<div styleName='repositoryList-empty'>Empty</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='control'>
|
||||||
|
<button
|
||||||
|
styleName='control-newRepositoryButton'
|
||||||
|
onClick={(e) => this.handleNewRepositoryButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-plus'/> New Repository
|
||||||
|
</button>
|
||||||
|
<button styleName='control-toggleButton'
|
||||||
|
>
|
||||||
|
<i className='fa fa-angle-double-right'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SideNav.propTypes = {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CSSModules(SideNav, styles)
|
||||||
@@ -2,7 +2,7 @@ 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 { toggleTutorial } from '../actions'
|
||||||
import ArticleNavigator from './ArticleNavigator'
|
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'
|
||||||
@@ -73,12 +73,14 @@ class HomePage extends React.Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
|
let { dispatch, status, user, articles, allArticles, modified, activeArticle, folders, tags } = this.props
|
||||||
|
let { repositories } = this.props
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='HomePage'>
|
<div className='HomePage'>
|
||||||
<ArticleNavigator
|
<SideNav
|
||||||
ref='nav'
|
ref='nav'
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
|
repositories={repositories}
|
||||||
status={status}
|
status={status}
|
||||||
user={user}
|
user={user}
|
||||||
folders={folders}
|
folders={folders}
|
||||||
@@ -212,6 +214,8 @@ function remap (state) {
|
|||||||
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
let activeArticle = _.findWhere(articles, {key: status.articleKey})
|
||||||
if (activeArticle == null) activeArticle = articles[0]
|
if (activeArticle == null) activeArticle = articles[0]
|
||||||
|
|
||||||
|
let { repositories } = state
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
folders,
|
folders,
|
||||||
@@ -220,7 +224,8 @@ function remap (state) {
|
|||||||
allArticles,
|
allArticles,
|
||||||
modified,
|
modified,
|
||||||
activeArticle,
|
activeArticle,
|
||||||
tags
|
tags,
|
||||||
|
repositories
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +240,8 @@ HomePage.propTypes = {
|
|||||||
activeArticle: PropTypes.shape(),
|
activeArticle: PropTypes.shape(),
|
||||||
dispatch: PropTypes.func,
|
dispatch: PropTypes.func,
|
||||||
folders: PropTypes.array,
|
folders: PropTypes.array,
|
||||||
tags: PropTypes.array
|
tags: PropTypes.array,
|
||||||
|
repositories: PropTypes.array
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(remap)(HomePage)
|
export default connect(remap)(HomePage)
|
||||||
|
|||||||
@@ -153,6 +153,17 @@ export function toggleTutorial () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v0.6.* Actions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function addRepo (data) {
|
||||||
|
return {
|
||||||
|
type: 'ADD_REPOSITORY',
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
updateUser,
|
updateUser,
|
||||||
|
|
||||||
@@ -174,5 +185,8 @@ export default {
|
|||||||
setSearchFilter,
|
setSearchFilter,
|
||||||
setTagFilter,
|
setTagFilter,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
toggleTutorial
|
toggleTutorial,
|
||||||
|
|
||||||
|
// v0.6.*
|
||||||
|
addRepo
|
||||||
}
|
}
|
||||||
|
|||||||
175
browser/main/modal/NewRepositoryModal.js
Normal file
175
browser/main/modal/NewRepositoryModal.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import React, { PropTypes } from 'react'
|
||||||
|
import CSSModules from 'browser/lib/CSSModules'
|
||||||
|
import styles from './NewRepositoryModal.styl'
|
||||||
|
import linkState from 'browser/lib/linkState'
|
||||||
|
import RepositoryManager from 'browser/lib/RepositoryManager'
|
||||||
|
import store from 'browser/main/store'
|
||||||
|
import actions from 'browser/main/actions'
|
||||||
|
|
||||||
|
const electron = require('electron')
|
||||||
|
const remote = electron.remote
|
||||||
|
|
||||||
|
function browseFolder () {
|
||||||
|
let dialog = remote.dialog
|
||||||
|
|
||||||
|
let defaultPath = remote.app.getHomeDir()
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
dialog.showOpenDialog({
|
||||||
|
title: 'Select Directory',
|
||||||
|
defaultPath,
|
||||||
|
properties: ['openDirectory', 'createDirectory']
|
||||||
|
}, function (targetPaths) {
|
||||||
|
if (targetPaths == null) return resolve('')
|
||||||
|
resolve(targetPaths[0])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewRepositoryModal extends React.Component {
|
||||||
|
constructor (props) {
|
||||||
|
super(props)
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
name: '',
|
||||||
|
path: '',
|
||||||
|
isPathSectionFocused: false,
|
||||||
|
error: null,
|
||||||
|
isBrowsingPath: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseButtonClick (e) {
|
||||||
|
this.props.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePathFocus (e) {
|
||||||
|
this.setState({
|
||||||
|
isPathSectionFocused: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePathBlur (e) {
|
||||||
|
if (e.relatedTarget !== this.refs.pathInput && e.relatedTarget !== this.refs.browseButton) {
|
||||||
|
this.setState({
|
||||||
|
isPathSectionFocused: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleBrowseButtonClick (e) {
|
||||||
|
this.setState({
|
||||||
|
isBrowsingPath: true
|
||||||
|
}, () => {
|
||||||
|
browseFolder()
|
||||||
|
.then((targetPath) => {
|
||||||
|
this.setState({
|
||||||
|
path: targetPath,
|
||||||
|
isBrowsingPath: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('BrowseFAILED')
|
||||||
|
console.error(err)
|
||||||
|
this.setState({
|
||||||
|
isBrowsingPath: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
handleConfirmButtonClick (e) {
|
||||||
|
let targetPath = this.state.path
|
||||||
|
let name = this.state.name
|
||||||
|
|
||||||
|
RepositoryManager
|
||||||
|
.addRepo({
|
||||||
|
targetPath,
|
||||||
|
name
|
||||||
|
})
|
||||||
|
.then((newRepo) => {
|
||||||
|
store.dispatch(actions.addRepo(newRepo))
|
||||||
|
this.props.close()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err)
|
||||||
|
this.setState({
|
||||||
|
error: err.message
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<div className='NewRepositoryModal'
|
||||||
|
styleName='root'
|
||||||
|
>
|
||||||
|
<div styleName='header'>
|
||||||
|
<div styleName='header-title'>New Repository</div>
|
||||||
|
<button styleName='header-closeButton'
|
||||||
|
onClick={(e) => this.handleCloseButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-times'/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div styleName='body'>
|
||||||
|
<div styleName='body-section'>
|
||||||
|
<div styleName='body-section-label'>Repository Name</div>
|
||||||
|
<input styleName='body-section-input'
|
||||||
|
ref='nameInput'
|
||||||
|
valueLink={this.linkState('name')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='body-section'>
|
||||||
|
<div styleName='body-section-label'>Repository Path</div>
|
||||||
|
<div styleName={!this.state.isPathSectionFocused ? 'body-section-path' : 'body-section-path--focus'}>
|
||||||
|
<input styleName='body-section-path-input'
|
||||||
|
valueLink={this.linkState('path')}
|
||||||
|
style={styles.body_section_path_input}
|
||||||
|
onFocus={(e) => this.handlePathFocus(e)}
|
||||||
|
onBlur={(e) => this.handlePathBlur(e)}
|
||||||
|
disabled={this.state.isBrowsingPath}
|
||||||
|
/>
|
||||||
|
<button styleName='body-section-path-button'
|
||||||
|
onClick={(e) => this.handleBrowseButtonClick(e)}
|
||||||
|
onFocus={(e) => this.handlePathFocus(e)}
|
||||||
|
onBlur={(e) => this.handlePathBlur(e)}
|
||||||
|
disabled={this.state.isBrowsingPath}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
this.state.error != null && (
|
||||||
|
<div styleName='body-error'>
|
||||||
|
{this.state.error}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div styleName='footer'>
|
||||||
|
<button styleName='footer-cancelButton'
|
||||||
|
onClick={(e) => this.handleCloseButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-times'/> Cancel
|
||||||
|
</button>
|
||||||
|
<button styleName='footer-confirmButton'
|
||||||
|
onClick={(e) => this.handleConfirmButtonClick(e)}
|
||||||
|
>
|
||||||
|
<i className='fa fa-check'/> Confirm
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NewRepositoryModal.propTypes = {
|
||||||
|
close: PropTypes.func
|
||||||
|
}
|
||||||
|
|
||||||
|
NewRepositoryModal.prototype.linkState = linkState
|
||||||
|
|
||||||
|
export default CSSModules(NewRepositoryModal, styles)
|
||||||
133
browser/main/modal/NewRepositoryModal.styl
Normal file
133
browser/main/modal/NewRepositoryModal.styl
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
$modal-width = 550px
|
||||||
|
$modal-header-color = #F2F2F2
|
||||||
|
$body-button-background-color = #2BAC8F
|
||||||
|
|
||||||
|
.root
|
||||||
|
modal()
|
||||||
|
width $modal-width
|
||||||
|
height 310px
|
||||||
|
|
||||||
|
.header
|
||||||
|
height 50px
|
||||||
|
background-color $modal-header-color
|
||||||
|
|
||||||
|
.header-title
|
||||||
|
font-size 24px
|
||||||
|
line-height 50px
|
||||||
|
padding-left 15px
|
||||||
|
|
||||||
|
.header-closeButton
|
||||||
|
position absolute
|
||||||
|
top 8.5px
|
||||||
|
right 8.5px
|
||||||
|
width 33px
|
||||||
|
height 33px
|
||||||
|
font-size 20px
|
||||||
|
background-color transparent
|
||||||
|
border none
|
||||||
|
color #AAA
|
||||||
|
&:hover
|
||||||
|
color #4D4D4D
|
||||||
|
|
||||||
|
.body
|
||||||
|
absolute left right
|
||||||
|
top 50px
|
||||||
|
bottom 50px
|
||||||
|
padding 35px 0
|
||||||
|
|
||||||
|
.body-section
|
||||||
|
height 33px
|
||||||
|
margin-bottom 15px
|
||||||
|
position relative
|
||||||
|
|
||||||
|
.body-section-label
|
||||||
|
absolute top bottom left
|
||||||
|
width 175px
|
||||||
|
text-align right
|
||||||
|
line-height 33px
|
||||||
|
padding-right 15px
|
||||||
|
|
||||||
|
.body-section-input
|
||||||
|
absolute top bottom
|
||||||
|
left 175px
|
||||||
|
width 315px
|
||||||
|
padding 0 10px
|
||||||
|
border $default-border
|
||||||
|
border-radius 5px
|
||||||
|
outline none
|
||||||
|
&:focus
|
||||||
|
border $active-border
|
||||||
|
|
||||||
|
.body-section-path
|
||||||
|
absolute top bottom
|
||||||
|
left 175px
|
||||||
|
width 315px
|
||||||
|
padding 0 10px
|
||||||
|
border $default-border
|
||||||
|
border-radius 5px
|
||||||
|
outline none
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
|
.body-section-path--focus
|
||||||
|
@extend .body-section-path
|
||||||
|
border $active-border
|
||||||
|
|
||||||
|
.body-section-path-input
|
||||||
|
absolute top left bottom
|
||||||
|
width 265px
|
||||||
|
border none
|
||||||
|
outline none
|
||||||
|
padding 0 10px
|
||||||
|
|
||||||
|
.body-section-path-button
|
||||||
|
absolute top right bottom
|
||||||
|
width 50px
|
||||||
|
border none
|
||||||
|
border-left $default-border
|
||||||
|
outline none
|
||||||
|
color white
|
||||||
|
background-color $body-button-background-color
|
||||||
|
transition 0.15s
|
||||||
|
&:hover
|
||||||
|
background-color lighten($body-button-background-color, 7%)
|
||||||
|
&:disabled
|
||||||
|
background-color lighten(gray, 15%)
|
||||||
|
|
||||||
|
.body-error
|
||||||
|
height 33px
|
||||||
|
margin 35px auto 0
|
||||||
|
width 320px
|
||||||
|
border-radius 5px
|
||||||
|
text-align center
|
||||||
|
line-height 33px
|
||||||
|
color $danger-color
|
||||||
|
background-color $danger-lighten-color
|
||||||
|
|
||||||
|
.footer
|
||||||
|
absolute left right bottom
|
||||||
|
height 50px
|
||||||
|
|
||||||
|
.footer-cancelButton
|
||||||
|
position absolute
|
||||||
|
height 33px
|
||||||
|
right 85.5px
|
||||||
|
width 72px
|
||||||
|
top 8.5px
|
||||||
|
border-radius 5px
|
||||||
|
border $default-border
|
||||||
|
background-color darken(white, 0.03)
|
||||||
|
&:hover
|
||||||
|
background-color white
|
||||||
|
|
||||||
|
.footer-confirmButton
|
||||||
|
position absolute
|
||||||
|
height 33px
|
||||||
|
right 8.5px
|
||||||
|
width 72px
|
||||||
|
top 8.5px
|
||||||
|
color white
|
||||||
|
border-radius 5px
|
||||||
|
border none
|
||||||
|
background-color #2BAC8F
|
||||||
|
&:hover
|
||||||
|
background-color lighten($body-button-background-color, 7%)
|
||||||
@@ -36,14 +36,16 @@ const initialStatus = {
|
|||||||
isTutorialOpen: false
|
isTutorialOpen: false
|
||||||
}
|
}
|
||||||
|
|
||||||
dataStore.init()
|
let data = {
|
||||||
let data = dataStore.getData()
|
articles: [],
|
||||||
|
folders: []
|
||||||
|
}
|
||||||
let initialArticles = {
|
let initialArticles = {
|
||||||
data: data && data.articles ? data.articles : [],
|
data: data && data.articles ? data.articles : [],
|
||||||
modified: []
|
modified: []
|
||||||
}
|
}
|
||||||
let initialFolders = data && data.folders ? data.folders : []
|
let initialFolders = data && data.folders ? data.folders : []
|
||||||
let initialUser = dataStore.getUser().user
|
let initialUser = {}
|
||||||
|
|
||||||
function user (state = initialUser, action) {
|
function user (state = initialUser, action) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@@ -298,9 +300,53 @@ function status (state = initialStatus, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v0.6.* Reducers
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repositories
|
||||||
|
* ```
|
||||||
|
* repositories = [{
|
||||||
|
* key: String,
|
||||||
|
* name: String,
|
||||||
|
* path: String, // path of repository
|
||||||
|
* status: String, // status of repository [LOADING, IDLE, ERROR]
|
||||||
|
* folders: {
|
||||||
|
* name: String,
|
||||||
|
* color: String
|
||||||
|
* },
|
||||||
|
* notes: [{
|
||||||
|
* key: String,
|
||||||
|
* title: String,
|
||||||
|
* content: String,
|
||||||
|
* folder: String,
|
||||||
|
* tags: [String],
|
||||||
|
* createdAt: Date,
|
||||||
|
* updatedAt: Date
|
||||||
|
* }]
|
||||||
|
* }]
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
import RepositoryManager from 'browser/lib/RepositoryManager'
|
||||||
|
const initialRepositories = RepositoryManager.getRepos()
|
||||||
|
|
||||||
|
function repositories (state = initialRepositories, action) {
|
||||||
|
console.log(state)
|
||||||
|
switch (action.type) {
|
||||||
|
case 'ADD_REPOSITORY':
|
||||||
|
let repos = state.slice()
|
||||||
|
repos.push(action.data)
|
||||||
|
return repos
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
// v0.6 end
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
user,
|
user,
|
||||||
folders,
|
folders,
|
||||||
articles,
|
articles,
|
||||||
status
|
status,
|
||||||
|
repositories
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user