1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Desktop: Fixes #996: Allow editing multiple notes in external editor

This commit is contained in:
Laurent Cozic 2018-11-21 19:50:50 +00:00
parent 897f53b13e
commit 19252af345
5 changed files with 65 additions and 53 deletions

View File

@ -24,7 +24,7 @@ const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('./InteropServiceHelper.js');
const ResourceService = require('lib/services/ResourceService');
const ClipperServer = require('lib/ClipperServer');
const ExternalEditWatcher = require('lib/services/ExternalEditWatcher');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
@ -802,6 +802,9 @@ class Application extends BaseApplication {
if (Setting.value('clipperServer.autoStart')) {
ClipperServer.instance().start();
}
ExternalEditWatcher.instance().setLogger(reg.logger());
ExternalEditWatcher.instance().dispatch = this.store().dispatch;
}
}

View File

@ -16,6 +16,12 @@ const Mark = require('mark.js/dist/mark.min.js');
class NoteListComponent extends React.Component {
constructor() {
super();
this.itemRenderer = this.itemRenderer.bind(this);
}
style() {
const theme = themeStyle(this.props.theme);
@ -169,7 +175,10 @@ class NoteListComponent extends React.Component {
menu.popup(bridge().window());
}
itemRenderer(item, theme, width) {
itemRenderer(item) {
const theme = themeStyle(this.props.theme);
const width = this.props.style.width;
const onTitleClick = async (event, item) => {
if (event.ctrlKey) {
event.preventDefault();
@ -269,6 +278,14 @@ class NoteListComponent extends React.Component {
titleComp = <span>{displayTitle}</span>
}
const watchedIconStyle = {
paddingRight: 4,
color: theme.color,
};
const watchedIcon = this.props.watchedNoteFiles.indexOf(item.id) < 0 ? null : (
<i style={watchedIconStyle} className={"fa fa-external-link"}></i>
);
// Need to include "todo_completed" in key so that checkbox is updated when
// item is changed via sync.
return <div key={item.id + '_' + item.todo_completed} style={style}>
@ -283,6 +300,7 @@ class NoteListComponent extends React.Component {
onDragStart={(event) => onDragStart(event) }
data-id={item.id}
>
{watchedIcon}
{titleComp}
</a>
</div>
@ -313,7 +331,7 @@ class NoteListComponent extends React.Component {
style={style}
className={"note-list"}
items={notes}
itemRenderer={ (item) => { return this.itemRenderer(item, theme, style.width) } }
itemRenderer={this.itemRenderer}
></ItemList>
);
}
@ -329,6 +347,7 @@ const mapStateToProps = (state) => {
notesParentType: state.notesParentType,
searches: state.searches,
selectedSearchId: state.selectedSearchId,
watchedNoteFiles: state.watchedNoteFiles,
};
};

View File

@ -305,6 +305,7 @@ class NoteTextComponent extends React.Component {
eventManager.on('todoToggle', this.onTodoToggle_);
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
ExternalEditWatcher.instance().on('noteChange', this.externalEditWatcher_noteChange);
}
componentWillUnmount() {
@ -318,8 +319,7 @@ class NoteTextComponent extends React.Component {
eventManager.removeListener('todoToggle', this.onTodoToggle_);
ResourceFetcher.instance().off('downloadComplete', this.resourceFetcher_downloadComplete);
this.destroyExternalEditWatcher();
ExternalEditWatcher.instance().off('noteChange', this.externalEditWatcher_noteChange);
}
async saveIfNeeded(saveIfNewNote = false) {
@ -332,7 +332,7 @@ class NoteTextComponent extends React.Component {
}
await shared.saveNoteButton_press(this);
this.externalEditWatcherUpdateNoteFile(this.state.note);
ExternalEditWatcher.instance().updateNoteFile(this.state.note);
}
async saveOneProperty(name, value) {
@ -371,7 +371,6 @@ class NoteTextComponent extends React.Component {
if (props.newNote) {
note = Object.assign({}, props.newNote);
this.lastLoadedNoteId_ = null;
this.externalEditWatcherStopWatchingAll();
} else {
noteId = props.noteId;
loadingNewNote = stateNoteId !== noteId;
@ -398,8 +397,6 @@ class NoteTextComponent extends React.Component {
// Scroll back to top when loading new note
if (loadingNewNote) {
this.externalEditWatcherStopWatchingAll();
this.editorMaxScrollTop_ = 0;
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
@ -962,42 +959,16 @@ class NoteTextComponent extends React.Component {
return splitStyle.join(marker);
}
externalEditWatcher() {
if (!this.externalEditWatcher_) {
this.externalEditWatcher_ = new ExternalEditWatcher((action) => { return this.props.dispatch(action) });
this.externalEditWatcher_.setLogger(reg.logger());
this.externalEditWatcher_.on('noteChange', this.externalEditWatcher_noteChange);
}
return this.externalEditWatcher_;
}
externalEditWatcherUpdateNoteFile(note) {
if (this.externalEditWatcher_) this.externalEditWatcher().updateNoteFile(note);
}
externalEditWatcherStopWatchingAll() {
if (this.externalEditWatcher_) this.externalEditWatcher().stopWatchingAll();
}
destroyExternalEditWatcher() {
if (!this.externalEditWatcher_) return;
this.externalEditWatcher_.off('noteChange', this.externalEditWatcher_noteChange);
this.externalEditWatcher_.stopWatchingAll();
this.externalEditWatcher_ = null;
}
async commandStartExternalEditing() {
try {
await this.externalEditWatcher().openAndWatch(this.state.note);
await ExternalEditWatcher.instance().openAndWatch(this.state.note);
} catch (error) {
bridge().showErrorMessageBox(_('Error opening note in editor: %s', error.message));
}
}
async commandStopExternalEditing() {
this.externalEditWatcherStopWatchingAll();
ExternalEditWatcher.instance().stopWatching(this.state.note.id);
}
async commandSetTags() {

View File

@ -9,14 +9,20 @@ const spawn = require('child_process').spawn;
class ExternalEditWatcher {
constructor(dispatch = null) {
constructor() {
this.logger_ = new Logger();
this.dispatch_ = dispatch ? dispatch : (action) => {};
this.dispatch = (action) => {};
this.watcher_ = null;
this.eventEmitter_ = new EventEmitter();
this.skipNextChangeEvent_ = {};
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new ExternalEditWatcher();
return this.instance_;
}
on(eventName, callback) {
return this.eventEmitter_.on(eventName, callback);
}
@ -33,10 +39,6 @@ class ExternalEditWatcher {
return this.logger_;
}
dispatch(action) {
this.dispatch_(action);
}
watch(fileToWatch) {
if (!this.watcher_) {
this.watcher_ = chokidar.watch(fileToWatch);
@ -56,9 +58,17 @@ class ExternalEditWatcher {
if (!this.skipNextChangeEvent_[id]) {
const note = await Note.load(id);
if (!note) {
this.logger().warn('Watched note has been deleted: ' + id);
this.stopWatching(id);
return;
}
const noteContent = await shim.fsDriver().readFile(path, 'utf-8');
const updatedNote = await Note.unserializeForEdit(noteContent);
updatedNote.id = id;
updatedNote.parent_id = note.parent_id;
await Note.save(updatedNote);
this.eventEmitter_.emit('noteChange', { id: updatedNote.id });
}
@ -82,8 +92,8 @@ class ExternalEditWatcher {
return this.instance_;
}
noteFilePath(note) {
return Setting.value('tempDir') + '/' + note.id + '.md';
noteFilePath(noteId) {
return Setting.value('tempDir') + '/' + noteId + '.md';
}
watchedFiles() {
@ -181,15 +191,15 @@ class ExternalEditWatcher {
this.logger().info('ExternalEditWatcher: Started watching ' + filePath);
}
async stopWatching(note) {
if (!note || !note.id) return;
async stopWatching(noteId) {
if (!noteId) return;
const filePath = this.noteFilePath(note);
const filePath = this.noteFilePath(noteId);
if (this.watcher_) this.watcher_.unwatch(filePath);
await shim.fsDriver().remove(filePath);
this.dispatch({
type: 'NOTE_FILE_WATCHER_REMOVE',
id: note.id,
id: noteId,
});
this.logger().info('ExternalEditWatcher: Stopped watching ' + filePath);
}
@ -231,7 +241,7 @@ class ExternalEditWatcher {
return;
}
const filePath = this.noteFilePath(note);
const filePath = this.noteFilePath(note.id);
const noteContent = await Note.serializeForEdit(note);
await shim.fsDriver().writeFile(filePath, noteContent, 'utf-8');
return filePath;

View File

@ -36,7 +36,12 @@ class ResourceService extends BaseService {
for (let i = 0; i < notes.length; i++) {
if (notes[i].id === noteId) return notes[i];
}
throw new Error('Invalid note ID: ' + noteId);
// The note may have been deleted since the change was recorded. For example in this case:
// - Note created (Some Change object is recorded)
// - Note is deleted
// - ResourceService indexer runs.
// In that case, there will be a change for the note, but the note will be gone.
return null;
}
for (let i = 0; i < changes.length; i++) {
@ -44,8 +49,12 @@ class ResourceService extends BaseService {
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
const note = noteById(change.item_id);
const resourceIds = await Note.linkedResourceIds(note.body);
await NoteResource.setAssociatedResources(note.id, resourceIds);
if (note) {
const resourceIds = await Note.linkedResourceIds(note.body);
await NoteResource.setAssociatedResources(note.id, resourceIds);
} else {
this.logger().warn('ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ' + change.item_id);
}
} else if (change.type === ItemChange.TYPE_DELETE) {
await NoteResource.remove(change.item_id);
} else {