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 RenameTagModal from 'browser/main/modals/RenameTagModal'
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 SearchButton from './SearchButton'
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'
import { every, sortBy } from 'lodash'
const { dialog } = remote
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,
showSearch: false,
searchText: ''
}
}
this.dismissColorPicker = this.dismissColorPicker.bind(this)
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
this.handleSearchButtonClick = this.handleSearchButtonClick.bind(this)
this.handleSearchInputChange = this.handleSearchInputChange.bind(this)
this.handleSearchInputClear = this.handleSearchInputClear.bind(this)
}
componentDidMount() {
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
}
componentWillUnmount() {
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
}
deleteTag(tag) {
const selectedButton = dialog.showMessageBox(remote.getCurrentWindow(), {
type: '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)
}
handleSearchButtonClick(e) {
const { showSearch } = this.state
this.setState({
showSearch: !showSearch,
searchText: ''
})
}
handleSearchInputClear(e) {
this.setState({
searchText: ''
})
}
handleSearchInputChange(e) {
this.setState({
searchText: e.target.value
})
}
handleHomeButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/home'))
}
handleStarredButtonClick(e) {
const { dispatch } = this.props
dispatch(push('/starred'))
}
handleTagContextMenu(e, tag) {
context.popup([
{
label: i18n.__('Rename Tag'),
click: this.handleRenameTagClick.bind(this, tag)
},
{
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(
this,
tag,
e.target.getBoundingClientRect()
)
},
{
type: 'separator'
},
{
label: i18n.__('Export Tag'),
submenu: [
{
label: i18n.__('Export as Plain Text (.txt)'),
click: e => this.handleExportTagClick(e, tag, 'txt')
},
{
label: i18n.__('Export as Markdown (.md)'),
click: e => this.handleExportTagClick(e, tag, 'md')
},
{
label: i18n.__('Export as HTML (.html)'),
click: e => this.handleExportTagClick(e, tag, 'html')
},
{
label: i18n.__('Export as PDF (.pdf)'),
click: e => this.handleExportTagClick(e, tag, 'pdf')
}
]
},
{
type: 'separator'
},
{
label: i18n.__('Delete Tag'),
click: this.deleteTag.bind(this, tag)
}
])
}
handleExportTagClick(e, tag, fileType) {
const options = {
properties: ['openDirectory', 'createDirectory'],
buttonLabel: i18n.__('Select directory'),
title: i18n.__('Select a folder to export the files to'),
multiSelections: false
}
dialog.showOpenDialog(remote.getCurrentWindow(), options, paths => {
if (paths && paths.length === 1) {
const { data, config } = this.props
dataApi
.exportTag(data, tag, fileType, paths[0], config)
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${paths[0]}`
})
})
.catch(error => {
dialog.showErrorBox(
'Export error',
error ? error.message || error : 'Unexpected error during export'
)
throw error
})
}
})
}
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
}
})
}
handleRenameTagClick(tagName) {
const { data, dispatch } = this.props
openModal(RenameTagModal, {
tagName,
data,
dispatch
})
}
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
const { showSearch, searchText } = this.state
ConfigManager.set({ isSideNavFolded: !config.isSideNavFolded })
dispatch({
type: 'SET_IS_SIDENAV_FOLDED',
isFolded: !config.isSideNavFolded
})
if (showSearch && searchText.length === 0) {
this.setState({
showSearch: false
})
}
}
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) {
const { location, data, config, dispatch } = this.props
const { showSearch, searchText } = this.state
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')
) {
let storageMap = data.storageMap
if (showSearch && searchText.length > 0) {
storageMap = storageMap.map(storage => {
const folders = storage.folders.filter(
folder =>
folder.name.toLowerCase().indexOf(searchText.toLowerCase()) !== -1
)
return Object.assign({}, storage, { folders })
})
}
const storageList = storageMap.map((storage, key) => {
const SortableStorageItem = SortableContainer(StorageItem)
return (
{i18n.__('Tags')}