1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-08 13:06:15 +02:00

Plugins: Added joplin.settings.onChange event

This commit is contained in:
Laurent Cozic 2021-01-08 22:20:59 +00:00
parent d75adc3740
commit 023170548f
6 changed files with 167 additions and 8 deletions

View File

@ -110,6 +110,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/api/JoplinSettings.d.ts
packages/app-cli/tests/services/plugins/api/JoplinSettings.js
packages/app-cli/tests/services/plugins/api/JoplinSettings.js.map
packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.d.ts
packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.js
packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.js.map

3
.gitignore vendored
View File

@ -99,6 +99,9 @@ packages/app-cli/tests/models_Note.js.map
packages/app-cli/tests/models_Setting.d.ts
packages/app-cli/tests/models_Setting.js
packages/app-cli/tests/models_Setting.js.map
packages/app-cli/tests/services/plugins/api/JoplinSettings.d.ts
packages/app-cli/tests/services/plugins/api/JoplinSettings.js
packages/app-cli/tests/services/plugins/api/JoplinSettings.js.map
packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.d.ts
packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.js
packages/app-cli/tests/services/plugins/api/JoplinViewMenuItem.js.map

View File

@ -0,0 +1,69 @@
import Setting from '@joplin/lib/models/Setting';
import PluginService from '@joplin/lib/services/plugins/PluginService';
const { waitForFolderCount, newPluginService, newPluginScript, setupDatabaseAndSynchronizer, switchClient, afterEachCleanUp } = require('../../../test-utils');
const Folder = require('@joplin/lib/models/Folder');
describe('JoplinSettings', () => {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
afterEach(async () => {
await afterEachCleanUp();
});
test('should listen to setting change event', async () => {
const service = new newPluginService() as PluginService;
const pluginScript = newPluginScript(`
joplin.plugins.register({
onStart: async function() {
await joplin.settings.registerSetting('myCustomSetting1', {
value: 1,
type: 1,
public: true,
label: 'My Custom Setting 1',
});
await joplin.settings.registerSetting('myCustomSetting2', {
value: 2,
type: 1,
public: true,
label: 'My Custom Setting 2',
});
joplin.settings.onChange((event) => {
joplin.data.post(['folders'], null, { title: JSON.stringify(event.keys) });
});
},
});
`);
const plugin = await service.loadPluginFromJsBundle('', pluginScript);
await service.runPlugin(plugin);
Setting.setValue('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting1', 111);
Setting.setValue('plugin-org.joplinapp.plugins.PluginTest.myCustomSetting2', 222);
// Also change a global setting, to verify that the plugin doesn't get
// notifications for non-plugin related events.
Setting.setValue('locale', 'fr_FR');
Setting.emitScheduledChangeEvent();
await waitForFolderCount(1);
const folder = (await Folder.all())[0];
const settingNames: string[] = JSON.parse(folder.title);
settingNames.sort();
expect(settingNames.join(',')).toBe('myCustomSetting1,myCustomSetting2');
await service.destroy();
});
});

View File

