1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-12 17:26:17 +00:00
Files
Boostnote/browser/main/SideNav/StorageItem.js

426 lines
11 KiB
JavaScript

import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import modal from 'browser/main/lib/modal'
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
import dataApi from 'browser/main/lib/dataApi'
import StorageItemChild from 'browser/components/StorageItem'
import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { push } from 'connected-react-router'
const { remote } = require('electron')
const { dialog } = remote
const escapeStringRegexp = require('escape-string-regexp')
const path = require('path')
class StorageItem extends React.Component {
constructor(props) {
super(props)
const { storage } = this.props
this.state = {
isOpen: !!storage.isOpen,
draggedOver: null
}
}
handleHeaderContextMenu(e) {
context.popup([
{
label: i18n.__('Add Folder'),
click: e => this.handleAddFolderButtonClick(e)
},
{
type: 'separator'
},
{
label: i18n.__('Export Storage'),
submenu: [
{
label: i18n.__('Export as txt'),
click: e => this.handleExportStorageClick(e, 'txt')
},
{
label: i18n.__('Export as md'),
click: e => this.handleExportStorageClick(e, 'md')
}
]
},
{
type: 'separator'
},
{
label: i18n.__('Unlink Storage'),
click: e => this.handleUnlinkStorageClick(e)
}
])
}
handleUnlinkStorageClick(e) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Unlink Storage'),
detail: i18n.__(
"This work will just detatches a storage from Boostnote. (Any data won't be deleted.)"
),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
const { storage, dispatch } = this.props
dataApi
.removeStorage(storage.key)
.then(() => {
dispatch({
type: 'REMOVE_STORAGE',
storageKey: storage.key
})
})
.catch(err => {
throw err
})
}
}
handleExportStorageClick(e, 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 { storage, dispatch } = this.props
dataApi.exportStorage(storage.key, fileType, paths[0]).then(data => {
dispatch({
type: 'EXPORT_STORAGE',
storage: data.storage,
fileType: data.fileType
})
})
}
})
}
handleToggleButtonClick(e) {
const { storage, dispatch } = this.props
const isOpen = !this.state.isOpen
dataApi.toggleStorage(storage.key, isOpen).then(storage => {
dispatch({
type: 'EXPAND_STORAGE',
storage,
isOpen
})
})
this.setState({
isOpen: isOpen
})
}
handleAddFolderButtonClick(e) {
const { storage } = this.props
modal.open(CreateFolderModal, {
storage
})
}
handleHeaderInfoClick(e) {
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key))
}
handleFolderButtonClick(folderKey) {
return e => {
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
}
}
handleFolderButtonContextMenu(e, folder) {
context.popup([
{
label: i18n.__('Rename Folder'),
click: e => this.handleRenameFolderClick(e, folder)
},
{
type: 'separator'
},
{
label: i18n.__('Export Folder'),
submenu: [
{
label: i18n.__('Export as txt'),
click: e => this.handleExportFolderClick(e, folder, 'txt')
},
{
label: i18n.__('Export as md'),
click: e => this.handleExportFolderClick(e, folder, 'md')
}
]
},
{
type: 'separator'
},
{
label: i18n.__('Delete Folder'),
click: e => this.handleFolderDeleteClick(e, folder)
}
])
}
handleRenameFolderClick(e, folder) {
const { storage } = this.props
modal.open(RenameFolderModal, {
storage,
folder
})
}
handleExportFolderClick(e, folder, 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 { storage, dispatch } = this.props
dataApi
.exportFolder(storage.key, folder.key, fileType, paths[0])
.then(data => {
dispatch({
type: 'EXPORT_FOLDER',
storage: data.storage,
folderKey: data.folderKey,
fileType: data.fileType
})
return data
})
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: 'Exported to "' + data.exportDir + '"'
})
})
.catch(err => {
dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
}
})
}
handleFolderDeleteClick(e, folder) {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Delete Folder'),
detail: i18n.__(
'This will delete all notes in the folder and can not be undone.'
),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (index === 0) {
const { storage, dispatch } = this.props
dataApi.deleteFolder(storage.key, folder.key).then(data => {
dispatch({
type: 'DELETE_FOLDER',
storage: data.storage,
folderKey: data.folderKey
})
})
}
}
handleDragEnter(e, key) {
e.preventDefault()
if (this.state.draggedOver === key) {
return
}
this.setState({
draggedOver: key
})
}
handleDragLeave(e) {
e.preventDefault()
if (this.state.draggedOver === null) {
return
}
this.setState({
draggedOver: null
})
}
dropNote(storage, folder, dispatch, location, noteData) {
noteData = noteData.filter(note => folder.key !== note.folder)
if (noteData.length === 0) return
Promise.all(
noteData.map(note =>
dataApi.moveNote(note.storage, note.key, storage.key, folder.key)
)
)
.then(createdNoteData => {
createdNoteData.forEach(newNote => {
dispatch({
type: 'MOVE_NOTE',
originNote: noteData.find(
note => note.content === newNote.oldContent
),
note: newNote
})
})
})
.catch(err => {
console.error(`error on delete notes: ${err}`)
})
}
handleDrop(e, storage, folder, dispatch, location) {
e.preventDefault()
if (this.state.draggedOver !== null) {
this.setState({
draggedOver: null
})
}
const noteData = JSON.parse(e.dataTransfer.getData('note'))
this.dropNote(storage, folder, dispatch, location, noteData)
}
render() {
const { storage, location, isFolded, data, dispatch } = this.props
const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
const folderRegex = new RegExp(
escapeStringRegexp(path.sep) +
'storages' +
escapeStringRegexp(path.sep) +
storage.key +
escapeStringRegexp(path.sep) +
'folders' +
escapeStringRegexp(path.sep) +
folder.key
)
const isActive = !!location.pathname.match(folderRegex)
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
let noteCount = 0
if (noteSet) {
let trashedNoteCount = 0
const noteKeys = noteSet.map(noteKey => {
return noteKey
})
trashedSet.toJS().forEach(trashedKey => {
if (
noteKeys.some(noteKey => {
return noteKey === trashedKey
})
)
trashedNoteCount++
})
noteCount = noteSet.size - trashedNoteCount
}
return (
<SortableStorageItemChild
key={folder.key}
index={index}
isActive={isActive || folder.key === this.state.draggedOver}
handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={isFolded}
noteCount={noteCount}
handleDrop={e => {
this.handleDrop(e, storage, folder, dispatch, location)
}}
handleDragEnter={e => {
this.handleDragEnter(e, folder.key)
}}
handleDragLeave={e => {
this.handleDragLeave(e, folder)
}}
/>
)
})
const isActive = location.pathname.match(
new RegExp(
escapeStringRegexp(path.sep) +
'storages' +
escapeStringRegexp(path.sep) +
storage.key +
'$'
)
)
return (
<div styleName={isFolded ? 'root--folded' : 'root'} key={storage.key}>
<div
styleName={isActive ? 'header--active' : 'header'}
onContextMenu={e => this.handleHeaderContextMenu(e)}
>
<button
styleName='header-toggleButton'
onMouseDown={e => this.handleToggleButtonClick(e)}
>
<img
src={
this.state.isOpen
? '../resources/icon/icon-down.svg'
: '../resources/icon/icon-right.svg'
}
/>
</button>
{!isFolded && (
<button
styleName='header-addFolderButton'
onClick={e => this.handleAddFolderButtonClick(e)}
>
<img src='../resources/icon/icon-plus.svg' />
</button>
)}
<button
styleName='header-info'
onClick={e => this.handleHeaderInfoClick(e)}
>
<span>
{isFolded
? _.truncate(storage.name, { length: 1, omission: '' })
: storage.name}
</span>
{isFolded && (
<span styleName='header-info--folded-tooltip'>
{storage.name}
</span>
)}
</button>
</div>
{this.state.isOpen && <div>{folderList}</div>}
</div>
)
}
}
StorageItem.propTypes = {
isFolded: PropTypes.bool
}
export default CSSModules(StorageItem, styles)