mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-12 17:26:17 +00:00
426 lines
11 KiB
JavaScript
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)
|