mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-11 00:36:26 +00:00
482 lines
14 KiB
JavaScript
482 lines
14 KiB
JavaScript
import PropTypes from 'prop-types'
|
|
import React from 'react'
|
|
import { push } from 'connected-react-router'
|
|
import CSSModules from 'browser/lib/CSSModules'
|
|
import dataApi from 'browser/main/lib/dataApi'
|
|
import styles from './SideNav.styl'
|
|
import { openModal } from 'browser/main/lib/modal'
|
|
import PreferencesModal from '../modals/PreferencesModal'
|
|
import ConfigManager from 'browser/main/lib/ConfigManager'
|
|
import StorageItem from './StorageItem'
|
|
import TagListItem from 'browser/components/TagListItem'
|
|
import SideNavFilter from 'browser/components/SideNavFilter'
|
|
import StorageList from 'browser/components/StorageList'
|
|
import NavToggleButton from 'browser/components/NavToggleButton'
|
|
import EventEmitter from 'browser/main/lib/eventEmitter'
|
|
import PreferenceButton from './PreferenceButton'
|
|
import ListButton from './ListButton'
|
|
import TagButton from './TagButton'
|
|
import {SortableContainer} from 'react-sortable-hoc'
|
|
import i18n from 'browser/lib/i18n'
|
|
import context from 'browser/lib/context'
|
|
import { remote } from 'electron'
|
|
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
|
import ColorPicker from 'browser/components/ColorPicker'
|
|
|
|
function matchActiveTags (tags, activeTags) {
|
|
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
|
}
|
|
|
|
class SideNav extends React.Component {
|
|
// TODO: should not use electron stuff v0.7
|
|
constructor (props) {
|
|
super(props)
|
|
|
|
this.state = {
|
|
colorPicker: {
|
|
show: false,
|
|
color: null,
|
|
tagName: null,
|
|
targetRect: null
|
|
}
|
|
}
|
|
|
|
this.dismissColorPicker = this.dismissColorPicker.bind(this)
|
|
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
|
|
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
|
|
}
|
|
|
|
componentDidMount () {
|
|
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
|
|
}
|
|
|
|
deleteTag (tag) {
|
|
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
|
ype: 'warning',
|
|
message: i18n.__('Confirm tag deletion'),
|
|
detail: i18n.__('This will permanently remove this tag.'),
|
|
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
|
})
|
|
|
|
if (selectedButton === 0) {
|
|
const { data, dispatch, location, match: { params } } = this.props
|
|
|
|
const notes = data.noteMap
|
|
.map(note => note)
|
|
.filter(note => note.tags.indexOf(tag) !== -1)
|
|
.map(note => {
|
|
note = Object.assign({}, note)
|
|
note.tags = note.tags.slice()
|
|
|
|
note.tags.splice(note.tags.indexOf(tag), 1)
|
|
|
|
return note
|
|
})
|
|
|
|
Promise
|
|
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
|
|
.then(updatedNotes => {
|
|
updatedNotes.forEach(note => {
|
|
dispatch({
|
|
type: 'UPDATE_NOTE',
|
|
note
|
|
})
|
|
})
|
|
|
|
if (location.pathname.match('/tags')) {
|
|
const tags = params.tagname.split(' ')
|
|
const index = tags.indexOf(tag)
|
|
if (index !== -1) {
|
|
tags.splice(index, 1)
|
|
|
|
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
handleMenuButtonClick (e) {
|
|
openModal(PreferencesModal)
|
|
}
|
|
|
|
handleHomeButtonClick (e) {
|
|
const { dispatch } = this.props
|
|
dispatch(push('/home'))
|
|
}
|
|
|
|
handleStarredButtonClick (e) {
|
|
const { dispatch } = this.props
|
|
dispatch(push('/starred'))
|
|
}
|
|
|
|
handleTagContextMenu (e, tag) {
|
|
const menu = []
|
|
|
|
menu.push({
|
|
label: i18n.__('Delete Tag'),
|
|
click: this.deleteTag.bind(this, tag)
|
|
})
|
|
|
|
menu.push({
|
|
label: i18n.__('Customize Color'),
|
|
click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
|
|
})
|
|
|
|
context.popup(menu)
|
|
}
|
|
|
|
dismissColorPicker () {
|
|
this.setState({
|
|
colorPicker: {
|
|
show: false
|
|
}
|
|
})
|
|
}
|
|
|
|
displayColorPicker (tagName, rect) {
|
|
const { config } = this.props
|
|
this.setState({
|
|
colorPicker: {
|
|
show: true,
|
|
color: config.coloredTags[tagName],
|
|
tagName,
|
|
targetRect: rect
|
|
}
|
|
})
|
|
}
|
|
|
|
handleColorPickerConfirm (color) {
|
|
const { dispatch, config: {coloredTags} } = this.props
|
|
const { colorPicker: { tagName } } = this.state
|
|
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
|
|
|
|
const config = { coloredTags: newColoredTags }
|
|
ConfigManager.set(config)
|
|
dispatch({
|
|
type: 'SET_CONFIG',
|
|
config
|
|
})
|
|
this.dismissColorPicker()
|
|
}
|
|
|
|
handleColorPickerReset () {
|
|
const { dispatch, config: {coloredTags} } = this.props
|
|
const { colorPicker: { tagName } } = this.state
|
|
const newColoredTags = Object.assign({}, coloredTags)
|
|
|
|
delete newColoredTags[tagName]
|
|
|
|
const config = { coloredTags: newColoredTags }
|
|
ConfigManager.set(config)
|
|
dispatch({
|
|
type: 'SET_CONFIG',
|
|
config
|
|
})
|
|
this.dismissColorPicker()
|
|
}
|
|
|
|
handleToggleButtonClick (e) {
|
|
const { dispatch, config } = this.props
|
|
|
|
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
|
|
dispatch({
|
|
type: 'SET_IS_SIDENAV_FOLDED',
|
|
isFolded: !config.isSideNavFolded
|
|
})
|
|
}
|
|
|
|
handleTrashedButtonClick (e) {
|
|
const { dispatch } = this.props
|
|
dispatch(push('/trashed'))
|
|
}
|
|
|
|
handleSwitchFoldersButtonClick () {
|
|
const { dispatch } = this.props
|
|
dispatch(push('/home'))
|
|
}
|
|
|
|
handleSwitchTagsButtonClick () {
|
|
const { dispatch } = this.props
|
|
dispatch(push('/alltags'))
|
|
}
|
|
|
|
onSortEnd (storage) {
|
|
return ({oldIndex, newIndex}) => {
|
|
const { dispatch } = this.props
|
|
dataApi
|
|
.reorderFolder(storage.key, oldIndex, newIndex)
|
|
.then((data) => {
|
|
dispatch({ type: 'REORDER_FOLDER', storage: data.storage })
|
|
})
|
|
}
|
|
}
|
|
|
|
SideNavComponent (isFolded, storageList) {
|
|
const { location, data, config } = this.props
|
|
|
|
const isHomeActive = !!location.pathname.match(/^\/home$/)
|
|
const isStarredActive = !!location.pathname.match(/^\/starred$/)
|
|
const isTrashedActive = !!location.pathname.match(/^\/trashed$/)
|
|
|
|
let component
|
|
|
|
// TagsMode is not selected
|
|
if (!location.pathname.match('/tags') && !location.pathname.match('/alltags')) {
|
|
component = (
|
|
<div>
|
|
<SideNavFilter
|
|
isFolded={isFolded}
|
|
isHomeActive={isHomeActive}
|
|
handleAllNotesButtonClick={(e) => this.handleHomeButtonClick(e)}
|
|
isStarredActive={isStarredActive}
|
|
isTrashedActive={isTrashedActive}
|
|
handleStarredButtonClick={(e) => this.handleStarredButtonClick(e)}
|
|
handleTrashedButtonClick={(e) => this.handleTrashedButtonClick(e)}
|
|
counterTotalNote={data.noteMap._map.size - data.trashedSet._set.size}
|
|
counterStarredNote={data.starredSet._set.size}
|
|
counterDelNote={data.trashedSet._set.size}
|
|
handleFilterButtonContextMenu={this.handleFilterButtonContextMenu.bind(this)}
|
|
/>
|
|
|
|
<StorageList storageList={storageList} isFolded={isFolded} />
|
|
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
|
|
</div>
|
|
)
|
|
} else {
|
|
component = (
|
|
<div styleName='tabBody'>
|
|
<div styleName='tag-control'>
|
|
<div styleName='tag-control-title'>
|
|
<p>{i18n.__('Tags')}</p>
|
|
</div>
|
|
<div styleName='tag-control-sortTagsBy'>
|
|
<i className='fa fa-angle-down' />
|
|
<select styleName='tag-control-sortTagsBy-select'
|
|
title={i18n.__('Select filter mode')}
|
|
value={config.sortTagsBy}
|
|
onChange={(e) => this.handleSortTagsByChange(e)}
|
|
>
|
|
<option title='Sort alphabetically'
|
|
value='ALPHABETICAL'>{i18n.__('Alphabetically')}</option>
|
|
<option title='Sort by update time'
|
|
value='COUNTER'>{i18n.__('Counter')}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div styleName='tagList'>
|
|
{this.tagListComponent(data)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return component
|
|
}
|
|
|
|
tagListComponent () {
|
|
const { data, location, config } = this.props
|
|
const { colorPicker } = this.state
|
|
const activeTags = this.getActiveTags(location.pathname)
|
|
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
|
let tagList = _.sortBy(data.tagNoteMap.map(
|
|
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
|
).filter(
|
|
tag => tag.size > 0
|
|
), ['name'])
|
|
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
|
|
const notesTags = data.noteMap.map(note => note.tags)
|
|
tagList = tagList.map(tag => {
|
|
tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
|
|
return tag
|
|
})
|
|
}
|
|
if (config.sortTagsBy === 'COUNTER') {
|
|
tagList = _.sortBy(tagList, item => (0 - item.size))
|
|
}
|
|
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
|
|
tagList = tagList.filter(
|
|
tag => tag.related
|
|
)
|
|
}
|
|
return (
|
|
tagList.map(tag => {
|
|
return (
|
|
<TagListItem
|
|
name={tag.name}
|
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
|
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
|
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
|
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
|
|
isRelated={tag.related}
|
|
key={tag.name}
|
|
count={tag.size}
|
|
color={config.coloredTags[tag.name]}
|
|
/>
|
|
)
|
|
})
|
|
)
|
|
}
|
|
|
|
getRelatedTags (activeTags, noteMap) {
|
|
if (activeTags.length === 0) {
|
|
return new Set()
|
|
}
|
|
const relatedNotes = noteMap.map(
|
|
note => ({key: note.key, tags: note.tags})
|
|
).filter(
|
|
note => activeTags.every(tag => note.tags.includes(tag))
|
|
)
|
|
const relatedTags = new Set()
|
|
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
|
|
return relatedTags
|
|
}
|
|
|
|
getTagActive (path, tag) {
|
|
return this.getActiveTags(path).includes(tag)
|
|
}
|
|
|
|
getActiveTags (path) {
|
|
const pathSegments = path.split('/')
|
|
const tags = pathSegments[pathSegments.length - 1]
|
|
return (tags === 'alltags')
|
|
? []
|
|
: decodeURIComponent(tags).split(' ')
|
|
}
|
|
|
|
handleClickTagListItem (name) {
|
|
const { dispatch } = this.props
|
|
dispatch(push(`/tags/${encodeURIComponent(name)}`))
|
|
}
|
|
|
|
handleSortTagsByChange (e) {
|
|
const { dispatch } = this.props
|
|
|
|
const config = {
|
|
sortTagsBy: e.target.value
|
|
}
|
|
|
|
ConfigManager.set(config)
|
|
dispatch({
|
|
type: 'SET_CONFIG',
|
|
config
|
|
})
|
|
}
|
|
|
|
handleClickNarrowToTag (tag) {
|
|
const { dispatch, location } = this.props
|
|
const listOfTags = this.getActiveTags(location.pathname)
|
|
const indexOfTag = listOfTags.indexOf(tag)
|
|
if (indexOfTag > -1) {
|
|
listOfTags.splice(indexOfTag, 1)
|
|
} else {
|
|
listOfTags.push(tag)
|
|
}
|
|
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
|
|
}
|
|
|
|
emptyTrash (entries) {
|
|
const { dispatch } = this.props
|
|
const deletionPromises = entries.map((note) => {
|
|
return dataApi.deleteNote(note.storage, note.key)
|
|
})
|
|
const { confirmDeletion } = this.props.config.ui
|
|
if (!confirmDeleteNote(confirmDeletion, true)) return
|
|
Promise.all(deletionPromises)
|
|
.then((arrayOfStorageAndNoteKeys) => {
|
|
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
|
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
|
})
|
|
})
|
|
.catch((err) => {
|
|
console.error('Cannot Delete note: ' + err)
|
|
})
|
|
}
|
|
|
|
handleFilterButtonContextMenu (event) {
|
|
const { data } = this.props
|
|
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
|
context.popup([
|
|
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
|
|
])
|
|
}
|
|
|
|
render () {
|
|
const { data, location, config, dispatch } = this.props
|
|
const { colorPicker: colorPickerState } = this.state
|
|
|
|
const isFolded = config.isSideNavFolded
|
|
|
|
const storageList = data.storageMap.map((storage, key) => {
|
|
const SortableStorageItem = SortableContainer(StorageItem)
|
|
return <SortableStorageItem
|
|
key={storage.key}
|
|
storage={storage}
|
|
data={data}
|
|
location={location}
|
|
isFolded={isFolded}
|
|
dispatch={dispatch}
|
|
onSortEnd={this.onSortEnd.bind(this)(storage)}
|
|
useDragHandle
|
|
/>
|
|
})
|
|
|
|
let colorPicker
|
|
if (colorPickerState.show) {
|
|
colorPicker = (
|
|
<ColorPicker
|
|
color={colorPickerState.color}
|
|
targetRect={colorPickerState.targetRect}
|
|
onConfirm={this.handleColorPickerConfirm}
|
|
onCancel={this.dismissColorPicker}
|
|
onReset={this.handleColorPickerReset}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const style = {}
|
|
if (!isFolded) style.width = this.props.width
|
|
const isTagActive = location.pathname.match(/tag/)
|
|
return (
|
|
<div className='SideNav'
|
|
styleName={isFolded ? 'root--folded' : 'root'}
|
|
tabIndex='1'
|
|
style={style}
|
|
>
|
|
<div styleName='top'>
|
|
<div styleName='switch-buttons'>
|
|
<ListButton onClick={this.handleSwitchFoldersButtonClick.bind(this)} isTagActive={isTagActive} />
|
|
<TagButton onClick={this.handleSwitchTagsButtonClick.bind(this)} isTagActive={isTagActive} />
|
|
</div>
|
|
<div>
|
|
<PreferenceButton onClick={this.handleMenuButtonClick} />
|
|
</div>
|
|
</div>
|
|
{this.SideNavComponent(isFolded, storageList)}
|
|
{colorPicker}
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
SideNav.contextTypes = {
|
|
router: PropTypes.shape({})
|
|
}
|
|
|
|
SideNav.propTypes = {
|
|
dispatch: PropTypes.func,
|
|
storages: PropTypes.array,
|
|
config: PropTypes.shape({
|
|
isSideNavFolded: PropTypes.bool
|
|
}),
|
|
location: PropTypes.shape({
|
|
pathname: PropTypes.string
|
|
})
|
|
}
|
|
|
|
export default CSSModules(SideNav, styles)
|