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:
parent
d75adc3740
commit
023170548f
@ -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
3
.gitignore
vendored
@ -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
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
@ -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 };
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user