mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +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 Mark = require('mark.js/dist/mark.min.js');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
const NoteListUtils = require('./utils/NoteListUtils');
|
||||
const { replaceRegexDiacritics, pregQuote } = require('lib/string-utils');
|
||||
|
||||
class NoteListComponent extends React.Component {
|
||||
@ -83,118 +84,20 @@ class NoteListComponent extends React.Component {
|
||||
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const notes = noteIds.map((id) => BaseModel.byId(this.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 () => {
|
||||
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);
|
||||
}}));
|
||||
const menu = NoteListUtils.makeContextMenu(noteIds, {
|
||||
notes: this.props.notes,
|
||||
dispatch: this.props.dispatch,
|
||||
});
|
||||
|
||||
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) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const width = this.props.style.width;
|
||||
|
||||
const onTitleClick = async (event, item) => {
|
||||
if (event.ctrlKey) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
event.preventDefault();
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECT_TOGGLE',
|
||||
@ -400,7 +303,7 @@ class NoteListComponent extends React.Component {
|
||||
|
||||
if (noteIds.length && keyCode === 46) { // DELETE
|
||||
event.preventDefault();
|
||||
await this.confirmDeleteNotes(noteIds);
|
||||
await NoteListUtils.confirmDeleteNotes(noteIds);
|
||||
}
|
||||
|
||||
if (noteIds.length && keyCode === 32) { // SPACE
|
||||
|
@ -28,6 +28,7 @@ const ArrayUtils = require('lib/ArrayUtils');
|
||||
const ObjectUtils = require('lib/ObjectUtils');
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
const dialogs = require('./dialogs');
|
||||
const NoteListUtils = require('./utils/NoteListUtils');
|
||||
const NoteSearchBar = require('./NoteSearchBar.min.js');
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
|
||||
@ -1491,6 +1492,61 @@ class NoteTextComponent extends React.Component {
|
||||
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() {
|
||||
const style = this.props.style;
|
||||
const note = this.state.note;
|
||||
@ -1510,12 +1566,10 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
|
||||
|
||||
if (!note || !!note.encryption_applied) {
|
||||
const emptyDivStyle = Object.assign({
|
||||
backgroundColor: 'black',
|
||||
opacity: 0.1,
|
||||
}, rootStyle);
|
||||
return <div style={emptyDivStyle}></div>
|
||||
if (this.props.selectedNoteIds.length > 1) {
|
||||
return this.renderMultiNotes(rootStyle);
|
||||
} else if (!note || !!note.encryption_applied) {
|
||||
return this.renderNoNotes(rootStyle);
|
||||
}
|
||||
|
||||
const titleBarStyle = {
|
||||
@ -1766,6 +1820,7 @@ class NoteTextComponent extends React.Component {
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
noteId: state.selectedNoteIds.length === 1 ? state.selectedNoteIds[0] : null,
|
||||
notes: state.notes,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
noteTags: state.selectedNoteTags,
|
||||
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 = {
|
||||
marginRight: 10,
|
||||
border: '1px solid',
|
||||
height: 30,
|
||||
minHeight: 30,
|
||||
minWidth: 80,
|
||||
maxWidth: 160,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
};
|
||||
|
||||
const lightStyle = {
|
||||
|
@ -3,15 +3,22 @@ const Folder = require('lib/models/Folder.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
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) {
|
||||
const existingNote = await Note.load(noteId);
|
||||
return !!existingNote;
|
||||
}
|
||||
|
||||
shared.saveNoteButton_press = async function(comp, folderId = null) {
|
||||
const releaseMutex = await saveNoteMutex_.acquire();
|
||||
|
||||
let note = Object.assign({}, comp.state.note);
|
||||
|
||||
// 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');
|
||||
let folder = await Folder.load(activeFolderId);
|
||||
if (!folder) folder = await Folder.defaultFolder();
|
||||
if (!folder) return;
|
||||
if (!folder) return releaseMutex();
|
||||
note.parent_id = folder.id;
|
||||
}
|
||||
|
||||
@ -46,7 +53,7 @@ shared.saveNoteButton_press = async function(comp, folderId = null) {
|
||||
const stateNote = comp.state.note;
|
||||
|
||||
// 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.)
|
||||
note = Object.assign(note, savedNote);
|
||||
@ -105,6 +112,8 @@ shared.saveNoteButton_press = async function(comp, folderId = null) {
|
||||
id: savedNote.id,
|
||||
});
|
||||
}
|
||||
|
||||
releaseMutex();
|
||||
}
|
||||
|
||||
shared.saveOneProperty = async function(comp, name, value) {
|
||||
|
Loading…
Reference in New Issue
Block a user