1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Plugins: Improved note change event handling. Also added tests and improved debugging plugins.

This commit is contained in:
Laurent Cozic 2020-12-01 14:08:41 +00:00
parent eed3dc8617
commit 05e9000087
25 changed files with 3934 additions and 64 deletions

View File

@ -86,6 +86,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.d.ts
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
packages/app-cli/tests/services/plugins/sandboxProxy.js packages/app-cli/tests/services/plugins/sandboxProxy.js
packages/app-cli/tests/services/plugins/sandboxProxy.js.map packages/app-cli/tests/services/plugins/sandboxProxy.js.map
@ -1124,6 +1127,9 @@ packages/lib/services/plugins/utils/executeSandboxCall.js.map
packages/lib/services/plugins/utils/loadContentScripts.d.ts packages/lib/services/plugins/utils/loadContentScripts.d.ts
packages/lib/services/plugins/utils/loadContentScripts.js packages/lib/services/plugins/utils/loadContentScripts.js
packages/lib/services/plugins/utils/loadContentScripts.js.map packages/lib/services/plugins/utils/loadContentScripts.js.map
packages/lib/services/plugins/utils/makeListener.d.ts
packages/lib/services/plugins/utils/makeListener.js
packages/lib/services/plugins/utils/makeListener.js.map
packages/lib/services/plugins/utils/manifestFromObject.d.ts packages/lib/services/plugins/utils/manifestFromObject.d.ts
packages/lib/services/plugins/utils/manifestFromObject.js packages/lib/services/plugins/utils/manifestFromObject.js
packages/lib/services/plugins/utils/manifestFromObject.js.map packages/lib/services/plugins/utils/manifestFromObject.js.map

6
.gitignore vendored
View File

@ -77,6 +77,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.d.ts
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js
packages/app-cli/tests/services/plugins/api/JoplinWorkspace.js.map
packages/app-cli/tests/services/plugins/sandboxProxy.d.ts packages/app-cli/tests/services/plugins/sandboxProxy.d.ts
packages/app-cli/tests/services/plugins/sandboxProxy.js packages/app-cli/tests/services/plugins/sandboxProxy.js
packages/app-cli/tests/services/plugins/sandboxProxy.js.map packages/app-cli/tests/services/plugins/sandboxProxy.js.map
@ -1115,6 +1118,9 @@ packages/lib/services/plugins/utils/executeSandboxCall.js.map
packages/lib/services/plugins/utils/loadContentScripts.d.ts packages/lib/services/plugins/utils/loadContentScripts.d.ts
packages/lib/services/plugins/utils/loadContentScripts.js packages/lib/services/plugins/utils/loadContentScripts.js
packages/lib/services/plugins/utils/loadContentScripts.js.map packages/lib/services/plugins/utils/loadContentScripts.js.map
packages/lib/services/plugins/utils/makeListener.d.ts
packages/lib/services/plugins/utils/makeListener.js
packages/lib/services/plugins/utils/makeListener.js.map
packages/lib/services/plugins/utils/manifestFromObject.d.ts packages/lib/services/plugins/utils/manifestFromObject.d.ts
packages/lib/services/plugins/utils/manifestFromObject.js packages/lib/services/plugins/utils/manifestFromObject.js
packages/lib/services/plugins/utils/manifestFromObject.js.map packages/lib/services/plugins/utils/manifestFromObject.js.map

View File

