mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 01:36:22 +00:00
When a tag is selected, the tag list narrows to show only the related ones: all tags associated to the currently visible notes. Clicking on the plus sign near another tag narrows the list again to the tags of notes associated with the firstly AND secondly selected tag. To show every tags again, press the tag icon on the top-left corner of Boostnote. Before:  After:  NOTE: Tags are joined with `&` character (`#` not works) in `location.pathname` thus it will make the tags with this character unavailable. Any suggestion to pass multiple values via pathname?
319 lines
9.5 KiB
JavaScript
319 lines
9.5 KiB
JavaScript
import PropTypes from 'prop-types'
|
|
import React from 'react'
|
|
import CSSModules from 'browser/lib/CSSModules'
|
|
const { remote } = require('electron')
|
|
const { Menu } = remote
|
|
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'
|
|
|
|
class SideNav extends React.Component {
|
|
// TODO: should not use electron stuff v0.7
|
|
|
|
componentDidMount () {
|
|
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
|
|
}
|
|
|
|
componentWillUnmount () {
|
|
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
|
|
}
|
|
|
|
handleMenuButtonClick (e) {
|
|
openModal(PreferencesModal)
|
|
}
|
|
|
|
handleHomeButtonClick (e) {
|
|
const { router } = this.context
|
|
router.push('/home')
|
|
}
|
|
|
|
handleStarredButtonClick (e) {
|
|
const { router } = this.context
|
|
router.push('/starred')
|
|
}
|
|
|
|
handleToggleButtonClick (e) {
|
|
const { dispatch, config } = this.props
|
|
|
|
ConfigManager.set({isSideNavFolded: !config.isSideNavFolded})
|
|
dispatch({
|
|
type: 'SET_IS_SIDENAV_FOLDED',
|
|
isFolded: !config.isSideNavFolded
|
|
})
|
|
}
|
|
|
|
handleTrashedButtonClick (e) {
|
|
const { router } = this.context
|
|
router.push('/trashed')
|
|
}
|
|
|
|
handleSwitchFoldersButtonClick () {
|
|
const { router } = this.context
|
|
router.push('/home')
|
|
}
|
|
|
|
handleSwitchTagsButtonClick () {
|
|
const { router } = this.context
|
|
router.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') && !location.pathname.match('/narrowToTag')) {
|
|
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 relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
|
let tagList = _.sortBy(data.tagNoteMap.map((tag, name) => {
|
|
return { name, size: tag.size }
|
|
}), ['name']).filter(tag => {
|
|
return (relatedTags.size === 0) ? true : relatedTags.has(tag.name)
|
|
})
|
|
if (config.sortTagsBy === 'COUNTER') {
|
|
tagList = _.sortBy(tagList, item => (0 - item.size))
|
|
}
|
|
return (
|
|
tagList.map(tag => {
|
|
return (
|
|
<TagListItem
|
|
name={tag.name}
|
|
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
|
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
|
isActive={this.getTagActive(location.pathname, tag.name)}
|
|
key={tag.name}
|
|
count={tag.size}
|
|
/>
|
|
)
|
|
})
|
|
)
|
|
}
|
|
|
|
getRelatedTags (activeTags, noteMap) {
|
|
const relatedNotes = noteMap.map(note => {
|
|
return {key: note.key, tags: note.tags}
|
|
}).filter((note) => {
|
|
return activeTags.every((tag) => note.tags.includes(tag))
|
|
})
|
|
let relatedTags = new Set()
|
|
relatedNotes.forEach(note => {
|
|
note.tags.map(tag => {
|
|
relatedTags.add(tag)
|
|
})
|
|
})
|
|
return relatedTags
|
|
}
|
|
|
|
getTagActive (path, tag) {
|
|
const pathTag = this.getActiveTags(path)
|
|
return pathTag.includes(tag)
|
|
}
|
|
|
|
getActiveTags (path) {
|
|
const pathSegments = path.split('/')
|
|
const tags = pathSegments[pathSegments.length - 1]
|
|
if (tags === 'alltags') {
|
|
return []
|
|
}
|
|
return tags.split('&')
|
|
}
|
|
|
|
handleClickTagListItem (name) {
|
|
const { router } = this.context
|
|
router.push(`/tags/${name}`)
|
|
}
|
|
|
|
handleSortTagsByChange (e) {
|
|
const { dispatch } = this.props
|
|
|
|
const config = {
|
|
sortTagsBy: e.target.value
|
|
}
|
|
|
|
ConfigManager.set(config)
|
|
dispatch({
|
|
type: 'SET_CONFIG',
|
|
config
|
|
})
|
|
}
|
|
|
|
handleClickNarrowToTag (name) {
|
|
const { router } = this.context
|
|
const { location } = this.props
|
|
let listOfTags = this.getActiveTags(location.pathname)
|
|
if (listOfTags.includes(name)) {
|
|
listOfTags = listOfTags.filter(function (currentTag) {
|
|
return name !== currentTag
|
|
})
|
|
} else {
|
|
listOfTags.push(name)
|
|
}
|
|
router.push(`/tags/${listOfTags.join('&')}`)
|
|
}
|
|
|
|
emptyTrash (entries) {
|
|
const { dispatch } = this.props
|
|
const deletionPromises = entries.map((note) => {
|
|
return dataApi.deleteNote(note.storage, note.key)
|
|
})
|
|
Promise.all(deletionPromises)
|
|
.then((arrayOfStorageAndNoteKeys) => {
|
|
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
|
dispatch({ type: 'DELETE_NOTE', storageKey, noteKey })
|
|
})
|
|
})
|
|
.catch((err) => {
|
|
console.error('Cannot Delete note: ' + err)
|
|
})
|
|
console.log('Trash emptied')
|
|
}
|
|
|
|
handleFilterButtonContextMenu (event) {
|
|
const { data } = this.props
|
|
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
|
|
const menu = Menu.buildFromTemplate([
|
|
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
|
|
])
|
|
menu.popup()
|
|
}
|
|
|
|
render () {
|
|
const { data, location, config, dispatch } = this.props
|
|
|
|
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
|
|
/>
|
|
})
|
|
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)}
|
|
</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)
|