@ -768,6 +768,17 @@ function newPluginScript(script: string) {
`;
}
async function waitForFolderCount(count: number) {
const timeout = 2000;
const startTime = Date.now();
while (true) {
const folders = await Folder.all();
if (folders.length >= count) return;
if (Date.now() - startTime > timeout) throw new Error('Timeout waiting for folders to be created');
await msleep(10);
}
}
// TODO: Update for Jest
// function mockDate(year, month, day, tick) {
@ -853,4 +864,4 @@ class TestApp extends BaseApplication {
}
}
module.exports = { afterAllCleanUp, exportDir, 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, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };
module.exports = { waitForFolderCount, afterAllCleanUp, exportDir, 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, currentClientId, id, ids, sortedIds, at, createNTestNotes, createNTestFolders, createNTestTags, TestApp };

View File

@ -1,6 +1,7 @@
import shim from '../shim';
import { _, supportedLocalesToLanguages, defaultLocale } from '../locale';
import { ltrimSlashes } from '../path-utils';
import eventManager from '../eventManager';
const BaseModel = require('../BaseModel').default;
const { Database } = require('../database.js');
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
@ -79,8 +80,10 @@ class Setting extends BaseModel {
private static keys_: string[] = null;
private static cache_: CacheItem[] = [];
private static saveTimeoutId_: any = null;
private static changeEventTimeoutId_: any = null;
private static customMetadata_: SettingItems = {};
private static customSections_: SettingSections = {};
private static changedKeys_: string[] = [];
static tableName() {
return 'settings';
@ -92,8 +95,10 @@ class Setting extends BaseModel {
static async reset() {
if (this.saveTimeoutId_) shim.clearTimeout(this.saveTimeoutId_);
if (this.changeEventTimeoutId_) shim.clearTimeout(this.changeEventTimeoutId_);
this.saveTimeoutId_ = null;
this.changeEventTimeoutId_ = null;
this.metadata_ = null;
this.keys_ = null;
this.cache_ = [];
@ -1070,6 +1075,8 @@ class Setting extends BaseModel {
static load() {
this.cancelScheduleSave();
this.cancelScheduleChangeEvent();
this.cache_ = [];
return this.modelSelectAll('SELECT * FROM settings').then(async (rows: any[]) => {
this.cache_ = [];
@ -1154,6 +1161,8 @@ class Setting extends BaseModel {
if (c.value === value) return;
this.changedKeys_.push(key);
// Don't log this to prevent sensitive info (passwords, auth tokens...) to end up in logs
// this.logger().info('Setting: ' + key + ' = ' + c.value + ' => ' + value);
@ -1169,6 +1178,7 @@ class Setting extends BaseModel {
});
this.scheduleSave();
this.scheduleChangeEvent();
return;
}
}
@ -1184,7 +1194,10 @@ class Setting extends BaseModel {
value: this.formatValue(key, value),
});
this.changedKeys_.push(key);
this.scheduleSave();
this.scheduleChangeEvent();
}
static incValue(key: string, inc: any) {
@ -1424,6 +1437,36 @@ class Setting extends BaseModel {
this.logger().info('Settings have been saved.');
}
static scheduleChangeEvent() {
if (this.changeEventTimeoutId_) shim.clearTimeout(this.changeEventTimeoutId_);
this.changeEventTimeoutId_ = shim.setTimeout(() => {
this.emitScheduledChangeEvent();
}, 1000);
}
static cancelScheduleChangeEvent() {
if (this.changeEventTimeoutId_) shim.clearTimeout(this.changeEventTimeoutId_);
this.changeEventTimeoutId_ = null;
}
public static emitScheduledChangeEvent() {
if (!this.changeEventTimeoutId_) return;
shim.clearTimeout(this.changeEventTimeoutId_);
this.changeEventTimeoutId_ = null;
if (!this.changedKeys_.length) {
// Sanity check - shouldn't happen
this.logger().warn('Trying to dispatch a change event without any changed keys');
return;
}
const keys = this.changedKeys_.slice();
this.changedKeys_ = [];
eventManager.emit('settingsChange', { keys });
}
static scheduleSave() {
if (!Setting.autoSaveEnabled) return;

View File

@ -1,7 +1,17 @@
import eventManager from '../../../eventManager';
import Setting, { SettingItem as InternalSettingItem } from '../../../models/Setting';
import Plugin from '../Plugin';
import { SettingItem, SettingSection } from './types';
export interface ChangeEvent {
/**
* Setting keys that have been changed
*/
keys: string[];
}
export type ChangeHandler = (event: ChangeEvent)=> void;
/**
* This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user.
*
@ -14,14 +24,18 @@ import { SettingItem, SettingSection } from './types';
export default class JoplinSettings {
private plugin_: Plugin = null;
constructor(plugin: Plugin) {
public constructor(plugin: Plugin) {
this.plugin_ = plugin;
}
private get keyPrefix(): string {
return `plugin-${this.plugin_.id}.`;
}
// Ensures that the plugin settings and sections are within their own namespace, to prevent them from
// overwriting other plugin settings or the default settings.
private namespacedKey(key: string): string {
return `plugin-${this.plugin_.id}.${key}`;
return `${this.keyPrefix}${key}`;
}
/**
@ -30,7 +44,7 @@ export default class JoplinSettings {
* The setting value however will be preserved from one launch to the next so there is no risk that it will be lost even if for some
* reason the plugin fails to start at some point.
*/
async registerSetting(key: string, settingItem: SettingItem) {
public async registerSetting(key: string, settingItem: SettingItem) {
const internalSettingItem: InternalSettingItem = {
key: key,
value: settingItem.value,
@ -56,21 +70,21 @@ export default class JoplinSettings {
/**
* Registers a new setting section. Like for registerSetting, it is dynamic and needs to be done every time the plugin starts.
*/
async registerSection(name: string, section: SettingSection) {
public async registerSection(name: string, section: SettingSection) {
return Setting.registerSection(this.namespacedKey(name), section);
}
/**
* Gets a setting value (only applies to setting you registered from your plugin)
*/
async value(key: string): Promise<any> {
public async value(key: string): Promise<any> {
return Setting.value(this.namespacedKey(key));
}
/**
* Sets a setting value (only applies to setting you registered from your plugin)
*/
async setValue(key: string, value: any) {
public async setValue(key: string, value: any) {
return Setting.setValue(this.namespacedKey(key), value);
}
@ -81,7 +95,23 @@ export default class JoplinSettings {
*
* https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142
*/
async globalValue(key: string): Promise<any> {
public async globalValue(key: string): Promise<any> {
return Setting.value(key);
}
/**
* Called when one or multiple settings of your plugin have been changed.
* - For performance reasons, this event is triggered with a delay.
* - You will only get events for your own plugin settings.
*/
public async onChange(handler: ChangeHandler): Promise<void> {
// Filter out keys that are not related to this plugin
eventManager.on('settingsChange', (event: ChangeEvent) => {
const keys = event.keys
.filter(k => k.indexOf(this.keyPrefix) === 0)
.map(k => k.substr(this.keyPrefix.length));
if (!keys.length) return;
handler({ keys });
});
}
}