@ -37,7 +37,8 @@
"tsc": "lerna run tsc --stream --parallel", "tsc": "lerna run tsc --stream --parallel",
"updateIgnored": "gulp updateIgnoredTypeScriptBuild", "updateIgnored": "gulp updateIgnoredTypeScriptBuild",
"updatePluginTypes": "./packages/generator-joplin/updateTypes.sh", "updatePluginTypes": "./packages/generator-joplin/updateTypes.sh",
"watch": "lerna run watch --stream --parallel" "watch": "lerna run watch --stream --parallel",
"i": "lerna add --no-bootstrap --scope"
}, },
"husky": { "husky": {
"hooks": { "hooks": {

View File

@ -5,6 +5,7 @@ import BasePluginRunner from '@joplin/lib/services/plugins/BasePluginRunner';
import executeSandboxCall from '@joplin/lib/services/plugins/utils/executeSandboxCall'; import executeSandboxCall from '@joplin/lib/services/plugins/utils/executeSandboxCall';
import Global from '@joplin/lib/services/plugins/api/Global'; import Global from '@joplin/lib/services/plugins/api/Global';
import mapEventHandlersToIds, { EventHandlers } from '@joplin/lib/services/plugins/utils/mapEventHandlersToIds'; import mapEventHandlersToIds, { EventHandlers } from '@joplin/lib/services/plugins/utils/mapEventHandlersToIds';
import uuid from '@joplin/lib/uuid';
function createConsoleWrapper(pluginId: string) { function createConsoleWrapper(pluginId: string) {
const wrapper: any = {}; const wrapper: any = {};
@ -31,6 +32,7 @@ function createConsoleWrapper(pluginId: string) {
export default class PluginRunner extends BasePluginRunner { export default class PluginRunner extends BasePluginRunner {
private eventHandlers_: EventHandlers = {}; private eventHandlers_: EventHandlers = {};
private activeSandboxCalls_: any = {};
constructor() { constructor() {
super(); super();
@ -45,7 +47,13 @@ export default class PluginRunner extends BasePluginRunner {
private newSandboxProxy(pluginId: string, sandbox: Global) { private newSandboxProxy(pluginId: string, sandbox: Global) {
const target = async (path: string, args: any[]) => { const target = async (path: string, args: any[]) => {
return executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler); const callId = `${pluginId}::${path}::${uuid.createNano()}`;
this.activeSandboxCalls_[callId] = true;
const promise = executeSandboxCall(pluginId, sandbox, `joplin.${path}`, mapEventHandlersToIds(args, this.eventHandlers_), this.eventHandler);
promise.finally(() => {
delete this.activeSandboxCalls_[callId];
});
return promise;
}; };
return { return {
@ -69,10 +77,25 @@ export default class PluginRunner extends BasePluginRunner {
vm.runInContext(plugin.scriptText, vmSandbox); vm.runInContext(plugin.scriptText, vmSandbox);
} catch (error) { } catch (error) {
reject(error); reject(error);
// this.logger().error(`In plugin ${plugin.id}:`, error);
// return;
} }
}); });
} }
public async waitForSandboxCalls(): Promise<void> {
const startTime = Date.now();
return new Promise((resolve: Function, reject: Function) => {
const iid = setInterval(() => {
if (!Object.keys(this.activeSandboxCalls_).length) {
clearInterval(iid);
resolve();
}
if (Date.now() - startTime > 4000) {
clearInterval(iid);
reject(new Error(`Timeout while waiting for sandbox calls to complete: ${JSON.stringify(this.activeSandboxCalls_)}`));
}
}, 10);
});
}
} }

View File

@ -0,0 +1,52 @@
import PluginService from '@joplin/lib/services/plugins/PluginService';
const { newPluginService, newPluginScript, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } = require('../../../test-utils');
const Note = require('@joplin/lib/models/Note');
const Folder = require('@joplin/lib/models/Folder');
const ItemChange = require('@joplin/lib/models/ItemChange');
describe('JoplinWorkspace', () => {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
afterEach(async () => {
await afterEachCleanUp();
});
test('should listen to noteChange events', async () => {
const service = new newPluginService() as PluginService;
const pluginScript = newPluginScript(`
joplin.plugins.register({
onStart: async function() {
await joplin.workspace.onNoteChange(async (event) => {
await joplin.data.post(['folders'], null, { title: JSON.stringify(event) });
});
},
});
`);
const note = await Note.save({});
await ItemChange.waitForAllSaved();
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
await service.runPlugin(plugin);
await Note.save({ id: note.id, body: 'testing' });
await ItemChange.waitForAllSaved();
const folder = (await Folder.all())[0];
const result: any = JSON.parse(folder.title);
expect(result.id).toBe(note.id);
expect(result.event).toBe(ItemChange.TYPE_UPDATE);
await service.destroy();
});
});

View File

@ -22,9 +22,7 @@ function newPluginService(appVersion: string = '1.4') {
service.initialize( service.initialize(
appVersion, appVersion,
{ {
joplin: { joplin: {},
workspace: {},
},
}, },
runner, runner,
{ {

View File

@ -50,6 +50,8 @@ const KeychainServiceDriver = require('@joplin/lib/services/keychain/KeychainSer
const KeychainServiceDriverDummy = require('@joplin/lib/services/keychain/KeychainServiceDriver.dummy').default; const KeychainServiceDriverDummy = require('@joplin/lib/services/keychain/KeychainServiceDriver.dummy').default;
const md5 = require('md5'); const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3'); const S3 = require('aws-sdk/clients/s3');
const PluginRunner = require('../app/services/plugins/PluginRunner').default;
const PluginService = require('@joplin/lib/services/plugins/PluginService').default;
const { Dirnames } = require('@joplin/lib/services/synchronizer/utils/types'); const { Dirnames } = require('@joplin/lib/services/synchronizer/utils/types');
const sharp = require('sharp'); const sharp = require('sharp');
@ -677,6 +679,39 @@ async function createTempDir() {
return tempDirPath; return tempDirPath;
} }
function newPluginService(appVersion = '1.4') {
const runner = new PluginRunner();
const service = new PluginService();
service.initialize(
appVersion,
{
joplin: {},
},
runner,
{
dispatch: () => {},
getState: () => {},
}
);
return service;
}
function newPluginScript(script) {
return `
/* joplin-manifest:
{
"id": "org.joplinapp.plugins.PluginTest",
"manifest_version": 1,
"app_min_version": "1.4",
"name": "JS Bundle test",
"version": "1.0.0"
}
*/
${script}
`;
}
// TODO: Update for Jest // TODO: Update for Jest
// function mockDate(year, month, day, tick) { // function mockDate(year, month, day, tick) {
@ -757,4 +792,4 @@ class TestApp extends BaseApplication {
} }
} }
module.exports = { synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp }; module.exports = { newPluginService, newPluginScript, synchronizerStart, afterEachCleanUp, syncTargetName, setSyncTargetName, syncDir, createTempDir, isNetworkSyncTarget, kvStore, expectThrow, logger, expectNotThrow, resourceService, resourceFetcher, tempFilePath, allSyncTargetItemsEncrypted, msleep, setupDatabase, revisionService, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync, checkThrow, encryptionService, loadEncryptionMasterKey, fileContentEqual, decryptionWorker, asyncTest, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

@ -1,11 +1,5 @@
// import { EditorCommand } from '@joplin/lib/services/plugins/api/types';
import bridge from '../bridge'; import bridge from '../bridge';
// interface JoplinWorkspace {
// execEditorCommand(command:EditorCommand):Promise<string>
// }
interface JoplinViewsDialogs { interface JoplinViewsDialogs {
showMessageBox(message: string): Promise<number>; showMessageBox(message: string): Promise<number>;
} }
@ -15,7 +9,6 @@ interface JoplinViews {
} }
interface Joplin { interface Joplin {
// workspace: JoplinWorkspace;
views: JoplinViews; views: JoplinViews;
} }

View File

@ -11,6 +11,17 @@ const ipcRenderer = require('electron').ipcRenderer;
const logger = Logger.create('PluginRunner'); const logger = Logger.create('PluginRunner');
// Electron error messages are useless so wrap the renderer call and print
// additional information when an error occurs.
function ipcRendererSend(message: string, args: any) {
try {
return ipcRenderer.send(message, args);
} catch (error) {
logger.error('Could not send IPC message:', message, ': ', args, error);
throw error;
}
}
enum PluginMessageTarget { enum PluginMessageTarget {
MainWindow = 'mainWindow', MainWindow = 'mainWindow',
Plugin = 'plugin', Plugin = 'plugin',
@ -46,7 +57,7 @@ function mapEventIdsToHandlers(pluginId: string, arg: any) {
callbackPromises[callbackId] = { resolve, reject }; callbackPromises[callbackId] = { resolve, reject };
}); });
ipcRenderer.send('pluginMessage', { ipcRendererSend('pluginMessage', {
callbackId: callbackId, callbackId: callbackId,
target: PluginMessageTarget.Plugin, target: PluginMessageTarget.Plugin,
pluginId: pluginId, pluginId: pluginId,
@ -132,7 +143,7 @@ export default class PluginRunner extends BasePluginRunner {
// Don't log complete HTML code, which can be long, for setHtml calls // Don't log complete HTML code, which can be long, for setHtml calls
const debugMappedArgs = fullPath.includes('setHtml') ? '<hidden>' : mappedArgs; const debugMappedArgs = fullPath.includes('setHtml') ? '<hidden>' : mappedArgs;
logger.debug(`Got message (3): ${fullPath}: ${debugMappedArgs}`); logger.debug(`Got message (3): ${fullPath}`, debugMappedArgs);
let result: any = null; let result: any = null;
let error: any = null; let error: any = null;
@ -142,7 +153,7 @@ export default class PluginRunner extends BasePluginRunner {
error = e ? e : new Error('Unknown error'); error = e ? e : new Error('Unknown error');
} }
ipcRenderer.send('pluginMessage', { ipcRendererSend('pluginMessage', {
target: PluginMessageTarget.Plugin, target: PluginMessageTarget.Plugin,
pluginId: plugin.id, pluginId: plugin.id,
pluginCallbackId: message.callbackId, pluginCallbackId: message.callbackId,

View File

@ -1,13 +1,5 @@
<html> <html>
<head> <head>
<script src="./plugin_index.js"></script> <script src="./plugin_index.js"></script>
<script>
// joplin.plugins.register({
// onStart: async function() {
// alert('PLUGIN STARTED');
// },
// });
</script>
</head> </head>
</html> </html>

View File

@ -3,6 +3,15 @@
const sandboxProxy = require('../../node_modules/@joplin/lib/services/plugins/sandboxProxy.js').default; const sandboxProxy = require('../../node_modules/@joplin/lib/services/plugins/sandboxProxy.js').default;
const ipcRenderer = require('electron').ipcRenderer; const ipcRenderer = require('electron').ipcRenderer;
const ipcRendererSend = (message, args) => {
try {
return ipcRenderer.send(message, args);
} catch (error) {
console.error('Could not send IPC message:', message, ': ', args, error);
throw error;
}
};
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const pluginId = urlParams.get('pluginId'); const pluginId = urlParams.get('pluginId');
@ -42,7 +51,7 @@
callbackPromises[callbackId] = { resolve, reject }; callbackPromises[callbackId] = { resolve, reject };
}); });
ipcRenderer.send('pluginMessage', { ipcRendererSend('pluginMessage', {
target: 'mainWindow', target: 'mainWindow',
pluginId: pluginId, pluginId: pluginId,
callbackId: callbackId, callbackId: callbackId,
@ -71,7 +80,7 @@
} }
if (message.callbackId) { if (message.callbackId) {
ipcRenderer.send('pluginMessage', { ipcRendererSend('pluginMessage', {
target: 'mainWindow', target: 'mainWindow',
pluginId: pluginId, pluginId: pluginId,
mainWindowCallbackId: message.callbackId, mainWindowCallbackId: message.callbackId,

View File

@ -1,9 +0,0 @@
{
"generator-node": {
"promptValues": {
"authorName": "Laurent Cozic",
"authorEmail": "laurent@cozic.net",
"authorUrl": ""
}
}
}

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2020 Laurent Cozic <laurent@cozic.net> Copyright (c) 2020 Laurent Cozic
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,6 +1,6 @@
const events = require('events'); const events = require('events');
class EventManager { export class EventManager {
private emitter_: any; private emitter_: any;
private appStatePrevious_: any; private appStatePrevious_: any;

View File

@ -0,0 +1,13 @@
module.exports = {
testMatch: [
'**/*.test.js',
],
testPathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/rnInjectedJs/',
'<rootDir>/vendor/',
],
testEnvironment: 'node',
};

View File

@ -1,6 +1,7 @@
const BaseModel = require('../BaseModel').default; const BaseModel = require('../BaseModel').default;
const Mutex = require('async-mutex').Mutex; const Mutex = require('async-mutex').Mutex;
const shim = require('../shim').default; const shim = require('../shim').default;
const eventManager = require('../eventManager').default;
class ItemChange extends BaseModel { class ItemChange extends BaseModel {
static tableName() { static tableName() {
@ -22,10 +23,25 @@ class ItemChange extends BaseModel {
const release = await ItemChange.addChangeMutex_.acquire(); const release = await ItemChange.addChangeMutex_.acquire();
try { try {
await this.db().transactionExecBatch([{ sql: 'DELETE FROM item_changes WHERE item_id = ?', params: [itemId] }, { sql: 'INSERT INTO item_changes (item_type, item_id, type, source, created_time, before_change_item) VALUES (?, ?, ?, ?, ?, ?)', params: [itemType, itemId, type, changeSource, Date.now(), beforeChangeItemJson] }]); await this.db().transactionExecBatch([
{
sql: 'DELETE FROM item_changes WHERE item_id = ?',
params: [itemId],
},
{
sql: 'INSERT INTO item_changes (item_type, item_id, type, source, created_time, before_change_item) VALUES (?, ?, ?, ?, ?, ?)',
params: [itemType, itemId, type, changeSource, Date.now(), beforeChangeItemJson],
},
]);
} finally { } finally {
release(); release();
ItemChange.saveCalls_.pop(); ItemChange.saveCalls_.pop();
eventManager.emit('itemChange', {
itemType: itemType,
itemId: itemId,
eventType: type,
});
} }
} }
@ -62,6 +78,6 @@ ItemChange.TYPE_DELETE = 3;
ItemChange.SOURCE_UNSPECIFIED = 1; ItemChange.SOURCE_UNSPECIFIED = 1;
ItemChange.SOURCE_SYNC = 2; ItemChange.SOURCE_SYNC = 2;
ItemChange.SOURCE_DECRYPTION = 2; ItemChange.SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC!
module.exports = ItemChange; module.exports = ItemChange;

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
{ {
"name": "@joplin/lib", "name": "@joplin/lib",
"version": "1.0.9", "version": "1.0.9",
"description": "> TODO: description", "description": "Joplin Core library",
"author": "Laurent Cozic <laurent@cozic.net>", "author": "Laurent Cozic",
"homepage": "", "homepage": "",
"license": "ISC", "license": "ISC",
"publishConfig": { "publishConfig": {
@ -11,9 +11,13 @@
"scripts": { "scripts": {
"tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json", "tsc": "node node_modules/typescript/bin/tsc --project tsconfig.json",
"watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json", "watch": "node node_modules/typescript/bin/tsc --watch --project tsconfig.json",
"generatePluginTypes": "rm -rf ./plugin_types && node node_modules/typescript/bin/tsc --declaration --declarationDir ./plugin_types --project tsconfig.json" "generatePluginTypes": "rm -rf ./plugin_types && node node_modules/typescript/bin/tsc --declaration --declarationDir ./plugin_types --project tsconfig.json",
"test": "jest",
"test-ci": "npm run test"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^26.0.15",
"jest": "^26.6.3",
"@types/node": "^14.14.6", "@types/node": "^14.14.6",
"typescript": "^4.0.5" "typescript": "^4.0.5"
}, },

View File

@ -8,4 +8,8 @@ export default abstract class BasePluginRunner extends BaseService {
throw new Error(`Not implemented: ${plugin} / ${sandbox}`); throw new Error(`Not implemented: ${plugin} / ${sandbox}`);
} }
public async waitForSandboxCalls(): Promise<void> {
throw new Error('Not implemented: waitForSandboxCalls');
}
} }

View File

@ -385,4 +385,8 @@ export default class PluginService extends BaseService {
return newSettings; return newSettings;
} }
public async destroy() {
await this.runner_.waitForSandboxCalls();
}
} }

View File

@ -37,7 +37,7 @@ export default class Joplin {
constructor(implementation: any, plugin: Plugin, store: any) { constructor(implementation: any, plugin: Plugin, store: any) {
this.data_ = new JoplinData(); this.data_ = new JoplinData();
this.plugins_ = new JoplinPlugins(plugin); this.plugins_ = new JoplinPlugins(plugin);
this.workspace_ = new JoplinWorkspace(implementation.workspace, store); this.workspace_ = new JoplinWorkspace(store);
this.filters_ = new JoplinFilters(); this.filters_ = new JoplinFilters();
this.commands_ = new JoplinCommands(); this.commands_ = new JoplinCommands();
this.views_ = new JoplinViews(implementation.views, plugin, store); this.views_ = new JoplinViews(implementation.views, plugin, store);

View File

@ -1,13 +1,31 @@
import { ModelType } from '../../../BaseModel';
import eventManager from '../../../eventManager'; import eventManager from '../../../eventManager';
import makeListener from '../utils/makeListener';
import { Disposable } from './types';
/** /**
* @ignore * @ignore
*/ */
const Note = require('../../../models/Note'); const Note = require('../../../models/Note');
enum ItemChangeEventType {
Create = 1,
Update = 2,
Delete = 3,
}
interface ItemChangeEvent {
id: string;
event: ItemChangeEventType;
}
type ItemChangeHandler = (event: ItemChangeEvent)=> void;
/** /**
* The workspace service provides access to all the parts of Joplin that are being worked on - i.e. the currently selected notes or notebooks as well * The workspace service provides access to all the parts of Joplin that
* as various related events, such as when a new note is selected, or when the note content changes. * are being worked on - i.e. the currently selected notes or notebooks as
* well as various related events, such as when a new note is selected, or
* when the note content changes.
* *
* [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins) * [View the demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins)
*/ */
@ -15,45 +33,68 @@ export default class JoplinWorkspace {
// TODO: unregister events when plugin is closed or disabled // TODO: unregister events when plugin is closed or disabled
private store: any; private store: any;
// private implementation_:any;
constructor(_implementation: any, store: any) { constructor(store: any) {
this.store = store; this.store = store;
// this.implementation_ = implementation;
} }
/** /**
* Called when a new note or notes are selected. * Called when a new note or notes are selected.
*/ */
async onNoteSelectionChange(callback: Function) { public async onNoteSelectionChange(callback: Function): Promise<Disposable> {
eventManager.appStateOn('selectedNoteIds', callback); eventManager.appStateOn('selectedNoteIds', callback);
return {};
// return {
// dispose: () => {
// eventManager.appStateOff('selectedNoteIds', callback);
// }
// };
}
/**
* Called when the content of a note changes.
* @deprecated Use `onNoteChange()` instead, which is reliably triggered whenever the note content, or any note property changes.
*/
public async onNoteContentChange(callback: Function) {
eventManager.on('noteContentChange', callback);
} }
/** /**
* Called when the content of a note changes. * Called when the content of a note changes.
*/ */
async onNoteContentChange(callback: Function) { public async onNoteChange(handler: ItemChangeHandler): Promise<Disposable> {
eventManager.on('noteContentChange', callback); const wrapperHandler = (event: any) => {
if (event.itemType !== ModelType.Note) return;
handler({
id: event.itemId,
event: event.eventType,
});
};
return makeListener(eventManager, 'itemChange', wrapperHandler);
} }
/** /**
* Called when an alarm associated with a to-do is triggered. * Called when an alarm associated with a to-do is triggered.
*/ */
async onNoteAlarmTrigger(callback: Function) { public async onNoteAlarmTrigger(callback: Function): Promise<Disposable> {
eventManager.on('noteAlarmTrigger', callback); return makeListener(eventManager, 'noteAlarmTrigger', callback);
} }
/** /**
* Called when the synchronisation process has finished. * Called when the synchronisation process has finished.
*/ */
async onSyncComplete(callback: Function) { public async onSyncComplete(callback: Function): Promise<Disposable> {
eventManager.on('syncComplete', callback); return makeListener(eventManager, 'syncComplete', callback);
} }
/** /**
* Gets the currently selected note * Gets the currently selected note
*/ */
async selectedNote(): Promise<any> { public async selectedNote(): Promise<any> {
const noteIds = this.store.getState().selectedNoteIds; const noteIds = this.store.getState().selectedNoteIds;
if (noteIds.length !== 1) { return null; } if (noteIds.length !== 1) { return null; }
return Note.load(noteIds[0]); return Note.load(noteIds[0]);
@ -62,7 +103,7 @@ export default class JoplinWorkspace {
/** /**
* Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes. * Gets the IDs of the selected notes (can be zero, one, or many). Use the data API to retrieve information about these notes.
*/ */
async selectedNoteIds(): Promise<string[]> { public async selectedNoteIds(): Promise<string[]> {
return this.store.getState().selectedNoteIds.slice(); return this.store.getState().selectedNoteIds.slice();
} }
} }

View File

@ -189,6 +189,10 @@ export interface Script {
onStart?(event: any): Promise<void>; onStart?(event: any): Promise<void>;
} }
export interface Disposable {
// dispose():void;
}
// ================================================================= // =================================================================
// Menu types // Menu types
// ================================================================= // =================================================================

View File

@ -0,0 +1,22 @@
import { EventManager } from '../../../eventManager';
import { Disposable } from '../api/types';
export default function(eventManager: EventManager, eventName: string, callback: Function): Disposable {
eventManager.on(eventName, callback);
return {};
// Note: It is not currently possible to return an object with a dispose() function because function cannot be serialized when sent via IPC. So it would need send callback mechanism as for plugin functions.
//
// Or it could return a simple string ID, which can then be used to stop listening to the event. eg:
//
// const listenerId = await joplin.workspace.onNoteChange(() => {});
// // ... later:
// await joplin.workspace.removeListener(listenerId);
// return {
// dispose: () => {
// eventManager.off(eventName, callback);
// }
// };
}