mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-24 08:12:24 +02:00
Desktop: When selecting multiple notes, display possible actions as buttons
This commit is contained in:
parent
1d4234caea
commit
21e049ab45
@ -15,6 +15,7 @@ const InteropServiceHelper = require('../InteropServiceHelper.js');
|
|||||||
const Search = require('lib/models/Search');
|
const Search = require('lib/models/Search');
|
||||||
const Mark = require('mark.js/dist/mark.min.js');
|
const Mark = require('mark.js/dist/mark.min.js');
|
||||||
const SearchEngine = require('lib/services/SearchEngine');
|
const SearchEngine = require('lib/services/SearchEngine');
|
||||||
|
const NoteListUtils = require('./utils/NoteListUtils');
|
||||||
const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils');
|
const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils');
|
||||||
|
|
||||||
class NoteListComponent extends React.Component {
|
class NoteListComponent extends React.Component {
|
||||||
@ -83,118 +84,20 @@ class NoteListComponent extends React.Component {
|
|||||||
|
|
||||||
if (!noteIds.length) return;
|
if (!noteIds.length) return;
|
||||||
|
|
||||||
const notes = noteIds.map((id) => BaseModel.byId(this.props.notes, id));
|
const menu = NoteListUtils.makeContextMenu(noteIds, {
|
||||||
|
notes: this.props.notes,
|
||||||
let hasEncrypted = false;
|
dispatch: this.props.dispatch,
|
||||||
for (let i = 0; i < notes.length; i++) {
|
|
||||||
if (!!notes[i].encryption_applied) hasEncrypted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const menu = new Menu()
|
|
||||||
|
|
||||||
if (!hasEncrypted) {
|
|
||||||
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: 'WINDOW_COMMAND',
|
|
||||||
name: 'setTags',
|
|
||||||
noteId: noteIds[0],
|
|
||||||
});
|
});
|
||||||
}}));
|
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Duplicate'), click: async () => {
|
|
||||||
for (let i = 0; i < noteIds.length; i++) {
|
|
||||||
const note = await Note.load(noteIds[i]);
|
|
||||||
await Note.duplicate(noteIds[i], {
|
|
||||||
uniqueTitle: _('%s - Copy', note.title),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}));
|
|
||||||
|
|
||||||
if (noteIds.length <= 1) {
|
|
||||||
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
|
|
||||||
for (let i = 0; i < noteIds.length; i++) {
|
|
||||||
const note = await Note.load(noteIds[i]);
|
|
||||||
await Note.save(Note.toggleIsTodo(note), { userSideValidation: true });
|
|
||||||
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
|
||||||
}
|
|
||||||
}}));
|
|
||||||
} else {
|
|
||||||
const switchNoteType = async (noteIds, type) => {
|
|
||||||
for (let i = 0; i < noteIds.length; i++) {
|
|
||||||
const note = await Note.load(noteIds[i]);
|
|
||||||
const newNote = Note.changeNoteType(note, type);
|
|
||||||
if (newNote === note) continue;
|
|
||||||
await Note.save(newNote, { userSideValidation: true });
|
|
||||||
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Switch to note type'), click: async () => {
|
|
||||||
await switchNoteType(noteIds, 'note');
|
|
||||||
}}));
|
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Switch to to-do type'), click: async () => {
|
|
||||||
await switchNoteType(noteIds, 'todo');
|
|
||||||
}}));
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Copy Markdown link'), click: async () => {
|
|
||||||
const { clipboard } = require('electron');
|
|
||||||
const links = [];
|
|
||||||
for (let i = 0; i < noteIds.length; i++) {
|
|
||||||
const note = await Note.load(noteIds[i]);
|
|
||||||
links.push(Note.markdownTag(note));
|
|
||||||
}
|
|
||||||
clipboard.writeText(links.join(' '));
|
|
||||||
}}));
|
|
||||||
|
|
||||||
const exportMenu = new Menu();
|
|
||||||
|
|
||||||
const ioService = new InteropService();
|
|
||||||
const ioModules = ioService.modules();
|
|
||||||
for (let i = 0; i < ioModules.length; i++) {
|
|
||||||
const module = ioModules[i];
|
|
||||||
if (module.type !== 'exporter') continue;
|
|
||||||
|
|
||||||
exportMenu.append(new MenuItem({ label: module.fullLabel() , click: async () => {
|
|
||||||
await InteropServiceHelper.export(this.props.dispatch.bind(this), module, { sourceNoteIds: noteIds });
|
|
||||||
}}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noteIds.length === 1) {
|
|
||||||
exportMenu.append(new MenuItem({ label: 'PDF - ' + _('PDF File') , click: () => {
|
|
||||||
this.props.dispatch({
|
|
||||||
type: 'WINDOW_COMMAND',
|
|
||||||
name: 'exportPdf',
|
|
||||||
});
|
|
||||||
}}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportMenuItem = new MenuItem({label: _('Export'), submenu: exportMenu});
|
|
||||||
|
|
||||||
menu.append(exportMenuItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
|
||||||
await this.confirmDeleteNotes(noteIds);
|
|
||||||
}}));
|
|
||||||
|
|
||||||
menu.popup(bridge().window());
|
menu.popup(bridge().window());
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmDeleteNotes(noteIds) {
|
|
||||||
if (!noteIds.length) return;
|
|
||||||
const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
|
|
||||||
if (!ok) return;
|
|
||||||
await Note.batchDelete(noteIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
itemRenderer(item) {
|
itemRenderer(item) {
|
||||||
const theme = themeStyle(this.props.theme);
|
const theme = themeStyle(this.props.theme);
|
||||||
const width = this.props.style.width;
|
const width = this.props.style.width;
|
||||||
|
|
||||||
const onTitleClick = async (event, item) => {
|
const onTitleClick = async (event, item) => {
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey || event.metaKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.props.dispatch({
|
this.props.dispatch({
|
||||||
type: 'NOTE_SELECT_TOGGLE',
|
type: 'NOTE_SELECT_TOGGLE',
|
||||||
@ -400,7 +303,7 @@ class NoteListComponent extends React.Component {
|
|||||||
|
|
||||||
if (noteIds.length && keyCode === 46) { // DELETE
|
if (noteIds.length && keyCode === 46) { // DELETE
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await this.confirmDeleteNotes(noteIds);
|
await NoteListUtils.confirmDeleteNotes(noteIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (noteIds.length && keyCode === 32) { // SPACE
|
if (noteIds.length && keyCode === 32) { // SPACE
|
||||||
|
@ -28,6 +28,7 @@ const ArrayUtils = require('lib/ArrayUtils');
|
|||||||
const ObjectUtils = require('lib/ObjectUtils');
|
const ObjectUtils = require('lib/ObjectUtils');
|
||||||
const urlUtils = require('lib/urlUtils');
|
const urlUtils = require('lib/urlUtils');
|
||||||
const dialogs = require('./dialogs');
|
const dialogs = require('./dialogs');
|
||||||
|
const NoteListUtils = require('./utils/NoteListUtils');
|
||||||
const NoteSearchBar = require('./NoteSearchBar.min.js');
|
const NoteSearchBar = require('./NoteSearchBar.min.js');
|
||||||
const markdownUtils = require('lib/markdownUtils');
|
const markdownUtils = require('lib/markdownUtils');
|
||||||
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||||
@ -1491,6 +1492,61 @@ class NoteTextComponent extends React.Component {
|
|||||||
return toolbarItems;
|
return toolbarItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderNoNotes(rootStyle) {
|
||||||
|
const emptyDivStyle = Object.assign({
|
||||||
|
backgroundColor: 'black',
|
||||||
|
opacity: 0.1,
|
||||||
|
}, rootStyle);
|
||||||
|
return <div style={emptyDivStyle}></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMultiNotes(rootStyle) {
|
||||||
|
const theme = themeStyle(this.props.theme);
|
||||||
|
|
||||||
|
const multiNotesButton_click = item => {
|
||||||
|
if (item.submenu) {
|
||||||
|
item.submenu.popup(bridge().window());
|
||||||
|
} else {
|
||||||
|
item.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = NoteListUtils.makeContextMenu(this.props.selectedNoteIds, {
|
||||||
|
notes: this.props.notes,
|
||||||
|
dispatch: this.props.dispatch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonStyle = Object.assign({}, theme.buttonStyle, {
|
||||||
|
marginBottom: 10,
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemComps = [];
|
||||||
|
const menuItems = menu.items;
|
||||||
|
|
||||||
|
for (let i = 0; i < menuItems.length; i++) {
|
||||||
|
const item = menuItems[i];
|
||||||
|
if (!item.enabled) continue;
|
||||||
|
|
||||||
|
itemComps.push(<button
|
||||||
|
key={item.label}
|
||||||
|
style={buttonStyle}
|
||||||
|
onClick={() => multiNotesButton_click(item)}
|
||||||
|
>{item.label}</button>);
|
||||||
|
}
|
||||||
|
|
||||||
|
rootStyle = Object.assign({}, rootStyle, {
|
||||||
|
paddingTop: rootStyle.paddingLeft,
|
||||||
|
display: 'inline-flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<div style={rootStyle}>
|
||||||
|
<div style={{display: 'flex', flexDirection: 'column'}}>
|
||||||
|
{itemComps}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const style = this.props.style;
|
const style = this.props.style;
|
||||||
const note = this.state.note;
|
const note = this.state.note;
|
||||||
@ -1510,12 +1566,10 @@ class NoteTextComponent extends React.Component {
|
|||||||
|
|
||||||
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
|
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
|
||||||
|
|
||||||
if (!note || !!note.encryption_applied) {
|
if (this.props.selectedNoteIds.length > 1) {
|
||||||
const emptyDivStyle = Object.assign({
|
return this.renderMultiNotes(rootStyle);
|
||||||
backgroundColor: 'black',
|
} else if (!note || !!note.encryption_applied) {
|
||||||
opacity: 0.1,
|
return this.renderNoNotes(rootStyle);
|
||||||
}, rootStyle);
|
|
||||||
return <div style={emptyDivStyle}></div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const titleBarStyle = {
|
const titleBarStyle = {
|
||||||
@ -1766,6 +1820,7 @@ class NoteTextComponent extends React.Component {
|
|||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
return {
|
return {
|
||||||
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||||
|
notes: state.notes,
|
||||||
selectedNoteIds: state.selectedNoteIds,
|
selectedNoteIds: state.selectedNoteIds,
|
||||||
noteTags: state.selectedNoteTags,
|
noteTags: state.selectedNoteTags,
|
||||||
folderId: state.selectedFolderId,
|
folderId: state.selectedFolderId,
|
||||||
|
126
ElectronClient/app/gui/utils/NoteListUtils.js
Normal file
126
ElectronClient/app/gui/utils/NoteListUtils.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
const { time } = require('lib/time-utils.js');
|
||||||
|
const BaseModel = require('lib/BaseModel');
|
||||||
|
const markJsUtils = require('lib/markJsUtils');
|
||||||
|
const { _ } = require('lib/locale.js');
|
||||||
|
const { bridge } = require('electron').remote.require('./bridge');
|
||||||
|
const Menu = bridge().Menu;
|
||||||
|
const MenuItem = bridge().MenuItem;
|
||||||
|
const eventManager = require('../../eventManager');
|
||||||
|
const InteropService = require('lib/services/InteropService');
|
||||||
|
const InteropServiceHelper = require('../../InteropServiceHelper.js');
|
||||||
|
const Search = require('lib/models/Search');
|
||||||
|
const SearchEngine = require('lib/services/SearchEngine');
|
||||||
|
const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils');
|
||||||
|
|
||||||
|
class NoteListUtils {
|
||||||
|
|
||||||
|
static makeContextMenu(noteIds, props) {
|
||||||
|
const notes = noteIds.map((id) => BaseModel.byId(props.notes, id));
|
||||||
|
|
||||||
|
let hasEncrypted = false;
|
||||||
|
for (let i = 0; i < notes.length; i++) {
|
||||||
|
if (!!notes[i].encryption_applied) hasEncrypted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menu = new Menu()
|
||||||
|
|
||||||
|
if (!hasEncrypted) {
|
||||||
|
menu.append(new MenuItem({label: _('Add or remove tags'), enabled: noteIds.length === 1, click: async () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'setTags',
|
||||||
|
noteId: noteIds[0],
|
||||||
|
});
|
||||||
|
}}));
|
||||||
|
|
||||||
|
menu.append(new MenuItem({label: _('Duplicate'), click: async () => {
|
||||||
|
for (let i = 0; i < noteIds.length; i++) {
|
||||||
|
const note = await Note.load(noteIds[i]);
|
||||||
|
await Note.duplicate(noteIds[i], {
|
||||||
|
uniqueTitle: _('%s - Copy', note.title),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}));
|
||||||
|
|
||||||
|
if (noteIds.length <= 1) {
|
||||||
|
menu.append(new MenuItem({label: _('Switch between note and to-do type'), click: async () => {
|
||||||
|
for (let i = 0; i < noteIds.length; i++) {
|
||||||
|
const note = await Note.load(noteIds[i]);
|
||||||
|
await Note.save(Note.toggleIsTodo(note), { userSideValidation: true });
|
||||||
|
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
||||||
|
}
|
||||||
|
}}));
|
||||||
|
} else {
|
||||||
|
const switchNoteType = async (noteIds, type) => {
|
||||||
|
for (let i = 0; i < noteIds.length; i++) {
|
||||||
|
const note = await Note.load(noteIds[i]);
|
||||||
|
const newNote = Note.changeNoteType(note, type);
|
||||||
|
if (newNote === note) continue;
|
||||||
|
await Note.save(newNote, { userSideValidation: true });
|
||||||
|
eventManager.emit('noteTypeToggle', { noteId: note.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.append(new MenuItem({label: _('Switch to note type'), click: async () => {
|
||||||
|
await switchNoteType(noteIds, 'note');
|
||||||
|
}}));
|
||||||
|
|
||||||
|
menu.append(new MenuItem({label: _('Switch to to-do type'), click: async () => {
|
||||||
|
await switchNoteType(noteIds, 'todo');
|
||||||
|
}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.append(new MenuItem({label: _('Copy Markdown link'), click: async () => {
|
||||||
|
const { clipboard } = require('electron');
|
||||||
|
const links = [];
|
||||||
|
for (let i = 0; i < noteIds.length; i++) {
|
||||||
|
const note = await Note.load(noteIds[i]);
|
||||||
|
links.push(Note.markdownTag(note));
|
||||||
|
}
|
||||||
|
clipboard.writeText(links.join(' '));
|
||||||
|
}}));
|
||||||
|
|
||||||
|
const exportMenu = new Menu();
|
||||||
|
|
||||||
|
const ioService = new InteropService();
|
||||||
|
const ioModules = ioService.modules();
|
||||||
|
for (let i = 0; i < ioModules.length; i++) {
|
||||||
|
const module = ioModules[i];
|
||||||
|
if (module.type !== 'exporter') continue;
|
||||||
|
|
||||||
|
exportMenu.append(new MenuItem({ label: module.fullLabel() , click: async () => {
|
||||||
|
await InteropServiceHelper.export(props.dispatch.bind(this), module, { sourceNoteIds: noteIds });
|
||||||
|
}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteIds.length === 1) {
|
||||||
|
exportMenu.append(new MenuItem({ label: 'PDF - ' + _('PDF File') , click: () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'WINDOW_COMMAND',
|
||||||
|
name: 'exportPdf',
|
||||||
|
});
|
||||||
|
}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportMenuItem = new MenuItem({label: _('Export'), submenu: exportMenu});
|
||||||
|
|
||||||
|
menu.append(exportMenuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.append(new MenuItem({label: _('Delete'), click: async () => {
|
||||||
|
await this.confirmDeleteNotes(noteIds);
|
||||||
|
}}));
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async confirmDeleteNotes(noteIds) {
|
||||||
|
if (!noteIds.length) return;
|
||||||
|
const ok = bridge().showConfirmMessageBox(noteIds.length > 1 ? _('Delete notes?') : _('Delete note?'));
|
||||||
|
if (!ok) return;
|
||||||
|
await Note.batchDelete(noteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = NoteListUtils;
|
@ -52,9 +52,11 @@ globalStyle.containerStyle = {
|
|||||||
globalStyle.buttonStyle = {
|
globalStyle.buttonStyle = {
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
border: '1px solid',
|
border: '1px solid',
|
||||||
height: 30,
|
|
||||||
minHeight: 30,
|
minHeight: 30,
|
||||||
minWidth: 80,
|
minWidth: 80,
|
||||||
|
maxWidth: 160,
|
||||||
|
paddingLeft: 12,
|
||||||
|
paddingRight: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
const lightStyle = {
|
const lightStyle = {
|
||||||
|
@ -3,15 +3,22 @@ const Folder = require('lib/models/Folder.js');
|
|||||||
const BaseModel = require('lib/BaseModel.js');
|
const BaseModel = require('lib/BaseModel.js');
|
||||||
const Note = require('lib/models/Note.js');
|
const Note = require('lib/models/Note.js');
|
||||||
const Setting = require('lib/models/Setting.js');
|
const Setting = require('lib/models/Setting.js');
|
||||||
|
const Mutex = require('async-mutex').Mutex;
|
||||||
|
|
||||||
const shared = {};
|
const shared = {};
|
||||||
|
|
||||||
|
// If saveNoteButton_press is called multiple times in short intervals, it might result in
|
||||||
|
// the same new note being created twice, so we need to a mutex to access this function.
|
||||||
|
const saveNoteMutex_ = new Mutex();
|
||||||
|
|
||||||
shared.noteExists = async function(noteId) {
|
shared.noteExists = async function(noteId) {
|
||||||
const existingNote = await Note.load(noteId);
|
const existingNote = await Note.load(noteId);
|
||||||
return !!existingNote;
|
return !!existingNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
shared.saveNoteButton_press = async function(comp, folderId = null) {
|
shared.saveNoteButton_press = async function(comp, folderId = null) {
|
||||||
|
const releaseMutex = await saveNoteMutex_.acquire();
|
||||||
|
|
||||||
let note = Object.assign({}, comp.state.note);
|
let note = Object.assign({}, comp.state.note);
|
||||||
|
|
||||||
// Note has been deleted while user was modifying it. In that case, we
|
// Note has been deleted while user was modifying it. In that case, we
|
||||||
@ -24,7 +31,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null) {
|
|||||||
const activeFolderId = Setting.value('activeFolderId');
|
const activeFolderId = Setting.value('activeFolderId');
|
||||||
let folder = await Folder.load(activeFolderId);
|
let folder = await Folder.load(activeFolderId);
|
||||||
if (!folder) folder = await Folder.defaultFolder();
|
if (!folder) folder = await Folder.defaultFolder();
|
||||||
if (!folder) return;
|
if (!folder) return releaseMutex();
|
||||||
note.parent_id = folder.id;
|
note.parent_id = folder.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +53,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null) {
|
|||||||
const stateNote = comp.state.note;
|
const stateNote = comp.state.note;
|
||||||
|
|
||||||
// Note was reloaded while being saved.
|
// Note was reloaded while being saved.
|
||||||
if (!isNew && (!stateNote || stateNote.id !== savedNote.id)) return;
|
if (!isNew && (!stateNote || stateNote.id !== savedNote.id)) return releaseMutex();
|
||||||
|
|
||||||
// Re-assign any property that might have changed during saving (updated_time, etc.)
|
// Re-assign any property that might have changed during saving (updated_time, etc.)
|
||||||
note = Object.assign(note, savedNote);
|
note = Object.assign(note, savedNote);
|
||||||
@ -105,6 +112,8 @@ shared.saveNoteButton_press = async function(comp, folderId = null) {
|
|||||||
id: savedNote.id,
|
id: savedNote.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
releaseMutex();
|
||||||
}
|
}
|
||||||
|
|
||||||
shared.saveOneProperty = async function(comp, name, value) {
|
shared.saveOneProperty = async function(comp, name, value) {
|
||||||
|
Loading…
Reference in New Issue
Block a user