diff --git a/browser/lib/Mutable.js b/browser/lib/Mutable.js index bcf39275..1f514fb2 100644 --- a/browser/lib/Mutable.js +++ b/browser/lib/Mutable.js @@ -61,7 +61,7 @@ class MutableSet { constructor (iterable) { this._set = new Set(iterable) Object.defineProperty(this, 'size', { - get: () => this._map.size, + get: () => this._set.size, set: function (value) { this['size'] = value } diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js index b1b3dcdf..6c4de386 100644 --- a/browser/main/NoteList/index.js +++ b/browser/main/NoteList/index.js @@ -38,7 +38,6 @@ class NoteList extends React.Component { } this.state = { - range: 0 } } @@ -57,19 +56,6 @@ class NoteList extends React.Component { resetScroll () { this.refs.list.scrollTop = 0 - this.setState({ - range: 0 - }) - } - - handleScroll (e) { - let notes = this.notes - - if (e.target.offsetHeight + e.target.scrollTop > e.target.scrollHeight - 250 && notes.length > this.state.range * 20 + 20) { - this.setState({ - range: this.state.range + 1 - }) - } } componentWillUnmount () { @@ -82,6 +68,7 @@ class NoteList extends React.Component { componentDidUpdate (prevProps) { let { location } = this.props + if (this.notes.length > 0 && location.query.key == null) { let { router } = this.context router.replace({ @@ -325,7 +312,7 @@ class NoteList extends React.Component { this.notes = notes = this.getNotes() .sort(sortFunc) - let noteList = notes.slice(0, 20 + 20 * this.state.range) + let noteList = notes .map((note) => { if (note == null) return null let tagElements = _.isArray(note.tags) @@ -424,7 +411,6 @@ class NoteList extends React.Component { ref='list' tabIndex='-1' onKeyDown={(e) => this.handleNoteListKeyDown(e)} - onScroll={(e) => this.handleScroll(e)} > {noteList} diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index 27933bd3..e6388ad1 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -2,6 +2,13 @@ import React, { PropTypes } from 'react' import CSSModules from 'browser/lib/CSSModules' import styles from './StorageItem.styl' import { hashHistory } from 'react-router' +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' + +const { remote } = require('electron') +const { Menu, MenuItem, dialog } = remote class StorageItem extends React.Component { constructor (props) { @@ -12,12 +19,59 @@ class StorageItem extends React.Component { } } + handleHeaderContextMenu (e) { + let menu = new Menu() + menu.append(new MenuItem({ + label: 'Add Folder', + click: (e) => this.handleAddFolderButtonClick(e) + })) + menu.append(new MenuItem({ + type: 'separator' + })) + menu.append(new MenuItem({ + label: 'Unlink Storage', + click: (e) => this.handleUnlinkStorageClick(e) + })) + menu.popup() + } + + handleUnlinkStorageClick (e) { + let index = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'warning', + message: 'Unlink Storage', + detail: 'This work will just detatches a storage from Boostnote. (Any data won\'t be deleted.)', + buttons: ['Confirm', 'Cancel'] + }) + + if (index === 0) { + let { storage, dispatch } = this.props + dataApi.removeStorage(storage.key) + .then(() => { + dispatch({ + type: 'REMOVE_STORAGE', + storageKey: storage.key + }) + }) + .catch((err) => { + throw err + }) + } + } + handleToggleButtonClick (e) { this.setState({ isOpen: !this.state.isOpen }) } + handleAddFolderButtonClick (e) { + let { storage } = this.props + + modal.open(CreateFolderModal, { + storage + }) + } + handleHeaderInfoClick (e) { let { storage } = this.props hashHistory.push('/storages/' + storage.key) @@ -30,22 +84,78 @@ class StorageItem extends React.Component { } } + handleFolderButtonContextMenu (e, folder) { + let menu = new Menu() + menu.append(new MenuItem({ + label: 'Rename Folder', + click: (e) => this.handleRenameFolderClick(e, folder) + })) + menu.append(new MenuItem({ + type: 'separator' + })) + menu.append(new MenuItem({ + label: 'Delete Folder', + click: (e) => this.handleFolderDeleteClick(e, folder) + })) + menu.popup() + } + + handleRenameFolderClick (e, folder) { + let { storage } = this.props + modal.open(RenameFolderModal, { + storage, + folder + }) + } + + handleFolderDeleteClick (e, folder) { + let index = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'warning', + message: 'Delete Folder', + detail: 'This work will deletes all notes in the folder and can not be undone.', + buttons: ['Confirm', 'Cancel'] + }) + + if (index === 0) { + let { storage, dispatch } = this.props + dataApi + .deleteFolder(storage.key, folder.key) + .then((data) => { + dispatch({ + type: 'DELETE_FOLDER', + storage: data.storage, + folderKey: data.folderKey + }) + }) + } + } + render () { - let { storage, location, isFolded } = this.props + let { storage, location, isFolded, data } = this.props + let { folderNoteMap } = data let folderList = storage.folders.map((folder) => { let isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key)) + let noteSet = folderNoteMap.get(storage.key + '-' + folder.key) + + let noteCount = noteSet != null + ? noteSet.size + : 0 return + + {!isFolded && + + } + diff --git a/browser/main/SideNav/StorageItem.styl b/browser/main/SideNav/StorageItem.styl index 9197d2df..029cd8a7 100644 --- a/browser/main/SideNav/StorageItem.styl +++ b/browser/main/SideNav/StorageItem.styl @@ -9,15 +9,22 @@ background-color $ui-button--hover-backgroundColor &:active .header-toggleButton + .header-addFolderButton color white + &:active + color $ui-active-color + .header--active @extend .header .header-info color $ui-button--active-color background-color $ui-button--active-backgroundColor .header-toggleButton + .header-addFolderButton color white &:active + &:hover + &:hover:active color white .header-toggleButton @@ -55,8 +62,23 @@ .header-info-path font-size 10px margin 0 5px + +.header-addFolderButton + position absolute + right 0 + width 25px + height 26px + padding 0 + border none + color $ui-inactive-text-color + background-color transparent + &:hover + color $ui-text-color + &:active + color $ui-active-color + .folderList-item - display block + display flex width 100% height 26px background-color transparent @@ -74,6 +96,7 @@ &:active color $ui-button--active-color background-color $ui-button--active-backgroundColor + .folderList-item--active @extend .folderList-item color $ui-button--active-color @@ -81,14 +104,23 @@ &:hover color $ui-button--active-color background-color $ui-button--active-backgroundColor + .folderList-item-name display block + flex 1 padding 0 10px height 26px line-height 26px border-width 0 0 0 6px border-style solid border-color transparent + +.folderList-item-noteCount + float right + line-height 26px + padding-right 5px + font-size 12px + .folderList-item-tooltip tooltip() position fixed @@ -102,6 +134,7 @@ height 26px margin-top -26px line-height 26px + .root--folded @extend .root .header @@ -133,3 +166,17 @@ opacity 1 .folderList-item-name padding-left 14px +body[data-theme="dark"] + .header-toggleButton + .header-addFolderButton + color $ui-dark-inactive-text-color + &:hover + color $ui-dark-text-color + &:active + color $ui-dark-active-color + .header--active + .header-toggleButton + .header-addFolderButton + color white + &:active + color white diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 09e312ff..9b80791b 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -36,7 +36,7 @@ class SideNav extends React.Component { } render () { - let { data, location, config } = this.props + let { data, location, config, dispatch } = this.props let isFolded = config.isSideNavFolded let isHomeActive = location.pathname.match(/^\/home$/) @@ -46,8 +46,10 @@ class SideNav extends React.Component { return }) let style = {} diff --git a/browser/main/modals/CreateFolderModal.js b/browser/main/modals/CreateFolderModal.js new file mode 100644 index 00000000..0b8adeca --- /dev/null +++ b/browser/main/modals/CreateFolderModal.js @@ -0,0 +1,109 @@ +import React, { PropTypes } from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './CreateFolderModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import store from 'browser/main/store' +import consts from 'browser/lib/consts' + +class CreateFolderModal extends React.Component { + constructor (props) { + super(props) + + this.state = { + name: '' + } + } + + componentDidMount () { + this.refs.name.focus() + this.refs.name.select() + } + + handleCloseButtonClick (e) { + this.props.close() + } + + handleChange (e) { + this.setState({ + name: this.refs.name.value + }) + } + + handleKeyDown (e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown (e) { + switch (e.keyCode) { + case 13: + this.confirm() + } + } + + handleConfirmButtonClick (e) { + this.confirm() + } + + confirm () { + if (this.state.name.trim().length > 0) { + let { storage } = this.props + let input = { + name: this.state.name.trim(), + color: consts.FOLDER_COLORS[Math.floor(Math.random() * 7) % 7] + } + + dataApi.createFolder(storage.key, input) + .then((data) => { + store.dispatch({ + type: 'UPDATE_FOLDER', + storage: data.storage + }) + this.props.close() + }) + .catch((err) => { + console.error(err) + }) + } + } + + render () { + return ( +
this.handleKeyDown(e)} + > +
+
New Folder
+
+ + +
+ this.handleChange(e)} + onKeyDown={(e) => this.handleInputKeyDown(e)} + /> + +
+
+ ) + } +} + +CreateFolderModal.propTypes = { + storage: PropTypes.shape({ + key: PropTypes.string + }) +} + +export default CSSModules(CreateFolderModal, styles) diff --git a/browser/main/modals/CreateFolderModal.styl b/browser/main/modals/CreateFolderModal.styl new file mode 100644 index 00000000..cc7d20c3 --- /dev/null +++ b/browser/main/modals/CreateFolderModal.styl @@ -0,0 +1,76 @@ +.root + modal() + max-width 340px + overflow hidden + position relative + +.header + height 50px + font-size 18px + line-height 50px + padding 0 15px + background-color $ui-backgroundColor + border-bottom solid 1px $ui-borderColor + color $ui-text-color + +.closeButton + position absolute + top 10px + right 10px + height 30px + width 0 25px + border $ui-border + border-radius 2px + color $ui-text-color + colorDefaultButton() + +.control + padding 25px 15px 15px + text-align center + +.control-input + display block + height 30px + width 240px + padding 0 5px + margin 0 auto 15px + border none + border-bottom solid 1px $border-color + border-radius 2px + outline none + vertical-align middle + font-size 18px + text-align center + &:disabled + background-color $ui-input--disabled-backgroundColor + &:focus, &:active + border-color $ui-active-color + +.control-confirmButton + display block + height 30px + border none + border-radius 2px + padding 0 25px + margin 0 auto + colorPrimaryButton() + +body[data-theme="dark"] + .root + modalDark() + + .header + background-color $ui-dark-button--hover-backgroundColor + border-color $ui-dark-borderColor + color $ui-dark-text-color + + .closeButton + border-color $ui-dark-borderColor + color $ui-dark-text-color + colorDarkDefaultButton() + + .description + color $ui-inactive-text-color + + .control-input + border-color $ui-dark-borderColor diff --git a/browser/main/modals/PreferencesModal/StorageItem.js b/browser/main/modals/PreferencesModal/StorageItem.js index d5251358..ee10833c 100644 --- a/browser/main/modals/PreferencesModal/StorageItem.js +++ b/browser/main/modals/PreferencesModal/StorageItem.js @@ -6,8 +6,8 @@ import consts from 'browser/lib/consts' import dataApi from 'browser/main/lib/dataApi' import store from 'browser/main/store' -const electron = require('electron') -const { shell } = electron +const { shell, remote } = require('electron') +const { dialog } = remote import { SketchPicker } from 'react-color' class UnstyledFolderItem extends React.Component { @@ -292,17 +292,26 @@ class StorageItem extends React.Component { } handleUnlinkButtonClick (e) { - let { storage } = this.props - dataApi.removeStorage(storage.key) - .then(() => { - store.dispatch({ - type: 'REMOVE_STORAGE', - storageKey: storage.key + let index = dialog.showMessageBox(remote.getCurrentWindow(), { + type: 'warning', + message: 'Unlink Storage', + detail: 'This work just detatches a storage from Boostnote. (Any data won\'t be deleted.)', + buttons: ['Confirm', 'Cancel'] + }) + + if (index === 0) { + let { storage, dispatch } = this.props + dataApi.removeStorage(storage.key) + .then(() => { + dispatch({ + type: 'REMOVE_STORAGE', + storageKey: storage.key + }) }) - }) - .catch((err) => { - throw err - }) + .catch((err) => { + throw err + }) + } } handleLabelClick (e) { diff --git a/browser/main/modals/RenameFolderModal.js b/browser/main/modals/RenameFolderModal.js new file mode 100644 index 00000000..107a9f55 --- /dev/null +++ b/browser/main/modals/RenameFolderModal.js @@ -0,0 +1,108 @@ +import React, { PropTypes } from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './RenameFolderModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import store from 'browser/main/store' + +class RenameFolderModal extends React.Component { + constructor (props) { + super(props) + + this.state = { + name: props.folder.name + } + } + + componentDidMount () { + this.refs.name.focus() + this.refs.name.select() + } + + handleCloseButtonClick (e) { + this.props.close() + } + + handleChange (e) { + this.setState({ + name: this.refs.name.value + }) + } + + handleKeyDown (e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown (e) { + switch (e.keyCode) { + case 13: + this.confirm() + } + } + + handleConfirmButtonClick (e) { + this.confirm() + } + + confirm () { + if (this.state.name.trim().length > 0) { + let { storage, folder } = this.props + dataApi + .updateFolder(storage.key, folder.key, { + name: this.state.name, + color: folder.color + }) + .then((data) => { + store.dispatch({ + type: 'UPDATE_FOLDER', + storage: data.storage + }) + this.props.close() + }) + } + } + + render () { + return ( +
this.handleKeyDown(e)} + > +
+
Rename Folder
+
+ + +
+ this.handleChange(e)} + onKeyDown={(e) => this.handleInputKeyDown(e)} + /> + +
+
+ ) + } +} + +RenameFolderModal.propTypes = { + storage: PropTypes.shape({ + key: PropTypes.string + }), + folder: PropTypes.shape({ + key: PropTypes.string, + name: PropTypes.string + }) +} + +export default CSSModules(RenameFolderModal, styles) diff --git a/browser/main/modals/RenameFolderModal.styl b/browser/main/modals/RenameFolderModal.styl new file mode 100644 index 00000000..cc7d20c3 --- /dev/null +++ b/browser/main/modals/RenameFolderModal.styl @@ -0,0 +1,76 @@ +.root + modal() + max-width 340px + overflow hidden + position relative + +.header + height 50px + font-size 18px + line-height 50px + padding 0 15px + background-color $ui-backgroundColor + border-bottom solid 1px $ui-borderColor + color $ui-text-color + +.closeButton + position absolute + top 10px + right 10px + height 30px + width 0 25px + border $ui-border + border-radius 2px + color $ui-text-color + colorDefaultButton() + +.control + padding 25px 15px 15px + text-align center + +.control-input + display block + height 30px + width 240px + padding 0 5px + margin 0 auto 15px + border none + border-bottom solid 1px $border-color + border-radius 2px + outline none + vertical-align middle + font-size 18px + text-align center + &:disabled + background-color $ui-input--disabled-backgroundColor + &:focus, &:active + border-color $ui-active-color + +.control-confirmButton + display block + height 30px + border none + border-radius 2px + padding 0 25px + margin 0 auto + colorPrimaryButton() + +body[data-theme="dark"] + .root + modalDark() + + .header + background-color $ui-dark-button--hover-backgroundColor + border-color $ui-dark-borderColor + color $ui-dark-text-color + + .closeButton + border-color $ui-dark-borderColor + color $ui-dark-text-color + colorDarkDefaultButton() + + .description + color $ui-inactive-text-color + + .control-input + border-color $ui-dark-borderColor